Plan schema
The plan.json fields agb run, validate and preflight accept, and every rule validatePlan enforces.
A plan is a JSON file that describes one build-out: the target repository, the gate commands that decide whether a ticket's work is acceptable, and a DAG of tickets. agb plan compiles one for you from an Antigravity plan or a local spec; you can also write it by hand. agb run, agb validate and agb preflight all load it through the same function, which runs validatePlan before anything else happens (loadPlan).
Example
This is the shipped template, templates/plan.example.json. Three tickets: T1 and T2 run in parallel, T3 depends on T1 and treats lib/math.mjs as a rail.
{
"repo": "/abs/path/to/target-repo",
"base": "main",
"gate": {
"test": "npm test"
},
"tickets": [
{
"id": "T1",
"title": "math utilities",
"body": "Create lib/math.mjs exporting two pure functions: add(a, b) returning the sum, and multiply(a, b) returning the product. Both must throw a TypeError when either argument is not a finite number. Create test/math.test.mjs using node:test + node:assert/strict covering: happy path for both functions, and the TypeError cases. Acceptance: `npm test` exits 0 and the new tests actually assert behavior (no empty tests).",
"scope": [
"lib/math.mjs",
"test/math.test.mjs"
],
"edges": [
{
"to": "T3"
}
],
"tier": "cheap",
"pool_hint": "gemini"
},
{
"id": "T2",
"title": "slugify",
"body": "Create lib/strings.mjs exporting slugify(input) that lowercases, trims, replaces runs of non-alphanumeric characters with single hyphens, and strips leading/trailing hyphens. It must throw a TypeError on non-string input. Create test/strings.test.mjs using node:test + node:assert/strict covering: basic phrase, unicode/punctuation collapse, leading/trailing junk, and the TypeError case. Acceptance: `npm test` exits 0.",
"scope": [
"lib/strings.mjs",
"test/strings.test.mjs"
],
"tier": "mid",
"pool_hint": "claude"
},
{
"id": "T3",
"title": "mean via math utils",
"body": "Create lib/stats.mjs exporting mean(numbers) that returns the arithmetic mean of a non-empty array of finite numbers, computed by importing and using add() from ./math.mjs (do not reimplement summation inline). Throw a RangeError on an empty array and a TypeError on non-array or non-finite elements. Create test/stats.test.mjs using node:test + node:assert/strict covering: happy path, single element, RangeError and TypeError cases. lib/math.mjs already exists on main — read it, never modify it. Acceptance: `npm test` exits 0.",
"scope": [
"lib/stats.mjs",
"test/stats.test.mjs"
],
"rails": [
"lib/math.mjs"
],
"tier": "cheap",
"pool_hint": "gemini"
}
]
}
Top-level fields
These fields are validated by lib/plan.mjs.
Prop
Type
The scheduler also reads these optional fields (plan.base, plan.caps, plan.prosecution.dryPasses). validatePlan does not check them.
| Field | Type | Default | Meaning |
|---|---|---|---|
base | string | main | Branch the run builds on and merges into. |
caps | object | pool defaults | Per-pool concurrency caps, merged over the defaults (gemini-flash, gemini-pro, claude, gpt-oss). See Calibration. |
prosecution.dryPasses | number | 1 | Consecutive clean prosecution passes required before a ticket may merge. |
Ticket fields
Prop
Type
Validation rules
validatePlan returns a list of error strings; an empty list means the plan is valid. Every rule below produces one error per offending item, so you see all problems at once.
Plan level
repomust be present (source).adlcBinmust be absent (C21).gatemust declarebuildortest. With strict gates, each declared command must match^npm (test|run [a-zA-Z0-9_:-]+)$, that isnpm testornpm run <script>and nothing else (C21). Strict mode is on unlessAGB_STRICT_GATES=0(compilePlan default);agb run,validateandpreflightalways callvalidatePlanwith its default, which is strict (default argument).ticketsmust be non-empty (source).
Ids
- No exact duplicates, and each id must match the ticket id pattern (source).
- Ids must be unique case-insensitively:
Fooandfooboth map to worktreeagb-fooand branchagb/foo, and creating the second would destroy the first (C21).
Per ticket
- Every edge needs a non-blank string
to, may not point at its own ticket, and must name a ticket that exists (source). bodyis required and non-blank;scopeis a non-empty array (source).scopeandrailsentries are normalized (a trailing/becomes/**) and must be valid pathspecs: relative, no.or..segments, no empty segments, already normalized, not*,**or., and matching the conservative character set inPATHSPEC_RE(validatePathspec, applied).tiermust becheap,midorfrontier(source).pool_hintmust be one ofgemini,claude,claude-gpt,gpt-ossorauto(C4), and the tier plus hint must yield at least one model candidate (source). The names are pool families, not individual models.
Graph
- The ticket DAG must be acyclic; a cycle is reported with the ids that form it (source).
Scope overlaps are a compile-time check
validatePlan does not reject two parallel tickets with overlapping scopes. agb plan does: during compilation it forecasts overlaps and feeds them back as blocking errors until the plan is repartitioned or an edge is added (compile loop). agb preflight reports the same forecast for hand-written plans.
Gate script integrity
Because npm test runs whatever package.json says, the scheduler also checks before merge that a ticket did not rewrite the gate script, or a script it delegates to, or its pre/post hooks (verifyGateScriptIntegrity). A tampered script fails the ticket with error kind gate_script_tampering. See Gates and sandboxing.
Related
- Writing plans
- Sweep schema, which compiles to this shape
agb validateand Exit and error codes