How the Gate Model in no-mistakes Works: Pause-and-Resume Pipeline Mechanics

The gate model in no-mistakes pauses pipeline execution when a step requires human or agent intervention, persisting an awaiting-agent marker in the database and surfacing an interactive CLI prompt for approval before resuming.

The no-mistakes repository implements a sophisticated pipeline orchestration system that uses a gate model to manage human-in-the-loop workflows. When automated steps encounter situations requiring judgment calls—such as lint failures or merge conflicts—the system gracefully pauses execution and surfaces an interactive interface for agents to approve, reject, or modify the proposed changes. This article examines the internal implementation of the gate model in no-mistakes, tracing how the executor coordinates between pipeline steps, database persistence, and CLI interactions.

Core Flow of the Gate Model

The gate model operates as a coordinated handshake between pipeline steps, the executor, and the database. When a step reaches a decision point, it signals the executor to enter a paused state that survives process restarts.

Step Execution and Gate Creation

Each pipeline step lives in internal/pipeline/steps and calls gate.New() when it requires external input. The constructor accepts a type (e.g., gate.TypeAwaitingApproval), a message for the agent, and optional metadata such as suggested fixes.

In internal/pipeline/steps/lint.go, a step might create a gate like this:

func (s *LintStep) Run(ctx context.Context, run *db.Run) error {
    // ... run lint ...
    if issuesFound {
        g := gate.New(gate.TypeAwaitingApproval,
            "Lint found problems – approve to continue or abort.",
            gate.WithMetadata(map[string]string{"details": details}))
        return s.Gate(g)
    }
    return nil
}

Returning the gate from the step signals the executor to pause rather than proceed to the next step.

Gate Registration and State Persistence

The executor in internal/pipeline/executor.go receives the gate and immediately writes an awaiting-agent flag to the database via db.SetRunAwaitingAgent(runID, true). This function writes a non-null awaiting_agent_since timestamp to the runs table, creating a durable marker that any process can query.

Simultaneously, the executor stores the gate struct in the run’s in-memory state (run.gate), allowing the system to reference the gate’s metadata while rendering CLI output.

Agent Interaction and Rendering

While paused, the run surfaces its status through the no-mistakes axi CLI command, implemented in internal/cli/axi_render.go. The output includes an awaiting_agent: parked <duration> field that updates in real-time using the injectable nowUnix variable, which keeps durations deterministic during tests.

$ no-mistakes axi status

# shows "awaiting_agent: parked 12s"

Gate Resolution and Pipeline Resumption

When an agent responds via no-mistakes axi respond yes, the executor’s resolveGate function clears the database flag using db.ClearRunAwaitingAgent(runID), removes the gate from the run’s state, and allows the pipeline to proceed:

func (e *Executor) resolveGate(runID int64, decision gate.Decision) {
    db.ClearRunAwaitingAgent(runID) // removes the parked marker
    // Continue pipeline processing ...
}

Safety Mechanisms and Crash Recovery

The gate model includes safeguards against stale state. The awaiting-agent flag is automatically cleared during crash recovery via executor.RecoverStaleRuns(), ensuring that runs never appear artificially parked after a system restart. Additionally, the gate model enforces a read-only boundary for external code—only the executor may set or clear the awaiting flag, preventing accidental state corruption by pipeline steps or CLI commands.

Key Implementation Files

Package / File Responsibility
internal/gate/gate.go Defines the Gate struct and helper constructors like gate.New().
internal/pipeline/executor.go Orchestrates run lifecycles, handles gate registration via handleGate(), and manages resolution through waitForApproval() and resolveGate().
internal/db/run.go Provides database helpers SetRunAwaitingAgent() and ClearRunAwaitingAgent() for the awaiting_agent_since timestamp.
internal/cli/axi_render.go Renders gate objects for the CLI, including parked duration calculations using nowUnix.
docs/src/content/docs/concepts/gate-model.md Documents the conceptual architecture of the gate model.

Practical Code Examples

Creating a Gate Inside a Step

// internal/pipeline/steps/lint.go (simplified)
func (s *LintStep) Run(ctx context.Context, run *db.Run) error {
    if issuesFound {
        g := gate.New(gate.TypeAwaitingApproval,
            "Lint found problems – approve to continue or abort.",
            gate.WithMetadata(map[string]string{"details": details}))
        return s.Gate(g)
    }
    return nil
}

Handling the Gate in the Executor

// internal/pipeline/executor.go (excerpt)
func (e *Executor) handleGate(g *gate.Gate, runID int64) error {
    if err := db.SetRunAwaitingAgent(runID, true); err != nil {
        return err
    }
    e.runState[runID].gate = g
    return e.waitForApproval(runID, g)
}

Resolving via the CLI

$ no-mistakes axi respond yes  # approves the gate

Internally, this triggers the resolution path:

func (e *Executor) resolveGate(runID int64, decision gate.Decision) {
    db.ClearRunAwaitingAgent(runID)
    // Resume execution...
}

Summary

  • The gate model pauses pipeline execution by creating a durable database marker via db.SetRunAwaitingAgent().
  • Gates are instantiated using gate.New() in pipeline steps and handled by the executor in internal/pipeline/executor.go.
  • The CLI renders gate status with real-time parked durations through internal/cli/axi_render.go.
  • Resolution clears the flag via db.ClearRunAwaitingAgent() and resumes the pipeline.
  • Crash recovery automatically clears stale awaiting-agent flags to prevent infinite pauses.

Frequently Asked Questions

What triggers a gate in no-mistakes?

A gate triggers when any pipeline step encounters a condition requiring human judgment—such as lint failures, test mismatches, or merge conflicts—and explicitly returns a gate.Gate object constructed via gate.New() to the executor.

How does no-mistakes handle crashes during a gated pause?

The system clears the awaiting_agent_since flag during startup via executor.RecoverStaleRuns(), ensuring that runs interrupted by crashes do not remain in a false parked state when the service restarts.

Can multiple gates be active simultaneously in a single run?

No, the current implementation stores a single gate per run in e.runState[runID].gate, meaning the pipeline processes gates sequentially; a new gate cannot be created until the previous one resolves.

What happens to the database state when a gate is resolved?

The executor calls db.ClearRunAwaitingAgent(runID), which sets the awaiting_agent_since column to null, effectively removing the parked marker and allowing the run to proceed to subsequent steps.

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 →