anchorleg
v0.8.1
Published
Rotate coding-agent subscriptions across sessions: a foundation session that switches accounts on its own, and worker sessions it can delegate to over MCP.
Downloads
3,020
Maintainers
Readme
anchorleg
複数のサブスクリプションを持っているときに、利用上限に合わせてアカウントを自動で切り替えながらエージェントのセッションを動かし続けるツールです。今は Claude Code に対応しています。
- ファンデーション: プロジェクトごとに 1 つ、あなたの端末で動く対話のセッションです。上限が近づくと、ターンの区切りで別のアカウントに切り替えて同じ会話を resume します。ターンの途中で上限に達した場合は、別のアカウントで resume して「続きをお願いします」を送ります。裏でコマンドやサブエージェントが動いている間は切り替えず、そのことをセッションに一度だけ知らせて、きりのよいところで片付けてもらいます。
ScheduleWakeupの予定が失われる場合は張り直すよう伝えます。新しい版の anchorleg がインストールされると、待機中に自動で新しい版に置き換わります。 - 作業役: ファンデーションが MCP のツールで起動する、独立したセッションです。タスクごとに新しいセッションを作り、途中で上限に達したら別のアカウントで resume して続けます。終わった作業役には、同じセッションのまま続きを頼めます。
- 常駐する作業役(
resident: true): ターンが終わってもプロセスを残し、裏で動かしたコマンドの完了通知・ScheduleWakeup・他のセッションからのメッセージ・ファンデーションからの続きの指示で再開します。ファンデーションが中止するか、待機が長引いて時間切れになるまで動き続けます。
使い方
# インストール(どちらか)
npm i -g anchorleg # リポジトリを一切変更しない。以下の npx は付けなくてよい
npm i -D anchorleg # プロジェクトに入れる。package.json と lock が変わるので、すぐにコミットする
# 最初に 1 回だけ: アカウントごとにトークンを登録する(`claude setup-token` で発行したもの)
npx baton token add 1
npx baton token add 2
# ファンデーションを起動する(2 回目以降は同じセッションを resume する)
npx baton -m fable -e high作業役への指示は、コマンドライン引数ではなく標準入力で渡します。ps にタスクの内容が出ず、pkill -f などのパターン指定の操作に巻き込まれることもありません。
anchorleg はリポジトリの中にファイルを作りません(状態は ~/.anchorleg/ に置きます)。未追跡のファイルや未コミットの変更があると push を拒否するリポジトリでは、npm i -D で変わった package.json と lock を、運用を始める前にコミットしてください。グローバルにインストールすれば、この手順は要りません。
トークンは画面に表示されない入力で受け取ります。macOS ではキーチェーン、それ以外では ~/.anchorleg/tokens/(権限 600)に保存します。環境変数 BATON_TOKEN_<id> で渡すこともできます。
コマンド
| コマンド | 内容 |
|---|---|
| baton [-m model] [-e effort] [-p prompt] | ファンデーションを起動する。-p は初回だけ送る最初の指示 |
| baton foundation --headless [--ask-via <session>] [-m model] [-e effort] [-p prompt] | このプロジェクトのファンデーションを端末なしで動かす(下記「端末なしのファンデーション」)。ふつうは別のファンデーションが baton_start_foundation で起こす |
| baton run [-n name] [-m model] [-e effort] <task> | 1 タスクを新しいセッションで実行し、結果を JSON で出力する |
| baton status [--watch] [--all] | 動いているセッション(全プロジェクト)、アカウントごとの使用率、このプロジェクトのタスク。動いているタスクは必ずすべて表示し、終わったタスクは 10 件まで(--all ですべて)。--watch で 3 秒ごとに更新(新しい版がインストールされると、そのまま新しい版の表示に切り替わる。末尾に動いている版を出す) |
| baton config / baton config set <key> <value> / baton config unset <key> | 設定の一覧と変更。変えた値は、動いているファンデーションと作業役にも次の確認から反映される |
| baton workdirs / baton workdirs add <path> / baton workdirs remove <path> | プロジェクトの外で作業役を起動してよい場所(別のリポジトリのチェックアウトなど)の一覧・登録・削除(下記「作業役を起動するディレクトリ」) |
| baton token add <id> / baton token remove <id> | アカウントの登録と削除 |
| baton init | 設定の確認(リポジトリには何も書かない) |
起動するセッションには、常に --dangerously-skip-permissions を付けます。初回だけ Claude Code の確認画面(フォルダーの信頼、バイパスモードの承認)が出るので、ファンデーションの起動時に承認してください。
ファンデーションから使える MCP ツール
MCP サーバーは baton が起動するセッションに自動で渡すので、プロジェクト側の設定は要りません。
| ツール | 内容 |
|---|---|
| baton_run_task | 作業役を独立したプロセスで起動し、すぐに task_id を返す。context_files に役割の説明などのファイルを渡すと、作業役が最初に読む(相対パスはプロジェクトの根が基準)。resident: true で常駐する作業役になり、resident_idle_minutes でそのタスクだけの待ち時間を決められる。cwd で claude を起動するディレクトリを、extra_args で claude に足す引数を指定できる(下記「作業役を起動するディレクトリ」)。model や effort を省くと、役割の表(下記)から名前で補う |
| baton_task_result | 状態と結果を返す。wait_seconds(最大 50)で、動いている間は待てる |
| baton_continue_task | 同じセッションに続きの指示を渡す(文脈が残る)。作業役のプロセスが動いていれば、そのまま届く(実行中なら今のターンのあと)。終わっていれば resume して起動し直す。失敗したタスクも、prompt を省略すれば「続きをお願いします」で再開できる。resident: true で常駐に切り替えられる(動いている作業役にもその場で効く)。restart: true で、動いているプロセスを止めて同じセッションを新しいプロセスで resume する(MCP の設定の読み直しなど。記録は end_reason: restarted)。model と effort を渡すと、同じセッションを新しいモデル・エフォートで resume する(文脈は保たれ、記録も書き換わる)。resident_idle_minutes で常駐の待ち時間を変えられる(null で全体の設定に戻す。これだけを渡したときは、終わっているタスクを起こさない)。どの起動し直しも、タスクの cwd で行う |
| baton_list_tasks / baton_cancel_task | タスクの一覧と中止。一覧には動いているタスクを必ずすべて含め、終わったタスクは新しい順に 30 件まで(all: true ですべて)。since に前回の結果の now を渡すと、そのあと記録が変わったタスクだけを返す(定期点検向け)。中止には reason を付けられ、end_detail に残る(常駐する作業役の退役はこれで行う) |
| baton_sessions | このマシンで動いているセッション(全プロジェクトのファンデーションと作業役)の一覧。cwd は claude が動いているディレクトリ |
| baton_roles / baton_set_role | 役割の表の一覧と編集(編集はファンデーションだけ) |
| baton_start_foundation / baton_stop_foundation | 別のプロジェクトのファンデーションを端末なしで起こす・止める(ファンデーションだけ。下記「端末なしのファンデーション」) |
| baton_workdirs / baton_set_workdir | 作業役を起動してよい場所の一覧と、プロジェクトの外の場所の登録・削除(登録はファンデーションだけ) |
| baton_accounts | アカウントごとの使用率 |
| baton_settings / baton_set_setting | 設定の一覧と変更(変更はファンデーションだけ)。「切り替えのしきい値を 85% にして」のように頼める |
タスクの状態:
| 状態 | 意味 |
|---|---|
| queued | 起動待ち |
| waiting | 空いているアカウントがなく待っている。理由は waiting_reason(例: no account available (1: 5h 97% until 09/24 22:43, 2: weekly 100% until 09/30 07:00)) |
| running | 実行中。turn_state: idle なら、ターンを終えて裏の作業(コマンド・サブエージェント・ScheduleWakeup)を待っている |
| idle | ターンが終わった(作業役は完了を宣言していない)。last_message が最後の応答。続きを頼める。常駐する作業役で alive: true なら、通知やメッセージを待っている(baton status では idle*) |
| completed | 常駐しない作業役が baton_complete_task で完了を宣言した。summary がそのまとめ |
| failed / cancelled | 失敗 / 中止。失敗の理由は error に入る(例: claude exited with code 1、claude was terminated by SIGTERM (not by anchorleg))。同じ内容がログにも残る |
通常の作業役のプロセスは、ターンが終わり、裏で動かしたコマンド・裏で動かしたサブエージェント(Agent ツールの既定)・ScheduleWakeup の予定がどれも残っていなければ終了します。残っていればプロセスを残し、完了の通知で目を覚まさせるので、裏の作業が失われることはありません。この間、状態は running のままで、turn_state が idle になります。
他のセッションからのメッセージや、ファンデーションからの続きの指示を、裏の作業がなくても待ち続けたい場合は resident: true を使ってください。
常駐する作業役は、自分では終わりません。終わるのは次のどちらかだけです。
- ファンデーションが
baton_cancel_taskで中止したとき(reasonに退役の理由を書くとend_detailに残る) - 待機したまま
resident-idle-hours(既定 12 時間)が過ぎたとき。タスクにresident_idle_minutesを指定していれば、その分数が過ぎたとき。このときはidle(end_reason: idle-timeout)になり、baton_continue_taskで再開できる
待機の時間は、最後にターンを始めたか終えた時刻(last_active_at)から数え、アカウントの切り替えや新しい版での起動し直しをまたいでも数え直しません。プロンプトのキャッシュが生きている間(例: 1 時間)だけ同じセッションで続け、それを過ぎたら終えて次は文書から起こし直す、という運用には resident_idle_minutes: 60 を使います。last_active_at は、続けるか起こし直すかを決める目安にもなります。
常駐する作業役が baton_complete_task を呼んでも、報告として記録される(last_report)だけで、セッションは続きます。「待つ」つもりで呼んでしまい、そのまま終わってしまう事故を防ぐためです。ツールの説明も、常駐かどうかで出し分けています。
常駐する作業役のアカウントは待機中に切り替えます。ただし、裏でコマンドが動いている間は切り替えません(下記「片付けの依頼」)。
ターンの途中で上限に達した場合(作業役・常駐する作業役・ファンデーションのどれでも)、実行中のコマンド(マージのロック待ちなど)は API を使わずに最後まで動けるので、終わるまで待ってから止めます。待つのは最大 limit-wait-minutes 分(既定 30 分)で、過ぎたらコマンドごと止めて、そのことも伝えて resume します。
anchorleg が claude を止めるときは、理由とシグナルをタスクのログ(~/.anchorleg/projects/<プロジェクト>/tasks/<id>.log)と ~/.anchorleg/baton.log に残します。
st-6b: account 2 hit its limit (the API rejected a request: five_hour)
st-6b: commands are still running; waiting up to 30 min for them before switching accounts
st-6b: anchorleg is stopping claude (pid 12345) with SIGTERM: to continue on another account (the API rejected a request: five_hour)このような記録がないのに claude が止まっていれば、anchorleg 以外が止めています(error にも terminated by SIGTERM (not by anchorleg) のように出ます)。
どちらの作業役にも、完了したら baton_complete_task を呼ぶことと、自分がどちらの種類かを、追加のシステムプロンプトで伝えています。
作業役には baton_run_task・baton_continue_task・baton_cancel_task を出さず、代わりに baton_complete_task を出します(作業役が作業役を増やし続けないように)。作業役のセッション名は <プロジェクト名>-<name> で、ファンデーションは <プロジェクト名>-foundation です。Claude Code のセッション間メッセージでは、この名前で相手を指定します。
作業役を起動するディレクトリ(cwd)
baton_run_task の cwd に、プロジェクトの根からの相対パスか絶対パスを渡すと、作業役の claude をそのディレクトリで起動します。Claude Code は .claude/settings.json(hooks や claudeMdExcludes を含む)を作業ディレクトリのものだけ読み、CLAUDE.md は祖先を根まで読むので、パッケージごとの設定で動かしたい作業役に使います。
{ "task": "…", "name": "pipeline-a", "cwd": "packages/a", "resident": true, "resident_idle_minutes": 60 }cwdは、プロジェクトの根か、同じリポジトリの git worktree(git worktree listに出るもの。根の外にあってもよい)か、登録した場所(下記)の内側に限ります。外を指すもの、シンボリックリンクで外に出るもの、存在しないもの、ディレクトリでないものは拒否しますcwdはタスクの記録に残し、タスクの間は変えません。baton_continue_taskでの再開・restart・モデルやエフォートの変更・アカウントの切り替え・新しい版での起動し直しのどれでも、同じディレクトリで同じセッションを resume します(Claude Code の会話記録は作業ディレクトリごとに分かれているので、別の場所からは resume できません)- 変わらないもの: セッション間メッセージの宛先名(
<プロジェクト名>-<name>)、ファンデーションの名前、MCP のツールが扱うプロジェクト(タスクの記録・役割の表・状態の置き場所)は、どれもプロジェクトの根のままです - 作業役の claude には、ファンデーションから受け継いだ
CLAUDE_PROJECT_DIRを渡しません。Claude Code は hooks には自分の作業ディレクトリを渡しますが、Bash ツールには受け継いだ値をそのまま渡すので、残すと作業役の Bash の$CLAUDE_PROJECT_DIRが親のプロジェクトを指してしまいます。anchorleg が内部で使う変数(BATON_ROLE・BATON_PROJECT・BATON_TURN_FILEなど)も渡しません context_filesの相対パスは、cwdではなくプロジェクトの根が基準です。作業役が根とは別のディレクトリで動くときは、読み違えないよう絶対パスに直して渡しますbaton_task_result・baton_list_tasks・baton_sessionsのcwdに、作業役が動いているディレクトリ(絶対パス)が出ます。baton statusでは、根とは別の場所で動くタスクとセッションにその場所を添えます
パッケージを別のリポジトリに分けたときなど、プロジェクトの外の場所で作業役を起動するには、その場所を先に登録します。登録した場所の内側と、その場所の git worktree を cwd に指定できるようになります。登録していない場所は、今までどおり拒否します。
npx baton workdirs add ../pkg-a # 相対パスはプロジェクトの根が基準
npx baton workdirs # 一覧
npx baton workdirs remove ../pkg-aファンデーションからは baton_set_workdir({ "path": "../pkg-a" }、外すときは "remove": true)で登録でき、baton_workdirs で一覧を見られます。一覧はプロジェクトごとの状態ディレクトリ(~/.anchorleg/projects/<project>/workdirs.json)に置き、リポジトリには書きません。登録を外しても、そこですでに動いているタスクは、同じ場所で続けられます。別のリポジトリで動く作業役でも、宛先名(<プロジェクト名>-<name>)と MCP のプロジェクトは、このプロジェクトのままです。
別のリポジトリのチェックアウトをこのプロジェクトの中に置くと、登録しなくても cwd に指定できます。ただし、このプロジェクトの CLAUDE.md が祖先として読み込まれます(外すには claudeMdExcludes を使います)。プロジェクトの外に置けば、読み込まれません。
extra_args には、作業役の claude に足す引数を配列で渡せます(例: ["--setting-sources", "project,local"])。記録に残し、起動し直すたびに同じものを渡します。値をいくつも取るオプション(--add-dir など)があとの引数を飲み込まないよう、引数の最後に置きます。anchorleg が自分で決める引数(--resume・--session-id・--name・--model・--effort・--print・--append-system-prompt・--no-session-persistence など)は拒否します。
端末なしのファンデーション
作業役が増えて、役割の表・作業役の一覧をプロジェクトごとに分けたいときは、別のプロジェクトのファンデーションを、今のファンデーションから端末なしで起こせます。新しい端末を開く必要はなく、ユーザーへの問いの窓口も今のファンデーション 1 つのままにできます。
{ "project_dir": "~/Projects/sixb-dev の絶対パス", "prompt": "包の管理を引き継いでください", "model": "opus", "effort": "xhigh" }baton_start_foundationは、そのディレクトリでbaton foundation --headlessを裏で起動します。出力はそのプロジェクトの状態ディレクトリのfoundation.logに残ります- 役はファンデーションのままで、そのプロジェクトのタスク・役割の表・作業役を使います。アカウントの切り替え・片付けの依頼・新しい版での起動し直しも、端末ありのものと同じように効きます(中身は常駐する作業役と同じ仕組みです)
- 誰も見ていないので、質問の画面(AskUserQuestion)は使えません(
--disallowedTools AskUserQuestion)。ユーザーへの問いは、ask_via(既定は起こした側のファンデーション)へ SendMessage で送るよう、システムプロンプトで伝えます。起こした側が問いをユーザーに取り次ぎ、答えを SendMessage で返します - 指示(
prompt)は、ps に出ないよう引数ではなく受け箱で渡します。すでに動いていれば、指示だけを渡します。ふだんのやり取りは、SendMessage で<ディレクトリ名>-foundationに送ります - 様子は
baton_sessionsで見られます(headless: true、ask_via、state)。baton_stop_foundationで止めます。セッションは残るので、次に起こすと同じ会話を resume します(端末でnpx batonを起動して、同じセッションを画面ありで続けることもできます) - 端末で動いているファンデーションは、起こし直したり止めたりしません
- 制限: どのアカウントにも空きがないときは、作業役と同じく claude を止めて空きを待ちます(そのあいだメッセージは届きません)
役割の表(名前ごとの既定のモデルとエフォート)
baton_run_task で model や effort を省くと、作業役の名前を役割の表に照らして補います。渡し忘れで、本来より軽いモデルのまま動き続けるのを防ぐためです。
npx baton roles set 'st-issue-*' -m sonnet -e high
npx baton roles set 'st-6b*' -m fable -e max
npx baton roles # 一覧
npx baton roles remove 'st-6b*'- 表はプロジェクトごとの状態ディレクトリ(
~/.anchorleg/projects/<project>/roles.json)に置き、リポジトリには書きません。ファンデーションからはbaton_set_roleで編集できます patternは*と?のワイルドカードで、上から順に照らし、最初に一致した行を使います- 明示した値が優先で、省いた項目だけを表から補います。
""を渡すと、意図して既定を使います(警告しません) - 省いた項目が表にもなく既定になるときは、設定
unmatched-roleに従います:warn(既定。起動して結果にwarningを付ける)/reject(起動しない)/allow - タスクの記録には、モデルとエフォートがどこから決まったか(
model_source/effort_source:explicit/role:<pattern>/default)が残ります。baton statusでは、既定のまま動いているタスクに!を付けます
タスクの記録
baton_task_result と baton_list_tasks が返す項目です(schema_version: 2)。古い版で作られた記録も、欠けている項目を既定値で埋めて同じ形で返します。
| 項目 | 型 | null になりうるか | 内容 |
|---|---|---|---|
| schema_version | 数値 | いいえ | この形の版(2) |
| task_id / name | 文字列 | いいえ | |
| status | 文字列 | いいえ | 上の「タスクの状態」 |
| turn_state | idle / running | はい(プロセスが動いていないとき) | 動いているプロセスの、今のターンの状態 |
| end_reason | 文字列 | はい(まだ終わっていないとき) | completed / turn-ended / idle-timeout / cancelled / restarted / failed |
| end_detail | 文字列 | はい | 中止の理由、または失敗の内容 |
| resident / alive | 真偽値 | いいえ | 常駐するか / プロセスが動いているか |
| resident_idle_minutes | 数値 | はい(全体の設定 resident-idle-hours に従うとき) | このタスクの常駐の待ち時間(分) |
| cwd | 文字列 | いいえ | 作業役の claude が動くディレクトリ(絶対パス。既定はプロジェクトの根。0.7.0 より前の記録もプロジェクトの根) |
| waiting_reason | 文字列 | はい | アカウントの空きを待っている理由 |
| created_at / updated_at | ISO 時刻 | いいえ | updated_at は記録が最後に変わった時刻(since の比較に使う) |
| last_active_at | ISO 時刻 | はい(まだ動いていない、または 0.7.0 より前の記録) | 作業役が最後にターンを始めたか終えた時刻。プロンプトのキャッシュを最後に使ったころで、常駐の待ち時間はここから数える |
| finished_at | ISO 時刻 | はい | |
| summary | 文字列 | はい | 常駐しない作業役の完了の宣言のまとめ(baton_task_result のみ) |
| last_report | {summary, at} | はい | 常駐する作業役の最新の報告(baton_task_result のみ) |
| last_message / error / session_id | 文字列 | はい | 最後の応答、エラー、セッション ID(baton_task_result のみ) |
| extra_args | 文字列の配列 | はい(指定がないとき) | claude に足す引数(baton_task_result のみ) |
| accounts | 文字列の配列 | いいえ(空のことがある) | 使ったアカウント(baton_task_result のみ) |
| model / effort | 文字列 | はい(既定を使ったとき) | |
| model_source / effort_source | 文字列 | はい(0.6.2 より前の記録) | explicit / role:<pattern> / default |
| runs | 数値 | いいえ | 実行した回数(baton_task_result のみ) |
アカウントの選び方
新しいセッション(作業役、切り替え後のファンデーションなど)は、score が一番大きいアカウントに振ります。どちらの枠かが max-util(既定 95%)以上のアカウントには振りません。
strategy: use-before-reset(既定): 週枠は、使わなかった分がリセットで消えます。そこで、リセットまでに使い切るのに必要な速さが大きいアカウントを優先します。
- 必要な速さ =(
max-util− 週枠の使用率)÷ 週枠のリセットまでの時間(%/時) - 5時間枠の残りが 25% を切っているアカウントは今すぐは使えないので、残りに比例して下げる
- score = 必要な速さ ÷(そのアカウントで動いているセッション数 + 1)。ファンデーションは 2 本分として数える
たとえば「週枠の残り 50%、リセットまで 12 時間」のアカウントは約 4.2%/時、「残り 60%、リセットまで 6 日」のアカウントは約 0.4%/時なので、前者を優先します。
strategy: headroom: 今の余裕(max-util −(5時間枠と週枠の高い方))÷(セッション数 + 1)で選びます(0.5.0 より前の振り方)。
score の差が 1% 未満のアカウントは同点とみなし、使用率の低い方を選びます。baton status と baton_accounts で、各アカウントの必要な速さ・score・次の振り先(<- next)を確認できます。
リセット時刻は API が返す UNIX 時刻で計算するので、タイムゾーンの影響を受けません。表示はこの Mac のローカル時刻です。記録上のリセット時刻を過ぎている週枠は、次の週(7 日後)にリセットされるものとして扱います。
待機中の先回りの切り替えは、5時間枠と週枠のうち高い方が switch-at(既定 90%)を超えたときに行います。
リセット間近の枠を使い切る(use-up-window)
max-util(既定 95%)は、上限に当たってターンが中断されないための余りです。ただ、枠は使わなかった分がリセットで消えるので、リセット間近ならこの余りも使い切ります。
リセットまでの残りが、枠の長さの use-up-window%(既定 10%。週枠なら約 16.8 時間、5時間枠なら 30 分)を切った枠は、次のように扱います。
- 上限を
max-utilではなく 100% とし、振り先に選べるようにする(例: 週枠 97% でも、リセットが 10 時間後なら使える) switch-atによる先回りの切り替えをしない。上限(100%)に達したら、上限のときの切り替えに任せるuse-before-resetの「リセットまでに使い切る速さ」も、100% までの残りで計算する
use-up-window を 0 にすると、今までどおり常に max-util で止めます。
どのアカウントも空いていないとき
ファンデーションは、どのアカウントにも空き(max-util 未満)がなくても、claude を止めたまま待ちません。止めているあいだは、ほかのセッションからのメッセージが届かなくなるからです。
- 起動するときに空きがなければ、いちばん使用率の低いアカウントで起動しておきます(ログに
starting on account … anyway so that this session keeps receiving messages) - 上限(100%)に達しても、移り先がなければ claude を止めずに動かしておきます
- どちらの場合も
interval(既定 60 秒)ごとに使用率を問い合わせ直し、空きのあるアカウントができたら切り替えます。上限に達していたアカウントから移ったときは、「上限に達していたあいだの依頼やメッセージは失敗したか、返事をしていないかもしれないので確かめて」と伝えます(届いたメッセージは会話の記録に残っています)
作業役(baton run を含む)は、今までどおり claude を止めて空きを待ちます(ログに no account available (…), waiting)。待っている間は interval ごとに使用率を問い合わせ直し、どれかの枠がリセットされて max-util を下回ったら、そのアカウントで自動で再開します。上限に達したばかりのアカウントを外すのは最初の選び直しの 1 回だけで、そのアカウントが先にリセットされれば、それを使います(0.7.5 までは、待っている間も外したままでした)。
待機中に上限(100%)に達したとき
待機中(ターンの区切り)に今のアカウントが上限に達した場合は、裏で作業が動いていても切り替えます。上限に達したあとは、待っても作業は進まないからです(サブエージェントは API を使えません)。ターンの途中で上限に達したときと同じく、動いているコマンドだけは最大 limit-wait-minutes 分待ってから止めます。片付けの依頼は送りません(もう API を使えないため)。再開したセッションには、種類を問わず(コマンド、サブエージェント、Monitor など)止まった可能性のある裏の作業を確かめて動かし直すよう伝え、ScheduleWakeup の予定が失われていれば張り直すよう伝えます。切り替え先がない(どのアカウントも max-util 以上)ときは、claude をそのまま動かしておき(裏のコマンドも動き続けます)、どれかのアカウントが空いたら切り替えます。0.7.6 までは、裏の作業があると上限に達していても切り替えを待ち続けていました。
再開のあとのファンデーション
anchorleg がファンデーションを起動し直して自分からメッセージを送るとき(ターンの途中で上限に達したときの「続きをお願いします」、切り替えや新しい版での起動し直しの知らせ)は、メッセージの頭に (anchorleg: …) を付け、何が起きたかがわかるようにします。ターンの途中で上限に達したときは (anchorleg: this session was restarted on another subscription account because the previous one reached its usage limit …) に続けて resume-message を送ります。
あわせて、ファンデーションのシステムプロンプトで次のように伝えています: このメッセージはあなたではなく anchorleg が送っていて、あなたは席を外しているかもしれない。そのターンでは中断した作業をそのまま続け、質問の画面(AskUserQuestion)は出さない。決めてほしいことがあれば文章で書き、それに関係しない作業を続ける。質問の画面は、答えるまでセッションを止め、作業役からのメッセージも処理できなくなるためです。ふだんのターンでは、質問の画面を使えます。
片付けの依頼(wind-down)
切り替えたいとき(switch-at を超え、もっと空いているアカウントがあるとき)に、裏で作業(Bash のバックグラウンドのコマンド、バックグラウンドのサブエージェント)が動いていると、切り替えは待ちます。セッションを止めると、その作業も止まってしまうからです。数えるのはそのセッション自身の作業だけです。baton_run_task の作業役は、ファンデーションのプロセスの木の外で動かすので、作業役のコマンドは数えません(0.7.3 までは数えていたため、作業役が動いている限り、ファンデーションの切り替えと新しい版での起動し直しが後回しになり続けていました)。ただ、そのまま待ち続けると、上限(100%)に達して作業が途中で失われることがあります。
そこで、ファンデーションと作業役のどちらにも、一度だけ次のように知らせます。
(anchorleg: account 1 is at 5h 92% / weekly 40%. A switch to a less used account is waiting for your background work: background commands.)知らせを受けたらどう動くか(すぐ終わる作業は終わらせ、長い作業はきれいに止めて動かし直すものを書き留め、新しい作業は始めずにターンを終える)は、anchorleg が起動時にシステムプロンプトで伝えてあります。知らせの文は事実だけにしています。フックの出力に指示を書くと、プロンプトの注入とみなされて従われないことがあるためです。
裏の作業がなくなったら切り替え、新しいアカウントで再開したセッションに「切り替えが終わった。止めた作業があれば動かし直してよい」と伝えます。
- 作業役へは、標準入力からメッセージとして送ります
- ファンデーション(画面ありのセッション)へは、ターンの終わりに起こす Claude Code のフック(
asyncRewake)で届けます。このフック(baton hook notice)は裏で待ち、知らせができたら出力して終わります。Claude Code は待機中のセッションを起こし、フックの出力として渡します - 知らせるのは、切り替え先があるときだけです。使用率が
switch-atを下回ったら、取り下げます - 設定
wind-down(既定on)をoffにすると知らせず、今までどおり裏の作業が終わるのを待つだけにします
使用率の求め方
setup-token には使用率 API を読む権限がないため、次の順で求めます。
- 動いているセッションの出力に流れる使用率のイベント(費用なし)
- 最後に分かった値(リセット時刻を過ぎた枠は 0% とみなす)
- 記録が古いときだけ、haiku に 1 トークンだけのリクエストを送り、応答ヘッダーから読む
モデル別の週枠(Fable など)は、上限に達したことをイベントで受け取った時点で記録し、そのモデルでは解除時刻までそのアカウントを使いません。
新しい版への更新
新しい版の anchorleg をインストールすると(npm i -D anchorleg@<版> など)、動いているファンデーションと常駐する作業役は、区切りのよいところで自動的に新しい版で起動し直します。あなたが端末で操作する必要はありません。
- ファンデーション: 待機中(ターンの区切り)に claude を止め、baton 自身を同じプロセスのまま新しい版に置き換えて(Node 23.11 以降の
process.execve。それより古い Node では新しい版を子として起動)、同じ会話を resume します - 常駐する作業役: 待機中に止め、新しい版の作業役として同じセッションを resume します。元の指示は送り直しません
- 常駐しない作業役: ターンが終われば終了するので、次に起動するものから新しい版になります
どちらも、次のものが動いている間は待ちます。
- ターンの途中(スクリプトの実行中など)
- 裏で動いているコマンド(Bash ツールの
run_in_backgroundなど) - 裏で動いているサブエージェント(Agent ツールの既定)。ファンデーションは会話記録から、作業役は claude の出力から数える
ScheduleWakeup で待っていた場合は、起動し直したあとに張り直すよう伝えます。自動の更新を止めるには baton config set auto-upgrade off を実行します。
0.5.0 以前はこの機能を持たないので、0.6.0 に上げるときだけは、ファンデーションの起動し直し(/exit → npx baton)と、常駐する作業役の baton_continue_task(restart: true) が必要です。
設定
値は 環境変数 → 設定ファイル(~/.anchorleg/config.json)→ 既定値 の順に決まります。baton config set で変えた値は設定ファイルに書かれ、動いているファンデーションや作業役も、次の確認から新しい値を使います。起動し直す必要はありません。ただし、環境変数を指定して起動したプロセスでは、環境変数の値が優先されます(baton config の source 欄で確認できます)。
npx baton config # 一覧(値と、env / config / default のどこから来たか)
npx baton config set max-util 90
npx baton config unset max-util # 既定値に戻す| キー(環境変数) | 既定値 | 内容 |
|---|---|---|
| switch-at(BATON_SWITCH_AT) | 90 | ファンデーションが待機中にこの 5 時間枠 % を超えたら、先回りして切り替える |
| unmatched-role(BATON_UNMATCHED_ROLE) | warn | モデルやエフォートを省き、役割の表にも一致しないとき: warn / reject / allow |
| auto-upgrade(BATON_AUTO_UPGRADE) | on | 新しい版がインストールされたら、ファンデーションと常駐する作業役を待機中に新しい版で起動し直す |
| strategy(BATON_STRATEGY) | use-before-reset | 振り先の選び方(上の「アカウントの選び方」)。headroom で今の余裕を優先する |
| max-util(BATON_MAX_UTIL) | 95 | 5 時間枠がこの % 以上のアカウントには切り替えない |
| interval(BATON_INTERVAL) | 60 | 使用率を確認する間隔(秒) |
| usage-max-age(BATON_USAGE_MAX_AGE) | 300 | 使用率の記録をこの秒数まで使う |
| use-up-window(BATON_USE_UP_WINDOW) | 10 | 枠のリセットまでの残りが、枠の長さのこの % を切ったら、その枠は 100% まで使い、先回りの切り替えもしない(上記「リセット間近の枠を使い切る」。0 で無効) |
| wind-down(BATON_WIND_DOWN) | on | 切り替えたいのに裏の作業が動いているとき、セッションに一度だけ知らせて片付けてもらう(上記「片付けの依頼」) |
| resume-message(BATON_RESUME_MSG) | 続きをお願いします | ターンの途中で切り替えたときに送るメッセージ |
| reschedule-message(BATON_RESCHEDULE_MSG) | (日本語の案内) | 待機中の切り替えで ScheduleWakeup の予定が失われたときに送るメッセージ |
| limit-wait-minutes(BATON_LIMIT_WAIT_MINUTES) | 30 | 上限に達しても、実行中のコマンドが終わるまでこの分数は待ってから切り替える |
| resident-idle-hours(BATON_RESIDENT_IDLE_HOURS) | 12 | 常駐する作業役がこの時間待機したままなら終える。タスクごとの resident_idle_minutes があれば、そちらを使う |
| events(BATON_EVENTS) | off | all にすると、作業役の出力イベント(思考やツールの結果を含む)を残す |
| events-max-mb(BATON_EVENTS_MAX_MB) | 5 | イベントを残す場合、この大きさで 1 世代だけ残してローテーションする |
| (BATON_HOME) | ~/.anchorleg | アカウント、使用率、設定、ログの置き場所。環境変数でのみ指定できる |
状態の置き場所
~/.anchorleg/: アカウントの一覧、使用率の記録、動いているセッション(active/)、ログ(baton.log)。異常終了したセッションの記録は、一覧を読むときに消します。その pid が別のプログラムに使い回されていても、baton のプロセスでなければ消します~/.anchorleg/projects/<プロジェクト名>-<ハッシュ>/: ファンデーションのセッション ID、タスクの記録とログ、役割の表(roles.json)、作業役を起動してよい場所(workdirs.json)
0.1.x はプロジェクトの中の .baton/ に置いていました。0.2.0 以降は最初の起動時にその中身を一度だけ引き継ぎ、.baton/MOVED.txt に「移設済み・ここは更新されない」ことと新しい置き場所を書きます。そのあと .baton/ は削除してかまいません。
記録はすべてローカルのファイルへの書き込みで、トークンは使いません。使用率の確認で API に問い合わせるときだけ、haiku の 1 トークンぶんを使います。
他のハーネスへの対応
ハーネスに固有の処理(起動引数、環境変数、出力イベント、使用率の読み方)は src/harness/claude.js にまとめています。他のハーネスに対応するときは、同じ形のアダプターを src/harness/ に追加します。
テスト
npm test偽の claude(test/fake-claude.js)と、使用率を返す偽の API サーバーを使います。本物のアカウントは使いません。
