The CLI is the only integration boundary: every surface — the Claude plugin, the Antigravity
plugin, CI, a developer at a terminal — drives ea-core through it. This document fixes the
invocation model, the stdout JSON envelope, the diagnostics shape, the exit-code table, the
write/confirm protocol, projection, and the command catalog.
ea <command> [subcommand] [positional-args] [--flags]
- Inputs: flags/args for simple cases; a JSON payload on stdin for complex inputs (e.g. authored artifact content the core commits). A command documents which it accepts.
- Global flags:
--scope org | --project <id>,--confirm <token>,--contract-version <v>(assert the version the caller targets; mismatch → usage error),--include-archived,--input -(read JSON payload from stdin). - The core is non-interactive: it never prompts. All input arrives via args/flags/stdin; all output is the envelope on stdout + narration on stderr. This makes interactive and headless behaviour identical.
- stdout carries exactly one JSON document — the response envelope. Nothing else.
- stderr carries human-readable narration, progress, and logs. Never machine-read.
- This split lets any consumer do
ea … | jqor gate CI on the envelope without scraping logs.
Every invocation that runs emits one envelope:
{
"contract_version": "1.1",
"command": "validate",
"status": "invalid",
"data": { },
"diagnostics": [ ]
}| Field | Type | Meaning |
|---|---|---|
contract_version |
string | the contract version this output conforms to |
command |
string | the invoked command path, e.g. "project delete" |
status |
enum | ok · invalid · error |
data |
object | command-specific result (per-command shape; may be {}) |
diagnostics |
array | zero or more diagnostic objects |
Output is canonical JSON: UTF-8, LF, keys sorted (envelope keys in the fixed order above; nested object keys lexicographic), arrays in a deterministic order. Identical inputs → byte-identical output.
status and the exit code agree but serve different consumers:
status: ok→ exit0. Operation succeeded; forvalidate, the source of truth is valid.status: invalid→ exit3. The tool ran fine but the architecture is invalid (validation findings indiagnostics). This is the key distinction CI relies on.status: error→ exit1/2/4(the tool could not complete the operation).
{
"severity": "error",
"code": "reference.dangling",
"scope": "payments",
"path": "business/capability/billing.md",
"rule": "refs-resolve",
"message": "reference {type: pattern, id: retry-with-backoff} does not resolve",
"hint": "promote the pattern to Org or add it to this project"
}| Field | Required | Meaning |
|---|---|---|
severity |
yes | error · warning · info |
code |
yes | stable, dotted, machine-matchable (e.g. schema.invalid, reference.dangling, layering.violation, id.pattern, manifest.unknown_type, method.deprecated, policy.dangling, renames.dangling, overlay.invalid, overlay.dangling) |
scope |
no | org or a project id, when scope-specific |
path |
no | repo-relative file path, when file-specific |
rule |
no | the method-pack rule id, when a rule produced it |
message |
yes | human-readable text |
hint |
no | optional suggested remedy |
Diagnostics are emitted in a stable order: by scope, then path, then code. Only
severity: error diagnostics make a validate invalid.
| Code | Name | Meaning |
|---|---|---|
0 |
ok | success; for validate, valid |
1 |
internal error | an unexpected core failure (bug, I/O failure) |
2 |
usage error | bad/missing args, unknown command, bad --scope, unsupported --contract-version, malformed stdin payload |
3 |
validation failed | ran fine; the architecture is invalid (status: invalid) |
4 |
guard refused | a write was refused: --confirm token missing/mismatched, or verify-before-delete fingerprint drift |
The set is closed and stable; new categories require a contract version bump. CI gates on
3 (fail the build on invalid architecture) vs 1/2/4 (tooling/operator problem).
All mutating commands are dry-run by default and follow one protocol:
- Plan (default). The command computes the intended change and returns
status: okwithdata.plandescribing every file create/update/delete, plusdata.confirmation_token. Where review needs more than the plan, the dry-run also carriesdata.preview(the full after content) and — forcommit—data.diff, a deterministic unified diff of the assembled content against the current on-disk file (byte-stable,""on a no-op, present only on the dry-run). - Token.
confirmation_token = "sha256:" + hex(sha256(canonical_plan_bytes)), wherecanonical_plan_bytesis the deterministic serialization of the planned change (for deletes, this includes the target fingerprint). The token is therefore content-bound: it changes if the computed effect changes. - Apply. Re-run the identical command with
--confirm <token>. The core recomputes the plan from current state, derives the token again, and:- if it matches the supplied token → executes the change, returns
status: okwithdata.applied; - if it does not match (state drifted since planning) → refuses, exit
4, no write.
- if it matches the supplied token → executes the change, returns
This is identical interactive and headless (no prompts), and no plan can be applied against a changed repository.
R = read-only · W = guarded write · F = fetch (side-effects the local pack cache, not
the source of truth; no repo and no --confirm required).
| Command | Kind | Scope | Summary |
|---|---|---|---|
ea version |
R | — | tool + contract_version |
ea init |
W | repo | scaffold a new repository (manifest, dirs) |
ea validate [--scope/--project/--all] |
R | scope/all | validate the source of truth; invalid → exit 3 |
ea report <kind> [--scope/--project] |
R | scope | deterministic report (coverage today) |
ea graph <kind> [--scope/--project] |
R | scope | dependency graph today (lineage · impact reserved) |
ea scaffold <type> --project <id> --id <id> [--domain <d>] |
W | project/org | stub a new artifact from its type; --domain inferred when unambiguous. A new use of a deprecation-stranded type → invalid/exit 3 (method.deprecated) |
ea commit <type> --project <id> --id <id> [--domain <d>] --input - |
W | project/org | commit agent-authored content (content-only JSON on stdin); the dry-run carries data.diff for review |
ea project create <id> |
W | repo | create a project |
ea project archive <id> / unarchive <id> |
W | project | reversible status change |
ea project delete <id> |
W | project | verify-before-delete guarded removal |
ea project list |
R | repo | list projects + status |
ea method add <pack@version#method> / remove <method> |
W | repo | (re)materialize the active subset into definitions/ |
ea method deprecate <method> [--reason <s>] / undeprecate <method> |
W | repo | set/clear a method-deprecation policy in the Manifest (guarded) |
ea method list |
R | repo | list active methods + provenance + deprecation status |
ea manifest show |
R | repo | emit the resolved manifest |
ea manifest set <path> <value> |
W | repo | set a domain/setting (guarded) |
ea manifest adopt <preset> |
W | repo | adopt an archetype preset (domains + method selections); materializes definitions/ |
ea manifest domain add <id> --types <t,…> [--title/--description] / set-types <id> --types <t,…> / remove <id> |
W | repo | incrementally assign types to domains in the Manifest (guarded; each --types entry must be an active type, enumerated on a miss) |
ea manifest relationship add <type>.<name> --target <t> --cardinality <c> / disable <type>.<name> / remove <type>.<name> |
W | repo | tailor the org relationship overlay: add cross-pack edges, override cardinality/target, or disable a pack edge (manifest.md) |
ea manifest rename add <pack@version> <from> <to> / remove <pack@version> <from> |
W | repo | locally rename a pack's rule id at materialization, resolving cross-pack id collisions (manifest.md) |
ea promote <type> <id> --project <id> |
W | project→org | promote an artifact to Org |
ea migrate <archetype> |
W | repo | archetype migration (re-adopt with carried tailoring) |
ea pack fetch <source> [pack@version …] |
F | — | fetch packs from a git remote or local directory into the local cache; resolution is then offline |
ea preset list / preset show <id> |
R | — | enumerate the adoptable archetype presets / show one resolved preset; unknown id → exit 2 enumerating the available ids |
ea pack list / pack show <pack@version> |
R | — | enumerate the resolvable packs (grouped by id with versions) / show one pack's types/rules/methods; unknown / unversioned → exit 2 enumerating options |
ea pack new <id> [--version/--title/--path] |
W | — | scaffold a new, schema-valid pack.yaml skeleton (guarded; needs no EA repo); target-exists / bad id / bad version → exit 2 |
ea pack validate <path> |
R | — | validate a pack file; valid/0, structural errors → invalid/3 with sorted data.errors, unreadable/malformed → exit 2 |
ea type show <type> |
R | repo | inspect one active type's authoring guidance: fields · advisory body sections · relationship metadata; unknown/inactive → exit 2 enumerating active types |
ea method show <id> |
R | repo | inspect one active method: summary · requires · ordered steps with detail; unknown/inactive → exit 2 enumerating active methods |
ea show <type> <id> [--project <id>] |
R | scope | inspect one artifact: its front-matter flat in data plus scope (where it resolved) and body; --project resolves [<pid>, org] (a project artifact shadows an org twin), else org only; --fields/--list-fields over a runtime-derived vocabulary; misses/ambiguity → exit 2 (below) |
The per-command data payload shape is defined alongside each command and evolves under
contract_version. Authoring content arrives via ea commit (JSON on stdin) — the core only
commits it as a guarded write; the judgment that produced it lives on the surface.
- Scoped commands accept exactly one of
--scope orgor--project <id>. Repository-wide commands accept--all(or are inherently repo-level, e.g.method add). - An operation that omits a required scope is a usage error (exit 2) — there is no implicit cross-scope default (no silent repository-wide effects).
Every read (R) command except graph dependency accepts an optional, purely subtractive
--fields a,b,c that trims data to the named top-level keys, and a --list-fields that
returns the projectable set under data.fields with no result data.
- Envelope unchanged. Projection touches only
data—contract_version,command,status, anddiagnosticsare emitted exactly as without projection. A projectedvalidatekeeps its exit-3 verdict;diagnosticsis envelope-level and never projected. - Subtractive only.
--fieldscan only remove keys an unprojected call already returns — never add, rename, recompute, re-nest, or reorder. An unprojected call is byte-for-byte the unversioned behaviour. Roll-up keys likesummaryare computed on the full result, then projected like any other key — a trimmed view never silently changes a count. - Trim, don't restructure. Keys are removed at the result root only; no flattening, re-nesting, or per-record trimming inside list-valued results.
- Deterministic. Identical inputs + identical
--fields→ byte-identical output. Flag order and duplicates don't matter.--fieldswith no surviving keys yields{}, not an error. - Runtime discovery, no hardcoding. A surface learns the field names at runtime: an unknown
name in
--fieldsis a usage error (exit 2) whose diagnostic enumerates the valid fields;--list-fieldsasks deliberately. For the record-shaped verbs the vocabulary is each verb'sdatatop-level keys, fixed undercontract_version— it never varies by repo. ea showis the one exception: its vocabulary is computed at runtime from repo state — the envelope keys, the type's effective field-schema properties and relationship ids,{scope, body}, and the resolved artifact's own front-matter keys (the union keeps every emitted key projectable, since field schemas stay open). This is the same kind of function of repo bytes astype show's field list, so P1 (byte-reproducibility for identical inputs) holds; what is versioned forshowis the derivation rule, not the set.
graph dependency accepts no --fields/--list-fields (deferred to a future holistic graph
pass, together with a --depth walk-bound).
When an id cannot be resolved, the usage error (exit 2) enumerates the valid ids inline, so
an agent self-corrects in one round without a separate list call:
| Trigger | Error enumerates |
|---|---|
preset show <unknown>, manifest adopt <unknown> |
the available preset ids (resolvable on EA_PRESET_PATH) |
pack show <pack>@<unknown-version>, method add / migrate with an unresolvable pin |
the available pack@version ids |
pack show <pack> with no @version |
that pack's available versions |
type show <unknown> |
the active type ids, sorted |
method show <unknown> |
the active method ids, sorted |
show <unknown-type> <id> |
the active type ids, sorted (as type show) |
show <type> <unknown-id> |
the available ids of that type in the visible scopes, sorted |
--project <unknown> (every verb taking the flag) |
the available project ids, sorted |
unknown name in --fields |
the verb's projectable fields |
unknown type in manifest domain --types |
the active type ids |
Form: <thing> '<id>' not found; available: <a>, <b>, … (available: (none) when the set is
empty). Because adopt/add/migrate route through the same resolvers as the discovery verbs, the
enumeration is uniform — no per-command special-casing. The --project row lands in the shared
project resolution, so every --project verb (validate, promote, report, graph, authoring,
show) gains the same available: enumeration.
Beside the misses, show has one refusal: a {type, id} that resolves under two domains within
the winning scope (reachable in a green repo, since a type may be permitted in several domains) is
ambiguous — exit 2 naming the colliding paths (…is ambiguous…; repository is malformed, the
refusal promote already makes) rather than silently picking one. Cross-scope duplication is not
ambiguity — that is shadowing, resolved by the [<pid>, org] fall-through and flagged by validate
as scope.shadow. An artifact whose front-matter fails to parse is unresolvable and falls under the
show <type> <unknown-id> enumeration; diagnosing it stays validate's job (artifact.format).
For one scope (exactly one of --scope org / --project <id>; no --all), reports per
domain which of that domain's permitted types are present (≥1 artifact of that
(domain, type) in the scope) vs missing. A report describes; it never judges — coverage
always returns status: ok (exit 0), even for an empty scope. The invalid/exit-3 distinction is
validate's alone.
{
"scope": "payments",
"domains": [
{ "domain": "business",
"types": [ { "type": "capability", "present": true, "count": 1 },
{ "type": "principle", "present": false, "count": 0 } ] }
],
"unmapped": [ ],
"summary": { "required_types": 2, "present_types": 1, "missing_types": 1 }
}domains[] (sorted by domain) → types[] (sorted by type). summary rolls up the required
surface. unmapped[] lists present (domain, type) pairs the Manifest does not permit, so a
green coverage report cannot silently hide stray artifacts.
For one scope, computes the directed graph whose nodes are that scope's artifacts plus everything they reach by following resolved outbound references, and whose edges are those resolved references. Traversal goes through the resolver, so visibility and layering hold by construction — a project's graph may pull in the Org artifacts it depends on (fall-through), never another project's. Transitive, seeded from the scope's artifacts.
Like a report, a graph describes — always status: ok. Unresolved references surface in
data.dangling (each with a reason); cycles in data.cycles. Turning either into an error
stays validate's job (the reference_resolves / acyclic rules) — a graph and a validation
never disagree by both judging.
{
"scope": "payments",
"kind": "dependency",
"nodes": [
{ "path": "org/business/principle/data-locality.md", "scope": "org",
"domain": "business", "type": "principle", "id": "data-locality" }
],
"edges": [
{ "from": "projects/payments/business/capability/billing.md",
"to": "org/business/principle/data-locality.md", "via": "governed-by" }
],
"dangling": [ ],
"cycles": [ ],
"summary": { "nodes": 1, "edges": 1, "dangling": 0, "cycles": 0 }
}A node is identified by its repo-relative path (unique, self-describing). Orders: nodes by
path; edges by (from, via, to); dangling by (from, via, target.type, target.id); each
cycle is the sorted node paths of a cyclic strongly-connected component, rotated to start at its
smallest path; cycles sorted by first element.
Read-only, no scope, no EA repo required — they read the search paths (EA_PRESET_PATH +
preset cache; EA_PACK_PATH + pack cache). What is listed = what is resolvable now, ordered
deterministically (lexicographic by id, then version), first-wins on search-path order. Packs are
addressed as an exact immutable pack@version — there is no latest float in selection;
pack list groups versions under a pack id as a display convenience only.
pack show is a curated projection — enough for an agent to judge what selecting a method
activates (types is the method's requires.artifact_types) without reading raw pack YAML.
Read-only, no --confirm; they require an EA repo with a materialized active subset and
inspect the resolved active definitions (definitions/) — the same objects scaffold /
commit author against. pack show describes what could be selected; type show /
method show describe what is active here, in authoring detail.
// type show <type>
data: { id, title, description,
fields: [ { name, required: <bool>, description?, type?, enum? } ], // schema order
sections: [ { id, heading, guidance } ], // advisory body template; [] when none
relationships: [ { name, target_type, cardinality, origin } ] }
// origin: "pack" (baseline, possibly overlay-overridden) | "overlay" (org-added)
// method show <id>
data: { id, title, summary,
requires: { artifact_types: [<type-id>, …], rules: [<rule-id>, …] },
steps: [ { guide, produces?, detail? } ] } // declared order; guidance, not a gatetype show's relationships are the effective edges (pack baseline + manifest relationship
overlay merged) — what scaffold and validate actually enforce — with each edge's origin
tagged. Live relationship candidates (which artifacts could fill an edge) stay a
scaffold-time, scope-aware concern.
Read-only. Resolves one {type, id} the way references do — with --project <pid> the lookup
walks [<pid>, org] (a project artifact shadows an org twin), else org only — and returns the
resolved artifact's front-matter flat as top-level data keys (its own vocabulary: envelope
identity + field values + relationship arrays) plus two structural keys: scope (where it
resolved, so fall-through is never silent) and body (the verbatim markdown, "" when empty).
// show <type> <id> [--project <id>]
data: { domain, type, id, name, /* …the type's field + relationship keys… */,
scope: "org" | "<project-id>", // where the artifact resolved
body: "<verbatim markdown>" } // "" when the body is emptydata key order is fixed by the canonical serializer (lexicographic), so byte-identical inputs
yield byte-identical output. --fields/--list-fields project over the runtime-derived vocabulary
(above); a type/artifact/project miss or an ambiguous collision is exit 2 (the not-found contract)
before projection, so a miss with --list-fields still exits 2. show composes what
model.load already knows — no write path, no persisted state, no judgment.
contract_versionis a single version string (e.g."1.0"); breaking changes bump the major.- Adapters target a version and SHOULD pass
--contract-version; a core that cannot serve the requested version returns a usage error (exit 2) rather than guessing. - Additive, backward-compatible changes (new optional
datafields, new commands, new diagnosticcodes) bump the minor and do not break existing adapters.
Validate (CI gate):
$ ea validate --all
{ "contract_version":"1.1","command":"validate","status":"invalid",
"data":{"scopes_checked":3,"artifacts_checked":41},
"diagnostics":[{"severity":"error","code":"reference.dangling","scope":"payments",
"path":"business/capability/billing.md","rule":"refs-resolve","message":"…"}] }
# exit 3 → CI fails the build
Guarded delete (two-phase):
$ ea project delete legacy-billing
{ …,"status":"ok","data":{"plan":{"deletes":[ "projects/legacy-billing/**" ],
"fingerprint":"sha256:…"},"confirmation_token":"sha256:abc123"} } # exit 0, nothing removed
$ ea project delete legacy-billing --confirm sha256:abc123
{ …,"status":"ok","data":{"applied":{"deleted":17}} } # exit 0, removed
# (if the project changed in between: exit 4, refused)