Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.5.13] — 2026-06-07

### Added

- **`burn --fresh`** — delete the artifact before running, for a clean slate.
- **`burn --timeout` perl fallback** — when neither `timeout` nor `gtimeout` (coreutils)
is on PATH, the run is bounded with a `perl` alarm (stock macOS ships neither). Only
when all three are missing does the run go unbounded (still warned).
- **`burn` summary line** — on finish, one line: tank used, reroute count, elapsed time,
artifact size.

### Fixed

- **`burn` no longer counts a STALE artifact as success.** A leftover artifact from a
previous run could make a failed task look like it succeeded. Success is now judged by
the artifact appearing *or its timestamp changing*, via the existing GNU-stat-first
`_clikae_mtime` helper (portable across macOS/Linux; whole-second resolution — `--fresh`
sidesteps a same-second overwrite).
- **Accurate agy docs & strings.** Comments and i18n that said agy "hardcodes ~/.gemini
and ignores env" were imprecise: agy's *state* follows `$HOME`, but its *login* is one
global Keychain entry — so different accounts can't be routed per-shell, which is why
switching is global. Also removed a dead, incorrect "agy can't be renamed" string (it
can — rename carries the per-tank Keychain login).
- **`remove agy <active-tank>`** now names a concrete other tank to switch to.

## [0.5.12] — 2026-06-05

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
> *"Kirikae" (切り替え, ki-ri-ka-e) is Japanese for "switching".*

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Status](https://img.shields.io/badge/status-v0.5.12-blue.svg)](CHANGELOG.md)
[![Status](https://img.shields.io/badge/status-v0.5.13-blue.svg)](CHANGELOG.md)

> ⚠️ **Unofficial.** `clikae` is a community tool. It is not affiliated with, endorsed by, or sponsored by any of the CLI vendors it integrates with. "Claude" is a trademark of Anthropic, PBC; other CLI names are trademarks of their respective owners.

Expand Down Expand Up @@ -108,6 +108,7 @@ clikae # your home board (run `clikae doctor` for a h
- **v0.5.5** — **Antigravity / agy becomes real multi-account** (each tank carries its own Google login via the macOS Keychain); **codex sessions join the home board's Continue list** (true cross-engine resume); **`clikae burn`** runs a headless task on a tank and re-fires it on the next when one runs dry (verified by artifact, not exit code); and a **cross-shell in-use guard** so `rename`/`migrate`/`remove` won't move a tank a session in another terminal is still using.
- **v0.5.6** — hardened that in-use guard to be truly best-effort: a restricted `ps` (CI runners, locked-down hosts) no longer aborts `rename`/`migrate`/`remove`.
- **v0.5.7** — the board shows only **burnable fuel tanks** (tool-CLI tanks live in `clikae tanks`); **`clikae app --board`** makes a launcher for the menu, not one tank; **Ghostty launchers** use a trusted config file (no "Allow Ghostty to execute…" dialog) and are re-signed for Apple Silicon; and switching to a tank whose CLI isn't installed gives a **helpful install hint** instead of `exec: … not found`.
- **v0.5.13** — **`burn` hardening** from two dogfoods: a stale artifact no longer reads as success (judged by timestamp change; `--fresh` for a clean slate), `--timeout` falls back to a `perl` alarm when coreutils is absent, and a one-line run summary. Plus accurate agy docs/strings (state follows `$HOME`, login is one global Keychain entry).
- **v0.5.12** — **state schema versioning**: `$CLIKAE_HOME/` now carries a `version` marker + a forward-migration runner, so future on-disk format changes are safe (read-only-preserving, invisible in normal use). With this, clikae's quality punch-list is empty.
- **v0.5.11** — reliability + honesty polish: `clikae watch` starts dependably; `clikae to codex` says "fresh start" instead of promising a resume it can't do; auto-reroute won't dead-end on agy; `clikae tanks` shows an agy tank's real account; and a new **["is this a bug?" Expectations guide](docs/EXPECTATIONS.md)** for behaviours that look surprising but are deliberate. Plus doc corrections (the board's language key is `l`, not `h`).
- **v0.5.10** — **`burn` won't spend the quota you're using.** Its auto-reroute now skips a tank an interactive session is live on (`--allow-active` to override) and skips tanks that share an already-dry account — closing the original "燒爆" footgun (a headless job rerouting onto your live conversation's tank). Plus a clearer agy burn-refusal + a `clikae tanks` footnote that agy is interactive-only.
Expand Down
2 changes: 1 addition & 1 deletion bin/clikae
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

set -eo pipefail

CLIKAE_VERSION="0.5.12"
CLIKAE_VERSION="0.5.13"

# Resolve install root (handles symlink chains, e.g., via Homebrew)
__resolve_self() {
Expand Down
29 changes: 15 additions & 14 deletions lib/commands/antigravity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
# lib/commands/antigravity.sh — Antigravity (agy) folded into clikae's standard
# grammar. See docs/grammar.md §6.
#
# WHY agy is special: its CLI (`agy`) hardcodes state under ~/.gemini and ignores
# every env var / has no config-dir flag (verified on a real install). clikae's
# clean per-shell model can't switch it, so the only way to give it multiple
# tanks is to SWAP ~/.gemini between per-tank directories via a symlink. That is
# GLOBAL (one tank active at a time across ALL terminals) and mutates your real
# home dir — so the first `init agy` asks before taking over, and it's reversible
# with `clikae agy --release`.
# WHY agy is special: its CLI (`agy`) keeps its login as one global Keychain entry
# (state follows $HOME, but the account doesn't) and has no per-account config-dir
# flag. clikae's clean per-shell model can't switch the account, so the only way to
# give it multiple tanks is to SWAP ~/.gemini between per-tank directories via a
# symlink (carrying each tank's Keychain login). That is GLOBAL (one tank active at
# a time across ALL terminals) and mutates your real home dir — so the first
# `init agy` asks before taking over, and it's reversible with `clikae agy --release`.
#
# The user types the SAME verbs as every other engine — there is no `agy enable`
# / `add` / `use` / `disable` subcommand tree any more:
Expand Down Expand Up @@ -155,7 +155,7 @@ _agy_takeover() {
tank its own account clikae copies that login between Keychain slots on every
switch. The token moves Keychain→Keychain and is never written to disk.
• It is GLOBAL: only one agy tank is active at a time across ALL terminals
(agy ignores per-shell env). Don't run two tanks at once.
(the login is one global Keychain entry). Don't run two tanks at once.
• Swapping while agy is running can corrupt that session.
Reversible: 'clikae agy --release' restores a normal ~/.gemini (your tanks and
their stashed logins are kept).
Expand Down Expand Up @@ -277,7 +277,8 @@ _agy_remove() {

# More than one tank remains.
if [ "$name" = "$active" ]; then
log_fail "'$name' is the active agy tank. Switch to another first: clikae agy <other-tank>"
local _other; _other="$(_agy_tank_names | grep -vx "$name" | head -1)"
log_fail "'$name' is the active agy tank. Switch to another first: clikae agy ${_other:-<other-tank>}"
fi
rm -rf "${slots:?}/$name"
_agy_kc_forget "$name"
Expand All @@ -292,11 +293,11 @@ Usage: clikae agy [tank] [-- args...] switch agy to <tank> and run it
clikae remove agy <tank> remove an agy tank
clikae agy --release restore a normal ~/.gemini, keep tanks

Antigravity (agy) hardcodes ~/.gemini and ignores env vars, so clikae can't
switch it per-shell like other engines. Instead it swaps ~/.gemini between tank
dirs via a symlink — a GLOBAL power mode: one agy tank is active at a time across
ALL terminals. The first `init agy` asks before taking over ~/.gemini; it's
reversible with `clikae agy --release`.
Antigravity (agy) keeps its login as one global Keychain entry, so clikae can't
switch the account per-shell like other engines. Instead it swaps ~/.gemini between
tank dirs via a symlink (carrying each tank's login) — a GLOBAL power mode: one agy
tank is active at a time across ALL terminals. The first `init agy` asks before
taking over ~/.gemini; it's reversible with `clikae agy --release`.
EOF
}

Expand Down
55 changes: 45 additions & 10 deletions lib/commands/burn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ produce (never the exit code — codex exec exits 0 even when it hit its limit a
wrote nothing), and if the tank ran dry, re-fire the SAME command on the next
tank in your reserve.

--artifact <path> the file the task must produce; its presence = success.
--artifact <path> the file the task must produce. Success = it appears, or (if
it already existed) its timestamp changes — a STALE file from
a previous run is NOT counted as success.
--fresh delete <artifact> before running, for a clean slate.
--to <target> explicit next hop on a dry tank (<engine>/<tank> or a bare
tank of this engine). Otherwise burn walks this engine's
other tanks. A cross-engine --to runs the SAME command under
that engine — only sensible if the command is engine-agnostic.
--timeout <secs> bound the run. NEEDS `timeout` or `gtimeout` (GNU coreutils)
on PATH — stock macOS has neither, so WITHOUT it the run is
NOT bounded and a warning is printed (install: `brew install
coreutils` for `gtimeout`).
--timeout <secs> bound the run. Uses `timeout`/`gtimeout` (coreutils) if present,
else a `perl` alarm (SIGALRM, direct child only). With none of
the three on PATH the run is NOT bounded and a warning is printed.
--no-reroute run once; on a dry tank, stop instead of falling through.
--allow-active let auto-reroute use a tank an interactive session is on.
By default the reserve SKIPS such tanks (rerouting a headless
Expand All @@ -51,6 +53,10 @@ Examples:
burn is the headless sibling of the interactive switch: pre-stage inputs to /tmp
(never hand a tank slow iCloud-backed I/O), and make tasks idempotent + artifact-
checked so a dropped one just re-fires elsewhere.

Boundary: burn only fits tasks whose success is a FILE you can name — codegen,
analysis, transforms. It CANNOT judge work whose proof is runtime behaviour (a UI
renders, a server answers); that still needs a human to verify.
EOF
}

Expand Down Expand Up @@ -95,12 +101,23 @@ EOF
_burn_timeout_bin() {
if command -v timeout >/dev/null 2>&1; then printf 'timeout'; return 0; fi
if command -v gtimeout >/dev/null 2>&1; then printf 'gtimeout'; return 0; fi
log_warn "--timeout needs \`timeout\`/\`gtimeout\` (coreutils) on PATH — running WITHOUT a time bound."
if command -v perl >/dev/null 2>&1; then printf 'perl'; return 0; fi
log_warn "--timeout needs \`timeout\`/\`gtimeout\` (coreutils) or \`perl\` on PATH — running WITHOUT a time bound."
return 0
}

# Artifact freshness uses _clikae_mtime (lib/core/adapter_loader.sh) — epoch mtime,
# 0 if absent, GNU-stat-first for Linux portability — so a STALE file from a prior
# run can't be mistaken for this run's success (2026-06-06 tugtile dogfood #2).
# Whole-second resolution; a same-second overwrite is invisible (--fresh sidesteps it).

# _burn_size <path> -> byte count, or "?" if absent (for the summary line).
_burn_size() {
if [ -e "$1" ]; then wc -c < "$1" 2>/dev/null | tr -d ' '; else printf '?'; fi
}

cmd_burn() {
local cli="" tank="" artifact="" to="" timeout_s="" reroute=1 allow_active=0
local cli="" tank="" artifact="" to="" timeout_s="" reroute=1 allow_active=0 fresh=0
local -a cmd=()
while [ $# -gt 0 ]; do
case "$1" in
Expand All @@ -110,6 +127,7 @@ cmd_burn() {
--timeout) shift; [ $# -gt 0 ] || log_fail "--timeout needs seconds"; timeout_s="$1"; shift ;;
--no-reroute) reroute=0; shift ;;
--allow-active) allow_active=1; shift ;;
--fresh) fresh=1; shift ;;
--) shift; cmd=("$@"); break ;;
-*) log_fail "Unknown flag: $1 (try: clikae burn --help)" ;;
*) if [ -z "$cli" ]; then cli="$1"
Expand All @@ -133,6 +151,18 @@ cmd_burn() {
command -v "$binary" >/dev/null 2>&1 || log_fail "'$binary' is not on PATH."
local envvar; envvar="$(adapter_meta_env_var 2>/dev/null || true)" # for the in-use guard

# #2 (tugtile dogfood): snapshot the artifact so a STALE file from a prior run
# isn't mistaken for success. --fresh clears it; otherwise warn + judge by mtime.
if [ "$fresh" -eq 1 ] && [ -e "$artifact" ]; then
rm -f "$artifact" 2>/dev/null
if [ -e "$artifact" ]; then log_warn "--fresh could not remove $artifact (judging by timestamp instead)."
else log_info "--fresh: cleared $artifact"; fi
elif [ -e "$artifact" ]; then
log_warn "artifact already exists: $artifact — judging success by a timestamp change (use --fresh for a clean slate)."
fi
local art_pre; art_pre="$(_clikae_mtime "$artifact")" # 0 when absent
local t0=$SECONDS

local cur="$tank" tried="" dried_accts="" reset out rc
while :; do
validate_name profile "$cur"
Expand All @@ -145,7 +175,10 @@ cmd_burn() {
local -a runner=()
if [ -n "$timeout_s" ]; then
local _tbin; _tbin="$(_burn_timeout_bin)"
[ -n "$_tbin" ] && runner=("$_tbin" "$timeout_s")
case "$_tbin" in
timeout|gtimeout) runner=("$_tbin" "$timeout_s") ;;
perl) runner=(perl -e 'alarm shift; exec @ARGV or exit 127' "$timeout_s") ;;
esac
fi
rc=0
out="$(
Expand All @@ -167,13 +200,15 @@ KV
# Remember this dried tank's account so the reserve skips its same-quota siblings (P1).
local _acct; _acct="$(_limit_tank_account "$cli" "$cur" 2>/dev/null || true)"
[ -n "$_acct" ] && dried_accts="${dried_accts}${_acct}"$'\n'
elif [ -e "$artifact" ]; then
elif [ -e "$artifact" ] && [ "$(_clikae_mtime "$artifact")" != "$art_pre" ]; then
dry_store_clear "$cli" "$cur" # a real success recovered this tank
log_ok "Done on $cli/$cur — artifact present: $artifact"
log_info "summary: tank=$cli/$cur reroutes=$(printf '%s' "$tried" | wc -w | tr -d ' ') elapsed=$((SECONDS - t0))s artifact=$(_burn_size "$artifact")B"
return 0
else
log_err "$cli/$cur produced no artifact and shows no limit — a real task failure (rc=$rc), not a dry tank."
log_err "$cli/$cur produced no fresh artifact and shows no limit — a real task failure (rc=$rc), not a dry tank."
printf '%s\n' "$out" | tail -n 5 | sed 's/^/ /'
log_info "summary: tank=$cli/$cur reroutes=$(printf '%s' "$tried" | wc -w | tr -d ' ') elapsed=$((SECONDS - t0))s artifact=none"
return 1
fi

Expand Down
6 changes: 3 additions & 3 deletions lib/commands/list.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ EOF
printf '%s' "$enriched" | _list_render_json
else
printf '%s' "$enriched" | _list_render_table "$show_paths"
# Flag agy's nature so nobody tries `burn agy`: it's global/single-account, so
# there's no per-tank headless burn — interactive switch only.
# Flag agy's nature so nobody tries `burn agy`: its login is global (one account
# across all shells), so there's no per-tank headless burn — interactive switch only.
case $'\n'"$enriched" in
*$'\n'"agy"$'\037'*) printf '\n%b agy is global/single-account — interactive switch only (clikae agy <tank>); not burnable.%b\n' "$__C_DIM" "$__C_RESET" ;;
*$'\n'"agy"$'\037'*) printf '\n%b agy login is global — one account active at a time, interactive switch only (clikae agy <tank>); not burnable.%b\n' "$__C_DIM" "$__C_RESET" ;;
esac
fi
}
4 changes: 2 additions & 2 deletions lib/commands/rename.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ EOF
validate_name profile "$new"
[ "$old" != "$new" ] || log_fail "Old and new names are the same ('$old')."

# agy is a symlink-managed target, not an env adapter: rename moves the slot dir
# and repoints ~/.gemini if it's the active one. (No alias/Keychain to carry.)
# agy is a symlink-managed target, not an env adapter: rename moves the slot dir,
# repoints ~/.gemini if it's the active one, and carries the per-tank Keychain login.
if [ "$cli" = "agy" ] || [ "$cli" = "antigravity" ]; then
# shellcheck source=./antigravity.sh
source "$CLIKAE_LIB/commands/antigravity.sh"
Expand Down
12 changes: 6 additions & 6 deletions lib/core/adapter_loader.sh
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,14 @@ load_adapter() {
local cli="$1"
local f="$CLIKAE_LIB/adapters/$cli.sh"
if [ ! -f "$f" ]; then
# agy (Antigravity) is a known engine with no adapter on purpose: it ignores
# env vars and hardcodes ~/.gemini, so it's switched globally, not per-shell
# or per-config-dir. Give that guidance instead of the generic "no adapter"
# whenever a generic path (env/app/alias/run/relay/migrate) is handed it.
# agy (Antigravity) is a known engine with no adapter on purpose: its login is
# one global Keychain entry (state follows $HOME, but the account doesn't), so
# accounts switch globally — not per-shell. Give that guidance instead of the
# generic "no adapter" whenever a generic path (env/app/alias/run/relay/migrate) is handed it.
case "$cli" in
agy|antigravity)
log_err "agy (Antigravity) is global — clikae can't route it per-shell or per-config-dir."
log_dim "Switch the machine-wide login with: clikae agy <tank> (see clikae agy --help)"
log_err "agy (Antigravity) login is global — clikae can't route its account per-shell."
log_dim "Switch the global login with: clikae agy <tank> (see clikae agy --help)"
exit 1 ;;
esac
log_err "No built-in adapter for '$cli'."
Expand Down
9 changes: 3 additions & 6 deletions lib/core/i18n.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ i18n_load() {
T_ACTIVE_HERE="active here"
T_ALSO_AVAILABLE="Also available"
T_NO_TANK_DEFAULT="no tank yet — opens default"
T_AGY_NOTE="single-account · opens ~/.gemini directly"
T_AGY_NOTE="single-account · global login (one account, all shells)"
T_LAUNCH="launch"
T_MORE="more"
T_INCOGNITO="Incognito"
Expand Down Expand Up @@ -120,7 +120,6 @@ i18n_load() {
T_RENAME_CURRENTLY="currently"
T_RENAME_NEW="New name: "
T_RENAME_CANCEL="Cancelled — name unchanged."
T_RENAME_NO_AGY="agy tanks can't be renamed (it's a global ~/.gemini target, not a per-shell engine)."
# filter / help / misc
T_FILTER_PROMPT="filter: "
T_FILTER_NONE="no matches"
Expand Down Expand Up @@ -150,7 +149,7 @@ i18n_load() {
T_ACTIVE_HERE="使用中"
T_ALSO_AVAILABLE="その他"
T_NO_TANK_DEFAULT="タンク未作成 — 既定で開く"
T_AGY_NOTE="シングルアカウント · ~/.gemini を直接開く"
T_AGY_NOTE="シングルアカウント · グローバルログイン(全シェル共通)"
T_LAUNCH="起動"
T_MORE="その他"
T_INCOGNITO="シークレット"
Expand Down Expand Up @@ -205,7 +204,6 @@ i18n_load() {
T_RENAME_CURRENTLY="現在"
T_RENAME_NEW="新しい名前: "
T_RENAME_CANCEL="キャンセル — 名前は変更されません。"
T_RENAME_NO_AGY="agy タンクは名前変更できません(~/.gemini を使うグローバル対象のため)。"
T_FILTER_PROMPT="絞込: "
T_FILTER_NONE="一致なし"
T_HELP_TITLE="clikae — キー操作"
Expand All @@ -232,7 +230,7 @@ i18n_load() {
T_ACTIVE_HERE="使用中"
T_ALSO_AVAILABLE="也可開啟"
T_NO_TANK_DEFAULT="尚無油箱 — 開預設"
T_AGY_NOTE="單帳號 · 直接開 ~/.gemini"
T_AGY_NOTE="單帳號 · 全域登入(所有 shell 共用)"
T_LAUNCH="啟動"
T_MORE="更多"
T_INCOGNITO="無痕"
Expand Down Expand Up @@ -287,7 +285,6 @@ i18n_load() {
T_RENAME_CURRENTLY="目前"
T_RENAME_NEW="新名稱:"
T_RENAME_CANCEL="已取消 — 名稱未變更。"
T_RENAME_NO_AGY="agy 油箱無法改名(它是吃 ~/.gemini 的全域對象,不是 per-shell 引擎)。"
T_FILTER_PROMPT="篩選:"
T_FILTER_NONE="無相符項目"
T_HELP_TITLE="clikae — 按鍵"
Expand Down
Loading
Loading