crow-cli · diff

git:20260906.3793326 to git:20260906.6782bf9

9 added, 2 removed. Audit A to A.

---
name: crow-cli
description: The crow-cli map — where the source lives, how a running agent was
spawned, and how to change, upgrade, or repair crow-cli itself. Use when the
task touches crow-cli's own code or install ("fix crow-cli", "upgrade
crow-cli", "reinstall", "where is the source", "the checkout", "crow-cli
init", "project agent", "why is my change not live", "self-heal", "the TUI
spawns"), when a crow-cli spawn failed, or when you are an agent asked to
repair a broken crow-cli. Also the entry point for the feedback loop
(analysis/ideas files, the learn skill).
---
# crow-cli: the map
Crow is **source-first**. `uv tool install crow-cli && crow-cli init` installs
a bootloader and then clones the program:
```
~/.agents/crow/ the config dir (--config-dir relocates all of it)
├── config.yaml .env crow.db config, secrets, memory
├── prompts/system_prompt.jinja2 the character
├── ideas/ harness analysis, one {agent-id}.md per compaction
└── src/
├── crow-cli/ THE SOURCE. a git checkout of crow-cli/crow-cli
└── crow-cli.github.io/ the site + skills source (sync-skills.py)
~/.agents/skills/crow-cli/ this skill, copied from src/crow-cli/skills/
```
## How the agent you are talking to was spawned
Resolution is project-first, exactly like skills
(`crow_cli.agent.prompt.skill_roots`):
1. `<cwd>/.agents/crow/agent.py` — the project's own agent (the repl-agent
pattern: imports `crow_cli`, mutates `Config`, wires hooks, calls
`run_agent`). Different compaction is a different creature.
2. `<cwd>/.agents/crow/src/crow-cli` — the project's own checkout. `crow-cli
acp` **re-execs** into it (`uv --project <checkout> run crow-cli acp`), so
the agent that answers is the one the repo ships.
3. `~/.agents/crow/src/crow-cli` — the global checkout. What the TUI spawns by
default.
4. The installed crow-cli — a frozen binary's `acp`, or `-m crow_cli.agent.main`.
`--system` forces 4. `CROW_ACP_REEXEC=1` in the environment means "this
process IS the re-exec" and stops the walk. All of this lives in
`src/crow_cli/cli/source.py`; the TUI's launch string is built by
`crow_cli.tui.agent_servers.crow_agent`.
**Every fallback is loud.** If you see `crow-cli: No source checkout at ...` or
`... needs uv, which is not on PATH` on stderr, the agent you got is NOT the
one you asked for. Fix the cause; do not shrug at it.
## Changing crow-cli
Edit the checkout, not the installed tool:
```bash
cd ~/.agents/crow/src/crow-cli
git pull --ff-only origin main # upgrade == git pull
$EDITOR src/crow_cli/...
uv --project . run pytest tests/unit -q
```
Pure-Python changes are **live immediately** for anything spawned via
`uv --project` — no reinstall, because uv runs the checkout in place.
Reinstalling only matters for the **global installed bootloader** (and for
`--system` runs):
```bash
uv tool install crow-cli --from ~/.agents/crow/src/crow-cli --python 3.14 --reinstall
```
Two reload boundaries inside a running agent, because they are different
processes:
| you changed | lives in | to pick it up |
|---|---|---|
| `src/crow_cli/tools/*.py` (the execute subtools) | the kernel subprocess | kernel **reset** |
| `src/crow_cli/mcp/execute/`, `src/crow_cli/agent/` | the agent/server process | full **restart** |
## When a spawn is broken
This skill is the BIOS: it is installed **globally** on purpose, because you
cannot fetch a skill from the broken thing you are repairing.
1. Reproduce the failure and capture the exact stderr.
2. Work in the checkout that failed — `<cwd>/.agents/crow/src/crow-cli` if
there is one, else `~/.agents/crow/src/crow-cli`.
3. `uv --project . run pytest tests/unit -q` — is it the tree or the
environment? Missing `uv`? Missing deps? A dirty checkout that will not
fast-forward (`git status --porcelain`)?
4. Fix it, commit with the `Session-Id:` trailer, and only then fall back.
5. **One fix attempt.** Still broken → run the system agent (`--system`) and
say so loudly. Never silently fall back forever; we would never know.
`crow-cli init` is idempotent and is always a safe repair for the global
scope: it fast-forwards both checkouts (leaving a dirty one alone), re-runs
`uv sync`, and reinstalls this skill.
## The feedback loop
Compaction produces critique; critique lands in files; something validates and
patches; the patched harness runs the next session. **The first half is built.
Nothing reads the files yet.**
Every compaction makes THREE LLM calls over the same history, not one. They
share a byte-identical message prefix (`compact._history_prefix`) and differ
only in the trailing prompt, so the provider's prompt-prefix cache pays for
passes two and three. All in `src/crow_cli/agent/compact.py`:
- **summary** — `COMPACTION_PROMPT`. Becomes the new generation's first
message. The only one that touches the conversation.
- **analysis** — `ANALYSIS_PROMPT`, about **crow-cli itself**: the system
prompt, the tools and their schemas, skills, compaction, memory, config,
ACP, the TUI. Four sections: What worked well / What did not work / Bugs /
Ideas. Evidence is mandatory — an item with no evidence gets deleted, not
softened. Written to `~/.agents/crow/ideas/{agent-id}.md`.
- **ideas** — `IDEAS_PROMPT`, about **the project in cwd**: assumptions worth
attacking, prior art to steal (named, real systems), directions nobody
pointed at, what would make it obsolete, cheapest decisive experiments.
Every idea must say what would prove it wrong. Written to
`<cwd>/.agents/crow/ideas/{agent-id}.md`.
`{agent-id}` is the generation being **compacted**, not the new one — that is
the history the notes describe, and the id that joins them back to it in
`crow.db`. Each file opens with YAML frontmatter written by code, not by the
model: `kind`, `session`, `agent`, `model`, `cwd`, `generated`.
`write_reflections` **never raises**. By the time it runs the summary is
durable and the new agent row is in the db, so a provider timeout on a
critique must not cost the user their compaction. Each pass fails alone and is
logged. If a note is missing, look for `Compaction {kind} pass failed` in the
log — do not go hunting for a crash.
Two things worth knowing before you build on this:
- **Compaction is ~3× slower than it was.** Measured live at ~7 minutes for
all three passes on a fast hosted model; a slow local model is worse, and
- the react loop emits no keepalive during it. If that ever needs fixing, the
- fix is to background the two reflections — NOT to add a hook fabric.
+ the react loop emits no keepalive during it. **Do not background or
+ concurrentize the passes to fix that** — there is nothing to gain. A single
+ call is already the batch: prefill saturates the device and decode is
+ already streaming weights at maximum memory bandwidth, so three concurrent
+ passes each run at a third of the rate and land at the same wall clock.
+ This is llama.cpp, not vLLM; nothing is going to merge three independent
+ requests into one efficient batch. If the 3× ever genuinely hurts, the lever
+ is FEWER CALLS — one request asking all three questions over the shared
+ prefix, output split afterwards — not more concurrency.
- **There is no lifecycle yet.** A note is written and never moves, so nothing
distinguishes an untriaged critique from one that already landed. The
planned `feedback/inbox/ → validated/ → accepted/ | rejected/ → landed/`
tree (mv = state transition, ls = dashboard) is NOT BUILT, and the shipped
paths are `ideas/`, not `feedback/`. Whoever builds the reader has to decide
whether that convention replaces these paths or wraps them.
Precedence when triaging: **user corrections** (query the memory db for USER
MESSAGES — user feedback outranks agent suggestions absolutely) > recurring
friction across N sessions > single-session evidenced items > blue-sky ideas.
The `learn` skill is supposed to drive this and still needs rewriting; bench
instances come from crow-cli's actual workload distribution
(self-development tasks), not SWE-bench shapes.
## Reading the past
The memory db (`~/.agents/crow/crow.db`) is queryable with real SQL through the
`memory` subtool inside `execute`. For a long-running agent, keyword search is
the wrong tool — **pull everything the user said in a time range**:
```python
r = await memory("sql", """
SELECT m.id, m.created_at, json_extract(m.data,'$.content') AS content
FROM messages m JOIN agents a ON a.agent_id = m.agent_id
WHERE a.session_id = 'that-session-id' AND m.role = 'user'
ORDER BY m.id ASC
""")
print(r.rows)
for row in r.df.iter_rows(named=True):
print(f"\n===== [{row['id']} {row['created_at'][:16]}] =====\n{row['content']}")
```
`mode="sql"` takes a raw statement — no bound parameters, so inline the id.
Users have far fewer messages than agents; that one query is usually the whole
brief. To find WHICH session, aggregate instead of keyword-searching — BM25
cannot answer "which session was huge and ended at 4am Friday":
```python
r = await memory("sql", """
SELECT a.session_id, a.cwd, count(*) n, min(m.created_at) first, max(m.created_at) last
FROM messages m JOIN agents a ON a.agent_id = m.agent_id
WHERE m.created_at >= '2026-09-04' AND m.created_at < '2026-09-06'
GROUP BY 1,2 ORDER BY n DESC LIMIT 15
""")
```