Gates

Pause points for user approval, plus preapproval and dynamically injected gates.

A gate is a pipeline step that pauses for a user decision. The shim process inserts a pending row into the global DB, marks the parent pipeline run pending, and polls every 250ms for a decision. The dashboard's run page shows the gate prompt and writes the user's choice back to the DB; the shim picks it up on the next poll and either continues, terminates the run as cancelled, or skips ahead.

Gates are the human checkpoint in an otherwise autonomous pipeline. The default contractor-base pipeline has exactly one — between review and close — so a reviewer can confirm the diff before the close step starts merging.

Schema-defined gates

A schema's pipeline: may include kind: gate steps inline:

Each gate step needs a description (shown in the dashboard prompt) and an optional when: guard. At conversion time the runner assigns ids gate, gate-2, gate-3, … in order.

Preapproval

You can approve a future gate before it fires — useful when you trust the pipeline up to a known step and don't want to stay around to click "approve":

  • From the CLI, the [a] keybinding in contractor run adds the next pending gate's step id to the run's preapproved_gates JSON column.
  • From the dashboard, the run page's "Preapprove next gate" action does the same.

When the runner reaches a gate step, it first calls isGatePreapproved(pipelineRunId, stepId) against pipeline_runs.preapproved_gates. If the step id is in the list, the runner skips the pending-gate insert and proceeds directly to the next step. Otherwise it inserts the pending gate and polls.

Preapproval is per-(pipeline_run_id, step_id) and only applies to that specific run — it does not persist across runs.

Dynamically injected gates

contractor run --gate-after <step-id> inserts a one-shot gate that fires after the named step completes. Useful for "stop after implement so I can sanity-check" without editing the pipeline definition:

The flag is validated against the resolved pipeline's step ids before the shim spawns; an unknown id rejects with Invalid --gate-after step IDs: ….

Injected gates live in their own table (injected_gates) keyed by (pipeline_run_id, after_step). The runner consults the table after every step completes; if a row is waiting for the just-finished step, it pauses with a synthetic gate prompt (id injected-gate-after-<after_step>). The dashboard renders these alongside schema-defined gates.

Where decisions are recorded

Two tables in ~/.contractor/contractor.db store gate state:

The dashboard's relay layer reads pending rows from both tables to render gate prompts; decisions are written back through the same relay. There is no in-process gate registry — the database is the single coordination point, which is why dashboard exit does not strand a run mid-gate.

A rejected gate terminates the pipeline as cancelled (distinct from failed, which indicates a crash). The shim's parent process — contractor run or the dashboard — exits with the matching status code (130 for cancellation, matching the SIGINT convention).