AGENTS.md · git:20260822.10f66d8 · 2026-08-22 · sha256 9abb9ec1d818a633

AGENTS.md git:20260822.10f66d8A

Immutable. This exact content is served forever at /api/v1/blob/9abb9ec1d818a633.

# keke — agent guide

keke is a multi-vendor terminal coding agent. Vendor-specific behavior lives in
replaceable plugins; the engine does not know that ChatGPT or Grok exist.

Read `docs/architecture.md` before making structural changes.

## Invariants

These are not style preferences. Each one exists because a reference
implementation lost it and paid for it.

1. **Nothing vendor-specific in `keke-core`.** No `keke-provider-*` or
   `keke-auth-*` dependency, no vendor name in a match arm. OpenAI's codex wrote
   *"resist adding code to codex-core"* in its own guide and `codex-core` still
   ended up depending on ~50 internal crates. Prose did not hold the line, so
   `scripts/check-layering.py` holds it instead and runs in CI.

2. **Dependencies point strictly downward by rank.** Contract crates (tier 0)
   depend on nothing above them and not on each other except in the documented
   order. Adding a crate means adding it to `RANK` in the layering script.

3. **Contract crates stay dependency-light.** `keke-protocol`, `keke-tool`,
   `keke-auth-api`, `keke-provider-api`, `keke-plugin-api`, `keke-config-types`,
   and `keke-paths` may use `serde`, `futures`, `thiserror`, `schemars`, and each
   other. No HTTP client, no runtime, no engine. This is what lets a vendor
   plugin authenticate without linking the world.

4. **RPITIT over `#[async_trait]`.** Write
   `fn f(&self, ..) -> impl Future<Output = T> + Send`. Where a trait must be
   held as `dyn`, box explicitly at that one boundary — see `keke_tool::ToolDyn`
   and `keke_plugin_api::ExtFuture`. Do not reach for `#[async_trait]`.

5. **Registration returns a disposer, or is composed once and frozen.** A
   registry either hands back something whose drop removes the contribution, or
   is a builder that produces an immutable value. A contribution must never be
   able to outlive the thing that made it.

6. **Model-visible implies logged.** Anything reaching a model request must be
   reconstructable from `SessionEvent`s. Adding a new kind of model-visible
   input means adding a variant to `keke_protocol::SessionEvent` first.

7. **Denial is monotonic.** Approval reviewers may allow or deny; `ToolGuard`s
   may only deny. No ordering of extensions can turn a denial back into
   permission.

8. **Ambiguity fails loud.** Two providers claiming a route with none configured
   is an error, not a silent pick. An empty stored credential is absent
   everywhere and never counts as configured.

9. **No deployment-varying constant hidden in a plugin.** If a deployment might
   reasonably want to change it, it is a validated field in
   `keke-config-types`, not a `DEFAULT_*` in the plugin.

10. **Ported code is attributed.** Files ported from another project live under
    `src/ported/<project>/`, carry a header naming the upstream path, and are
    listed in that crate's `THIRD_PARTY_NOTICES.md`. keke never takes a path or
    git dependency on `../codex` or `../grok-build` — it must build from its own
    checkout alone.

## Conventions

- Crates are named `keke-*`; the directory matches the crate name.
- Extension crates expose `pub fn install(registry: &mut ExtensionRegistryBuilder, ..)`.
  `keke-cli` is the only composition root and the only place `install` is called.
- New traits get a doc comment explaining their role and what implementers are
  expected to do — a trait without one is incomplete.
- Comments explain *why*, not *what*. A comment restating the code is noise.
- Tests assert behavior described in prose above, not implementation details.
  `a_permissive_guard_cannot_undo_a_restrictive_one` is the model.

## Checks

```sh
cargo fmt --all --check
RUSTFLAGS="-D warnings" cargo clippy --workspace --all-targets
cargo test --workspace
python3 scripts/check-layering.py
```