Riding Two Release Lines — Splitting Antigravity's 2.0 and 2.1 Tracks Between Unattended and Interactive Work
On July 1, Antigravity published 2.0-line and 2.1-line builds side by side. Here is a version policy that keeps unattended automation on the stable line and interactive work on the feature line, with a declaration file and drift detection.
On July 1, the Antigravity releases page listed 2.1.4, 2.0.11, 2.0.10, 2.0.6, 2.0.1, and 2.0.0 — all published at once. If you paused in front of the update dialog, you were not alone. Move to the 2.1 line and you get the new features. Stay on the 2.0 line and behavior stays put. So which one should the scheduled runs that fire at 2 a.m. be riding?
If you make that call by mood each time the dialog appears, an unattended run will eventually break in silence. This article lays out how I manage the two lines now that they ship in parallel: a declared policy per workload, a small reconciliation script, and a promotion procedure between the lines — shared with the actual files I use.
Choosing Not to Keep Everything on the Latest
Start with what the July 1 release actually signals: the 2.0 line is being stabilized while the 2.1 line carries new features, and both are maintained in parallel. That hands users two things at once — the freedom to choose a line, and the responsibility to manage that choice.
As an indie developer I hand a good share of my daily work — content updates across several sites, app build verification — to agents running unattended. What I fear most in that kind of operation is not missing features. It is behavior shifting slightly with every update. In interactive use, a shift is a shrug: you notice, you adapt. In unattended runs, nobody notices, and the next steps keep stacking on top of the changed behavior.
Line up the requirements per workload and the choice of line falls out naturally.
Aspect
Interactive (IDE, chat)
Unattended (CLI, scheduled runs)
Top priority
New features, speed
Reproducibility, unchanged behavior
Tolerance for behavior shifts
High — a human absorbs it on the spot
Low — detection lags and failures cascade
Update cadence
Follow releases early
Move only when you decide to
Suitable line
2.1
2.0
The conclusion is simple: environments where I am hands-on ride the 2.1 line and absorb new features early; environments that run without me stay pinned to the 2.0 line. Between the two sits an "observe, then promote" procedure. The rest of this article turns that stance into working machinery.
version-policy.json — One Place That Declares the Lines
Managing line assignments in your head, or as a note in a README, collapses the moment you have more than two machines. I declare the intended line for each surface-and-workload pair in a single JSON file.
The problem this file solves: making "which environment should be riding what" checkable by any person and any script, without archaeology.
{ "$comment": "version-policy.json — single source for release-line policy", "updated": "2026-07-02", "policies": [ { "surface": "desktop", "workload": "interactive", "line": "2.1", "range": ">=2.1.4 <2.2.0", "note": "Interactive development. Absorb new features early" }, { "surface": "cli", "workload": "unattended", "line": "2.0", "range": ">=2.0.11 <2.1.0", "note": "Scheduled runs. Never moves except by explicit decision" }, { "surface": "cli", "workload": "interactive", "line": "2.1", "range": ">=2.1.4 <2.2.0", "note": "Local verification. Always ahead of the unattended line" } ]}
The detail that matters is writing range with an upper bound on the line: >=2.0.11 <2.1.0 accepts point releases within the 2.0 line while rejecting any accidental hop to 2.1. An update within a line and a migration across lines are operations with risks of a different order, so the declaration keeps them distinct from the start.
Pinning the exact version a scheduled run executes with — reproducibility at the run level — is a separate layer I covered in pinning the Antigravity CLI version for reproducible scheduled runs. The policy in this article sits one level above: deciding which line you pin to in the first place.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Decision criteria for splitting release lines between unattended and interactive workloads, managed declaratively through a version-policy.json file
✦A drift-detection script that reconciles actual desktop, CLI, and multi-machine versions against the declared policy every morning
✦A promotion procedure that safely feeds observations from the 2.1 line into 2.0-line unattended operations, with a real incident it prevented in late June
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Drift Detection — Reconciling Declared Versus Actual Every Morning
A declaration alone will not tell you when reality drifts away from it. A small script collects the actual version from every surface and reconciles the report against the policy; I run it as one step in my morning scheduled batch.
// check-version-policy.mjs — reconcile actual versions against the policy// usage: node check-version-policy.mjs <version-policy.json> <report.json>import { readFileSync } from "node:fs";const [policyPath, reportPath] = process.argv.slice(2);const policy = JSON.parse(readFileSync(policyPath, "utf8"));// report.json is written by each machine from `agy --version` output// shape: [{ "host": "mac-studio", "surface": "cli", "workload": "unattended", "version": "2.0.11" }]const report = JSON.parse(readFileSync(reportPath, "utf8"));const parse = (v) => v.split(".").map(Number);const cmp = (a, b) => { const [a1, a2, a3] = parse(a); const [b1, b2, b3] = parse(b); return a1 - b1 || a2 - b2 || a3 - b3;};const inRange = (version, range) => range.split(" ").every((cond) => { const m = cond.match(/^(>=|<=|<|>)?([\d.]+)$/); if (!m) return true; const [, op = ">=", bound] = m; const c = cmp(version, bound); return op === ">=" ? c >= 0 : op === "<=" ? c <= 0 : op === "<" ? c < 0 : c > 0; });let violations = 0;for (const entry of report) { const rule = policy.policies.find( (p) => p.surface === entry.surface && p.workload === entry.workload ); if (!rule) { console.log(`⚠️ ${entry.host}/${entry.surface}: no policy defined`); violations++; continue; } if (!inRange(entry.version, rule.range)) { console.log( `❌ ${entry.host}/${entry.surface}(${entry.workload}): ` + `${entry.version} is outside ${rule.range} (line ${rule.line} declared)` ); violations++; }}console.log(violations === 0 ? "✅ all surfaces within policy" : `${violations} violation(s)`);process.exit(violations === 0 ? 0 : 1);
The collection side is trivial — each machine's cron or scheduled task writes one line of agy --version output into the report. The range check is a deliberately minimal hand-rolled comparison so the script runs with zero dependencies; if I ever need full semver semantics I will switch libraries, but "drift becomes visible the next morning" was the state worth reaching first.
The script returns an exit code so the same reconciliation can be dropped in front of unattended runs as a gate: an environment that violates the policy simply does not run. That quiet, negative defense is the one that pays off most.
Promotion — Feeding 2.1-Line Observations into 2.0-Line Operations
Once the lines are split, the question becomes when and how the unattended line moves. My procedure:
Install new 2.1-line builds only in the interactive environment. The update dialog gets obeyed there and nowhere else
Observe for a week in normal interactive work. Any behavior difference goes into a dated one-line note, to cross-reference against the changelog later
Wait for fixes to land on the 2.0 line. This is the payoff of parallel publishing: problems first encountered on the 2.1 line often come back down as 2.0-line point releases
Raise the lower bound of range in version-policy.json and commit. The policy file's history becomes the decision log for version management
The exception is when a bug currently biting an unattended run is fixed only on the 2.1 line. Only then do I consider a cross-line promotion, and it goes in three stages: at least three days of observation, migrating exactly one unattended task first, and moving the rest after a week of running both in parallel. I have never migrated every task across lines at once.
The Incident This Caught in Late June
One concrete record. In late June, after moving the interactive environment to a 2.1-line point release, my note read: "substring search feels faster on long output, but the result ordering seems different." A few days later, reviewing one of my unattended runs — a task that picks specific lines out of generated logs and feeds them to the next step — I realized that if the same update had reached the unattended environment, an implicit dependency on that ordering would have broken it.
Nothing actually happened, because the unattended environment was pinned to the 2.0 line. The drift report stayed at "✅ all surfaces within policy." A story about an incident that did not occur is unglamorous, but that is precisely the value of splitting lines: you can design "nothing happened" into your operations. I rewrote the ordering dependency into an explicit sort that same week, so the 2.0-line update could be accepted without worry.
How the Changelog Reads Differently
Operating on split lines changes how you read release notes. The question is no longer just "what landed on 2.1" but "which of the 2.1 fixes also came down to 2.0." Days like July 1, when several builds publish simultaneously, are the best moments to map the correspondence between lines.
My morning batch diffs the releases page, and on days a new build appears, the only judgment I make is which step of the promotion procedure it touches. There is no need to read everything daily. With "does this affect the unattended line?" as the first filter, the check takes a few minutes.
Start by Writing One Declaration File
Parallel 2.0 and 2.1 lines mean, put bluntly, that line selection is now the user's responsibility. As a next step, adapt the version-policy.json above to your own surfaces and compare it against the versions actually installed. If even one environment turns out to be off-policy, that is reason enough to make the reconciliation a daily habit.
If you run unattended automation of your own, I hope this saves you the incident I almost had. Thank you for reading.
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.