Skip to content

Latest commit

 

History

History
410 lines (336 loc) · 23.1 KB

File metadata and controls

410 lines (336 loc) · 23.1 KB

CLI Contract

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.

Invocation

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 / stderr discipline

  • 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 … | jq or gate CI on the envelope without scraping logs.

Response envelope

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 vs exit code

status and the exit code agree but serve different consumers:

  • status: ok → exit 0. Operation succeeded; for validate, the source of truth is valid.
  • status: invalid → exit 3. The tool ran fine but the architecture is invalid (validation findings in diagnostics). This is the key distinction CI relies on.
  • status: error → exit 1/2/4 (the tool could not complete the operation).

Diagnostic object

{
  "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.

Exit codes

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).

Write & confirm protocol

All mutating commands are dry-run by default and follow one protocol:

  1. Plan (default). The command computes the intended change and returns status: ok with data.plan describing every file create/update/delete, plus data.confirmation_token. Where review needs more than the plan, the dry-run also carries data.preview (the full after content) and — for commitdata.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).
  2. Token. confirmation_token = "sha256:" + hex(sha256(canonical_plan_bytes)), where canonical_plan_bytes is 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.
  3. 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: ok with data.applied;
    • if it does not match (state drifted since planning) → refuses, exit 4, no write.

This is identical interactive and headless (no prompts), and no plan can be applied against a changed repository.

Command catalog

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.

Scope selector

  • Scoped commands accept exactly one of --scope org or --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).

Read-side projection (--fields)

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 datacontract_version, command, status, and diagnostics are emitted exactly as without projection. A projected validate keeps its exit-3 verdict; diagnostics is envelope-level and never projected.
  • Subtractive only. --fields can 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 like summary are 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. --fields with no surviving keys yields {}, not an error.
  • Runtime discovery, no hardcoding. A surface learns the field names at runtime: an unknown name in --fields is a usage error (exit 2) whose diagnostic enumerates the valid fields; --list-fields asks deliberately. For the record-shaped verbs the vocabulary is each verb's data top-level keys, fixed under contract_version — it never varies by repo.
  • ea show is 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 as type show's field list, so P1 (byte-reproducibility for identical inputs) holds; what is versioned for show is 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).

The self-describing not-found contract

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).

Selected data contracts

report coverage

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.

graph dependency

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.

Discovery verbs (preset / pack)

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.

// preset list
data: { presets: [ { id, title, description, domains: [<domain-id>, ], methods: <int> } ] }

// preset show <id>
data: { id, title, description,
        domains: [ { id, title, types: [<type-id>, ] },  ],
        methods: [ "<pack>@<version>#<method>",  ] }        // exact pin-native selections

// pack list
data: { packs: [ { id, title, description, versions: [<semver>, ], methods: <int> } ] }

// pack show <pack@version>
data: { id, version, title, description,
        artifact_types: [ { id, title },  ],
        rules:          [ { id, kind, applies_to },  ],
        methods:        [ { id, title, summary, types: [<type-id>, ] },  ] }

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.

Inspection verbs (type show / method show)

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 gate

type 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.

Single-artifact inspect (show)

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 empty

data 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.

Versioning

  • contract_version is 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 data fields, new commands, new diagnostic codes) bump the minor and do not break existing adapters.

Worked examples

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)