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:

  1. Step Detection – A step implementation in internal/pipeline/steps detects a condition requiring intervention (e.g., lint errors) and constructs a Gate using gate.New(gate.TypeAwaitingApproval, message, opts...).

  2. Gate Registration – The step returns the gate to the executor, triggering handleGate() which immediately invokes db.SetRunAwaitingAgent(runID, true) to persist the awaiting-agent timestamp.

  3. 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.

  4. Interactive Rendering – The axi CLI command (implemented in internal/cli/axi_render.go) queries the database, detects the non-null awaiting_agent_since field, and displays awaiting_agent: parked <duration>. The duration calculation uses an injectable nowUnix variable to ensure deterministic test output.

  5. 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 invokes resolveGate().

  6. Cleanup and Resumption – Inside resolveGate(), the executor calls db.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 orphaned awaiting_agent_since timestamps 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 axi renderer accepts a nowUnix injection point rather than calling time.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 Gate struct and setting an awaiting_agent_since timestamp in the database via internal/db/run.go.
  • Only the executor in internal/pipeline/executor.go can modify the awaiting-agent flag, ensuring state integrity while external tools remain read-only.
  • The CLI renderer in internal/cli/axi_render.go displays 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 triggers db.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →