# Ground Control: From Google AI Studio Prototype to Local Production with Antigravity 2.0

> Source: <https://dev.to/estherirawati/ground-control-from-google-ai-studio-prototype-to-local-production-with-antigravity-20-1acd>
> Published: 2026-07-10 16:13:24+00:00

This guide targets

Antigravity 2.0— the

four-surface release (desktop app,`agy`

CLI,`google-antigravity`

SDK, and

enterprise cloud) that shares one agent harness — together with theAI Studiothat shipped alongside it. The SDK is pre-v1.0; symbol

lifecycle export

names, bundle fields, and CLI flags below reflect the documented API as of

mid-2026. Treat thepatternsas stable and re-check exact signatures against

the current docs before you ship.

Every AI engineering team knows this moment. Someone spends an afternoon in

Google AI Studio and comes back with something genuinely impressive: a

multi-agent app that classifies incoming support tickets, investigates the

customer's history, checks refund policy, and drafts a resolution — with typed

handoffs between agents and a sandbox transcript to prove it works. The demo

kills. Leadership asks the only question leadership ever asks: *"When can this
be in production?"*

And then the project stalls, because between the sandbox and production sits a

canyon. On one side: a cloud playground optimized for iteration speed — prompts

in text boxes, tools declared in a UI, model parameters behind a slider, test

runs you eyeball. On the other side: everything production actually demands —

version control, typed contracts, security policy, audit logs, CI, and tool

implementations that hit *your* databases, not a mock.

Historically, teams escaped the canyon in one of two bad ways:

Both failure modes share a root cause: **the prototype was never an artifact.**

It was a pile of UI state, and UI state doesn't survive contact with a

repository.

Antigravity 2.0 closes the canyon from both ends. AI Studio's **lifecycle
export** turns the entire prototype — agent architecture, prompts, tool

`agy`

CLI`google-antigravity`

This tutorial walks the full bridge, end to end, with a real workload:

**TriageDesk**, a four-agent support-ticket triage system. We'll prototype it

in AI Studio, export it in one click, scaffold a local workspace with exact

terminal commands, and then wire it into a production execution harness with

typed contracts, default-deny policy, and a parity gate that replays the

sandbox transcripts as regression tests.

The word doing the heavy lifting in "lifecycle export" is *lifecycle*. It is

not a chat log, and it is not "the prompt." An agent lifecycle is the complete,

declarative description of a multi-agent system — everything needed to

reproduce its behavior, and nothing tied to the surface it happens to run on:

| Lifecycle component | In the AI Studio sandbox | In your local codebase |
|---|---|---|
Agent architecture |
Nodes on the app canvas |
`lifecycle.json` manifest (agents + edges) |
Prompts |
System-instruction text boxes |
`prompts/*.md` , one file per agent, checksummed |
Tool configurations |
Function declarations in the UI |
`tools/tools.json` (interfaces) + your Python implementations |
Handoff contracts |
Structured-output schemas |
`schemas/handoffs.json` → Pydantic models |
Generation parameters |
Temperature / top-p sliders | Pinned in the manifest, applied by the harness |
Evidence |
Sandbox test runs you eyeballed |
`eval/golden.jsonl` → replayable parity tests |

Read that table column by column and you can see the whole thesis of this

article: **every piece of sandbox state has a canonical home in a repository.**

The export is a projection from one column to the other. Nothing is

reconstructed from memory; nothing is lossy.

```
   GOOGLE AI STUDIO (cloud sandbox)                LOCAL WORKSPACE (production)
  ┌──────────────────────────────┐               ┌──────────────────────────────┐
  │  canvas: 4 agents, 2 edges   │               │  bridge/harness.py   policy  │
  │  prompts in text boxes       │   1-click     │  bridge/lifecycle.py loader  │
  │  tool declarations (UI)      │   export      │  bridge/tools.py     impls   │
  │  output schemas (UI)         │ ───────────▶  │  bridge/schemas.py   typed   │
  │  temp/top-p sliders          │  .agy bundle  │  bridge/orchestrator.py      │
  │  sandbox test transcripts    │               │  bridge/parity.py    gate    │
  └──────────────────────────────┘               └──────────────────────────────┘
                 ▲                                              │
                 │            agy studio pull / diff            │
                 └──────────────── parity ◀────────────────────┘
```

The reason "rewrite from screenshots" fails isn't laziness — it's that agent

behavior is *absurdly* sensitive to details humans don't transcribe faithfully.

A prompt that lost a trailing instruction in copy-paste. A temperature of 0.7

where the sandbox ran 0.2. A tool schema where `priority`

silently became a

string instead of an enum. Each one changes behavior; none of them shows up in

a code review, because there's no "before" to diff against.

So we treat parity as a **contract with four clauses**, each mechanically

checkable:

`Resolution`

disagrees with the sandbox `Resolution`

on a golden
ticket, the deploy gate fails.Everything we build in Steps 3 and 4 exists to enforce one of those four

clauses. Keep the table and the contract in your head; the rest is

implementation.

This is a tutorial about *leaving* the sandbox, so we'll keep the sandbox part

brief — but the shape of the prototype matters, because a well-structured

prototype exports cleanly and a mushy one doesn't.

In AI Studio, create a new **multi-agent app** and lay out four agents:

`TicketClass`

:
category (`billing`

/ `technical`

/ `account`

/ `other`

), severity, and a
one-line summary.`TicketClass`

; declares two
function tools, `search_tickets`

(prior tickets from this customer) and
`read_account`

(plan, billing history). Emits an `Investigation`

.`read_policy`

. Emits a `PolicyVerdict`

—
what the refund/SLA policy permits for this category.`Investigation`

and `PolicyVerdict`

and emits the final `Resolution`

: an action, a customer-facing reply draft,
and an `escalate`

flag with rationale.Wire the edges on the canvas: Classifier fans out to Investigator and Policy

Checker (parallel branch), which join into Resolver. A diamond.

Three habits at this stage pay for themselves at export time:

`schemas/handoffs.json`

in the bundle, and typed
handoffs are what make the local orchestrator deterministic. An agent
without an output schema exports as a prose-emitter, and you'll pay for it
later with regex.`eval/golden.jsonl`

:
your regression suite, for free.Tune the temperature down to 0.2 for the Classifier and Policy Checker (you

want determinism), leave the Resolver a little warmer for reply drafting, and

iterate until the diamond behaves. That's the prototype. Total UI time: an

afternoon. Now let's make it an artifact.

In the app's toolbar: **Deploy ▸ Export lifecycle**. AI Studio bundles the

entire app definition into a single archive — `triagedesk.agy.zip`

— and offers

two delivery paths: a direct download, or a push to your project's export

registry that the CLI can pull by app ID (we'll use both in Step 3).

Unzip it and look around; this bundle is the contract between the two worlds:

```
triagedesk.agy.zip
├── lifecycle.json            # the manifest — architecture, params, checksums
├── prompts/
│   ├── classifier.md         # byte-exact system instructions, one per agent
│   ├── investigator.md
│   ├── policy_checker.md
│   └── resolver.md
├── tools/
│   └── tools.json            # function DECLARATIONS (JSON Schema) — no impls
├── schemas/
│   └── handoffs.json         # structured-output contracts between agents
└── eval/
    └── golden.jsonl          # your approved sandbox runs: input → typed output
```

The manifest is the piece worth reading line by line:

```
{
  "format": "antigravity.lifecycle/v1",
  "name": "triagedesk",
  "exported_from": "aistudio://apps/triagedesk-8f3a21",
  "exported_at": "2026-07-08T14:32:00Z",
  "model_defaults": {
    "model": "gemini-3-pro",
    "generation": { "temperature": 0.2, "top_p": 0.95, "max_output_tokens": 2048 }
  },
  "agents": [
    { "id": "classifier",
      "prompt": "prompts/classifier.md",
      "tools": [],
      "output_schema": "schemas/handoffs.json#/TicketClass" },
    { "id": "investigator",
      "prompt": "prompts/investigator.md",
      "tools": ["search_tickets", "read_account"],
      "output_schema": "schemas/handoffs.json#/Investigation" },
    { "id": "policy_checker",
      "prompt": "prompts/policy_checker.md",
      "tools": ["read_policy"],
      "output_schema": "schemas/handoffs.json#/PolicyVerdict" },
    { "id": "resolver",
      "prompt": "prompts/resolver.md",
      "tools": [],
      "generation": { "temperature": 0.6 },
      "output_schema": "schemas/handoffs.json#/Resolution" }
  ],
  "edges": [
    { "from": "classifier", "to": ["investigator", "policy_checker"], "mode": "parallel" },
    { "from": ["investigator", "policy_checker"], "to": "resolver", "mode": "join" }
  ],
  "checksums": {
    "prompts/classifier.md":     "sha256:1f9c2e…",
    "prompts/investigator.md":   "sha256:aa07d1…",
    "prompts/policy_checker.md": "sha256:83b4f0…",
    "prompts/resolver.md":       "sha256:c95a72…"
  }
}
```

What to notice, because each field maps to a parity clause from Section 2:

`model_defaults`

+ per-agent `generation`

overrides`checksums`

`agents[].tools`

+ `tools/tools.json`

`read_account`

with hand-typed JSON;
your production `read_account`

will hit the real billing database with real
credentials that never touch a bundle file.`edges`

`eval/golden.jsonl`

`{agent_inputs, expected_output}`

pairs.One click, and the prototype stopped being UI state. It's an artifact. Now we

bring it home.

Everything in this section is exact terminal commands. First, the two surfaces

you'll use locally — the **CLI** (`agy`

, the Go-based terminal harness) and the

**SDK** (`google-antigravity`

, the same harness as a Python library):

```
# 1. Install the Antigravity CLI (macOS / Linux). It lands at ~/.local/bin/agy
curl -fsSL https://antigravity.google/cli/install.sh | bash

# Windows PowerShell:  irm https://antigravity.google/cli/install.ps1 | iex

# 2. Confirm the harness is live and see which models you can reach
agy models

# 3. Authenticate. Either a direct Gemini key…
export GEMINI_API_KEY="your_key_here"
#    …or route through Vertex AI with Application Default Credentials:
#    gcloud auth application-default login   # then set VERTEX=1 in .env
```

`agy init`

with the `studio-import`

template creates a workspace pre-shaped for

an ingested lifecycle — an `export/`

directory the bundle lands in, a `bridge/`

package for the production wiring, and a `.agy/`

config that tells the harness

where the manifest lives:

```
# 4. Scaffold the local IDE workspace
agy init triagedesk --template studio-import
cd triagedesk

# 5. Python environment + SDK. IMPORTANT: install the published wheel, not a
#    source checkout — the wheel ships the `localharness` binary the SDK drives.
python -m venv .venv && source .venv/bin/activate
pip install google-antigravity pydantic python-dotenv
```

Two equivalent paths. If you downloaded the zip:

```
# 6a. Ingest the downloaded bundle into ./export/triagedesk/
agy studio import ~/Downloads/triagedesk.agy.zip
```

Or skip the download entirely and pull from AI Studio's export registry by app

ID — this is the path you'll automate later, because it means "re-sync the

prototype" is one non-interactive command:

```
# 6b. …or pull the latest export straight from AI Studio
agy studio pull --app triagedesk-8f3a21
```

Either way, the CLI unpacks the bundle, records its provenance (`exported_from`

,

`exported_at`

) in `.agy/lifecycle.lock`

, and your workspace now looks like this:

```
triagedesk/
├── .agy/
│   └── lifecycle.lock        # provenance pin: which export this workspace tracks
├── export/
│   └── triagedesk/           # the ingested bundle, verbatim (treat as read-only)
│       ├── lifecycle.json
│       ├── prompts/  tools/  schemas/  eval/
├── bridge/                   # YOUR code — the production wiring (Step 4)
├── main.py
└── requirements.txt
```

The discipline embedded in that layout: ** export/ is read-only, regenerated
by re-import; bridge/ is yours, written once.** Prompt changes happen in AI

`export/`

in place — thatThree commands before you write any Python:

```
# 7. Validate the ingested lifecycle: manifest schema, checksums, schema refs,
#    edge graph is acyclic, every declared tool has a JSON Schema entry
agy lifecycle validate

# 8. Parity check: has the cloud app drifted since this export?
agy lifecycle diff --against studio
#    → "in sync with aistudio://apps/triagedesk-8f3a21 (exported 2026-07-08)"
#    …or a unified diff of any prompt/param/schema that changed upstream

# 9. Smoke-test the lifecycle in the local harness — sandbox-mocked tools,
#    no production wiring yet. Proves the bundle itself is runnable.
agy run --lifecycle export/triagedesk/lifecycle.json \
    --mock-tools export/triagedesk/eval/golden.jsonl \
    -p "Ticket: 'I was charged twice for my Pro plan this month.'" \
    --output-format json
```

Unpacking the flags on that last command, because it's doing something subtle:

`--lifecycle`

`--mock-tools`

`--output-format json`

`Resolution`

on stdout, pipeable
into `jq`

or CI.If step 9 prints the same `Resolution`

the sandbox produced for that ticket,

the bridge held: the prototype is now running on your laptop, byte-identical

prompts, pinned parameters, same harness. What it doesn't have yet is

*production* — real tools, real policy, real telemetry. That's Step 4.

The CLI proved the lifecycle is portable. The SDK is where it becomes

*deployable*: a Python package (`bridge/`

) that loads the exported manifest

into governed agent configs, binds real tool implementations against the

exported interfaces, drives the diamond with typed handoffs, and gates deploys

behind a golden-transcript replay.

Every agent in the lifecycle gets minted through one factory with one set of

guardrails. Prompts and parameters come from the *bundle*; policy and

observability come from *here*. That separation is the production boundary in

one sentence: **AI Studio decides what the agents say; your harness decides
what they're allowed to do.**

``` python
# bridge/harness.py
import os
from typing import Callable, Sequence

from google.antigravity import LocalAgentConfig
from google.antigravity.hooks import hooks, policy

# Shared guardrails — least privilege, encoded once, inherited by every agent
# the lifecycle loader mints. The exported bundle can never widen these.
BASE_POLICIES = [
    policy.allow("search_tickets"),
    policy.allow("read_account"),
    policy.allow("read_policy"),
    policy.deny("run_command"),   # a triage agent never shells out
    policy.deny("write_file"),    # …or writes to disk
    policy.deny("*"),             # default-deny: anything undeclared is refused
]

class AuditTrailHook(hooks.PostToolCallHook):
    """One structured audit line per tool call, in every agent of the diamond."""
    async def run(self, context: hooks.HookContext, data) -> None:
        print(
            f"[audit] trajectory={getattr(context, 'trajectory_id', '?')} "
            f"tool={getattr(data, 'name', '?')} ok={getattr(data, 'ok', True)}"
        )

def build_agent_config(
    *,
    system_instructions: str,
    tools: Sequence[Callable] = (),
    model: str,
    generation: dict | None = None,
) -> LocalAgentConfig:
    kwargs = dict(
        model=model,
        system_instructions=system_instructions,
        tools=list(tools),
        policies=BASE_POLICIES,      # the production floor — non-negotiable
        hooks=[AuditTrailHook()],    # telemetry on every node
    )
    if generation:
        kwargs["generation"] = dict(generation)   # parameter parity, applied
    if os.getenv("VERTEX") == "1":
        kwargs["vertex"] = True
    elif os.getenv("GEMINI_API_KEY"):
        kwargs["api_key"] = os.environ["GEMINI_API_KEY"]
    return LocalAgentConfig(**kwargs)
```

The design decision that matters: `BASE_POLICIES`

is **not** part of the

export, on purpose. Policy is an attribute of the *environment*, not the

*prototype* — the same lifecycle might run with generous policies in staging

and airtight ones in production. The bundle can name tools; only the harness

can grant them.

The bundle shipped `tools/tools.json`

: three function declarations with full

JSON Schema parameters. Locally, you write the real implementations and

**validate them against the exported declarations at import time**. This is

interface parity as executable code — if AI Studio exported

`read_account(customer_id: string)`

and you wrote

`read_account(account_id: str)`

, you find out at startup, not in an incident

review.

``` python
# bridge/tools.py
import inspect
import json
from pathlib import Path

BUNDLE = Path(__file__).parent.parent / "export" / "triagedesk"

def search_tickets(customer_id: str, days: int = 90) -> str:
    """Return this customer's tickets from the last `days` days, as JSON."""
    from .backends import ticket_store            # real system, injected here
    return json.dumps(ticket_store.recent(customer_id, days=days))

def read_account(customer_id: str) -> str:
    """Return the customer's plan, billing history, and account flags."""
    from .backends import billing
    return json.dumps(billing.account_summary(customer_id))

def read_policy(category: str) -> str:
    """Return the support policy text applicable to the given ticket category."""
    from .backends import policy_docs
    return policy_docs.for_category(category)

TOOL_REGISTRY = {
    "search_tickets": search_tickets,
    "read_account": read_account,
    "read_policy": read_policy,
}

def validate_registry() -> None:
    """Interface parity: every exported declaration must have a local impl
    whose signature matches the exported JSON Schema. Fail at startup."""
    declared = json.loads((BUNDLE / "tools" / "tools.json").read_text())
    for decl in declared["functions"]:
        impl = TOOL_REGISTRY.get(decl["name"])
        if impl is None:
            raise RuntimeError(f"exported tool '{decl['name']}' has no local implementation")
        got = set(inspect.signature(impl).parameters)
        want = set(decl["parameters"]["properties"])
        if got != want:
            raise RuntimeError(
                f"signature drift on '{decl['name']}': bundle declares {sorted(want)}, "
                f"local implementation has {sorted(got)}"
            )
```

Line by line, the load-bearing parts:

`ticket_store`

, `billing`

,
`policy_docs`

). This is the code the sandbox `validate_registry()`

compares Python signatures to exported JSON Schema
properties.`account_id`

and the tool keeps
erroring" three weeks after launch.Now the centerpiece: a loader that reads `lifecycle.json`

and mints one

governed `LocalAgentConfig`

per agent — verifying prompt checksums as it goes.

This function *is* the bridge.

``` python
# bridge/lifecycle.py
import hashlib
import json
from pathlib import Path

from google.antigravity import LocalAgentConfig

from .harness import build_agent_config
from .tools import BUNDLE, TOOL_REGISTRY, validate_registry

class ParityError(RuntimeError):
    """The local bundle no longer matches what AI Studio exported."""

def _verify_checksum(root: Path, rel: str, expected: str) -> None:
    digest = hashlib.sha256((root / rel).read_bytes()).hexdigest()
    if f"sha256:{digest}" != expected:
        raise ParityError(
            f"prompt drift in {rel}: bundle expects {expected[:19]}…, "
            f"file hashes to sha256:{digest[:12]}…. Re-import the export; "
            f"never hand-edit export/."
        )

def load_lifecycle(bundle: Path = BUNDLE) -> dict[str, LocalAgentConfig]:
    manifest = json.loads((bundle / "lifecycle.json").read_text())
    validate_registry()                              # interface parity gate

    defaults = manifest["model_defaults"]
    configs: dict[str, LocalAgentConfig] = {}
    for spec in manifest["agents"]:
        _verify_checksum(bundle, spec["prompt"],     # prompt parity gate
                         manifest["checksums"][spec["prompt"]])
        configs[spec["id"]] = build_agent_config(
            system_instructions=(bundle / spec["prompt"]).read_text(),
            tools=[TOOL_REGISTRY[name] for name in spec["tools"]],
            model=spec.get("model", defaults["model"]),
            generation={**defaults.get("generation", {}),
                        **spec.get("generation", {})},   # parameter parity
        )
    return configs
```

Walk it:

`validate_registry()`

runs before any agent is built`_verify_checksum`

on every prompt.`ParityError`

message tells the
operator the `dict[str, LocalAgentConfig]`

— means the rest of the
system addresses agents by their `classifier`

,
`investigator`

, …). The names on the AI Studio canvas are now the names in
your stack traces. Small thing; enormous debugging dividend.The bundle's `schemas/handoffs.json`

defines what flows along each edge. In

Python, those become Pydantic models — the same contracts, now enforced by the

harness's structured-output machinery:

``` python
# bridge/schemas.py
from enum import Enum
from pydantic import BaseModel, Field

class Category(str, Enum):
    BILLING = "billing"; TECHNICAL = "technical"; ACCOUNT = "account"; OTHER = "other"

class TicketClass(BaseModel):
    category: Category
    severity: int = Field(ge=1, le=4)          # 1 = critical … 4 = low
    summary: str

class Investigation(BaseModel):
    customer_id: str
    prior_tickets: int
    account_standing: str
    relevant_facts: list[str]

class PolicyVerdict(BaseModel):
    policy_section: str
    permitted_actions: list[str]
    requires_human_approval: bool

class Resolution(BaseModel):
    action: str
    reply_draft: str
    escalate: bool
    rationale: str
```

The manifest's `edges`

said: classify, then fan out in parallel, then join.

The production orchestrator encodes exactly that — and nothing else. All the

intelligence lives in the exported prompts; this file is just the topology,

made explicit and typed.

``` python
# bridge/orchestrator.py
import asyncio
import os
from typing import Type, TypeVar

from google.antigravity import Agent
from pydantic import BaseModel

from .lifecycle import load_lifecycle
from .schemas import Investigation, PolicyVerdict, Resolution, TicketClass

T = TypeVar("T", bound=BaseModel)

AGENTS = load_lifecycle()          # parity gates run HERE, at import time

async def run_once(config, prompt: str, schema: Type[T]) -> T:
    """One isolated agent, one typed turn, then tear down."""
    async with Agent(config) as agent:
        resp = await agent.chat(prompt, response_schema=schema)
        return await resp.parsed()               # a validated Pydantic instance

async def triage(ticket_text: str, customer_id: str) -> Resolution:
    # Node 1 — classify (no tools, cold temperature, deterministic)
    tclass = await run_once(
        AGENTS["classifier"],
        f"Classify this support ticket.\n\nTICKET:\n{ticket_text}",
        TicketClass,
    )

    # Nodes 2 & 3 — the parallel branch from the manifest's edges.
    # Two isolated contexts, launched at the same instant.
    shared = (
        f"TICKET:\n{ticket_text}\n\ncustomer_id: {customer_id}\n"
        f"classification: {tclass.model_dump_json()}"
    )
    investigation, verdict = await asyncio.gather(
        run_once(AGENTS["investigator"],
                 f"Investigate. Use your tools.\n\n{shared}", Investigation),
        run_once(AGENTS["policy_checker"],
                 f"Determine what policy permits.\n\n{shared}", PolicyVerdict),
    )

    # Node 4 — the join. Tool-less: the resolution must be a pure function of
    # the two typed findings, which is what makes it auditable.
    return await run_once(
        AGENTS["resolver"],
        "Draft the resolution from these independent findings.\n\n"
        f"Investigation: {investigation.model_dump_json(indent=2)}\n"
        f"PolicyVerdict: {verdict.model_dump_json(indent=2)}",
        Resolution,
    )

async def triage_batch(tickets: list[tuple[str, str]]) -> list[Resolution]:
    limit = int(os.getenv("TRIAGE_CONCURRENCY", "8"))
    sem = asyncio.Semaphore(limit)               # bound live model sessions

    async def _guarded(t: tuple[str, str]) -> Resolution:
        async with sem:
            return await triage(*t)

    return await asyncio.gather(*(_guarded(t) for t in tickets))
```

The parts worth reading twice:

`AGENTS = load_lifecycle()`

at module import.`ParityError`

naming the exact
file, instead of serving subtly-wrong triage for a week.`run_once`

opens a fresh `Agent`

per node.`async with Agent(config)`

is
a full harness session with its own isolated context window. The
Investigator's tool output never pollutes the Policy Checker's reasoning —
the same state isolation the sandbox gave the diamond, reproduced locally.`asyncio.gather`

is the manifest's `"mode": "parallel"`

edge, verbatim.`max(investigate, policy)`

, not the
sum — identical to how the AI Studio canvas ran it.`response_schema=schema`

on every turn.`resp.parsed()`

hands
back a validated instance. Fan-in stays deterministic.`triage_batch`

Last clause of the contract: behavioral parity. The bundle's `eval/golden.jsonl`

holds every sandbox run you approved. Replay them through the *local* system —

real harness, real policy, mocked tools (so backends don't flake the gate) —

and compare the structured fields that matter:

``` python
# bridge/parity.py
import asyncio
import json

from .orchestrator import triage
from .tools import BUNDLE

async def replay_golden() -> tuple[int, int]:
    """Replay approved sandbox transcripts against the local lifecycle.
    Returns (passed, total). Wire this into CI as the deploy gate."""
    passed = total = 0
    for line in (BUNDLE / "eval" / "golden.jsonl").read_text().splitlines():
        case = json.loads(line)
        total += 1
        local = await triage(case["ticket"], case["customer_id"])
        expected = case["expected"]                     # sandbox Resolution
        ok = (local.action == expected["action"]
              and local.escalate == expected["escalate"])
        passed += ok
        print(f"[parity] {case['id']}: {'PASS' if ok else 'FAIL'}")
    return passed, total

if __name__ == "__main__":
    p, t = asyncio.run(replay_golden())
    raise SystemExit(0 if p == t else 1)                # CI-friendly exit code
```

Note what we compare: `action`

and `escalate`

— the decisions — not

`reply_draft`

, which is legitimately creative at temperature 0.6. A parity gate

that string-matches prose will cry wolf until someone deletes it; a gate that

checks decisions earns its place in CI. `python -m bridge.parity`

exits nonzero

on any regression, which makes the deploy pipeline one line:

```
agy lifecycle diff --against studio && python -m bridge.parity && ./deploy.sh
```

Read that line back slowly, because it's the whole article: *the cloud hasn't
drifted from the bundle, the local system still behaves like the approved
sandbox runs, therefore ship.* Three worlds — AI Studio, the repository, and

``` python
# main.py
import asyncio, json, sys
from bridge.orchestrator import triage

async def main():
    ticket = sys.stdin.read().strip() or "I was charged twice for my Pro plan."
    resolution = await triage(ticket, customer_id="cus_4821")
    print(json.dumps(resolution.model_dump(), indent=2))

asyncio.run(main())
bash
$ echo "I was charged twice for my Pro plan this month." | python main.py
[audit] trajectory=tj_01 tool=search_tickets ok=True
[audit] trajectory=tj_01 tool=read_account ok=True
[audit] trajectory=tj_02 tool=read_policy ok=True
{
  "action": "refund_duplicate_charge",
  "reply_draft": "Hi — you're right, and I'm sorry: our records confirm a duplicate charge…",
  "escalate": false,
  "rationale": "Billing history confirms duplicate charge on 2026-07-02; policy §4.2 permits immediate refund of verified duplicates without approval."
}
```

Same prompts as the sandbox, byte-for-byte. Same parameters, applied by the

harness. Same diamond, now with real tools, default-deny policy, an audit line

per tool call, and a regression suite that was generated by *using the
prototype*. The distance from demo to this was four files of wiring — and none

The canyon between "look what I built in AI Studio" and "it's in production"

was never really about code. It was about **state that lived in a UI** — and

every team that lost a week reconstructing a prototype from screenshots, or

shipped a playground behind a proxy and prayed, was paying interest on that

one architectural debt.

Antigravity 2.0's answer is to make the prototype an artifact with a defined

projection into a repository. The **lifecycle export** captures architecture,

prompts, tool interfaces, handoff schemas, parameters, and evidence in one

bundle. The ** agy CLI** makes ingesting, validating, and drift-checking that

The deeper shift is organizational. When export is one click and ingestion is

one command, the prototype stops being a throwaway and becomes **the first
commit** — product-minded builders iterate in AI Studio, engineers govern the

Prototype where iteration is cheapest. Productionize where governance is

strongest. With a checksummed bridge between them, you no longer have to

choose — and the next time leadership asks *"when can this be in production?"*,

the honest answer is: it already compiled.

Google Cloud Credits are provided for this project. #AgenticArchitectSprint #Antigravity

`agy`

):
