feat(gemini): install the CLI on linux and fix backend selection - #110
Merged
Conversation
The panel/code-review gemini backend was only ever installed from the Brewfile, so the Darwin-guarded osx role left the linux host without it while roles/fish still symlinked conf.d/gemini.fish there. That host was exporting GEMINI_API_KEY for a binary it did not have. apt has no package and mise's registry offers exactly one backend (npm:@google/gemini-cli), so npm is the route rather than a preference. The pin sits in linux.toml with the other linux-only pins, but the install runs from the environments role: the npm backend needs node on PATH and roles/linux runs first, so linux_mise_tools is walked before node exists. Gated on 'mise ls --missing' rather than on a string in mise's output. mise emits 'WARN gemini installed but not activated' on every run while the tool is undeclared, so the linux role's 'installed in stderr' test would report changed forever.
The ~/.gemini directory and the 1Password sync lived in the Darwin-guarded osx role, so the linux host got the fish snippet that reads the key but nothing that writes it. Neither task was ever macOS-specific; both move to the cross-platform claude role and keep the osx tag so 'mise run osx' still syncs on a Mac. Teach the sync script the headless path. A headless host has no 1Password desktop app to authorize against, so op fails before it reads anything; fall back to the machine-local service-account token exactly as ssh-lan-config-sync.sh does. That constrains the vault: a service account cannot be granted Personal or Private, so the item has to live in Dotfiles Service Account and must be addressed with an explicit --vault. Without one op refuses every field with 'a vault query must be provided', which reads like a missing item and is not. Moving the item between vaults also reassigned its id, so ITEM_UUID now names the item in that vault rather than the one it had in Private. Verified end to end on the linux host: CHANGED on first run, OK on the second, key file 0600 and the directory 0700 (the gemini CLI creates that directory itself at 0755, so the mode assertion is load-bearing).
/panel-review and /code-review resolved the machine profile from PANEL_REVIEW_PROFILE alone, defaulting to 'personal'. Nothing in this repo ever sets that variable, so the work host fell through to the default and reached for gemini on every review, the exact opposite of what the profile table asks for. Resolve the dotfiles inventory alias instead (DOTFILES_HOST, else ~/.config/dotfiles/host, else work), the same indirection scripts/playbook.sh and roles/fish/files/ollama.fish already use, so the work host picks codex with nothing to remember. PANEL_REVIEW_PROFILE is still honoured ahead of it as a per-run override. Note the fallback direction deliberately differs from ollama.fish: there an unresolved alias must set nothing, here it means work, matching playbook.sh. Fold server into the personal/alt row, and record gemini CLI drift measured on 0.54.4: the CLI now refuses headless runs in untrusted directories, and it downgrades --approval-mode plan to default BEFORE aborting, so a future version that stops aborting would run with the read-only guard already stripped. Both commands now pass --skip-trust and keep plan mode.
…g-sign-off] The previous commit split the pin from its install across two roles, on the premise that mise's npm backend needs node on PATH and that roles/linux runs before roles/environments owns the node pin. Measurement refutes the premise: with a throwaway MISE_DATA_DIR (node uninstalled) and node absent from PATH, 'mise install npm:is-odd@3.0.1' still succeeds, because mise bootstraps a node for the backend rather than borrowing the host's. So gemini joins linux_mise_tools like every other entry, and the two bespoke tasks in roles/environments go away. That also closes a real hole those tasks had: 'mise ls --missing <tool>' prints nothing both when a tool is installed AND when it is merely undeclared (verified with an uninstalled, undeclared 'deno'), so the gate could not tell convergence from a missing linux.toml symlink. A tag-filtered 'mise run environments' on a host whose linux role had not run would have skipped the install silently and reported success — and 'mise run environments' is exactly what the new error strings told the operator to run. Confirmed the linux role's existing loop converges for this tool: once gemini is declared, a re-install prints nothing on stderr, so its changed_when: "'installed' in stderr" test stays false. The 'installed but not activated' WARN that would have broken it only fires while the tool is undeclared.
… resolver [pending-sign-off] The resolver introduced two commits ago claimed to work 'exactly as scripts/playbook.sh and ollama.fish' do, and did not. It dropped their residual '*panela*' hostname match, so an alt Mac (which legitimately has no alias file, per the machine-local files section of CLAUDE.md) resolved to work and reached for codex, a CLI it never logs into. Fixing the work host by breaking the alt host is not a fix. It also tested '[ -f "$alias_file" ]' rather than the file's contents. An empty or newline-only alias file yields an empty profile, which is not 'work' and so falls through to gemini. Reproduced: a touched alias file puts the work host back on gemini, which is the exact bug the change exists to fix, re-entered through a different door. The branch now binds to a non-empty trimmed value. Also honours DOTFILES_HOST_FILE, which playbook.sh honours and this did not, so a host that relocates its alias file no longer has playbook.sh and the review commands disagreeing about which machine it is. Validated with a 10-case table covering all three regressions and seven already-correct cases: alt-with-no-file, empty file, newline-only file, unconfigured non-alt host, each named alias, alias-beats-hostname, and both override precedences. All three failing cases were confirmed failing against the previous snippet first. Mirrors the whole block into /code-review, including the two elements it was missing (the not-in-table default note and the service-account vault caveat), and makes /panel-review's Maintenance section name the reciprocal sync duty so drift is catchable from either side rather than only from one.
…-off] Splitting detection from the sync is what roles/ssh already does for the LAN alias sync, and its comment records the reason: a host without the 1Password CLI genuinely cannot do this and should be skipped, while a host that has op and still fails is a real error. Hard-failing on a merely absent op puts linux-migration Task 7's 'two consecutive runs, zero failed tasks' convergence out of reach. That split was unnecessary while these tasks sat in the Darwin-guarded osx role, because the Brewfile guaranteed op on every Mac that ran them. Moving them into the unguarded claude role removed the guarantee without replacing it, so a non-work host lacking op would abort the LAST role in main.yml partway through, taking the planwright marketplace and plugin installs below it with them, on every subsequent run. Also drops an over-citation: the task comment claimed D-6 in specs/pair-flow/design.md as the authority for server using gemini. D-6 predates this host and names only work, personal and alt. It is background here, not authority; the current mapping lives in the repo CLAUDE.md.
Batched documentation corrections from the review pass. Each was a statement this branch made false, or left false: - The 'Review backends' section claimed the profile 'is not a per-run choice', then documented two per-run overrides in the next paragraph, and silently dropped the three --backends-only backends. It also asserted parity with playbook.sh and ollama.fish while omitting their alt branch, and asserted it twelve lines before noting the fallback deliberately differs from ollama.fish. Both claims are now accurate and the one real divergence is named once. - The Linux install rationale documented an ordering constraint that measurement showed does not exist; it now records what was measured and why the earlier split was abandoned, so the dead end is not re-walked. - The tag story was half told: the sync carries osx AND linux tags, which is the only place a platform tag names a task outside its platform role. Both the consequence and the missing 'mise run claude' task are now stated. - Two machine-local table rows had gone stale: 'host' gained the two review commands as readers, 'op-service-account-token' gained the gemini sync. README gained the token row it never had, including how each of the two consumers degrades without it. - The vault section spoke of one item and one script; there are now two of each. Records the blast radius that creates, and that moving an item between vaults reassigns its id. - gemini.fish still described itself as the personal/alt read side. - The --skip-trust note asserted --approval-mode plan compensates for the bypassed folder-trust gate. A direct test on 0.54.4 (a project .gemini/settings.json MCP server, run under --skip-trust --approval-mode plan) did not execute it, so the section now reports what was measured rather than what was assumed in either direction.
Seed material for /spec-draft, not acted on here: the mise ls --missing gate being blind to undeclared tools, claude-mcp-sync-github.sh still being Darwin-only while roles/claude went cross-platform, and roles/claude having neither a mise task nor a CI matrix entry.
…g [pending-sign-off] Four findings from the review pass, applied together on your direction because they share one file and one class. All were latent rather than active: none fires on this host today. - An EMPTY token file passed the -f test and the mode check, and the resulting empty OP_SERVICE_ACCOUNT_TOKEN switched OFF the desktop-app path that would otherwise have worked, so a Mac failed with an error naming the item and the vault while the real fault was a placeholder file. Now -s, with a message that names the file. - An unreadable token file (mode 600 but root-owned, which is what creating it under sudo leaves) aborted on cat's own status under set -eu, ending the run with a raw 'Permission denied' and none of this script's FAILED: convention. Both reads now route through fail(). - The token was exported process-wide, so it was inherited by every later child including the sh -c that holds the plaintext API key, which carried both secrets at once. It is now scoped to the single op call via an op_get helper, which is what this file's own header already claimed. - The fast path returned OK without asserting the key file's mode. chmod 600 ran only on the slow path, so a file with the right bytes at the wrong mode stayed loose forever, since no later run rewrites matching content. Reachable on exactly this host: the fish snippet read that path long before anything wrote it, and the gemini CLI creates ~/.gemini itself at 0755. Validated with a four-case table against the real script (empty, whitespace-only, bad mode, absent) plus an end-to-end regression for the last one: chmod 644 the key file, run, observe OK and the file back at 0600.
…ending-sign-off] The token-file block in claude-gemini-auth-sync.sh was copied from this script, so the empty-file and unreadable-file gaps the previous commit fixed were inherited from here rather than invented there. Fixing only the copy is the two-site drift that made the duplication worth flagging in the first place. Scoped to those two guards. This script's export stays process-wide, because it makes a single op call with no second secret in flight, so the narrowing that mattered for the gemini script buys nothing here. Its own harness still passes 18/18 (scripts/ssh-lan-config-sync-test.sh), which covers the happy path, idempotent re-run, unresolved reference, symlink refusal and unparseable render. The four new token cases were checked against this script too.
…g-sign-off] Both commands invoke gemini with --skip-trust, which is unavoidable for a headless run: without it the CLI downgrades --approval-mode plan to default and then aborts. That leaves the folder-trust gate off inside a working tree, and /code-review's tree is someone else's checked-out PR. The prompt already reaches the model on stdin, so neither command ever needed the tree as its cwd. Running from a scratch directory means --skip-trust has nothing to trust: no .gemini/settings.json, project hooks, skills or GEMINI.md can load from the reviewed code at all. Explicitly defence in depth, not a patched hole. A direct test on gemini-cli 0.54.4 -- a .gemini/settings.json declaring an MCP server whose command writes a marker file, run under --skip-trust --approval-mode plan -- did NOT execute it. The earlier claim that this was drive-by code execution did not survive reproduction, and the note says so rather than implying a fix for a live vulnerability. Also drops GEMINI_CLI_TRUST_WORKSPACE=true as a suggested equivalent: the flag is per-invocation, while the env var trusts every directory for every later gemini run in that shell.
…n [pending-sign-off] Second review pass, against the previous two commits' own fixes. - The unconditional 'chmod 600' added to the fast path turned the steady state into a hard failure on a host where the key file is owned by someone else: content matches, so the old code printed OK, and an EPERM there fails the Ansible task and aborts the last role in main.yml on EVERY run. That is the failure the op probe was added to prevent, re-entered through a different door. It now chmods only when the mode is actually wrong, and says to check the owner when it cannot. - The directory got the same treatment, which the file-mode fix's own comment argued for and then did not do: the gemini CLI creates ~/.gemini at 0755, and on a converged host the slow path never runs again to correct it. - The whitespace guard did not guard. '$(cat)' strips trailing newlines and nothing else, so a file holding ' ' yielded a non-empty token that reached op and came back as 'failed to parseToken, format is invalid' -- an error about the token's shape when the fault is a placeholder file. Both scripts now test a whitespace-stripped copy while passing the real token through untouched. The first pass's test asserted only that SOME FAILED: line appeared, so it passed spuriously; the case list now asserts the message. - '-e' without a regular-file test captured a directory at the token path, failing with a confusing 'is mode 755'. - op_get was a no-op when a caller had already exported the token, so the export path kept the behaviour the helper exists to remove. The exported copy is now hoisted into op_token and unset, making op_get the only route. - The temp-file trap covered EXIT only, so an interrupt between write and rename left a plaintext key at ~/.gemini/.api-key.XXXXXX that nothing would ever clean up. Now EXIT INT TERM HUP. Validated with a 13-case table (three whitespace shapes across both scripts, directory-at-path, converged 600, loose 644, loose 755 dir, trap contents, no leftover temps) plus the sibling's own harness at 18/18.
…'s own claims [pending-sign-off] Second review pass, against the previous commits' documentation. The scratch-directory guidance relocated the trust rather than removing it. It said to cd to 'the directory holding $prompt_file', and the only prompt-file convention either command defines puts that in /tmp -- world-writable, so any local user can pre-plant /tmp/GEMINI.md or /tmp/.gemini/settings.json and have a --skip-trust run load it, persistently, across every later review. Both commands now require a fresh 'mktemp -d'. They also now use a subshell: this session's shell keeps its cwd between tool calls, so a bare cd would have stranded every later git and gh step of the review outside the repo, including /code-review's checkout restore. The limits are stated too, since the old wording implied more than it delivered: user-level ~/.gemini/ config loads regardless of cwd, and plan mode still lets the model read tree files by absolute path. Corrections to claims the last pass introduced or left standing: - 'Both scripts address their item by id' was false. ssh-lan-config-sync.sh uses the item NAME via op:// references, so a vault move does not touch it. - 'Two consequences' had grown to three bullets. - 'this one Claude task' undercounted the four tasks carrying the osx+linux tags. - The README token row had both halves of its degradation story backwards: the probe skips on op being ABSENT, not on the token being absent, and a Mac with an unlocked desktop app needs no token file at all. - The CLAUDE.md resolver paragraph again claimed parity with playbook.sh while being deliberately stricter than it. It now says so, and names the latent bug that strictness avoids: playbook.sh tests mere existence, so a touched alias file there produces 'ansible-playbook -l ""', which Ansible reads as no limit and runs every inventory host against the machine. Left unfixed on purpose; this change has no business editing the playbook entrypoint. - panel-review's Maintenance clause cited /code-review 'step 1b', which is the Slack step; the mirror is step b. - The two files' gemini flag order had drifted apart again. - The skip notice said the key file was 'left as-is' and then that the variable would be unset, without connecting the two. - The skip notice now carries the same CI gate as the task it reports on, so a CI runner stops printing an auth warning about commands it never runs.
Third review pass. The correctness lens came back clean; these are the six remaining text defects, all introduced by the two preceding passes. - CLAUDE.md said two resolver clauses were dropped in the first cut; three were. Both command files already said three. DOTFILES_HOST_FILE was the third. - The tag-rationale comment in roles/claude/tasks/main.yml still said 'both tasks' after the probe and skip notice took the count to four. This is the same miscount the previous pass fixed in CLAUDE.md and did not chase into the comment that introduces the rule, or into the observation entry, which said two as well and called a directory-creation task a 1Password one. - panel-review left its prompt file in /tmp while telling the operator to cd elsewhere, and cited that convention as the reason for mktemp -d. code-review had already moved the prompt into the scratch directory, so two files that must mirror prescribed different homes for the same artifact. panel-review now matches, which also fixes the prompt file's mode: it carries the entire diff of a private repo and was created world-readable in /tmp. - panel-review's reciprocal sync pointer said 'step b'; /code-review refers to that block as step 5b in its own text. - The supersession note on the decouple-review-tooling observation sat inside the 'Open decision' section only, so three other passages still asserted PANEL_REVIEW_PROFILE as the current profile mechanism. Moved to the header where it scopes them all.
…g-sign-off] Dogfood pass: /panel-review's own gemini backend, run against the diff that changes /panel-review. It found four things nine Claude lens agents across four polish iterations did not, three of them introduced by those iterations' fixes. - The gemini bullet spends a paragraph explaining that a /tmp prompt file exposes the whole diff of a private repo, and the Ollama block five lines below it kept doing exactly that. Both now use a 0700 mktemp -d, which also settles the collision case the $$ namespacing was there to make unlikely. - The mktemp -d guidance added two commits ago never cleaned up, so every review leaked a directory containing the full diff. Both commands now trap. - The token hoist added two commits ago validated the file path and let a caller-supplied OP_SERVICE_ACCOUNT_TOKEN bypass the whitespace guard entirely. Both scripts now hold both routes to the same bar. - op_err was mktemp'd once per loop iteration and removed only on the normal paths, so an interrupt during any op call leaked a file with nothing to clean it up. It is now created once, before the loop, under a trap. Plus the sibling-drift item the panel raised independently after it was scoped out of an earlier round: ssh-lan-config-sync.sh now scopes its bearer token per-call through op_run, matching op_get. That script spawns sed, grep, cmp, chmod, mv and an 'ssh -G' after the injection, and an exported token sat in the environment of every one of them. Validated: 11-case table (both scripts' caller-token guard, op_err trap and single mktemp, op_run wiring and absent export, both docs' prompt/scratch handling, end-to-end sync), the sibling harness at 18/18, and the full gate set green.
…-off] Panel iteration 2, against iteration 1's own fixes. - The scratch trap covered EXIT only, in the same commit that widened claude-gemini-auth-sync.sh's trap to INT TERM HUP for exactly this reason. Ctrl-C during a multi-minute backend call is the likeliest way one of these runs ends early, and it would have left the full diff behind. - mktemp -d's failure was unchecked, so a failure left $scratch empty and everything after it operated on the wrong path. Both apply to all three scratch blocks across the two commands. The panel's third finding (sed's pipe delimiter against a $VAULT containing a pipe, in ssh-lan-config-sync.sh) is declined: pre-existing, untouched by this branch, and the backend rated its reachability very low itself.
Surfaced by running /panel-review --nested against a branch that changes /panel-review: ~/.claude/commands symlinks to the main checkout, so the skill loader served main's copy. Same for the mise conf.d pin. Seed material for /spec-draft, not acted on here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Installs the Gemini CLI on the Linux host, which had the fish snippet exporting
GEMINI_API_KEYbut nothing that installed the binary or wrote the key, and fixes the reason the work Mac reached for gemini on every review: both/panel-reviewand/code-reviewresolved the machine profile fromPANEL_REVIEW_PROFILEalone, defaulting topersonal, and nothing in this repo ever sets that variable. They now resolve the dotfiles inventory alias, the same indirectionscripts/playbook.shandollama.fishalready use.How to review: start with
CLAUDE.md's new "Review backends: codex vs gemini" section, which is the map. Thenroles/claude/tasks/main.yml(the key sync moving out of the Darwin-guarded role) andscripts/claude-gemini-auth-sync.sh(the headless service-account path). The two command files carry the resolver./polish --nested(4 iterations, converged),/self-review(published this PR),/panel-review --nested(2 iterations, gemini backend)git revertaway.Four things worth flagging before you read the diff.
The design rationale in the first cut was wrong, and measuring killed it. I split the version pin from its install across two roles to work around mise's npm backend needing node on PATH before
roles/environmentsinstalls it. That constraint does not exist: with a throwawayMISE_DATA_DIRand node both uninstalled and absent from PATH,mise install npm:<pkg>succeeds, because mise bootstraps its own node.geminiis now one line inlinux_mise_toolsand the two bespoke tasks are gone.The security finding did not survive reproduction. A review agent reported that
--skip-trustgives drive-by code execution inside a reviewed PR via.gemini/settings.json. I built that exact payload; it was never executed, and the chunk filenames the agent cited do not exist in this install. What survives is a real but lower-severity point: the CLI never needs the untrusted tree as its cwd, since input arrives on stdin. Both commands now run it from a freshmktemp -din a subshell.The review loop repeatedly caught its own fixes. The fast-path
chmodadded in one pass would have failed a converged run on a root-owned key file; the whitespace guard added in another did not catch whitespace, and the test that "verified" it passed spuriously by asserting only that someFAILED:line appeared. Both are fixed, with the case lists tightened.And the branch was dogfooded through the thing it changes.
/panel-review --nestedran its own gemini backend against this diff, using the mise-installed binary, the new alias resolver, and the newmktemp -d+ subshell +--skip-trustinvocation. Two findings are worth calling out. Its pre-flight correctly stopped withGemini CLI not installed; mise run linux will install it— because~/.config/mise/conf.d/linux.tomlsymlinks to the main checkout, so the pin is not live until this merges. That is the right behaviour and the corrected remediation string. And it found four defects nine Claude lens agents across four polish iterations had missed, three of them introduced by those iterations' own fixes — including that the gemini bullet spends a paragraph warning that a/tmpprompt file exposes the whole diff of a private repo while the Ollama block five lines below did exactly that. A non-Anthropic model noticed that my own prose indicted the code beneath it. That is the variance the panel exists for.Worth knowing for next time: a command change cannot be dogfooded through its own slash command from a worktree, because
~/.claude/commandssymlinks to the main checkout. The run above followed this branch's instructions manually.Audit record — 4 polish iterations, converged
Lens coverage
altregression, empty-alias-file, undeclared-tool gate, chmod regression, whitespace guard,-ewideningcatunderset -eu; trap on EXIT onlymise runalone gave a working backend; temp-file leakAuto-applicable
Agent-resolvable
Needs sign-off
linux_mise_tools, deleted both tasks3cd9d25altbranch, tested[ -f ], ignoredDOTFILES_HOST_FILE50977dfop --versionprobe + skip notice, mirroringroles/ssh0bfeebe9ae844f62870b9--skip-trustruns with folder trust off inside a working treec0f7003ed7b19b/tmp, which is world-writable, and leaked cwdmktemp -d+ subshell; six doc corrections59b90b0/tmp; scratch dirs leaked; caller-supplied token skipped the whitespace guard;op_erruntrapped; ssh script still exported globally9dc0eecEXITonly andmktemp -dwas unchecked|| exit 1+EXIT INT TERM HUPon all three blocks58a0d8cNeeds human judgment
--skip-trustpostureDeclined log
/tmpprompt file world-readable (Ollama backends)scripts/has no shared-helper convention; recorded rather than inventedssh-lan-config-sync.shstill exports the token process-wideopcall with no second secret in flightspecs/pair-flowD-6 not formally amendedBashtool runs bashplaybook.shaccepts an empty alias file (-l ""= no limit)CLAUDE.mdclaude-gemini-auth-sync.sh; CI reaches none of the new tasksPending sign-off
linux_mise_toolsinstead of a two-role split · commit3cd9d25git revert 3cd9d25altbranch, empty-file guard andDOTFILES_HOST_FILE· commit50977dfgit revert 50977dfopbefore running · commit0bfeebegit revert 0bfeebe9ae844fgit revert 9ae844fssh-lan-config-sync.sh· commit62870b9git revert 62870b9c0f7003git revert c0f7003ed7b19bgit revert ed7b19bmktemp -din a subshell, plus six doc corrections · commit59b90b0git revert 59b90b0/tmp, scratch cleanup, caller-token guard,op_errtrap, ssh token scoping · commit9dc0eecgit revert 9dc0eecmktemp -dguarded and scratch trap widened toINT TERM HUP· commit58a0d8cgit revert 58a0d8cPass summary
/polish --nested× 4 iterations (converged),/self-reviewstandalone for publication, then/panel-review --nested× 2 iterations against the gemini backend (dogfood).mktemp -d, prompt inside it, subshellcd,--skip-trust --approval-mode plan, prompt on stdin). Output was a clean markdown table both times with no reasoning-trace leakage. Panel iteration 1 surfaced 4 findings + 1 re-raise, all applied; iteration 2 surfaced 3, two applied and one declined as pre-existing.command -v geminiwith the corrected Linux remediation string, because the mise pin is not live until merge. The backend was then invoked through its install path directly, the only deviation from the documented invocation.origin/main@cf22d10, pinned at pre-flight.worktree-geminiis not aplanwright/<spec>/task-<ids>branch and this work is not a declared task in any bundle, so the Agent-resolvable bucket was unavailable for the whole run. Declared rather than stretchinglinux-migration's brief to cover it.yamllint,ansible-lint(production profile),ansible-playbook --syntax-check,shellcheck --severity=warning,gitleakswith the repo config,skill-contracts.sh.scripts/ssh-lan-config-sync-test.sh18/18 after every change to that script.mise ls --missingon an undeclared tool; npm backend with node absent from PATH; npm backend with a throwawayMISE_DATA_DIR; declared-tool re-install convergence; the--skip-trustMCP payload; token/permission guards (13 cases across both scripts); end-to-end Gemini API call.none; panel iteration 2's residual was two one-line consistency fixes plus one declined pre-existing item. No safety condition fired. Two hard pauses (zone screen) resolved by human direction.~/.claude/commandssymlinks to the main checkout, so/panel-reviewinvoked as a slash command served main's copy, not this branch's. The run followed this branch's instructions manually. A command change cannot be dogfooded through its own slash command from a worktree./spec-draft.~/.claudeand~/.config/mise.tomlinto a temporary directory, so it must be run from the main checkout after merge.