The integration point between misospace/dispatch
(GitHub issue assignment layer) and LLMKube Foreman
(agentic execution): a CronJob that claims groomed, ready issues from dispatch
lane queues and materializes Foreman Workloads for them — then shepherds the
failures.
dispatch lanes ──claim──► bridge ──create──► Workload ──► code → [verify] → review
│
├─ retry pass: Failed Workloads are deleted and
│ recreated (≤ RETRY_MAX_ATTEMPTS), carrying the
│ reviewer's NO-GO findings into the retry coder's
│ prompt (explicit spec.pipeline + payload.prompt)
└─ escalation: exhausted issues are re-laned to
ESCALATION_LANE + unclaimed; the next tick
claims them there with that lane's coder Agent
[verify] is opt-in — controlled by VERIFY_ENABLED, off by default. Enable it
to run the Foreman gate; left off, the bridge relies on repository CI. Enabling
requires Foreman >= 0.9.9.
Each tick (one CronJob run): reconcile failures first, then claim one ready issue per lane.
| Env | Default | Meaning |
|---|---|---|
DISPATCH_URL |
http://dispatch.llm:3000 |
dispatch base URL |
DISPATCH_AGENT_TOKEN |
(required) | Bearer token for the dispatch API |
DISPATCH_AGENT_NAME |
foreman-coder |
queue identity (use a dash, not a slash) |
DISPATCH_LANES |
(empty = discover) | lanes polled per tick. Unset, the bridge asks Dispatch for its lane topology (GET /api/lanes) and polls every claimable lane, so adding a lane needs no bridge change. Set explicitly to poll a subset; an explicit value always wins. Falls back to local,cloud,frontier only when unset AND the endpoint is unavailable. Concurrent lane fetches are soft-capped at 16 workers per tick (MAX_LANE_WORKERS); lanes beyond the cap are still polled, just with fewer in-flight requests. If you need to watch more than 16 lanes with full concurrency, split into multiple bridge deployments. An over-cap value logs a one-time WARNING (silence with DISPATCH_LANES_WARN=0). |
DISPATCH_LANES_WARN |
1 |
log a one-time WARNING when DISPATCH_LANES exceeds the 16-worker cap; set 0 to silence |
FOREMAN_NAMESPACE |
llm |
namespace for Workloads |
GATEPROFILE_MAP |
(empty) | JSON {repo: GateProfile} with "*" wildcard |
LANE_CODER_AGENTS |
(empty) | JSON {lane: coderAgentName} with "*" wildcard; wins over BASE_CODER_AGENTS Keys accept a lane id or a Dispatch role name (default/escalation); an exact lane id wins over a role, which wins over *. |
BASE_CODER_AGENTS |
(empty) | JSON {language: coderAgentName} with "*" wildcard; routes the base lane's coder by the repo's GATEPROFILE_MAP language |
CODER_AGENT_SLOTS |
{} |
JSON map {coderAgent: slotCount} capping each coder's in-flight Workloads; "*" wildcard covers unnamed agents. Empty keeps the legacy issue-number split. |
ESCALATION_LANE |
(empty = by role) | lane exhausted issues re-lane into. Unset, the bridge resolves the claimable lane whose Dispatch role is escalation, so this does not have to name a deployment-specific lane id. An explicit value always wins. Empty and unresolvable means escalation is off, as before. |
RETRY_MAX_ATTEMPTS |
3 |
attempts before escalate/tombstone A coder NO-GO repeated on a second attempt parks the issue instead of spending the rest of the budget: re-running re-derives the same judgement. |
PR_FIX_ENABLED |
(off) | enable the PR-fix drain/reconcile loop |
PR_FIX_MAX_ATTEMPTS |
3 |
pr-fix attempts before BLOCKED/tombstone |
FIX_FIRST_AGENTS |
(empty) | JSON list ["coder"] (or comma-separated names) of agents that only join the issue rotation when their fix lane is idle (no fix work in flight, a free slot). Fixes stay first-priority on an uncontended slot. See issue #134. |
GITHUB_TOKEN |
(empty) | used to check a PR's mergeable_state before marking a pr-fix FIXED (unauthenticated if unset) |
VERIFY_ENABLED |
false |
set to true to add the verify step and run the Foreman gate. Requires Foreman >= 0.9.9 and a verifier Agent; older Foreman versions reject Workloads without the required verifierAgentRef, so a deployment on one of those must set this. |
VERDICT_SELF_GO |
(unset) | Comma-separated lanes whose terminal Workload verdict the bridge is allowed to auto-mark selfGo (recommended for use with foreman dispatch run --wait) |
MAX_IN_PROGRESS |
0 |
Maximum concurrent in-progress Workloads per lane (0 disables the cap, i.e. unlimited) |
PRUNE_COMPLETED_AFTER_HOURS |
6 |
GC age, in hours, after which Completed Workloads are pruned (set to 0 to disable the sweeper) |
PRUNE_FAILED_AFTER_HOURS |
48 |
GC age, in hours, after which Failed/Crashed/Cancelled Workloads are pruned (set to 0 to disable the sweeper) |
REVISION_CODER_AGENTS |
{} |
JSON map {revisionLane: coderAgentName} overriding the agent used when a revision prompt reaches the coder stage Keys accept a lane id or a Dispatch role name (default/escalation); an exact lane id wins over a role, which wins over *. |
REPO_CODER_AGENTS |
{} |
JSON map {repoFullName: coderAgentName} overriding the agent used when a generic revision prompt reaches the coder stage |
PR_FIX_LANE_AGENTS |
{} |
JSON map {prFixLane: coderAgentName} overriding the agent used when a PR-fix prompt reaches the coder stage Keys accept a lane id or a Dispatch role name (default/escalation); an exact lane id wins over a role, which wins over *. |
DELETE_WORKLOAD_TIMEOUT_S |
60 |
Seconds the bridge waits for the cluster to acknowledge a Workload deletion request before giving up |
LOG_FORMAT |
json |
Log line format — json (structured, default) or plain (human-readable) |
LOG_LEVEL |
INFO |
Minimum log level emitted — one of DEBUG, INFO, WARNING, ERROR |
INFRA_PROBE_ENABLED |
true |
probe models backing infra-parked issues for automatic recovery |
INFRA_PROBE_URL |
http://litellm.llm:4000/v1 |
OpenAI-compatible base URL used for recovery probes |
INFRA_PROBE_API_KEY |
(empty) | optional bearer token for recovery probes |
PR-fix retries preserve the pipeline shape set at creation — rebuild_prfix_manifest
reuses the existing spec, so toggling this env variable after a PR-fix Workload exists
has no effect on its retries. Issue-path retries always pick up the current env value
each attempt because they rebuild from scratch via build_workload.
Backward-compat note (parked-for-human comments): every comment the bridge
posts for a needs-human issue now carries a stable path: <value> tag in its
header — declared-human, exhausted-attempts, exhausted-infra, go-no-pr,
or rail-demoted (a review the issueAsk/scope-overlap rail demoted from GO,
which re-running cannot fix, #287) — so triage can group issues by cause
without opening the Workload (issue #260). The header line is the closest thing to a contract: it is now
**Needs a human decision** (path: ) (or the GO-with-no-PR variant
with the same tag), so any operator script that matched the bare
**Needs a human decision** line exactly should match on the prefix instead.
The reason text, Issue:/Workload/branch: lines, and the footer are
unchanged.
The bridge needs, in FOREMAN_NAMESPACE:
workloads.foreman.llmkube.dev:create,get,list,deleteagentictasks.foreman.llmkube.dev:get,list(reads a failed Workload's review findings to build feedback-carrying retries)
pip install -r requirements.txt -r requirements-dev.txt
python -m pytest tests/ -q
Pure-logic modules (claim, workload, retry) take injected transports and
k8s callables, so the tests run without a cluster or network.
Tag vX.Y.Z → CI publishes ghcr.io/misospace/foreman-dispatch-bridge:X.Y.Z
and creates the GitHub release. Deployed via
home-ops (kubernetes/apps/base/llm/dispatch/foreman-dispatch-bridge/),
where the full pipeline is documented in the app README.
Extracted from joryirving/containers at 0.5.1 (fresh history); versions continue from 0.6.0.