Purpose: Control transitions between phases with configurable approval requirements.
Orc defaults to fully automated gates - the system runs without human intervention by default. Human gates are opt-in for workflows that require oversight.
Quality assurance is handled by backpressure (tests, lint, build), not LLM-based evaluation. This provides deterministic, repeatable quality checks.
| Type | Description | Use Case |
|---|---|---|
auto |
Proceed immediately if criteria met | Default for all phases |
human |
Requires manual approval | Critical decisions |
skip |
No gate, always continues | Fast iteration |
ai |
AI agent evaluates gate | Automated review/validation |
| Mode | Behavior | Use Case |
|---|---|---|
gate |
Synchronous, blocks phase progression until resolved | Default - validation before proceeding |
reaction |
Asynchronous, fire-and-forget | Notifications, logging, non-blocking checks |
Location: internal/workflow/types.go:40-46
Actions define what happens on gate approval or rejection:
| Action | Behavior |
|---|---|
continue |
Continue to next phase |
retry |
Retry from specified phase (see retry_from) |
fail |
Fail the task |
skip_phase |
Skip the next phase |
run_script |
Execute a script (see script) |
Location: internal/workflow/types.go:48-57
Actions are resolved via resolveApprovedAction()/resolveRejectedAction() in internal/executor/gate_actions.go and dispatched in the main phase loop (internal/executor/workflow_executor.go).
On Rejection:
| Configured Action | Executor Behavior |
|---|---|
fail |
Task fails immediately |
retry |
Retries from retry_from phase; falls back to fail if max retries exceeded or no retry_from |
skip_phase / continue |
Proceeds to next phase (rejection ignored) |
run_script |
Script already executed during gate evaluation; applies fail as secondary action |
| (empty) | Legacy behavior: review phase → fail, other phases → continue |
On Approval:
| Configured Action | Executor Behavior |
|---|---|
continue / (empty) |
Default — proceed to next phase |
skip_phase |
Marks the NEXT phase as SKIPPED and advances past it |
run_script |
Script already executed during gate evaluation; secondary is continue |
Script execution: Scripts run during evaluatePhaseGate() via runGateScript() (workflow_gates.go:145). Script output can override the gate decision (flip approved↔rejected). Infrastructure errors (missing script, timeout) log warnings and continue.
RetryFrom precedence: GateOutputConfig.RetryFrom > PhaseTemplate.RetryFromPhase > config retry map. See resolveRetryFrom() at gate_actions.go:44.
Controls what context flows into the gate evaluator:
| Field | Type | Purpose |
|---|---|---|
include_phase_output |
[]string |
Phase IDs whose output to include |
include_task |
bool |
Include task details in context |
extra_vars |
[]string |
Additional variable names to pass |
Controls what happens with gate evaluation results:
| Field | Type | Purpose |
|---|---|---|
variable_name |
string |
Store result in workflow variable |
on_approved |
GateAction |
Action when gate approves |
on_rejected |
GateAction |
Action when gate rejects |
retry_from |
string |
Phase to retry from (when action=retry) |
script |
string |
Script path (when action=run_script) |
Location: internal/workflow/types.go:69-83, DB layer: internal/db/gate_config.go
Triggers are agent-backed hooks that fire at specific points in the task lifecycle. They use the same gate infrastructure (modes, input/output config) as phase gates.
Implementation: internal/trigger/ package. See internal/trigger/CLAUDE.md for package details.
| Mode | Behavior | Error Handling |
|---|---|---|
gate |
Synchronous, blocks progression if rejected | Returns GateRejectionError |
reaction |
Async goroutine, fire-and-forget | Errors logged, never blocks |
Default mode is gate when unspecified.
Single TriggerRunner used by executor, CLI, and API for consistent trigger evaluation.
| Call Site | File | Event |
|---|---|---|
| Executor (before phase) | executor/workflow_triggers.go:19 |
Before each phase |
| Executor (completion) | executor/workflow_triggers.go:95 |
on_task_completed |
| Executor (failure) | executor/workflow_triggers.go:76 |
on_task_failed |
| CLI task creation | cli/cmd_trigger.go |
on_task_created |
| CLI initiative plan | cli/cmd_initiative_plan.go |
on_initiative_planned |
| API task creation | api/task_server.go |
on_task_created |
API CRUD: api/workflow_server_lifecycle_trigger.go — Add/Update/Remove lifecycle triggers on workflows. See docs/API_REFERENCE.md → Lifecycle Triggers.
Triggers that run before a phase starts, enabling pre-validation or preparation:
workflow_phases:
- phase_template_id: implement
before_triggers:
- agent_id: "dependency-check"
mode: gate # blocks if agent rejects
input_config:
include_task: true
output_config:
on_rejected: fail
variable_name: "DEP_CHECK_RESULT" # output flows to variable| Field | Type | Purpose |
|---|---|---|
agent_id |
string |
Agent to execute |
input_config |
GateInputConfig |
Context for the agent |
output_config |
GateOutputConfig |
Result handling + variable capture |
mode |
GateMode |
gate (blocking) or reaction (async) |
Error resilience (SC-1): Infrastructure errors (agent crash, parse failure) log a warning and continue the phase. Only explicit gate rejections block.
Location: internal/workflow/types.go:85-91, evaluation: internal/trigger/runner.go:128
React to task/initiative lifecycle events at the workflow level:
workflows:
triggers:
- event: on_task_completed
agent_id: "notify-slack"
mode: reaction
enabled: true
- event: on_task_failed
agent_id: "failure-analyzer"
mode: gate
input_config:
include_task: true| Event | Fires When | Gate Rejection Effect |
|---|---|---|
on_task_created |
Task is created (CLI or API) | Task status set to BLOCKED |
on_task_completed |
Task completes all phases | Task status set to BLOCKED instead of COMPLETED |
on_task_failed |
Task fails during execution | Logged as warning |
on_initiative_planned |
Initiative planning creates tasks | Error returned to caller |
Built-in trigger: The dependency-validator agent runs on on_initiative_planned in implement-medium and implement-large workflows. It detects missing blocked_by dependencies between initiative tasks by analyzing descriptions/specs for code-level ordering requirements. See templates/agents/dependency-validator.md.
Location: internal/workflow/types.go:59-101, evaluation: internal/trigger/runner.go:55, initiative trigger: internal/trigger/runner.go:207
| Profile | Default Gate | Description |
|---|---|---|
auto |
All auto | Default - Full automation, no human approval |
fast |
All auto + no pre-merge | Maximum speed, no retry on failure |
safe |
Auto + human merge | Balanced - Automatic until final merge |
strict |
Human on spec/merge | Full oversight for critical phases |
# Run with profile
orc run TASK-001 --profile auto # (default)
orc run TASK-001 --profile safe # human on merge
orc run TASK-001 --profile strict # human on spec/merge| Phase | Trivial | Small | Medium | Large | Greenfield |
|---|---|---|---|---|---|
| research | - | - | - | auto | auto |
| spec | - | - | - | auto | auto |
| implement | auto | auto | auto | auto | auto |
| test | auto | auto | auto | auto | auto |
| validate | - | - | - | auto | auto |
# orc.yaml - default automation-first configuration
gates:
default_type: auto # Default gate type for all phases
auto_approve_on_success: true # Auto-approve when phase succeeds
retry_on_failure: true # Enable cross-phase retry
max_retries: 3 # Max retry attempts per phase
# Override specific phases
phase_overrides:
merge: human # Human approval before merge
# Override by weight
weight_overrides:
greenfield:
spec: human # Human review for greenfield specs
# Cross-phase retry configuration
retry:
enabled: true
max_retries: 3
retry_map:
test: implement # Test failures retry from implement
validate: implement # Validation failures retry from implementPhase templates and workflow phases support additional gate fields:
# Phase template with AI gate
phase_templates:
- id: security-review
gate_type: ai
gate_agent_id: "security-reviewer"
gate_mode: gate
gate_input_config:
include_phase_output: ["implement"]
include_task: true
gate_output_config:
variable_name: "SECURITY_RESULT"
on_approved: continue
on_rejected: retry
retry_from: implementGate evaluations can produce structured output data that flows into the workflow variable system for use in subsequent phases.
GateAgentResponse.data → Decision.OutputData + Decision.OutputVar
→ GateEvaluationResult.OutputData/OutputVar
→ applyGateOutputToVars()
├── vars[OutputVar] = JSON(OutputData) # Immediate use
└── rctx.PhaseOutputVars[OutputVar] = JSON # Survives ResolveAll()
→ {{VARIABLE_NAME}} in subsequent phase prompts (including retry)
gate_output_config:
variable_name: "SECURITY_RESULT" # Variable name for downstream access
on_approved: continue
on_rejected: retry
retry_from: implement| Scenario | Output Stored? | Details |
|---|---|---|
| Gate approved with output data | Yes | applyGateOutputToVars() stores JSON to vars[variable_name] + rctx.PhaseOutputVars |
| Gate rejected with output data | Yes | Output stored even on rejection (available to retry phase) |
Retry triggers ResolveAll() |
Yes | Variables restored from rctx.PhaseOutputVars by addBuiltinVariables() |
No variable_name configured |
No | Silently skipped |
| No output data from gate | No | Silently skipped |
| JSON serialization error | No | Logged warning, execution continues |
Gate output variables survive the retry flow. When a gate rejects and triggers retry from an earlier phase, ResolveAll() creates a fresh vars map. Gate output variables are restored from rctx.PhaseOutputVars (populated by applyGateOutputToVars()), making them available in the retried phase's prompt.
Why this matters: Without persistence to rctx.PhaseOutputVars, gate output would be lost during retry. Templates like {{GATE_REVIEW}} would render empty, losing the AI reviewer's feedback that should guide the fix.
Implementation: executor/workflow_gates.go:235 (output storage), variable/resolver.go:403 (restoration via addBuiltinVariables())
When a gate rejects or a phase fails, orc can automatically retry from an earlier phase:
implement → test (FAIL) → implement (retry #1) → test → validate
The retry phase receives structured retry variables ({{RETRY_ATTEMPT}}, {{RETRY_FROM_PHASE}}, {{RETRY_REASON}}) in its prompt, enabling the agent to fix the root cause rather than just re-running blindly.
Auto gates check deterministic criteria against phase output:
| Criterion | Description |
|---|---|
has_output |
Phase produced non-empty output |
no_errors |
Output doesn't contain "error" |
has_completion_marker |
JSON response has {"status": "complete"} |
| Custom string | Check if string appears in output |
# Plan YAML - auto gate with criteria
phases:
- id: implement
gate:
type: auto
criteria:
- has_output
- has_completion_markerHuman gates work differently depending on whether the task is running interactively (CLI) or headlessly (API/WebSocket).
When running via orc run in a terminal:
[GATE] Human approval required for merge
Task: TASK-001 - Add user authentication
Phase: merge
Files changed: 8
Tests: 24 passing
Approve? [y/n/q(questions)]: _
The CLI blocks and waits for input. Enter y to approve, n to reject with reason, or q to ask clarifying questions.
When running via the API (e.g., from the web UI), gates don't block:
- Task hits human gate - Gate evaluator detects headless mode
- Task blocked - Status changes to
blocked - Event emitted -
decision_requiredWebSocket event broadcast - User notified - Web UI shows approval prompt
- User decides - Frontend calls
POST /api/decisions/:id - Decision recorded - State and database updated
- Status updated - Task becomes
planned(approved) orfailed(rejected) - Resume required - User must explicitly resume the task
┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ Executor │───▶│ Gate │───▶│ PendingDecisionStore│
│ (phase run) │ │ (human gate) │ │ (in-memory map) │
└─────────────┘ └──────────────┘ └─────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────────────┐
│ Publisher │ │ POST /decisions/:id│
│ (emit event) │ │ (resolve decision) │
└──────────────┘ └─────────────────────┘
│ │
▼ ▼
┌───────────────────────────────────────────┐
│ WebSocket Subscribers │
│ (receive decision_required/resolved) │
└───────────────────────────────────────────┘
Note: Pending decisions are stored in-memory. Server restart clears them; tasks remain blocked and can be resolved via orc approve CLI.
-
Terminal (if interactive):
[GATE] Human approval required for merge Task: TASK-001 - Add user authentication Phase: merge Files changed: 8 Tests: 24 passing orc approve TASK-001 # Approve orc reject TASK-001 # Reject with reason orc diff TASK-001 # View changes -
WebSocket Event (if headless):
{ "type": "event", "event_type": "decision_required", "data": { "decision_id": "gate_TASK-001_merge", "task_id": "TASK-001", "task_title": "Add user authentication", "phase": "merge", "gate_type": "human", "question": "Please verify the following criteria:", "context": "Code review passes\nTests pass", "requested_at": "2026-01-10T10:30:00Z" } } -
Desktop Notification (if configured)
-
Webhook (Slack, email, etc.)
# Approve current gate
orc approve TASK-001
# Approve with comment
orc approve TASK-001 --comment "LGTM"
# Reject with reason (required)
orc reject TASK-001 --reason "Missing error handling"
# View what's pending
orc status --waiting# List gate config for all workflow phases (table or JSON)
orc gates list
orc gates list --json
# Show detailed config for a specific phase
orc gates show <phase>
# View gate decision history for a task
orc show TASK-001 --gates
# Skip all gates during execution (dev/testing)
orc run TASK-001 --skip-gatesImplementation: internal/cli/cmd_gates.go (list/show), internal/cli/cmd_show.go (--gates flag), internal/cli/cmd_run.go (--skip-gates flag)
# Database: states table
gates:
- phase: spec
type: auto
decision: approved
timestamp: 2026-01-10T10:45:00Z
- phase: merge
type: human
decision: approved
timestamp: 2026-01-10T15:45:00Z
approver: randy
comment: "Tested locally, looks good"Quality is validated through phase-level quality checks - deterministic checks that run after a phase claims completion.
Quality checks are defined per phase template in the database:
[
{"type": "code", "name": "tests", "enabled": true, "on_failure": "block"},
{"type": "code", "name": "lint", "enabled": true, "on_failure": "block"},
{"type": "code", "name": "build", "enabled": true, "on_failure": "block"},
{"type": "code", "name": "typecheck", "enabled": true, "on_failure": "block"}
]| Type | Behavior |
|---|---|
code |
Looks up command from project_commands table by name |
custom |
Uses the command field directly |
| Mode | Behavior |
|---|---|
block |
Phase fails, context injected for retry |
warn |
Warning logged, completion accepted |
skip |
Check disabled |
Commands are seeded during orc init based on project detection and stored in the project_commands database table. Manage with orc config commands.
Quality checks provide objective, repeatable quality validation without LLM judgment calls.
See internal/executor/quality_checks.go for implementation.
# Force approval (logged with reason)
orc approve TASK-001 --force --reason "P0 hotfix"Creates audit entry:
- phase: merge
type: human
decision: override
approver: randy
override_reason: "P0 hotfix"
timestamp: 2026-01-10T03:00:00Z