## File: README.md
---
> [!IMPORTANT]
> **Receipt-Driven Development (RDD) is the supported stable path, and it is opt-in.** `v2.2.0` was the historical release where that path became supported after RDD began in `v1.47.0`: small work stays direct, broader implementation is delegated, SDD stays optional, and once RDD is enabled every route converges on structural proof, bounded review, an exact receipt, and delivery authorization. RDD is off until you enable it with `gentle-ai review mode enable --scope global`.
>
> The current stable release is [`v2.3.0`](https://github.com/Gentleman-Programming/gentle-ai/releases/tag/v2.3.0). `@latest` is the stable channel:
>
> ```bash
> go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@latest
> ```
>
> To opt into the current prerelease, [`v2.4.0-rc.1`](https://github.com/Gentleman-Programming/gentle-ai/releases/tag/v2.4.0-rc.1), install its exact tag:
>
> ```bash
> go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.4.0-rc.1
> ```
>
> Use `@main` only for unreleased development changes. See the [full RDD version policy](docs/quickstart.md#version-policy).
>
> Note the `/v2` suffix: Go requires it for major version 2 and above. Releases before `v2.0.0` use the unsuffixed import path.
## What It Does
Gentle-AI is NOT an AI agent installer. It adapts the agent runtime(s) already on your machine; it never installs one for you. If a selected agent isn't detected, Gentle-AI refuses and names the exact command you'd run yourself instead. It is an **ecosystem configurator** that equips the AI coding agent(s) you already use with persistent memory, Spec-Driven Development (SDD), curated skills, MCP servers, model routing, a teaching-oriented persona, and bounded native review.
**Before**: "I installed Claude Code / OpenCode / Cursor, but it's just a chatbot that writes code."
**After**: Your agent now has memory, skills, workflow, MCP tools, and a persona that actually teaches you.
### Supported Agent Integrations
| Agent | Delegation Model | Key Feature |
| ------------------- | :------------------------------: | --------------------------------------------------------------- |
| **Claude Code** | Full (Task tool) | Sub-agents, output styles |
| **OpenCode** | Full (multi-mode overlay) | Per-phase model routing |
| **Kilo Code** | Full (multi-mode overlay) | OpenCode-compatible config in `~/.config/kilo` |
| **Gemini CLI** | Full (experimental) | Custom agents in `~/.gemini/agents/` |
| **Cursor** | Full (native subagents) | 10 SDD agents in `~/.cursor/agents/` |
| **VS Code Copilot** | Full (runSubagent) | Parallel execution |
| **Codex** | Solo-agent | CLI-native, TOML config |
| **Windsurf** | Solo-agent | Plan Mode, Code Mode, native workflows |
| **Antigravity** | Solo-agent + Mission Control | Built-in Browser/Terminal sub-agents |
| **Kimi Code** | Full (native custom agents) | Modular prompt templates in `~/.kimi` |
| **Kiro IDE** | Full (native subagents) | Native `~/.kiro/agents/` + steering orchestration |
| **Qwen Code** | Full (native sub-agents) | Slash commands, `~/.qwen/commands/`, `auto_edit` mode |
| **OpenClaw** | Solo-agent | Workspace-first `AGENTS.md` / `SOUL.md` with global MCP config |
| **Trae** | Solo-agent | Desktop app by ByteDance; `~/.trae/skills/` + OS-specific rules |
| **Pi** | Full (package-managed subagents) | First-class `gentle-pi` harness with Pi-native persona/models, SDD, and Engram memory |
| **Hermes** | Detect-only | YAML MCP config, SOUL.md persona; install manually first |
> **Pi is package-managed, not just configured.** Selecting Pi installs the first-class [`gentle-pi`](docs/pi.md) harness, which owns Pi-native persona and model controls, SDD assets, chains, and memory wiring.
> **Note**: This project supersedes [Agent Teams Lite](https://github.com/Gentleman-Programming/agent-teams-lite) (now archived). Everything ATL provided is included here with better installation, automatic updates, and persistent memory.
### Organic Routing and Review Boundaries
Every configured agent receives the same outcome-first routing, even when the optional SDD component is not selected. Ask for the outcome; the agent uses exactly one implementation route and reviews the candidate only after implementation.
| Situation | Expected behavior |
| --- | --- |
| Understanding needs 1-3 files, or one mechanical file change is already understood | Keep the bounded action direct and inline. |
| Understanding needs 4+ files, reading prepares a write, broad research is needed, or a writer changes 2+ non-trivial files | Delegate the narrow exploration or one focused writer without creating SDD state. |
| Durable proposal, spec, design, and task artifacts would materially reduce substantial ambiguity | Offer optional SDD; select it only after an explicit request or an accepted proposal. |
| A candidate is ready for review | Freeze the exact bytes and derive review effort from evidence, never size alone. Interactive starts ask once per clone before reviewer work; non-interactive tier-1/tier-2 starts proceed without prompting and report how to disable review mode. |
| Commit, push, PR, or release | Validate the same content-bound receipt at the applicable delivery gate; never silently reopen review or create another budget. |
| Scope changes or an operation is interrupted | Use provider-owned status, recovery, and reconciliation; do not infer authority or replay safety from narration. |
Implementation routing does not decide review strength, and per-action test, build, install, or review workers do not change the selected route. Native commands own repository identity, candidate scope, lifecycle transitions, receipts, and safe continuations. See [Organic Implementation Routing](docs/trigger-rules.md), the [Organic RDD architecture](docs/architecture/organic-rdd.md), and the [review authority threat model](docs/review-authority-threat-model.md).
---
## Quick Start
### Install (recommended)
> [!NOTE]
> `gentle-ai install` requires Node.js 18+ and npm on every platform (it warns if either is missing). See [Prerequisites](docs/quickstart.md#prerequisites) for your distro's install hint.
**macOS / Linux**
```bash
curl -fsSL https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.sh | bash
```
**Windows (PowerShell)**
```powershell
go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@latest
```
> [!WARNING]
> Windows source builds and CI/runtime tests remain supported, but official Windows binary distribution and Scoop are temporarily unavailable. Windows installation and upgrades require Go 1.25.10+ and fail closed to source-install guidance; they never download an unsigned Gentle AI executable or execute a remote update script.
> [!IMPORTANT]
> After replacing or upgrading the `gentle-ai` binary, run `gentle-ai sync` to refresh its managed assets. See the [sync and upgrade reference](docs/usage.md#sync).
### Configure project context
Once your agents are configured, open your AI agent in a project and run these two commands to register the project context:
| Command | What it does | When to re-run |
| ---------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `/sdd-init` | Detects stack, testing capabilities, activates Strict TDD Mode if available | When your project adds/removes test frameworks, or first time in a new project |
| `gentle-ai skill-registry refresh` | Scans installed skills and project conventions, builds the registry | After installing/removing skills, or first time in a new project |
These are **not required** for basic usage. The SDD orchestrator runs `/sdd-init` automatically if it detects no context. Startup hooks normally keep the skill registry fresh for agents that support hooks, including Codex, Claude Code, OpenCode, and Pi through `gentle-pi`. If you start Pi with `pi -ns`, startup skill loading/hooks are skipped, so run the registry refresh manually when you need updated project rules.
Run `gentle-ai doctor` at any time for a read-only health check of your ecosystem (tool binaries, `state.json`, Engram reachability, disk space).
**Alternative install and scope options**
**Homebrew (macOS / Linux)**
```bash
brew tap Gentleman-Programming/homebrew-tap
brew trust --formula gentleman-programming/tap/gentle-ai # one-time, for Homebrew tap trust
brew install gentle-ai
```
**Go install: stable channel (any platform with Go 1.25.10+)**
```bash
go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@latest
```
Note the `/v2` in the module path: Go requires it for major version 2 and
above. Releases before `v2.0.0` use the unsuffixed path.
**Scoop (Windows)** — temporarily unavailable while official Windows binary distribution is held for public-trust Authenticode signing. Use the Windows `go install` command above.
By default, `gentle-ai install` writes agent-scoped files to each selected agent's global config directory. To keep the Gentleman stack isolated to one project, run:
```bash
gentle-ai install --scope=workspace
```
Workspace scope applies to selected agents for agent-scoped files such as system prompts, skills, SDD agents, and persona files. Global-only integrations remain global by design.
**Unreleased development channel (`main`)** — use only to test changes that are not part of a release. The managed beta installer tracks `main`; it requires Go 1.25.10+:
```bash
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.sh | bash -s -- --channel beta
# Windows (PowerShell)
$env:GENTLE_AI_CHANNEL="beta"; go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@main
```
### RDD version policy
Receipt-Driven Development (RDD) started in `gentle-ai` `v1.47.0` on 2026-07-10, with the first bounded native review transactions, and became the supported stable path in `v2.2.0`. Those are historical milestones; the negotiated public review contract was published in `v2.1.6`.
The current stable release is [`v2.3.0`](https://github.com/Gentleman-Programming/gentle-ai/releases/tag/v2.3.0). The current prerelease is [`v2.4.0-rc.1`](https://github.com/Gentleman-Programming/gentle-ai/releases/tag/v2.4.0-rc.1). `main` is unreleased development.
**Stable channel (`@latest`, currently `v2.3.0`)**
```bash
go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@latest
gentle-ai version
```
**Prerelease channel (`v2.4.0-rc.1`)**
```bash
go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.4.0-rc.1
gentle-ai version
```
**Unreleased development (`main`)**
```bash
go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@main
gentle-ai version
```
The managed installer tracks the channel's latest version and does not accept an arbitrary release pin. Use `go install` when reproducibility requires an exact version.
---
## Core Workflow
1. **Install and configure.** Run the installer, select the agents and components you want, then open your agent in a project.
2. **Use the smallest implementation route.** Keep bounded work direct, delegate actions that need fresh context, and use SDD only after an explicit request or an accepted proposal. SDD artifacts can live in **Engram** for cross-session memory, **OpenSpec** for versioned files, or **hybrid** for both.
3. **Build with discipline.** `/sdd-init` detects project testing capabilities; when Strict TDD is active, SDD apply works test-first. SDD verify audits RED/GREEN evidence and runs verification. Agents that support delegation use focused subagents instead of one growing conversation.
4. **Review one candidate.** After implementation, bounded native review freezes the candidate and issues one content-bound receipt. Commit, push, and PR validate that same receipt. Releases validate native authority and its receipt, unless the protected-main fast path has the exact tag/current `origin/main` SHA, exact-SHA successful CI, a remote-head recheck, and no fresh risk.
> **Trust what the system can derive, not agent narration.** [Chapter 21 — Verifiable Trust](https://the-amazing-gentleman-programming-book.vercel.app/en/book/Chapter21_Verifiable-Trust) explains the mental model: agents assess the candidate; native authority and delivery gates independently derive what may be trusted.
5. **Upgrade, then sync.** Refresh the binary and the managed agent assets together:
```bash
gentle-ai upgrade
gentle-ai sync
```
### The flow at a glance
Once you enable it, both implementation routes converge on RDD: a bounded native review freezes the candidate and issues the one receipt that every delivery gate validates — review is never reopened for unchanged content. RDD is opt-in, so with it off both routes deliver under ordinary repository policy instead.
**Organic route (no SDD)** — the agent picks the smallest useful route and RDD enters at the end, over the frozen candidate:
```mermaid
flowchart TD
A["User requests a change(Claude Code · OpenCode · Codex...)"] --> B{"Implementationroute"}
B -->|"decide/verify1–3 files"| C["Direct inline"]
B -->|"4+ file explorationor 2+ non-trivial writes"| D["Delegated direct(one bounded worker)"]
C --> E["Implementation + tests"]
D --> E
E --> F{"RDD enabled?(user-owned, opt-in)"}
F -->|"off (default)"| Z["Ordinary deliveryreports disabled/unmanaged"]
F -->|"on (explicitly enabled)"| G["review status --next-transition(provider-owned negotiated route)"]
G --> H{"Risk frozenat START"}
H -->|"low"| I["Structural readback0 lenses · silent"]
H -->|"standard"| J["1 focus lens+ consent"]
H -->|"high"| K["Canonical 4R + consent + forecastRisk · Readability · Reliability · Resilience"]
J --> L["Reviewers inspect the immutable candidate(review inspect-candidate)"]
K --> L
L --> M{"Severe candidate-causedfindings?"}
I --> N["Receipt: approved"]
M -->|"no"| N
M -->|"yes"| O["One bounded correction(frozen budget)"]
O --> P["Fix validator(read-only, immutable trees)"]
P -->|"passes"| N
P -->|"fails with evidence"| Q["Escalated"]
P -->|"no access to the diff"| R["Inconclusive: attempt notconsumed, capture again"]
R --> P
Q --> S["review recover(authorized successor)"]
N --> T["Delivery gatespre-commit → pre-push → pre-prvalidate the SAME receipt"]
T --> U["Commit → Push → PR"]
Z --> U
style N fill:#2D4F67,color:#fff
style Q fill:#B8860B,color:#fff
style U fill:#2D4F67,color:#fff
```
**SDD route** — durable planning artifacts first, then apply, with RDD reviewing the candidate before verify and archive requiring the receipt:
```mermaid
flowchart TD
A["User: sdd-new / sdd-explore(or sdd-ff to fast-forward planning)"] --> B["Exploreinvestigate codebase and approaches"]
B --> C["Proposeintent · scope · approach"]
C --> D{"User approvesthe proposal?"}
D -->|"no"| B
D -->|"yes"| E["Specrequirements + scenarios"]
E --> F["Designarchitecture decisions"]
F --> G["Tasksordered deliverable checklist"]
G --> H["Applysub-agent implements against specs(sdd-attempt acquire/settle · CAS · budgets)"]
H --> I["RDD over the frozen candidate"]
subgraph RDD["RDD — same machine as the organic route"]
I --> J{"Risk"}
J -->|"low"| K["Structural readback"]
J -->|"standard / high"| L["1 lens or 4R + consent"]
L --> M{"Severe findings?"}
M -->|"yes"| N["One bounded correction+ fix validator"]
M -->|"no"| O["Receipt: approved"]
K --> O
N -->|"validates"| O
N -->|"fails"| P["Escalated → recover"]
end
O --> Q["Verifyindependent verification againstspec · design · tasks"]
Q -->|"passes"| R["Archivemerge delta-specs · close the cycle(requires reviewGate allow or disabled)"]
Q -->|"fails"| H
R --> S["Delivery gatesvalidate the same receipt"]
S --> T["Commit → Push → PR"]
style O fill:#2D4F67,color:#fff
style P fill:#B8860B,color:#fff
style T fill:#2D4F67,color:#fff
```
Size, file count, or perceived risk never select SDD on their own — only an explicit request or an accepted proposal does. Either way, one candidate gets one review, one possible correction, and one receipt.
### Control receipt-driven development
Review mode is user-owned and available independently of the review lifecycle. **Receipt-driven development is opt-in: it is off until you turn it on.**
```bash
gentle-ai review mode status --cwd .
gentle-ai review mode enable --scope global --cwd .
gentle-ai review mode disable --cwd .
```
`status` is read-only. With no source expressing an opinion the effective mode is `off`, reported as decided by `default`; only an explicit global enable turns review on. Any global or clone-local disabled source wins; a clone can opt out with `--scope clone` but cannot force review on, so `--scope global` is the only way in. Enabling applies only to future candidates, while declining a one-candidate review prompt does not change the mode. When review is off, existing exact governing receipts remain authoritative; otherwise native review gates report `disabled/unmanaged` and defer delivery to ordinary repository policy without fabricating approval.
Historical note: `v2.2.2` introduced the native delivery-gate `disabled/unmanaged` disposition. Current SDD status does not use that disposition: with review disabled, it skips review authority, emits no `reviewGate`, and pre-verify continues without routing to a review that cannot start. Archive proceeds under ordinary repository policy when `reviewGate` is absent; a present `reviewGate.result: allow` is required only for discovered review activity. This differs from native delivery gates, which report `disabled/unmanaged` when review is disabled.
### Release verification
Official macOS and Linux release archives require an authenticated `checksums.txt`. The built-in upgrader verifies its Minisign signature, its exact `Gentleman-Programming/gentle-ai` + release-tag binding, and the selected archive checksum **before** replacing the installed binary. Release archives are capped at **128 MiB**, including chunked or unknown-length responses. Missing, oversized, malformed, untrusted, or placeholder key material fails closed without changing the installed binary.
To verify a release manually, obtain the production public-key payload and fingerprint from a maintainer-controlled channel, then download `checksums.txt` and `checksums.txt.minisig` from the same release:
```bash
minisign -VQm checksums.txt -x checksums.txt.minisig -P "$GENTLE_AI_MINISIGN_PUBLIC_KEY"
# Expected output: repo=Gentleman-Programming/gentle-ai;tag=vX.Y.Z
sha256sum --check --strict --ignore-missing checksums.txt
```
Do not bootstrap trust from a public key downloaded only beside the artifacts it verifies. See [Release signing and key rotation](docs/release-signing.md) for the first-signed-release procedure, exact CI injection points, and rotation runbook.
Windows archives and Scoop publication remain omitted until publicly trusted RSA Authenticode signing is provisioned (prefer managed OIDC with Azure Artifact Signing), both amd64 and arm64 executables are signed before archive and checksum generation, and release verification fails if either executable is unsigned.
### Review a focused staged candidate
For a monorepo or shared worktree, explicitly review exactly what is in the Git index:
```bash
git add apps/my-service
git diff --cached
gentle-ai review start --projection staged
```
The staged projection freezes the **complete existing index**, including all previously staged paths. It starts review but does not itself issue an approved receipt; unstaged and untracked worktree content is excluded. The default `workspace` projection remains the complete workspace review, and an existing authority is never auto-converted between projections. See the [review authority threat model](docs/review-authority-threat-model.md) for delivery and base-ref details.
### Backups
Every install, sync, and upgrade automatically snapshots your config files. Backups are **compressed** (tar.gz), **deduplicated** (identical configs are not re-backed up), and **auto-pruned** (keeps the 5 most recent). Pin important backups via the TUI (`p` key) to protect them from pruning.
See [Backup & Rollback Guide](docs/rollback.md) for details.
---
## Key Features You Should Know About
### OpenCode SDD Profiles
Assign different AI models to different SDD phases -- a powerful model for design, a fast one for implementation, a cheap one for exploration. OpenCode uses **`gentle-orchestrator`** as the base SDD conductor, and generated named profiles still appear as `sdd-orchestrator-{name}` entries.
```bash
# Via CLI
gentle-ai sync --profile cheap:openrouter/qwen/qwen3-30b-a3b:free
gentle-ai sync --profile-phase cheap:sdd-design:anthropic/claude-sonnet-4-20250514
# Or via TUI: gentle-ai → "OpenCode SDD Profiles" → Create
```
After creating a profile, open OpenCode and press **Tab** to switch between `gentle-orchestrator` (default) and your custom profiles.
| What you need | Use this |
| --------------------- | --------------------------------------------------------------- |
| Default SDD conductor | `gentle-orchestrator` |
| Legacy configs | `sdd-orchestrator` is migrated to `gentle-orchestrator` on sync |
| Named model profiles | `sdd-orchestrator-cheap`, `sdd-orchestrator-premium`, etc. |
**Full guide**: [OpenCode SDD Profiles](docs/opencode-profiles.md)
### Engram (Persistent Memory)
Your AI agent automatically remembers decisions, bugs, and context across sessions. You don't need to do anything -- but when you do:
```bash
engram projects list # See all projects with memory counts
engram projects consolidate # Fix name drift ("my-app" vs "My-App")
engram search "auth bug" # Find a past decision from the terminal
engram tui # Visual memory browser
```
**Full reference**: [Engram Commands](docs/engram.md)
---
## Documentation
| Your task | Start here |
| --- | --- |
| Understand the Gentle-AI mental model | [Intended Usage](docs/intended-usage.md) |
| Choose direct, delegated, or optional SDD routing | [Organic Implementation Routing](docs/trigger-rules.md) |
| Plan substantial work with SDD | [Intended Usage](docs/intended-usage.md) and [OpenSpec Config](docs/openspec-config.md) |
| Configure a supported agent | [Agents](docs/agents.md) for the feature matrix and per-agent notes |
| Use the Pi package harness | [Pi Agent](docs/pi.md) for packages, Pi-native commands, models, and troubleshooting |
| Configure OpenCode phase models | [OpenCode SDD Profiles](docs/opencode-profiles.md) |
| Review or deliver a change safely | [Review Integration Contract](docs/review-integration.md) for provider consumers; [Review Authority Threat Model](docs/review-authority-threat-model.md) for technical boundaries; [Chapter 21 — Verifiable Trust](https://the-amazing-gentleman-programming-book.vercel.app/en/book/Chapter21_Verifiable-Trust) for the mental model |
| Find or share persistent context | [Engram Commands](docs/engram.md) |
| Refresh or troubleshoot an installation | [Usage](docs/usage.md), [Backup & Rollback](docs/rollback.md), and [Platforms](docs/platforms.md) |
| Extend or contribute to Gentle AI | [Codebase Guide](docs/CODEBASE-GUIDE.md), [Components, Skills & Presets](docs/components.md), [Skill Registry](docs/skill-registry.md), and [Architecture & Development](docs/architecture.md) |
| Understand how agent behavior is tested | [Testing Agents Deterministically](docs/testing-agents-deterministically.md) for the real-agent E2E and its model fixture |
---
## Community Highlights
This project gets better when the community builds on top of it.
### Community Integrations
- [sub-agent-statusline](https://github.com/Joaquinvesapa/sub-agent-statusline) — optional OpenCode TUI plugin that shows sub-agent activity, status, elapsed time, and token/context usage when OpenCode exposes it.
- [sdd-engram-plugin](https://github.com/j0k3r-dev-rgl/sdd-engram-plugin) — optional OpenCode TUI plugin to manage SDD profiles and browse Engram memories directly from OpenCode, with runtime profile activation and no restart required.
When you select OpenCode in the installer, Gentle-AI asks whether to register each community plugin and offers a browser shortcut to review the repository first. Gentle-AI only ensures `~/.config/opencode/tui.json` exists and adds the plugin package names to its `plugin` array; OpenCode installs/loads those packages the next time it starts. Once OpenCode has materialized a plugin under `~/.config/opencode/node_modules/`, `gentle-ai update` can compare its local `package.json` version with the plugin's GitHub releases.
### Contributors
This project exists because of the community. See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list.
---
## Next Steps
- **Just installed?** Read [Intended Usage](docs/intended-usage.md) for the mental model, then run `gentle-ai doctor` if anything looks wrong.
- **Starting work?** Read [Organic Implementation Routing](docs/trigger-rules.md) to understand direct, delegated, and optional SDD behavior.
- **Reviewing a focused change?** Start with the [Organic RDD architecture](docs/architecture/organic-rdd.md) and [review authority threat model](docs/review-authority-threat-model.md).
- **Maintaining Gentle AI?** Use the [Codebase Guide](docs/CODEBASE-GUIDE.md) to find package ownership and review boundaries.
- **Using Pi?** Read [Pi Agent](docs/pi.md) for the `gentle-pi` harness, Pi commands, persona, and model assignments.
- **Ready to contribute?** Start at the [Community Roadmap](docs/community-roadmap.md) — everything labelled [`up-for-grabs`](https://github.com/Gentleman-Programming/gentle-ai/issues?q=is%3Aissue+is%3Aopen+label%3Aup-for-grabs) is scoped, approved and unclaimed. Then read [CONTRIBUTING.md](CONTRIBUTING.md).
---
---
## File: bench/README.md
# gentle-ai-bench
Measures the **friction** of driving `gentle-ai`'s review lifecycle, so a
"before" binary and an "after" binary can be compared and the change can be
shown rather than asserted.
Its core corpus is a **black box**. It drives a `gentle-ai` binary given by
`--binary` as a subprocess and never instruments the product, so it works
against any build including old releases. It is **deterministic and offline**:
no model is ever called. Every journey runs in a fresh temp directory with its
own `HOME`, `XDG_*`, a throwaway git repository and, where the flow needs one, a
local bare remote. It never touches your real config or repositories.
That claim is scoped to the core on purpose. A run can additionally select an
**opt-in axis** with `--axis`, and an axis measures states the CLI cannot
construct — so it is not black-box and not portable across builds. No axis runs
unless you name it, each one declares what it costs, and the report prints that
declaration next to the journeys it contributed. See
[Opt-in axes](#opt-in-axes).
## Its own module, on purpose
`bench/` declares its own `go.mod`, so the root module's `go build ./...`,
`go vet ./...` and `go test ./...` do not see it. That is deliberate: the tool
must never be able to break, slow, or enter a release build of the product it
measures. The cost is that nothing verifies it automatically — build it from
inside this directory:
```
cd bench
go build ./...
go vet ./...
go test ./...
```
The portable core contains 57 journeys. `j57` is deliberately excluded because
it requires the product's `bench_fixture` seam; it is an explicit
`source-coupled` axis, not a portable black-box measurement.
The measured binary is passed in with `--binary`, so the tool never depends on
the sources next to it. That is what lets it measure an old release and the
current build with identical code.
## Two modes, two questions
| Mode | Question it answers |
|---|---|
| `run` (driven) | "What does this binary cost to drive through a fixed corpus?" Reproducible, comparable between binaries. |
| `record` + `analyze` (observed) | "What did a real agent actually experience this session?" Honest about one agent, one session, whatever it happened to do. |
Both compute the dimensions with the **same** classifier function. `compare`
refuses to compare a driven run against an observed run: they measure
different populations and the table would be meaningless.
### Driven
```
gentle-ai-bench run --binary /path/to/gentle-ai --out results-after.json
gentle-ai-bench run --binary /path/to/old-gentle-ai --out results-before.json
gentle-ai-bench compare --before results-before.json --after results-after.json
```
`run --only j05-gate-without-any-review,j10-invalid-flag-combination` runs a
subset. `run --axis damaged-store` adds an opt-in axis; `--axis all` adds every
registered one. An unknown axis name is a hard error, never a quiet fall back to
the core.
Run the portable SDD authority controls against a selected public binary:
```sh
gentle-ai-bench run --binary /path/to/gentle-ai --only \
j52-sdd-stale-authority-does-not-shadow-approved-candidate,\
j53-sdd-ambiguous-authorities-fail-closed,\
j54-sdd-missing-authority-receipt-fails-closed,\
j55-sdd-mismatched-authority-receipt-fails-closed,\
j56-sdd-non-allow-post-apply-gate-fails-closed,\
j58-sdd-foreign-openspec-path-fails-closed
```
Run the source-coupled receipt-drift proof only with its tagged product binary.
Build the product from the repository root, then run the benchmark from `bench/`:
```sh
# From the repository root.
go build -tags bench_fixture -o /path/to/gentle-ai ./cmd/gentle-ai
# From bench/, after building gentle-ai-bench above.
./gentle-ai-bench run --binary /path/to/gentle-ai --axis source-coupled --only \
j57-sdd-authority-drift-during-discovery-fails-closed
```
**`run` fails closed on failed journeys.** A journey that reports `failed`
produced no numbers — the harness could not build or prove its fixture, or an
assertion fired — and community issue #1883 found that such a run still exited
0, so a CI gate reading the exit saw success in a run that measured nothing
for those rows. `run` now exits nonzero when any journey failed; the results
file is still written first, so the evidence survives the failure. Journeys
that report `unsupported` do **not** fail the run: driving an older binary is
a designed use, "this build lacks that surface" is a real measurement, and
the summary line plus the `unsup` cells keep it impossible to read as either
a pass or a failure. Both rules are pinned in `main_test.go`.
### Observed
```
gentle-ai-bench record --binary $(which gentle-ai) --out session.jsonl
# follow the printed PATH line, then run your agent through the testing guide
gentle-ai-bench analyze --session session.jsonl --out results-observed.json
```
A ready-to-paste prompt for the agent — which starts and closes the recording
itself — lives in [`AGENT-PROMPT.md`](AGENT-PROMPT.md). It carries one rule
worth repeating here: **the agent must not read gentle-ai's source.** An agent
that has read the implementation recovers using knowledge a real user does not
have, so the run comes out clean for the wrong reason. The whole point is
measuring whether the tool explains itself.
`record` writes a directory containing an executable named `gentle-ai` and
prints the one line that puts it first on `PATH`. The shim logs every
invocation and delegates to the real binary, preserving argv, stdin, stdout,
stderr and the exit code. Because it intercepts at the process boundary, it
works with any agent or harness.
**Shim fidelity rule.** A stream that is a character device (a terminal, and
also `/dev/null`) is passed through untouched instead of being teed. Replacing
it with a pipe would flip `gentle-ai`'s own interactivity check — it decides
whether to ask the consent question by testing whether stdin *and* stderr are
character devices — and a benchmark that changes the thing it measures is
worthless. The cost is that such invocations are recorded with
`stdout_captured: false` / `stderr_captured: false`, and the dimensions that
depend on them become `null` rather than a guess.
## The seven dimensions
| # | Dimension | What it counts | How |
|---|---|---|---|
| 1 | `human_prompts` | Times the flow would stop to ask a human | Runs non-TTY. `gentle-ai` prints a consent-skipped notice on **stderr** when it would have asked; the benchmark counts occurrences of that exact string. |
| 2 | `manual_tokens` | Steps needing a hand-assembled authorization | Invocations whose argv carries a non-empty `--maintainer-authorization`. Both `--flag value` and `--flag=value`. |
| 3 | `commands_to_completion` | Binary invocations from start to terminal state | Every product invocation the journey issues. Benchmark instrumentation (capability probes) is **not** counted. |
| 4 | `blocks` | Every non-zero exit or denial, in five buckets | See the classifier below. |
| 5 | `recovery_round_trips` | Commands spent between a block and the flow resuming | From the blocking command up to and including the first subsequent command that is not itself a block. |
| 6 | `model_runs` | Reviewer/lens invocations the flow required, re-runs included | Driven mode: measured — the benchmark issues them. Observed mode: **proxy**, see below. |
| 7 | `human_surface_bytes` | Human-facing narration volume | Total stderr bytes. |
There is one extra, informational field, deliberately **not** one of the seven:
- `git_subprocesses` — git processes the product spawned, counted from
`GIT_TRACE` lines. In driven mode `GIT_TRACE` points at a per-journey log;
in observed mode the shim sets it only when the user has not. It is a lower
bound if a build ever performs git operations in-process. It is reported
because it is cheap and reliable to observe, not because it is a friction
dimension.
## The block classifier
This is the load-bearing part and it is **mechanical on purpose**. Given the
same bytes it always returns the same class, so the `in_band` / `out_of_band`
split cannot drift into opinion between two runs or two reviewers. It lives in
one function, `Classify` in `classify.go`, and is unit-tested against recorded
real output in `testdata/observations.json`.
**Is it a block?** Non-zero exit, or a JSON envelope with `allowed: false`, or
a denying `result` (`invalidated`, `scope-changed`, `deny`, `denied`, `stop`,
`blocked`, `corrupted`), or `action: "stop"`. A denial that exits 0 still
counts: the flow cannot proceed.
**Which class?** In this order:
1. The flow continued with no extra command → `self_recovered`.
2. The emitted text (stdout or stderr) contains a runnable `gentle-ai …`
command → `in_band`.
3. The stdout JSON envelope carries a `next_action`, `recovery_operation`, or
`collect.capture_operation` (and its execute-shaped sibling
`next_transition.execute.operation`) naming an operation that is not
`stop`/`none`/empty → `in_band`.
4. The corpus declares the refusal correct **and** the exact next-action text
it quotes is verified present in the emitted bytes → `by_design`.
5. The journey corpus declares that no continuation exists → `dead_end`.
6. Otherwise → `out_of_band`: blocked, and the output named no runnable
continuation, so the user had to go and look it up.
Two precise sub-rules:
- **"Runnable" excludes templates.** `gentle-ai review validate --gate `
is not runnable — the user still has to fill it in — so it does not make a
block in-band on its own. A line offering both a templated command and a
clean one counts as in-band on the strength of the clean one.
- **`action` is deliberately not a continuation key.** A gate denial carrying
`action: "explicit-maintainer-action"` names a posture, not an operation you
can run.
Two classes are **author-declared** rather than derived, because neither
"nothing exists to run next" nor "nothing *could* honestly exist to run next"
is decidable from outside the binary. Both are set per step in the corpus, and
a mechanically detected continuation always overrides either one.
- `dead_end` (`Step.DeadEnd`) says there is **no next action**. The flow is
over. The current corpus declares none, so `dead_end` is 0 everywhere — an
honest 0, not a measured absence of dead ends in the product as a whole.
- `by_design` (`Step.ByDesign`) says there **is** a next action, the product
already stated it, and it is not expressible as a `gentle-ai` command.
They are opposite answers to "is there anything to do next?", so a step
declaring both is contradicting itself and the run refuses to start.
`by_design` is deliberately the most expensive thing to declare in this
benchmark, because it is the only annotation that can make `out_of_band`
smaller. It costs two things:
- **A shape, from a closed vocabulary.** `operator-knowledge` — the product
cannot know a value only the operator has. `world-action` — the exit is an
action, not a command: edit the code, free some disk space, plug the mount
back in. `human-authority` — the block *is* a human decision, and if a
command could produce the authorization the gate would be theatre. Not free
text: an unrecognised shape is a corpus error and `run` exits before driving
anything.
- **A quote of the product's own next-action text**, which the classifier
verifies is really in the bytes the product emitted. This is the load-bearing
half. "No command can exist" never excuses "the message says nothing", so
`Error: no.` cannot be declared by-design: there is nothing to quote, and a
quote that is not in the output is not a quote — the declaration does not
apply and the block stays `out_of_band`.
An exemption is a **reclassification, never a subtraction**: the block is still
a block and still inside `4 blocks (total)`. Every declaration in the run is
printed under *By-design blocks* with its shape and its verified quote,
including the ones that did **not** apply — a declaration the classifier
refused is the first sign the product's message changed under the corpus, so it
is reported rather than dropped.
The corpus declares one, in `j17-bare-repository`
(`operator-knowledge`): `review start` in a bare repository can only offer
`--cwd `, an unfillable template, because it cannot know
where the operator's checkout is. What it prints instead is the action —
*"run the same command again from a checkout"* — and that is the string the
classifier checks for.
Observed mode has no corpus, so nothing there can declare an exemption and
`by_design` is 0 by construction, not by measurement. A recorded session's
correct refusals are counted as `out_of_band` exactly as before.
## `unsupported` is never `0`
The "before" binary will be an older release whose CLI surface differs. Before
a step runs, the benchmark probes the binary (` --help`, uncounted) for
the verb and every flag the step needs. A missing surface records
`unsupported`; the journey aborts cleanly and is **excluded from totals** and
counted separately. In every table an unsupported journey renders as `unsup`,
never as a number.
Runtime detection backs this up, matching on output rather than exit code
alone: `flag provided but not defined`, `unknown … command "…"`,
`unexpected … argument "…"` and friends. Matching on the message is deliberate
— exit codes for "I do not have that flag" are not guaranteed to differ from
ordinary state failures, and counting a missing surface as a state failure
would make an old binary look capable.
**When `--help` is not a help surface.** The `sdd-attempt` operations parse
their own flags and reject `--help` with `flag provided but not defined:
-help` — the same words a genuinely missing flag produces. The default probe
would therefore report a build that fully supports the verb as lacking it. Such
a capability declares `Probe` instead: a complete argv that carries the flag
under test and can only fail on *state*. A build with the flag answers
`sdd-attempt requires --cwd`; a build without it answers `flag provided but not
defined`. Probe invocations are uncounted like every other probe.
## Comparing two binaries
`compare` computes the dimension totals over the **comparable subset**:
journeys that completed in *both* runs. Summing a run of 14 completed journeys
against a run of 5 would produce a large delta that reads as a regression when
it is really just a wider corpus. Excluded journeys are named in the output and
in `excluded_journeys`, never silently dropped, and the per-journey breakdown
still shows every journey with `unsup -> n` where the older binary could not
run it.
## What this deliberately does NOT measure
- **Wall-clock time.** Excluded by design. Review duration is dominated by the
model provider, which makes it non-comparable between two runs of the same
binary, let alone between two binaries. The recorded session carries
timestamps only to order records; no duration is computed or reported.
- **Real model tokens.** Excluded by design: provider-dependent, costly, and
not reproducible. Where a journey needs reviewer output, it is synthesized
from the binary's **own** preflight/collect envelope — the subject hash and
the changed-path manifest come straight from the product, so the capture is
admitted for the same reason a real reviewer's would be. That is what makes
"model runs" countable without spending a token.
- **A single composite friction score.** Explicitly rejected. A weighted sum
can improve while `dead_end` and `out_of_band` increase; collapsing the
dimensions would hide exactly the regression that matters most. The tables
print every dimension separately and `compare` emits no aggregate.
## Honesty contract — known gaps
Everything below is a real limitation, stated because a benchmark that quietly
invents a metric is worse than one that admits a gap.
1. **`model_runs` in observed mode is a proxy, not a measurement.** The agent's
own model calls never cross the process boundary, so the shim cannot see
them. It counts `review capture-result` invocations (one per lens run, plus
recaptures; `--preflight` excluded because it reads no result). It is
emitted with `"derivation": "proxy"` and a note, and `compare` propagates
the proxy label so it can never be laundered into a measurement by summing.
In driven mode it *is* measured, because the benchmark issues the runs
itself.
2. **`human_prompts` never counts a real prompt.** Runs are non-TTY by
construction, so what is counted is the consent-skipped stderr notice: the
number of times the tool **would have asked**. The interactive question
itself (testing-guide flow 5) needs a real terminal and a human answer, and
is therefore outside what this benchmark can drive. It is not in the corpus.
3. **`human_surface_bytes` is `null` for terminal streams.** See the shim
fidelity rule above. In driven mode it is always measured.
4. **`git_subprocesses` is a lower bound.** It counts `GIT_TRACE` lines, which
only appear for real `git` subprocesses. A build performing git work
in-process would undercount, silently. This is why it is informational and
not one of the seven.
5. **`dead_end` is author-declared.** See above.
6. **The disabled-reviews gate is exempted by its delivery disposition, and
that exemption is narrow on purpose.** It answers at exit 0 with
`allowed: false`, `action: "repository-policy"` and
`delivery: "disabled/unmanaged"`, and its reason says delivery follows
ordinary repository policy. Nothing is stopped. Counting it reported the one
guarantee the kill switch exists to provide as two blocks the product
inflicted, so `IsBlock` returns false on that disposition alone. The sibling
`unmanaged`, the switch ON with no receipt yet, stays a block, because there
the operator really is stopped. Both are pinned from recordings of a real
binary so they cannot be conflated again. This is the one place the
classifier reads a field other than exit code and denial shape, and widening
it would let the product talk its way out of a denial.
7. **The corpus is honest, not exhaustive.** Fifty-seven mandatory portable
black-box journeys run end to end, weighted toward failure paths because that
is where friction lives. `j57` is one explicit source-coupled journey that
requires a `bench_fixture`-tagged product binary, for 58 registered journey
IDs total. Testing-guide flows 1 (install) and 8 (no phantom SDD artifacts)
are inspection steps rather than review-lifecycle friction and are not
modelled.
8. **Some edge cases are unreachable from a temp directory and are guide flows
instead.** A network mount where advisory locks fail in ways that are
neither "busy" nor "missing", a read-only filesystem, a disk that fills
during a receipt write, a case-insensitive or Unicode-normalizing volume,
Windows antivirus holding a file mid-write, Windows long paths, and a system
clock moving backwards all need a machine this harness cannot build. They
are flows 27 to 33 of `docs/testing/organic-rdd-testing-guide.md`. A flaky
journey inside a loop is worse than no journey, because it gets blamed on
whatever changed last.
9. **`by_design` is an author-declared exemption, and it is the one number in
here that can be gamed.** It exists because `out_of_band` was counting two
different things: a defect — the product blocked the operator and gave them
nothing runnable when a runnable continuation could exist — and a correct
refusal for which naming a command would mean naming a dead end. Only the
first is what the release criterion cares about. Splitting them makes the
defect count mean something; it also opens a channel for laundering real
defects into a clean bucket. What it costs to declare one is described
above: a shape from a closed vocabulary, and a quote of the product's own
next-action text that the classifier verifies against the emitted bytes.
**The failure mode it introduces:** a declaration is a claim about a message
the corpus does not own. The quote can keep matching while the sentence
around it stops being useful, and the harness cannot tell the difference —
it checks that the words are there, not that they still help. So the
exemption is never a subtraction (the block stays in the total, in its own
column, in its own section, quote included) and the honest way to read the
section is to read the quote, not the count. A declaration that no longer
applies is printed as stale rather than silently ignored, which catches the
message disappearing but not the message rotting.
10. **The report is not byte-identical between two runs, though every count
except one is.** Each journey runs under `os.MkdirTemp`, whose random
suffix varies in length, and several journeys quote that path back in a
block message — `j14`, `j17`, `j31`, `j33`, `j34`, `j37`, `j38`, `j39` and
`j40` observed so far, and any journey that drives a refusal naming a
repository path can join them. Which of them actually moves between a given
pair of runs is chance: the suffix is 9 or 10 digits. No `damaged-store`
axis journey has been observed to move: its refusals name lineages,
artifacts and target identities, all of which are fixed by the fixture.
No `real-world` axis journey has been observed to move either — its one
refusal that quotes a path quotes the repository-relative `".wt/test/"`,
not the sandbox prefix. So
the quoted messages differ run to run and
`human_surface_bytes` wobbles by one byte per affected journey; every block
classification, every count and `git_subprocesses` are stable. This is why the "byte-identical" claim under
*Measured* below is scoped to the 14-journey corpus it describes — no
journey in that corpus echoed a sandbox path. Making it hold again means
choosing between a fixed-width random suffix (stabilises the numbers, not
the quoted paths) and a deterministic per-journey path (stabilises both,
and makes two concurrent runs collide). That is a maintainer's call and it
has not been made.
11. **An axis is not black-box, and an axis run is not a core run.** The core
corpus reaches every state it measures through the process boundary, which
is what lets `--binary` point at any build. An axis exists because some
states cannot be reached that way, and reaching them means reading or
writing something the product owns. So a `--axis` run gives up portability:
against a build whose internals have moved, those journeys report `failed`
or `unsupported` rather than a number, by design — see the two checks under
[Opt-in axes](#opt-in-axes) that make sure a fixture cannot quietly build
the wrong state and pass. **This entry is not where that lives.** The
property belongs to the axis and travels with the run: it is in
`results.json` under `axes[]`, on every journey as `axis`, in the table's
`axis` column, and printed in full under *Opt-in axes in this run*. A
property a reader has to come and find is a property that gets missed, so
the report states it and this entry only points at it. What remains a real
gap is that no axis can be portable — that is inherent, not a bug to fix,
and the honest response is that the core stays black-box and the axis stays
opt-in.
12. **The `real-world` axis is black-box and is still not the core, and its
strongest assertions prove less than they might seem to.** Nothing in that
axis touches product-owned state, so entry 11's portability cost does not
apply to it — but it remains opt-in because it is a different population
(cluttered repositories, interleaved lifecycles) governed by a different
growth rule: community-reported shapes become journeys, so its size tracks
community reports, not releases, and folding it into the core would make
"57 core journeys" a moving claim. Two of its numbers need careful reading.
`rw01` pins issue #1881 while a product fix is in flight: a block there is
the truth about today's build, not a permanent verdict, and the journey is
kept precisely so the fix has a permanent pin. And the no-echo assertions
in `rw03`/`rw09` prove absence of the sentinel **from the emitted bytes of
counted commands only** — a black-box harness cannot see whether the
product ever *read* the secret file or the ignored binary, only whether it
quoted them. "The journey passed" means "nothing counted echoed it",
nothing stronger.
13. **A boundary-specific fix is omitted when this harness cannot cross its real
boundary.** Issue #2028 is an OpenCode plugin session-local retry and remains
plugin-covered; a CLI journey would never exercise that session boundary.
Issue #2074 is a Claude install/registry migration and remains E2E-covered;
this isolated review-lifecycle harness does not install or migrate Claude.
Issue #910 requires genuine Windows PowerShell host resolution; a Linux
fixture pretending to be Windows would be a proxy, not benchmark coverage.
## Measured: the current build
`results-after.json` in this directory is a real run of the whole corpus
against `gentle-ai 1.49.1-0.20260726001603-c2b91ac966ca+dirty`, built from
this repository at commit `c2b91ac9`. All 14 journeys
completed; nothing was unsupported. Re-running produces byte-identical numbers,
`git_subprocesses` included.
```
1 human_prompts 6 (one per medium/high-risk `review start`)
2 manual_tokens 1 (`review abandon`)
3 commands_to_completion 92
4 blocks (total) 10
4a self_recovered 0
4b in_band 3
4c out_of_band 7
4d dead_end 0 (author-declared; corpus declares none)
5 recovery_round_trips 4
6 model_runs 15
7 human_surface_bytes 2605
- git_subprocesses 2787 (informational)
```
Those numbers are the **14-journey** corpus against the binary named above,
kept as-is because they belong to that named build. The portable core has since
grown to 57 journeys; the source-coupled `j57` receipt-drift proof is opt-in.
Re-run `run` against your own binary rather than reading the block above as
current totals. The row labels moved too: `by_design` did not exist when this
was recorded and is now printed as `4d`, next to the number it carves out of,
with `dead_end` at `4e`.
`results-before.json` is the same corpus against `v2.1.2`, kept as a worked
example of the cross-version path: 5 journeys completed and 9 recorded
`unsupported` (no `review capture-result`, no `review capture-evidence`, no
`review status`, no `review mode`). Nothing crashed and nothing was scored as
zero. On the 5 comparable journeys, blocks fell from 10 to 3 — all seven
removed blocks were `out_of_band` — and `human_surface_bytes` from 604 to 383,
with `commands_to_completion` unchanged at 16.
## The corpus
Journeys are data — a slice of `Step` in `journeys.go`. Adding one is
appending to that slice.
### Every journey declares its review precondition
Receipt-driven development is opt-in, and the sandbox `HOME` each journey runs
under is a fresh install, so a journey gets no review by standing still.
`Journey.Review` says what the runner does about that, and it is **mandatory** —
`validateCorpus` fails the whole run on a journey that does not declare one.
- `reviewOptedIn` — before the journey's first step, the runner opts in the way
a user does: `gentle-ai review mode enable --scope global`, run from a
throwaway checkout of its own, then read back. The journey fails if the
product does not report the switch on. It is sandbox setup, not operator work,
so it is never counted in `commands_to_completion`. Global is the only scope
that can assert "on"; a clone may only ever assert "off".
- `reviewUntouched` — the runner runs no mode command at all. This is for a
journey whose subject IS the switch (`j03-kill-switch` drives it itself,
`j31-nonsense-mode-value` authors the record under test) and for one that has
nothing to do with reviews (`j2138`, `j3043`, `j97` install agents).
The declaration is mandatory because the alternative already cost us once: the
corpus measured the review lifecycle only because the product's default happened
to say yes, and the day that default changed those journeys did not fail — they
quietly measured a different flow, with the review refused and the gate passing
under ordinary repository policy.
| ID | Flow | Source |
|---|---|---|
| `j01-docs-happy-path` | docs change: review, approve, commit, push gate | guide flow 3 + 9 |
| `j02-high-risk-four-lens` | four lenses, evidence, approval | guide flow 4 + review contract |
| `j03-kill-switch` | disable, start refused, re-enable, review | guide flow 2 |
| `j04-size-does-not-escalate` | 1200 lines of prose still reviews low | guide flow 4 |
| `j05-gate-without-any-review` | gate before any receipt exists | community failure path |
| `j06-pre-push-after-publication` | pre-push after the reviewed commit was pushed | guide flow 9 |
| `j07-disabled-with-stale-receipts` | reviews off with two stale receipts | guide flow 6 + 9 |
| `j08-finalize-without-reviewer-results` | finalize with no reviewer results | community failure path |
| `j09-finalize-without-evidence` | finalize with results but no evidence | guide flow 12 |
| `j10-invalid-flag-combination` | staged projection plus base ref | guide flow 13 |
| `j11-unborn-head` | first commit in a repo with no history | guide flow 10 |
| `j12-rejected-capture-then-recapture` | a rejected reviewer result, then a recapture | community failure path |
| `j13-next-transition-runs-verbatim` | the printed transition executes as printed | guide flow 11 |
| `j14-abandon-needs-a-hand-built-token` | abandoning a lineage needs an assembled authorization | `review abandon` contract |
### Edge cases (`journeys_edge.go`)
Journeys 1 to 14 came from the community testing guide and the failure paths it
collected. Journeys 15 to 36 are the edge cases those flows never reached. Each
one is tied to one of the five shapes a night of real defects clustered into,
and the shape is named in the journey's `Source`. Journeys 37 to 43 in
`journeys_sdd.go` reuse the same vocabulary:
| shape | what it is |
|---|---|
| 1 | **asymmetric comparison** — one operand canonicalized, the other not |
| 2 | **transient read as permanent** — a retryable condition surfacing as terminal or ambiguous |
| 3 | **a guard behind the wrong condition** — a check gated on something that is not its own precondition |
| 4 | **a message naming something that does not work** |
| 5 | **two sources of truth** — a document and the code disagreeing about the same fact |
A journey that stresses none of them is not added: it would only make the
number look covered.
| ID | Flow | Shape |
|---|---|---|
| `j15-linked-worktree` | one repository, two absolute paths, two HEADs, one shared review store | 1 + 5 |
| `j16-detached-head` | the whole cycle with no branch at all | 3 |
| `j17-bare-repository` | a repository with no working tree | 4 + 2 |
| `j18-space-and-non-ascii-path` | repository path with spaces and non-ASCII characters | 1 |
| `j19-submodule-gitlink` | a 160000 index entry with no blob behind it | 1 |
| `j20-symlink-candidate` | mode 120000 whose blob is a path | 1 |
| `j21-mode-only-change` | `100644` → `100755`, identical blob on both sides | 1 |
| `j22-pure-rename` | every byte identical, only the path moved | 1 |
| `j23-deletion-only` | a candidate whose new side is empty | 1 |
| `j24-empty-file` | zero bytes, zero changed lines | 4 |
| `j25-no-trailing-newline` | the last line has no terminator | 1 |
| `j26-crlf-content` | carriage returns survive into the staged blob | 1 |
| `j27-merge-in-progress` | review a conflict resolution before the merge commit exists | 3 |
| `j28-rebase-in-progress` | detached HEAD plus a rebase state directory | 3 |
| `j29-cherry-pick-in-progress` | `CHERRY_PICK_HEAD` present throughout | 3 |
| `j30-kill-switch-flipped-mid-review` | reviews turned off between START and FINALIZE | 3 + 5 |
| `j31-nonsense-mode-value` | a switch record that is readable and holds a value that is not `on`/`off` | 2 + 4 |
| `j32-recovery-of-a-recovery` | the named continuation has to work twice | 4 |
| `j33-escalate-then-recover` | escalate on a failed verification, then recover after fixing it | 2 + 4 |
| `j34-abandon-then-start-again` | an abandoned lineage must not poison the repository | 2 |
| `j35-correction-budget-exactly-zero` | forecasting a correction against a budget of 0 | 3 + 4 |
| `j36-contract-right-name-wrong-version` | `--contract` with the right name and a version this build lacks | 2 + 4 |
### The SDD remediation successor cycle (`journeys_sdd.go`)
Journeys 37 to 43 close the corpus's largest blind spot. A community tester
found a hard deadlock on this path that no internal audit had caught, and two
of its blocks were fixed by hand with nothing in a loop pinning either one. Up
to journey 36 the benchmark reported zero `out_of_band` blocks and zero dead
ends for a surface it had simply never driven.
| ID | Flow | Shape |
|---|---|---|
| `j37-sdd-remediation-self-successor` | a bound passing attempt over a corrected candidate is refused, and the refusal names the finish that IS accepted | 4 |
| `j38-sdd-remediation-distinct-successor` | with a real recovery successor in the way, the same refusal must route to review and must NOT name a finish | 3 + 4 |
| `j39-sdd-remediation-stranded-successor` | a successor that can never be finalized: the named route runs and changes nothing | 4 + 2 |
| `j40-sdd-attempt-reset-after-drift` | terminal attempt plus candidate drift: begin refuses, reset is the only way on | 2 + 4 |
| `j41-kill-switch-versus-sdd-pre-verify` | reviews off at the pre-verify decision: the router steps aside instead of naming a review the operator may not start | 5 |
| `j42-kill-switch-versus-sdd-archive` | reviews off at the archive decision: the product defers and never fabricates an approval | 5 |
| `j43-recovery-guard-rails-as-an-operator-meets-them` | three correct refusals around healthy approved authority, and the exit that is not a command | 4 |
Two of them measure something no test could: `j41` and `j42` each take one item
off the documented known-open list and let the number say whether it is still
open. Both came back **closed**, and both are therefore journeys with **no
blocks at all** — the pin is an assertion on the envelope rather than a block
count, so a regression fails the journey loudly instead of passing quietly.
`j41` is the clearest example of the corpus working as intended: it was written
to measure a believed-open dead end where the SDD pre-verify router demanded a
review the kill switch forbade, and it FAILED its own assertions on the run that
found the behavior fixed. The failure was the finding. It now pins both
positions of the switch — off routes to `verify` with no blocked reasons, on
routes to `review` with a reason that says why — because either half alone would
pass while the other regressed.
The state these journeys need cannot be built with git alone: an attempt
ordinal, a populated review binding and the leaf/non-leaf topology of a lineage
all live inside the product. Every fixture and composite here therefore reads
them back out of the product (`Sandbox.readBack`, uncounted, `GIT_TRACE`
blanked so its git calls are never charged to the next counted invocation) and
fails the journey when the state is not what it claims — including the two
premises that matter most: that the plain passing finish really does block, and
that the topology really is the leaf or the non-leaf shape the journey says it
is.
**Every fixture proves its own edge case before the journey trusts the result.**
A fixture that sets its edge case up wrongly and then passes is the failure mode
these journeys exist to avoid, so each one reads the state back out of git and
fails the journey when it is not what it claims: the linked worktree asserts
that `.git` is a file and that its git dir differs from the common dir, the
mode-only change asserts the index really went `100644` → `100755` with a
`0\t0` numstat, the mid-operation fixtures assert `MERGE_HEAD` /
`rebase-merge` / `CHERRY_PICK_HEAD` exist *after* the conflict is resolved, and
the nonsense mode record asserts that it still parses as JSON so the journey
cannot silently decay into the already-covered corrupt-record case.
### Wave 1 integration regressions (`journeys_wave1.go`)
Journeys 44 to 51 pin fixes that internal tests already covered below the user
boundary. All remain core black-box journeys: fixtures create repository inputs,
and every native authority state is reached through the measured binary.
| ID | Flow | Shape |
|---|---|---|
| `j44-corrected-current-changes-delivery` | corrected current-changes receipt in a proven linked worktree, exact one-commit delivery, selector-free pre-push discovery | issue #1819 + 3 |
| `j45-completed-final-verification-retry` | procedural final-verification failure, status-derived retry successor, successful completion, authoritative inventory and selector-free post-apply | issue #1915 + 3 |
| `j46-correction-required-staged-recovery` | correction-required base-diff authority, negotiated staged-overlay recovery, fresh successor review, exact pre-commit/pre-push/pre-PR delivery | issue #1921 |
| `j47-disabled-mode-archives-discovered-invalidated-receipt` | enabled discovered invalidation, disabled/unmanaged archive without allow, explicit invalid receipt control, and re-enabled enforcement | issue #2128 |
| `j48-recovered-workspace-preserves-full-candidate-scope` | two-path workspace candidate, strict-subset correction, complete terminal and recovered scope, immediate pre-commit allow, byte/path drift controls | issue #2090 |
| `j49-status-without-cwd-honors-kill-switch` | clone-local mode disabled, explicit-CWD control, omitted-CWD status using the same repository identity, disabled/unmanaged archive without approval | issue #2129 |
| `j50-candidate-decline-preserves-frozen-delivery-identity` | status-derived v2 consent relay and decline, exact non-authorizing delivery identity, clean authority inventory, release and byte/path drift controls | issue #2045 |
| `j51-unrelated-noop-authority-keeps-composed-delivery` | two approved delivered segments, recorded composed pre-PR span, unrelated clean approved no-op, identical composed span afterward | issue #2125 |
`j44` proves the linked checkout/common-dir topology and remote baseline before
review starts, then proves the staged delivery tree equals the corrected receipt
and `HEAD` is exactly one clean commit above upstream before pre-push runs.
`j45` constructs the canonical incident and exact maintainer authorization only
from negotiated status fields, then requires the global inventory to report the
predecessor as `superseded`, the approved successor as `recovered`, and the
inventory as complete and authoritative before selector-free post-apply runs.
`j46` proves the staged overlay is the exact authorized recovery target and
delivers only through its fresh approved successor. `j47` preserves one native
authority revision while review mode is toggled, proving that only discovered
governance steps aside and an explicit invalid receipt remains fail-closed.
`j48` keeps one-path correction evidence local while corrected and recovered
authority retain the complete two-path tree and manifest; exact pre-commit
targets allow, while later byte and path drift still fail closed. `j49` proves
that explicit and omitted CWD status calls reach the same clone-local switch and
the same archive-ready change without fabricating review authority. `j50`
executes only provider-emitted START and decline invocations, then proves the
unchanged staged candidate retains its base/tree/path identity without review
authority while release, byte drift, and path drift cannot inherit the decline.
`j51` records the composed pre-PR span across two delivered segments before an
unrelated clean no-op authority exists, then approves that no-op on a clean
worktree and requires the identical selector-free gate to allow the identical
span. Comparing the span, not just the verdict, is what makes it a regression:
a graph that admitted the no-op self-loop denied composition for every
unrelated lineage in the repository.
### SDD authority discovery controls (`journeys_sdd.go`)
Portable journeys 52 to 56 and 58 prove SDD chooses the sole exact approved
authority over stale history and fails closed for every public authority shape.
Each uses the public binary through the normal benchmark sandbox, not a
source-level proxy. The `j57` receipt-drift proof is source-coupled and is
listed with its axis below.
| ID | Flow | Source |
|---|---|---|
| `j52-sdd-stale-authority-does-not-shadow-approved-candidate` | newer approved same-path authority wins over stale history | issue #1893 |
| `j53-sdd-ambiguous-authorities-fail-closed` | multiple eligible authorities block selection | compact authority discovery contract |
| `j54-sdd-missing-authority-receipt-fails-closed` | a missing published receipt is not approval | compact authority discovery contract |
| `j55-sdd-mismatched-authority-receipt-fails-closed` | receipt bytes must match approved authority state | compact authority discovery contract |
| `j56-sdd-non-allow-post-apply-gate-fails-closed` | changed bytes cannot inherit an otherwise valid authority | compact authority discovery contract |
| `j58-sdd-foreign-openspec-path-fails-closed` | mixed OpenSpec paths cannot govern the selected change | compact authority discovery contract |
## Opt-in axes
Everything above this line is the core corpus, and the core corpus is
black-box. That property is what lets `--binary` point at any build, including
a release from before half the surface existed, and get a number rather than a
crash.
It also has a cost, and the cost took a community report to name: **a black-box
harness can only visit states the product agrees to construct.** Some states a
real repository reaches cannot be built by running commands, precisely because
the product validates on the way in and refuses to build them. Measuring those
means reading or writing something the product owns, and a journey that does
that is no longer black-box and no longer portable.
Rather than let that property leak into the whole harness and be explained away
in a footnote, it is confined to an **axis**: an opt-in extension that carries
its own declaration.
- **Nothing runs unless you name it.** Default is the core alone. `--axis all`
takes everything registered. An unknown name is a hard error, because
"57 core journeys" and "57 core journeys plus an axis" are different
measurements and a typo must never silently produce the first.
- **The core does not depend on any axis.** `rm bench/axis_damaged_store*.go`
leaves the corpus compiling, testing and reporting exactly the numbers it
reported before. That is the test of whether the seam is real, and it is worth
re-running whenever an axis grows.
- **The declaration is in the run, not in this file.** `results.json` carries an
`axes[]` block with each axis's name, `black_box`, properties and journey ids;
every `JourneyResult` carries `axis`; the table gains an `axis` column; and
the text report prints the properties in full under *Opt-in axes in this run*.
Someone reading a run's output can tell which journeys were black-box and
which were not without opening this document.
Adding an axis is adding one file with an `init()` that calls `RegisterAxis`.
The seam in `axis.go` is deliberately small — one registry, one flag, one report
section — and it does not know what any axis measures.
### `source-coupled` (`axis_source_coupled.go`)
The preserved `j57-sdd-authority-drift-during-discovery-fails-closed` fixture
uses the `bench_fixture` build tag to mutate its fresh sandbox receipt between
the product's immutable authority reads. That hook is intentionally absent from
ordinary binaries, so this is not portable black-box core coverage. Run it only
with `--axis source-coupled` and a product binary built with
`-tags bench_fixture`.
| ID | Coupling | What it tests |
|---|---|---|
| `j57-sdd-authority-drift-during-discovery-fails-closed` | tagged sandbox receipt mutation seam | authority reads must remain immutable during discovery |
### `damaged-store` (`axis_damaged_store.go`)
Journeys that start from a compact-v2 review store already damaged on disk.
`validateCompactRecoveryEdge` runs at write time on both `review recover` and
the compact transport import, so **no sequence of CLI commands produces a store
holding a recovery edge that does not re-derive**. A community tester reached
one anyway; reproducing it needed store bytes written directly, and once
reproduced it turned out to be a dead end. Real repositories reach states like
that through history — a store written by an older build, an operation
interrupted between two writes, a revision that drifted while something else
moved. Ours never do, because ours are minutes old.
| ID | Damage | What it tests |
|---|---|---|
| `ds01-two-recovery-edges-neither-admitted` | two edges, both with a correctly-prefixed authorization binding content the record no longer holds | the reported shape: `anomaly_classes: []` on both, and every advertised surface refusing |
| `ds02-damaged-edge-pristine-successor` | one such edge, successor never captured anything | the exit exists (`review abandon`) and nothing names it |
| `ds03-damaged-edge-successor-holds-results` | the same edge, successor holds a captured lens result | the pristineness rule refuses; correct guards composing into no way out |
| `ds04-recovery-edge-with-no-predecessor` | the predecessor entry is gone | a different path: the edge is never classified, only reported as dangling |
| `ds05-half-written-successor-record` | the record is truncated mid-write | a third path: the entry never parses, and the refusal names a continuation that cannot load it either |
**How each fixture stops lying when the format moves.** Authoring store bytes
couples these journeys to a persisted format instead of to a CLI, and the
failure mode is specific: the format moves, the bytes stop producing the state
the journey claims, and the journey keeps passing while measuring nothing. Two
checks stand in the way, and both fail the run loudly rather than degrading it.
1. **Every fixture reads its damage back out of the product** — `review
inspect-authority` or `review status` — and requires the product to report
exactly the damage the journey claims, before a single counted command is
spent. This is the same discipline the git fixtures already follow, and here
it is doing more work: it is also the drift tripwire.
2. **Before any edit, the record's own revision is re-derived from the bytes
just read** and required to equal the recorded one. The product's revision is
a SHA-256 over the canonical marshalling of the state, so reproducing it is
proof that this axis can still write bytes the product will accept. When the
marshalling moves this fails *first*, naming the reason, instead of a fixture
writing a store the product then rejects as a checksum mismatch — a symptom
that looks nothing like its cause.
The layout is derived from a store the fixture itself builds through the CLI,
never from the product's Go structs: what these journeys depend on is the
persisted format, and depending on it directly is what makes the drift visible.
Setup commands (`review start`, `finalize`, `recover`) run **uncounted**. The
operator whose friction is being measured did not run them; they opened a
repository whose store was already in this state.
### `real-world` (`axis_real_world.go`)
Journeys through repositories that are not sterile, and review lifecycles that
are not contiguous.
The detection-gap audit named the corpus's first two blind spots — unvisited
paths, unconstructable states. This axis exists because a community tester
named the third by hitting it: they ran the RC on a real production repository
and `review start` was walled by a nested git worktree, not gitignored, inside
the tree (`logical path is not canonical: ".wt/test/"`, exit 1, empty stdout —
issue #1881). No fixture had that shape, because **every repository in the
corpus is minutes old** — minimal, historyless, and touched by nothing except
the fixture that built it. Real repositories carry tool residue, and real
operators interleave git life with the review lifecycle instead of running it
back to back. Two families follow, and every journey names its family:
- **Family A — ecosystem clutter**: shapes produced by tools and by
accumulated sessions, not by the git operations the corpus runs.
- **Family B — life between commands**: the lifecycle interleaved with
ordinary git operations.
**This axis is black-box**, unlike `damaged-store`: every fixture is built
with git, the filesystem and the product's own CLI; every state is proven
through git or the product before a counted command runs; nothing
product-owned is read or written; the journeys are portable across builds the
way the core is. It is an axis anyway, for two reasons. First, a core run and
a core-plus-axis run are different measurements and must never look alike.
Second, it carries a standing rule the core does not:
> **Community-reported shapes become journeys: the reporter's fixture is the
> finding.** This axis exists because a tester's production repository was the
> fixture nobody wrote, and its job is to keep absorbing those — it grows at
> the pace of community reports, not at the pace of releases.
`rw01` is #1881 verbatim, measured honestly: a product fix is in flight, so
the journey may block today and clear tomorrow, and the axis records the truth
either way — once fixed it is the permanent pin. `rw08` and `rw09` rebuild the
two elements of the same reporter's published production composite that the
corpus could never have built from a fresh fixture: review state accumulated
across days of sessions, and a large gitignored binary inside the tree.
| ID | Shape | Family |
|---|---|---|
| `rw01-nested-worktree-not-ignored` | linked worktree at `.wt/test` INSIDE the tree, untracked, not ignored — #1881 verbatim | A |
| `rw02-node-modules-scale-untracked-tree` | 3,000 untracked files beside a docs candidate; STATUS must collect explicit exclusion before START | A |
| `rw03-untracked-env-with-secrets` | untracked `.env` holding a sentinel secret; explicit exclusion must not quote the value | A |
| `rw04-mutating-pre-commit-hook` | husky-style hook rewrites a tracked file during commit; bytes proven moved between review and commit | A |
| `rw05-dirty-submodule-gitlink-bump` | staged gitlink bump while the submodule's working tree holds uncommitted edits | A |
| `rw06-shallow-clone-depth-1` | `--depth 1` clone proven to hold 1 of 3 commits; pre-push derivation against missing history | A |
| `rw07-fork-topology-tracks-upstream` | `origin` + `upstream`, branch proven tracking upstream; cross-remote publication derivation | A |
| `rw08-three-stale-reviewing-lineages` | three lineages left in `reviewing` by prior sessions, proven inventoried, then a fresh fourth review | A |
| `rw09-ignored-15mb-binary` | 15MB gitignored binary proven invisible to git; ignored content must cost nothing and be cited nowhere | A |
| `rw10-rebase-onto-moved-main` | approve, commit, rebase onto moved main (id AND tree proven changed), then pre-push | B |
| `rw11-amend-identical-tree` | approve, commit, `--amend` (id proven changed, tree proven identical), then pre-push | B |
| `rw12-pull-into-reviewed-branch` | approve, commit, `git pull` proven to wrap the reviewed commit in a merge, then pre-push | B |
The fixture-proof discipline is unchanged and did real work here: `rw12`'s
first draft cloned the shared remote without naming a branch, the colleague's
commit landed on a different branch, and the pull under test was a no-op —
the ancestry proof caught it before the journey could pass while measuring
nothing. `rw03` and `rw09` add an assertion no other journey has: every
counted observation is scanned for a planted sentinel (the `.env`'s secret
value, the ignored binary's path), and the journey **fails naming the echo**
if it ever appears. The firing half of that detector is pinned in
`axis_real_world_test.go`, because a detector that could never fire would
make the passing half a tautology. `rw08`'s three stale lineages are built
through the product's own CLI, uncounted, for the same reason damaged-store's
setup is: the operator being measured did not run them — they opened a
repository that already held the residue.
**Rejected candidates**, because a journey that stresses no distinct product
path only makes the number look covered:
- **Start → `git stash` → pop → resume.** `stash pop --index` restores the
candidate byte-for-byte (provably: same `write-tree`), so every product
invocation after the detour meets a state indistinguishable from no detour.
The one distinct surface — a read-only `review status` against an absent
candidate — did not carry a journey once the production composite arrived.
- **Start → switch branch → switch back → resume.** Staged bytes carry across
the switch, j16 proves review identity needs no branch at all, and j15
proves two branches; every counted invocation would meet byte-identical
state.
- **`core.autocrlf=true` with CRLF working-tree content** (j26's ON case).
A config variant whose candidate is still the staged blob the receipt
already binds; the divergent working-tree operand only meets the product at
delivery staging, which `rw04` stresses harder with bytes that actually
moved. Displaced by the production composite.
- **A nested gitignored worktree** (also in the reporter's composite).
Ignored-path exclusion is pinned by `rw09`, and the worktree-ness of an
ignored path adds no operand the un-ignored `rw01` does not already pin.
- **Two remotes without upstream tracking.** A subset of `rw07`: the tracking
half is exactly what makes cross-remote derivation have to answer.
**What this axis still cannot reach.** Platform-specific clutter: Windows
Defender holding a file mid-write, macOS `.DS_Store` semantics and
Unicode-normalizing or case-insensitive volumes, Windows long paths. Same
verdict as honesty-contract entry 8 — they need a machine this harness cannot
build in a Linux temp directory, and naming them is honest where implying
coverage would not be.
## Layout
```
main.go run / record / analyze / compare / __shim dispatch
classify.go Observation, IsBlock, IsUnsupported, Classify <- the contract
metrics.go Dimension, BlockCounts, accumulator, aggregate
runner.go Sandbox, capability probe, journey engine
journeys.go the corpus, as data — guide flows and their failure paths
journeys_edge.go the edge-case part of the corpus, with self-proving fixtures
journeys_sdd.go the SDD remediation successor cycle, the kill switch against
SDD, and the recovery guard rails
journeys_wave1.go integrated community fixes exercised at their CLI boundary
axis.go the opt-in axis seam: registry, --axis selection, provenance
axis_damaged_store.go ONE axis, deletable: journeys starting from a store
damaged on disk. Not black-box; declares so itself.
axis_real_world.go ONE axis, deletable: cluttered repositories and
interleaved lifecycles. Black-box, and opt-in anyway;
community-reported shapes become its journeys.
record.go the recording shim and session log
analyze.go observed-mode metrics, same classifier
report.go plain-text tables and the comparison JSON
testdata/ recorded real gentle-ai output, used by the classifier tests
```
---
## File: docs/architecture/guard-population.md
# Guard population declarations
Guard population declarations make the accepted input set of selected production guards explicit at the check itself. The v2.2.1 contract covers ten evidenced guard families in `internal/cli`, `internal/reviewtransaction`, and `internal/sddstatus`.
## Review rule
A production Go guard qualifies when all of these are true:
1. It is in one of the three scoped packages.
2. It decides whether external, repository, filesystem, or persisted review state is legitimate for a security, integrity, admission, repair, or governance boundary.
3. It belongs to one of the registered families below.
Arbitrary control flow does not qualify. Shell and workflow guards are out of scope for v2.2.1.
| Family | Legitimate population under review |
|---|---|
| `shared-rar-owner` | Owners allowed to host shared Windows review authority |
| `darwin-search-ancestor` | Directory permission shapes allowed during secure ancestry traversal |
| `nested-worktree-scope` | Opaque nested repositories excluded from or admitted to review scope |
| `authority-repair-removal` | Damaged authority graphs on which one repair may proceed |
| `result-reopen-state` | Review states eligible to quarantine contaminated reviewer input |
| `convergent-lock-contention` | Lock contenders allowed to wait rather than fail immediately |
| `finalize-result-admission` | Reviewer-result sources allowed to govern finalization |
| `receipt-content-governance` | Terminal receipts allowed to govern delivered content |
| `persisted-sync-state-integrity` | Persisted sync state admitted before persona mutation |
Reviewers own identification of a new or omitted qualifying guard. The mechanism cannot derive the real-world population from source and MUST NOT be described as semantic completeness.
## Declaration contract
Place one declaration immediately above the `if`, `switch`, or `return` node that enforces the population boundary:
```go
// guard:population :
```
Use `too-tight` when drift is expected to reject legitimate inputs, `too-loose` when it may admit illegitimate inputs, and `fail-closed` when the important contract is safe refusal under uncertainty.
`TestEveryRegisteredGuardPopulationDeclarationMatchesProduction` AST-binds each declaration to the adjacent guard node. `.guard-population-baseline.txt` freezes the source path, family, direction, claim, node kind, and guard-node fingerprint. The test fails in both directions: a declaration missing from the registry and a registry entry missing from production are both drift.
After an intentional reviewed declaration or guard-node change, regenerate the registry before final verification:
```bash
GENTLE_AI_GUARD_POPULATION_UPDATE=1 go test ./internal/cli -run TestEveryRegisteredGuardPopulationDeclarationMatchesProduction -count=1
```
## Proof boundary
The contract proves declaration presence, AST adjacency, and exact registry agreement. It does not prove that the population claim is true, that every qualifying guard was identified, or that tests sample the outside world. Review must challenge the claim against production platforms, repository shapes, persisted states, and incident evidence.
---
## File: docs/architecture/organic-rdd.md
# Organic RDD — architecture and change record
> Technical reference for PR [#1801](https://github.com/Gentleman-Programming/gentle-ai/pull/1801). 154 commits, 340 files, +58,379 / −6,586. For the story behind it, see [the-organic-rdd-story.md](the-organic-rdd-story.md).
## 1. What changed at the top
Receipt-Driven Development used to be a control plane. A change was routed into a work-run, the run carried capabilities, and the capabilities decided ceremony. That plane was deleted (`feat!: delete the retired work-routing control plane`) and replaced by three ideas that fit in a paragraph each.
**Review happens after the candidate, not before the work.** There is no plan to approve, no run to open. You change something, and if it is worth reviewing, a review is offered on the exact bytes you produced.
**Tier is decided by evidence, never by size.** A thousand-line documentation change is tier 0 and gets no reviewer. Two lines touching authentication are tier 2 and get four. The classifier names its own reason, so the cost is never unexplained.
**The switch is a switch, and it starts off.** RDD is opt-in: until someone runs `gentle-ai review mode enable --scope global`, RDD does not exist — nothing blocks, nothing gates, delivery falls to ordinary repository policy. `gentle-ai review mode disable` returns to that same state. Turning it on re-validates from the current state rather than resuming stale obligations.
## 2. The lifecycle
```
review start ──▶ reviewing ──▶ validating ──▶ approved ──▶ gates
│ │ │ │
frozen reviewer verification receipt
candidate results evidence governs
```
Every transition is bound to an immutable candidate identity. Authority never advances on anything but the exact bytes that were frozen.
**`review start`** freezes the candidate, classifies risk, selects lenses, and creates the authority. It renders the frozen reviewer context *before* committing anything, so a candidate that cannot be expressed as reviewer work never becomes an authority.
**`review capture-result`** admits one reviewer result per lens, bound to the frozen subject hash. `gentle-ai review schema reviewer` emits the schema with a working example.
**`review finalize`** consumes captured results, then verification evidence, then reaches a terminal receipt.
**`review validate --gate `** is the delivery boundary. Gates are `post-apply`, `pre-commit`, `pre-push`, `pre-pr`, `release`. They discover and validate the same receipt and never launch reviewers.
## 3. The negotiated contract
Two output modes, selected by the presence of `--contract gentle-ai.review-integration/v1`:
| | human form | negotiated form |
|---|---|---|
| audience | a person reading a terminal | a tool driving the lifecycle |
| shape | prose refusals, exit codes | typed JSON envelopes |
| carries | the reason | the reason, the code, the next action |
**Mode divergence was the largest defect class in this branch.** The two modes emitted different data, the mode was selected by an invisible flag, and nothing told the caller. Four separate community reports traced back to it. It is now guarded: a conformance test fails CI when contract documentation names a schema, command, or field that no code emits, and a parity test requires the negotiated envelope to be at least as specific as the human surface for the same condition.
### Transitions are literally executable
`next_transition` no longer carries only a dotted operation name. Every argument carries its exact argv `token`, and an `execute` transition carries the complete `command`:
```json
{
"kind": "execute",
"execute": {
"operation": "review.start",
"command": "gentle-ai review start --contract=... --target=sha256:... --projection=workspace",
"arguments": [
{ "name": "target", "value": "sha256:...", "token": "--target=sha256:..." }
]
}
}
```
The verb is derived from `reviewIntegrationOperationRegistry`, the same table that already owned the mapping, so there is no second source to drift. Values carrying operator free text are POSIX-quoted, because `review repair` takes `--reason` and `--actor` and joined raw the shell split them into positionals every verb refuses.
A `collect` transition carries tokens too — its arguments *are* the flags of `review capture-result` — but deliberately no `command`, because `--input` points at an artifact that does not exist until a model has run the lens.
## 4. Recovery
The governing rule of this branch, stated once:
> **A message may name a command only if running that command resolves the block.**
Naming a dead end is worse than naming nothing. That rule is the reason several fixes here look larger than the defect that prompted them.
**Scope-changed** at `pre-push` has two classes. One completes assessment and derives diagnostics; the other errors during discovery and previously carried none. Both now name a recovery, and the named recovery is gate-conditional: at `pre-push` over an already-committed delivery, a bare `recover` freezes an empty successor that re-trips the same rule, so the denial names the committed base-diff shape with a derived merge-base rather than a remote ref that can move between reading and running.
One sub-case keeps the honest fallback on purpose: when committed content is byte-identical to what was approved and only commit topology changed, no single `recover` expresses it, and naming a two-step chain whose first step does not clear the gate would repeat the defect.
**Preflight refusals** in the negotiated envelope collapsed into one opaque code with an empty `required_inputs`. The specific reason now travels in `cause`, set once at the collapse point rather than at eighty call sites. A stale snapshot gets its own code and `next_action: review.status`, because `correct_request` is actively wrong there: no edit to the request makes a stale snapshot fresh.
**Git trust refusals** are typed rather than collapsed. gentle-ai never provisions `safe.directory` and never relaxes an ownership check; the fix is diagnostic only, and detection requires three independent signals from one failure so a miss degrades to the generic message and can never mislabel.
## 5. The kill switch
Three consultation points existed in the whole binary, two of them behind `if !negotiated`. Any caller passing `--contract` never received the escape at all, and `internal/sddstatus` consulted it nowhere.
It now reaches: the negotiated gate for every discovery kind, both non-stale ambiguous compositions, corrupted authority, mixed compact/legacy authority, the SDD remediation obligation, and the SDD archive gate.
Three invariants hold while disabled:
- **It never fabricates approval.** When no exact governing receipt applies, `disabled/unmanaged` keeps `allowed: false`. It exits 0 because it defers, not because it approved.
- **It never destroys information.** An outcome the gate could not decide says so and carries its typed cause.
- **An unreadable switch is not a disabled switch.** It resolves to managed, so a damaged or tampered mode record can never manufacture an unmanaged result.
Declining relayed consent creates no review lineage or receipt. Instead, it atomically records one canonical native candidate-decline authorization in the Git common directory, bound to the frozen candidate identity, trees, paths, modes, base, and untracked proof. With RDD still enabled, that record permits only exact `pre-commit`, `pre-push`, and `pre-pr` delivery under ordinary repository policy and reports `candidate_declined/unmanaged`; it never reports approval and never authorizes release. A changed candidate, base, path, mode, untracked set, publication range, or advertised head cannot inherit the choice. Replaying the exact decline recovers lost output, while corruption or multiple matching records fail closed. The prompt's off-path text still matters: decline is one candidate's unmanaged delivery choice, not the global kill switch, and every later candidate asks again.
## 6. Platform work
**Windows self-upgrade.** It never worked: the routing short-circuited to a binary strategy before the Go check. With Go on PATH it now upgrades through a pinned `go install`, verified by the Go checksum database — a different trust anchor than our minisign key, not a missing one. Linux and macOS keep the authenticated binary download, enforced structurally rather than by ordering: gentle-ai routes through a helper whose only go-install exit is gated on Windows, so declaring `GoImportPath` cannot revive the previously-dead generic rule on every platform at once.
On every platform, an upgrade now verifies that `go install` wrote where the user actually executes from, and names both absolute paths on mismatch.
**macOS.** Four defects had escaped because CI has no Darwin lane: `/var` path aliasing, `EPERM` under managed profiles, reviewer-result publication on ExFAT, and first-use store contention. All four are now fixed and, for the first time, verified on real hardware.
**Codex.** The permissions component stopped writing to `~/.codex/config.toml` entirely. It no longer injected a profile; what remained was a migration that removed the `default_permissions` pointer unconditionally while removing the profile table only when empty, so anyone who had customized that profile lost the pointer, kept the table, and Codex refused to start. Probing Codex directly showed every formulation of a surgical fix is also invalid, so the cleanup is gone rather than narrowed.
## 7. The guards
Ten defects in this branch shared one shape: **tests verified something was emitted, never that a consumer could act on it.** `review recover` is a real verb with real flags and the existence test passed, while the recovery it named dead-ended when run.
Four mechanical guards now cover that class, all derived from source rather than hand-maintained lists:
| Guard | What it proves |
|---|---|
| `TestPrePushScopeChangeNamedRecoveryReachesAllow` | reads the recovery out of the frozen diagnostics the denial carries, runs it, requires `allow` |
| `TestEveryNamedReviewContinuationIsStructurallyReal` | AST-walks refusal strings; every named verb and flag resolves against the real dispatch and `FlagSet` |
| mode parity in `review_preflight_reason_test.go` | every distinguishing token of the human refusal is recoverable from the negotiated envelope |
| `scripts/deadcode-ratchet.sh` | fails on a new unreachable function; the 230 already present are frozen |
| [guard population declarations](guard-population.md) | AST-binds ten scoped population claims to production guards and rejects exact registry drift |
The ratchet is a ratchet on purpose. Demanding zero before it could exist would have meant it never existed.
## 8. The friction benchmark
`bench/` is a separate Go module that drives a real `gentle-ai` binary through 36 end-to-end journeys and reports where the operator gets stuck. It is the evidence behind every friction claim in this branch, and it ships so the claims are reproducible rather than asserted.
```
cd bench && go run . run --binary $(command -v gentle-ai)
```
It classifies every block into exactly one class, and the split is the measurement, not the total:
| Class | Meaning |
|---|---|
| `in_band` | the refusal names a command that runs and clears it |
| `out_of_band` | the operator is stopped with nothing runnable named |
| `by_design` | a correct refusal for which no command can honestly exist |
| `dead_end` | nothing resolves it, anywhere |
| `self_recovered` | the flow continued with no extra command |
Two rules keep it from grading itself generously. Mechanical evidence outranks corpus annotation: a named runnable command classifies as `in_band` regardless of what the journey declared. And a `by_design` declaration costs a shape from a closed vocabulary plus the exact substring of the product's own next-action text, which is **verified present in the emitted bytes** before the exemption applies. A refusal with nothing to quote cannot be exempted, so an invalid declaration can only make a block look worse.
Two harness defects found by pointing it at itself are worth knowing about, because both produced plausible numbers:
- A lifecycle gate answering `disabled/unmanaged` at exit 0 was counted as an out-of-band block. It carries `allowed: false` because RDD is declining to express an opinion, not declining the delivery. The kill switch working was being reported as friction the product caused.
- A missing `GIT_TRACE` file was read as unobservable rather than zero, so one journey that legitimately spawns no git erased the subprocess total for the whole corpus.
`bench/README.md` carries the honesty contract: ten entries naming what the instrument does not measure, cannot measure, or measures with a known bias. Current known gap: `human_surface_bytes` varies by a byte or two across runs because `os.MkdirTemp` suffixes vary in length and two journeys quote that path back. Every classification and every count is stable.
## 9. Contract surface
`contracts/review-integration/v1` is published and digest-pinned. Three digests moved in this branch, each deliberately:
| Artifact | Change | Observable by a pinned consumer |
|---|---|---|
| `schemas/status.schema.json` | `command` property, required `token` on execute arguments, corrected `$comment` | added optional properties; strict validators still accept |
| `fixtures/status.fixture.json` | token keys on collect arguments | additive; `name`/`value` byte-identical |
| `schemas/failure.schema.json` | not pinned; gained `cause` usage | none |
`recovery_required_inputs` remains pinned at exactly six entries, which is why the gate-conditional recovery selectors are rendered in the human message and deliberately not projected into the negotiated envelope.
## 10. Known open
- **`review status` and `--next-transition` do not carry escalation numbers.** `finalize` and the gates do.
- **Reviewer-result authoring is discover-by-iteration strict.** `finding.lens` must be the unprefixed name; supplying the selector's own output string is rejected.
- **`max` reasoning effort does not exist.** The Codex effort type accepts `low`, `medium`, `high`, `xhigh`. Codex itself validates nothing, so an unknown value would be silently ignored rather than rejected.
### Resolved disabled-mode SDD behavior
The disabled-mode SDD limitations previously listed here were resolved in `v2.3.0` and remain resolved in `v2.4.0-rc.1` and `main`. When review is disabled, SDD status skips review authority, omits `reviewGate`, and pre-verify does not route to review. Archive proceeds under ordinary policy when `reviewGate` is absent; `reviewGate.result: allow` is required only for a present gate representing discovered review activity. Native lifecycle delivery gates remain separate and, when no exact governing receipt applies, report `disabled/unmanaged`.
---
## File: docs/architecture/the-organic-rdd-story.md
# The story of fixing RDD
> How two sleepless days, an entire community and three monthly resets turned into a release. For the technical detail, see [organic-rdd.md](organic-rdd.md).
## What RDD is, in one sentence
When you change something important, someone reviews it before it ships. That is all.
The hard part is not the idea, it is that it **must not get in the way**. A system that forces ceremony to change a comma gets uninstalled in three days. One that says nothing when you touch authentication is worth nothing.
## How it works now
You change something. The tool looks at **what** you changed, not how much.
- **Edited a README** → it asks nothing. Zero ceremony.
- **Wrote a thousand lines of documentation** → still nothing. Size does not matter.
- **Touched two lines of login code** → four reviewers.
And if you want none of it:
```
gentle-ai review mode disable
```
Done. It is off. **Not "off but still in your way"** — off. Do whatever you want, and if you turn it back on it tells you it is going to re-validate whatever was never reviewed.
---
## The part nobody tells
### It started badly, and not for the reason you would guess
The first version of this was built with Codex GPT 5.6 in ultra mode, and what came out was enormous. It was also not what needed doing.
The easy version of that story is that someone let a model loose and it went wrong. That is not what happened, and the real version is the one worth telling.
**The audit document was mine.** An agent wrote it, and I was behind every line: the facts it stood on, the reasoning, the architecture it described. Every decision that mattered was made by a person who had sat down and thought about it.
Which is exactly how I tell everyone to work. You direct, the agent executes, the human leads. I was not skipping that step. I was doing it.
And the agent built something else anyway.
It read that audit, saw it mention enterprise-level requirements, and **inferred** that HTTP support, remote execution and a whole infrastructure for large teams were needed. Nobody had asked for any of it. It deduced a need out of a document that was about something else, and then built all of it. Complete. Coherent. Well made.
Then all of it had to come out. Removing something large and well-built is harder than removing something broken, because **it looks like it works**.
That consumed **three monthly Codex resets**.
### Why it happened
Not because the thinking was skipped. Because nothing was holding the agent to it.
I was doing it the way the big companies do it: assemble the context, hand it to the model, trust what comes back. What I was not doing was using my own tool.
That is the whole argument for this thing, and I paid three resets to learn it concretely. **Good human decisions do not survive contact with an agent unless something enforces them.** A document that states the architecture is a suggestion, and an agent under pressure to be helpful will read a suggestion as a starting point. Phases with explicit contracts, one writer per lane, verify before asserting, and a failing existing test that stops the work are not suggestions.
The architecture was never the problem. The absence of anything making it binding was.
### The second run
Same model class. Same person. Same decisions. What changed was that the decisions were now enforced by **phases with explicit contracts, one writer per lane, verify before asserting, and the rule that a failing existing test is never edited — you stop and report.**
That last rule alone caught **nine wrong premises**. Nine times an agent was about to fix something, an old test went red, and it turned out the test was right and the diagnosis was not.
After two days of working flat out, I am still at **66% of the weekly limit**. The difference was not the model. It was the method.
---
## What the community found
This is the part I like most.
I shipped a pre-release and people broke it. In the good way.
**@Wladimirfn, @Denver2828, @MarsSall and @Freedom2828** reported the same failure from four angles. It looked like a Windows bug. It was not: it happened when the reviewed commit had already been published. Denver2828 reached the same diagnosis independently, building the branch with print statements, and **his patch was identical to mine, line for line**.
**@ElCaaarnal** typed a flag by hand and hit something I had announced as fixed. He was right: I had fixed the tool so it stopped *printing* the broken form, not the parser so it would accept it. **The changelog overclaimed and he lost time to it.**
**@ardelperal** reported a command exiting successfully when it should have failed. I investigated: it was a measurement trap. In bash, `$?` gives the status of the *last command in the pipeline*, not the binary. His report was not a bug, but it documented a trap that would have cost the next person an afternoon.
**@Blue-XL** found that a deliberately forged authorization was accepted and stored in the audit record as though genuine. Worse than having no field: an absent authorization is honestly absent, **a wrong one lies**.
**@AlbertGC13** found two things on Windows with a rigour worth copying: he separated explicitly what he had tested from what he had only read in the code, and **stated what he was not claiming**. He found a Git permissions refusal being turned into advice that could not possibly be followed.
**@edwinsaavedran** showed that four macOS defects had escaped because CI never runs on Darwin, and built the case with a link to each one.
**@Matere413** found that a reviewer result my own agents produce is rejected by my own admission, because two of my documents disagree about the required shape.
**@Andiveli** found the kill switch's hardest case: reviews off, but old approved receipts still lying around in the store. It used to fail with `authority_corrupted`. He came back on the next refresh, reran the exact scenario, and confirmed the fix reports the state without faking an approval.
**@decode2** did not report a bug, he reported a **deadlock**. A corrected candidate that passes verification and holds an approved review still could not finish its remediation, because finishing needs a distinct successor, creating a successor needs an invalidated predecessor, and invalidating is refused for a healthy approved one. Three rules, each defensible alone, forming a closed ring. He reproduced it against the current head rather than an old tag and wrote out the cycle edge by edge.
**@danielxxomg** went looking for what a well-behaved tester would not do. He killed `review start` mid-flight with `kill -9` and then checked whether the store came back clean, whether the lock leaked, whether anything was corrupted. It recovered. That case is nowhere in my own corpus.
**@AndySabina** ran the guide twice across refreshes on WSL2, published the SHA-256 of the exact binary tested both times, and, when the second round found nothing, **said so and opened nothing**. A clean report that stays quiet is worth as much as a defect and is much rarer.
**@frankirova** passed all eight flows and then did the thing almost nobody does: he stated what he had **not** covered. He noticed the asset he downloaded was built from a tag two commits behind the branch, named both commits, and reasoned about whether they touched the paths he was testing. A result is only as good as its stated scope.
**@ftorga** re-validated behaviour-first against the tag and the live head, found two gaps still reproducible, and linked each one to the specific comment where it had been validated before, so nothing had to be taken on trust.
**@MarcosArispe, @dnlrsls, @GinoL221, @orlo-dragomir, @lu149e, @salema97, @diegofercho21323, @blickcbot, @Deco** and several more kept testing refresh after refresh.
None of those findings came from an internal audit. **They came from people using the tool.**
---
## The audits: the ones that worked and the ones that did not
### The ones that worked
The mechanical ones. The ones derived from the code rather than from a list someone has to remember to update.
One walks the syntax tree looking for error messages that name a command, and checks that the command and its flags **actually exist**. It found messages pointing at things that were not there.
Another rejects new functions nobody calls. When I removed the Codex cleanup, it told me **fifteen functions** had gone dead — an entire parser that existed only for that. I deleted them following that evidence.
That guard was eight hours old when it found its first real defect.
### The ones that did not
The ones that verified something was **emitted**, never that it was usable.
The perfect case: there was a message telling you "to get out of this, run this command". There were tests. They verified the message was emitted, with its exact text. All green.
**Nobody had ever run the command the message named.**
When I ran it, it did not work. I had been sending people into a dead end, with green test coverage, for months.
That is where the rule governing everything else came from:
> **A message may name a command only if running that command resolves the block.**
Naming a dead end is worse than naming nothing.
---
## The benchmark
At some point I stopped arguing about whether it was better and measured it.
The tool counts how often you get stuck and, above all, **how** you get stuck:
- **In band** — it stops you and tells you what to run
- **Out of band** — it stops you and tells you nothing
- **Dead end** — it stops you and there is nothing you can do
It does not measure speed. Speed depends on the provider and the day; friction is yours.
And it ships. It is in the repository, it drives a real binary rather than a mock, and you can point it at your own build:
```
cd bench && go run . run --binary $(command -v gentle-ai)
```
That is deliberate. A number I publish and you cannot reproduce is a claim about my honesty. A number you can run yourself is evidence.
First measurement: **six blocks, every one of them out of band.**
Then the corpus itself was the problem. Fourteen journeys, all drawn from the testing guide, so it measured the paths a tester already walks and nothing else. Twenty-two more went where those never did: bare repositories, linked worktrees, a merge left half-finished, a file with no bytes in it, the switch flipped in the middle of a review.
Latest, over all thirty-six: **zero dead ends. Fifteen blocks in band, three out of band, two declared correct by design.**
The block total did not move. That is the point, and it took a while to see: the tool does not stop you less than it did. It stops you exactly as often, and now it tells you how to get out.
Two things that measurement taught me about measuring. The first run after a round of fixes produced numbers identical to the run before it, which is what made me look: the build had failed silently and I had measured the old binary. The second was worse. The analyzer was counting the kill switch **working** as friction the tool caused, because a gate that hands delivery back to ordinary policy still reports `allowed: false`. It says so in the same breath, in its own words, and nothing was stopped. An instrument that flatters you is useless; one that convicts you of the wrong crime is worse.
A tester said it better than my own tool: *"it communicates the state correctly, but proposes no continuation command"*.
---
## The loop
Once the benchmark existed, the obvious move was to stop reading it and start running it. Measure, fix what it found, rebuild, measure again. Keep going until the number stops moving.
That is a small idea with one sharp edge: **the loop only works if you are allowed to find that the instrument is wrong.** A loop that can only ever fix the product will happily converge on a lie.
Round one was tidy. Two blocks named a maintainer when the operator could have cleared them alone, and in both cases the correct reason was already sitting in the JSON — computed, published for machines, and thrown away on the line a human reads. Two others turned out to be refusing correctly, which was worth the same as fixing them, because now it is written down why.
Then the corpus grew to thirty-six and round two found the one I would not have found on my own.
**The kill switch did not stop anything from being written.** Turn reviews off, and `review start` refuses, correctly. Run `review finalize` and it returns success, state approved, terminal receipt on disk. A review had been approved with reviews switched off.
The cause was almost funny. The authorization function was correct. It had two modes, one for starting and one for advancing, and the advancing one was documented, in the source, as *"Disabled mode rejects it"*. It was called from nowhere. One production caller, always passing the other mode. **The guard was written, reasoned about, commented, and never wired up** — which is exactly why a unit test on it passed happily the whole time.
And it mattered beyond tidiness, because the promise is that turning reviews back on re-validates whatever was never reviewed. A receipt minted while the switch was off survives being turned back on, and nothing re-validates it.
Round three, I broke my own measurement. I built the binary with the wrong package path, the build failed, the `&&` did not stop the run that followed, and the benchmark measured the previous binary. The tell was that the totals came back **byte-identical** to the run before. Numbers that do not move after real changes are not a comfort, they are a symptom.
Round four found the same defect for the third time on this branch. The specific reason computed, published to the machine envelope, discarded on the human path. Three separate places, months apart. That stopped being a bug the second time; the third time it is a shape, and it is now written down as one.
That round also caught something more embarrassing than a bug: an escalated review told the operator to `run review.status`. It looks like a command. It is the internal routing name, and typing it does nothing. Worse, when I first fixed it I was about to translate it into the real command — and the agent doing the work proved that the real command **also** resolves nothing there. It describes the state; it does not move it. Naming a command that runs and does not help is more expensive than naming nothing, because now the person trusts it.
The loop is still running. That is not a failure to converge; it is what a loop looks like while it is honest.
---
## The mistakes I made
Because if this is going to be honest, it goes in whole.
**I wrote guide steps without running them.** Three times. A tester followed them, they did not work, and reported the failure. A new rule came out of that: before naming a continuation, execute it.
**I turned a finding into a documentation patch.** Three different testers could not complete a flow. Instead of taking that as the data it was, I wrote the recipe into the guide. The maintainer called it out: doing that **destroyed the measurement** and hid the defect. I reverted it. The real defect was that the tool had a command emitting exactly what was needed, and no path led to it.
**I staged a file without reading its diff** while an agent was writing in it. I swept up 154 lines of someone else's half-finished work and pushed a branch that did not compile. I have ratchets, guards, and tests that demand commands work. **None of that protects you from a hasty `git add`.**
**I chased a defect that was my own measurement error.** I wrote a command's output inside the repository I was measuring, which added a file, changed the state, and the system correctly refused. I lost an hour. But something good came out: that refusal explained nothing either, so I fixed it, and it is now documented as a trap in the guide.
---
## Where it landed
The four macOS defects: closed and verified on real hardware, not on a synthetic profile.
Windows updates itself for the first time.
Codex used to start up broken after syncing, and now it **does not touch its configuration file at all** — verified with the same inode number before and after, meaning it is not opened for writing at all, not merely that the same bytes get written back.
The kill switch is a kill switch.
And things remain open, written down in the technical document, because an honest list of what is missing is worth more than a release claiming everything is done.
---
## What I learned
**Good human decisions do not survive contact with an agent unless something enforces them.** I directed the audit, the reasoning and the architecture, and the agent still built something nobody asked for. Directing well is necessary and it is not sufficient. That gap is the entire reason this tool exists, and I paid three monthly resets to find out I needed my own.
**A test that verifies something was emitted does not verify it is usable.** That distinction explains nearly every defect in this branch.
**A guard nobody calls is not a guard.** The kill switch had one written, documented, and reasoned about, refusing exactly what it should have refused, wired to nothing. Every unit test of it passed. Correct code that is never reached is indistinguishable from code that was never written, and it is more dangerous, because it reads like coverage.
**Point the instrument at itself first.** The benchmark spent a round counting the kill switch working as friction the tool caused, and a round measuring a binary I had failed to build. Both times the number looked plausible. A measurement you cannot audit is an opinion with decimals.
**Dead code that is still documented is a lie.** There was a function that installed dependencies. Nothing called it. The docs said the tool installed dependencies. A Linux user read that and expected it to work.
**Over-engineering is harder to remove than a bug.** A bug is visible. An entire architecture nobody asked for, well built and coherent, defends itself.
**The community finds what audits do not.** The four most valuable reports of these days came from people using the tool on their machine, with their repository, with their odd configuration. No internal audit would have found them, because an audit looks for what you already know to look for.
**And the rule that ran over everything else:** if you tell someone what to do, make sure it works.