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:
pipeline:
steps:
- phase: implement
critical: true
- phase: review
critical: true
- kind: gate
description: Review changes before continuing to close
- phase: close
critical: falseEach 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 incontractor runadds the next pending gate's step id to the run'spreapproved_gatesJSON 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:
contractor run --gate-after implement
contractor run --gate-after implement --gate-after reviewThe 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:
gates
id TEXT PRIMARY KEY
run_id TEXT NOT NULL
step TEXT NOT NULL
status TEXT NOT NULL DEFAULT 'pending' -- pending | approved | rejected | cancelled
prompt TEXT
decided_at TEXT
created_at TEXT NOT NULL DEFAULT (datetime('now'))
injected_gates
id TEXT PRIMARY KEY
pipeline_run_id TEXT NOT NULL
after_step TEXT NOT NULL
status TEXT NOT NULL DEFAULT 'waiting' -- waiting | satisfied | cancelled
description TEXT
created_at TEXT NOT NULL DEFAULT (datetime('now'))
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).