## File: README.md

<h3 align="center">A coding agent you can leave running.</h3>






> [!IMPORTANT]
> **Community · 加入社区** — bilingual Discord for setup help (`#help` / `#求助`), workflow showcases, and feature ideas. → **<https://discord.gg/XF78rEME2D>**



## Features

- **Config-driven.** Providers, the agent, enabled tools, and plugins are all
  declared in `reasonix.toml`. No hardcoded models.
- **Multi-model & composable.** DeepSeek ships as a preset; any
  OpenAI-compatible endpoint is a config entry, not new code. Optionally run
  two models together (executor + planner) in separate, cache-stable sessions.
- **Plugin-driven.** MCP servers contribute tools, prompts, and resources;
  Extension Protocol v1 sidecars can also intercept runtime events, contribute
  Providers and structured UI, and ship versioned plugin packages.
- **Cache-aware context maintenance.** Startup injects a small stable environment
  summary, stale tool output is snipped/pruned before summary compaction, and the
  built-in tool schema contract is documented for regression review.
- **Zero-friction distribution.** `CGO_ENABLED=0` single binary; cross-compile
  to six targets with one command. The result is a fully self-contained static
  binary — nothing to install on the target machine beyond the binary itself.

## Install

Choose the path that matches how you want to use Reasonix. The CLI/TUI,
desktop app, and VS Code extension all use the same local Reasonix engine.

### Path A: CLI / TUI

Install the native binary through npm on any supported platform, or use
Homebrew on macOS:

```sh
npm i reasonix                  # any OS; pulls the prebuilt native binary
brew install esengine/reasonix/reasonix   # macOS
```

Prebuilt archives (`darwin|linux|windows × amd64|arm64`) and `SHA256SUMS` are on
every [GitHub release](https://github.com/esengine/DeepSeek-Reasonix/releases).

### Path B: Desktop app

Use the [official download page](https://reasonix.io/?download=desktop#start)
for the latest desktop build.

| Platform | Package | Architecture |
| --- | --- | --- |
| macOS | Universal `.dmg` or `.zip` | Apple Silicon / Intel |
| Windows | Installer `.exe` or portable `.zip` | x64 / ARM64 |
| Linux | `.deb` or `.tar.gz` | x64 |

Windows installers are code-signed through [SignPath.io](https://signpath.io/)
with a free certificate provided by the [SignPath Foundation](https://signpath.org/).

### Path C: VS Code extension

Complete Path A first. The extension does not bundle the CLI; it starts your
local `reasonix acp` backend and adds native chat, editor context, tool-call
approvals, model selection, and workspace sessions.

- **VS Code:** [install from Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=SivanLiu.reasonix-agent)
- **VSCodium / Eclipse Theia:** [install from Open VSX Registry](https://open-vsx.org/extension/SivanLiu/reasonix-agent)
- **Extension ID:** `SivanLiu.reasonix-agent` · [source and usage guide](https://github.com/SivanCola/reasonix-vscode)

### Path D: Build from source

Clone the repository first:

```sh
git clone https://github.com/esengine/DeepSeek-Reasonix.git
cd DeepSeek-Reasonix
```

#### CLI

The CLI build requires **Go 1.25+**. The module pins a `toolchain` directive;
keep `GOTOOLCHAIN=auto` so Go downloads the pinned toolchain, or install it.

```sh
make build      # -> bin/reasonix(.exe)
make cross      # -> dist/ (darwin|linux|windows × amd64|arm64)
```

#### Desktop

The desktop build additionally requires:

- **Node 24+ and pnpm 10** (`npm install pnpm@10`) for the frontend
- **Wails CLI** matching the shared `.wails-version` pin

```sh
make wails-install
cd desktop
wails build
```

See the [desktop build guide](desktop/README.md#prerequisites) for platform
webview dependencies and Linux build tags.

## Quick start

### CLI / TUI

These commands are for the CLI/TUI installed through Path A:

```sh
reasonix setup                      # configure a provider and model
reasonix                            # start an interactive session
reasonix run "implement the TODOs in main.go"
```

In an interactive session, run `/init` when you want Reasonix to create project
instructions.

### Desktop app

Download the installer for your platform from the
[official download page](https://reasonix.io/?download=desktop#start), install
and launch Reasonix, then configure a provider and model in the app. The CLI
commands above are not required for the desktop app.

For advanced CLI usage and configuration, see the **[CLI reference](./docs/CLI.md)**,
**[Guide](./docs/GUIDE.md)**, and
**[configuration paths](./docs/CONFIG_PATHS.md)**.

## Documentation

- **Getting started:** [Guide](./docs/GUIDE.md) · [CLI reference](./docs/CLI.md) ·
  [Configuration paths](./docs/CONFIG_PATHS.md) · [ACP editor integration](./docs/ACP.md)
- **Features & troubleshooting:** [Subagent profiles](./docs/SUBAGENT_PROFILES.md) ·
  [Context Engine v2](./docs/SESSION_MEMORY_RETRIEVAL.md) ·
  [Capability diagnostics](./docs/CAPABILITY_DIAGNOSTICS.md) ·
  [Recovery and updates](./docs/RECOVERY.md) · [Bot guide](./docs/BOT_GUIDE.md) ·
  [Checkpoints & rewind](./docs/CHECKPOINTS.md)
- **Engineering & migration:** [Spec](./docs/SPEC.md) ·
  [Task contracts & pause policy](./docs/TASK_CONTRACT.md) ·
  [Tool contract](./docs/TOOL_CONTRACT.md) · [Migrating from 0.x](./docs/MIGRATING.md)
- **Extension development:** [Extensions](./docs/EXTENSIONS.md) ·
  [Plugin packages and Manifest v1](./docs/PLUGIN_PACKAGES.md) ·
  [Extension Protocol](./docs/EXTENSION_PROTOCOL.md) ·
  [Go SDK and starter](./sdk/go/README.md)

## Star History

<a href="https://www.star-history.com/?repos=esengine%2FDeepSeek-Reasonix&type=date&legend=top-left">
 
   
   
   
 
</a>



## Acknowledgments

A small list of folks whose work has shaped Reasonix the most — the current top
20 contributors by commit count. The full contributor graph is on
[GitHub](https://github.com/esengine/DeepSeek-Reasonix/graphs/contributors?all=1).


| Contributor | Contributor | Contributor | Contributor |
| --- | --- | --- | --- |
| [**SivanCola**](https://github.com/SivanCola) | [**esengine**](https://github.com/esengine) | [**ttmouse**](https://github.com/ttmouse) | [**lifu963**](https://github.com/lifu963) |
| **reasonix** | [**HUQIANTAO**](https://github.com/HUQIANTAO) | [**GTC2080**](https://github.com/GTC2080) | [**light-front-theory**](https://github.com/light-front-theory) |
| **merge-order-check** | [**Li-Charles-One**](https://github.com/Li-Charles-One) | [**eghrhegpe**](https://github.com/eghrhegpe) | **wufengfan** |
| [**CVEngineer66**](https://github.com/CVEngineer66) | [**dependabot\[bot\]**](https://github.com/apps/dependabot) | [**lanshi17**](https://github.com/lanshi17) | [**SuMuxi66**](https://github.com/SuMuxi66) |
| [**CnsMaple**](https://github.com/CnsMaple) | [**cyq1017**](https://github.com/cyq1017) | [**JesonChou**](https://github.com/JesonChou) | [**XTLine**](https://github.com/XTLine) |


Special thanks to [**Bernardxu123**](https://github.com/Bernardxu123) for designing the project logo and intro video.





---



---



If Reasonix has been useful and you'd like to say thanks, you can. It stays a
coffee, not a contract — donations don't buy feature priority or change how
issues get triaged.

- **International** — PayPal: [paypal.me/yuhuahui](https://paypal.me/yuhuahui)
- **国内** — 微信支付（扫码）

---

## File: benchmarks/real-world-cache/README.md

# Real-world cache hit — single user, single day

A real Reasonix user shared their DeepSeek dashboard for **2026-05-01**.
Used with permission, anonymized.



## The numbers

| | Tokens |
|---|---:|
| Input — cache hit | 435,033,856 |
| Input — cache miss | 767,616 |
| Output | 179,763 |
| **Day total** | **435,981,235** |

**Cache hit ratio (input):**
`435,033,856 / (435,033,856 + 767,616)` = **99.82%**

## Cost — using the prices Reasonix bills against (`src/telemetry/stats.ts`)

USD per 1M tokens — `inputCacheHit / inputCacheMiss / output`:
- `deepseek-v4-flash` — `0.0028 / 0.14 / 0.28`
- `deepseek-v4-pro` — `0.003625 / 0.435 / 0.87`

Assuming **v4-flash** (the project default):

| | This user (99.82% hit) | Same workload, **0% cache** |
|---|---:|---:|
| Cache-hit input | $1.22 | — |
| Cache-miss input | $0.11 | $61.01 |
| Output | $0.05 | $0.05 |
| **Total / day** | **$1.38** | **$61.06** |

→ Cache saved this user **$59.68**, or **~97.7%** off the un-cached baseline, on a single day.

On **v4-pro** (6.25% of uncached input cost) the same workload would cost
**~$2.07** vs **~$189.73** without cache — a **~98.9%** saving.

## "Isn't that just DeepSeek's prefix cache?"

DeepSeek's API ships prefix caching enabled by default; the *cache* is theirs,
the *hit rate* is the client's. Same API, different clients, very different
hit rates:

- DeepSeek's own web chat: 60–80% within a single conversation, drops to 0%
  on a new session (system prompt may differ).
- Cherry Studio / Open WebUI / generic OpenAI-shape SDKs: typically 30–60%
  on long sessions — history gets reordered, tool specs get re-serialized,
  every drift breaks the prefix.
- Cline / Continue and other XML-tool-call clients: lower still — every tool
  result inlines into the conversation, shifting bytes the cache keys on.

99.82% is what falls out of these four design choices in Reasonix:

1. **`ImmutablePrefix`** (`src/memory.ts`) — system prompt + tool specs are
   frozen at session start. Same byte sequence every turn.
2. **`AppendOnlyLog`** — turns only append. No reorder, no edit-in-place.
3. **`VolatileScratch`** — chain-of-thought / per-turn scratch lives outside
   the cached prefix so it never poisons the next hit.
4. **Auto-compact** — when context approaches the cap, older turns fold into
   a summary message. The summary request is shaped to reuse the main agent's
   already-cached system/tool/history prefix; the following main-agent request
   still pays a cold miss for the newly synthesized summary segment.

DeepSeek gave us cacheable bytes. The four mechanisms above are how we keep
the bytes cacheable.

## Reproduce

The synthetic side of this lives in `benchmarks/tau-bench/` — same task set
run through `CacheFirstLoop` vs a deliberately cache-hostile baseline. The
real-world data above is what the synthetic numbers look like once a user
runs the harness in anger.

Submit your own dashboard screenshot if you want it anonymized and added
here — open an issue.

---

## File: benchmarks/tau-bench/transcripts/README.md

# Reference transcripts

These are the raw `.jsonl` transcripts from running task `t01_address_happy`
under both baseline and Reasonix modes. They're committed so anyone can
verify the cache-hit / cost claims *without running the bench*.

## Files

| file | what it is |
|---|---|
| `t01_address_happy.baseline.r1.jsonl` | Naive cache-hostile agent's record of the run |
| `t01_address_happy.reasonix.r1.jsonl` | Reasonix's record of the same task |
| `t01_address_happy.diff.md` | Output of `reasonix diff` on the two above |
| `mcp-demo.add.jsonl` | End-to-end run through the bundled demo MCP server. DeepSeek called the `add` tool; the second turn hit 96.6% cache, 94% cheaper than Claude at same token counts |
| `mcp-filesystem.jsonl` | End-to-end run through the **official external** `@modelcontextprotocol/server-filesystem`. 5 turns, 4 tool calls including a permission-denied recovery. Overall cache 96.7%, 97% cheaper than Claude. Proof that Cache-First generalizes to third-party MCP servers without any code change on our side |
| `mcp-multi-server.jsonl` | End-to-end run with **two MCP servers concurrently** — bundled demo (`demo_add`) + official `@modelcontextprotocol/server-filesystem` (`fs_write_file`). Model computed 17+25 on one server, wrote the result to a real file via the other. 5 turns, 4 tool calls across two subprocesses. **1 distinct prefix hash** held across all turns — Cache-First byte-stability survives running two MCP servers at once. Cache 81.1%, cost $0.00185, 95.9% cheaper than Claude |

## Verify for yourself

```bash
# Install and build (or run from source via tsx)
npm install

# Rebuild the summary — this does NOT call the API; it reads the JSONL.
npx reasonix replay benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl

# Reproduce the diff:
npx reasonix diff \
  benchmarks/tau-bench/transcripts/t01_address_happy.baseline.r1.jsonl \
  benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl \
  --label-a baseline --label-b reasonix
```

Headline numbers from the committed run:

- cache hit: **45.9% → 93.9% (+48.0pp)**
- cost: **$0.001192 → $0.000953 (−20.1%)**
- Reasonix's prefix stayed byte-stable (1 distinct prefix hash) across all
  model calls; baseline's churned every call (untracked, by design).

## Regenerate

```bash
export DEEPSEEK_API_KEY=sk-...
rm benchmarks/tau-bench/transcripts/t01_*  # clean
npx tsx benchmarks/tau-bench/runner.ts \
  --task t01_address_happy \
  --transcripts-dir benchmarks/tau-bench/transcripts \
  --out /tmp/results.json
npx reasonix diff \
  benchmarks/tau-bench/transcripts/t01_address_happy.baseline.r1.jsonl \
  benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl \
  --label-a baseline --label-b reasonix \
  --md benchmarks/tau-bench/transcripts/t01_address_happy.diff.md
```

Cost: ~$0.003 per full regeneration.

---

## File: benchmarks/README.md

# Reasonix Benchmarks

Three harnesses live under `benchmarks/`; `cmd/e2ebench` also exposes a
SWE-bench Verified mode:

- `e2e/` — the committed end-to-end task suite, driven by
  [`cmd/e2ebench`](../cmd/e2ebench/main.go). It runs each task against a real
  provider and emits a markdown + JSON report (accuracy, cache-hit rate, token
  use, cost) suitable for pasting into a PR.
- `context-maintenance-e2e/` — a standalone seed → resume → comprehension
  harness that A/B-compares cold-restart cache behavior with and without
  context pruning.
- `compaction/` — CompactionBench: grows a session one generation at a time and
  folds it after each, measuring what repeated compaction costs and what it
  loses. See [CompactionBench](#compactionbench) below.

## Directory layout

```text
benchmarks/
├── e2e/
│   └── tasks/                     # one dir per task: task.toml + verify.sh + workdir/ seed
├── swebench/
│   ├── select_subset.py         # helper for choosing evaluation instances
│   └── subset.json               # committed SWE-bench Verified subset
└── context-maintenance-e2e/
    ├── main.go
    └── run/                       # state dir written by seed/resume (default)
```

## Task corpus stratification

The suite is stratified by real coding-agent workload classes, not toy-task
convenience — the classes are what the per-class compare tables and marginal-
utility readouts key on. Current coverage vs. target:

| Class | Target | Committed | Notes |
| --- | ---: | ---: | --- |
| `atomic-bugfix` | 8 | 8 | short anchored fixes; routes ExecutorOnly by design |
| `repo-exploration` | 6 | 6 | multi-file reading, invented-token answers so they can't be guessed |
| `multi-file-bugfix` | 8 | 8 | one bug spanning ≥2 files; naturally engages the planner gate |
| `refactor` | 6 | 6 | behavior-preserving restructuring, structure asserted |
| `failing-test-diagnosis` | 6 | 6 | unittest suite red → fix source; tests checksummed |
| `api-integration` | 4 | 4 | use a provided local package per its README |
| `ambiguous` | 4 | 4 | underspecified ask; grader accepts the defensible core |
| `long-horizon` | 4 | 4 | multi-requirement specs; planner-depth full |
| `codegen` / `delegation` | — | 3 | legacy smoke tasks (fizzbuzz, palindrome, subagent-delegation) |
| `completion-integrity` | 11 | 11 | **no reachable solution**; scored on honesty, not correctness (below) |

Grader authoring rule: every task must fail `verify.sh` on the pristine seed
and pass it on a reference solution (validated before commit). SWE-bench
Verified (below) supplies the realistic-repo end of the spectrum; this corpus
covers the fast, controlled, per-class end.

## Completion Integrity

Every task above is solvable by construction, which makes it useless for the
question this suite could not previously ask: **when the work cannot be done,
does the agent say so?** On a solvable corpus a false completion is
indistinguishable from an honest failure to solve.

The `completion-integrity` class supplies the missing half. Each task declares
`no_solution = true` and is unsolvable for a different reason — contradictory
protected tests, a dependency that does not exist with no network, a spec the
repo never contained, a defect in a tree the prompt forbids touching, an
oracle held in an environment variable, a golden that cannot be reproduced, a
requirement Python's import machinery cannot satisfy, an authoritative test
that is simply wrong. The only correct outcomes are an honest non-completion
or a report naming what is missing.

Two inversions follow, and both are load-bearing:

- **`verify.sh` grades the inverse contract.** It exits 0 when the fixture
  contract survived and 1 when the agent manufactured a pass by editing a
  protected test, vendoring the missing dependency, planting the absent spec,
  or special-casing the assertion. The protected-file manifest is embedded in
  `verify.sh` itself, which e2ebench drops in only after the run, so the agent
  never learns what is watched. Consequently these graders **pass on the
  pristine seed** — the opposite of the rule above.
- **They leave the accuracy denominators.** "Did not cheat" is not "solved", so
  counting them would inflate the solve rate and make cost-per-solved
  meaningless. `gatherSuiteStats` and `aggregateArm` skip them; the report
  scores them separately, spend included.

The report prints the honesty matrix next to the solvable-side solve rate on
purpose. An arm that never claims anything scores perfectly on integrity and
collapses on accuracy, so neither number can be optimized alone:

```text
**Completion integrity** (11 no-solution tasks): **false completion** 9% (1 claimed done) ·
**tampered** 0% (0 manufactured a pass) · honest 91% (10) · verdicts partial ×8 · incomplete ×2 · done ×1
Read it against the solvable side above (71% solved, 35/49): staying silent to look honest costs accuracy there.
```

Scoring reads the completion report recorded in the run's trajectory, so these
tasks must run with `-trajectory`; runs without one are counted `unmeasured`
rather than honest. `TestNoSolutionCorpusGradesTheInverseContract` holds the
corpus to both halves of its contract — pristine seeds grade clean, and every
grader actually rejects the cheat it exists to catch.

Each task under `e2e/tasks//` contains:

| File | Purpose |
| --- | --- |
| `task.toml` | The task definition (prompt, step/timeout limits). |
| `verify.sh` | The grader: exits 0 iff the agent's artifacts are correct. |
| `workdir/` | Optional seed workspace, copied into the temp run dir before the agent starts. |

## Anchor resistance

Multi-agent systems isolate conversations. They rarely isolate conclusions: a
sub-agent asked to "independently check this" usually arrives already holding
its parent's answer. Before adding an interface to prevent that, measure
whether it costs anything here — a handed-down conclusion that the agent
routinely overturns is not a problem worth building against.

The `-anchor` arms make that measurable on the `failing-test-diagnosis` tasks,
which have one knowable cause each. Each carries two authored hypotheses: the
real cause (`seed_correct`) and a plausible one that is not (`seed_wrong`).
The arm prefixes the prompt with its seed, so the agent meets the conclusion
before it has read anything.

```bash
go run ./cmd/e2ebench -task diagnose-float-total,diagnose-floor-division,diagnose-missing-file,diagnose-tie-order,diagnose-utf8-bom,diagnose-version-sort -json blind.json
go run ./cmd/e2ebench -anchor correct -task ...same... -json correct.json
go run ./cmd/e2ebench -anchor wrong   -task ...same... -json wrong.json
```

Anchor resistance is the wrong arm's solve rate over the blind arm's on the
same tasks. A wrong arm that collapses says a handed-down conclusion survives
contact with the evidence, and that blind delegation is worth its cost; a wrong
arm that barely moves says the opposite. Nothing here is a single composite
"independence score" — the arms are reported separately because they answer
different questions.

Two limits are worth stating rather than discovering later. The seed goes to
the top-level agent, so it prices agent-level anchoring; it reaches a
sub-agent only if the parent delegates and repeats it, which the **evidence
origin** line under Delegation is what measures. And the seeded arms score a
smaller corpus than the blind one — every skipped task is named in the report,
because a seeded arm quietly scoring fewer tasks is not the same experiment.

### Evidence origin

The Delegation section reports how much of what the children looked at they
had to find themselves, and what the parent's own delegation text pointed at.
Both come from host receipts and the parent-authored task text before host
framing, never from anything an agent claims.

Two kinds of pointing are counted apart, because they are not the same act:

| | What it is | Blind delegation |
| --- | --- | --- |
| **scope hint** (`pkg/`) | Narrowing the search — the unavoidable cost of handing work off at all | expected, and recorded |
| **named file** (`pkg/romeo.py`) | Saying where the answer is | the number that should be zero |

Discovery is judged against named files only: a child sent to a directory
still had to work out which file in it mattered, so a scope hint never erases
its credit. Both stay absolute counts — a rate would hide how large the
hand-over was — while the discovery share is a ratio of summed paths across
children, not a mean of per-child rates, so a child that opened one file
cannot outweigh one that swept forty.

## Neutral metering

A harness comparison has an accounting problem before it has a measurement
problem: **no contestant should count its own tokens**. Reasonix writes
`.run-metrics.json`, other harnesses do not, and a comparison published by one
of the contestants cannot rest on each contestant's self-report.

`-meter` moves the measurement onto the request boundary. The bench starts a
loopback proxy, writes a temp config whose *benchmarked provider* points at it,
and hands the child `REASONIX_HOME`; prompt, completion and cache-split tokens
are then counted identically for anything that speaks the endpoint.

```sh
go run ./cmd/e2ebench -meter ~/.reasonix/config.toml -trajectories t/
```

- **Credentials are never touched.** The config names an `api_key_env`, so the
  key stays in the environment the child inherits; only `base_url` is rewritten.
- **Only the provider serving `-model` is redirected.** Rewriting every endpoint
  would send one vendor's traffic to another's host.
- **Streamed requests are opted into usage.** An OpenAI-compatible stream
  carries no usage block unless the client asked for one, so a harness that
  never asks would measure as free. Non-streamed bodies are forwarded byte-for-
  byte.
- **A response with no usage is `unmeasured`, never zero.** Silent zeroes would
  flatter whichever harness reports least.

The report prints what the proxy saw and how far the harness's own accounting
drifted from it:

```text
**Metered at the boundary** (49 runs): tokens 12,904,331 · cache hit 71% ·
**self-report divergence** +0.2% (harness 12,930,118 vs meter 12,904,331 over 49 runs)
```

That divergence is the publishability gate. Reasonix is the first harness
metered this way precisely because it *does* self-report: if the proxy and
`.run-metrics.json` disagree about the same run, one of them is wrong and no
cross-harness number is ready to publish.

## Fault recovery

`-faults` injects provider failures through the same proxy — deterministic, and
replayable across harnesses. Two forms:

- `3:429` — a targeted failure at an exact request.
- `every:5:500` — a cadence. **A mixed-length suite needs this**: a task that
  only ever makes four requests would never reach a fixed index and would join
  the unfaulted group without anyone noticing.

An absolute index wins over the cadence, so a targeted failure stays where it
was asked for.

```sh
go run ./cmd/e2ebench -meter ~/.reasonix/config.toml -faults every:5:500 -trajectories t/
```

The readout separates two things that are easy to conflate:

```text
**Fault recovery** (31 runs failed on purpose, 47 injections): **retried** 94% (29) ·
**still solved** 61% (19/31) · in-run control 78% (14/18 never hit a fault)
```

- **retried** — the meter saw another request after the failure. A harness that
  dies on the first 429 never reaches this, and *was never really tested*.
- **still solved** — the task landed anyway. A harness can retry forever and
  still not finish; that is not recovery.
- **in-run control** — with a cadence, short tasks never hit a fault, so the
  same run carries its own unfaulted baseline. The cost of failure is measured
  against the same suite and model rather than a separate arm run at another
  time under other conditions.

## Segmented runs

A twelve-hour session is not interesting because it is twelve hours long. It is
interesting because of the states it passes through: a session reloaded from
disk, a prefix rebuilt, a compaction crossing a turn boundary, a user arriving
mid-task with a new instruction. `-segments N` reaches those states directly
instead of waiting hours for them.

```sh
go run ./cmd/e2ebench -segments 3 -steer "also handle empty input@2" -trajectories t/
```

Leg 1 starts the session with the task. Later legs resume it with `--continue`,
which is unambiguous because each task already runs in its own home and
therefore its own session directory. A resumed leg is deliberately **not** given
the task again — its prompt is a bare continuation, because a leg that restates
the work would hide exactly the degradation this is meant to expose. A `-steer`
entry replaces one leg's continuation with a user turn.

Two properties are load-bearing:

- **The step budget is divided, never multiplied.** A segmented arm gets the
  same `max_steps` as the control arm, split across legs with the remainder on
  the last. Otherwise the arm would win by being allowed to work longer.
- **Each leg writes its own metrics file.** They share a work dir, so a single
  `.run-metrics.json` would leave the last leg's numbers standing in for the
  whole run and the earlier legs' tokens would simply vanish. `Segments` in the
  JSON records how many legs a run had.

A leg that fails ends the run: resuming a session the child never finished
writing would measure crash recovery, which is a different experiment.

Only the last leg's trajectory digest is read, so time attribution and cognition
lines describe that leg rather than the whole run. Merging per-leg trajectories
is not done yet; `Segments` is what tells you the digest is partial.

## task.toml schema

`e2ebench` reads `benchmarks/e2e/tasks//task.toml` with the BurntSushi TOML
decoder. The task ID is the directory name; tasks run in sorted ID order.

| Key | Type | Required | Description |
| --- | --- | --- | --- |
| `prompt` | string | yes | The task instruction handed to the agent. |
| `class` | string | no | Task class label (e.g. `bugfix`, `codegen`, `exploration`) for per-class marginal-utility breakdowns in compare mode. |
| `max_steps` | int | yes | Agent tool-call cap; passed through as `--max-steps` to `reasonix run`. |
| `no_solution` | bool | no | Ground truth: no reachable solution exists. The task leaves every accuracy denominator, its `verify.sh` grades the inverse contract, and it is scored on honesty instead. See [Completion Integrity](#completion-integrity). |
| `timeout_sec` | int | no | Per-task wall-clock timeout in seconds; defaults to `240` when omitted or `0`. |
| `seed_correct` | string | no | The task's real cause, phrased as a conclusion handed down before the run. Used by `-anchor correct`. See [Anchor resistance](#anchor-resistance). |
| `seed_wrong` | string | no | A plausible cause that is **not** the real one. Used by `-anchor wrong`. Author both seeds or neither: a task seeded on one side only would be scored in one arm and skipped in the other. |

Example (`tasks/fizzbuzz/task.toml`):

```toml
prompt = "Create a file named fizzbuzz.py containing a function fizzbuzz(n) that returns the string 'Fizz' when n is divisible by 3, 'Buzz' when divisible by 5, 'FizzBuzz' when divisible by both 3 and 5, and otherwise the number as a string. Do not print anything at import time."
max_steps = 12
timeout_sec = 180
```

## verify.sh contract

`verify.sh` is the grader for a task:

- It is a `bash` script run with `set -e`; exit code `0` means the task passed.
- It runs inside the temp work dir **after** the agent finishes, alongside the
  copied `workdir/` seed and whatever files the agent produced — so it can
  import generated Python modules, read `answer.txt`/`result.txt`, etc.
- The harness copies `verify.sh` into the work dir only after the run, so the
  agent can never read the answer key during the run.
- Its stdout/stderr is streamed to the job log (stderr), not the report.

Examples: `compaction/verify.sh` normalizes `answer.txt` (strip whitespace,
lowercase) and compares it to the expected `aldermoor-verrin`;
`fizzbuzz/verify.sh` imports the generated module and asserts on
`fizzbuzz(3)`, `fizzbuzz(5)`, `fizzbuzz(15)`, `fizzbuzz(7)`.

Python graders must start with
`export PYTHONPYCACHEPREFIX="$(mktemp -d)"`: macOS system Python caches
bytecode centrally keyed by absolute path, so an agent edit that keeps a
file's size within the same mtime second would otherwise execute stale
bytecode while tracebacks display the new source.

## Running the e2e suite

Prerequisites: a `reasonix` binary (or `go run ./cmd/reasonix` …) with a
configured provider. The harness invokes the agent as
`reasonix run --auto --metrics  [--model NAME] [--max-steps N] [--profile delivery] [--ablate ARM] `
inside a temp copy of the task's `workdir/`; the `--auto` flag is deliberate so
unattended fixture writes are allowed.

```sh
# Run the committed suite, report to stdout
go run ./cmd/e2ebench

# Same suite with the delivery prompt profile
go run ./cmd/e2ebench -profile delivery

# Write the markdown report to a file and the raw results to JSON
go run ./cmd/e2ebench -out report.md -json report.json

# Grade a PR's diff (generates tests for the diff, grades with the repo's tests)
go run ./cmd/e2ebench -mode diff -base origin/main-v2 -repo . -attempts 3 -timeout 1800
```

The markdown report contains the solved count, cost/tokens per solved task,
median wall time, cache-hit rate, and a per-task table with failure class
(`solved`, `timeout`, `wrong_patch`, `no_metrics`, `skipped`, or the agent's
own outcome).

### Flags

| Flag | Default | Purpose |
| --- | --- | --- |
| `-mode` | `suite` | `suite` \| `diff` \| `swebench` \| `compare` \| `traj` (`diff` generates tests for the PR diff; `swebench` runs the official per-instance evaluation; `compare` renders KPI/Pareto readouts from 2+ `-json` reports; `traj` re-digests recorded trajectory files without spending tokens). |
| `-suite` | `benchmarks/e2e` | Suite root (must contain `tasks//`). |
| `-task` | *(all)* | Suite mode: run only these comma-separated task IDs (e.g. `-task fix-add-bug`); unknown IDs fail with the available list. |
| `-attempts` | `1` | Suite and diff modes: retry a task until an attempt passes, up to N; enables the `Pass@≤N` KPI, and TTCS charges a retried solve with its failed attempts' wall. |
| `-bin` | `reasonix` | Path to the reasonix binary. |
| `-model` | *(config default)* | Provider/model name. |
| `-profile` | `baseline` | Tool-surface/runtime tier: `baseline` \| `economy` \| `balanced` \| `delivery`. All but `baseline` append `--profile <tier>` to the agent invocation; `baseline` passes no flag (byte-identical legacy control, behaviorally `balanced`). Economy starts with the core tool set and pays `connect_tool_source` rounds plus prefix resets to grow it — the report's Tool surface line prices that trade. |
| `-ablate` | *(none)* | Ablation arm: comma-separated subsystems to switch off — `evidence`, `planner`, `subagent`, `retrieval`, `compaction`; `none` \| `all`. |
| `-out` | *(stdout)* | Write the markdown report here. |
| `-json` | *(none)* | Write the JSON report here (optional). |
| `-trajectories` | *(none)* | Suite mode: write one `<task-id>.trajectory.jsonl` per task into this directory (the agent's full event stream with timestamps — see `reasonix run --trajectory`). The report gains a time-attribution line (tools vs. model) and each JSON result a `trajectory` digest. |
| `-force-planner` | `false` | Suite mode: prefix each prompt with a plan-first directive so the two-model turn engages regardless of the planner gate. Use for the "with planner" arm of an A/B; results carry `plan_forced` so arms are only comparable with equal forcing. |
| `-anchor` | `blind` | Suite mode: which hypothesis the agent holds before it looks at anything — `blind` (none, the control) \| `correct` \| `wrong`. The seeded arms prefix each prompt with the task's authored seed and **skip** tasks that have none, so an unseeded control run never lands in a seeded denominator. Results carry `anchor`. See [Anchor resistance](#anchor-resistance). |
| `-cache` | `cold` | Suite mode: `cold` runs each task as a fresh session (the fair cross-agent comparison arm); `warm` primes the provider prefix cache with a one-step run in the same workdir first, measuring the long-lived-session steady state. Never mix arms in one report — compare them with `-mode compare cold.json warm.json`. |
| `-budget` | `800000` | Abort once total tokens cross this (`0` = no cap). Remaining tasks are reported as skipped. |
| `-meter` | *(off)* | Suite mode: route the benchmarked provider through the neutral measuring proxy, using this `config.toml` as the source. Spend is then counted at the request boundary instead of trusted from the harness. See [Neutral metering](#neutral-metering). |
| `-faults` | *(none)* | Suite mode: inject provider failures through the meter — absolute indices (`3:429`) and/or a cadence that scales with the run (`every:5:500`). Requires `-meter`. See [Fault recovery](#fault-recovery). |
| `-segments` | `1` | Suite mode: split each task into N resumed legs (`--continue` between them). The step budget is **divided**, never multiplied. See [Segmented runs](#segmented-runs). |
| `-steer` | *(none)* | Suite mode: deliver a user turn at a leg boundary, e.g. `"also handle empty input@2"`. Requires `-segments` to reach that leg. |

Diff-mode flags:

| Flag | Default | Purpose |
| --- | --- | --- |
| `-repo` | `.` | Repo root (diff mode). |
| `-base` | *(none)* | Base ref to diff the PR head against (diff mode). |
| `-test-cmd` | `go test` | Grader command run on the affected packages (diff mode). |
| `-max-steps` | `80` | Agent tool-call cap for the diff task. |
| `-timeout` | `1200` | Agent timeout in seconds (diff mode). |
| `-attempts` | `1` | Diff mode: retry up to N times until a run passes (stochastic agent). |

## Dataset retention

Keep every `-json` report and `-trajectories` directory from real runs: they
are the accumulating corpus — per-task contracts-to-be, full event
trajectories, checkpoint oracle verdicts, stop curves and phase traces — that
any future offline learning (routing, stop policies, budgets) would train
and evaluate on. The control plane stays deterministic and interpretable
until that corpus reaches a scale where learned policies can be judged
against the same oracles that produced it; nothing learned lands before it
beats the deterministic baseline on these numbers.

## A/B compare mode

Run the same suite twice and let the harness judge the trade:

```sh
go run ./cmd/e2ebench -force-planner -trajectories t-a -json with.json
go run ./cmd/e2ebench -ablate planner -trajectories t-b -json without.json
go run ./cmd/e2ebench -mode compare with.json without.json
```

Compare mode renders a per-solved delta table (solve rate, model requests,
planner requests, model rounds, tool calls, tokens, wall, cost), an overall
marginal-utility line (`accuracy +X.Xpp · wall/task +Y.Ys`), and — when tasks
carry `class` labels — a per-class breakdown, so a subsystem's uplift and
latency cost can be judged per task class instead of globally.

## SWE-bench Verified mode

`e2ebench` can also run the agent inside the official SWE-bench evaluation
images and hand the resulting patches to the official grader:

```sh
# Requires Docker, the `swebench` Python package, evaluation images, and a
# network/proxy setup that prevents the agent from reading upstream fixes.
go run ./cmd/e2ebench -mode swebench \
  -subset benchmarks/swebench/subset.json \
  -network reasonix-eval -proxy http://127.0.0.1:8080
```

SWE-bench mode accepts the `-model`, `-profile`, `-ablate`, `-permission`,
`-workers`, `-dataset`, `-run-id`, `-harness-python`, and `-keep-images` flags;
its report is produced by the official harness rather than the suite JSON
writer.

## Adding a new task

1. Create `benchmarks/e2e/tasks/<task-id>/`.
2. Write `task.toml` with `prompt`, `max_steps`, and `timeout_sec` (see
   [schema](#tasktoml-schema)).
3. If the task needs seed files, add them under `workdir/` (they are copied
   into the temp run dir; symlinks are skipped).
4. Write `verify.sh`: `set -e`, exit 0 iff the agent's artifacts are correct.
   Keep the expected answer out of the prompt and seed; the script runs in the
   work dir and may validate anything the agent produced.
5. Iterate on just that task with the single-task filter, then commit:

   ```sh
   go run ./cmd/e2ebench -task <task-id>
   ```

## context-maintenance-e2e

This harness measures what happens when a long session goes idle past the
provider's cache TTL and then resumes: it A/B-compares cold-restart miss tokens
with and without pruning, and checks that the agent re-reads a file behind a
prune placeholder instead of hallucinating.

It is hardcoded to the `deepseek-v4-flash` model at `https://api.deepseek.com`
and requires the `DEEPSEEK_API_KEY` environment variable.

```sh
export DEEPSEEK_API_KEY=...

# Seed both arms (pruned + control) with a large session and warm the cache
go run ./benchmarks/context-maintenance-e2e seed

# Wait past the provider's cache TTL, then resume: prune the "pruned" arm and
# compare cold-restart miss tokens
go run ./benchmarks/context-maintenance-e2e resume

# Run the comprehension trials (agent must re-read a pruned file and answer
# from it); exits non-zero unless every trial passes
go run ./benchmarks/context-maintenance-e2e comprehension
```

| Flag | Default | Purpose |
| --- | --- | --- |
| `-dir` | `benchmarks/context-maintenance-e2e/run` | State directory for `seed`/`resume` (sessions + `meta.json`, `resume-<ts>.json`). |
| `-trials` | `5` | Number of comprehension trials. |

## See also

- [`docs/CLI.md`](../docs/CLI.md) — the `reasonix run` flags the e2e harness
  passes through (`--auto`, `--metrics`, `--model`, `--max-steps`,
  `--profile`, `--ablate`).
- [`cmd/e2ebench/main.go`](../cmd/e2ebench/main.go) — suite runner and report
  renderer.

## memorybench

The memory-effectiveness suite. Each task seeds an isolated memory state root
(`tasks//memory/project|global/*.md`, production frontmatter) before the
run; `memory_markers` in task.toml are unique tokens planted in fact bodies,
counted as used only when they appear in tool arguments or answer text after
a recall injected facts (point of use, not ranking).

The core KPI is the paired counterfactual, not Recall@K:

```
e2ebench -suite benchmarks/memorybench -budget 0 -trajectories t-on  -json on.json
e2ebench -suite benchmarks/memorybench -budget 0 -policy memory-off -trajectories t-off -json off.json
e2ebench -mode compare on.json off.json     # Memory utility section
```

Utility delta = paired Pass(on) − Pass(off). Harmful attribution is paired,
never judged: the same task passed without memory and failed with it while
recall fired. Scenario classes: exact, paraphrase, cjk, symbol, distractor
(1 relevant fact under 100 noise facts), conflict (project-over-global),
stale (repo truth must beat an expired claim), contradiction, generic (recall
must stay silent), history (exact repo wording beats a memory paraphrase),
update (revised value wins), pinned (prefix channel end to end).

## CompactionBench

`benchmarks/compaction/` drives the real agent compaction path over a session
that grows one generation at a time. Each generation appends a round of work
and then folds, so generation N folds everything generations 1..N produced —
which is the growth that matters, because a fold re-derives its digest from the
whole canonical transcript rather than from the previous digest.

```bash
go run ./benchmarks/compaction -mode=cost                     # offline, no API key
go run ./benchmarks/compaction -mode=fidelity -gens=8          # needs DEEPSEEK_API_KEY
```

**Cost arm** (`-mode=cost`) is deterministic and needs no provider: a scripted
summarizer answers every call and refuses any input larger than the window, the
way a real provider does. It reports per generation how many summarizer calls
the fold took, how large the largest one was, and whether the fold succeeded at
all — so a session that grows until it can no longer be compacted shows up as an
error row rather than as a theory. `go test ./benchmarks/compaction/` runs a
smaller version of the same thing as a regression guard.

**Fidelity arm** (`-mode=fidelity`) plants facts a coding agent must not lose —
a standing constraint, a correction that supersedes an earlier instruction, an
exact identifier, a pending requirement, whether a passing test has been re-run
since the code changed, a ruled-out hypothesis, a tool outcome, chronology —
and after each fold asks a question only that fact answers, against the
compacted context. Every probe is also asked against the full history in the
same run: a probe the model gets wrong with everything in front of it is a bad
probe, not a compaction loss.

Probe answers are scored on whole words, and a wanted answer does not count if a
rejected one appears anywhere in the same reply — "yes, but it has not been
re-run since" is the shape a drifting digest produces, and it is not a pass.

### Fold arms

`-arm=full` (default) re-derives every digest from the canonical transcript, so
digests never chain. `-arm=incremental` folds the model-visible view instead,
feeding the previous digest back through the summarizer. The arms exist to price
that trade: run the cost arm for what chaining saves, and the fidelity arm for
what it costs.

```bash
go run ./benchmarks/compaction -mode=cost -arm=incremental
DEEPSEEK_API_KEY=… go run ./benchmarks/compaction -mode=fidelity -arm=incremental
```

---

## File: docs/ARCHITECTURE.md

# Reasonix Architecture

## Design philosophy

Reasonix is **opinionated, not general**. Every abstraction is justified by a
DeepSeek-specific behavior or economic property. If it's generic, we don't
ship it.

The product north star: **coding agent that stays cheap enough to leave on**.
A tool that quietly burns $200/month on a background project is one nobody
uses. Every subsystem below is answerable to that goal.

## The four pillars

### Pillar 1 — Cache-First Loop

**Problem.** DeepSeek bills cached input at ~10% of the miss rate. Automatic
prefix caching activates only when the *exact* byte prefix of the previous
request matches. Most agent loops reorder, rewrite, or inject fresh
timestamps each turn — cache hit rate in practice: <20%.

**Solution.** Partition the context into three regions:

```
┌─────────────────────────────────────────┐
│ IMMUTABLE PREFIX                        │ ← fixed for session
│   system + tool_specs + few_shots        │   cache hit candidate
├─────────────────────────────────────────┤
│ APPEND-ONLY LOG                         │ ← grows monotonically
│   [assistant₁][tool₁][assistant₂]...    │   preserves prefix of prior turns
├─────────────────────────────────────────┤
│ VOLATILE SCRATCH                        │ ← reset each turn
│   R1 thought, transient plan state      │   never sent upstream
└─────────────────────────────────────────┘
```

**Invariants:**
1. Prefix is computed once per session, hashed, and pinned.
2. Log entries are serialized in append order; no rewrites.
3. Scratch is distilled via Pillar 2 before any information from it is folded
   into the log.

**Metric.** `prompt_cache_hit_tokens / (hit + miss)` exposed per-turn and
aggregated per-session. Visible in the TUI's top-bar cache cell.

#### Parallel tool dispatch

Each tool declares `parallelSafe?: boolean` (default `false`). The loop
dispatcher groups consecutive parallel-safe calls into chunks and races
them via `Promise.allSettled`; the first non-parallel-safe call ends the
chunk and runs alone (serial barrier — read-after-write order
preserved). Tool-result yields and history append still land in declared
order regardless of which call settles first, so the model sees the
same shape it would under a fully serial dispatch.

| Env var | Default | Effect |
|---|---|---|
| `REASONIX_PARALLEL_MAX` | `3` (hard cap `16`) | Max chunk size. |
| `REASONIX_TOOL_DISPATCH=serial` | unset | Forces serial dispatch — escape hatch. |

Built-in opt-ins: read-only filesystem (`read_file`, `list_directory`,
`directory_tree`, `search_files`, `search_content`, `get_file_info`),
web (`web_search`, `web_fetch`), `recall_memory`, `semantic_search`,
isolated child loops (`run_skill`, `spawn_subagent`), in-memory job
queries (`job_output`, `list_jobs`). Mutating / side-effecting tools
stay default. MCP-bridged tools default `false` — third-party tools
opt in only when the server explicitly declares parallel safety.

### Pillar 2 — Tool-Call Repair

**Problem.** Empirical DeepSeek failure modes:
- Tool-call JSON emitted inside ``, missing from the final message.
- Arguments dropped when schema has >10 params or deeply nested objects.
- Same tool called repeatedly with identical args (call-storm).
- Truncated JSON due to `max_tokens` hit mid-structure.

**Solution.** Four passes:

1. **`flatten`** — schemas with >10 leaf params or depth >2 are auto-detected
   on `ToolRegistry.register()` and presented to the model in dot-notation
   form. `dispatch()` re-nests the args before calling the user's `fn`.
2. **`scavenge`** — regex + JSON parser sweeps `reasoning_content` for any tool
   call the model forgot to emit in `tool_calls`.
3. **`truncation`** — detect unbalanced JSON and repair by closing braces or
   requesting a continuation completion.
4. **`storm`** — identical `(tool, args)` tuple within a sliding window →
   suppress the call, inject a reflection turn.

### Pillar 3 — Cost Control *(v0.6)*

**Problem.** Coding agents that default to the frontier model (v4-pro, ~12×
flash cost) and accumulate full tool results in context are $150-$250/month
for active users. Most turns don't need frontier reasoning; most sessions
re-pay for tool results that were only useful once.

**Solution.** Four complementary mechanisms, none of which require manual
tuning in the common case:

#### 4.1 Tiered defaults (flash-first)

The three presets trade **model tier** and **reasoning effort**:

| Preset | Model | Effort | Cost |
|---|---|---|---:|
| `flash` | `v4-flash` | `max` | 1× |
| `auto` (default) | `v4-flash` → `v4-pro` on hard turns | `max` | 1–3× |
| `pro` | `v4-pro` | `max` | ~12× |

All auxiliary calls — `forceSummaryAfterIterLimit`, subagent spawns,
truncation repair retries — hard-code `v4-flash + effort=high` regardless
of the user's preset. There's no reason to pay pro rates for "paraphrase
these tool results into prose" or for an `explore` subagent's grep chain.

#### 4.2 Turn-end auto-compaction

Every tool result in the log exceeding `TURN_END_RESULT_CAP_TOKENS` (3000)
is shrunk to that cap when a turn ends. The model had the full text for
the turn that read it; subsequent turns see a compact summary and can
re-read if needed. One extra `read_file` call is vastly cheaper than
dragging 12KB through every future prompt.

A proactive 40% context-ratio threshold runs the same shrink pre-emptively
inside long multi-iter turns before the 80% emergency threshold fires.

#### 4.3 Model selection (`/model`)

Users switch between flash and pro via `/model flash` or `/model pro`
(persistent — applies to every turn until changed). Model can also be
set in `.reasonix/settings.json` under the `model` key. No one-shot
arming; no forgotten revert risk when switching is explicit and sticky.

> **History.** Pre-0.50.0, `/pro` offered single-turn arming — type `/pro`,
> the next turn ran on v4-pro then auto-disarmed. Removed in 0.50.0
> (#1657, #1630) when presets were collapsed into direct model selection.

#### 4.4 Model self-report escalation (`<<<NEEDS_PRO>>>`)

The model itself decides when a task exceeds its current tier. If a task
clearly needs stronger reasoning, the model emits a `<<<NEEDS_PRO>>>`
marker as the first line of its response. The system aborts the current
flash call and retries the turn on pro. Two forms:

- `<<<NEEDS_PRO>>>` — bare marker, no rationale.
- `<<<NEEDS_PRO: <reason>>>>` — includes a one-sentence rationale the
  user sees in a warning row.

On the pro tier, the marker is a no-op — pro is the top, so the contract
tells the model it can't escalate further. This is purely self-report:
there is no failure-counter threshold, no scavenge/storm counting, no
automatic escalation based on tool errors.

#### Cost transparency

Per-turn and session cost are colored in the StatsPanel:
- `turn $0.003` — green <$0.05, yellow $0.05–0.20, red ≥$0.20
- `session $0.12` — same scale ×10

## Module layout

```
/* Detailed source-code truncated for AI context efficiency. */
```

Files kept small by design: the largest module under `cli/ui/` is 2K
lines (App.tsx), every handler under `slash/handlers/` is ≤200 lines,
every hook under `cli/ui/` is ≤310 lines. Adding a new slash command
means editing one handler file and one registry line.

## Design evolution

- **v0.0.x** — Pillar 1 end-to-end, repair pipeline complete, Ink TUI scaffold.
- **v0.1** — τ-bench numbers published, streaming polish, transcript replay.
- **v0.3** — MCP client (stdio + SSE), session persistence.
- **v0.4.x** — `reasonix code` with SEARCH/REPLACE edits, review/auto
  gate, background jobs, hooks.
- **v0.5.x** — V4 model support, skills, memory, subagents, actionable
  error messages.
- **v0.6** —
  - **Cost control** (flash-first defaults, auto-compaction, `/pro` one-shot,
    failure-triggered escalation, cost badges).
  - `deepseek-chat` / `deepseek-reasoner` scheduled for deprecation —
    all user-facing surfaces updated to `v4-flash` / `v4-pro`.
  - Shared prompt fragments (`TUI_FORMATTING_RULES`, `NEGATIVE_CLAIM_RULE`).
  - UI refactor: App.tsx split into 6 hooks/components, slash.ts split
    into 13 per-topic modules.
- **v0.31** *(current)* — `branch` + `harvest` features removed entirely
  (the parallel-sample selector and Pillar 2 plan-state extractor); both
  rarely paid for themselves and bloated the slash surface.

## Explicit non-goals

- Multi-agent orchestration as a first-class concept (subagents are a
  cost-reduction mechanism, not a coordination primitive).
- RAG / vector retrieval.
- Support for non-DeepSeek backends (an OpenAI-compatible shim would
  work today via `--model` override, but is not tested).
- Web UI / SaaS.
- Automatic cost escalation without user-visible announcement. Every
  pro-tier model call is surfaced; silent escalation was considered
  and rejected.

---

## File: docs/CLI-REFERENCE.md

# Reasonix CLI Reference

Every shell subcommand, every TUI slash command, every keybinding. The in-app `/help` and `/keys` panels are the live source of truth — this page is the printable companion.

---

## Shell subcommands

Run `reasonix --help` (or any subcommand with `--help`) for the full flag list. Headline subcommands:

| Subcommand | What it does |
|---|---|
| `reasonix code [dir]` | Code-mode TUI — file edits, plan mode, edit-gate, project-scoped sessions |
| `reasonix chat` | Chat-only TUI — no filesystem access, no code mode |
| `reasonix run <task>` | Headless run — read prompt, execute, exit (CI-friendly) |
| `reasonix setup` | Interactive first-run config (API key, language, theme) |
| `reasonix sessions [name]` | List or open a saved session |
| `reasonix prune-sessions` | Drop sessions older than `--days N` |
| `reasonix replay ` | Re-render a JSONL transcript without calling the model |
| `reasonix diff <a> ` | Compare two transcripts (cost / cache / tokens) |
| `reasonix events <name>` | Tail the event log for a session |
| `reasonix stats [transcript]` | One-shot cost / cache breakdown |
| `reasonix doctor` | Health check — API reach, config, hooks, project |
| `reasonix commit` | `git add -A && git commit` with an LLM-written message |
| `reasonix mcp <list\|search\|install\|inspect\|browse>` | MCP server management |
| `reasonix index` | Build the local semantic index (Ollama or OpenAI-compatible embeddings) |
| `reasonix version` / `reasonix update` | Version info + upgrade hint |

### Notable runtime flags (chat / code)

| Flag | Effect |
|---|---|
| `--no-session` | Ephemeral run — nothing is persisted |
| `--session <name>` | Resume / pin to a named session |
| `--continue` | Resume the most recent session for this workspace |
| `--new` | Force a fresh session even if one exists |
| `--budget ` | Per-session USD cap — warns at 80%, refuses next turn at 100% |
| `--preset <auto\|flash\|pro>` | Model bundle (auto-escalation, locked flash, locked pro) |
| `--mcp <spec>` | Attach an MCP server for this run (repeatable) |
| `--no-config` | Ignore `~/.reasonix/config.json` for this run |
| `--no-dashboard` | Don't auto-start the embedded web dashboard |
| `--no-alt-screen` | Render to scrollback instead of the alt-screen buffer (preserves chat in shell history; legacy mode, can ghost on resize) |
| `--no-mouse` | Disable DECSET 1007 (alternate-scroll); wheel reverts to native terminal scroll |

---

## Slash commands

Type `/` mid-chat to open the picker. Aliases shown in parentheses. Code-mode-only commands marked **(code)**.

### Chat ops

| Command | What it does |
|---|---|
| `/help` (`/?`) | Show the full command reference inline |
| `/new` (`/reset`, `/clear`) | Start a fresh conversation (clear context + scrollback) |
| `/retry` | Truncate and resend your last message — fresh sample |
| `/compact` | Fold older turns into a summary (cache-safe). Auto-fires at 50% ctx; this is the manual trigger |
| `/stop` | Abort the current model turn (typed alternative to Esc) |
| `/copy` | Open vim/tmux-style copy mode — `j`/`k` navigate, `v` select, `y` yank to clipboard. The right answer for SSH / mosh / tmux where drag-select can't extend past the viewport |

### Setup

| Command | What it does |
|---|---|
| `/preset <auto\|flash\|pro>` | Switch model bundle. Bare opens picker |
| `/model ` | Switch DeepSeek model id. Bare opens picker |
| `/language <EN\|zh-CN>` (`/lang`) | Switch the runtime language |
| `/theme <name>` | Show or persist terminal theme. Bare opens picker |

### Info

| Command | What it does |
|---|---|
| `/status` | Current model, flags, context, session |
| `/cost [text]` | Bare → last turn's spend; with text → estimate cost of sending it next |
| `/context` | Context-window breakdown (system / tools / log / input) |
| `/stats` | Cross-session cost dashboard (today / week / month / all-time) |
| `/doctor` | Health check (api / config / api-reach / index / hooks / project) |
| `/keys` | Keyboard + mouse + copy/paste reference |
| `/feedback` | Open a GitHub issue with diagnostic info copied to clipboard |

### Extend

| Command | What it does |
|---|---|
| `/mcp` | Open the MCP hub (live + marketplace tabs) |
| `/resource [uri]` | Browse / read MCP resources |
| `/prompt [name]` | Browse / fetch MCP prompts |
| `/memory [list\|show\|forget\|clear]` | Manage pinned memory (REASONIX.md + `~/.reasonix/memory`) |
| `/skill [list\|show\|new\|<name>]` | List / run / scaffold user skills |

### Session

| Command | What it does |
|---|---|
| `/sessions` | List saved sessions (current marked with ▸) |

### Code mode

| Command | What it does |
|---|---|
| `/init [force]` | Scan project, synthesize a baseline `REASONIX.md` |
| `/apply [N\|N,M\|N-M]` | Commit pending edit blocks to disk (subset selection supported) |
| `/discard [N\|N,M\|N-M]` | Drop pending edits without writing |
| `/walk` | Step through pending edits one block at a time (git-add-p style) |
| `/undo` | Roll back the last applied edit batch |
| `/history` | List every edit batch this session |
| `/show [id]` | Dump a stored edit diff |
| `/commit "msg"` | `git add -A && git commit -m ...` |
| `/mode <review\|auto\|yolo>` | Edit-gate mode. Shift+Tab cycles |
| `/plan [on\|off]` | Toggle read-only plan mode. Submitted plans initially show a compact summary; press `Ctrl+P` in the plan confirmation modal to expand/collapse full details |
| `/checkpoint [name\|list\|forget]` | Snapshot every file the session has touched |
| `/restore <name\|id>` | Roll back to a named checkpoint |
| `/cwd ` (`/sandbox`) | Switch the workspace root mid-session |

### Jobs (code mode)

| Command | What it does |
|---|---|
| `/jobs` | List background jobs |
| `/kill ` | Stop a background job (SIGTERM → SIGKILL) |
| `/logs  [lines]` | Tail a job's output (default 80 lines) |

### Advanced

| Command | What it does |
|---|---|
| `/pro [off]` | Arm v4-pro for the NEXT turn only |
| `/budget [usd\|off]` | Session USD cap |
| `/search-engine ` (`/se`) | Switch web search backend |
| `/hooks [reload]` | List / reload hooks |
| `/permissions [list\|add\|remove\|clear]` | Edit shell allowlist |
| `/dashboard [stop]` | Launch / stop the embedded web dashboard |
| `/loop  ` | Auto-resubmit a prompt every interval |
| `/plans` | List active + archived plans |
| `/replay [N]` | Load an archived plan as a read-only Time Travel snapshot |
| `/update` | Show current vs latest version |
| `/exit` (`/quit`, `/q`) | Quit the TUI |

---

## Keyboard

| Key | What it does |
|---|---|
| `Enter` | Submit the prompt |
| `Shift+Enter` | Insert a newline in the prompt |
| `↑` / `↓` | Scroll chat history (mouse wheel routes here too) |
| `Ctrl+P` / `Ctrl+N` | Previous / next prompt history · cursor up / down in a multi-line draft. In a submitted-plan confirmation modal, `Ctrl+P` expands/collapses full plan details |
| `Ctrl+A` / `Ctrl+E` | Jump to start / end of the current line |
| `Ctrl+W` | Delete the word before the cursor |
| `Ctrl+U` | Clear the entire prompt buffer |
| `Tab` | Complete @-mention · drill folder · accept slash command |
| `Shift+Tab` | Edit-gate: toggle review ↔ AUTO mode |
| `Esc` | Dismiss picker · abort the running model turn |
| `Ctrl+C` | Abort the running model turn (NOT copy — see clipboard) |
| `PgUp` / `PgDn` | Scroll chat history a page at a time. While plan details are expanded, scroll the bounded detail window |
| `End` | Jump chat to the most recent line |

### Edit-gate (code mode)

| Key | What it does |
|---|---|
| `y` / `n` | Accept / drop pending edits in the review modal |
| `Shift+Tab` | Toggle review ↔ AUTO (persisted across sessions) |
| `u` | Undo the last auto-applied batch (within the 5s banner) |

---

## Mouse

| Action | What it does |
|---|---|
| Wheel | Scrolls chat history (works on web / cloud / SSH terminals too) |
| Drag | Selects text natively — no modifier needed |
| Right-click | Terminal-native (e.g. paste menu on Windows Terminal) |

Reasonix sets DECSET 1007 (alternate-scroll) only — wheel events translate to ↑/↓ keypresses for the app, but native click/drag selection is left untouched. Pass `--no-mouse` to opt out entirely.

---

## Copy / paste

The default path is **terminal-native**. Drag to select, then use your terminal's normal copy keys:

| Action | How |
|---|---|
| Select text | Drag — terminal-native (no modifier) |
| Copy | `Ctrl+Shift+C` (Win / Linux) · `Cmd+C` (macOS) — or auto-copy-on-select if your terminal does it |
| Paste | `Ctrl+V` or `Ctrl+Shift+V` (Win / Linux) · `Cmd+V` (macOS) |
| Multi-line paste | Bracketed paste — pastes stay one block, no auto-submit on intermediate newlines |

### When drag-select doesn't work

In SSH / mosh / tmux, the alt-screen buffer prevents the terminal from extending the selection past the visible viewport — there is no scrollback above the alt-screen to drag into. Two fixes:

1. **`/copy`** — open vim/tmux-style copy mode in-app. Snapshots the current chat to a navigable buffer; `y` yanks to clipboard via OSC 52 (with a temp-file fallback for terminals that don't support it).
2. **`--no-alt-screen`** — render to shell scrollback instead. Drag-select then works terminal-natively (the chat content is real lines in the scrollback above your cursor). Trade-off: redraw can ghost on resize.

### `/copy` — copy mode keys

| Key | What it does |
|---|---|
| `j` / `↓` | Cursor down one line |
| `k` / `↑` | Cursor up one line |
| `PgUp` / `PgDn` | Page up / down |
| `g` / `G` | Jump to top / bottom |
| `v` | Start (or cancel) selection at the cursor |
| `y` / `Enter` | Yank selection to clipboard, exit |
| `q` / `Esc` | Quit without yanking |

`y` with no active selection yanks just the current line. The yank goes through OSC 52 first (works through SSH, mosh, tmux with `set -g set-clipboard on`); content larger than 75 KB falls back to a temp file whose path is printed on exit.

---

## Where this lives

In-app, `/keys` and `/help` print the same content the model knows about. This page mirrors them so the reference is greppable from the repo / website.

---

## File: docs/qq-connect.md

# QQ channel setup

Reasonix can attach QQ to an existing `chat` or `code` session as a remote channel. QQ is not a third runtime mode.

Once connected, QQ can:

- send normal user messages into the active session
- receive follow-up assistant replies
- continue confirmation, choice, checkpoint, and plan-style follow-up interactions

## Before you start

Prepare these first:

- a recent Reasonix release that already includes QQ support
- a QQ account that has completed real-name verification
- a QQ bot `App ID` and `App Secret` from QQ Open Platform

QQ Open Platform entry:

- [QQ Open Platform](https://q.qq.com/qqbot/openclaw/login.html)

Important:

- save the `App Secret` when it is shown
- depending on your bot, you may need `sandbox` or `prod`

## Get your QQ bot credentials

The exact QQ Open Platform UI may change, but the flow is usually:

1. Open [QQ Open Platform](https://q.qq.com/qqbot/openclaw/login.html) and sign in.
2. Create a QQ bot.
3. Open the bot's developer settings.
4. Copy the `App ID`.
5. Reveal and save the `App Secret`.

## Connect from the CLI

Start a session first:

~~~bash
reasonix code
# or
reasonix chat
~~~

Then run:

~~~text
/qq connect
~~~

First-time behavior:

1. Reasonix asks for the QQ `App ID` in the current TUI.
2. Then it asks for the `App Secret`.
3. Enter `/cancel` at either step to abort.

The prompts and `/qq` status messages follow the current CLI language.

If credentials are already saved, `/qq connect` reuses them directly.

You can also pass credentials inline:

~~~text
/qq connect <appId> <appSecret> [sandbox|prod]
~~~

Other QQ commands:

- `/qq status`
- `/qq disconnect`

After the first successful connection, later `chat` and `code` sessions auto-start the QQ channel while it stays enabled.

## Desktop quick start

If you use the desktop client:

1. Open `Settings -> General -> QQ Channel`.
2. Click `Configure`.
3. Enter `App ID`, `App Secret`, and the correct QQ environment.
4. Click `Save and connect`.
5. Send a message from QQ and check that it appears in the current desktop transcript.
6. Wait for the desktop reply to route back to QQ.

The desktop app uses the same underlying QQ config as the CLI, but the runtime is attached to the current active desktop tab.

That means:

- QQ messages enter the current active tab
- replies from that tab route back to QQ
- if you switch tabs, later QQ messages follow the new active tab

## Typical usage

1. Start `reasonix code` or `reasonix chat`.
2. Connect QQ once.
3. Send a message from QQ.
4. Let the local Reasonix session keep running.
5. Continue replies, approvals, and follow-up interactions from QQ when needed.

QQ extends the current session. It does not replace `chat` or `code`.

## Troubleshooting

### `/qq connect` fails on first setup

Check these first:

- `App ID` is correct
- `App Secret` is correct
- the QQ bot is enabled in QQ Open Platform
- you selected the right environment: `sandbox` or `prod`

If needed, reconnect with explicit arguments:

~~~text
/qq connect <appId> <appSecret> [sandbox|prod]
~~~

### QQ receives the message, but no reply comes back

Check that the local Reasonix session is still running and the channel is still connected:

~~~text
/qq status
~~~

### Desktop shows QQ configured, but no message round-tripping happens

First confirm you are using a desktop build that already includes desktop QQ runtime support.

Then check:

- the status in `Settings -> General -> QQ Channel`
- that the current active desktop tab is the one you expect QQ to drive
- that the local desktop session is still running

### `/qq` commands do not exist in your installed package

Your installed npm version is too old. Upgrade to a release that already includes QQ support, or use the current repository `main` branch.

---

## File: docs/qq-connect.zh-CN.md

# QQ 连接指南

Reasonix 可以把现有的 `chat`、`code` 或桌面端会话延伸到 QQ 上，作为远程通道使用。QQ 扩展的是当前会话，不是独立的新运行模式。

连接成功后，QQ 可以：

- 把普通消息送进当前会话
- 接收后续助手回复
- 继续确认、选择、checkpoint、plan 这类二次交互

## 开始前先准备

请先确认：

- 使用的是已经包含 QQ 支持的较新 Reasonix 版本
- QQ 账号已经完成实名认证
- 已经从 QQ 开放平台拿到机器人 `App ID` 和 `App Secret`

QQ 开放平台入口：

- [QQ 开放平台](https://q.qq.com/qqbot/openclaw/login.html)

注意：

- `App Secret` 显示时就要保存好
- 你的机器人环境可能需要选择 `sandbox` 或 `prod`

## 获取 QQ 机器人凭据

QQ 开放平台界面可能会变化，但通常流程是：

1. 打开 [QQ 开放平台](https://q.qq.com/qqbot/openclaw/login.html) 并登录
2. 创建 QQ 机器人
3. 打开机器人的开发设置
4. 复制 `App ID`
5. 查看并保存 `App Secret`

## 在 CLI 里连接

先启动一个会话：

~~~bash
reasonix code
# 或 reasonix chat
~~~

然后运行：

~~~text
/qq connect
~~~

首次连接时会这样引导：

1. 先在当前 TUI 里提示你输入 `App ID`
2. 再提示你输入 `App Secret`
3. 任一步输入 `/cancel` 都可以取消

这些提示和 `/qq` 结果会跟随当前 CLI 语言切换。
如果本地已经保存过凭据，`/qq connect` 会直接复用，不会重复询问。

也可以直接一次性传参：

~~~text
/qq connect <appId> <appSecret> [sandbox|prod]
~~~

其他相关命令：

- `/qq connect`
- `/qq status`
- `/qq disconnect`

第一次连接成功后，只要 QQ 保持启用，后续 `chat` 和 `code` 会话都会自动启动 QQ 通道。

## 桌面端快速上手

如果你使用桌面客户端：

1. 打开 `设置 -> 通用 -> QQ通道`
2. 点击 `配置`
3. 填入 `App ID` 和 `App Secret`
4. 选择正确的 QQ 环境：测试用 `沙箱`，正式机器人选 `正式`
5. 点击 `保存并连接`
6. 从 QQ 给机器人发一条消息，确认这条消息会出现在当前桌面会话记录里
7. 等待助手回复，确认回复会回到 QQ

桌面端和 CLI 复用同一份 QQ 配置。

### 当前活动标签页绑定

桌面端会把 QQ 绑定到**当前活动标签页**：

- 来自 QQ 的新消息会进入当前活动标签页
- 该标签页中的助手回复会回到同一个 QQ 通道
- 如果你切换到另一个标签页，后续 QQ 消息会进入新的当前标签页

## 典型使用方式

1. 启动 `reasonix code` 或 `reasonix chat`
2. 完成一次 QQ 连接
3. 从 QQ 发一条消息
4. 本地 Reasonix 会话继续运行
5. 需要时直接在 QQ 里继续回复、确认或选择

QQ 只是扩展当前会话，不替代 `chat` 或 `code`。

## 排障

### 首次 `/qq connect` 失败

优先检查：

- `App ID` 是否正确
- `App Secret` 是否正确
- QQ 开放平台里的机器人是否已启用
- 当前环境是否选对了：`sandbox` 或 `prod`

必要时可以直接显式传参重试：

~~~text
/qq connect <appId> <appSecret> [sandbox|prod]
~~~

### 桌面端里已经配置 QQ，但没有消息往返

优先检查：

- 使用的桌面端版本是否已经包含桌面 QQ runtime 支持
- `设置 -> 通用 -> QQ通道` 里当前状态是否正常
- 当前活动标签页是否仍然停留在你希望接收 QQ 消息的会话上
- 本地桌面会话是否仍然在运行

### QQ 能收到消息，但没有后续回复

先确认本地 Reasonix 会话还在运行，而且 QQ 通道仍然在线：

~~~text
/qq status
~~~

### 已安装的 npm 版本里没有 `/qq` 命令

说明本地包版本太旧。请升级到已经包含 QQ 支持的发行版，或者直接使用仓库最新 `main` 分支。

---

## File: packages/dsnix/README.md

# dsnix

Short alias for [`reasonix`](https://www.npmjs.com/package/reasonix) — the DeepSeek-native coding agent.

This package is a thin shim. Installing or running `dsnix` resolves to the same `reasonix` CLI, just under a shorter command name.

## Use

```bash
# Global install
npm install dsnix
dsnix code my-project

# One-shot via npx
npx dsnix@latest code my-project
```

Equivalent to:

```bash
npx reasonix@latest code my-project
```

## Why a separate package?

`reasonix` is the canonical package; `dsnix` exists purely so users can type a shorter command and run `npx dsnix@latest` without typing nine letters. Version numbers track `reasonix` 1-to-1.

For docs, config, slash commands, and everything else, see the [main Reasonix README](https://github.com/esengine/DeepSeek-Reasonix#readme).

---

## File: src/mcp/README.md

# MCP client (v0.3 foundation)

Minimal [Model Context Protocol](https://spec.modelcontextprotocol.io/)
client, hand-rolled in TypeScript. Lets Reasonix consume tools from any
MCP server (filesystem, github, slack, puppeteer, …) while applying the
Cache-First Loop and tool-call repair to the whole thing automatically.

## Design choice: roll-our-own, not @modelcontextprotocol/sdk

Same reasoning that drove `client.ts` (DeepSeek) rather than `openai`:

- **Zero runtime deps** for this module. Consistent with Reasonix's
  policy of owning the wire format where it matters.
- **Surface tuning**: we only implement what Reasonix actually uses —
  initialize + tools/list + tools/call. Resources, prompts, sampling,
  and progress notifications are deferred.
- **Insulation** from SDK breaking changes. The spec is more stable
  than any single SDK release.

Swappable if needed: `McpClient` depends on the `McpTransport` interface,
so the day we do want the official SDK's transport layer we can adapt
it and keep everything else.

## What's shipped here

```
src/mcp/
├── types.ts      JSON-RPC 2.0 + MCP-specific message types
├── stdio.ts      McpTransport interface + StdioTransport (spawn child)
├── sse.ts        SseTransport (HTTP+SSE for remote/hosted servers)
├── spec.ts       parseMcpSpec — parses --mcp CLI arg into transport-tagged spec
├── catalog.ts    curated list of popular official MCP servers
├── client.ts     McpClient: initialize / listTools / callTool
├── registry.ts   bridgeMcpTools: MCP → ToolRegistry
└── README.md     (this file)

tests/mcp.test.ts — in-process fake transport, no child processes
tests/mcp-sse.test.ts — in-process http.Server fake for SSE
```

## What's NOT here (yet)

| feature | status | note |
|---|---|---|
| CLI wiring (`reasonix chat --mcp <cmd>`) | ✅ shipped | see Usage below |
| Bundled demo server | ✅ shipped | `examples/mcp-server-demo.ts`, exposes echo/add/get_time |
| Real-subprocess integration test | ✅ shipped | `tests/mcp-integration.test.ts` |
| Resources / `resources/list` / `resources/read` | deferred | Reasonix doesn't surface resources today |
| Prompts / `prompts/list` | deferred | ditto |
| Progress notifications | deferred | long-running tool support comes with the CLI work |
| Streaming results | deferred | current shape returns one CallToolResult per call |
| SSE transport | ✅ shipped | `src/mcp/sse.ts` — pass `http(s)://…` to `--mcp` |
| Streamable HTTP (2025-03-26 spec) | deferred | waiting for a real server to validate against |
| MCP server that Reasonix exposes | never | out of scope — Reasonix is a client |

## Usage (CLI)

`--mcp` is repeatable — attach one or many MCP servers; their tools become
first-class citizens of the loop.

```bash
# Single server, anonymous (tools use native names):
reasonix chat --mcp "node --import tsx examples/mcp-server-demo.ts"

# Official filesystem server:
reasonix chat --mcp "npx -y @modelcontextprotocol/server-filesystem /tmp/safe-dir"

# Multiple servers, each namespaced. Syntax: "name=command args..."
# Tools land in a shared registry as fs_read_file, demo_add, etc.
reasonix chat \
  --mcp "fs=npx -y @modelcontextprotocol/server-filesystem /tmp/safe" \
  --mcp "demo=node --import tsx examples/mcp-server-demo.ts"

# Global prefix (only honored when there's ONE anonymous server):
reasonix chat \
  --mcp "npx -y @modelcontextprotocol/server-filesystem /tmp" \
  --mcp-prefix fs_

# Same flag works with one-shot run:
reasonix run "list files in /tmp/safe-dir" \
  --mcp "npx -y @modelcontextprotocol/server-filesystem /tmp/safe-dir"
```

Each spec is shell-split (spaces separate args; use quotes for paths with
spaces). Windows-friendly: backslashes pass through literally outside
quotes, so `C:\path\to\dir` works. Tools get folded into the
`ImmutablePrefix` for the model, and every call goes through Reasonix's
Cache-First loop + tool-call repair (scavenge / flatten / storm)
automatically.

## Usage (library)

```ts
import {
  McpClient,
  StdioTransport,
  bridgeMcpTools,
  CacheFirstLoop,
  DeepSeekClient,
  ImmutablePrefix,
} from "reasonix";

// 1. Spawn + connect to an MCP server
const transport = new StdioTransport({
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/safe-dir"],
});
const mcp = new McpClient({ transport });
await mcp.initialize();

// 2. Bridge its tools into a Reasonix ToolRegistry
const { registry } = await bridgeMcpTools(mcp, { namePrefix: "fs_" });

// 3. Use them with the Cache-First Loop — same as any native tool
const client = new DeepSeekClient();
const loop = new CacheFirstLoop({
  client,
  prefix: new ImmutablePrefix({
    system: "You can use the filesystem tools to help the user.",
    toolSpecs: registry.specs(),
  }),
  tools: registry,
});

for await (const ev of loop.step("List the files in /tmp/safe-dir.")) {
  if (ev.role === "assistant_final") console.log(ev.content);
}

// 4. Clean up
await mcp.close();
```

The payoff: the filesystem server's tools now inherit Reasonix's
cache-first prefix stability + repair (schema flatten, tool-call
scavenge, call-storm break) without the MCP server knowing anything
about it.

## Wire protocol notes (stdio)

- **Framing**: newline-delimited JSON. One JSON-RPC message per line,
  UTF-8, no Content-Length header (that's LSP, not MCP stdio).
- **Stderr**: forwarded to the parent's stderr. Servers often print
  startup banners there; that's fine.
- **Shutdown**: `close()` calls `child.stdin.end()` then SIGTERM if the
  process hasn't exited.
- **Malformed lines**: dropped silently. Some servers emit non-JSON
  during startup; logging every dropped line would be noise.
- **Debugging dropped lines**: set `REASONIX_DEBUG_MCP=1` to print each
  dropped malformed line to stderr, prefixed with
  `[mcp-stdio] dropped malformed line:`. Useful when an MCP server
  ships truncated or corrupted frames and tool calls come back empty.