Agent workflows
Agent workflows turn the plan → review → execute → review loop into a durable, observable state machine inside Jentrix. A planner agent drafts a plan, a reviewer agent validates it and returns structured findings, the planner fixes them, and they iterate until the reviewer is satisfied — then the same loop runs over the execution result. A human approves before execution and at final acceptance. Jentrix owns the state, enforces whose turn it is, records every finding and how it was fixed, and surfaces all of it in the Control Tower.
Jentrix does not run the agents. The planner (Claude) and reviewer (Codex) run on your machine via the external runner and drive the workflow over the MCP tools below. Jentrix coordinates and observes.
The full spec → plan → stage → review → fix → validate → commit automation built
on this loop — the HarnessRun / HarnessStage spine, reusable Runbooks, the
freeze and commit gates, delivery correlation, and harness playbook mining — is
documented in Harness Automation. This page describes the
underlying loop engine the harness reuses.
The job state machine
Each workflow is an AgentJob anchored to a task. Its phase advances
through:
| Phase | Whose turn | Advanced by |
|---|---|---|
PLAN_DRAFTING | planner | request_agent_workflow |
PLAN_REVIEW | reviewer | submit_plan |
PLAN_REVISION | planner | submit_review (changes requested) |
PLAN_APPROVAL_GATE | human | submit_review (approved) |
EXECUTING | planner | human approves the pre-execution gate |
EXECUTION_REVIEW | reviewer | submit_execution_result |
EXECUTION_REVISION | planner | submit_review (changes requested) |
FINAL_ACCEPTANCE_GATE | human | submit_review (approved) — base task loop |
MERGE_GATE | human | submit_review (approved) — delivery loop (the production boundary) |
DEPLOY_GATE | human | merge approved under gate_at_promotion (the Vercel promotion) |
DONE | — | human approves the final / merge / deploy gate |
BLOCKED | human | a failed run, a stalled heartbeat, or non-convergence |
CANCELLED | — | a human cancels |
The plan itself is a Work Order that gains a new immutable, diffable
version on every submit_plan. The Work Order is only launched (and the
execution begins) when the human approves the pre-execution gate.
Convergence guard. Each phase has a maxIterations budget (default 5).
Exceeding it auto-blocks the job to a human rather than letting two agents
ping-pong forever.
Workflow templates
A template is data that parameterizes the one state machine above — it sets
the convergence budget and the roles a start must name; it never adds new
transition code. The default is two_agent_plan_execute_review (the base
task loop). On top of it sits the delivery family, for driving software
delivery end to end:
Template (templateKey) | Use | Opens a PR | Budget |
|---|---|---|---|
feature | Build a feature → linked PR | yes | 5 |
bugfix | Fix a bug → linked PR | yes | 5 |
ci_failure_triage | Push a fix to an existing PR branch so CI re-triggers | no | 3 |
release_candidate | Bundle merged PRs into a release | no | 4 |
hotfix | Urgent fix → linked PR | yes | 3 |
Every delivery template runs the same loop with the same pre-execution gate and
convergence guard. The difference is intent and the terminal gates: the
executor opens a real PR (for the templates that do), the reviewer reacts to real
CI evidence, and the loop's terminal meaning is "the PR is ready for merge"
rather than "the task is complete". ci_failure_triage and hotfix carry a
tighter budget so an auto-fix loop reacting to a flaky check converges (or blocks
to a human) quickly.
Merge and deploy gates (M8.3). After execution review, a delivery loop pauses
at a merge gate — the production boundary — in place of the base loop's
final-acceptance gate. Whether a further deploy gate fires depends on the
project's release-control mode (the board's production Environment):
gate_before_merge(default): the merge gate is the production boundary. Approving it brokers arepo:mergeGitHub credential and merges the PR; the Vercel deploy that follows is observed/recorded (no deploy credential).gate_at_promotion: the loop pauses again at a deploy gate that brokers adeploy:productionVercel credential authorizing the production promotion.
The loop never merges or deploys without a gate — approving a gate triggers the
brokered execution (Stage 3.4) as a post-commit effect: a policy-gated, leased,
single-use brokered credential (repo:merge via brokerRepoToken,
deploy:production via brokerCloudToken) carries it out, never a standing raw
token. GitHub performs the merge in-process; the production promotion executes
externally on the brokered credential and Jentrix records the Deployment
(records-not-fires). An agent has no MCP path to self-approve a merge or deploy
(gates are UI-resolved). Rejecting a merge or deploy gate sends the change back for
more work. Rollback is the same shape — admin-gated, release:rollback-brokered,
recorded as a Deployment that references the one it reverts. A blocked or stalled delivery run surfaces in
the existing Control Tower queues (Failed jobs / Stuck runs) exactly like a
base task loop, because the delivery family reuses the same AgentJob/AgentRun
substrate.
Start a delivery loop with request_delivery_workflow (it is constrained to
the delivery family — a non-delivery templateKey is rejected with
INVALID_INPUT). It optionally binds the job to an existing PR via
prLinkId: the PR must sit on the task's own board (repo↔board is 1:1, so a PR
on another board can't belong to this task), and the bound repo owner/name is
derived from that PR. Bind for ci_failure_triage / release_candidate (they
react to an existing PR); omit for feature / bugfix / hotfix, which open a
new PR. (request_agent_workflow still starts the base task loop and also
accepts a delivery templateKey, but without the PR binding or the CI context
below.)
Authoring loop and engine side effects (M9.2)
The authoring template runs the manual "draft a PRD → reviewer attacks it →
fix → validated" cycle on the same state machine — it is data, not a second
engine. It converges on a spec (terminalDisposition: "spec_validated")
rather than a task or a PR: the synthesized PRD is the deliverable, so approving
the plan-approval gate completes the loop with no execution phase (a
data-driven terminal branch, exactly like the delivery family's merge-gate
branch). The convergence budget is unchanged — a review loop that never
converges blocks to a human.
Template (templateKey) | Use | Terminal | Budget |
|---|---|---|---|
authoring | Synthesize a PRD → review → validate into an accepted spec | spec validated, run → planning | 5 |
M9 adds one new primitive to the engine: a declared side-effect hook. A
template may declare, as data, that when a named human gate is approved the
engine runs a named action — { atTransition: { onGateApproved }, action }.
Bindings are validated at registration (a template naming an unknown gate or
action throws at load, never mid-run) and run post-commit as the harness
service actor (source: "automation"), through the normal authz/policy guards —
a side effect is not a back door, and it can never un-approve the gate that
fired it. The authoring template binds the pre-execution (authoring) gate to
promote_spec_version: on approval it accepts the synthesized PRD as a
SpecVersion, pins it on the harness run, advances the run to planning, and
emits harness.spec_validated. The gate itself stays UI-resolved — an agent
has no MCP path to validate its own spec.
The decomposition template is the second plan-only loop (terminalDisposition: "plan_validated"): it decomposes the validated SpecVersion into ordered
stages, each with a per-stage M2 Work Order (strictly 1:1). Its coverage-enforcing
accept action createStagesFromPlan checks spec coverage — every spec
acceptance criterion (definitionOfDone) must be traced by at least one stage, and
an orphaned criterion blocks the decomposition (the run never reaches ready
with an incomplete plan). On a covered plan it mints the stages + Work Orders
(each stage carries its acceptance criteria, expected branch behavior, suggested
tests, and risk notes), snapshots the registry (a new version; prior versions are
retained, so reordering / splitting / merging on a later pass preserves lineage),
advances the run to ready, and emits harness.plan_validated. Work Orders
are task-scoped, so a decomposable run must anchor a task.
The template is started only through start_harness_decomposition (the generic
request_agent_workflow intentionally rejects it). The planner submits ordered
harnessStages through submit_plan; the accept gate validates coverage and
mints the stages and their Work Orders atomically.
Template (templateKey) | Use | Terminal | Budget |
|---|---|---|---|
decomposition | Validated spec → ordered stages + Work Orders (coverage-enforced) | plan validated, run → ready | 5 |
Stage execution loop (M9.3)
Once a run is decomposed into ordered stages, it executes one stage at a time.
The runner always picks the deterministically-first undone stage — the same
1.2 resolver that backs the registry, never the denormalized currentStageId
cache — so execution is inherently serial: stage N+1 is not launchable until N
is done. Each stage runs on the SAME M7 engine as a stage_execution job
(plan → review → execute → review); its terminal disposition is
stage_ready_to_commit (reaching DONE means the change is ready to commit — the
commit boundary itself is gated in a later stage, so this template adds no merge
gate), and its iteration budget IS the existing M7 convergence guard (no second
loop engine).
Launching a stage (launchFirstUndoneStage) acquires an advisory M5 lease on
the stage's Work Order before opening the job, then opens an AgentJob bound
to the stage (AgentJob.harnessStageId), the run's anchor task, and the run's
planner/reviewer agents (whose tokens are resolved + validated like
request_agent_workflow, never copied raw — so the turn guard is never a no-op),
and moves the stage into execution. The exclusivity is the stage CAS, not the
lease: the launch update guards on both status and leaseId: null, so even two
racing launches yield exactly one executing stage (the loser's CAS no-ops and its
lease is released). The lease is advisory on purpose — the M7 PRE_EXECUTION
gate runs launchWorkOrder on the SAME Work Order, and an exclusive/blocking
agent-held lease would deadlock it; the advisory lease instead carries the current
attempt's agentJobId (the stale-hook guard) and is heartbeat-renewed (a
healthy long stage never loses it), with a lapsed heartbeat surfacing in the
existing STUCK_RUNS queue — unchanged.
The lease is released on every exit path. On success the declared
validate_harness_stage side effect releases it (→ VALIDATED); on a failure
exit (convergence BLOCKED, human fail, or cancel) parkStageForFailedJob (wired
post-commit into failAgentJob / submitReview / cancelAgentJob) moves the
stage to BLOCKED and releases the lease; a stuck run's lease expires via the
sweep when heartbeats stop. Both terminal hooks no-op unless the firing job is the
stage's current attempt (the lease's agentJobId), so a late hook from a
superseded job can neither validate nor block the newer one. Retrying a parked
stage is a re-launch — never the generic M7 unblock (which unblockAgentJob
refuses for stage jobs): a human unblocks the stage (BLOCKED → READY) and
launchFirstUndoneStage opens a fresh AgentJob under a fresh lease (each attempt
its own job, auditable; no lighter retry op that could leak a lease or run two
jobs).
A mid-build blocking Decision (linked via linkHarnessDecision, 1.2)
pauses the stage (pauseStage → BLOCKED, releasing the lease) — it requires
a blocking, not-yet-accepted Decision — until it is accepted, then resumes it
(resumeStage → BLOCKED → READY, relaunchable; both honor the serial
first-undone order). Resume returns the stage to READY (not straight to
EXECUTING, which would strand it without a job); the next launch opens a fresh
attempt.
HARNESS_STAGE_STARTED / HARNESS_STAGE_VALIDATED activities and the
harness.run_started / harness.stage_started / harness.stage_validated
workspace events make the loop observable.
Evidence gate + review/fix cycle (M9.3, Stage 3.2). A stage reaches
validated ONLY when (a) every required evidence kind is attached
(EXECUTION_SUMMARY + DIFF_OR_PATCH + TEST_EVIDENCE) AND (b) no critical/high
finding is still unresolved — the pure evaluateStageValidationGate
(src/server/harness/evidence.ts). "Validated with no tests / open critical
findings" is impossible: the gate is enforced both on the explicit
requestStageValidation op AND in the validate_harness_stage completion bridge,
which — when the gate is NOT clear — parks the stage at EXECUTION_REVIEW
holding its lease (recording the missing evidence / blocking-finding count)
rather than validating. submitStageResult records the executor's structured
output (changed files, commands, tests run/not-run, risks) as a redacted
EXECUTION_SUMMARY artifact; the diff + test-evidence artifacts attach by the same
stage scope. The review/fix cycle is the existing M7 contract (submit_review
CHANGES_REQUESTED → fix turn → re-review under the convergence guard); each
re-review turn is fed the review continuity — get_agent_job carries a
harnessReview block (stageReviewContext) with the gate + the prior rounds'
findings + resolutions (the 2.3 buildReviewContinuity, empty on round 1), so the
reviewer reviews WITH memory of its own earlier rounds instead of re-flagging a
resolved point.
The stage_execution template is registered but, like decomposition, its
start is gated off in request_agent_workflow — stage jobs are opened ONLY by
the harness execution launch path (which resolves the first-undone stage and
acquires its lease), never the generic request path. The live MCP write tools that
drive this loop (submit/advance a stage) land in the Phase-2 harness write surface.
Findings, review continuity, and Runbooks (M9.2)
Codex findings + Claude resolutions are surfaced as a first-class harness-level
finding table (HarnessFinding) over the M7 review-finding mechanism — a
normalized projection, not a parallel store: each finding carries the reviewer
AgentRun that raised it (its round) and moves through a guarded lifecycle
(open → accepted/fixed/rejected → verified; reopened on regression) that can never
silently drop a critical/high finding. The harness page's Findings tab renders
them as a structured table (target, severity, category, status, reviewer,
resolution, verification).
The harness's own runtime carries review continuity: buildReviewContinuity
reconstructs a $PRIOR_ROUNDS block purely from the stored findings (each prior
round's findings + dispositions, redaction-clean), and the reviewer Runbook
renders that block plus a reconciliation instruction on a re-review (empty on
round 1) — so the reviewer reviews with memory of its own prior rounds and does
not re-flip a position a prior round demanded. This is the same continuity that
governs the M9 build loop, applied to the harness's runtime review loop.
Runbooks are versioned, checksummed HarnessPromptTemplates whose body
references live Jentrix IDs / MCP tools / state queries through placeholders
({{run_slug}}, {{stage_number}}, {{prior_rounds}}, {{reconciliation}}, …)
— never a stale hardcoded checklist. renderRunbook resolves them against a
concrete run + stage at launch. A Runbook must be approved before it is
rendered for use (Codex reviews its safety/scope/turn-discipline first); approval
is human/loop-gated.
The accepted-PRD and accepted-plan human gates are the M7 plan-approval gates
(AgentApprovalRequest), resolved only through the UI / Control Tower approval
surface — there is no MCP tool that resolves an agent job gate, so an agent
cannot self-approve its own spec or plan.
Reviewer reacts to CI
When a job is bound to a PR, get_agent_job carries a delivery block — the
PR's state and review decision, its CI runs with their conclusions, and the
live evidence artifacts (the redacted CI logs plus any diff/patch attached
to the PR or its runs). The reviewer turn reads this and reviews against real
evidence — the actual CI verdict and diff — rather than the plan alone. The
ci_failure_triage executor reads the same block to learn the PR's head branch,
then pushes its fix to that branch through the governed push path (CI re-triggers
on the new commit — no Actions re-run permission needed). delivery is null
on an unbound or base job.
CI-aware automation
A CI run entering a failing conclusion on a PR-linked task fires a
check.failed automation trigger (the same signal as the
delivery.check_failed event). Pair it with the start_delivery_workflow action to
auto-launch a ci_failure_triage loop bound to the failing PR. The action runs
as the rule's creator with source: "automation" and is deduped against an
already-active delivery job for that PR, so a flaky check can't spawn a swarm
of triage loops; and because automations can never trigger other automations
(the hard loop guard), the triage loop's own writes never re-fire the rule.
Mined into playbooks
A daily job clusters the delivery loops that reached DONE over the trailing 90
days by template + board kind and proposes a playbook draft for each
recurring pattern (≥ 2 successful runs), pre-filling the tool surface and
proposed eval cases from the template. The same cron mines completed harness
runs the same way (clustered by branch strategy + board kind — see
Harness playbook mining). Drafts land in the
Playbooks queue for a human to review and promote; nothing is enabled
automatically. See Playbook mining.
Delivery correlation (M9.4)
Each stage binds to the M8 delivery graph (PRs, CI runs, commits) with no
manual branch wiring. The run's branch template puts the anchor task key first
(harness/{taskKey}-{slug}-stage-{number}), so the inbound pull_request lands
matched on the anchor task (not orphan) under the existing PR→task rule; the
owning stage is then resolved by matching the PR's head branch to
HarnessStage.branch. Inbound PR/CI events — from both the GitHub webhook
path and the generic CI ingestion path — update that stage's delivery dimension,
and a merged PR over a committed stage re-asserts the run's current-stage
advance (idempotent with the commit boundary). These writes are system-sourced
(no actor, never caller-supplied), atomically guarded against a concurrent branch
change or run archive, and never echo: they write only a harness activity plus
an in-app workspace event — never a GitHub write or a board webhook. A failing
required check is recorded as a delivery failure (surfaced in the Delivery tab
and the live registry) but does not auto-revert the terminal committed state —
that needs the merge gate, a fix, or a new iteration. get_harness surfaces a
delivery block (per-stage branch/commit + correlated PR state and CI roll-up)
and the harness page gains a Delivery tab; the MCP surface is unchanged (the
rich PR/CI is also reachable through the M8 delivery tools).
Who may act — turn discipline
Two independent guarantees keep a stuck or hallucinating agent from corrupting state:
- Claim + heartbeat. A runner
claim_agent_jobs the current turn, opening a single live AgentRun (a database compare-and-swap guarantees only one open run per job). Itheartbeat_agent_jobs while the session works; a run whose heartbeat lapses is swept toEXPIREDand the job toBLOCKED, surfacing in the Control Tower's Stuck runs queue. - Turn-guard. Every
submit_*carries the claimedrunIdand is rejected withCONFLICTunless (a) the run is open and matches the phase's role, and (b) the calling token is the role's token (planner submits plans, reviewer submits reviews). Submitting out of turn fails loudly.
MCP tools
Reads (read scope): list_agent_jobs, get_agent_job (the poll endpoint —
full phase, turn, runs, findings, gates, and plan versions).
Agent writes (write scope): request_agent_workflow (start),
request_delivery_workflow (start a delivery loop bound to a repo/PR — see
above), submit_plan, submit_review (verdict + findings + verified-finding
ids), submit_execution_result (summary + artifact ids).
Runner lifecycle (admin scope, runner only): claim_agent_job (returns the
runId), heartbeat_agent_job, fail_agent_job.
A planner/reviewer agent PAT needs read,write; the runner PAT needs
read,write,admin. Findings carry a resolutionNote recording exactly how each
was fixed; resubmitting a plan/execution passes resolutions to mark them
addressed.
Severity-gated verdicts. Every review finding carries a required
severity 0–100 (≥80 critical, 60–79 high, 40–59 medium, 20–39 low, <20 nit —
the same bands the harness severityFromInt projection uses), and the server
computes the verdict the state machine acts on: CHANGES_REQUESTED stands only
when a blocking (severity ≥ 60) finding backs it — submitted in that call,
or raised in a prior round and still OPEN (excluding ids verified in the same
call). A rejection carried only by sub-blocking findings — or by no findings at
all — is recorded as APPROVED, its findings kept as open follow-ups, and the
AGENT_JOB_REVIEWED activity payload carries verdictCoerced,
submittedVerdict, and coercionReason so the downgrade is auditable. An
APPROVED verdict is never escalated: approving alongside a blocking finding
parks at the evidence/human gate instead. This keeps a strict reviewer's
observations on the record without letting nits burn convergence-budget
iterations (the failure mode that blocked stages under gpt-5.6-sol in July
2026).
Scope disputes go back to the plan (returnToPlan). A rejection means one
of two different things, and conflating them is what makes a stage fight
forever: either the code is wrong, or the stage as planned cannot satisfy
the criterion — the fix needs work the plan's expectedBranchBehavior excludes,
or the finding's closure predicate spans subsystems beyond the stage. The
executor has no authority over scope, so re-rejecting its code can never settle
the second kind. submit_review therefore takes an optional returnToPlan,
which routes a CHANGES_REQUESTED execution review to PLAN_REVISION instead
of EXECUTION_REVISION; the plan gate then puts a human in the loop before work
resumes. It is charged to planIterations (not the execution budget — the
executor did nothing wrong) so a plan↔execute bounce is bounded, and it is
ignored on an approved review, on a plan review, and whenever a round-cap or
adjudication park applies (a park is the stricter reading of "a human must
decide"). M18 stage 6 is the case it exists for: one finding, six review rounds,
four cap parks, and no channel for "this is bigger than this stage."
Disposition completeness (harness stage reviews). Silence is not a
disposition. On a stage review every finding of that job that is still OPEN at
blocking severity must appear in exactly one of findings[] (re-raised),
verifiedFindingIds[] (now fixed), or sustainedFindingIds[] (deliberately left
open); a submission that omits one is rejected INVALID_INPUT naming the
unclassified ids and titles. Without it a reviewer could verify a fix in prose
while leaving the row OPEN simply by not mentioning it — the row kept blocking
the stage and the loop burned rounds on an objection nobody had restated (M18
stage 6, four rounds). It applies to an APPROVED review too: approving over an
open blocker parks at a gate the evidence check can never clear, so an approval
may not go silent either. Scope: the reviewing job's own findings — the ids
verifiedFindingIds can act on. A retry attempt's stage-ledger rows belong to an
earlier job and cannot be verified through this call, so demanding them would be
unsatisfiable; the reopen-adjudication park covers that oscillation instead.
Plain M7 jobs and run-level harness loops are unchanged.
Stage review-round cap (harness stage jobs). The per-job convergence
budget (maxIterations) resets on every retry, so it cannot end a fight by
itself — the M12 run burned four full attempts on one stage. For a harness
stage job the server also counts execution-review rounds plus human
final-acceptance rejections across every attempt on the stage (plan reviews
are gate prerequisites, not revision burns); once a further rejection would
reach the cap (six), the submission is still recorded — its findings and
verifications land in the ledger — but the job parks BLOCKED with a
Review round cap reached… reason instead of opening another revision loop.
The sweep's auto-retry recognizes that marker and leaves the stage to a person:
accept/waive the open findings (or cancel the stage), then retry.
The count window starts at the stage's newest cap park. A retry on its own never resets it (that was the M12 defect the cap exists to prevent), but a cap park is by construction a human decision — the sweep refuses to auto-retry one — so the accept/waive-and-retry re-arms the full budget. Without the window a stage that ever reached the cap re-parked on the first rejection of every later attempt forever: each human unblock bought one more executor pass whose review could never reach the fixer (M18 stage 6, parked at "5 rounds, cap 3").
Run forensics. claim_agent_job accepts a model alongside provider,
and the reference runner presents the configured role binding — so the run ledger
answers "which model actually reviewed this round" and a silent model change
is diagnosable from get_agent_job in minutes.
Human gates
submit_review with an APPROVED verdict opens an AgentApprovalRequest that
surfaces in the Control Tower's Approvals queue and on the job's page. A
human approving the pre-execution gate launches the Work Order and moves the job
to EXECUTING; approving the final gate completes it. For a delivery loop the
post-review gate is the merge gate (kind MERGE), and under
gate_at_promotion a deploy gate (kind DEPLOY) follows it — both resolve
through the same path and surface in the same Approvals queue. Gate decisions are
UI-first: they are made through the single Control Tower resolution path,
never an MCP tool, so an agent cannot approve its own merge or deploy. Cancel and
unblock are available on the job page. A blocked harness stage is the exception:
its job page shows Retry stage to admins, which moves the stage back to
READY and opens a fresh leased AgentJob attempt; it never resumes the failed
attempt in place.
Run-level freeze (M9.5)
A harness run has a second human control beside the gate: a freeze. An
admin freezes a run from the harness page (or via the admin freeze_harness
tool); the run records the freezing actor, a redacted reason, and the prior
status to restore on resume. Freeze is enforced at every Jentrix boundary
the moment it lands:
claim_*rejects — a frozen run starts no new turn.submit_plan/submit_review/submit_execution_resultreject (park) — no submission advances a frozen run's job.heartbeat_*keeps the run/lease alive but returnsfrozen: true.- Brokered
repo:*/deploy:*/ cloud credential requests fail closed — no push/PR/deploy credential is minted while frozen. (The local-commit boundary is likewise refused.)
Runner contract. Jentrix cannot kill a local process, so freeze blocks every
Jentrix-mediated action immediately, and the external runner is contractually
required to pause or interrupt its current subprocess on the next heartbeat/poll
that returns frozen. In-flight local work may therefore continue until the
runner's next tick.
Resume (unfreeze_harness, admin-only — an agent has no path to unfreeze its
own run) restores the run's exact recorded prior status and clears the freeze,
fully attributed. cancel_harness ends a run terminally while preserving its
audit history; set_harness_policy adjusts the loop's budgets/ordering/reviewer
knobs. The harness:freeze / harness:advance policy classes let a team govern
the outer loop itself (Policies).
Run-level stuck queues (M9.5)
A frozen run is one operational exception; a stuck run is the other. Every
harness exception surfaces as an actionable Control Tower
item through seven read-only, self-healing guardedSource queues over the
HarnessRun/HarnessStage spine (the same records-not-fires pattern as the M3
operational queues and the M8.3 delivery queues — resolution routes through the
existing resolution ledger, and each queue returns nothing rather than throwing
when its substrate is absent):
| Queue | Lights up when |
|---|---|
| Stuck harnesses | A run is blocked, a stage sits ready/todo with no execution (the runner is down), or a human gate has aged past its threshold. |
| Frozen harnesses | A run is frozen; a freeze older than the staleness threshold surfaces at a raised severity (a forgotten freeze). |
| Harness approvals | A human gate (commit / authoring / plan / final acceptance) on a harness stage is pending — resolved UI-first on the harness page, never an MCP self-approve. |
| Harness open findings | A critical/high review finding is unresolved and blocks validation. |
| Harness missing evidence | A stage claims validation but is missing required evidence (execution summary / diff / test evidence). |
| Registry drift | The generated registry no longer matches the latest stored snapshot — the in-repo markdown built from it is stale. |
| Lease conflicts | Two live holders contend for the same Work Order / repo path / PR, one of them a harness stage lease. |
These are additive to the per-agent-run Stuck runs / Failed jobs
queues (a lapsed heartbeat still surfaces there) and de-duped with the
generic Approvals queue — a harness gate appears in Harness approvals
only, so an item is never counted twice. The new harness.stuck workspace event
is the canonical outbound signal for these conditions.
Observability
Active jobs and their state appear across the existing Control Tower queues —
Handoffs (awaiting an agent), Approvals (awaiting a human gate),
Stuck runs (lapsed heartbeat), Failed jobs (blocked). The dedicated
Agent Jobs page (/<workspace>/agent-jobs) shows every loop's phase
stepper, current turn, run timeline, and the findings table with how-fixed
notes. Every transition is written to the workspace activity feed, attributed to
the acting agent.
The agent.handoff webhook
On every handoff Jentrix fires an agent.handoff event on the anchor task's
board, which fans out to your outbound webhooks. Verify it like any Jentrix
webhook (see Webhooks). The payload identifies the next role:
{
"event": "agent.handoff",
"data": {
"jobId": "…",
"phase": "PLAN_REVIEW",
"turn": "REVIEWER",
"toRole": "REVIEWER",
"taskId": "…",
"taskKey": "STK-123",
"openFindingCount": 2,
"repoOwnerName": "owner/name"
}
}
The external runner
The reference runner (agents/workflow-runner.ts) listens for agent.handoff
deliveries (and/or polls list_agent_jobs), claims the current turn, and spawns
the runtime/model from the role's durable release binding, passing the claimed
runId. A missing or mismatched route is refused before a model turn; there is
no hard-coded reviewer runtime or silent fallback. It
heartbeats the run while the session works and calls fail_agent_job on a
provider error. jentrix runner setup composes the existing self-hosted bootstrap,
worker enrollment, subscription attestation, and node-bound credential paths;
see agents/README.md.
The bundled reference agents
Moved here from Agent setup variants: three of the four exercise planes (webhooks, CRM sidecars, agent-job turns) that only a full deployment carries, and they ship as one package with one setup story.
agents/ ships four production-shaped agents that exercise the whole platform. It's a standalone package — its dependencies never touch the app build:
| Agent | Shape | Scopes | What it does |
|---|---|---|---|
bug-triage | long-running webhook receiver | read, write | On every created task: sets bug severity, links duplicates, asks for repro steps (idempotent across webhook retries) |
crm-follow-up | cron one-shot | read, write | Drafts follow-up comments on deals closing soon with no recent human activity |
standup | cron one-shot | read | Daily digest of human vs. agent activity — stdout or Slack |
workflow-runner | long-running review-loop driver | read,write,admin | Drives the plan→review→execute→review loop: claims agent-job turns and spawns planner/reviewer/executor sessions, heart-beating each run — see Agent workflows |
Setup
cd agents
pnpm install
cp .env.example .env # STACKS_MCP_URL, STACKS_TOKEN, runtime auth (below)
Runtime variant A: Claude Agent SDK (default)
Set ANTHROPIC_API_KEY in agents/.env. That's it — STACKS_AGENT_PROVIDER defaults to claude.
Runtime variant B: OpenAI Codex SDK (ChatGPT subscription)
No API key — it uses the credentials from a one-time interactive codex login on the machine that runs the agents:
npm i -g @openai/codex && codex login # once, on the host
# in agents/.env:
STACKS_AGENT_PROVIDER=codex
Both runtimes run the same prompts against the same MCP server with shell/filesystem access disabled — the agent's whole world is the Jentrix tool surface. STACKS_AGENT_MODEL optionally overrides the active runtime's model.
Running them
pnpm standup # one-shot; add SLACK_WEBHOOK_URL to post to Slack
pnpm crm-follow-up # one-shot
pnpm bug-triage # server on :8787 — create a task.created webhook pointing at it,
# put the webhook secret in STACKS_WEBHOOK_SECRET
Cron lines for the one-shots:
0 8 * * 1-5 cd /path/to/agents && pnpm standup
0 7 * * * cd /path/to/agents && pnpm crm-follow-up
Hosting variants
Local machine (dev / trying it out). Point STACKS_MCP_URL at your dev server (http://localhost:3000/api/mcp). For bug-triage against a local Jentrix, set STACKS_WEBHOOK_ALLOW_PRIVATE=1 on the Jentrix side so the webhook may target localhost (dev only).
Always-on host (recommended for production). Any small VPS, Railway/Fly.io app, or spare machine:
STACKS_MCP_URL=https://tm.jentrix.ai/api/mcp, production PAT inSTACKS_TOKEN.- Crontab entries for
standup/crm-follow-up. bug-triageas a service (systemd,pm2, or the platform's process manager), exposed via a public HTTPS URL — a tunnel likecloudflaredworks if the host has no public ingress. Point the workspace webhook at that URL.- Codex runtime: run
codex loginonce on the host (or copy~/.codex/auth.jsonfrom a machine where you logged in).
Vercel (not supported as-is). The agents intentionally don't deploy with the app: bug-triage is a persistent server, both SDKs spawn local subprocesses with multi-minute runs, and Codex's subscription auth requires an interactive login. If you must run agent logic on Vercel, the path is a rewrite: fold the cron agents into the app as CRON_SECRET-guarded API routes using the Claude SDK with ANTHROPIC_API_KEY, schedule them in vercel.json, and accept the function-duration constraints. The always-on-host variant is simpler and keeps agents as true outside consumers of the API.