Runner Interface
The CLI is the primary shipped runner. It is not the only runner. AgentXchain also ships a declared library boundary for CI, hosted, or programmatic runners that need to drive the governed state machine without shelling out to agentxchain step.
If you want the constitutional model, start with Protocol v6. If you want the conformance corpus for protocol implementations, read Protocol Implementor Guide. If you want to build a runner, this page is the boundary that matters.
Versioned contract
The runner interface is a declared ES module at cli/src/lib/runner-interface.js.
| Field | Current value | Meaning |
|---|---|---|
RUNNER_INTERFACE_VERSION | 0.2 | Current versioned boundary for runner authors |
Versioning is not decorative. Adding, removing, or breaking runner operations changes the interface version.
Lifecycle operations
These are the state-machine operations a runner uses to drive governed execution:
| Export | Purpose |
|---|---|
loadContext(dir?) | Load project root, config, and validation |
loadState(root, config) | Load current governed state |
initRun(root, config) | Initialize a run from idle state |
reactivateRun(root, state, details?) | Reactivate a blocked or paused run |
assignTurn(root, config, roleId) | Assign the next governed turn |
acceptTurn(root, config, opts?) | Accept a staged turn result |
rejectTurn(root, config, result, reason, opts?) | Reject a staged turn result |
approvePhaseGate(root, config) | Approve a pending phase transition |
approveCompletionGate(root, config) | Approve a pending run completion |
markRunBlocked(root, details) | Persist a blocked state explicitly |
escalate(root, config, details) | Raise an operator escalation |
assignTurn() success returns { ok, state, turn }. That top-level turn is part of the contract. Runner consumers should not have to rediscover the assigned turn by traversing state.active_turns.
Support operations
These operations are not the lifecycle itself, but real runners need them:
| Export | Purpose |
|---|---|
writeDispatchBundle(root, state, config, opts?) | Write repo-native dispatch artifacts for an active turn |
getTurnStagingResultPath(turnId) | Return the canonical .agentxchain/staging/<turn_id>/turn-result.json path |
runHooks(root, hooksConfig, phase, payload, opts?) | Execute hook phases |
emitNotifications(root, config, state, event, payload?, turn?) | Emit notification events |
acquireLock(root) | Acquire the acceptance lock |
releaseLock(root) | Release the acceptance lock |
getActiveTurns(state) | Return the active turn map |
getActiveTurnCount(state) | Return the current active-turn count |
getActiveTurn(state) | Return the current active turn when one exists |
getMaxConcurrentTurns(config) | Read the configured concurrency limit |
getTurnStagingResultPath() is in the declared boundary on purpose. If a runner must stage a turn result for acceptTurn(), it should not import turn-paths.js directly just to discover the canonical path.
acceptTurn() is destructive for transient turn artifacts. After the acceptance journal commits, AgentXchain removes the accepted turn's .agentxchain/dispatch/turns/<turn_id>/ bundle and .agentxchain/staging/<turn_id>/ directory. Runners that need those files for auditing or upload must inspect or copy them before acceptance.
Canonical runner loop
The happy-path runner loop is:
loadContext/loadState
-> initRun or reactivateRun
-> assignTurn
-> writeDispatchBundle or runner-specific dispatch
-> stage turn result at getTurnStagingResultPath(turn.turn_id)
-> acceptTurn or rejectTurn
-> approvePhaseGate or approveCompletionGate when pending
Runner-specific dispatch is intentionally outside the interface. The CLI can use manual, local_cli, mcp, or api_proxy. A CI runner may stage a known-good result directly. A hosted runner may call network workers. The protocol cares about state transitions and artifacts, not how you invoked the agent runtime.
Shipped runner proofs
AgentXchain ships three runner proof tiers, each defending a different failure boundary:
| Tier | Script | What it proves |
|---|---|---|
| 1. Single-turn primitive | examples/ci-runner-proof/run-one-turn.mjs | A single governed turn can be assigned, dispatched, staged, and accepted through the raw runner-interface operations. |
| 2. Multi-turn primitive | examples/ci-runner-proof/run-to-completion.mjs | A full governed lifecycle (pm → dev → qa) can be driven through the raw runner-interface, including gate approvals, rejection, and retry on the same turn_id. |
| 3. Run-loop composition | examples/ci-runner-proof/run-with-run-loop.mjs | The runLoop library composes runner-interface operations into reusable continuous execution — gate pauses, rejection/retry, and typed stop reasons — without re-implementing lifecycle logic. |
All three proofs:
- import governed execution operations only through
runner-interface.js(Tiers 1–2) orrun-loop.jswhich itself only imports fromrunner-interface.js(Tier 3) - execute governed turns programmatically without shelling out to
agentxchain - stage turn results at the canonical staging path
- validate state-machine artifacts and cleanup behavior
- run in GitHub Actions via
.github/workflows/ci-runner-proof.yml
The point is not convenience. The point is proof that governed execution is not locked to the CLI shell.
Tiers 1–2 validate the primitive operations. Tier 3 validates that runLoop composes them correctly. Neither layer replaces the other — they are complementary proof boundaries.
What is not in this interface
These concerns are intentionally excluded:
- CLI parsing and output formatting
- dashboard serving
- export and report generation
- intake workflow commands
- adapter dispatch strategy
Those are runner features or adjacent product surfaces. The runner interface is the governed execution boundary.