◉ANTIGRAVITY LABJP
Articles/Integrations
⬡ Integrations/2026-11-06Intermediate

Moving Conductor From a Gemini CLI Extension to an Antigravity CLI Plugin: Leave spec.md and plan.md Alone, Compare Before and After

Conductor is now a plugin instead of a Gemini CLI extension, and it runs in Antigravity CLI too. Your spec.md and plan.md files don't need to move; here is a small script and a short routine to prove nothing changed when you install it.

ConductorAntigravity CLI43pluginsspec-driven developmentmigration16

I had two tracks in flight the week I learned that Conductor had stopped being a Gemini CLI extension and become a plugin. One line, agy plugins install, apparently does it. My hands still hesitated. If a "work in progress" marker in plan.md shifted, or a spec.md quietly changed, I would lose the history of every decision I had made so far.

Here is the short version: there is almost nothing to migrate. Conductor's output lives in your repository, in the conductor/ directory, as plain Markdown. It does not live inside the extension. Google's developer blog post (July 16, 2026) says as much: your spec.md and plan.md "aren't going anywhere." What changes is the thing that reads and writes them.

I still wanted to compare the state before and after, once. The point was less the migration itself than being able to say "nothing broke" and mean it.

What changes and what doesn't

This is limited to what I could confirm from the official blog post and the Conductor repository README.

ItemStays the sameChanges
Where artifacts liveproduct.md, tech-stack.md, workflow.md, tracks.md under conductor/Nothing
Track layoutspec.md, plan.md, metadata.json under conductor/tracks/<id>/Nothing
How you start itThe old slash commands still work, according to the announcementNatural conversation now works too
How you install it—agy plugins install for Antigravity; a marketplace registration for Claude Code
Where the plugin itself lives—~/.gemini/config/plugins/ for a global install, .agents/plugins/ for one project

The plugin and its artifacts live in different places, and that matters. Uninstalling the plugin leaves conductor/ in place, and installing it has no reason to rewrite conductor/. But "has no reason to" is a statement about design, not something I had seen on my own machine. So I built the check first.

A script that compares before and after

It does three things. It records which core files exist, a short hash of each track's three files, and a count of the checkboxes in plan.md ([ ] pending, [~] in progress, [x] done) into a JSON file. Later it takes the same measurement again and compares.

#!/usr/bin/env python3
"""conductor/ の状態を JSON に残し、プラグイン導入の前後で差分を取る。"""
import hashlib
import json
import re
import sys
from pathlib import Path
 
CORE = ["product.md", "product-guidelines.md", "tech-stack.md", "workflow.md", "tracks.md"]
BOX = re.compile(r"^\s*[-*]\s*\[( |x|~)\]", re.M)  # [ ] 未着手 / [~] 作業中 / [x] 完了
 
 
def digest(p: Path) -> str:
    return hashlib.sha256(p.read_bytes()).hexdigest()[:12]
 
 
def snapshot(root: Path) -> dict:
    base = root / "conductor"
    out = {"missing_core": [c for c in CORE if not (base / c).exists()], "tracks": {}}
    for tdir in sorted((base / "tracks").glob("*/")) if (base / "tracks").exists() else []:
        entry = {"files": {}, "boxes": {" ": 0, "~": 0, "x": 0}}
        for name in ("spec.md", "plan.md", "metadata.json"):
            f = tdir / name
            entry["files"][name] = digest(f) if f.exists() else None
        plan = tdir / "plan.md"
        if plan.exists():
            for m in BOX.findall(plan.read_text(encoding="utf-8")):
                entry["boxes"][m] += 1
        out["tracks"][tdir.name] = entry
    listed = (base / "tracks.md").read_text(encoding="utf-8") if (base / "tracks.md").exists() else ""
    out["unlisted_tracks"] = [t for t in out["tracks"] if t not in listed]
    return out
 
 
def diff(a: dict, b: dict) -> list:
    msgs = []
    for t in sorted(set(a["tracks"]) | set(b["tracks"])):
        x, y = a["tracks"].get(t), b["tracks"].get(t)
        if x is None or y is None:
            msgs.append(f"{t}: {'追加' if x is None else '消失'}")
        elif x != y:
            changed = [n for n in x["files"] if x["files"][n] != y["files"][n]]
            msgs.append(f"{t}: 変更 {changed}")
    return msgs
 
 
if __name__ == "__main__":
    mode, root = sys.argv[1], Path(sys.argv[2])
    if mode == "save":
        snap = snapshot(root)
        Path(sys.argv[3]).write_text(json.dumps(snap, ensure_ascii=False, indent=1))
        print(f"tracks={len(snap['tracks'])} missing_core={snap['missing_core']} unlisted={snap['unlisted_tracks']}")
    elif mode == "compare":
        before = json.loads(Path(sys.argv[3]).read_text())
        msgs = diff(before, snapshot(root))
        print("\n".join(msgs) if msgs else "差分なし")
        sys.exit(1 if msgs else 0)

I cut the hash to 12 characters so I can compare it by eye. It isn't doing security work, so that length is enough.

To test it, I built a small dummy conductor/ with two tracks (dark-mode and login-fix) and ran save and compare:

python3 conductor_snapshot.py save . before.json
# tracks=2 missing_core=[] unlisted=[]
 
python3 conductor_snapshot.py compare . before.json
# no differences  (prints 差分なし)
 
# after hand-editing an in-progress item in plan.md to done
python3 conductor_snapshot.py compare . before.json
# dark-mode_20261020: 変更 ['plan.md']   (exit code 1)

The non-zero exit code means you can drop it into a runbook or a shell script. It also means you will see differences whenever you are normally working through a plan, so compare only two points: right before the install and right after it. The output strings are in Japanese; change them if you like, since only the exit code matters to a script.

The routine I follow

  1. Finish the current track to a natural stopping point and get git status clean. If an edited plan.md is still uncommitted, you won't be able to tell afterward whether you or the plugin caused a difference.
  2. Run python3 conductor_snapshot.py save . before.json, and keep before.json outside the repository (your home directory works). Inside the repo, it becomes a difference itself.
  3. Run agy plugins install https://github.com/gemini-cli-extensions/conductor.
  4. Run compare again. You want "no differences" and a clean git status, both.
  5. If something differs, read git diff conductor/. If it's an unintended rewrite, stop there and restore with git checkout -- conductor/.

If step 4 is clean, the main work of the migration is over. What remains is on the usage side: whether you keep using the old commands or switch to talking to the agent.

Pitfalls and a judgment call

Some tracks may be missing from the index. unlisted_tracks catches directories under conductor/tracks/ whose names never appear in tracks.md. Hand-made or copied tracks show up there. It has nothing to do with installing the plugin, so I look at it once while saving, separately from the before-and-after comparison.

Decide whether to keep the Gemini CLI extension. If both routes write to the same conductor/, both can update the checkboxes in plan.md. I let the two coexist for a while, ran one track to completion on the new side, and only then removed the old one. I can't claim that is the right order. The one line I hold firmly is this: never advance the same track from two tools at once.

Token use goes up, and the official README says so. It notes that the spec-driven approach reads a lot of project context and can raise consumption on larger projects. Now that you can drive it by conversation, it is easy to call it casually and let usage creep up.

What to do next

Moving to the plugin turned out not to be a job of moving files. It was a job of confirming the files hadn't moved. Save right before, compare right after. The two runs together take under a minute.

I'd start by picking one in-progress track and running save on it. For related setup work, I wrote up how to carry MCP server settings around in Adding MCP Servers Without Opening the Config File: The agy mcp Subcommand, and how to check that the CLI actually answers before an unattended run in Ask Antigravity CLI Once Whether It Actually Answers, Right Before a Scheduled Run. As an indie developer working on my own apps, I keep this kind of before-and-after check close to hand.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Integrations2026-10-22
The Project-Local mcp_config.json Is Read, but No MCP Server Starts — Checking With a One-File Probe
Antigravity CLI logs that it is using a project-local config, yet the MCP servers in it never start. Based on reports in the official repository, here is what that log line may and may not promise, plus a tiny probe server you can use to check your own setup.
⬡ Integrations2026-10-19
Connecting GitHub MCP to Antigravity CLI: The Fix Was One Word, url to serverUrl
To use GitHub MCP from Antigravity CLI, the url key in your config has to become serverUrl. Based on the official MCP docs, here are the remote and Docker setups, a small script that checks the file before you load it, where to keep the token, and the order to debug in.
⬡ Integrations2026-10-10
Figma MCP or Stitch MCP: Choosing by Whether the Screen Already Exists
When you turn a screen into code, which MCP should Antigravity talk to: Figma or Stitch? One question settles it: does the correct screen already exist? Here is the rule I use, a switching routine, and a small script for the days I can't decide.