feat(plugin): self-contained Gemini CLI extension in plugins/gemini/#2803
Conversation
Mirrors the OpenCode plugin's activity-tracking surface for Gemini CLI: sets 🤖 on BeforeAgent, 💬 on AfterAgent, and clears on SessionEnd via `wt config state marker`. No skills, slash commands, or MCP servers — those can follow if/when there's demand. Refs #2763
Gemini's extension loader treats the manifest's parent as ${extensionPath}
and auto-discovers skills/ and hooks/ beneath it. Moving the manifest to
the repo root lets the existing skills/worktrunk and skills/wt-switch-create
load without duplication, and the hooks.json now invokes
.claude-plugin/hooks/wt.sh instead of carrying its own copy of the shim.
Per maintainer direction in #2765.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror the broader phrasing from .claude-plugin/plugin.json so the extension blurb covers the bundled skills (configuration guidance) in addition to activity tracking — the previous wording only described the markers.
Integrate the Gemini extension with the merged plugin consolidation (#2789). Gemini hard-probes ${extensionPath}/{hooks,skills}/ with no path indirection and `gemini extensions install` copies the extension dir, so the extension must be self-contained and can live anywhere: - move gemini-extension.json + hooks/hooks.json into plugins/gemini/ (own payload dir; repo root stays clean — consistent with #2789) - bundle hooks/wt.sh (byte-identical to plugins/worktrunk/hooks/wt.sh); hooks call ${extensionPath}/hooks/wt.sh, not the cross-dir ../worktrunk path (would break under copy-on-install) nor the .claude-plugin/ path #2789 deleted - skills -> ../../skills symlink reuses the repo-root skills (Gemini accepts symlinked skills — verified via `gemini extensions validate`) - extend test_plugin_layout_is_consolidated: Gemini lives in plugins/gemini/ with nothing at the repo root, hooks reference the bundled shim, and the bundled wt.sh stays byte-identical to the canonical plugins/worktrunk/hooks/wt.sh (drift guard) Verified: `gemini extensions validate` passes; full pre-merge gate green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
A few install-time concerns surfaced from reading the gemini-cli source (google-gemini/gemini-cli, packages/cli/src/config/extension-manager.ts and extensions/github.ts). The local-install path the PR verifies (gemini extensions validate) doesn't exercise these, so flagging in case they're not already factored in.
gemini extensions install <github-url> no longer resolves the manifest
tryParseGithubUrl only accepts owner/repo — no subdirectory component — and cloneFromGit clones to tempDir with localSourcePath = tempDir. loadExtensionConfig(localSourcePath) then reads <tempDir>/gemini-extension.json (the constant EXTENSIONS_CONFIG_FILENAME is "gemini-extension.json", joined directly to the extension dir). With the manifest now at plugins/gemini/gemini-extension.json, a clone of this repo has nothing at <tempDir>/gemini-extension.json, so gemini extensions install max-sixty/worktrunk (or any https://github.com/... form) fails with Configuration file not found at <tempDir>/gemini-extension.json. The previous arrangement in this branch's first commits (manifest at the repo root) made that path work; the move into plugins/gemini/ trades it away.
Today there's no wt config plugins gemini install command yet (per the closed #2765's description), so the only working install path is gemini extensions install /local/path/to/plugins/gemini. Worth confirming this is the intended tradeoff — and probably worth a note in the test docstring or plugins/worktrunk/CLAUDE.md saying "github-direct install is not supported; install via a local path / wt config plugins gemini install (TBD)" so a future contributor doesn't read "self-contained… can live anywhere" and assume the github URL path works.
plugins/gemini/skills -> ../../skills doesn't survive gemini extensions install
copyExtension (called for local, git, and github-release install types — only link skips it) is fs.promises.cp(source, destination, { recursive: true }). With Node's defaults (dereference: false, verbatimSymlinks: false), relative symlinks are rewritten to absolute paths pointing at the resolved target. Reproduction:
$ mkdir -p src/ext skills && echo x > skills/file && ln -s ../../skills src/ext/skills
$ node -e "require('fs').promises.cp('src/ext','dest',{recursive:true}).then(()=>console.log(require('fs').readlinkSync('dest/skills')))"
/abs/path/to/skills
For local installs that's tolerable — the absolute symlink keeps working as long as the worktrunk source repo stays in place (though it silently breaks if the user moves the repo). For git installs (if/once the manifest finds its way to a place Gemini can load it), localSourcePath = tempDir, the symlink gets rewritten to an absolute path inside that tempDir, and the finally block (extension-manager.ts:~440, fs.promises.rm(tempDir, { recursive: true, force: true })) deletes the target before the user's first session. loadSkillsFromDir then fs.stats a broken path, hits ENOENT, and silently returns [] — the skills load to nothing with no error surfaced.
Not blocking for the local path, but if wt config plugins gemini install ends up wrapping gemini extensions install <url>, the skills the manifest description advertises ("configuration guidance…") won't actually be there. The drift guard already enforces wt.sh byte-identity to handle the same problem for the shim — for skills, the equivalent would be copying the SKILL.md trees into plugins/gemini/skills/ at build/release time rather than relying on a symlink that fs.cp mangles.
Docs drift
CLAUDE.md → "Plugin Layout" still reads "One plugin payload serves both tools, in plugins/worktrunk/. The repo root keeps only the two loader-mandated marketplace pointers…" — no mention that Gemini now has its own sibling under plugins/gemini/. Worth a sentence so the section matches the new layout this PR introduces; plugins/worktrunk/CLAUDE.md (scoped to Claude+Codex) can probably stay as-is.
worktrunk-bot flagged that moving the manifest under plugins/gemini/ trades away github-URL install, and that gemini's fs.cp rewrites the relative skills symlink to an absolute path (silently empty on a git/URL install). Both are accepted tradeoffs for the supported local-path install; document them so a future contributor doesn't read "self-contained" and assume the github-URL path works: - CLAUDE.md "Plugin Layout": new paragraph describing plugins/gemini/, the bundled drift-guarded wt.sh, and the two install constraints - test_plugin_layout_is_consolidated docstring: same constraints + the robust fix (release-time SKILL.md bundling) tracked with the wt config plugins gemini install follow-up No code change — the reviewer agreed none is blocking for the local path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supersedes #2765 (the original branch was based on a pre-consolidation main and its
${extensionPath}/.claude-plugin/hooks/wt.shreference was deleted by #2789 — see "Why a new PR" below).Adds a Gemini CLI extension for
wt listactivity tracking, integrated with the consolidated plugin layout from #2789.Design
Gemini's loader has no path indirection:
gemini-extension.jsonis{name, version, description}only — Gemini hard-probes${extensionPath}/hooks/hooks.jsonand${extensionPath}/skills/relative to wherever the manifest sits. Andgemini extensions installcopies the extension directory. So the extension must be entirely self-contained re: its hooks/shim. It is not freely relocatable, though:gemini extensions install owner/repoclones and reads<clone-root>/gemini-extension.json, so with the manifest underplugins/gemini/the github-URL install path is unsupported — install is local-path only (gemini extensions install <repo>/plugins/gemini) until awt config plugins gemini installcommand exists. This is an accepted tradeoff of keeping the repo root clean; documented inCLAUDE.md → "Plugin Layout"and the test docstring.The extension lives in its own payload dir,
plugins/gemini/— consistent with #2789's principle (every tool's payload underplugins/, repo root clean). It can't shareplugins/worktrunk/because that dir'shooks/hooks.jsonslot is Claude's (incompatible format).wt.shis bundled (not referenced cross-dir) specifically becauseinstallcopies the dir — a../worktrunk/hooks/wt.shreference would break in the installed copy.wt.shis a generic PATH-shim (exec wt "$@"+ a Windows-Terminal-alias guard), so the duplication is a trivial shim, not logic — andtest_plugin_layout_is_consolidatedasserts the two copies stay byte-identical so they can't silently diverge.Gemini activity tracking is genuinely viable (unlike Codex): Gemini has an
AfterAgentturn-end event, so 🤖→💬 actually works within a session.Why a new PR instead of updating #2765
#2765's branch was never rebased — based on a main predating the codex-hooks removal, the deprecation fixes, and the whole plugin consolidation. Its only content was two repo-root files whose hook command pointed at
.claude-plugin/hooks/wt.sh, a path #2789 deleted. Rebasing + reworking it would have meant a force-push rewriting that branch's history; opening fresh is cleaner.Reviewer navigation
plugins/gemini/— the whole extension (4 files).tests/integration_tests/config_show.rs—test_plugin_layout_is_consolidatedextended: Gemini lives inplugins/gemini/with nothing at the repo root, hooks reference the bundled${extensionPath}/hooks/wt.sh, and the bundledwt.shis asserted byte-identical toplugins/worktrunk/hooks/wt.sh.Verification
gemini extensions validatepasses on the layout (including the symlinkedskills/). Full pre-merge gate green: 3742 passed, 0 skipped, lints clean, no snapshot churn. End-to-endgemini extensions installwas not run non-interactively (it blocks on an interactive trust/settings prompt);validateis the non-interactive proxy.Reviewer (worktrunk-bot) cross-checked the gemini-cli source for the copy path
validatedoesn't exercise:gemini extensions installrunsfs.cp(src, dest, {recursive:true}), which rewrites the relativeskills -> ../../skillssymlink to an absolute path at the resolved target. Consequence: a local install resolves skills only while the source repo stays in place; a git/URL install would point the symlink into the deleted clone tempdir and skills would silently load as empty. Non-blocking for the supported local path; the robust fix is below.Follow-ups (not in this PR)
wt config plugins gemini installcommand (the install path is currentlygemini extensions install <local>/plugins/gemini; the github-URL flow has no subdir syntax). This command should also bundle copied SKILL.md trees intoplugins/gemini/skills/at install/release time (drift-guarded likewt.sh) so skills survive Gemini's symlink-mangling copy — the durable fix for thefs.cpissue above.skills/wt-switch-createis exposed to Gemini via the shared symlink but its body invokes Claude'sEnterWorktreetool — same accepted tradeoff as Codex, documented inCLAUDE.md → "Plugin Layout".🤖 Generated with Claude Code