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__darwin_amd64.tar.gz │ │ darwin │ arm64 │ eeco__darwin_arm64.tar.gz │ │ linux │ amd64 │ eeco__linux_amd64.tar.gz │ │ linux │ arm64 │ eeco__linux_arm64.tar.gz │ │ windows │ amd64 │ eeco__windows_amd64.zip │ │ windows │ arm64 │ eeco__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___. --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 //.eeco/ and adds // to .gitignore if it is not already ignored, so nothing eeco does can leak into your tracked history. 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 /.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] │ 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 │ scaffold a new workflow │ │ /hooks [ on|off] │ show or toggle the opt-in reversible hooks │ │ /settings [ ] │ 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 ] 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 /.eeco workspace under / eeco run run one workflow (read-only / safe by default) eeco run --ai allow this run's gated, budget-capped AI pass eeco new 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 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 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 print the effective value of one config key eeco config set [--global] set a key in this workspace, or (--global) in the cross-project layer eeco config import [--force] copy config.local, cockpit.json, and workflows from another eeco project eeco cockpit target [--global] list|add |rm 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] "" answer a question with ranked file:line pointers (no AI) eeco docs new [--overwrite] scaffold a tracked-tree doc (targets: vision, readme) eeco docs refresh re-render a scaffolded doc's marker-wrapped block (targets: vision, readme) eeco docs compact [--keep-last N --heading ] [--archive ] [--dry-run] archive old regions of (markers, or heading-discovered with --keep-last) into a sibling eeco history show the private workspace-history log (recent commits) eeco history snapshot [-m ] 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 "" append a free-form note to the workspace eeco add fact --description "" [flags] "" record a durable memory fact eeco add task "" 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: - 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>).