This file is for agentic coding tools working in this repo.
This repository is a Go CLI app named no-mistakes.
The binary entrypoint is cmd/no-mistakes.
Most implementation code lives under internal/.
Environment
- Go version:
1.25.0fromgo.mod - Build tooling: standard Go toolchain plus
Makefile - CLI/UI libraries:
cobra,bubbletea,bubbles,lipgloss - Database: SQLite via
modernc.org/sqlite
Primary Commands
- Build with release metadata:
make build - Plain build:
go build -o ./bin/no-mistakes ./cmd/no-mistakes - Install locally:
make install - Cross-compile archives:
make dist - Run unit/integration tests:
make test - Run unit/integration tests directly:
go test -race ./... - Run end-to-end tests:
make e2e - Re-record end-to-end fixtures:
make e2e-record - Regenerate the committed agent skill:
make skill - Run skill drift check and vet:
make lint - Run vet directly:
go vet ./... - Format all Go files:
make fmt - Format directly:
gofmt -w . - Check formatting only:
gofmt -l . - Clean build output:
make clean
Single-Test Commands
- Run one package:
go test ./internal/cli - Run one package with race detector:
go test -race ./internal/cli - Run one top-level test:
go test ./internal/update -run '^TestCompareVersions$' - Run a subset by regex:
go test ./internal/tui -run 'TestModel_' - Re-run without test cache:
go test ./internal/cli -run '^TestDoctorBasic$' -count=1
Safest local verification sequence after non-trivial changes:
gofmt -w .make lintgo test -race ./...make e2ewhen touching agent integrations, the e2e harness, or recorded fixturesgo build -o ./bin/no-mistakes ./cmd/no-mistakes
Project Layout
cmd/no-mistakes: process entrypointinternal/cli: cobra commands and CLI wiringinternal/daemon: background daemon and run managementinternal/pipelineandinternal/pipeline/steps: orchestration plus review/test/lint/push/PR/CI stepsinternal/agent: Claude, Codex, Rovo Dev, OpenCode, Pi, Copilot, and ACP/acpx integrationsinternal/git,internal/ipc,internal/config,internal/db,internal/paths,internal/types: shared infrastructureinternal/tui: terminal UI
Fork Routing
repos.upstream_urlis the parent repository used for PR base routing.repos.fork_urlis an optional GitHub fork push target.no-mistakes init --fork-url <url>expectsoriginto point at the GitHub parent repository and<url>to point at the contributor fork.- Plain
no-mistakes initpreserves an existing fork URL on idempotent refresh. - Push code must use
Repo.PushURL()so configured forks receive branch updates. - GitHub PR code must keep
--repopointed at the parent and use--head <fork_owner>:<branch>whenfork_urlis set. - GitHub existing-PR lookup must not pass
<owner>:<branch>togh pr list --head; list by the bare branch and filter the returned head owner fields. - GitLab and Bitbucket fork MR/PR routing is intentionally out of scope until implemented end to end.
- If a legacy or manually edited row has
fork_urlfor GitLab or Bitbucket, PR creation must skip instead of opening a self PR.
GitLab Backend (internal/scm/gitlab)
- The GitLab
Hostis constructed viagitlab.New(cmd, cliAvailable, host, projectPath), mirroring the GitHub backend's positional constructor.hostis the repo's GitLab hostname (fromscm.ExtractHost(UpstreamURL));projectPathis thegroup/projectpath (subgroups allowed, fromgitlab.ProjectPath- which lives in the gitlab package next to theHostthat consumes it, mirroringgithub.RepoSlug). Both are optional; passing"", ""reproduces the legacy unscoped behavior used by unit tests. - glab's flag surface drifts between versions; the backend is pinned against
glab v1.5x. Two flags bit us:glab auth statusmust be host-scoped with--hostname <host>(unscoped, it checks every configured instance and fails if ANY has a stale token, falsely reporting an authenticated repo as unauthenticated); andglab mr listno longer accepts--state opened(open is the default; v1.5x exposes-c/--closed,-M/--merged,-A/--all) - passing the removed flag fails the whole command. When the host is unknown, fall back to the unscoped auth check (fail-safe). - The daemon operates in a detached-HEAD worktree (it checks out the commit, not a branch).
glab ci getrefuses to run there ("you're not on any Git branch (a 'detached HEAD' state)") even with an explicit--pipeline-id. Read pipeline jobs via the branch-independent REST endpoint instead:glab api projects/<url-encoded group%2Fproject>/pipelines/<id>/jobs(Host.pipelineJobsArgs). The legacyglab ci getpath is kept only as the fallback when no project path is supplied. Theglab api .../jobspayload carriesfinished_at, mapped intoCheck.CompletedAt(needed for CI re-run detection).
Documentation
- Keep
README.mdconcise and high-level. The bar needs to be extremely high for what has to show up there. - Do not put technical details or deep reference material in
README.md. - Most documentation should live in
docs/which is the published docs site.
Agent-Guidance Surfaces
skills/no-mistakes/SKILL.mdis generated, not hand-written: the source of truth is thebodyconstant ininternal/skill/skill.go. Edit the body, thenmake skillto regenerate;make lintrunsskill-check(genskill --check) and fails CI on drift. Never editSKILL.mddirectly.no-mistakes initinstalls/refreshes this same rendering at user level, so the strings in the Go source are what ships to agents.- The "how an agent drives the pipeline" guidance lives in three surfaces that must stay in sync: (1) the skill body above (loaded when an agent invokes
/no-mistakes); (2) the liveaxioutput strings ininternal/cli/axi*.go- the homehelp(axi.go), the gatenote/helpand run/respond return help (axi_render.gogateFields), and the--helpLong strings (axi_drive.go); and (3) the publisheddocs/src/content/docs/guides/agents.md. When you change driving guidance in one, mirror it in the others. The point-of-useaxistrings are the layer an agent reads while driving without reopening the skill. - Review auto-fix is disabled by default (
config.goautoFixDefaultsReview: 0; a repo or globalauto_fix.review > 0override re-enables it throughAutoFixLimit(types.StepReview)and the executor auto-fix loop), so blocking and ask-user review findings park for an agent decision rather than being silently self-fixed. An info-level auto-fix review finding under the default neither parks nor is fixed, so keep the skill, liveaxinote, and docs qualified if you touch review auto-fix.
Context, Concurrency, and Processes
- Thread
context.Contextthrough long-running, subprocess, and networked work. - Prefer
exec.CommandContextfor subprocesses. - Route every long-lived subprocess spawned on behalf of a cancellable step/agent invocation through
shellenv.ConfigureShellCommand(cmd)after building the*exec.Cmd. It puts the child in its own process tree boundary (UnixSetpgid, Windows job object withtaskkillfallback) and installscmd.Cancelto kill the whole tree on context cancellation. Without it,exec.CommandContextonly kills the direct child and grandchildren survive (e.g.npm->nodetest workers, agent-spawned git/build/editor), keep running, and hold the worktree locked so the next run on the same branch cannot proceed. Applied to the step shell runner (runShellCommandWithEnv) and the native agentrunOncebuilders (claude, codex, pi, copilot, acpx); apply it to any new subprocess in those paths. cmd.Cancelonly covers the cancellation half of the lifecycle. On a clean exit (exit 0) or an error return it never fires, so a grandchild that outlived the leader - a test runner's worker pool, a build watcher, a dev server - is not reaped. This is the agent-spawning test step's failure mode: a repo with nocommands.testasks the agent to run the tests, the agent's worker pool leaks on every clean run, and the orphans accumulate (each a multi-hundred-MB pool) until the host is out of memory and the OS OOM-killer SIGKILLs the daemon - surfacing on the next start asdaemon crashed during execution(no Go stack trace, because SIGKILL is uncatchable). Useshellenv.RunShellCommand,shellenv.OutputShellCommand, orshellenv.CombinedOutputShellCommandfor one-shot commands; they start the command and reap the group on success/error paths too. When manual pipe handling is needed, useshellenv.StartShellCommand(cmd)and ensureshellenv.TerminateShellCommandGroup(cmd)runs as soon as the command is done or the parse loop fails. For stdout/stderr parsers that read until EOF, make the Wait owner terminate the group when the leader exits so a descendant holding inherited pipes cannot wedge the parser.startNativeAgentCommandowns that lifecycle for the native agent runners. Group termination is a harmless no-op (ESRCH) when nothing survived.ConfigureShellCommandalso installs acmd.WaitDelaypipe backstop (5s, now on unix as well as Windows) so a grandchild holding an inherited stdout/stderr pipe open after exit can't wedgecmd.Wait/CombinedOutputforever; it bounds the hang into a graceful step failure instead of taking the daemon down. Regressions:TestCodexAgent_Run_ReapsLeakedGrandchildOnCleanExit(agent path),TestRunShellCommandWithEnv_ReapsGrandchildOnCleanExit(configured-command path),TestTerminateShellCommandGroup_*(the primitive).- Use derived contexts and timeouts for cleanup and HTTP calls.
- Use
context.Background()mainly at top-level boundaries, background tasks, or in tests. - Protect shared mutable state with
sync.Mutex,sync.RWMutex,sync.Map, oratomicwhere appropriate. - Be explicit about ownership and cleanup of goroutines, worktrees, temp dirs, and channels.
Filesystem and Paths
- Use
filepath.Joinand related helpers. - Respect
NM_HOMEwhen working with app state. - Tests should isolate filesystem state with
t.TempDir()andt.Setenv("NM_HOME", ...). - Existing code typically uses
0o755for directories and0o644for files such as logs. - On macOS, remember that path comparisons may need symlink resolution like
/varvs/private/var.
Testing Conventions
- Tests live next to the code in
*_test.gofiles. - Use the standard
testingpackage. - Table-driven tests are common and use
tests := []struct { ... }plust.Run. - Use
t.Helper()in helpers. - Use
t.TempDir()for isolated filesystem state. - Use
t.Setenv()for environment-dependent behavior. - Prefer creating real git repos in temp directories instead of relying on heavy mocking.
- CLI tests often capture output and assert with
strings.Contains. - Prefer e2e tests, new or existing, for behavior that crosses a process or I/O boundary: CLI flags, config loading, git operations, agent spawning, daemon/process coordination, stdout/stderr, and recorded fixtures.
- Unit-test pure helpers and tightly scoped package behavior where speed and failure localization are worth more than full-product realism.
- Prefer targeted package tests while iterating, then finish with
go test -race ./...andmake e2ewhen your change affects those process or I/O boundaries. - The e2e suite lives behind the
e2ebuild tag, so it is excluded fromgo test ./...and runs separately in CI viamake e2e.
Repo Config Trust Boundary (security)
- The daemon runs
commands.*from.no-mistakes.yamlverbatim viash -c, andagentselects which process launches (incl.acp:targets) with the maintainer's credentials. To prevent supply-chain RCE, the code-executing selection fields (commands.{test,lint,format}andagent) are loaded from the trusted default branch, never from the pushed SHA. Seeinternal/daemon/manager.gostartRun+loadTrustedRepoConfig, andconfig.EffectiveRepoConfig. startRunfetches the default branch, resolves it to an exact commit SHA (git.ResolveRef), andloadTrustedRepoConfigreads.no-mistakes.yamlat that pinned SHA (not theorigin/<defaultBranch>ref name). On fetch failure (or if the ref does not resolve) the trusted SHA is empty →loadTrustedRepoConfigreturns nil →EffectiveRepoConfigforces emptycommands/agent. This fails closed: a staleorigin/<default>ref left in the shared bare repo by a previous run cannot serve a value the live default branch removed. Regression tests:TestLoadTrustedRepoConfig_FailClosedOnFetchFailure,TestLoadTrustedRepoConfig_PinnedSHAReadsFreshDefaultBranch.- Non-executing fields (
ignore_patterns,auto_fix,intent,test) are still read from the pushed branch. allow_repo_commandsis per-repo, read from the trusted default-branch copy of.no-mistakes.yaml(declared onRepoConfig), never the global config and never the pushed SHA. It defaultsfalse; whentruethe maintainer has opted in to honoring the pushed branch'scommandsandagentwholesale. A contributor cannot self-enable it from a pushed branch. When changing this logic, keepcommands/agentlocked to the default branch and update the e2e testTestRepoConfigCommandsFromDefaultBranch(incl. thepushed_branch_cannot_self_enablesubtest).- The e2e harness models a trusted single-developer environment, so it commits
allow_repo_commands: trueto the default-branch.no-mistakes.yamlviaSetupOpts.AllowRepoCommands; security tests passfalseto exercise the secure default.
CI Monitor Lifecycle
- The CI step (
internal/pipeline/steps/ci.go) babysits an open PR until it is merged, closed, the run is cancelled, orci_timeoutelapses. It auto-fixes failing checks and rebases on merge conflicts viaautoFixCI. ci_timeoutis an idle timeout, not an absolute deadline: it re-arms (timeoutAnchor = now()) every time the upstream default-branch tip advances, so an actively-rebased green PR keeps its monitor no matter how long it stays open.startedstays fixed for poll-interval/grace-period pacing; onlytimeoutAnchormoves. Re-arm only ever extends the deadline, so a transient base-tip resolution failure is fail-safe.baseBranchTipis injectable for tests.config.CITimeoutsemantics:>0finite,0= unset (step falls back toconfig.DefaultCITimeout, 7 days),<0=config.CITimeoutUnlimited(never self-terminate). Config keywordci_timeout: "unlimited"(alsonone/off/never) or any non-positive duration resolves to the unlimited sentinel viaparseCITimeout. Keepconfig.DefaultCITimeoutand thedefaultConfigYAMLci_timeoutvalue in sync (TestDefaultConfigYAML_MatchesGoDefaults).- Reap a run by id from outside its worktree with
no-mistakes axi abort --run <id>(runAxiAbortByRunID). It needs onlyNM_HOME+ the daemon, not a repo/branch/worktree, becauseipc.MethodCancelRun→RunManager.HandleCancelonly cancels runs live in daemon memory. An unknown/inactive id, or a stopped daemon, is an idempotent no-op (aborted: false), not an error. This is how an orphaned monitor (worktree torn down before merge) gets reaped deterministically. Bareaxi abort(no--run) stays worktree/branch-scoped.
Parked / Awaiting-Agent Signal
- A run carries a pollable "parked, awaiting the driving agent" marker so a supervisor can tell in one
axi statusread whether a run is waiting for the agent to drive a gate versus actively running/fixing/ci. It is observability only: it does not change gate resolution, auto-resume, or the--yesdefault. - Storage:
runs.awaiting_agent_since(unix seconds, nullable) ondb.Run.AwaitingAgentSince.ipc.RunInfoexposes bothAwaitingAgent bool(= since != nil) andAwaitingAgentSince *int64;runToInfoderives them. - Invariant:
awaiting_agent_sinceis non-nil iff a step is actually parked at anawaiting_approval/fix_reviewgate. The executor (internal/pipeline/executor.go) sets it viadb.SetRunAwaitingAgenton gate entry (right before the step status flips to the gate state, so it is already set once pollers observe the gate) and clears it viadb.ClearRunAwaitingAgentthe momentwaitForApprovalreturns - covering both the agent'saxi respondand a cancel.RecoverStaleRunsalso clears it so a crash-recovered (failed) run is never reported as parked. - Surface: the
run:TOON object addsawaiting_agent: parked <duration>right afterstatus, rendered only whileAwaitingAgentSince != niland the run is non-terminal (internal/cli/axi_render.gorunObjectFieldWithKey+formatParkedFor). The render clock is the injectablenowUnixpackage var so parked-duration tests are deterministic. - Tests: db set/clear + recovery (
internal/db/run_test.go), executor flips-on-gate/clears-on-respond (internal/pipeline/executor_approval_test.go), formatter + render shape (internal/cli/axi_test.go), and e2eTestAxiParkedAwaitingAgentSignal.
Rebase Base & Force-Push Safety (data-loss prevention)
- The whole job of this tool is to not lose people's code. Two invariants protect the rebase/push path; favor failing safe (refuse the push, surface a finding) over any clever recovery.
- Rebase base comes from the freshly-fetched authoritative remote, never local/stale state. The rebase step (
internal/pipeline/steps/rebase.go) fetchesorigin/<default>andorigin/<branch>(or the fork tracking ref) and rebases onto those remote-tracking refs - never the local default branch. - A gated branch must not silently bundle the contributor's unpushed local-default-branch commits.
detectBundledLocalDefaultCommitsreads the working repo's local<default>tip (Repo.WorkingPath), and when that tip is ahead oforigin/<default>and is an ancestor of the branch HEAD (i.e. the branch was built on unpushed default-branch work), the step returnsNeedsApproval+AutoFixable=falseso a human decides instead of widening the PR. Detection is best-effort: if the local default advanced past the branch point, or the working repo can't be read, it returns nil and the run proceeds. Regression:TestRebaseStep_DetectsUnpushedLocalDefaultBranchCommits(#283). - Every force-push is lease-guarded against discarding unseen upstream commits. All force-push sites (
PushStepinpush.go, CI auto-fixpushUpdatedHeadSHAinci_fix.go) route throughresolveForcePushDecision(internal/pipeline/steps/forcepush.go). It re-reads the live remote head and allows the push only when: the branch is new; the remote already equals the head; the remote still equalslastSeenSHA(what the run last observed); or every commit now on the remote is already incorporated by patch-id (git rev-list --cherry-pick --right-only HEAD...current), excluding history the run is knowingly rewriting (^baseSHA, i.e. reachable from the run base - the common amend or reverting the pipeline's own autofix). Anything else returnsforcePushWouldDiscardErrorand the caller must NOT push. An out-of-band commit reaches the branch after the run base, so it is never an ancestor ofbaseSHAand stays flagged. lastSeenSHAmust stay the head the run last observed, never the live remote tip. The push step passes the remote-tracking ref the rebase step synced (lastFetchedBranchTip); the CI step passesRun.HeadSHA. Both callers also passRun.BaseSHAfor the^baseSHAexclusion. Critically, the rebase step refreshesorigin/<branch>only on a normal push, NOT on a force push - on a force push it skips both the rebase-onto and the fetch, so the tracking ref stays the last-observed head. If it refreshed on a force push,lastSeenSHAwould equal the live tip, thecurrent == lastSeenSHAfast path would pass without the content check, and an out-of-band commit on the branch would be silently clobbered. Anchoring the lease to a SHA read from the remote immediately before pushing is the original #281 bug (it always passes and protects nothing); making the rebase always-fetch the branch was the same bug re-created for the force-push path. Never reintroduce either, and never degrade to a bare--force/--force-with-leasewithout an explicit anchor when ls-remote/fetch fails (fail closed instead). Regressions:TestCIStep_CommitAndPush_RefusesToClobberUnseenUpstreamCommit(#281),TestPushStep_RefusesToClobberAdvancedUpstreamBranch(#305),TestForcePushRun_RefusesToClobberOutOfBandBranchCommit(force-push fast-path clobber), andTestResolveForcePushDecision_*.
When Making Changes
- Whenever you must bring in new dependencies, check latest documentation for knowledge, and discuss with the user.
- Always use test driven development for bug fixes and feature development.