Skip to content

feat(cli): add Gemini CLI as a build target and registry citizen - #623

Draft
tkman59 wants to merge 23 commits into
mainfrom
feat/511-gemini-flat-build-target
Draft

feat(cli): add Gemini CLI as a build target and registry citizen#623
tkman59 wants to merge 23 commits into
mainfrom
feat/511-gemini-flat-build-target

Conversation

@tkman59

@tkman59 tkman59 commented Aug 12, 2026

Copy link
Copy Markdown

🎯 What & why

Gemini CLI users had no supported path to AIDD. This adds gemini as the sixth tool: a flat build target, and the full install/uninstall/update/restore/doctor/status lifecycle.

It also closes a multi-owner bug that gemini would have made worse, and that already affected codex.

🛠️ How it works

The build target declares all six artifact kinds. Skills go to .agents/skills/ — the same shared tree codex writes — agents to .gemini/agents/, and MCP, hooks and the AGENTS.md context wiring merge into .gemini/settings.json through a single authority module, since a real Gemini user already owns that file. aidd-orchestrator is excluded: it is built on Claude Code's plugin activation and GitHub Action, so shipping it would ship a plugin that cannot run. That needed a general exclusion mechanism on the build contract rather than a per-tool branch.

Two tools writing one tree is the failure co-ownership cannot survive, so it is eliminated rather than managed. Gemini reuses codex's skills rendering verbatim, and framework-build-golden.e2e.test.ts now asserts gemini's shared paths carry codex's exact hashes.

The uninstall guard was wider than expected. Plugin uninstall deleted tracked files unconditionally, tool uninstall deleted the departing tool's plugin files with no guard at all, and its shared-path computation ignored the surviving tools' plugins entirely. Since skills reach a project through the plugin path, an owners view that skips plugins was the bug, not a subset of it. One function now answers "may this uninstall delete that file" for all three routes, reading a new Manifest.getPathOwners() — the manifest could always express co-ownership, nothing could read it that way. No schema change.

Two opencode literals were removed from the plugin translation path. The built tree's layout comes from the plugins capability, which already resolved to flat and was simply never read; file ownership comes from the tool's directory plus the shared .agents/ root. MCP merging now uses the key each tool declares (mcp for opencode, mcpServers for gemini) instead of assuming opencode's — running one tool's unmerge against the other's file would have left a stray section in a user-owned config. Every opencode translator suite passes unmodified.

🧪 How to verify

  • cd cli && pnpm typecheck && pnpm lint && pnpm test → 2237/2237
  • cd cli && pnpm smoke → 78 pass, coverage 100%, 4 pre-existing failures unrelated to this branch (see Heads-up)
  • Build and run against the real binary:
    cd cli && pnpm build && mkdir -p /tmp/g
    node dist/cli.js framework build --source .. --target gemini --flat --out /tmp/g
    cd /tmp/g && git init -q && gemini skills list --all   # every aidd-* skill Enabled
    Verified against Gemini CLI 0.52.0: 88 skills discovered, 3 agents loaded with no AgentLoadError, AGENTS.md picked up as context.

⚠️ Heads-up

Two install-path gaps, measured and documented, not fixed here. The archive is complete; aidd ai install gemini is not.

  1. .gemini/settings.json is never written by any install path, so an installed-only setup has no hooks and no AGENTS.md wiring. Closing it needs a SettingsCapability change: it supports merge-strategy enums or static content, not the idempotent context.fileName array union.
  2. A plugin installed from a raw local path lands skills one level deeper than the vendor's layout, and the real binary discovers none of them. Cause: translateSkill never consults SkillsCapability.buildInstallPath, unlike agents and rules. This predates gemini and fixing it moves codex's and opencode's flat install output too.

Both are recorded in the plan amendments and in the issue. The README steers users to the archive meanwhile.

Pre-existing failures, not from this branch. pnpm smoke's four corrupt-cache cases fail because setup --plugins recommended now installs aidd-dev, so the fault injection hits "already installed" before any catalog read — published-catalog drift, fixable by picking a non-recommended plugin. And auth status fails locally whenever an AIDD_TOKEN is exported: the e2e sandbox redirects HOME but does not scrub credential env vars.

Scope. Four planned parts are stacked on one branch: the build target, the shared-tree fix, the registry work and the docs. Say the word and I'll split it.

🔗 Linked issue

Closes #511

✅ I certify

  • I DO CERTIFY I READ EACH LINE OF THE PULL REQUEST BECAUSE I AM A SOFTWARE ENGINEER, NOT A AI PUPPY.

🤖 Generated with Claude Code

tkman59 and others added 23 commits August 12, 2026 16:21
Extends AiToolId/AI_TOOL_IDS/FrameworkBuildTarget, adds the gemini
CONFIG_ASSETS entry and settings.json seed, and writes
domain/tools/ai/gemini.ts (agents, skills, mcp, plugins:unsupported)
so the id lands atomically with a registered tool — AI_TOOL_IDS is
the runtime source of truth for "install all tools", so registering
the id without the tool would have crashed real installs.

Phase 1 of part-1 (feat/511-gemini-flat-build-target), pulling
forward what was originally Phase 4 task 1; see the plan's
Amendments for the reasoning and the resulting Phase 4 trim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
domain/formats/gemini-settings-merge.ts becomes the single authority
on .gemini/settings.json: mergeGeminiSettingsHooks translates Claude
hook event names to Gemini's own vocabulary (verified against the
shipped gemini-cli 0.52.0 bundle's hooks-migrate table) and warns on
anything unmapped rather than writing an invalid event name;
mergeGeminiSettingsSeed does an idempotent, order-preserving
context.fileName array union so a pre-existing user value is never
lost. Both preserve every key they don't own, so hooks, mcpServers
(reusing mergeVscodeMcp, no new code needed there) and the seed can
write into the same file in sequence without clobbering each other.

Phase 2 of part-1 (feat/511-gemini-flat-build-target).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets a ToolBuildContract exclude a plugin by name (excludedPlugins),
consumed via a new shouldBuildPlugin(name) predicate on
BuildOutputStrategy — always true for marketplace, contract-driven
for flat — with zero per-tool/per-artifact branching added to either
orchestrator. FrameworkBuildUseCase's plugin loop skips a rejected
plugin with one warn and excludes it from the build result entirely.

This is what will let gemini's flat contract exclude aidd-orchestrator
(Claude-Code-coupled, no Gemini equivalent) in Phase 4 without any
tool-specific branch in the orchestrators.

Phase 3 of part-1 (feat/511-gemini-flat-build-target).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
buildGeminiFlatContract() declares all six artifact kinds: skills
reuse codex's exact .agents/skills/ tree (extracted AGENTS_SKILLS_PREFIX
into flat-paths.ts as a shared constant so this is byte-identical by
construction, not convention), agents are rebuilt to name+description
only (Gemini's strict Zod schema rejects unknown frontmatter keys),
mcp reuses mergeVscodeMcp against the mcpServers key (no shape
transform needed), hooks reuse Phase 2's event-translating merge, and
the settings seed lands via emitConfigArtifact. excludedPlugins
applies Phase 3's mechanism to drop aidd-orchestrator (Claude-Code-
coupled, no Gemini equivalent). No gemini:marketplace row exists.

Wired into deps.ts (gemini:flat registry row), the framework/ai
commands' target lists and help text, the interactive menu hint, and
the CI build matrix (10 cells). biome.json and package.json updated
for the new tool directory/keyword.

Phase 4 of part-1 (feat/511-gemini-flat-build-target). The tool
definition itself landed in Phase 1; see that phase's Amendments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Empirically proved the flat archive against the real gemini-cli
0.52.0 binary in a sandboxed HOME: skills discovered via
`gemini skills list --all`, agents parse cleanly (AgentRegistry
loaded, zero AgentLoadError), both hooks fire with translated event
names, and AGENTS.md is picked up via context.fileName. Standalone
(no codex present), never touching the repo root or real user config.

Adds gemini to the golden framework-build snapshot's FLAT_TARGETS
(9-cell matrix -> 10-cell). Regenerated additively: diffed old vs.
new snapshot to confirm all 9 pre-existing cells are byte-identical,
only the new gemini:flat cell (188 files) was added.

Phase 5 of part-1 (feat/511-gemini-flat-build-target) — the last
phase of this child plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All 5 phases done, success_condition (cd cli && pnpm typecheck &&
pnpm lint && pnpm test) green modulo 2 pre-existing environment-
coupled auth test failures unrelated to this work (documented in the
plan's Amendments). Master plan's Part 1 checkpoint checked, Part 2
(shared tree safety) unblocked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The repair itself is folded into the two commits that opened the gaps,
so this carries the plan and its phases only.

The fold was done by rebuilding the branch with cherry-picks rather
than the interactive rebase the plan names: interactive git is not
available in the environment this ran in. Same end state, verified by
diffing the rebuilt branch against a pre-fold backup — identical but
for the two-line amendment in part-1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing one owner of a co-owned tree destroyed files another owner
still needed. Skills reach a project through the plugin path, so the
plugin routes were the ones that mattered, and none of them was
guarded:

- plugin uninstall deleted every tracked file unconditionally
- tool uninstall deleted all of the departing tool's plugin files with
  no guard at all
- its shared-path computation built the retained set from the surviving
  tools' own files and merge files only, never their plugins, so even
  the route that looked guarded would delete a co-owned tree

One function now answers "may this uninstall delete that file" for all
three, spanning tool files, merge files and plugin files, with the
departing claim expressed as a tool plus an optional plugin name so
whole-tool and single-plugin removals share it. Each retained path is
reported once, naming the owners that still claim it.

Mutation-checked: neutralizing the guard fails 3 of the 5 new cases,
the two that assert deletion staying green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The manifest could already express two tools claiming one path; nothing
could read it that way. Every consumer walked tool-and-path pairs, which
is how one owner came to delete a tree another still needed.

getPathOwners() derives the inverse view on read: path to owners, each
owner carrying how it claims the path (the tool itself, a merge file, or
one of its plugins) and its hash. Hashes travel per owner so a
divergence between two owners of one path stays visible instead of
collapsing into a single entry.

No schema change. The version stays 6 and no migration is added:
co-ownership was always representable, only unreadable.

The uninstall guard now reads this instead of walking tools, merge files
and plugins itself, leaving the departing-claim match as the only logic
in the application layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both reported on a surface they could not see. They walked tool-and-path
pairs and each tool's own directory, so the shared skills tree, which
belongs to no single tool and arrives through plugins, was either
invisible or counted twice.

- a co-owned file now yields one issue, not one per owner
- owners that expect different bytes for one path are reported on their
  own: the file cannot satisfy both, and restoring it will not settle
  the disagreement
- a directory claimed only through plugins no longer looks untracked
- a shared tree abandoned by its last owner is reported orphaned, which
  the per-tool-directory check structurally could not do
- status scans every directory a tool actually claims files in

The golden command-matrix baseline did not move, so nothing was
re-baselined. Mutation-checked against the previous source: 4 of the 6
new doctor cases fail there, the 2 survivors being negative assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two tools rendering different bytes to one shared path is the failure
co-ownership cannot survive. The guards added earlier keep such a file
alive; this keeps it from diverging in the first place.

The golden suite now builds the codex and gemini flat cells and requires
every shared skill path gemini writes to carry codex's exact hash,
naming the first offending path and both hashes on failure. It also
requires gemini to write at least one such file, so the assertion cannot
pass by finding nothing to compare.

Mutation-checked by flipping gemini's skill-name rewrite: the suite
fails naming the diverging SKILL.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The translator asked which tool it was dealing with twice: once to pick
the built tree's layout, once to decide which files in a flat build
belonged to the plugin. Both answers were already derivable.

The layout comes from the plugins capability, which resolves to "flat"
for flat tools and was simply never read. Ownership comes from the
tool's own directory plus the shared .agents/ root, matching a
plugin-namespaced segment anywhere below it rather than at a fixed
depth: gemini's agents sit one level shallower than opencode's skills.

The hooks-skip reason moves onto the capability too, so a tool explains
its own limitation instead of borrowing opencode's.

Gemini declares flat plugins, which it needs to materialize skills and
agents at all. That put it back under a conformance guard demanding a
marketplace probe, so the guard is rekeyed on PluginFormat membership:
writing plugin content and reading a marketplace are different claims,
and only the second needs a probe.

Every opencode translator suite passes unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two tools now share the capability shape that routes plugin MCP merging,
and they keep their servers under different keys: mcp in opencode.json,
mcpServers in .gemini/settings.json. Both sides of the merge assumed
opencode's, so installing or removing a plugin for gemini would have
written a stray section into a user-owned settings file and left the
real one untouched.

The section key now travels with the merge, and the qualification helper
returns it instead of a boolean, which is what forced callers to
re-derive it from the tool name. A tool that qualifies but declares no
section raises a typed error: defaulting there writes one tool's section
into another tool's file.

Collision messages name the config actually being written rather than
always naming opencode.json.

Mutation-checked by pinning the key back: 2 of 4 gemini cases fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gemini joins the smoke tool loop, the e2e install/uninstall matrix, the
flat-target translation matrix, and three new integration suites: flat
materialization, MCP merge, and built-tree materialization.

Smoke goes from 73 to 78 passes at unchanged 100% coverage, so every
per-tool ai leaf command now executes for gemini against the built
binary.

The flat materialization suite records something the plan did not know:
install and the archive do not write to the same places. The archive
namespaces skills as .agents/skills/<plugin>-<skill>/, install nests
them at .gemini/skills/<plugin>/<skill>/. The suite asserts what install
actually does and says it is not the archive's layout, leaving to the
real-binary phase whether a skill is discovered at that depth.

Restore gains no gemini config reference: gemini consumes only the
already-listed config/mcp.json, its settings seed being a bundled asset
rather than a framework-sourced file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nary

Marketplace-sourced install reaches parity with the archive: 88 skills
discovered and enabled, 3 agents loaded with no AgentLoadError.

Two gaps found, both recorded rather than fixed. The raw local-path
install fallback writes skills nested one level deeper than the vendor's
layout, and the binary discovers zero of them; the cause is that flat
skill translation never consults the capability's install path, unlike
agents and rules, which predates gemini and would move every flat tool's
output. And .gemini/settings.json is never written by any install path,
so hooks and the AGENTS.md context wiring are absent.

Also corrects the previous phase's claim that install and the archive
diverge: only the fallback path does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The memory hook rejected `gemini` outright, and eleven reference tables
enumerated five tools. Both now know the sixth.

Every edit is a row added beside an unchanged claude row, except two
hook tables that gain a Gemini column and so rewrite every row without
changing a single claude cell.

Hook event names come from the mapping shipped in `gemini hooks
migrate`, read out of the 0.52.0 bundle: Gemini renames most moments and
folds turn stop and subagent stop onto one event.

The golden snapshot is red from here until the re-baseline phase, which
is what that phase exists for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gemini moves to supported flat in the compatibility table, with an
install block that carries both gating constraints inside it rather than
in a general prerequisites section a reader skips: the 0.28.0 minimum,
below which no skill is ever discovered, and the folder-trust step,
without which Gemini lists nothing and explains nothing.

The block also steers users to the archive rather than `aidd ai install
gemini`, because the install path writes no .gemini/settings.json and so
carries neither hooks nor the AGENTS.md context wiring.

Every version claim states how it was established: the minimum is
derived from the CLI's own source, since the vendor publishes none, and
0.52.0 is what was actually verified.

Five targets and nine build cells become six and ten across the CLI
docs, the architecture rule and the project memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan expected this part's content edits to move the golden snapshot.
They do not: the suite builds from a committed fixture copy of the
plugins, not from the live tree, so regenerating produces a byte-
identical file and the subset invariant still holds.

The fixture is frozen on purpose — that is what makes the golden a test
of build behaviour rather than of plugin content — so it is left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The matrix documents `plugin sync` and `ai sync` runs from May against
v4.1.0-beta.11. Neither command exists in the CLI today, so the ten
gemini pairs cannot be run, and writing PASS beside them would be
inventing results. The document stays as the accurate record of what was
true when it was written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue #511 now carries the verified mapping, the two prerequisites that
silently produce an empty result, and the install-path limitation. The
original mapping is preserved and superseded rather than overwritten.

All four parts are done. Two checkpoints remained unticked and both are
now met: the command matrix is green for gemini with the smoke coverage
gate held at 100%, and a Gemini-only project consumes the archive end to
end against the real 0.52.0 binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lone auth status failure came from an expired AIDD_TOKEN in the
developer's shell, which the e2e sandbox redirects HOME around but does
not scrub. Without it the suite is 2237/2237.

Part 3's chain still does not exit 0: it also runs smoke, whose four
corrupt-cache cases fail on published-catalog drift, not on the
environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): add Gemini CLI as a per-tool build target

1 participant