Markdown 1595 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>Usage</h1>
<p><i>Everything eeco can do, verb by verb.</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> ·
<b>Usage</b> ·
<a href="ARCHITECTURE.md"><b>Architecture</b></a> ·
<a href="PUBLIC_API.md"><b>Public API</b></a> ·
<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>
---
Everything eeco can do. The runtime story, directory layout, trust
boundaries, and frozen public surface are documented in
[`ARCHITECTURE.md`](ARCHITECTURE.md).
## 1. Install
Single static binary, zero runtime dependencies. Four install routes:
pre-built binary, Homebrew, Scoop, from source.
### 1.1 Pre-built binary (recommended)
Download the archive for your platform from the
[releases page](https://github.com/ajhahnde/eeco/releases), verify it
against `SHA256SUMS`, extract the `eeco` binary, and put it on your
`PATH`.
| OS | Arch | Archive |
| --------- | ------- | ------------------------------------------ |
| `darwin` | `amd64` | `eeco_<version>_darwin_amd64.tar.gz` |
| `darwin` | `arm64` | `eeco_<version>_darwin_arm64.tar.gz` |
| `linux` | `amd64` | `eeco_<version>_linux_amd64.tar.gz` |
| `linux` | `arm64` | `eeco_<version>_linux_arm64.tar.gz` |
| `windows` | `amd64` | `eeco_<version>_windows_amd64.zip` |
| `windows` | `arm64` | `eeco_<version>_windows_arm64.zip` |
Checksum verification:
```
shasum -a 256 -c SHA256SUMS
```
Signature and provenance (recommended). `SHA256SUMS` is signed
keylessly with cosign — no key to fetch or trust on file; the signing
identity is the release workflow itself. Verify it:
```
cosign verify-blob \
--certificate SHA256SUMS.pem \
--certificate-identity-regexp \
'^https://github.com/ajhahnde/eeco/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--signature SHA256SUMS.sig \
SHA256SUMS
```
Each archive also carries GitHub build provenance. Verify a downloaded
archive came from this repository's release workflow:
```
gh attestation verify eeco_<version>_<os>_<arch>.<ext> --repo ajhahnde/eeco
```
macOS Gatekeeper. The `darwin` binaries are distributed unsigned by
design; their integrity is established by the `SHA256SUMS` signature
and provenance above rather than Apple's notary service. After
extracting, clear the quarantine attribute the browser sets on the
download:
```
xattr -d com.apple.quarantine ./eeco
```
The cosign + provenance route above is the supported integrity check
on macOS; the darwin binaries are intentionally distributed without an
Apple Developer ID signature.
### 1.2 From source
Requires Go 1.24+.
Released archives are produced by GitHub Actions on every `v*` tag and
uploaded to the [releases page](https://github.com/ajhahnde/eeco/releases).
Running `make release` locally is an alternate path — useful for
offline rebuilds and for verifying that a published release is
byte-identical to a fresh build from the tag.
Reproducing the released binaries byte-for-byte requires the same Go
toolchain CI used. `go.mod` pins it; when your installed Go differs,
set `GOTOOLCHAIN=go1.24.2` so the build fetches and uses the pinned
version.
```
git clone https://github.com/ajhahnde/eeco
cd eeco
make build # ./eeco with injected version
make release VERSION=vX.Y.Z # cross-build the published matrix into dist/
make verify # go build ./... && go vet ./... && go test ./...
```
`make build` produces `./eeco` with `version`, `commit`, and `buildDate`
injected via `-ldflags` so `eeco version` reports the real build
identity. `make release` writes archives plus `SHA256SUMS` into `dist/`;
`make packaging` writes `eeco.rb` and `eeco.json` from the checksums.
### 1.3 Homebrew (macOS, Linux)
```
brew install ajhahnde/eeco/eeco
```
Installs the binary from the latest tagged release via the
`ajhahnde/eeco` tap. `brew upgrade eeco` follows new tags. The formula
is generated per release and published with the release assets as
`eeco.rb`.
### 1.4 Scoop (Windows)
```
scoop bucket add eeco https://github.com/ajhahnde/scoop-eeco
scoop install eeco
```
`scoop update eeco` follows new tags. The manifest is published with
the release assets as `eeco.json`.
## 2. First use in a project — `eeco init`
```
cd /path/to/your/repo
eeco init
```
`eeco init`:
- creates a **private, per-user workspace** at `<repo>/<owner>/.eeco/`
and adds `/<owner>/` to `.gitignore` if it is not already ignored, so
nothing eeco does can leak into your tracked history. `<owner>` is your
git `user.name`; override it with `--username NAME` or the
`EECO_USERNAME` environment variable (resolution order: flag, env, git
`user.name`, then a prompt). `--workspace NAME` renames the inner
`.eeco` engine directory;
- scaffolds the engine, memory store, builtin workflows, and state
inside that directory;
- detects the project **profile** (zig, python, node, go, generic) and
the matching parse/build gate, and classifies the project **type** to
choose the knowledge directories it scaffolds. Detection is a
deterministic marker scan; `init_detection_threshold` (a value in
`[0,1]` in `config.local`) is the confidence at or above which init
accepts the scan without prompting (`0`, the default, uses the
detector's own default). `--type CATEGORY` forces a type and skips
detection; `--ai` opts into a single gated AI pass for an ambiguous
tree.
**The one commit eeco makes.** When init adds the `.gitignore` line it
stages *only that file*, commits it as `eeco init`, and pushes — the
single sanctioned write to your git history (everything else eeco writes
stays in the gitignored workspace). `--no-commit` skips the commit and
push; `--no-push` commits locally without pushing. On a non-git or
not-yet-real repo the step is skipped with a manual hint, and any git
failure is a warning — init never fails because of it.
**Migrating an older workspace.** If a legacy `<repo>/.eeco` workspace
from before the per-user layout is found, init reports it and offers to
run `eeco migrate v1` (§4) before continuing; declining makes no changes.
This is also the "new project, professional setup" path: one command
gives a repo its private workspace plus the whole ecosystem.
## 3. The control center (TUI) — `eeco`
Run `eeco` with no arguments to open the control center. Not a terminal
(piped/CI)? It prints a one-screen status digest and exits 0 instead.
The control center opens on a home screen: a centred eeco logo at the
top, the version and one rotating usage tip below it, a hint above the
input, and a thin dim status footer (repo, profile, gate, automation,
memory and queue counts, hook state, last run) below the input. Every
command is discoverable by typing `/` (the slash-command palette) or
pressing `?` (the shortcut overlay), so the home screen stays
uncluttered. Typing or running a command collapses the home view to
input + footer so streamed output reads cleanly above; clearing the
input recovers the home view. Output stays in the normal terminal
scrollback after you quit — no full-screen takeover.
Typing `/` opens a slash-command palette: a live dropdown of every
command with its one-line purpose, between the input and the footer.
Filter it by typing (`/h` narrows to `/help` and `/hooks`), move the
highlight with Up/Down, and accept the selection with Tab or Enter; the
palette closes on a space, a newline, or when the input clears.
The input is a multi-line composer: type or paste a request that spans
several lines. Enter submits; Alt+Enter or Ctrl+J inserts a newline
(Ctrl+J is the fallback where a terminal swallows Alt+Enter). The box
grows with the draft up to eight rows and then scrolls internally,
shrinking back when you submit or clear.
Free text (no leading slash) no longer opens an in-binary chat. eeco
configures the harness that runs the AI (see `eeco cockpit`), so the
control center stays a deterministic command surface rather than a
second-rate agent: a free-text line prints a dim one-line reminder to
use a slash command (`/run`, `/memory`, …) or `?` for help, and spends
nothing. The in-binary chat turn, its read-only tool loop, and `/clear`
were retired in the move to the cockpit model.
Input accepts:
| Input | What it does |
| ------------------ | --------------------------------------------------------- |
| `/run [--ai] <wf>` | run a workflow (`--ai` opts into one gated pass) |
| `/queue` | show items awaiting a decision |
| `/memory` | list stored facts |
| `/gc` | run memory garbage collection |
| `/new <wf>` | scaffold a new workflow |
| `/hooks [<n> on\|off]` | show or toggle the opt-in reversible hooks |
| `/settings [<k> <v>]` | view or set the AI config (persists to config.local) |
| `/help` | command and key reference |
| `/quit` | leave the control center |
| plain text | a dim reminder to use a slash command (chat is retired) |
Keys: Up/Down browse command history at the top and bottom of the
composer (and move the cursor within a multi-line draft otherwise, or
move the palette highlight when it is open), Tab completes a command or
workflow name, Alt+Enter / Ctrl+J insert a newline, `?` toggles a
shortcut overlay, Esc interrupts a running task — an animated spinner in
the footer marks one in flight. `q` (on an empty line), `/quit`, and
Ctrl-C always leave the terminal in a sane state. `NO_COLOR` is honoured.
Plain text never spends silently: each conversation turn is consented and
budget-capped, and parked-and-queued when it cannot run (the conversation
does not advance on a parked turn). Commands never require AI.
## 4. Commands
```
eeco open the control center (digest if non-TTY)
eeco init [--no-track] [--from <path>] bootstrap the ecosystem in this repo (--from imports settings from another eeco project; --no-track skips the private workspace-history repo)
eeco migrate v1 [--yes] move a legacy <repo>/.eeco workspace under <username>/
eeco run <workflow> run one workflow (read-only / safe by default)
eeco run --ai <workflow> allow this run's gated, budget-capped AI pass
eeco new <workflow> scaffold a new workflow from the template
eeco gc run memory garbage collection
eeco queue print the workspace queue (resolve by editing queue.md)
eeco stats print cumulative AI usage from the call ledger (real token counts)
eeco hooks <name> on|off toggle an opt-in hook (reversible; names: pre-commit, post-merge, session-start, commit-msg, commit-guard)
eeco hooks session-start refresh re-render every session_files block from current project state
eeco hooks <name> refresh rewrite an installed hook (pre-commit, post-merge, commit-msg, commit-guard) with the current eeco binary path
eeco config list show every config key, its effective value, and origin (default | global | local)
eeco config get <key> print the effective value of one config key
eeco config set [--global] <key> <value> set a key in this workspace, or (--global) in the cross-project layer
eeco config import [--force] <path> copy config.local, cockpit.json, and workflows from another eeco project
eeco cockpit target [--global] list|add <t>|rm <t> manage the active (or cross-project) harness target set
eeco gates check-attribution [flags] scan tracked files + commit bodies for AI-attribution fingerprints
eeco update [--apply] check for a newer release; --apply verifies + swaps
eeco doctor run workspace and config diagnostics
eeco go [--brief] [--metrics] [--write|--json|--copy] print an AI-ready brief; --brief trims it; --metrics adds a stderr readout; --json emits JSON, --write saves, --copy clipboards
eeco ask [--limit N] [--json] "<question>" answer a question with ranked file:line pointers (no AI)
eeco docs new [--overwrite] <target> scaffold a tracked-tree doc (targets: vision, readme)
eeco docs refresh <target> re-render a scaffolded doc's marker-wrapped block (targets: vision, readme)
eeco docs compact [--keep-last N --heading <prefix>] [--archive <path>] [--dry-run] <path> archive old regions of <path> (markers, or heading-discovered with --keep-last) into a sibling
eeco history show the private workspace-history log (recent commits)
eeco history snapshot [-m <msg>] commit the current workspace state into the private history repo
eeco history compact [--dry-run] [--yes] squash the private history log into one commit (reflog-recoverable)
eeco guide page the in-binary user manual through the host pager
eeco uninstall [--yes] write a handoff summary, print the removal command, and de-init the private history repo (--yes skips the confirm)
eeco report-bug [--submit] file a structured bug report (--submit opens the pre-filled issue URL in your browser)
eeco add note "<text>" append a free-form note to the workspace
eeco add fact --description "<s>" [flags] "<body>" record a durable memory fact
eeco add task "<title>" append an item to the workspace queue
eeco adaptations <name> on|off toggle an AI-adaptation fact on or off
eeco workflows [<name> on|off] list scaffolded workflows or toggle one on/off
eeco show notes list the workspace notes, newest first
eeco show adaptations list AI-adaptation facts with their on/off state
eeco show prompt [name] list the prompt library or print one prompt body
eeco refresh-manifest [<dir>] rebuild the .ai.json manifests in the knowledge dirs
eeco version print the version
eeco help command reference
```
**Exit codes** (also the workflow contract): `0` clean ·
`1` finding/failure · `2` blocked (a required tool is missing) ·
`3` AI pass deferred (no `--ai`).
## 4a. Configuration — `eeco config`
Configuration resolves in **three layers**, each overriding the one
before it:
```
built-in defaults
→ user-global ~/.config/eeco/config.local (shared by every project)
→ workspace <repo>/<user>/.eeco/config.local (this project only)
```
Inspect and edit the two file layers with `eeco config`:
```
eeco config list # every key, effective value, and origin
eeco config get automation # one effective value, bare (for scripts)
eeco config set automation=manual # write the workspace layer (this project)
eeco config set --global automation=auto # write the cross-project layer
```
`set` accepts `key=value` or `key value`. A value is validated against the
same rules `config.local` uses, so a typo'd key or a malformed number is
rejected before anything is written. (Floor-invariant keys such as
`automation` tolerate any value and normalize at load, exactly as they do
in a hand-edited `config.local`.)
### Sharing settings across projects — the global layer
Set a key **once** with `--global` and every project inherits it, unless
that project overrides it in its own workspace `config.local`. This is the
git `--global` model: machine-wide defaults with per-repo overrides. The
global file lives at `~/.config/eeco/config.local` — or
`$XDG_CONFIG_HOME/eeco/config.local`, or wherever `EECO_CONFIG_HOME`
points (the override also makes the layer hermetic under tests).
`eeco config set --global …` is the one eeco command that writes outside a
repository, by design; everything else stays inside the project. It does
not need an initialised workspace, or even to be inside a repo.
Cockpit targets share the same model:
```
eeco cockpit target --global add cursor # new projects inherit this target set
eeco cockpit target list # falls back to the global set when this
# project has no cockpit.json of its own
```
### Copying settings from one specific project — `--from` / `import`
Where the global layer is a *live* shared default, `--from` is a *one-shot
copy* from one named project into another. It carries three things —
`config.local`, the cockpit selection (`cockpit.json`), and the scaffolded
`workflows/` — and nothing else (project-specific knowledge, state, and bug
reports never travel).
```
eeco init --from ~/other-project # bootstrap a new repo, then copy from another
eeco config import ~/other-project # copy into an already-initialised project
eeco config import --force ~/other-project # let the source win over existing files/keys
```
The source path may be the other repo's root or any directory inside it. Into a
**fresh** workspace the `config.local` is copied verbatim (full fidelity); into
an **existing** one it is key-merged. Without `--force`, files and keys the
destination already has are preserved; `--force` lets the source win. Importing
from a project of a different type may pull in type-specific keys (`profile`,
`gate`) — `eeco config set` fixes any you don't want.
## 5. Builtin workflows
| Workflow | Inspects | Writes | AI |
| ------------------ | -------------------------------- | ------------- | ---- |
| `comment-hygiene` | source/docs for tooling fingerprints | nothing | no |
| `leak-guard` | staged + tracked tree, commit msg | nothing | no |
| `version-sync` | declared `version_locations` | nothing | no |
| `gate` | the declared `gate` command chain | nothing | no |
| `bug-sweep` | code, statically | a triage ledger | opt |
| `handover-refresh` | code/docs/git history | queue proposal | opt |
| `evolve` | run history + repeated actions | queue | opt |
| `memory-drift` | memory facts with a `ref` | queue | no |
| `doc-drift` | `CHANGELOG.md` vs git tags | queue | no |
| `manifest-refresh` | knowledge dirs (paths + kinds) | `.ai.json` | no |
- **`comment-hygiene`** fails if any shippable file carries an
AI-attribution / tooling string.
- **`leak-guard`** blocks a commit that would leak an attribution
string, a `Co-Authored-By` trailer, or a private-workspace path into
tracked files.
- **`version-sync`** reports drift between the version strings declared
in `<workspace>/config.local`'s `version_locations` list. Each entry
is a `path:regex` pair (split on the first colon); the path is
repo-relative and the regex must declare at least one capture group
(group 1 captures the version string). With no `version_locations`
entries the workflow exits 0 (the gate is opt-in per project). Use
the multiline flag `(?m)` when the regex anchors on the start of a
line further down a file — Go's `^` is start-of-string by default.
Set `version_locations=auto` instead of an explicit list to let
version-sync discover the version locations itself. In auto mode it
scans a fixed, high-precision set of common version files — `VERSION`,
`CHANGELOG.md` (the first `## [vX.Y.Z]` heading), `package.json`,
`pyproject.toml`, and `Cargo.toml` — and reports drift across
whichever of them the project actually has. A file that is absent, or
present but carrying no version-shaped string, is skipped. `auto`
cannot be mixed with explicit `path:regex` entries — it is the whole
list or none of it — and composes with `version_anchor` the same way
an explicit list does.
The `version_anchor` config key selects the source of truth declared
locations are checked against. Three modes:
- **Unset (default) — consistency-only.** The first declared location
is the anchor; the rest must match it. Exits 0 when every declared
location agrees, 1 when any drifts, and 2 when a declared path is
missing on disk. The slice-1 behaviour, preserved for backward
compatibility.
- **`version_anchor=tag` — tag-anchor.** The latest semver-shaped
(`vX.Y.Z`) tag reachable from `HEAD` is the source of truth.
Declared locations must be semver-`>=` the tag, so a release commit
can bump declared locations ahead of the not-yet-pushed tag (the
tag is pushed after the commit). Backward-drift still fails. If no
semver-shaped tag is reachable yet, falls back to the
consistency-only behaviour with a note in the summary.
- **`version_anchor=<path>:<regex>` — designated-file mode.** Same
shape as a `version_locations` entry; the captured version is the
source of truth and every declared location must strict-equal it.
A missing path exits 2 (blocked) so a typo surfaces rather than
silently falling through to consistency-only.
Worked example. Declare two entries that both must carry the latest
released tag — the `CHANGELOG.md` heading and the matching footer
compare-link, which often drift when a release bumps one but
forgets the other — and enable tag-anchor so a release commit's
bump catches a forgotten location:
```
version_locations=CHANGELOG.md:(?m)^## \[v(\d+\.\d+\.\d+)\]
version_locations=CHANGELOG.md:(?m)^\[v(\d+\.\d+\.\d+)\]: https://github
version_anchor=tag
```
Once `version_locations` is declared, wire the gate into the
pre-commit hook so drift is caught before the commit lands rather
than only under `make gates`: `eeco hooks pre-commit on` installs a
`.git/hooks/pre-commit` that runs the default workflow chain
(`leak-guard` + `version-sync`) and blocks the commit on a non-zero
exit. See §9 for the override knob and the byte-identical reversal
contract.
- **`gate`** runs the project's declared parse/build gate — the ordered
command chain in `<workspace>/config.local`'s `gate` key — step by
step, with the repository root as the working directory, stopping at
the first failure. The `gate` key is repeatable: each occurrence adds
one step (a whitespace-split command), and the first occurrence resets
the profile default so the operator-declared chain fully replaces it.
A bare single-command `gate` is a one-step chain; a project with no
gate (the generic profile, or a lone `gate=`) is a clean no-op, so the
gate is opt-in per project. Every step's command is checked on `PATH`
before the first step runs, so a chain that cannot complete exits 2
(blocked); a step that exits non-zero exits 1 and stops the chain.
Run it with `eeco run gate`, or add `gate` to `pre_commit_workflows`
(§9) to run the whole validation chain before each commit.
Worked example. A Go project that wants vet, a static linter, and the
unit tests as one gate, run in order and stopping at the first to
fail:
```
gate=go vet ./...
gate=staticcheck ./...
gate=go test ./...
```
- **`bug-sweep`** does a static read and maintains an append-only triage
ledger (open findings → resolved log); with `--ai` it performs the
reasoning pass, otherwise it reports and parks the prompt.
- **`handover-refresh`** drafts a dated handover plus a "what's now
dead" diff and queues it for your approval — it never overwrites a
note directly.
- **`evolve`** watches for repetition and *proposes* new workflows into
the queue. It never activates one on its own. The
proposal pass is two-stage: a **deterministic signal extraction**
scans the most recent 40 commit subjects for repeated
conventional-commit types (`<type>(<scope>)?!?: …`) and surfaces one
workflow candidate per type that appears at least three times (cap
five candidates per run, sorted count-desc then key-asc). Each
candidate becomes its own `evolve` queue item — the operator
resolves or dismisses one at a time. The gated AI pass becomes
**optional enrichment** layered on top: `--ai` (or `automation=auto`)
opts into it; on no consent or any provider skip eeco still exits 0
when the deterministic pass found at least one candidate, and only
preserves the exit-3 (AI deferred) contract when there is genuinely
nothing to report — no consent **and** no deterministic candidates.
It also maintains a **repetition ledger** at
`<workspace>/state/evolve-history.json`. Each surfaced candidate
writes one record (signal kind + key, count at proposal, the queue
row's kind + title, the proposal timestamp); a re-run suppresses any
candidate whose `(signal_kind, signal_key)` is already in the
ledger, so a recurring signal is proposed exactly once in its
lifetime even after the queue item is ticked. Each run also
reconciles unresolved records against the queue: a record whose
queue row is now checked flips to `resolved: true` with a
`resolved_at` timestamp. Reconciliation runs once per evolve
invocation — the queue is not watched live, so an operator who
ticks a row between runs sees the ledger update on the next
`eeco run evolve`. The re-propose-on-signal-recurrence knob is a
follow-on slice; today a resolved record still suppresses. A
future slice may add a file-touch signal alongside the commit-type
one (deferred because it needs a new `gitx` helper).
- **`memory-drift`** flags a memory fact whose `ref:` file has been
committed on a later calendar day than the fact's own `created:` date
— a sign the fact may now describe code that has moved on. `eeco gc`
already catches a `ref:` that no longer exists on disk; `memory-drift`
catches the complementary case where the file is still there but has
changed since the fact was written. One review item per stale fact is
routed to the queue — eeco flags the drift, you reconcile the fact. It
needs git on `PATH` (it exits 2, blocked, without it) and exits 0 when
every ref-carrying fact is still current. Run it with `eeco run
memory-drift`, or wire it to run automatically after a merge with the
`post-merge` hook (§9), where it is in the default workflow list.
- **`doc-drift`** flags drift between the release sections in
`CHANGELOG.md` and the project's git tags. Two cases: a `vX.Y.Z` git
tag with no `## [vX.Y.Z]` CHANGELOG section (a release that was never
documented), and a `## [vX.Y.Z]` section with no matching tag (a
release documented but never tagged). The newest section is exempt: a
release commit adds the `## [vX.Y.Z]` heading before the `vX.Y.Z` tag
is pushed, so a section strictly ahead of the latest tag is the
expected release-in-progress state, not drift. One review item per
drift is routed to the queue — eeco flags it, you reconcile the
`CHANGELOG.md` or the tag. It needs git on `PATH` (it exits 2,
blocked, without it) and exits 0 when `CHANGELOG.md` is absent, no
tags exist yet, or every tag and section agree. Run it with `eeco run
doc-drift`, or let the `post-merge` hook (§9) run it automatically — it
is in that hook's default workflow list alongside `memory-drift` and
`manifest-refresh`.
- **`manifest-refresh`** rebuilds the per-directory `.ai.json` manifests
across the knowledge dirs — the project-type directories scaffolded
under `<repo>/<owner>/` as siblings of the engine workspace. Each
manifest is a deterministic skeleton — a JSON document `{"dir",
"purpose"?, "items":[{"path","kind"}…]}` with `kind` either `"file"`
or `"dir"`, sorted by path; the optional `desc`/`find_when` fields are
reserved for a future enrichment pass and stay empty in the
deterministic walk. The `.ai.json` file is skipped in its own walk, so
a re-run over unchanged dirs reproduces byte-identical output. Writes
land in the gitignored per-user area, never the tracked tree; it needs
no AI and never blocks. Run it on demand with `eeco refresh-manifest
[<dir>]` (the same deterministic rebuild, optionally scoped to one
knowledge dir), or let the `post-merge` hook (§9) run it.
Run any of them from the TUI or `eeco run <name>`.
These ten are the complete builtin set. Additional builtin workflows
are tracked as a post-v0.1.0 item.
### Scaffolding a workflow with `eeco new`
`eeco new <name>` scaffolds a user workflow under
`<workspace>/workflows/<name>/` from a per-profile template. The
scaffold layout is picked from the project's detected profile:
- `go` profile — `run` PATH-checks `go` and runs `go vet ./...` as
the inert default.
- `python` profile — `run` PATH-checks `python3` and runs
`python3 -m compileall -q .` as the inert default.
- `generic` profile (and any profile without a dedicated template:
`zig`, `rust`, `node`) — `run` is a bare sh stub that prints a
placeholder line and exits 0.
In every case the scaffolded `run` is a starting point — replace the
default body with the real check this workflow is meant to perform.
The chosen template is internal to eeco; the verb shape `eeco new
<name>` is unchanged.
### Toggling a scaffolded workflow on or off
Once scaffolded, a workflow is **on** by default; eeco runs it
whenever it is asked to (a manual `eeco run <name>`, a hook chain
that names it, the TUI). To pause a workflow without deleting it:
```
eeco workflows <name> off # mark <name> as disabled
eeco workflows <name> on # bring <name> back online
eeco workflows # list every scaffolded workflow + state
```
`off` plants an empty sentinel marker file at
`<workspace>/workflows/<name>/disabled`; the loader sees it and
reports the workflow as blocked (exit 2) without running it. `on`
removes the marker. Repeating the same action is a clean no-op.
Builtin workflows are unaffected by this toggle — their contract is
part of the frozen public surface. To remove a scaffolded workflow
for good, delete its directory.
## 6. Memory and garbage collection
The store holds one fact per file (flat frontmatter: name, description,
type, dates, optional `ref:`/`expires:`, `pin:`), with a regenerated
`MEMORY.md` index and `[[links]]` between facts. Types: `user`,
`feedback`, `project`, `reference`, `finding`.
`eeco gc` (or the Memory screen) keeps the store honest:
- a memory whose `ref:` path is gone, that has expired, a resolved
finding, or an unused reference past the stale window is **archived**
to the recoverable attic;
- anything **load-bearing** (project/feedback/user) is **queued for
your decision**, never silently deleted;
- `pin: true` exempts a fact permanently.
This is automated and conservative — the algorithm never loses a fact
that still matters; it asks you instead.
Re-running `eeco gc` (for example through the `post-merge` hook chain)
no longer files duplicate `gc-review` items for an unresolved finding:
an open queue row for the same fact + reason is recognised and the
second filing is skipped. Resolving the row (checking its box) lets a
future trigger file again, so the queue is never wedged on a stale
recommendation.
`eeco add fact` records a fact without hand-authoring the frontmatter:
```
eeco add fact --description "<summary>" [--type <type>] [--name <slug>] [--ref <path>] [--pin] [--provenance <text>] [--agent <name>] "<body>"
```
It writes one frontmatter file under `<workspace>/memory/` and
regenerates `MEMORY.md`. `--description` is the one-line summary that
drives relevance matching and is required. `--type` is one of `user`,
`feedback`, `project`, `reference`, `finding` and defaults to `project`
— the safe default, since `project`/`user`/`feedback` facts are queued
for review rather than archived. `--name` is the lower-kebab-case
filename stem; when omitted it is derived from `--description`. `--ref`
records a repo-relative path the fact documents (garbage collection
validates it); `--pin` exempts the fact from collection.
`--provenance "<text>"` records the snippet that triggered the fact
(`source:` in frontmatter, capped at 120 characters); `--agent <name>`
records which assistant filed it (`agent:`). Both are **required for
`--type feedback` and `--type user`** — those types are durable
adaptations to the operator, and an unauthored adaptation is the silent
drift `eeco show adaptations` exists to surface. Hand-authored facts
remain permissive: an older fact file without the new fields still
loads.
eeco refuses to overwrite an existing fact — pass a different `--name`,
or edit the file directly. The command needs an initialised workspace;
it writes only inside it (Constraint 1) and never stages or commits
(Constraint 6). Editing a fact later is `$EDITOR
<workspace>/memory/<name>.md`; removing one is `rm`.
### Auditing AI adaptations
A `feedback`- or `user`-typed memory fact is a durable adaptation to
the operator — every future assistant briefed by `eeco go` honours it.
`eeco show adaptations` lists every adaptation newest-first with its
provenance and on/off state, so the operator can audit *what the AI is
doing automatically now* without grepping the memory directory:
```
eeco show adaptations
```
`eeco adaptations <name> on|off` flips a `disabled:` flag on one fact
in place and regenerates the `MEMORY.md` index so the change shows at
once. A disabled fact is **preserved on disk** but hidden from
`eeco go` briefs and `eeco ask` rankings, so re-enabling is one CLI
flip:
```
eeco adaptations terse-feedback off # mute it without deleting
eeco adaptations terse-feedback on # restore it
```
`eeco gc` skips disabled facts so a deliberately muted adaptation
never gets routed for staleness review.
A disabled fact is hidden from the AI-facing surfaces (`eeco go`,
`eeco ask`, the TUI relevance ranker) but stays **visible — marked —
on the human audit surfaces**: the regenerated `MEMORY.md` index lists
it under a `## disabled` heading, and the TUI `/memory` screen tags its
line `[off]`. This holds for a fact of any type, so a disabled
`project` or `reference` fact is never silently lost from view.
`eeco show adaptations` is the audit surface; the fact file under
`<workspace>/memory/<name>.md` is the source of truth. Constraints 1 +
6 hold (writes inside the workspace; nothing staged or committed).
## 7. The queue — the only thing that interrupts you
Workflows and GC never act unilaterally on a meaningful change. They
append an item to the queue. View it with `eeco queue` (or `/queue` in
the TUI); the status digest shows the open count. The queue file is
yours — you resolve an item by ticking its checkbox (`[ ]` → `[x]`) or
deleting the row in `<workspace>/state/queue.md`. Nothing else nags you;
nothing runs unless you invoke it.
You can also file a queue item yourself — or have an assistant file one
for you — with `eeco add task`:
```
eeco add task "wire the man page into brew/scoop"
eeco add task --kind review --detail "check the brew formula" "audit packaging"
```
The title is the checklist row; `--kind` is the short queue tag (default
`task`); `--detail` is an optional elaboration printed beneath the row.
The item is appended to `<workspace>/state/queue.md` and shows up
alongside workflow- and GC-filed items in `eeco queue` (or `/queue`) and
the status digest's queue count. `eeco add task` writes only inside the
workspace (Constraint 1) and never stages or commits (Constraint 6); it
requires an initialised workspace. Like `eeco add fact`, it closes the
read/write loop — an assistant briefed by `eeco go` can route a decision
back into eeco's one decision channel instead of letting it evaporate.
## 8. AI configuration
AI is **opt-in** (`--ai`, or an `automation` level that grants standing
consent) and **budget-capped**. eeco no longer runs an in-binary model
client — it configures the harness that runs the AI. The one inference
path it keeps for its own chores is a **CLI provider** that shells any
program of your choice (prompt on stdin, reply on stdout); with none
configured, every AI pass is parked, never failed. Configure in
`<workspace>/config.local`, or set the common keys from the control
center with `/settings`:
```
automation = propose # manual | propose | scaffold | auto
ai_budget = 1 # gated passes per invocation; 0 disables AI
# CLI provider — any program, prompt on stdin, reply on stdout:
ai_command = yourcli --print
ai_provider = cli # cli | none; empty/unknown auto-selects
```
- **Auto-select (default).** With `ai_provider` empty or unset — or an
unknown/legacy value such as `anthropic` — a configured `ai_command`
picks the CLI provider; otherwise every AI pass is **parked and
queued**, never failed — the deterministic, non-AI result still
stands. `ai_provider = none` is also accepted; nothing here is ever a
config error.
- `ai_model` and `ai_api_key_env` are inert legacy keys: read and passed
through, but ignored by the CLI provider (there is no native API path).
They stay only so an old `config.local` loads unchanged.
- Only `automation=auto` is itself standing AI consent; every other
level needs an explicit `--ai` on the run.
- The same gating — consent, budget cap, and prompt-parking — wraps
every provider call, so a run is always safe and never an uncontrolled
spend. Every attempt is recorded to the AI-call ledger
(`state/ai-calls.json`): label, provider, resolved model, prompt and
response hashes, and token usage.
`/settings` (no arguments) shows the current values; `/settings <key>
<value>` writes one to `config.local`. A change applies the next time
eeco starts; a long-lived session keeps the budget cap it began with.
### Cumulative AI usage — `eeco stats`
`eeco stats` aggregates that ledger (`state/ai-calls.json`) into a
one-glance readout of how much AI eeco has actually used on this project:
```
eeco stats
eeco stats: 12 AI calls (9 ran, 3 parked) from 2026-05-21 to 2026-05-30
tokens: 18432 input · 4096 cached · 6210 output (28738 total)
by provider: cli 9 · none 3
```
It reports the total number of gated AI calls, how many actually ran
versus were parked, the cumulative token counts, the per-provider call
breakdown, and the recorded date range. Unlike `eeco go --metrics`,
whose token figures are bytes/4 estimates marked with `≈`, these token
totals are the **real provider counts** the ledger stored, so they carry
no `≈`. The command is **read-only** and makes **no** AI call; an
uninitialised workspace or an empty ledger reports `no AI calls recorded
yet` and still exits 0. The exact wording and the displayed figures are a
human-readable readout and are **not** part of the frozen surface.
## 9. Hooks (opt-in, reversible)
Off by default. Inspect with `eeco hooks` (or `/hooks`); toggle with
`eeco hooks <name> on|off` (or `/hooks <name> on|off`). Names:
`pre-commit`, `post-merge`, `session-start`, `commit-msg`,
`commit-guard`.
- **pre-commit** — a local `.git/hooks/pre-commit` (untracked,
repo-scoped) that runs the configured builtin workflows in declared
order and stops at the first non-zero exit, blocking the commit when
any step fails. Defaults to `leak-guard` (blocks a leaking commit)
followed by `version-sync` (silent on projects that have not declared
`version_locations`, so opt-in per project). Override the list with
one or more `pre_commit_workflows` keys in `config.local`; the first
occurrence resets the binary default, subsequent occurrences append.
An empty `pre_commit_workflows=` value clears the list and `eeco
hooks pre-commit on` refuses to install. The hook is installed only
if no pre-commit hook already exists, and removed only when the
on-disk script is byte-identical to what eeco wrote; a foreign or
hand-edited hook is always left untouched. After upgrading the eeco
binary (for example a `brew upgrade` that moves the binary) or editing
`pre_commit_workflows`, run `eeco hooks pre-commit refresh` to rewrite
the installed script with the current binary path and workflow set.
- **post-merge** — a local `.git/hooks/post-merge` (untracked,
repo-scoped) that runs the configured drift-detection workflows after
a `git merge` / `git pull` — the moment another author's changes land,
the natural time to re-check whether eeco's recorded state has drifted
from the code. Defaults to `memory-drift` followed by `doc-drift` (both
silent no-ops on a project with no memory `ref:` facts and no
CHANGELOG/tags). Unlike pre-commit it does **not** block: the merge has
already completed, so a finding or a missing tool surfaces as a queue
item, never as a hook failure (each step's exit is swallowed and the
hook does not use `set -e`). The drift workflows file at most one open
queue item per finding — a repeated run does not pile up duplicates of
a finding still awaiting a decision. Override the list with one or more
`post_merge_workflows` keys in `config.local` (same reset-then-append
semantics as `pre_commit_workflows`; an empty `post_merge_workflows=`
clears the list and `eeco hooks post-merge on` refuses to install).
Install / removal / refresh behave exactly as for pre-commit: a foreign
or hand-edited hook is always left untouched.
- **session-start** — composes up to three blocks at session start
and stays silent when every block is empty:
1. A reading routine — repo-relative paths to read before
substantive work. Auto-detected from a built-in list
(`docs/PUBLIC_API.md`, `docs/ARCHITECTURE.md`, `CHANGELOG.md`,
`ARCHITECTURE.md`, `docs/USAGE.md`, `README.md`); existing
entries surface, missing ones are skipped. The
most-recently-modified match of `roadmap*.md` is appended as
the live planning surface. Override the doc list with one or
more `session_start_docs` keys in `config.local`; change the
roadmap pattern with `session_start_roadmap_glob` (empty
disables discovery).
2. A mailbox warning — surfaces when `Ideas.md` at the repo root
has content beyond its empty template (the parser skips the
file header and any HTML comment blocks). Change the filename
with `session_start_mailbox` in `config.local`; an empty value
disables the block. The hook does not create the file — it
surfaces only when the operator opted in by creating it.
3. A one-line queue reminder — `eeco: N items awaiting a decision`
when the queue is non-empty.
4. **(opt-in)** Pinned memory bodies — the full body of every
`pin: true` memory fact under `<workspace>/memory/`, separated by
Markdown dividers. Off by default, so the hook output is
byte-identical until enabled. Opt in via
`session_start_pinned_bodies=true` in
`config.local` (workspace default) or the `--with-pinned-bodies`
flag on `eeco hooks session-emit` (one invocation only). Useful
when an AI assistant treats the hook output as a system-reminder
so a pinned policy memory (for example a no-AI-attribution rule)
lands in the model's context at session start.
The session-start hook ships with two brand-free delivery channels;
use either alone or compose both:
- **JSON settings file** — an exact-match-removable entry in an AI
CLI's user-global JSON settings file (the Claude Code shape). Set
the file location with `session_settings_path` in `config.local`
or the `EECO_SESSION_SETTINGS` environment variable. The file is
backed up (inside the workspace) before the edit and re-validated
after; an invalid result is rolled back.
- **Marker block in a text/markdown file** — eeco maintains a
delimited block of the same content the JSON channel renders, in
one or more files the assistant already reads at session start
(`CLAUDE.md` for Claude Code, `GEMINI.md` for Gemini CLI,
`AGENTS.md` for OpenAI Codex, `.cursorrules` for Cursor,
`~/.config/<tool>/...`, etc.). Declare one path per `session_files=` line in
`config.local`; each value is either repo-relative (held inside
the repo) or absolute. The block is fenced by HTML comments —
`<!-- eeco:session:start -->` / `<!-- eeco:session:end -->` —
and replaces in place on re-`on` or `refresh`. The block content
is whatever `eeco hooks session-emit` prints. eeco never edits
bytes outside the marker pair, and a block whose contents have
been hand-edited since install is left untouched on `off`. Files
eeco itself created are removed when `off` would leave them
empty; files that already existed are restored byte-identical to
their pre-`on` state when the block is at end-of-file.
Neither channel is configured by default: `eeco hooks session-start
on` reports "not configured" until at least one of the two is set.
When the file channel is wired, run `eeco hooks session-start refresh`
to re-render the block — the JSON channel does not need it (it
carries a command eeco runs at session start, so it is always
fresh).
- **commit-msg** — a local `.git/hooks/commit-msg` (untracked,
repo-scoped) that rejects commit messages carrying an AI-attribution
trailer. The policy is universal and lives inside the eeco binary:
the on-disk script is a one-line wrapper that execs
`eeco hooks commit-msg-check "$1"`, so the pattern set refreshes with
every `brew upgrade eeco`. Matches the trailer-anchored forms
(`Co-Authored-By:` lines naming `claude`, `anthropic`, or
`noreply@anthropic`) plus the Claude Code robot-emoji (`U+1F916`)
"Generated-with" signature; deliberately stricter than the broad
repo-tree scan so a
docs commit that *discusses* the policy ("remove Co-Authored-By
trailer") still passes. Exit code 1 prints the matched line and names
`--no-verify` explicitly as the bypass; conscious overrides remain
available without surprise. Install / removal behave exactly as for
pre-commit and post-merge: foreign hooks are never modified. After a
`brew upgrade eeco` rewires the eeco binary path, run `eeco hooks
commit-msg refresh` to rewrite the on-disk wrapper without an
off-then-on toggle.
- **commit-guard** — the harness-layer companion to commit-msg. Where
the git `commit-msg` hook guards eeco's **own** repository, the
commit-guard installs a **Claude Code PreToolUse hook** that denies any
pending `git commit` carrying AI attribution — in **any** repository the
assistant drives, and not bypassable by `git commit --no-verify` (the
hook sits above git). It runs the **same** attribution detector
`leak-guard` uses, so the patterns stay one source of truth (the foreign
repo's own `attribution_pattern` entries are honoured when its config
loads). Default **off**; enable with `eeco hooks commit-guard on`, which
installs a `PreToolUse` group into the JSON settings file named by
`session_settings_path` (or `EECO_SESSION_SETTINGS`) — the same file the
session-start JSON channel uses, backed up and re-validated the same way;
it reports "not configured" when neither is set. The runner detects a
real `git commit` token-by-token (so `git status` and `echo "git commit"`
never fire), scans the assembled message (`-m`/`-F`/`COMMIT_EDITMSG`),
the staged diff, and the raw command, and **denies only on a positive
finding** — any parse or infrastructure uncertainty degrades **open**
(the commit proceeds), so a session is never wedged; the git `pre-commit`
/ `commit-msg` hooks and CI remain the hard gates. `off` removes only
eeco's group (foreign `PreToolUse` groups are preserved); after a
`brew upgrade eeco` run `eeco hooks commit-guard refresh` to rewrite the
binary path.
Every action is recorded in `<workspace>/state/hooks.json` so it can
be cleanly undone. The git-hook scripts and the JSON-settings writes —
the session-start and commit-guard entries plus any `session_files`
blocks — are the only touches eeco ever makes outside the gitignored
workspace.
## 9a. Gates — composed read-only policy checks
`eeco gates check-attribution` is a CI-facing companion to the
`commit-msg` hook (§9). The hook blocks at the source on a developer
machine; the gate is the CI backstop that catches a leak when a
developer bypassed the hook (`--no-verify`) or never installed it. Both
share the same trailer-anchored pattern set so prose discussing the
policy never false-fires.
Scope:
- **Tracked-file scan** — broad: delegates to the same detector
`comment-hygiene` uses (line-anchored `Co-Authored-By:`, the
`Generated with/by <ai-ish>` co-marketing line, and the robot-emoji
Generated signature). Enumerates the file list from `git ls-files`
and filters to a built-in text-extension allowlist (`.md`, `.sh`,
`.go`, `.zig`, `.S`, `.inc`, `.zon`, `.yml`, `.yaml`, `.txt`, `.ld`,
`.json`, `.toml`). Hits print as `path:line: excerpt`.
- **Commit-body scan** — strict trailer-anchored: `Co-Authored-By:`
lines naming `claude`, `anthropic`, or `noreply@anthropic`, plus the
robot-emoji "Generated" signature. Default range is
`origin/main..HEAD`; falls back to `HEAD~10..HEAD` with a stderr
notice when `origin/main` is unresolvable (fresh clone, detached
HEAD, initial commit). Hits print as `commit <short-sha>: line N:
excerpt`.
Flags (all optional):
| Flag | Effect |
| --- | --- |
| `--paths "<a b c>"` | override the tracked-files filter; pass an explicit space-separated list. |
| `--commits N` | force `HEAD~N..HEAD` instead of the default range. |
| `--no-commits` | skip the commit-body scan (file-only). |
| `--no-files` | skip the file scan (commits-only). |
| `--exclude <path>` | repeatable; repo-relative path to skip during the file scan. |
Exit codes: `0` clean, `1` on any hit (sets stderr with one line per
finding plus a `N finding(s)` summary), `2` on usage error. The CI
invocation pattern is one step before `git push`-eligible builds:
```yaml
- name: ai-attribution gate
run: eeco gates check-attribution
```
Use `actions/checkout@v5` with `fetch-depth: 0` so the commit-body
range resolves; default `1` makes `origin/main..HEAD` empty and the
fallback notice triggers.
## 10. Safety guarantees
- eeco writes only inside the repo's gitignored workspace; a path guard
enforces this and refuses `..` traversal.
- It never commits or pushes your project's tracked tree. The optional
private workspace-history repo (see §11a) commits only inside the
gitignored workspace, locally, and is never pushed.
- No AI-attribution string is ever written to a tracked file.
- Everything it does is invoked by you or queued for you.
## 11. Removing eeco from a project
`eeco uninstall` writes a single `eeco-handoff.md` at the repository
root summarising what eeco knew about the project, then prints the
git command to remove the workspace by hand. It never deletes anything
itself, so nothing is lost if you change your mind.
```
eeco uninstall # bare handoff (just paths + removal command)
eeco uninstall --scope facts # + memory facts
eeco uninstall --scope queue # + open queue items
eeco uninstall --scope everything # + facts + queue + scaffolded workflows + hooks
```
The handoff file is written at the repository root, alongside your
tracked tree; the file itself is not gitignored, so you choose whether
to keep, commit, or delete it. The workspace is gitignored, so a single
`git rm -rf .eeco` (substitute your workspace name) is the only on-disk
removal needed once the handoff has been reviewed. Turn opt-in hooks
off first (`eeco hooks pre-commit off`, `eeco hooks session-start off`)
so the reversibility ledger can do its work before the workspace goes.
If the private workspace-history repo exists (§11a), `eeco uninstall`
offers to de-init it — removing only its `.git`, never your data — after
a confirmation prompt. Pass `--yes` to skip the prompt, or decline to
keep the history and remove it yourself later. Your workspace data files
are never deleted by `eeco uninstall`.
## 11a. Workspace history (the private logbook)
At `eeco init`, eeco stands up a **private, local git repository inside
its own gitignored workspace directory** (`<username>/`) to version its
knowledge layer — memory facts, the queue, decisions, manifests — over
time. This repository has **no remote and is never pushed**, and it
**never touches your project's tracked tree**: it records only what eeco
already writes inside its own gitignored workspace, so it is invisible to
your project's git.
The `workspace_history` config key controls it:
- `manual` (default) — the repo exists; eeco commits only when you run
`eeco history snapshot`.
- `auto` — as `manual`, plus eeco commits automatically after each
mutating verb (`add fact`/`note`/`task`, `gc`, `go --write`, `run`,
`new`, …); still local-only, no remote.
- `off` — no private repo at all.
```
eeco init # creates the private repo (manual)
eeco init --no-track # skip it for this run
eeco history # show the recent commit log
eeco add note "decided X" # mutate the workspace
eeco history snapshot # commit the current state
eeco history snapshot -m "milestone" # …with a message
eeco history compact --dry-run # preview: how many commits would collapse
eeco history compact # squash the whole log into one (confirms first)
eeco history compact --yes # …without the prompt
```
Under `auto` the log grows a commit per mutating verb, so it can get long.
`eeco history compact` squashes the **entire** history into a single commit
whose contents are the current workspace state — the git pendant to the
docs-compaction protocol. Your workspace files are never touched (only the
commit log is collapsed), and the old commits stay reachable through the git
reflog, so the squash is recoverable; eeco runs no `git gc`. It is manual
only — there is no auto-trigger — and it confirms before rewriting unless you
pass `--yes`; `--dry-run` reports the count and changes nothing.
The repo is created opt-out (`--no-track` for one run, or
`workspace_history=off` durably) and removed at `eeco uninstall` (§11).
Inside it eeco has a free hand — commit, and later squash — because
nothing there is shared or tracked by the host repo.
## 12. Filing bug reports
`eeco report-bug` captures friction with eeco itself into a structured
Markdown record and prints a pre-filled GitHub Issues URL so the
report can reach the project in one click. The command works for
everyone: anyone who installed eeco can file, with or without an
initialised workspace.
```
eeco report-bug --note "leak-guard tripped on a doc-only commit" \
--cmd "eeco run leak-guard"
```
Both flags are optional. The record contains the eeco version, a
whitelisted environment snapshot (OS, arch, Go version, `SHELL`,
`TERM`, and any `EECO_*` variables — never the full env), the note,
and the invoking command.
Where the record lands:
- **Inside an initialised workspace.** Records go to
`<workspace>/bug-reports/`; override the directory with the
`bug_report_dir` key in `<workspace>/config.local` (workspace-
relative — absolute paths and `..` traversal are rejected at parse
time).
- **Anywhere else** (no `eeco init` yet, or not inside a git repo at
all). Records go to `~/.eeco/bug-reports/` so a fresh-install user
can file a report against eeco itself without having to set anything
up first. The repo's tracked tree is never touched.
Sharing the record:
`eeco report-bug` prints a pre-filled URL like
<https://github.com/ajhahnde/eeco/issues/new?title=...&body=...>.
Opening it in a browser fills the issue form with the record body
already pasted — clicking Submit files it to the project. The local
file is your durable copy; you can refine it before clicking, or
attach it by hand if the URL ends up too long for the browser. Nothing
is sent automatically — the upstream side of the loop is always a
human click.
Pass `--submit` to have eeco open that URL for you (the `$BROWSER`
command if set, otherwise the platform default opener). It is opt-in
and changes nothing else: the form opens pre-filled, you still review
and click Submit, and the command still reports that nothing was sent
automatically — opening a pre-filled form is not the same as sending.
If no browser can be opened, the command prints a short note alongside
the URL and still exits 0; `--submit` is a convenience, never a failure
point.
## 13. Briefing an AI assistant
`eeco go` prints a compact, deterministic project brief written for an
AI assistant. It is the fast, cheap way to bring any assistant — not
only the strongest — up to speed on a project: one command returns a
map of the project instead of a scan across many files.
```
eeco go
```
The brief has six sections:
- **Working with eeco** — what eeco is, the read-only commands an
assistant can run, and the rule that decisions go to eeco's queue
rather than silent edits.
- **Project** — the detected profile, the parse/build gate, and the
top-level layout (taken from the tracked set, so build artifacts and
the workspace stay out).
- **Where to look** — every memory fact that records a file pointer,
as a topic → file map. This is the assistant's shortcut to the right
file.
- **What eeco knows** — the load-bearing memory facts (project,
feedback, user).
- **Open decisions** — the items waiting in eeco's queue.
- **Recording back** — how the assistant keeps the brief useful:
record durable facts and route decisions through the queue.
`eeco go` calls no AI provider — the brief is assembled entirely from
what eeco already tracks, so it costs no provider budget and is safe to
run any number of times. The output is deterministic: the same
workspace state produces the same brief.
The brief is Markdown by default — written for an assistant to read.
The `--json` flag emits the same project state as a JSON object instead,
the machine-readable form for a downstream agent or script that parses
the brief rather than reading it:
```
eeco go --json
```
The JSON object has nine top-level keys — `project`, `profile`, `gate`
(an argv array), `top_level`, `initialized`, `workflows`,
`where_to_look` (`description`/`ref` objects), `knowledge`
(`name`/`description`/`type` objects), and `open_decisions`. Every array
is always present, an empty list rather than null, so a consumer can
iterate without a nil check. These top-level keys are part of eeco's
frozen public surface. `--json` prints to stdout and cannot be combined
with `--write`.
By default the brief is printed to stdout and nothing is written. The
`--write` flag saves it instead to a stable file inside the workspace —
`.eeco/context.md` by default — so an assistant's instructions file can
point at a fixed path rather than re-running the command each session:
```
eeco go --write # writes .eeco/context.md
```
The destination is workspace-relative; override it with the
`context_path` key in `<workspace>/config.local` (absolute paths and
`..` traversal are rejected). `--write` requires an initialised
workspace, and the file it writes is byte-identical to the stdout
brief. The file lives in the gitignored workspace, so it is never
committed; re-run `eeco go --write` whenever you want it refreshed.
For an assistant on a tight context budget the `--brief` flag reshapes
the output: the "Working with eeco" preamble and "Recording back" outro
drop out, and the per-section lists (`Where to look`, `What eeco knows`,
`Open decisions`) are capped at five entries each. `--brief` composes
with `--json` and `--write`, so every delivery axis gets the size knob:
```
eeco go --brief # smaller Markdown brief
eeco go --brief --json # smaller JSON brief
eeco go --brief --write # writes the smaller variant
```
In `--brief --json` the nine frozen top-level keys are preserved —
arrays may simply be shorter, never absent or null — so a consumer that
parses the JSON brief sees the same shape, never an unexpected key
disappearance.
To keep the persisted `.eeco/context.md` under a known size, set the
`context_budget` key in `<workspace>/config.local` to a maximum **byte**
count (roughly four bytes per token is a useful rule of thumb):
```
context_budget=4000 # cap the --write brief at 4000 bytes
```
When `context_budget` is set, `eeco go --write` trims the saved brief
down a deterministic ladder until it fits: it tries the full brief
first, then the smaller `--brief` form, then that form with the
per-section lists capped progressively shorter (5, 4, 3, 2, 1, 0). It
writes the largest tier that fits and reports which one — for example
`wrote .eeco/context.md (brief, 1840/4000 bytes)`. A `context_budget`
of `0` (the default) means no cap. The budget applies only to
`--write`; bare `eeco go` and `eeco go --brief` printed to stdout are
unchanged. If the budget is so small that even the smallest tier
overruns it, eeco still writes that smallest brief and prints a note —
a brief slightly over budget beats no brief at all.
To surface the recent free-form notes from `eeco add note` (§17)
inside the brief, set the `brief_include_notes` key in
`<workspace>/config.local`:
```
brief_include_notes=true
```
When enabled, `eeco go` adds a **Recent notes** section between
**What eeco knows** and **Open decisions**, listing the five newest
notes from `<workspace>/notes/` with their UTC timestamp and first
line. The section appears in Markdown output only — `eeco go --json`
still emits exactly the nine frozen top-level keys
([`PUBLIC_API.md`](PUBLIC_API.md) §"CLI commands and flags"), since
notes belong to the assistant-prose channel rather than a parsed
brief. The key accepts the standard `strconv.ParseBool` set —
`true`/`false`, `1`/`0`, `t`/`f` (in either case); a typo like
`yes`/`no` is rejected at parse time so the operator notices.
The default is `false`, so bare `eeco go` omits the section; the key
composes with `--brief`, `--write`, and `--copy` exactly like the other
sections.
To wire the brief into a session, either point your assistant's
instructions file at `.eeco/context.md`, or — if you use the bundled
session-start hook (§9) — add `session_start_docs=.eeco/context.md` to
`config.local` so the hook lists it in the reading routine
automatically. Any assistant that can read a file or run a shell
command can consume the brief; eeco assumes no particular tool.
For a chat-only assistant — Gemini web, a Gem, AI Studio, ChatGPT,
or any LLM behind a chat box — the `--copy` flag writes the brief to
the host operating system's clipboard so you can paste it in one
shot:
```
eeco go --copy # full Markdown brief on the clipboard
eeco go --copy --brief # smaller brief for a tight context window
eeco go --copy --json # structured payload for a chat that parses JSON
```
`--copy` works on macOS (pbcopy), Windows (clip.exe), and Linux
(wl-copy on Wayland, xclip or xsel on X11). When none of those tools
is on `PATH` eeco exits 2 with an install hint — the same
"blocked: required tool missing" contract every other workflow
follows. `--copy` does not compose with `--write` (one delivery axis
per invocation); it does compose with `--brief` and `--json`.
Re-run `eeco go --copy` whenever the project state has moved on.
### Measuring the brief — `--metrics`
The `--metrics` flag adds a one-line readout to **stderr** after the
brief, so you can see what the brief actually saved:
```
eeco go --metrics
eeco go: assembled in 412µs · brief 1840 bytes (≈460 tokens) · distilled ≈980 tokens of project knowledge into ≈460 (≈53% smaller)
```
It reports three things: how long the brief took to assemble (real
wall-clock), the brief's own size, and how much of the project's
knowledge layer — the on-disk memory facts, the queue, and any notes —
the brief distils. Byte counts are real measurements; token figures are
**estimates** via the ~4-bytes-per-token heuristic and are marked with a
leading `≈`, never presented as exact counts (eeco ships no tokenizer,
by design). The compression percentage is grounded in real on-disk bytes
and is never reported below `0%`.
The readout goes to stderr, so it never mixes into the brief itself:
`eeco go --metrics > brief.md` writes a clean brief to the file and
prints the readout to the terminal. It always describes the **canonical
Markdown brief** — even alongside `--json` or `--copy`, where it measures
the Markdown brief the assistant would read — and it never appears in
`--json` stdout, so the nine frozen top-level keys
([`PUBLIC_API.md`](PUBLIC_API.md) §"CLI commands and flags") are
untouched. With `--write` under a `context_budget` the readout still
describes that canonical brief, not the budget-trimmed file that was
persisted — the metric measures how much eeco's brief compresses the
knowledge layer, a stable property, rather than one write's fitted size.
`--metrics` is off by default and composes with every other flag.
### Briefing assistants other than Claude Code
eeco's delivery channels are brand-free — point them at whichever
assistant you use:
- **Gemini CLI** (Google's terminal-native assistant) reads
`GEMINI.md` at session start. Set `session_files=GEMINI.md` in
`<workspace>/config.local`, then `eeco hooks session-start on`.
Same recipe as Claude Code with `CLAUDE.md`.
- **OpenAI Codex** reads `AGENTS.md`. Set `session_files=AGENTS.md`.
- **Cursor** reads `.cursorrules`. Set `session_files=.cursorrules`.
- Any other assistant whose instructions file lives at a known
path: add that path as a `session_files=` line and eeco will keep
a marker block in it (§9).
For an assistant with no terminal and no filesystem access (Gemini
web, Gems, AI Studio, ChatGPT, any chat box), `eeco go --copy` is
the one-shot bridge — render the brief into the clipboard and paste
it into the chat. Re-run when the project state moves on.
### Asking a targeted question
Where `eeco go` is a one-shot overview, `eeco ask` answers a *specific*
question with a ranked set of pointers — the interactive counterpart to
the static brief:
```
eeco ask "where is the project brief rendered"
```
The answer has two sections: **Memory** — the memory facts whose name,
description, or body overlaps the question, shown as a topic → file map
when the fact carries a `ref`; and **Code** — the best-matching lines in
the repository's tracked files, each as a `path:line` reference with the
matching line. Results are ranked by how many distinct question words
they match, with stable tie-breaks, so the same question over the same
tree always gives the same answer.
```
eeco ask --limit 3 "how does version-sync detect drift" # cap code hits
eeco ask --json "queue" # machine-readable
```
`--limit N` caps the number of code locations (default 10); every
matching memory fact is always shown. `--json` emits an object with
three frozen top-level keys — `question`, `memory`, `code` — both arrays
always present, never null. Like `eeco go`, `eeco ask` calls no AI
provider (relevance is a word-overlap score over what eeco already
tracks), reads anywhere in the repo but writes nothing, and works in any
git repository — the memory section is simply empty when the workspace
is not initialised. Binary and very large files are skipped.
## 14. Applying an update
`eeco update` checks for a newer release and prints the gap, but does
not download anything. The opt-in `--apply` flag turns it into a
verified self-replace: download the platform release archive, verify
its sha256 against the published `SHA256SUMS`, verify the cosign
signature on `SHA256SUMS`, verify the GitHub build-provenance
attestation on the archive, then atomically swap the running binary.
```
eeco update --apply
```
Verification gates. All three must pass before any swap. Any one
failure leaves the running binary untouched and leaves the downloaded
files in `<workspace>/state/update-<tag>/` for inspection.
- `cosign verify-blob` against the keyless certificate identity baked
into the release workflow (`refs/tags/v…` on this repo).
- `sha256` of the downloaded archive equal to its line in `SHA256SUMS`.
- `gh attestation verify <archive> --repo ajhahnde/eeco`.
Requirements. `cosign` and `gh` must be on `PATH`. The command must run
inside an initialised eeco workspace — `<workspace>/state/` holds the
staging directory, the backup, and the swap ledger
(`<workspace>/state/binary.json`). The running binary must be on a
released tag (`v…`); an unpinned `0.0.0-dev` build refuses with a
reinstall hint.
Package-manager handoff. If the resolved running-binary path lives
under a known package-manager root (Homebrew, Scoop, Linuxbrew),
`--apply` refuses and points at the package manager's upgrade verb
(`brew upgrade eeco` or `scoop update eeco`). Self-replace would fight
the package manager on its next sync; the manager owns its prefix.
Rollback. The previous binary is preserved at
`<workspace>/state/update-<tag>/eeco.bak` (or `eeco.exe.bak` on
Windows). To revert by hand:
```
mv <workspace>/state/update-<tag>/eeco.bak <running-path>
```
The exact `<running-path>` is recorded in
`<workspace>/state/binary.json` under `running_path`.
What stays the same. Bare `eeco update` (no flag) is unchanged: it
still lists the gap and exits cleanly, without touching any binary.
Constraint 6 holds — eeco only swaps when the operator passed
`--apply`.
## 15. Reading the in-binary guide
`eeco guide` pages the user manual built into the binary — a verbatim
mirror of this file (`docs/USAGE.md`) at the tag the binary was built
from. Every install route ships the manual offline; no network access
needed, no doc directory to clone.
```
eeco guide
```
The pager selection is `$PAGER` first (split on whitespace; the first
token must be on `PATH`), then `less -R` if `less` is available, then
a plain stdout dump as the final fallback. When stdout is not a
terminal (piped or redirected) the command always dumps the manual
plainly and exits 0 — the same one-screen path the TUI uses for
`eeco` with no arguments.
At a terminal the manual is prettified for reading: Markdown tables
become box-drawing grids, headings are styled with a rule, and inline
`code` / **bold** / links are rendered rather than shown as raw markup.
`NO_COLOR` drops the colour but keeps the layout. Piped or redirected
output stays raw Markdown, byte-identical to `docs/USAGE.md`, so a
script that captures the guide (`eeco guide > usage.md`) gets the
source unchanged.
Quick recipes.
```
eeco guide # page interactively
eeco guide | less # explicit pager pipeline
eeco guide > usage.md # capture the embedded snapshot
PAGER='less +/^## 4' eeco guide # jump straight to the commands table
```
The embedded snapshot is locked to the binary version. Pair it with
`eeco update --apply` to keep the manual in sync with the running
release. The tracked-tree `docs/USAGE.md` and the in-binary guide
stay byte-identical in CI — a release commit that forgets to re-sync
the mirror fails the build.
## 16. Authoring tracked docs
`eeco docs new <target>` seeds a tracked-tree documentation file at
the repository root. Two targets ship today:
| Target | File written | Purpose |
| -------- | ------------- | --------------------------------------------- |
| `vision` | `VISION.md` | seed a short manifesto: what this project is for, what it gives you, what it deliberately is not, and where the roadmap lives |
| `readme` | `README.md` | seed a new-reader-friendly README: tagline, what the project does, a quick-start recipe, and a short "how it works" |
The scaffolder is deterministic — no AI call, no provider spend — and
project-shape-aware: the rendered "See also" only links to companion
docs that already exist (`README.md`, `docs/USAGE.md`,
`docs/ARCHITECTURE.md`). The `readme` target's "See also" never
self-links, so the link set narrows to `docs/USAGE.md` and
`docs/ARCHITECTURE.md`.
```
eeco docs new vision # write VISION.md (refuse if it exists)
eeco docs new --overwrite vision # replace an existing VISION.md
eeco docs new readme # write README.md (refuse if it exists)
eeco docs new --overwrite readme # replace an existing README.md
```
Refuse-on-existing is the default. Pass `--overwrite` to replace a
prior scaffold or hand-edited file. **`--overwrite` discards the
existing body** — use it deliberately, especially on a populated
`README.md`. eeco writes the file but never stages or commits it
(Constraint 6); review the result and run `git add <file> && git
commit` yourself.
The seed file carries a one-line HTML comment at the top marking it
as a scaffold and naming the eeco version it was generated against.
The comment is invisible in any rendered Markdown view and can be
deleted in the same edit you fill the body in.
`eeco docs refresh <target>` re-renders the project-state-derived
region of a previously-scaffolded doc — the same project-shape-aware
"See also" block `eeco docs new` rendered — leaving operator prose
outside the marker pair untouched. The region is delimited by a pair
of HTML-comment markers:
```
<!-- eeco:docs:start -->
… rendered content …
<!-- eeco:docs:end -->
```
Markers inside fenced code blocks are ignored, mirroring `eeco docs
compact`. Unmatched, nested, or out-of-order markers exit 1 with a
parse error naming the offending line; the file is not touched. A
scaffold with no markers is auto-initialised: the freshly
rendered block is appended at EOF, marker-wrapped, with a blank-line
separator. The operator removes the prior in-place block manually.
Where `--overwrite` on `eeco docs new` discards the entire body,
`refresh` is the non-destructive complement.
```
eeco docs refresh vision # re-render VISION.md's marker block
eeco docs refresh readme # re-render README.md's marker block
```
`eeco docs compact <path>` moves resolved regions of a long doc into
a sibling archive file so the working copy stays short, then leaves a
single-line pointer stub in place at each cut. A region is anything
between a pair of HTML-comment markers:
```
<!-- eeco:archive:start -->
… content to archive …
<!-- eeco:archive:end -->
```
Markers inside fenced code blocks are ignored, so a doc explaining
the markers (this very paragraph in the original `docs/USAGE.md`)
does not trigger a self-archive.
```
eeco docs compact roadmap_v1.x.md
eeco docs compact --dry-run roadmap_v1.x.md
eeco docs compact --archive history.md roadmap_v1.x.md
```
The default archive destination is `<basename>.archive<ext>` next to
the source — e.g. `roadmap_v1.x.md` → `roadmap_v1.x.archive.md`.
Override the destination with `--archive`; pass a `--dry-run` to
preview which line ranges would move without writing. Re-running
after a successful compact is an idempotent no-op (`no archive
markers found`); the archive is appended to on subsequent runs so a
long-lived doc can be compacted in waves.
Unmatched, nested, or out-of-order markers exit 1 with a parse error
naming the offending line; the archive is not touched. Both paths
must resolve inside the repository (absolute paths and `..` traversal
are rejected). eeco writes the source and the archive but never
stages or commits either (Constraint 6); review both files and `git
add` / commit when ready.
For a log that grows by prepending a new section each time — a session
journal, a changelog, a steering file — placing markers by hand every
round is fiddly. `--keep-last N` discovers the archivable regions by
heading instead. Pair it with `--heading <prefix>`, the heading-line
prefix that opens an archivable section:
```
eeco docs compact --keep-last 4 --heading "## Snapshot" RESUME.md
```
This keeps the **N most-recent** matching sections — newest meaning
topmost in the file — and archives everything older. The `#` run in
`--heading` fixes the section level: a section runs from its heading to
the next heading of the same or a higher level, so a live tail such as a
trailing `## Next session` or `## Pointers` block is never swallowed. A
deeper sub-heading inside a section does not split it, and (as in marker
mode) any heading inside a fenced code block is ignored. Adjacent
archived sections collapse into one pointer stub.
`--keep-last` and explicit markers are mutually exclusive: running
heading mode on a source that still carries a paired marker region exits
1 rather than mixing the two schemes. `--keep-last` requires `--heading`
and vice versa. `--dry-run` and `--archive` work the same in both modes;
re-running once only the kept window remains is an idempotent no-op.
## 17. Keeping notes
A note is a place to scribble — a half-formed idea, a reminder, a
thread to pick up later. It is deliberately neither a memory fact
(frontmatter-strict, matched to AI relevance) nor a queue item (the
append-only decision channel): routing scratch text into either of
those pollutes their contract. Notes get their own surface.
```
eeco add note "check round-robin fairness in queue promotion"
eeco show notes
```
`eeco add note "<text>"` saves the text verbatim to one plain Markdown
file under `<workspace>/notes/`, named `YYYY-MM-DD-HHMMSS-<slug>.md`
(the timestamp is UTC; the slug is derived from the text). The
directory is created on first use, so a fresh `eeco init` is not
required — being inside a git repository is enough. eeco writes only
inside the workspace (Constraint 1) and never stages or commits
(Constraint 6).
`eeco show notes` lists every note newest-first, one row per note:
the local date and time followed by the note's first line. An empty or
absent notes directory prints a friendly hint and exits 0.
This first slice is append + list only. To edit a note, open it in your
editor (`$EDITOR <workspace>/notes/<file>`); to remove one, delete the
file (`rm <workspace>/notes/<file>`).
---
[← Prev: Cockpit](COCKPIT.md) · [Next: Architecture →](ARCHITECTURE.md)