How the no-mistakes Gate Model Works Internally: Pipeline Pausing and Agent Resolution
The no-mistakes gate model pauses CI/CD pipeline execution by instantiating a Gate struct that sets an awaiting-agent flag in the database, surfaces an interactive CLI prompt via the axi command, and blocks until an external agent approves, rejects, or modifies the pending step.
The kunchenguid/no-mistakes repository implements a deterministic gate mechanism that allows automated pipelines to safely halt for human or AI intervention. When a step requires external judgment—such as approving lint failures or reviewing rebases—the system creates a gate that persists state across process restarts and renders real-time status updates. Understanding this internal flow reveals how the tool balances automation with safety-critical manual checkpoints.
Core Components of the Gate Model
Gate Data Structure and Constructors
At the heart of the system lies the Gate struct defined in internal/gate/gate.go. This immutable data structure captures the gate type, display message, and optional metadata suggested fixes. Steps instantiate gates using gate.New(), passing enums like gate.TypeAwaitingApproval to classify the intervention required.
The constructor supports functional options such as gate.WithMetadata() for attaching structured context. Once created, the gate becomes the canonical token representing the paused state, passed between the pipeline executor and the database layer.
Database Persistence and the Awaiting-Agent Flag
The gate model achieves durability through the runs table managed in internal/db/run.go. When a step invokes a gate, the executor calls db.SetRunAwaitingAgent(runID, true), which writes a non-null awaiting_agent_since timestamp. This column acts as the global semaphore indicating that a run is parked awaiting external resolution.
The timestamp serves dual purposes: it flags the awaiting state for external observers (like the axi status command) and provides the baseline for calculating parked duration. The companion function db.ClearRunAwaitingAgent(runID) nullifies this field upon resolution, atomically releasing the run to continue execution.
Executor Orchestration and State Management
The Executor type in internal/pipeline/executor.go owns the gate lifecycle. It maintains an in-memory cache mapping runID to gate objects via e.runState[runID].gate, allowing the process to reference the pending decision without repeated database queries.
The executor implements handleGate() to transition into the paused state and waitForApproval() to block the pipeline goroutine. Resolution arrives via resolveGate(runID, decision), which coordinates the database clearance and pipeline resumption. Additionally, executor.RecoverStaleRuns() automatically clears awaiting-agent flags for runs that crashed while gated, preventing phantom parked states.
Step-by-Step Execution Flow
The interaction between pipeline steps, the executor, and the agent follows a strict protocol:
-
Step Detection – A step implementation in
internal/pipeline/stepsdetects a condition requiring intervention (e.g., lint errors) and constructs aGateusinggate.New(gate.TypeAwaitingApproval, message, opts...). -
Gate Registration – The step returns the gate to the executor, triggering
handleGate()which immediately invokesdb.SetRunAwaitingAgent(runID, true)to persist the awaiting-agent timestamp. -
State Persistence – The executor stores the gate struct in its internal run state while the database write ensures visibility to other processes, including CLI tooling and potential distributed observers.
-
Interactive Rendering – The
axiCLI command (implemented ininternal/cli/axi_render.go) queries the database, detects the non-nullawaiting_agent_sincefield, and displaysawaiting_agent: parked <duration>. The duration calculation uses an injectablenowUnixvariable to ensure deterministic test output. -
Agent Resolution – An operator or automated agent runs
no-mistakes axi respond yes(or similar), which sends a resolution message to the executor. The executor validates the decision and invokesresolveGate(). -
Cleanup and Resumption – Inside
resolveGate(), the executor callsdb.ClearRunAwaitingAgent(runID)to remove the parked marker, deletes the gate from in-memory state, and unblocks the pipeline to execute subsequent steps.
Safety Mechanisms and Concurrency Guarantees
The gate model implements defensive programming to prevent stuck pipelines and state corruption:
-
Crash Recovery – On startup,
executor.RecoverStaleRuns()scans for runs with orphanedawaiting_agent_sincetimestamps where the coordinating process died. It automatically clears these flags, ensuring runs never remain artificially parked. -
Read-Only External Interface – External code can inspect gate status through the CLI and database views, but only the executor may modify the awaiting-agent flag. This prevents accidental state corruption from external scripts or API consumers.
-
Deterministic Time Tracking – The
axirenderer accepts anowUnixinjection point rather than callingtime.Now()directly. This design enables reproducible tests that verify duration calculations without flaky timing assertions.
Code Examples
Creating a Gate Inside a Pipeline Step
When linting detects issues, the step constructs a gate and signals the executor to pause:
// internal/pipeline/steps/lint.go (conceptual implementation)
func (s *LintStep) Run(ctx context.Context, run *db.Run) error {
// Execution logic...
if issuesFound {
g := gate.New(
gate.TypeAwaitingApproval,
"Lint found problems – approve to continue or abort.",
gate.WithMetadata(map[string]string{"details": details}),
)
// Returning the gate instructs the executor to pause here
return s.Gate(g)
}
return nil
}
Executor Handling the Gate Pause
The executor receives the gate, persists the awaiting-agent flag, and blocks:
// internal/pipeline/executor.go (simplified excerpt)
func (e *Executor) handleGate(g *gate.Gate, runID int64) error {
// Atomic write to database marks run as parked
if err := db.SetRunAwaitingAgent(runID, true); err != nil {
return err
}
// Cache gate for later reference
e.runState[runID].gate = g
// Block until agent responds
return e.waitForApproval(runID, g)
}
Resolving a Gate via CLI Interaction
An agent interacts with the paused pipeline through the axi subcommand:
# Check current status showing parked duration
$ no-mistakes axi status
Project: my-service
Run ID: 42
Status: awaiting_agent: parked 12s
# Approve and continue execution
$ no-mistakes axi respond yes
Internally, the resolution triggers database cleanup:
// internal/pipeline/executor.go
func (e *Executor) resolveGate(runID int64, decision gate.Decision) {
// Remove the awaiting-agent timestamp
db.ClearRunAwaitingAgent(runID)
// Remove from memory and resume pipeline
delete(e.runState[runID], "gate")
// Continue to next step...
}
Summary
- The no-mistakes gate model creates a persistent pause point in CI/CD pipelines by instantiating a
Gatestruct and setting anawaiting_agent_sincetimestamp in the database viainternal/db/run.go. - Only the executor in
internal/pipeline/executor.gocan modify the awaiting-agent flag, ensuring state integrity while external tools remain read-only. - The CLI renderer in
internal/cli/axi_render.godisplays real-time parked durations using deterministic time injection, enabling both human and automated agents to observe and resolve gates. - Crash recovery automatically clears stale flags during startup, preventing runs from becoming permanently stuck in an awaiting-agent state.
- Resolution requires explicit agent action through
no-mistakes axi respond, which triggersdb.ClearRunAwaitingAgent()and resumes pipeline execution.
Frequently Asked Questions
What triggers the creation of a gate in no-mistakes?
A gate is created when any pipeline step determines it needs external validation before proceeding. According to the source code in internal/pipeline/steps, steps such as lint or rebase call gate.New() with types like TypeAwaitingApproval when they encounter conditions that require human judgment or automated policy checks, returning the gate object to the executor to initiate the pause.
How does the executor prevent runs from staying paused indefinitely after a crash?
The executor.RecoverStaleRuns() function runs during process initialization and scans the database for runs with a non-null awaiting_agent_since timestamp that are no longer associated with a live executor process. It automatically invokes db.ClearRunAwaitingAgent() on these orphaned entries, ensuring that crashed or restarted services do not leave pipelines artificially blocked.
Can external automation bypass the CLI and resolve gates directly?
No, the gate model enforces a read-only external interface. While external tools can query the awaiting status through database views or the axi status command, only the internal executor's resolveGate() method can call db.ClearRunAwaitingAgent(). This design prevents race conditions and ensures that state transitions occur only through the validated no-mistakes axi respond code path.
Why does the parked duration display use an injectable nowUnix variable instead of system time?
The axi renderer in internal/cli/axi_render.go accepts an injectable nowUnix parameter to calculate the parked <duration> string. This dependency injection pattern allows unit tests to pass a frozen timestamp, verifying that duration calculations and formatting logic behave correctly without relying on flaky system clock assertions or time.Sleep() calls in test suites.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →