Skip to main content

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:

PhaseWhose turnAdvanced by
PLAN_DRAFTINGplannerrequest_agent_workflow
PLAN_REVIEWreviewersubmit_plan
PLAN_REVISIONplannersubmit_review (changes requested)
PLAN_APPROVAL_GATEhumansubmit_review (approved)
EXECUTINGplannerhuman approves the pre-execution gate
EXECUTION_REVIEWreviewersubmit_execution_result
EXECUTION_REVISIONplannersubmit_review (changes requested)
FINAL_ACCEPTANCE_GATEhumansubmit_review (approved) — base task loop
MERGE_GATEhumansubmit_review (approved) — delivery loop (the production boundary)
DEPLOY_GATEhumanmerge approved under gate_at_promotion (the Vercel promotion)
DONEhuman approves the final / merge / deploy gate
BLOCKEDhumana failed run, a stalled heartbeat, or non-convergence
CANCELLEDa 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)UseOpens a PRBudget
featureBuild a feature → linked PRyes5
bugfixFix a bug → linked PRyes5
ci_failure_triagePush a fix to an existing PR branch so CI re-triggersno3
release_candidateBundle merged PRs into a releaseno4
hotfixUrgent fix → linked PRyes3

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 a repo:merge GitHub 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 a deploy:production Vercel 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)UseTerminalBudget
authoringSynthesize a PRD → review → validate into an accepted specspec validated, run → planning5

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)UseTerminalBudget
decompositionValidated spec → ordered stages + Work Orders (coverage-enforced)plan validated, run → ready5

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 (pauseStageBLOCKED, releasing the lease) — it requires a blocking, not-yet-accepted Decision — until it is accepted, then resumes it (resumeStageBLOCKED → 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 continuityget_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). It heartbeat_agent_jobs while the session works; a run whose heartbeat lapses is swept to EXPIRED and the job to BLOCKED, surfacing in the Control Tower's Stuck runs queue.
  • Turn-guard. Every submit_* carries the claimed runId and is rejected with CONFLICT unless (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_result reject (park) — no submission advances a frozen run's job.
  • heartbeat_* keeps the run/lease alive but returns frozen: 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):

QueueLights up when
Stuck harnessesA 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 harnessesA run is frozen; a freeze older than the staleness threshold surfaces at a raised severity (a forgotten freeze).
Harness approvalsA 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 findingsA critical/high review finding is unresolved and blocks validation.
Harness missing evidenceA stage claims validation but is missing required evidence (execution summary / diff / test evidence).
Registry driftThe generated registry no longer matches the latest stored snapshot — the in-repo markdown built from it is stale.
Lease conflictsTwo 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:

AgentShapeScopesWhat it does
bug-triagelong-running webhook receiverread, writeOn every created task: sets bug severity, links duplicates, asks for repro steps (idempotent across webhook retries)
crm-follow-upcron one-shotread, writeDrafts follow-up comments on deals closing soon with no recent human activity
standupcron one-shotreadDaily digest of human vs. agent activity — stdout or Slack
workflow-runnerlong-running review-loop driverread,write,adminDrives 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:

  1. STACKS_MCP_URL=https://tm.jentrix.ai/api/mcp, production PAT in STACKS_TOKEN.
  2. Crontab entries for standup / crm-follow-up.
  3. bug-triage as a service (systemd, pm2, or the platform's process manager), exposed via a public HTTPS URL — a tunnel like cloudflared works if the host has no public ingress. Point the workspace webhook at that URL.
  4. Codex runtime: run codex login once on the host (or copy ~/.codex/auth.json from 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.