Events

Every meaningful action emits a typed event to an append-only JSONL log. flow.* events are about phase discipline; supervisor.* events are about autopilot operation.

Where they live

Per-repo events go to <cwd>/.xcoder/flow-events.jsonl. Override with FLOW_EVENTS_PATH (mostly for tests). Per-agent events (autopilot scoped) go to ~/.xcoder/events/<agentId>.jsonl.

Best-effort writes. A failed write logs to stderr but never throws — flow progress must not depend on event-sink availability.

Event shape

ts
interface SupervisorEvent {
  type: SupervisorEventType
  at: string                         // ISO 8601
  phase: FlowPhase                   // current phase when emitted
  taskId: string | null              // active task, if any
  data: Record<string, unknown>      // type-specific payload
}

flow.* events

TypeWhenNotable data
flow.transitionEngine advances from one phase to the next.{ from, to, reason }
flow.acceptance.passedAn acceptance check returned pass=true.{ invariant, checkId, reason, bypassedBy? }
flow.acceptance.failedAn acceptance check returned pass=false.{ invariant, checkId, reason }
flow.bypassA bypass is recorded (env / config / command / flag).{ source, identifier, invariant, phase, reason }
flow.guard.allowA PreToolUse hook allowed the action.{ guardId, reason }
flow.guard.blockA PreToolUse hook blocked the action (exit 2).{ guardId, reason }
flow.guard.bypassA PreToolUse hook allowed via a recorded bypass.{ guardId, reason, bypass }
flow.tool.invokedA state-mutating tool call was dispatched (I-12).{ toolName, toolInputKeys }
flow.commit.format.violationA commit was made with a non-conventional subject (post-commit signal).{ subject, expected }

supervisor.* events mostly post-v1

TypeWhenNotable data
supervisor.question.sentAutopilot needs human input; task marked waiting-for-reply.{ replyToken, channel, body }
supervisor.reply.receivedReply correlated to a question; task resumes.{ replyToken, body }
supervisor.reply.unparseableInbound message couldn't be correlated.{ raw }
supervisor.budget.warningSession/daily/monthly cap hit warning_at_pct.{ scope, used, cap, pct }
supervisor.budget.haltHard cap exceeded; engine halts.{ scope, used, cap }
supervisor.routingTask routed by performance profile.{ taskId, decision, successRateAtTier }

task.* events

TypeWhenNotable data
task.completedTask wrapped successfully (engine reached idle).{ taskId, prNumber, duration_ms }
task.failed.scoped-downTask split into sub-tasks (scope-down).{ taskId, children }
task.failed.escalatedTask escalated to human; autopilot won't retry.{ taskId, reason }
task.scope_down.proposedEngine suggested decomposing the task.{ taskId, proposed }
iteration.cap.hitTest↔implement loop hit its iteration cap.{ taskId, error }

Reading events

CLI

bash
xc flow events                              # last 50, all types
xc flow events -n 200                       # last 200
xc flow events --type flow.transition
xc flow events --type flow.bypass
xc flow events --json | jq 'select(.type == "flow.acceptance.failed")'

Programmatic

ts
import { readEvents } from '@xcoder/flow-engine'

const events = readEvents(process.cwd(), {
  types: ['flow.transition', 'flow.acceptance.failed'],
  limit: 1000,
})

Audit example

How many bypasses did this repo's agents take last week?

bash
xc flow events --type flow.bypass --json   | jq -s 'map(select(.at >= (now - 86400*7 | strftime("%Y-%m-%dT%H:%M:%SZ")))) | length'

Group bypasses by invariant:

bash
xc flow events --type flow.bypass --json   | jq -s 'group_by(.data.invariant) | map({invariant: .[0].data.invariant, count: length})'

Next