The Gate Model
no-mistakes intercepts pushes by placing a local bare git repo between your
working repo and the configured push target. That bare repo is the gate.
The point is not to hide Git. The point is to create one deliberate place where validation can happen before a branch is shared.
Architecture overview
flowchart TD repo["Working repo"] -->|"git push no-mistakes"| gate["Local bare gate repo"] gate --> admission["pre-receive admission"] admission --> hook["post-receive notification"] admission --> daemon["Daemon"] hook --> daemon daemon --> worktree["Disposable worktree"] worktree --> pipeline["intent -> rebase -> review -> test -> document -> lint -> push -> pr -> ci"] pipeline --> target["Push target"] daemon --> db["SQLite state"] daemon --> ipc["IPC socket"] ipc --> tui["TUI clients"] ipc --> axi["AXI clients"]
What no-mistakes init does
When you run no-mistakes init in a repo:
- It creates a local bare gate repo under
~/.no-mistakes/repos/<id>.git. - It installs a
pre-receiveadmission hook and apost-receivenotification hook in that gate repo. - It enables Git push options for the gate repo.
- It best-effort isolates the gate repo’s hooks path from shared local Git config writes when Git supports
config --worktree. - It adds a
no-mistakesremote to your working repo that points at the gate. - When
--fork-urlis supplied, it records that GitHub fork as the branch push target while keepingoriginas the parent repository used for PR bases. - It installs or refreshes the
/no-mistakesagent skill at user level, into~/.claude/skills/no-mistakes/SKILL.mdand~/.agents/skills/no-mistakes/SKILL.md, on a best-effort basis, following existing symlinks between the home.claudeand.agentsskill directories. It writes no skill files into the repo; if the repo still carries a vendored copy from an older version,initprints a notice that the copy can be removed. - It makes sure the daemon is running so incoming pushes can start runs.
init is idempotent.
If the repo is already initialized, it refreshes the existing gate instead of failing: managed hook installation, push-option support, hook-path isolation, gate and working remotes, origin/default-branch metadata, and the /no-mistakes agent skill are repaired or updated where needed. A no-mistakes remote that is not this home’s own gate wiring is never rewritten; see no-mistakes init for when init refuses it.
If the working repo was renamed or moved and the old path no longer exists, init reattaches the existing gate from the leftover no-mistakes remote, updates the stored working path, and preserves the repo ID plus run history.
A checkout is one repository however its path is spelled: when no stored working path matches exactly, lookup falls back to filesystem identity (os.SameFile), so a case variant on a case-insensitive volume or a symlinked spelling resolves to the registered repository from the main checkout and from any linked worktree, and init refreshes it instead of registering a second one. The stored path keeps the spelling it was first recorded with. Directories that differ only in case on a case-sensitive volume stay separate repositories.
If the working repo was copied and the original path still exists, init treats the copy as a new repo and repoints the copied no-mistakes remote to a fresh gate.
If daemon startup fails during a refresh, init reports the error but does not eject the pre-existing gate.
After init, your original origin still points at the real upstream remote.
With --fork-url, that origin should be the parent repository, and the fork URL is stored separately for branch pushes.
That is a core design choice, not an implementation detail.
How a push flows
- You run
git push no-mistakes <branch>. - The gate repo’s
pre-receivehook asks the daemon to admit the update before Git changes any gate ref. An active validation-step descendant is refused, including a direct push that bypasses the CLI. - Git writes an admitted push into the local bare gate repo.
- The gate repo’s
post-receivehook notifies the daemon. - The daemon creates a detached worktree for this run.
- The nine core steps run in order:
intent -> rebase -> review -> test -> document -> lint -> push -> pr -> ci. Repository gates, when configured, run immediately after their anchors. - If a step pauses, you can attach with the TUI or use
no-mistakes axi respondto approve, fix, skip, or abort. Useno-mistakes axi abortonly when you mean to cancel the whole run. AXI run objects showawaiting_agent: parked <duration>while a non-terminal run is parked at that gate, so a supervising agent can distinguish a waiting run from active work in one status read. While a step is actively running or fixing, AXI run objects can also showactive_stepswith the active duration, latest activity, native agent PID, and current execution or fix round. - After local checks pass, the push step forwards the branch to the configured push target only after verifying that the update will not discard unincorporated commits already on that target, and the PR step creates or updates the pull request.
For GitHub fork routing, the push target is the fork and the PR base repository is the parent from
origin. - The CI step keeps watching the open PR until it is merged, closed, or its configured idle timeout elapses with no base-branch movement, and can auto-fix failures or merge conflicts when supported. While it watches, the TUI and terminal title surface a
Checks passedsignal once checks are green and the PR is mergeable (or the trusted default-branch config declaresno_ci: trueand no checks are registered), andno-mistakes axireturnsoutcome: checks-passedwith instructions to summarize the run and list any pipeline fixes, so agents stop and ask you to review and merge it. An empty forge response without that declaration stays not-ready. Underci.release_after_checks_passthe step instead records readiness and finishes at that verdict, so the run completes aschecks_passed, releases its worktree, and leaves the merge decision - and any later observation of it - to you.
Key design decisions:
- Named remote -
originis never hijacked. You push tono-mistakeson purpose, so regulargit pushstill works normally. - Recursive-run containment - managed gate identity and authenticated daemon peer ancestry prevent active validation steps from starting or controlling another pipeline.
NO_MISTAKES_GATEis diagnostic evidence only, not authorization. - Disposable worktrees - each run happens in its own detached worktree, under
~/.no-mistakes/worktrees/by default or under the directoryworktree_rootsnames for that repository. The daemon can safely modify files, run tests, and commit fixes without touching your working directory. - Fixed pipeline - the step order is opinionated and not configurable:
intent → rebase → review → test → document → lint → push → pr → ci. What you can configure is the commands each step runs, how many auto-fix attempts are allowed, whether transcript-based intent extraction is used when intent is not supplied directly, and extragatesthat run after a core step - additions only, never a removal or a reordering. - Remote data-loss guard - force-pushes are checked against the live push target and refused when they would discard commits the run did not incorporate.
Why it is built this way
Named remote
The remote is explicit because trust matters. no-mistakes is an opt-in gate,
not a trap door that silently rewires normal Git behavior.
Bare gate repo
The local bare repo gives Git a normal place to receive pushes. A managed
pre-receive hook asks the daemon to authorize the update before any gate ref
changes, and a post-receive hook hands an admitted push off to the daemon.
Git operations on the gate name the bare repo explicitly with --git-dir
instead of relying on working-directory discovery, so hardened environments
that set safe.bareRepository=explicit (common in agent harnesses and CI)
work unchanged.
Private mirror reconciliation
A rebase can leave the gate branch on an older history that rejects the next ordinary push. Reconciliation compares exact commit heads and final-tree survival; commit messages and per-commit patch identities are not evidence. A clean three-way merge of the private head into the live head whose resulting tree equals the live tree proves every private commit contained, so a rebased copy whose patch ID changed only because the new base edited nearby context lines is not at risk, while changes discarded by a merge or revert are never counted as surviving content.
A rebased branch whose later commits edit the lines the private commits wrote (for example a new run on a branch an earlier run already published) makes that merge conflict although nothing was dropped. Survival is then proven in two halves instead: merging with every conflicting hunk resolved toward the live head must still leave the live tree unchanged, so a private hunk that merges cleanly - content the live head reverted or never had - still refuses; and a commit on the live head’s first-parent history touching the private paths must pass the clean-merge check, placing all of the private content in the live history. A side branch discarded by a merge never serves as that point. Both halves prove the whole private head, so a rebased copy whose patch ID drifted with context and whose lines a later live commit edited is not at risk either. At most 64 first-parent candidates are tried; past that bound the proof fails closed.
If survival cannot be proven, the whole private-only range is reported as at risk.
Recovery-anchor preservation credit (issue #1233): a private-only commit
that a refs/no-mistakes/recover/<run> anchor reaches is preserved, so it is
re-derived out of the at-risk set. Those anchors are the tool’s own sanctioned
preservation record - terminalization pins every verified unpublished head at
that ref before the managed worktree can be removed, and
custody.PreserveRecoveryAnchor never replaces evidence (a conflicting or
symbolic anchor fails closed and is never dereferenced). The credit exists so
the sanctioned sync --recover -> rerun -> push loop cannot deadlock against
the very anchor that loop wrote to declare the work preserved.
This is a preservation credit, not containment evidence. It says nothing about whether the content lands in the published tree; ancestry and final-tree survival remain the only proofs of that, and Decision 41-A below remains the only head exception. The archive-before-delete contract is unchanged, so a credited reconciliation still archives the exact private head before removing the branch ref. A commit no anchor reaches is still refused, and the refusal names every proof that would clear it.
The credit scan never walks what the anchors can see. Recovery anchors
accumulate for the life of the gate, so each membership scan is a git rev-list of candidates that excludes the live head as well as every
anchor; because the candidates are private-only, that walk is bounded by the
private-only range. Only credited commits are then attributed to the first
anchor that reaches them. When no recovery anchor exists no credit is possible
and the ordinary at-risk refusal is returned. Otherwise the candidates are
scanned in successive batches of maxRecoveryCandidates (in
internal/gate/reconcile.go), so a long, fully anchored history still earns
its credit; the scan is never truncated or refused by size, because dropping
candidates would drop their preservation credit and silently reintroduce the
deadlock the credit exists to end.
Accepted Decision 41-A (issue #983): pipeline publication may replace a
private mirror head that is exactly equal to a head the publishing run itself
placed on the mirror without tree-survival proof: its
Run.SubmittedHeadSHA, or, once it has published, its durable
Run.LastPushedSHA. The last pushed head is recorded only after a verified push to the configured
push target and mirror settlement, so it is never an external or newer head.
This narrow policy exception permits reviewed rebases and conflict resolutions
to change the submitted patch, including a CI merge-conflict repair that is
revalidated from Review after the run has already published. The replacement
head must still be review-approved, and the force push to the configured target
stays leased on the same last pushed head. Ownership is not containment evidence. The
exception does not extend to another recorded head, an agent-created head, an
abbreviated SHA, or an external, newer, or divergent private head. Fresh AXI
submissions do not receive this exception.
Reconciliation requires direct private branch and archive refs; symbolic refs,
including dangling symbolic refs, are refused before containment checks. Ref
creation and deletion use exact names without dereferencing and expected old
values. Before deleting a reconciled branch ref, the gate archives its exact
head at refs/tags/no-mistakes-abandoned/<branch>/<sha>. Outside Decision 41-A
and the recovery-anchor credit above, unproven private content refuses before
upstream publication, leaves the private branch untouched, and names every
at-risk commit. An ancestor already supports an ordinary fast-forward. A gate
head that is a newer descendant of the published head stays untouched,
including through the detached worktree’s shared branch refs.
Correction and CI-repair recording persist the agent-created worktree head in the run and database without moving a branch ref shared with the gate. Repairs awaiting review retain their uncertified range. Publication reconciliation therefore still sees any intervening private head; ordinary worktrees with separate ref storage retain their local branch bookkeeping.
Publication plans reconciliation before pushing and applies it only after verifying the upstream head. If mirror settlement then fails or is cancelled, it restores the archived branch when no intervening ref has appeared, so a retry can still resolve the branch. AXI reconciles before its ordinary submission push and restores an archived ref after a failed submission if no intervening ref has appeared. Neither path forces the private mirror.
Daemon
The daemon owns long-running work: creating worktrees, running the pipeline, streaming events, tracking state, and recovering from crashes. Without it, the CLI would need to stay attached to every run.
Disposable worktrees
The worktree is where no-mistakes can safely rebase, run commands, let the
agent edit files, and commit fixes. Your day-to-day working tree stays clean.
Component overview
Receive hooks
Before Git changes a managed gate ref, the pre-receive hook asks the daemon
to authorize the pushing process. The daemon refuses descendants of an active
validation step before mutation, including direct pushes, and safely omits run
or phase details when authenticated ancestry cannot identify them uniquely.
An existing custom pre-receive hook is preserved and runs after admission.
When git push no-mistakes <branch> lands, the bare repo’s post-receive hook
fires. It resolves the gate to an absolute bare-repo path using Git’s own view
of the repository, falling back to the hook location if needed, then calls
no-mistakes daemon notify-push with that gate path, ref name, old/new SHAs,
and any Git push options such as no-mistakes.skip=test,lint.
For compatibility with older managed hooks, notify-push also normalizes
relative gate paths before handing them to the daemon.
The post-receive hook never blocks an already admitted push - Git ignores its
exit status - but notification failures are surfaced to the pushing client on
stderr and appended to notify-push.log in the bare repo for later inspection.
Daemon
A long-running background process that manages pipeline runs. It:
- Listens on a Unix socket at
~/.no-mistakes/socket - Writes its identity record to
~/.no-mistakes/daemon.pid - Holds an exclusive OS lock on
~/.no-mistakes/daemon.lockfor its whole lifetime, so only one live daemon can own anNM_HOMEat a time - Serializes concurrent pushes to the same branch (new push cancels the in-progress run)
- Creates and cleans up worktrees
- Scopes configured commands and one-shot agent subprocesses to the step lifetime by terminating remaining child processes on completion, failure, or cancellation
- Persists state to SQLite
- Streams events to connected TUI clients via IPC
The installer prefers setting up the daemon as a managed background service, and no-mistakes, init, attach, rerun, and update make sure the daemon is running when needed.
Bare no-mistakes then attaches to the active run on the current branch when one exists, or routes to the setup wizard when it needs to create a new branch/run.
If managed service install or startup is unavailable or fails, startup falls back to a detached daemon process.
update resets the daemon after replacing the binary when the daemon is running or stale daemon artifacts exist.
If the daemon is already running from a different executable path, update prompts before replacing it.
If the daemon executable path cannot be determined, update aborts before replacing anything.
You can also manage it explicitly with no-mistakes daemon start|stop|restart|status.
Daemon & Worktrees
owns the active-run guard, the scope of --force and --yes, and recursive
validation-step containment for lifecycle commands.
On startup, the daemon validates crash-recovery state before resuming work. Daemon & Worktrees owns the exact restart, parked-gate reconciliation, cleanup, and fail-closed behavior.
Pipeline executor
The executor runs each step sequentially and manages the approval/fix loop. It
can also end early after rebase if the branch has no diff against the default
branch, marking the remaining steps as skipped.
- Execute the step
- If the step finds
action: auto-fixfindings, the step result is auto-fixable, and auto-fix is enabled, loop back with the agent to fix them (up to the configured limit) - If blocking findings remain, any finding has
action: ask-user, or Review has a selected finding still awaiting positive rereview verification, pause and wait for user action. The Review carry-forward rule is detailed in the pipeline-step reference. action: no-opfindings are informational only unless a Review finding was selected for fixing and remains outstanding; the user can approve, fix selected findings, skip, or cancel the run when the step pauses
While the executor is paused at an approval or fix-review gate, it persists a run-level awaiting-agent timestamp that AXI renders as awaiting_agent: parked <duration>.
That timestamp is observability only and does not alter approval behavior.
When the wait ends, it atomically clears the marker and adds the elapsed wall time to the run’s local parked-time total, so a crash cannot leave that time undercounted.
While a step is running or fixing, the executor also records the latest meaningful step activity from log lines and native subprocess lifecycle events.
AXI renders that activity in active_steps, including a quiet prefix when no activity has arrived for longer than the configured step_quiet_warning.
IPC
Communication between the CLI and daemon uses JSON-RPC 2.0 over the Unix socket. The subscribe method streams real-time events (step progress, log chunks, findings) to the TUI, while the axi commands use request/response IPC for non-interactive agent control.
Database
SQLite at ~/.no-mistakes/state.sqlite tracks repos, runs, step results, step rounds, derived intent summaries, local agent invocation performance, and the minimum session metadata needed to resume review-loop roles.
Step rounds record each execution attempt (initial, auto-fix) with its own findings and duration, plus selected finding IDs, whether the selection came from the user or auto-fix filtering, the merged finding payload actually sent to the fix agent for that round, and the one-line fix summary for fix rounds.
Step results also store the last active timestamp, last activity text, native agent PID while a subprocess is active, and the effective auto-fix limit used by AXI status.
That merged payload can include per-finding user notes and user-authored findings from the TUI or AXI interface.
Intent stores the summary, source, session ID, and match score on each run when transcript matching is used, plus cached summaries for matching transcript sessions.
An agent-supplied AXI intent is stored directly on the run.
Raw transcript text is not stored in this database.
Legacy user_fix rounds are still read as auto-fix for backward compatibility.
Run records also store the nullable awaiting_agent_since timestamp used only to render the AXI parked signal while a gate is waiting for the driving agent, plus accumulated parked_ms for local performance reporting. For version-specific debugging, inspect runs.no_mistakes_version and runs.no_mistakes_build_sha: each new run records the version returned by internal/buildinfo.CurrentVersion() and the internal/buildinfo.Commit build SHA embedded through release -ldflags, the same identity shown by no-mistakes --version. Historical rows remain NULL.
Each agent invocation records local-only purpose, provider/model metadata, session mode and a truncated session-identity hash, timing, failure category, and token usage; prompts, outputs, diffs, and credentials are never stored there.
Use no-mistakes stats --agents for aggregates or no-mistakes stats --run <id> for a run timeline and parked time.
Repo records store the parent upstream_url and an optional fork_url; branch pushes use fork_url when present, while PR and CI provider context stays anchored to the parent.
At the start of each run, no-mistakes best-effort refreshes those URLs from the working clone without changing any clone or gate remote.
origin is the upstream authority, and an existing fork registration is refreshed only when exactly one other clone remote identifies the same fork repository.
The two registered URLs are replaced atomically after validation; an unreadable, invalid, credential-bearing, or ambiguous remote, or a database failure, leaves the exact prior registration in place and does not stop the run.
Local state
Everything lives under ~/.no-mistakes/ by default. Set NM_HOME to relocate it.
| Path | Contents |
|---|---|
state.sqlite | SQLite database |
socket | Unix domain socket for IPC |
daemon.pid | Daemon identity record |
daemon.lock | Singleton lock; the OS lock a live daemon holds so a second daemon for the same root cannot start |
config.yaml | Global configuration |
update-check.json | Cached update check result |
servers/ | PID-tracking records for managed agent servers |
repos/<id>.git | Bare gate repos |
repos/<id>.git/notify-push.log | Persistent hook notification failure log |
worktrees/<repoID>/<runID>/ | Run worktrees, subject to cleanup and retention rules; worktree_roots can place a repository’s elsewhere |
logs/<runID>/<step>.log | Per-step log files |
logs/daemon.log | Bounded daemon lifecycle log |
logs/daemon-bootstrap.log | Bounded pre-logger bootstrap and direct crash output |
logs/managed-server.log | Bounded stdout and stderr from daemon-managed Rovo Dev and OpenCode servers |
logs/wizard-agent.log | Managed agent-server output captured during setup wizard runs |
logs/cli.log | Caller attribution (PID, parent PID, parent command line) for daemon stop, daemon restart, and update invocations |
New repo IDs are the first 6 bytes (12 hex chars) of sha256(absolute_working_path).
When an initialized working repo is renamed or moved, init preserves the existing repo ID instead of deriving a new one from the new path.