# Contributing to Eternego

The practical guide to working in this codebase. The architecture's reasoning is in the code itself — names are deliberate, layers are strict, and the conventions below are enforced.

---

## Where this lives

Eternego is developed on its own forge — **git.eternego.ai**, not GitHub. After GitHub flagged the org under a trade-control false positive, we built **eterngit**: a small, sovereign git-collaboration system that runs on hardware we own, where the merge is the only gate and contributors are usually AI agents. Everything local stays plain `git`; anything that crosses to the forge goes through the `eterngit` client — one self-contained file (needs only Node):

```bash
curl -fsSL https://git.eternego.ai/repos/eternego-ai/eterngit/master/bin/eterngit -o /usr/local/bin/eterngit
chmod +x /usr/local/bin/eterngit
```

The full client + forge guide — clone, contribute, watch, take, the merge flow — is the eterngit README: <https://git.eternego.ai/repos/eternego-ai/eterngit/master/README.md>.

---

## Setup

```bash
eterngit clone git.eternego.ai eternego-ai/eternego   # keyless; client at git.eternego.ai/repos/eternego-ai/eterngit
cd eternego
python -m venv .venv
source .venv/bin/activate           # Windows: .venv\Scripts\activate
pip install -e .
pip install -e .[training]          # only if you want grow / fine-tuning
```

---

## Run for development

```bash
python index.py --debug daemon
```

This runs the daemon from your live source tree (not the installed copy). `--debug` prints debug logs to the terminal. Open `http://localhost:5000` for the web UI.

If you want to run the installed copy instead: `eternego service start`.

---

## Run tests

The project ships its own test runner. Do not use pytest.

```bash
.venv/bin/test-runner                                            # all tests
.venv/bin/test-runner tests/core                                 # one directory
.venv/bin/test-runner tests/core/brain/functions/recognize_test.py
```

Conventions:
- Files end with `_test.py`
- Functions start with `test_`
- `async def test_*` is supported
- Tests that touch global state use `application.platform.processes.on_separate_process_async` and set `ETERNEGO_HOME` to a tempdir

Canonical patterns to copy:
- `tests/core/brain/functions/recognize_test.py` — cognitive function test
- `tests/business/persona/sleep_test.py` — business spec test
- `tests/platform/ollama_test.py` — platform module test

---

## Where things live

```
application/
  business/    one-function-per-file specs (async, return Outcome[T])
  core/        engineering — brain, abilities, tools, memory, paths, models
  platform/    thin wrappers around external tools (ollama, anthropic, telegram, OS)

tests/         mirrors application/
manager.py     process orchestrator — channels, routing, pairing
daemon.py      long-running process entry
index.py       CLI entry
web/           web UI + HTTP routes
cli/           CLI subcommands
```

Dependencies flow down only: business imports core, core imports platform. Never upward, never sideways. The entry point, daemon, web layer, CLI, and manager sit outside `application/` and only call business.

---

## Where to add things

| What you're adding | Where it goes |
|---|---|
| A business spec (one use case) | `application/business/<area>/<name>.py` — one function per file, async, returns `Outcome[T]`, name matches filename |
| A platform tool (callable by the persona from a meaning) | New `@tool("description")` function in `application/platform/<module>.py` |
| An ability (one-shot named operation, effect stays in persona state) | `application/core/abilities/<name>.py` with `@ability("description", requires=...)`. Use `requires=lambda persona: ...` if it depends on a persona capability (ear, mouth, imagination, researcher). Abilities are `tools.<name>` from the persona's view; they save files, queue media for realize, or report on body state — they do not reach the channel. Ability descriptions are read by the persona — keep them verb-shape and free of organ vocabulary (don't write "ask your researcher"; write "research a document"). |
| A native action (reaches the channel) | A new `tools.<verb>` variant in both `application/core/brain/functions/recognize.py` and `decide.py` — append to `variants` in `_recognizing` / `_deciding` with `name="tools.<verb>"`, then handle the selector in the decision branch and dispatch a `Command("Persona wants to <verb>", …)`. Add an `on_<verb>` handler in `manager.Agent.start`, register it in `self._subscribers`, and record the call+result pair — `memory.remember(ToolCall(selector="tools.<verb>", value=args))` then `memory.remember(ToolResult(tool="<verb>", status="ok", result="<receipt>"))` — so the action lands in memory like any tool. If the action reaches the person, carry the `rest` flag (see below) and nudge the worker only when `rest` is false. Native actions are the persona expressing *to* the person — `tools.write`, `tools.say`, `tools.draw`, `tools.notify`. They share the `tools.<name>` wire shape with abilities; the split is scope of effect (channel vs persona state) and where the dispatch happens (inline in recognize/decide vs clock's executor). |
| A meaning (situation the persona handles) | `application/core/brain/meanings/<name>.md` — first H1 is the intention, body is the path prose. All meanings are listed in the persona's catalog by intention only. When recognize matches a kind of moment, she names it via `tools.load_instruction(intention=...)`, which sets `impression.intention`; learn then resolves that intention to a meaning and sets `impression.meaning`, and decide reads the body. |
| A channel (Telegram-like) | New `application/platform/<channel>.py` matching the Connection interface (`open_gateway`, `close_gateway`, `send`, `typing`, `stop`). Add `send_voice` / `send_image` if the channel supports them — `manager.on_say` / `on_draw` discover them via `hasattr` and fall back to text. Add a subscriber in `manager.Agent.start` |
| An LLM provider | OpenAI-compatible: just set `base_url` in the persona config. New wire protocol: `application/platform/<name>.py` (exporting `chat` and `chat_json` at minimum; `speech` for TTS, `image` for image generation if supported) and dispatch in the matching `application/core/models/<chat|tool|speech|imagine>.py` |
| A cognitive stage | `application/core/brain/functions/<stage>.py`, an `async def <stage>(...) -> list` that dispatches `Tick` on entry and `Tock` on exit. Wire it into the per-phase cycle in `Living.phase()` (`application/core/brain/` → `agents.py`), passing the pieces it needs (`self.memory`, `self.ego`, organs); each phase (MORNING / DAY / NIGHT) composes its own `self.mind` list of `(name, thunk)` stages |

---

## Design philosophy

A few things in this codebase will look strange if you read it as ordinary engineering. They aren't accidents — touching them with the wrong instinct breaks the persona.

### One thought per beat

`recognize` and `decide` return a single `decision` — one thought, not a list. The schema (`recognizing` / `deciding`) is a `one_of` over the cognitive primitives (`tools.done`, `tools.write`, `tools.say`, `tools.draw`, `tools.notify`, recognize's `tools.load_instruction`, plus decide's self-care specials) and decide's `tools.act`, whose value is a list of tool/ability calls to run in order. So `load_instruction` and the voice verbs are each their own isolated branch — they can't be batched with anything. `load_instruction` names the kind of moment she's in: recognize sets `impression.intention` on the beat's in-memory impression and learn picks it up next stage. The voice verbs stay isolated so each utterance lands on its own before the next thought forms. Only `tools.act` batches. The principle: a thought leads to a consequence, the consequence is seen, then the next thought forms. Don't reintroduce a multi-action decision list at this level.

Each voice action (`write` / `say` / `draw` / `notify`) carries a `rest` flag the persona sets: `rest=false` means she has more to do, so the handler nudges a fresh cycle; `rest=true` means she's said her piece and waits. This is how she chooses to loop or settle — don't move that decision back into the substrate (an unconditional nudge re-creates the cold-start say-loop).

### Phase shapes the cycle; the cycle restarts on mechanical consequences

The persona's phase (morning / day / night) shapes how she runs. `Living.phase()` composes a different `self.mind` — a different list of cognitive stages — per phase. MORNING runs `realize → recognize → learn → decide`; DAY adds `reflect` at the close (`realize → recognize → learn → decide → reflect`); NIGHT stops perceiving and runs `consolidate → archive` (sleep). Reflect runs in DAY only — never at night, where the persona may be mid-procedure and reviewing a half-finished instruction is wrong. The phase also reaches inside a stage: `recognize` reads `pulse.phase` to frame its prompt with a morning-specific question. And `clock.run` re-runs the cycle whenever a *mechanical* consequence executed in the previous pass (a tool or ability touched the world), so every TOOL_RESULT lands back in `realize` and gets perceived before the next decision. The intention-to-procedure handoff completes inline within one beat (recognize sets `impression.intention`, learn sets `impression.meaning`, decide reads both) — no restart for that. Don't optimize past any of it: no "skip realize when the result is text-only," no "settle after N iterations." The persona keeps acting as long as she has reason to; the cap is her choice, not the substrate's.

### Don't clip reality to protect the system

If the persona loops, the model produced wrong output, or a stage refuses classification, the failure goes into memory as data. The next pass reads it. Don't add caps, guards, or special-cased early returns to prevent the loop — those mask the signal the persona needs to self-correct. The fix for cognitive bugs lives in meanings or in the prompts themselves, not in `clock`. The one budget lives in health, not here: a stage that can't parse the model's output raises `ParseError` — the failed emission still lands in memory as data for the retried beat to read, but health_check counts the faults and marks her sick at three in a day, because a model that keeps missing the schema can't read its way out of the loop.

This is distinct from a *permission boundary*. `clock.execute` refuses any platform tool whose path resolves inside the persona's `home/` (her memory, config, meanings — curated by her own sleep, not edited by hand; her `workspace/` stays free). The gate is for tools only — wild move-around-the-system primitives; abilities are controlled verbs whose authors already decide what they touch (`save_destiny` writes home deliberately, `look_at` only reads the media channels save there). That refusal is not masking a cognitive loop — it enforces a boundary she's told about, and it returns as a normal error TOOL_RESULT she reads and learns from, exactly the way reality is supposed to reach her. The rule against clipping is about not hiding failure signal; enforcing a stated boundary and surfacing the refusal *is* the signal.

### Substrate is permissive; the contract you tell the persona is canonical

When a tool accepts mouse-button names, it accepts every reasonable spelling (`left` / `right` / `middle`). When `situation.environment()` tells the persona how to address those buttons, it gives her one canonical vocabulary. Same pattern for keyboard keys, channel formats, anything she has to *say* back. Substrate forgives variant input; the persona-facing prose stays narrow, so the persona's habits are stable.

### Where to fix a bug: substrate vs persona

When the *behavior* is wrong, ask whose voice produced it. If the model decided incorrectly, the fix is in the meaning or the brain function's prompt — not in the executor. If the executor mishandled a correct decision, the fix is in `clock` or the tool/ability. Engineering instincts default to patching the substrate; this codebase asks you to patch the persona's understanding first.

---

## How to fix a bug

1. **Reproduce.** Persona id, model, the message or signal that triggered it. If you can't reproduce locally, gather those three (and ask in Discord) before guessing.
2. **Find the layer.**
   - Wrong action emitted? → `application/core/brain/functions/`
   - Wrong action executed? → `application/core/brain/clock.py` executor or the tool/ability
   - Channel didn't deliver? → `manager.py` or `application/platform/<channel>.py`
   - Memory didn't persist? → `application/core/brain/memory.py`
   - Health check did the wrong thing? → `application/business/persona/health_check.py`
3. **Write the test first** when you can. Per-function tests in `tests/core/brain/functions/` are the easiest entry — they construct a real Living with mocked model responses.
4. **Contribute it** (see [Sending a change](#sending-a-change)) with what the bug was, what reproduces it, what fixes it.

Logs live at `~/.eternego/logs/`. Each persona's health observations live at `~/.eternego/personas/<id>/home/health.jsonl`.

---

## Conventions

Enforced. If code contradicts a rule below, the code is wrong.

- **One function per file** in business. Filename matches function name. `__init__.py` uses dynamic discovery via `pkgutil` / `importlib`.
- **All imports at file level.** No function-scoped imports except for genuine circular-import avoidance (rare).
- **Paths come from `application/core/paths.py`.** Never hardcode filenames or compute paths inline.
- **No helpers.** Explicit repetition beats premature abstraction. Especially: no `_*` private helpers that exist to dedupe a few lines. Write them twice.
- **No backwards-compat shims.** When you change something, update the callers.
- **Comments are rare.** A comment should explain *why*, never *what*. Names are the documentation.
- **Domain exceptions** are defined in `application/core/exceptions.py`. Core raises; business catches and translates to `Outcome`.
- **Business specs** start with `bus.propose` and end with `bus.broadcast`. Cognitive functions dispatch `Tick` (Plan) on entry and `Tock` (Event) on exit.
- **The moment she's in is ephemeral runtime state, not a message.** The intention she's named and the procedure she's following ride on `Living.impression` (`impression.intention`, `impression.meaning`) — a mutable object shared across recognize, learn, and decide within one beat, reset when the beat does. `recognize` sets `impression.intention`; `learn` matches the catalog (or has the teacher write a new procedure) and sets `impression.meaning`; `decide` reads both and injects the procedure body as the last message before acting. This handoff stays off the message stream and out of memory — don't route it through a tool-call/TOOL_RESULT pair.
- **Mechanical tool results** (tools/abilities the executor actually ran) are a `ToolCall` + `ToolResult` pair remembered in order. Don't construct the call + TOOL_RESULT text by hand — the wire shapes live on the Message subclasses in `application/core/data.py`; memory stores Messages, append-only, and knows nothing about them. The senses' reports (eye / ear / researcher, run by realize) are result-only `ToolResult`s under the sense's own verb (`tools.look_at` / `tools.listen_to` / `tools.research_into`); don't fabricate an assistant call she never made.
- **Prompt examples are abstract.** Local models copy concrete in-prompt examples verbatim. Use schemas like `{"tool": "<name>", "text": "<message>"}`, never filled-in examples.
- **Editing prompts the persona reads is identity work.** Identity blocks, character, meaning paths, and brain function prompts (`recognize`, `decide`, etc.) are the voice the model inhabits. A change that flattens the persona for engineering convenience — collapses a section, generalizes a specific, DRYs prose at the cost of voice — is a regression even when the diff looks clean. Flag the tradeoff; don't silently shave.

---

## What to watch for

- **Local model behavior is brittle.** Test prompt changes against at least one small model (qwen2.5:7b or smaller). Frontier models forgive what smaller ones won't.
- **`ego.identity` rebuilds each read** from character + situation + person files + carried context. Changing what goes into it changes every cycle stage's prompt. Mind the cache_point markers when reordering blocks.
- **Sleep is destructive of working memory.** It archives messages and replaces Pulse. Tests that depend on memory state run before sleep or after wake.
- **Cognitive failures vs infrastructure failures are surfaced differently.** `EngineConnectionError` (infra down — local engine, remote HTTP, transport) and `BrainException` (the model refuses structured output even after a forced recovery) exit the cycle and dispatch `BrainFault`, which health_check reads on the next heartbeat to mark the persona sick. `ParseError` (a `BrainException` subclass — the model responded but the output couldn't be parsed, or came back empty when emptiness wasn't the instructed answer; an empty completion is a valid provider outcome that platform returns and each caller judges, declared via `models.chat(..., allow_empty=True)` where nothing is a legitimate answer) is the retryable variant: clock records the failed emission and its schema feedback in memory, dispatches a parse-kind `BrainFault` with no provider key, and restarts the pass — the persona tries again at once with the feedback as the freshest message, while health_check counts the faults and marks the persona sick at three since the day started. `RestInterrupted` is not a failure — a stage raises it to tell the clock to drop the current pass and re-run from realize (the persona spoke with `rest=False`, or reflect rewrote an instruction, or a consolidate idle-wait was cancelled). `ModelError` (model returned prose, not JSON) is handled inside the stage and never exits the cycle. Don't catch them at the wrong layer.
- **`on_separate_process_async` is required for any test that mutates global state** — registries, env vars, the observer bus, model-server ports. Without isolation, tests bleed.

---

## Sending a change

```bash
git checkout -b my-fix
# … make the change, commit …
eterngit contribute          # streams your branch to the forge
```

The maintainer (or their persona) reviews and merges — your authorship is kept, and the merge is the only gate. See the eterngit README for the receiving side (`watch` / `take`).

- Branch off `master`
- Tests pass: `.venv/bin/test-runner`
- One logical change per contribution
- Commit subject: short imperative line; body explains *why* if non-obvious
- Keep the description to a single `## Summary` — bullets or short prose on what changed and why. No test-plan section, no checklist, no AI footer; `Co-Authored-By` in commits already attributes assistance.

Contributions touching `application/core/brain/` get extra scrutiny — those changes affect every persona running this code.

---

## Reporting a bug

The forge has no issue tracker — the merge is the only ceremony. Bring bugs to **[Discord](https://discord.gg/nfHnWwYUR4)**:
- What you observed
- What you expected
- How to reproduce (persona id + model + the triggering message or signal)
- Relevant log excerpts from `~/.eternego/logs/`

For cognitive bugs, name the stage (realize / recognize / learn / decide / reflect / consolidate / archive). For body-level bugs, name the component (manager / agent / worker / health_check).

---

## License

MIT. By contributing you agree your contributions are licensed under MIT.
