ajhahn.de
← eeco
Markdown 409 lines
<div align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="../assets/eeco_logo_dark.png">
    <img src="../assets/eeco_logo_light.png" alt="eeco" width="280">
  </picture>

<h1>Public API</h1>

<p><i>The exhaustive enumeration of eeco's frozen public surface.</i></p>

<p>
    <a href="../README.md"><b>README</b></a> ·
    <a href="../VISION.md"><b>Vision</b></a> ·
    <a href="COCKPIT.md"><b>Cockpit</b></a> ·
    <a href="USAGE.md"><b>Usage</b></a> ·
    <a href="ARCHITECTURE.md"><b>Architecture</b></a> ·
    <b>Public API</b> ·
    <a href="../EXTENDING.md"><b>Extending</b></a> ·
    <a href="../CONTRIBUTING.md"><b>Contributing</b></a> ·
    <a href="UPGRADING.md"><b>Upgrading</b></a> ·
    <a href="../VERSIONING.md"><b>Versioning</b></a> ·
    <a href="../CHANGELOG.md"><b>Changelog</b></a> ·
    <a href="../SECURITY.md"><b>Security</b></a>
  </p>

</div>

---

This document is the exhaustive enumeration of eeco's **frozen public
surface**. Companion to [`USAGE.md`](USAGE.md) (the user-facing
reference) and [`ARCHITECTURE.md`](ARCHITECTURE.md) (the architecture
overview).

## Scope

From v0.1.0 eeco follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
under the pre-stability caveat of [`VERSIONING.md`](../VERSIONING.md) §2.1. The
semver promise covers exactly the items listed here and nothing else. A breaking
change to any one of them takes a MAJOR bump once eeco is post-1.0; while eeco is
on the `v0.x` line a MINOR MAY make it, with a CHANGELOG migration note. Internal
package APIs under `internal/` are not part of this surface and may change in any
release.

v0.1.0 is the first public release of the cockpit-generator product:
every CLI command, flag, config key, JSON top-level key, queue and
ledger format, memory frontmatter field, and builtin workflow name
enumerated below is part of the tracked surface from this release.

Cockpit (`eeco cockpit …`, the `cockpit-sync` builtin, `handover_glob`,
and `state/cockpit.json`) is **pre-1.0 and not yet frozen** — see
[`COCKPIT.md`](COCKPIT.md). It is deliberately **not** enumerated in the
frozen lists below.

## Frozen surface

### CLI commands and flags

Every command and flag documented in [`USAGE.md`](USAGE.md) §4. The
`eeco gates check-attribution` subcommand and its flag set
(`--paths`, `--commits`, `--no-commits`, `--no-files`, `--exclude`)
are frozen as documented in §9a; additional `eeco gates …`
subcommands are additive and may land in any minor release. Additional
`eeco history …` subcommands (e.g. `compact`) are likewise additive and
may land in any minor release.

The `eeco config` verb group (`list`, `get`, `set`, `import`, the
`--global` flag on `set`, and the `--force` flag on `import`) is covered
by the [`USAGE.md`](USAGE.md) §4 command list. Its existence is frozen;
its exact output wording is a human-readable, evolving readout and is not
frozen. The `--global` flag on `eeco cockpit target` and the `--from
<path>` flag on `eeco init` are likewise additive and covered by §4. What
`--from`/`import` copies (config.local, cockpit.json, workflows — never
knowledge, state, or bug reports) is the frozen behaviour; the merge
wording is not.

The `eeco go --json` output is a JSON object whose **top-level keys**
are frozen: `project`, `profile`, `gate`, `top_level`, `initialized`,
`workflows`, `where_to_look`, `knowledge`, `open_decisions`. Their
meanings are documented in [`USAGE.md`](USAGE.md) §13. Nested object
fields are best-effort and may gain keys in a minor release.

The `eeco go --metrics` flag is **additive and not part of the frozen
surface**: it prints a human-readable assembly readout to stderr (timing,
brief size, and an estimated compression of the knowledge layer) and does
**not** appear in or alter the frozen `eeco go --json` nine-key surface.
Its wording and the token estimates are not frozen.

The `eeco stats` verb is frozen (its existence is covered by the
[`USAGE.md`](USAGE.md) §4 command list), but — like `--metrics` — its
**readout wording and the displayed figures are not frozen**: they are a
human-readable, evolving readout aggregated from the `state/ai-calls.json`
ledger, so a future minor release may add fields or formats.

The `eeco report-bug --submit` flag is **additive and not part of the
frozen surface**: the flag's existence is covered by [`USAGE.md`](USAGE.md)
§4, but its **submission mechanism and the lines it prints are not
frozen** — `--submit` currently opens the pre-filled issue URL in a
browser, and a future minor release may change or add to how the report
reaches the project. The invariant that `eeco report-bug` never sends
anything without a human action is part of the frozen behaviour.

The `eeco ask --json` output is a JSON object whose **top-level keys**
are frozen: `question`, `memory`, `code`. Both arrays are always present
(an empty list, never null). Their meanings are documented in
[`USAGE.md`](USAGE.md) §13. Nested object fields (the per-hit `score`,
`path`, `line`, etc.) are best-effort and may gain keys in a minor
release.

### Workflow contract

- **Exit codes:** `0` clean, `1` finding or failure, `2` blocked (a
  required tool is missing), `3` AI pass deferred (no consent).
- The `Env` value passed to a workflow.
- The read-only `gitx` helpers a user workflow may import: `Available`,
  `TrackedFiles`, `HeadSHA`, `ChangesSince`, `RemoteTags`,
  `LatestSemverTag`, `LastCommitDate`, `SemverTags`.

### Config keys

Keys recognised in `<workspace>/config.local`: `profile`, `gate`,
`stale_days`, `attribution_pattern`, `automation`, `ai_command`,
`ai_budget`, `ai_provider`, `ai_model`, `ai_api_key_env`,
`session_settings_path`, `bug_report_dir`, `context_path`,
`context_budget`, `brief_include_notes`, `session_start_pinned_bodies`,
`session_start_docs`,
`session_start_mailbox`, `session_start_roadmap_glob`, `session_files`,
`version_locations`, `version_anchor`, `pre_commit_workflows`,
`post_merge_workflows`, `workspace_history`.
Unknown keys are tolerated and preserved for forward compatibility.

Config resolves in three layers, each overriding the previous: built-in
defaults → the **user-global** file → the **workspace** `config.local`.
The global file is `config.local` under the user-global config directory,
resolved as `$EECO_CONFIG_HOME`, else `$XDG_CONFIG_HOME/eeco`, else
`$HOME/.config/eeco`. The global layer holds the same keys as a workspace
`config.local`; a project inherits it unless its own `config.local`
overrides the key. `eeco config set --global …` and
`eeco cockpit target --global …` are the only commands that write outside
a repository, and they write only into that global directory — never the
tracked tree. The three-layer resolution order is a frozen behaviour; the
global directory locations above are part of the contract.

The `workspace_history` key selects whether `eeco init` stands up a
private, local git repository inside the gitignored workspace directory
to version eeco's own knowledge layer, and how often it commits:
`off` (no repo), `manual` (the default — commit only on
`eeco history snapshot`), or `auto` (commits automatically after each
mutating verb). An unknown value falls back to the default. The repo has
no remote and is never pushed; see [`USAGE.md`](USAGE.md) §11a.

The `gate` key is repeatable: each occurrence declares one step of the
project's parse/build gate chain (a whitespace-split command). The
first occurrence resets the profile default so the operator-declared
chain fully replaces it; subsequent occurrences append. A lone empty
`gate=` clears the chain. The `gate` builtin workflow runs the chain in
declared order, with the repository root as the working directory,
stopping at the first failing step.

The four `session_start_*` keys tune what the bundled session-start
hook (`eeco hooks session-start on`) surfaces:

- `session_start_docs` — repeatable, repo-relative path; explicit
  reading routine in order. When unset the hook auto-detects from a
  built-in list (`docs/PUBLIC_API.md`, `docs/ARCHITECTURE.md`,
  `CHANGELOG.md`, `ARCHITECTURE.md`, `docs/USAGE.md`, `README.md`).
  Paths that point outside the repo are rejected at parse time.
- `session_start_mailbox` — repo-relative filename of the mailbox the
  hook checks for unprocessed content. Default: `Ideas.md`. Empty
  disables the mailbox block.
- `session_start_roadmap_glob` — glob, relative to the repo root, for
  the live planning surface; the most-recently-modified match is
  appended to the reading routine. Default: `roadmap*.md`. Empty
  disables roadmap discovery.
- `session_start_pinned_bodies` — boolean, default `false`. When
  `true`, the bundled session-start hook composes a fourth block that
  emits the full body of every `pin: true` memory fact. The
  `--with-pinned-bodies` flag on `eeco hooks session-emit` enables
  this for one invocation without editing config; the flag and the
  config key compose harmlessly.

The `eeco hooks session-emit` subcommand also accepts `--if-initialized`,
which suppresses all output unless the working directory holds an
initialized eeco workspace (the `IsInitialized` 5-subdir check). It
composes with `--with-pinned-bodies`. The command installed by
`eeco hooks session-start on` (and rewritten by `… refresh`) now carries
this flag, so the bundled brief emits only inside an eeco workspace —
repos without one stay silent regardless of which docs they contain.

The three `ai_*` provider keys select and tune which provider gated AI
passes use:

- `ai_provider``cli` or `none`, or empty to auto-select. `cli` uses
  the CLI provider when `ai_command` is set, otherwise the
  not-configured stub (every pass parks). Empty, `none`, or any
  unknown/legacy value (e.g. `anthropic`) auto-selects the CLI provider
  when `ai_command` is set, otherwise the not-configured stub — never a
  config error. eeco runs no in-binary model client; the AI lives in the
  harness eeco configures.
- `ai_model` — an inert legacy key. It is read and passed through but
  ignored by the CLI provider; there is no native API path to consume
  it. Kept only so an old `config.local` loads unchanged.
- `ai_api_key_env` — an inert legacy key naming an environment variable.
  The retired native provider read its API key from it; the CLI provider
  does not. Kept only for backward-compatible config loading; no key
  value is read from or written to disk.

The `version_locations` key is repeatable; each value is a
`<repo-relative-path>:<RE2-regex>` pair split on the first colon, and
the regex must declare at least one capture group. Absolute paths and
`..` traversal are rejected at parse time. The `version-sync` builtin
workflow consumes the list; with no entries the workflow exits 0. The
reserved value `auto` switches `version-sync` to scan a fixed set of
common version files instead of an explicit list; it must stand alone
and cannot be mixed with `path:regex` entries.

The `version_anchor` key is single-valued and selects the source of
truth `version-sync` compares declared `version_locations` against.
Three modes: unset (default) keeps the consistency-only behaviour
(first declared location is the anchor); `tag` uses the latest
semver-shaped (`vX.Y.Z`) tag reachable from HEAD and lets declared
locations be semver-`>=` the tag so a release commit can bump declared
locations ahead of the not-yet-pushed tag (backward-drift still fails);
a `<repo-relative-path>:<RE2-regex>` value designates a file whose
captured version is the source of truth, and declared locations must
strict-equal it. Absolute paths and `..` traversal in the
designated-file form are rejected at parse time; the regex must
declare at least one capture group.

The `pre_commit_workflows` key is repeatable; each value is one
builtin workflow name. The first occurrence in the file resets the
binary default (`leak-guard`, `version-sync`), subsequent occurrences
append. An empty value clears the list and `eeco hooks pre-commit on`
refuses to install. Whitespace inside a value is rejected at parse
time; unknown workflow names are rejected at hook-install time.

The `post_merge_workflows` key is repeatable with the same semantics as
`pre_commit_workflows`; each value is one builtin workflow name run by
the `post-merge` hook after a merge. The first occurrence resets the
binary default (`memory-drift`, `doc-drift`, `manifest-refresh`),
subsequent occurrences
append. An empty value clears the list and `eeco hooks post-merge on`
refuses to install. Whitespace inside a value is rejected at parse time;
unknown workflow names are rejected at hook-install time. The binary
default additionally wires the pre-1.0 `cockpit-sync` machinery (not part
of the frozen builtin enumeration — see [`COCKPIT.md`](COCKPIT.md)).

The `context_budget` key is single-valued: a non-negative integer byte
cap on the file `eeco go --write` renders. When positive, `eeco go
--write` trims the saved brief down a deterministic ladder (full, then
the smaller `--brief` form with progressively shorter lists) until it
fits the budget. `0` (the default) means no cap; an empty value resets
to the default; a negative value is rejected at parse time.

The `brief_include_notes` key is single-valued and boolean: when set
truthy, `eeco go` adds a **Recent notes** section to the Markdown
brief, listing the five newest files under `<workspace>/notes/`. The
JSON brief (`eeco go --json`) is unchanged — the nine frozen
top-level keys remain the only surface; notes live on the Markdown
channel only. Accepted values are the standard `strconv.ParseBool`
set (`true`/`false`, `1`/`0`, `t`/`f`, case-insensitive); an empty
value resets to the default `false`; anything else is rejected at
parse time.

The `session_files` key is repeatable; each value declares one
text/markdown file where the `session-start` hook maintains a marker
block carrying the same content `eeco hooks session-emit` prints. An
entry is either repo-relative (held inside the repo by the same
path-traversal guard `session_start_docs` uses) or absolute (matching
the precedent set by `session_settings_path`). Whitespace inside a
value is rejected at parse time. With no entries the file-delivery
channel is disabled; the JSON-settings channel keyed by
`session_settings_path` is independent and either channel alone is
enough for `eeco hooks session-start on`. The block is fenced by
`<!-- eeco:session:start -->` / `<!-- eeco:session:end -->`; bytes
outside the marker pair are never edited. `eeco hooks session-start
refresh` re-renders the block; `eeco hooks session-start off` removes
the block (or the whole file when eeco created it and the block was
its only content).

### Memory frontmatter

Each memory fact carries flat, shell- and Go-parseable frontmatter:

| Field         | Meaning                                                          |
| ------------- | ---------------------------------------------------------------- |
| `name`        | kebab-case identifier.                                           |
| `description` | one-line summary, used for relevance matching.                   |
| `type`        | one of `user`, `feedback`, `project`, `reference`, `finding`.    |
| `created`     | creation date, `YYYY-MM-DD`.                                     |
| `last_used`   | date the fact was last surfaced, `YYYY-MM-DD`.                   |
| `ref`         | optional repo-relative path; garbage collection validates it.    |
| `expires`     | optional expiry date, `YYYY-MM-DD`.                              |
| `status`      | optional; for `finding` facts only — `open` or `resolved`.       |
| `pin`         | `true` or `false`; a pinned fact is never garbage-collected.     |
| `source`      | optional snippet (≤120 chars) of what triggered the fact.        |
| `agent`       | optional assistant identity that recorded the fact.              |
| `disabled`    | optional; `true` hides the fact from `eeco go` and `eeco ask` and exempts it from garbage collection. Omission means `false`. |

`source`, `agent`, and `disabled` are additive and optional on the
wire: a fact file without them still loads.
`disabled` is omitted from serialisation when `false` so legacy facts
round-trip without gaining a new line. The `eeco add fact` CLI requires
`--provenance` (which populates `source`) for `--type=feedback` and
`--type=user` facts; the store layer remains permissive so a
hand-authored fact can still be loaded if its provenance is unknown.

### Queue file format

`state/queue.md` is a Markdown checklist. Each item is one line —
`- [ ] **<kind>** — <title> _(<project>, <date>)_` — followed by an
indented detail line. The count of unchecked items is the queue count.

### Ledger formats

Three ledger files inside `<workspace>/state/` are part of the frozen
surface. All follow the same additive discipline: adding a top-level
key or a per-record field is non-breaking; older ledgers without it
still load. Removing or renaming a ledger filename, a top-level key,
or a documented per-record field is breaking and ships in a major
release only.

**`state/hooks.json`** records every hook eeco has installed, so each
one can be cleanly reverted. The toggleable hook names are
`pre-commit`, `post-merge`, `session-start`, `commit-msg`, and
`commit-guard` (`eeco hooks <name> on|off`); the ledger carries one
record per hook (`pre_commit`, `post_merge`, `session_start`,
`commit_msg`, `commit_guard`). The `session_start` record may carry an
additional `files[]` array — one entry per file the file-delivery
channel manages (`session_files`), each recording its `path`, the
`sha256` of the eeco-written block at install time, and a `created`
boolean; a ledger without the field still loads. The `commit_msg` and
`commit_guard` records are optional; a ledger without the key still
loads (absent = off). Additive `eeco hooks` subcommands and managed
hook names (like `commit-guard`) are non-breaking, the same way additive
`eeco gates` subcommands are: they extend the surface without changing
the frozen verb set.

**`state/evolve-history.json`** records every workflow candidate the
`evolve` builtin has surfaced, so a recurring signal is proposed
exactly once in its lifetime and the operator's resolve-state on the
queue row can be reconciled back into the ledger. Top-level shape:
`{ "records": [ … ] }`. Per-record fields:

| Field               | Meaning                                                                 |
| ------------------- | ----------------------------------------------------------------------- |
| `signal_kind`       | the signal class (`commit-type` today; future kinds are additive).      |
| `signal_key`        | the specific signal value (e.g. `fix`).                                 |
| `count_at_proposal` | the signal's count in the inspected history window at proposal time.   |
| `queue_kind`        | the queue row's `Kind` (always `evolve`).                               |
| `queue_title`       | the queue row's `Title` (e.g. `Workflow candidate: fix-workflow`).      |
| `proposed_at`       | RFC 3339 UTC timestamp of the proposal.                                 |
| `resolved`          | optional; `true` once the queue row's checkbox is ticked. Omitted false. |
| `resolved_at`       | optional; RFC 3339 UTC timestamp of the resolution. Omitted when empty. |

A corrupt ledger file degrades to the empty ledger so a broken file
never wedges `evolve`; the next save rewrites it. The same
additive-field discipline applies — later slices may add
fields (e.g. accepted-vs-rejected disambiguation) without breaking
older readers.

**`state/ai-calls.json`** records every gated provider attempt — ran,
parked, or gated-out — so the operator has an audit trail of what the
AI was asked and what it produced. It stores hashes, never raw text:
the prompt and response bodies live under `state/parked/` when
applicable and are not duplicated here. Top-level shape:
`{ "records": [ … ] }`. Per-record fields:

| Field             | Meaning                                                                       |
| ----------------- | ----------------------------------------------------------------------------- |
| `label`           | the call's parking/ledger key (e.g. `evolve`, `bug-sweep`).                   |
| `provider`        | the selected provider's name (`cli` or `none`; `anthropic` may appear in old ledgers — a legacy/tolerated value, not selectable). |
| `model`           | optional; the model the provider resolved (omitted when none was resolved).   |
| `prompt_sha256`   | SHA-256 of the folded prompt (the same bytes the parked file stores).         |
| `response_sha256` | optional; SHA-256 of the response text. Omitted on a parked pass, except a pass blocked by the attribution filter, which still records the blocked response's hash. |
| `ran`             | `true` when the provider produced text.                                       |
| `parked`          | `true` when the pass was parked (no consent, over budget, or provider error). |
| `park_reason`     | optional; why the pass parked. Omitted when empty.                            |
| `tokens`          | `{ input, cached_input, output }` token counts; zero on a pass parked before the provider was called. |
| `tools`           | optional; the tool names the model invoked in this round (a tool-using chat pass records one entry per round). Omitted when empty. |
| `ts`              | RFC 3339 UTC timestamp of the attempt.                                        |

A corrupt `ai-calls.json` degrades to the empty ledger and the next
write rewrites it; recording is best-effort and never turns a gated
pass into a hard failure.

### Builtin workflow names

`comment-hygiene`, `leak-guard`, `version-sync`, `gate`, `bug-sweep`,
`handover-refresh`, `evolve`, `memory-drift`, `doc-drift`,
`manifest-refresh`. Removing or renaming any one of these is a breaking
change.

### `eeco version` output

The first line, `eeco <version>`, is stable. The indented `commit:` and
`built:` lines are best-effort build metadata; they may change and are
not intended to be parsed.

## Not frozen

Internal package APIs under `internal/` — the packages the CLI and the
builtin workflows are implemented on top of — are deliberately excluded
from the public surface and may change in any release.

---

[← Prev: Architecture](ARCHITECTURE.md) · [Next: Extending →](../EXTENDING.md)