crab0.4.0

Design record

Decisions, with the arguments that lost.

crab keeps a sealed decision record: twenty-one entries, each with the reasoning attached, written so that the reasoning — not the decision — is what you have to refute before reopening it. Below is the substance of the ones that shaped the code you can see.

The original document is in the repository at DESIGN.md, in Chinese, and is explicitly not a to-do list. Its closing section names the three things most likely to kill the project: that agent quality lives in the prompt rather than the code, that self-hosting has a chicken-and-egg problem, and that the deferred-feature list will keep knocking.

Q1

A spiritual successor to pi, not a compatible port

crab borrows pi's concept model — an agent loop, a session log, tool descriptions that live next to the tool — but has its own format and its own extension mechanism. There is no .pi/ compatibility layer.

Why. A half-compatible tail makes users expect full compatibility. And copying pi's source, prompt text or session schema would be a derivative work with attribution obligations; the concepts are not copyrightable, the strings and formats are. That is why this repository is Apache-2.0.
Q2

Acceptance is a commit, not a benchmark

The criterion: a commit whose diff was produced entirely by crab. Reached at f3fec8e. Star counts, benchmarks and 'learn Rust' were explicitly rejected as measures.

Why. It is the only standard that cannot be self-deceived, and it forces out the real requirements — streaming interruption, long context, edit precision — faster than any roadmap.
Q4

Named crab, never published to crates.io yet

crab on crates.io is a CIL scraping tool that stopped shipping in 2020. The workspace crate is crab-agent, which was verified free. Publishing waits until the acceptance criterion is met and held.

Why. A name is a one-way door. Claim one and abandon it, and you become the next stale crab.
Q19

Two crates, cut once

crab-core holds the loop, session, provider, tools and prompt assembly with a hard rule: no terminal dependency, no println!. crab-cli holds clap, crossterm, reedline, rendering and the confirm UI.

Why. This is what makes a mock provider possible, and what lets someone embed the agent without being dragged into terminal dependencies. More crates would add ceremony, not safety.
Q11

The loop owns cancellation, not the channel layout

The agent runs on its own tokio task: mpsc for commands, broadcast for events, a cancellation token for abort. v0 implements submit, abort and one FIFO queue; steering vs follow-up semantics were deferred.

Why. Interruption must persist the text produced and the tool results completed so far — otherwise --continue replays an unpaired tool_use and most providers answer 400. Cancel-safety is the real problem; the channel structure is not.
Q3

One provider first; abstract on the second

A single OpenAI-compatible client got the loop working. The Provider trait was extracted only when a second provider arrived. Authentication is environment variables plus ~/.crab/auth.json; OAuth subscription login is explicitly out.

Why. Premature abstraction bends the trait into the shape of whichever API you saw first. OAuth was excluded for legal and token-refresh reasons, which are not technical problems you can code around later.
Q13

The SSE parser is a pure state machine

parse_chunk touches no IO. A tolerant accumulator keys slots by array index, degrading to 'append to the last slot' when the index is missing. Arguments are parsed only once complete; a parse failure feeds the raw string back to the model.

Why. OpenAI-compatible tool-call streaming is fragile and inconsistent across DeepSeek, Kimi, vLLM and llama.cpp. This is the single most test-worthy component in the project, and real SSE captures are kept as fixtures per provider.
Q8

Append-only JSONL sessions, no SQLite

Entries carry id and parent_id. v0 implements linear append plus --continue; branch, fork and clone are future work, but the fields exist now.

Why. SQLite would buy query power crab does not need and cost tail -f, git diff and hand-editing — which matter enormously when you are debugging an agent. Adding parent_id now is nearly free; adding it later means migrating every historical file.
Q6

Six tools, then seven — each described beside its implementation

v0 shipped read, write, edit, bash, glob, grep; web_fetch arrived later. Every tool's description text lives in the same file as its Tool implementation.

Why. When a description drifts from the behaviour, the model goes wrong in ways you cannot debug. Keeping them adjacent makes the drift a compile-time adjacency problem instead of a documentation problem.
Q14

edit is literal, unique, all-or-nothing, and gated on freshness

Exact match, no whitespace tolerance. Zero or more than one match is an error. Multiple edits match against the original text. Failure applies nothing. edit and write require a prior read this session and an unchanged mtime. Empty old is rejected.

Why. Fuzzy matching replaces a loud failure with a silent wrong edit, and the model reads 'ok' and continues — a disaster in an agent. The freshness gate blocks the worst failure mode: you fix something in your editor and the agent clobbers it with a stale read. Similarity search is used only to write the error hint, never to decide where to edit.
Q9

The permission gate is a seatbelt, and says so

The gate sits at the tool-call layer, not in a shell parser. Reads pass; writes, network and non-read-only bash ask; --yolo allows everything; a allows a class for the session. The README states outright that this is not a security boundary.

Why. Any command-string allowlist is bypassed by bash -c, eval, $(...), writing a script, or find -exec. A gate that looks like security but isn't is worse than one honestly labelled a seatbelt against typos and confused models.
Q7

Extension is MCP; in-process means fork and recompile

v0 exposes Rust traits (Tool, EventHandler, Renderer) in-process. The only external mechanism is MCP.

Why. Embedding JS (no async, no npm) or Deno (binary past 50 MB, blowing the size budget) or WASM (cannot draw a TUI, cannot see native event objects, and nobody writes WASM for a new agent) all end in a plugin system with zero plugins. Merging 'extension' with 'MCP server' inherits the whole existing ecosystem for free.
Q10

Streaming output and a line editor, not an alt-screen app

v0 uses crossterm streaming output with a reedline input line, with an upgrade path to ratatui Viewport::Inline + insert_before. A full-screen alt-screen app is excluded.

Why. Alt-screen takes over scrolling and selection, so you must write scrollback, mouse selection and search yourself — weeks of work unrelated to the acceptance criterion. Upgrading to inline is incremental; jumping to alt-screen is a rewrite. Streaming output also stays pipeable and | tee-able.
Q12

AGENTS.md and TOML

Context files are AGENTS.md; configuration lives in .crab/ as TOML. User customisation may only append to the system prompt, never replace it.

Why. Users already maintain AGENTS.md; inventing CRAB.md means a second file that gets ignored — a negative-sum game. JSON cannot hold comments, and comments are exactly what a config file needs. Full prompt replacement would let a user silently remove the tool protocol and then file the resulting bug against crab.
Q15

CI never calls a real model

MockProvider for the loop, recorded SSE fixtures for the parser, wiremock for transport (429 backoff, mid-stream disconnect, missing [DONE]), assert_cmd for the CLI contract, insta for snapshots. A manual live smoke test stays outside CI.

Why. A CI that exercises a model tests the model: it flakes, it costs money, and when it fails you cannot tell 'I wrote a bug' from 'the model is having a bad day'. --provider mock keeps a permanent entry point because it remains the fastest way to debug the loop.
Q5

Measure size in CI, measure time locally

Binary size is a hard CI assert (frozen at 16 MiB). Start-up time and idle RSS live in a local make perf script.

Why. The same code always compiles to the same byte count in CI, so size is a stable assertion. Start-up time on a shared runner varies 2–3×, which makes a hyperfine threshold a flake generator. Performance is defended against regression, but 'N× faster' was never the pitch — 'controllable and changeable' is.
Q17

Unwind stays enabled

The release profile sets lto = "thin", codegen-units = 1 and strip = true — but not panic = "abort".

Why. panic = abort conflicts with the cancellation design: tokio loses unwind capture, so a slice-overflow panic inside edit — which should become a JoinError::Panic fed back to the model as a tool failure — instead aborts the process and loses the session. The 5–10% size saving is not worth that.
Q16

No local tokeniser

Only the provider's reported usage is authoritative. A chars/4 approximation is used for one thing only: the pre-request 'about to exceed the window' warning.

Why. tiktoken-rs ships BPE tables into the binary, and is simply wrong for DeepSeek, Kimi and Claude. You would pay the size cost and still only get an estimate.
Q18

Ship binaries when a second person wants one

v0 was cargo install --git only. Prebuilt tarballs followed, and then a managed installer in the style of pi: versioned releases side-by-side under ~/.crab/install/releases/, a launcher that resolves the current one, and crab update verifying downloads against the release's SHA256SUMS.

Why. macOS signing and notarisation are pure tax. Paying it before anyone has asked is a way to feel productive without shipping — which is why the binaries came after the first tag, not before it.
Q21

The v0 fence, and the discipline of keeping it

Kept out of v0: slash commands, prompt templates, themes, compaction, session branching, image input, @ file references, MCP, sub-agents, OAuth, exact local token counts, OS sandboxing, and any JS/WASM plugin runtime. Most have since landed; others are still out.

Why. Every item is individually persuasive — 'just for a little while'. The only effective defence was a rule: add nothing outside the fence until you can do your own work with the tool. Which is what Q2 then measured.

Consequences

What this buys, and what it costs.

A record like this is not free. It constrains future work, and it makes the project slower to add features that other agents shipped months earlier.

In exchange you get a tool whose behaviour you can predict from a document, and a short list of the reasons each surprising-looking choice was made.

Three tensions the record admits

  • Prompt quality dominates code quality. If crab feels dumber than pi, no performance number will save it, and it will look like a bug in the loop. Mitigation: a fixed set of real tasks re-run by eye after every prompt change.
  • Self-hosting is a chicken-and-egg problem. crab cannot write crab until edit is good enough. Mitigation: a graded threshold — the first crab commit was allowed to be "a typo fix plus a test".
  • The deferred list keeps knocking. Mitigation, and the only one that worked: add nothing outside the fence until you can do your own work with the tool.

Read further

The user-facing manual is separate.

The design record explains why; the README and the docs explain how. The dogfood log explains what actually happened.

esc