Issue Router operator guide
This guide covers the Issue Router, which dispatches to Codex or OpenCode. Router configuration is optional. A project without router: can use every existing Kanbus command; router commands report that the router is not configured.
Configure a project
Add a review status to the project workflow before enabling the router. Map all router roles to statuses that already exist in that workflow. Start with one Codex profile and a small WIP limit:
router:
workflow:
pending: open
active: in_progress
review: review
blocked: blocked
terminal: [closed]
limits:
project_wip: 2
review_wip: 1
class_wip:
implementation: 1
provider_wip:
codex-default: 1
providers:
codex-default:
adapter: codex
command: codex
args: []
classes:
implementation:
providers: [codex-default]
forge:
provider: github
repository: owner/repository
base_branch: main
api_url: https://api.github.com
token_env: GITHUB_TOKEN
watch_interval: 30s
retries:
max_attempts: 3
The router requires the workflow and limits blocks, one or more provider profiles (adapter: codex or adapter: opencode), and a GitHub repository. base_branch defaults to main, api_url defaults to https://api.github.com, and token_env defaults to GITHUB_TOKEN. Class routes use the configured provider order. The optional command and args values can point to a controlled fake adapter during tests. Put the GitHub token in the named environment variable, not in .kanbus.yml; the router does not read a fallback token.
The router publishes its event history and router-owned issue status records to the dedicated refs/heads/kanbus/router-state branch using an isolated hidden Git worktree. Without an origin remote, router state remains local to the repository. If a configured origin is unreachable, publication fails.
The router uses the project's existing coordination.providers configuration. [git] and [mqtt, git] are soft coordination; MQTT may improve claim visibility, but duplicate starts remain possible. [mutex_api, mqtt, git] requires a live Mutex API lease before work can start. If Mutex API is unavailable, the router does not start work through a weaker provider. Hard leases provide exclusion while they are live; expiry permits a newer claim to take over.
Opt issues in and inspect the plan
Apply exactly one routing label to each package root:
agent-class:implementationto use the named class's provider list.agent-provider:codex-defaultto pin work to one exact provider profile.
Do not change the human assignee to show execution ownership. The router keeps the assignee as accountability metadata. Untagged descendants stay in the nearest tagged package. A tagged descendant forms a separate package and is excluded from its ancestor's run.
Inspect the text or machine-readable plan before dispatch:
kanbus router plan
kanbus router plan --json
The plan identifies each package root, route, selected profile, package issue IDs, pending-entry time, and upcoming attempt. Deferred rows give one deterministic reason. Resolve invalid routes, blocking dependencies, or policy rejection on the issue. If a WIP limit is reached, review or blocked packages still consume capacity until their status changes.
Run and watch
Run one synchronous scheduling pass:
kanbus router run --once
It processes at most the first eligible package, waits for the agent adapter's result, validates it, records accepted checkpoint and artifact references in router history, publishes router-owned status, and opens or updates a pull request when complete. The summary reports started, completed, review, failed, and deferred counts.
For ongoing operation, start watch mode:
kanbus router run --watch
Watch mode reconciles immediately and polls router state and GitHub pull requests at watch_interval. When MQTT coordination is available, the watcher also subscribes before publishing claims and uses peer notifications to wake reconciliation before the next scheduled poll. If MQTT is unavailable, the router continues on its bounded Git polling interval.
Pause, hold, inspect, cancel, and stop
Use a global pause when no new router package should start. Active work continues unless you cancel it.
kanbus router pause
kanbus router resume
Hold one class or provider profile to defer only matching packages:
kanbus router hold --class implementation
kanbus router unhold --class implementation
kanbus router hold --provider-profile codex-default
kanbus router unhold --provider-profile codex-default
Inspect scheduler state, active run count, and sorted holds:
kanbus router status
Cancel an active package when its current work should stop. The router requests adapter cancellation, blocks the package, and preserves the latest accepted checkpoint:
kanbus router cancel kbs-123
Both runtimes record the active claim and adapter process in Git-common local state. A separate kanbus router cancel invocation signals that process, records the durable cancellation request, and fences later publication from the cancelled claim.
Stop watch mode running in this clone after the current adapter call finishes and release the scheduler claim:
kanbus router stop
Pause, resume, hold, unhold, and stop actions are written to router history for audit. The effective pause, holds, and watch state are kept in Git-private local state, so they apply to this clone and are not inherited by another clone. Releasing a hold does not clear the local global pause.
Review and merge
The router opens a pull request after a completed adapter outcome and moves the package into the configured review status. The adapter completing work does not close the Kanbus issue. Opening or synchronizing a PR leaves the package in review. A failed required check returns the package to active for repair; a successful check leaves it in review.
GitHub requested changes move the package back to active and ahead of pending work. The next run continues the same pull request. Approval alone keeps the package in review, and a new commit invalidates approval for the old head. The router moves the package to a configured terminal status only after GitHub reports the current approved PR head as merged. Closing without merge moves the package to blocked. The router does not merge PRs.
Failure recovery
Retryable adapter failures keep the package active, preserve the last accepted checkpoint, and use bounded backoff. With three maximum attempts, the first two retry delays are 30 and 60 seconds. The third retryable failure moves the package to blocked with a diagnostic. A structured blocked result blocks immediately.
After a process dies, a later router can take over after the execution lease expires or ownership becomes stale. It creates a new claim ID and logical revision and supplies the latest accepted checkpoint reference to the adapter. Accepted checkpoint refs are lease-pushed to origin, so another clone can restore the referenced commit. Artifact values remain named metadata references unless the adapter separately materializes and publishes their objects. Once the newer claim is visible, stale claims are rejected from accepted result publication. See the design for implementation details.
If hard coordination is configured and Mutex API is unavailable, restore the endpoint and credentials before retrying. The router intentionally starts no adapter through a weaker provider. In soft mode, duplicate starts are possible when workers cannot see each other's claims. Revision fencing rejects stale results after the newer claim becomes visible. The connected-clone offline scenario does not reproduce a network partition; the live mutex and MQTT harness paths also need to be run against configured services before treating them as validated. See the integration harness notes for the exact test scope.
See Issue Router design for the JSON contract, exact plan ordering, configuration validation, event schema, and detailed WIP rules.
OpenCode adapter (GPT-OSS on AWS Bedrock)
Provider profiles accept adapter: codex or adapter: opencode. Optional model
is passed as --model; optional env is merged over the router's environment.
command defaults to the adapter name.
router:
providers:
gpt-oss-bedrock:
adapter: opencode
model: amazon-bedrock/openai.gpt-oss-20b-1:0
env: {AWS_REGION: us-east-1, AWS_PROFILE: my-profile}
service_tier: flex # optional: flex (about half price, slower), priority, default
classes:
implementation: {providers: [gpt-oss-bedrock]}
Requirements: the opencode CLI on PATH, AWS credentials with
bedrock:InvokeModel*, and model access enabled in the Bedrock region. The router
runs opencode run --format json with stdin closed and PWD set to the worktree
(OpenCode otherwise waits on stdin and uses a stale PWD as its project root).
The result JSON is taken from the last matching object in the model's text.
service_tier is sent to Bedrock through OpenCode's per-model serviceTier option
(provider-level options are ignored by OpenCode). It requires adapter: opencode
and a provider/model model, and the model must support the tier. Confirm requests
resolved to it in CloudWatch (AWS/Bedrock, dimension ResolvedServiceTier).
Concurrency: each OpenCode run gets a private XDG_DATA_HOME (with auth.json
copied in), because parallel OpenCode processes otherwise share one session
database and fail with database is locked. Set XDG_DATA_HOME in the profile's
env to override.
Reliability note: openai.gpt-oss-20b-1:0 is a weak agent. In live trials it
sometimes ended without a result, emitted malformed tool calls, or returned issue
updates that violate the contract. The router's strict validation rejects these
as OpenCode router adapter returned invalid JSON/result. Run the opt-in live
check with KANBUS_LIVE_BEDROCK=1 pytest python/tests/test_router_opencode_live.py.
Evidence required: a completed result must leave something behind: a file
change in the agent's worktree, an issue_comments entry, or an issue_updates
entry. A completed result with none of these is treated as a retryable failure
(and blocked after retries.max_attempts), because models occasionally report
work they never did. Agents may not edit Kanbus project state (project/,
committed or not, except the derived project/.cache); such a run is preserved
for Review with the reason in a router comment.
Answering an agent's question (session resume)
When an agent finishes a turn with outcome blocked, the router records its
question and saved session and moves the issue to Blocked. To answer, add a
comment on the issue and move it back to Ready. The router then resumes the
same agent session with your comment(s) written after the question, instead of
starting over:
- The resumed run reuses the branch and worktree the agent already worked in, so its unfinished work and its saved session context are intact.
- Codex resumes with
codex exec resume, OpenCode withopencode run --session. - OpenCode can only resume a session from the directory the session started in
(from anywhere else it silently hangs). If that worktree no longer exists, the
router starts a fresh session instead and gives it your reply, and records why.
OpenCode's session data is kept per package under
<git-common-dir>/kanbus-router-adapters/opencode/<package>. - Moving an issue back to Ready without a newer human comment starts a fresh session, as before.
- Comments written by the router itself never count as a reply.