§1 — What this is, and what it replaces

This document is the architecture reference for orchestrating external coding-harness workers from reckon. It repurposes the orchestrator pattern published as tuoxiansp/fable-the-boss — an Anthropic-model boss that composes self-contained task prompts, dispatches them to long-lived worker sessions of other vendors' harnesses running as true background processes, and wakes on completion to review reports rather than code. Six of that skillset's ideas survive intact: the session/workspace split, the ephemeral per-task worktree cut from HEAD, the background-dispatch-then-yield loop, the structured advice escape hatch, the reporting-surface discipline, and a live console for otherwise-silent dispatches.

What does not survive is its packaging. The upstream skillset is a standalone skill with its own registry file, its own Node daemon on its own port, and its own dispatch prose. Reckon already owns every one of those concerns: plan state, project config, a systemd-managed HTTP server on 127.0.0.1:8765, an MCP surface, worktree lifecycle via worktree_fleet.py, and a sprint orchestration contract with manifest-to-file delivery. Bolting a second orchestration system beside it would fork the execution story.

So the gap this work closes is narrow and specific. Reckon's reckon-ship already coordinates sprints, isolates workers in worktrees, audits scoped diffs, integrates commits and cleans up conservatively. It has no path to dispatch a worker that is not an in-process subagent of the orchestrator's own harness, no worker roster or session reuse, no live view of an in-flight worker, no structured way for a worker to ask for help instead of thrashing, and no machine-enforced gate between execution waves. Those five things are the entire scope.

§2 — The dispatch seam

The central design question is where harness-specific knowledge lives. Two axes look independent — who orchestrates, and who works — but they vary in the same place. A skill file is the orchestrator's instructions, so consider what changes when a different harness reads reckon-ship/SKILL.md. Target resolution, plan pre-flight through MCP, gate fences, scope allocation, worker verification, commit integration, state writeback, cleanup and reporting are all identical. Only the mechanics of starting a background worker and observing it differ — which is exactly where the worker side varies too.

Both axes therefore collapse into one seam, and that seam belongs in reckon's code rather than in prose an agent reads. The skill states what it wants; reckon resolves how. The consequence is that SKILL.md carries a single dispatch instruction covering every backend, and per-backend flag translation lives in a unit-testable module the agent never sees.

agent /reckon-ship <target> reckon crew dispatch resolves flight config codex CLI claude CLI native — harness subagent launch: cli launch: in-harness reckon spawns · returns run id reckon prepares · agent dispatches
One skill, one uniform call. Backend selection resolves from flight config inside reckon, so the skill never names a model, an effort level or a harness. The agent branches once — on launch: kind, two cases — and adding a fourth harness never adds a third branch.

The single branch is unavoidable and bounded. A launch: cli backend is an external process reckon can spawn, so reckon crew dispatch creates the worktree, launches it, writes the live pointer and returns a run id; the agent backgrounds that call and yields. A launch: in-harness backend is the orchestrator's own delegation primitive, which reckon cannot spawn on its behalf — so reckon prepares the worktree, live pointer, manifest path and resolved fences, returns a dispatch directive, and the agent binds its own task back with reckon crew attach.

This also settles a question that arose during design: whether a second skill should exist for external-harness execution. It should not. Classifying the intended additions showed only two of six were harness-specific — the launch mechanics and session reuse. The other four (a run ledger, the escape hatch, the summary reflex, gate fences) are improvements to reckon-ship that a sibling skill would have duplicated. The durable rule: split a skill when the process differs — a different checkpoint model, a different authority boundary. Never when only the substrate differs.

§3 — Verified backend matrix

All three backends were probed live before this design was accepted, because the long-lived-session half of the pattern depends on each harness exposing a resumable session id through its machine-readable stream. Both external CLIs do, and their shapes are close enough that one document describes both.

Concerncodexclaudenative
Version probedcodex-cli 0.147.0Claude Code 2.1.228host harness
Launchcodex exec --json -C <wt>claude -p --output-format stream-json --verbose --add-dir <wt>harness delegation primitive
Model-m <model>--model <model>harness-selected
Reasoning effort-c model_reasoning_effort=<level>--effort <level>harness-selected
Permission tier--dangerously-bypass-approvals-and-sandbox--dangerously-skip-permissionsharness policy
Read-only tier--sandbox read-onlypermission modeharness policy
Session id sourcethread.started event → thread_idsystem/init event → session_idnone — ephemeral
Resumeresume <thread_id>--resume <session_id>not applicable
Terminal eventturn.completedresult with is_error, total_cost_usdtool return
Final message-o <file>result.result fieldtool return
Long promptstdin via trailing -stdin via --input-formattool argument

Two probe results are worth recording because they de-risk load-bearing assumptions. A codex session resumed by thread_id demonstrably retained memory of its prior turn, confirming that a worker's session can outlive its workspace. And codex 0.147 ships --approve-for-me, a genuinely reviewed tier routing approval requests through automatic review under a workspace-write sandbox — a capability the upstream reference states does not exist on the headless path. It is not the chosen default, because a workspace-write sandbox is inherited by child processes and breaks test runners, builds and anything spawning subprocesses; the detached worktree is the blast-radius boundary instead. It is nonetheless available per-node and should be reconsidered whenever a node does not need to build.

The native backend is not a consolation option. It is the correct choice for short read and synthesis nodes where process-spawn overhead dominates, for nodes needing tools only the host harness exposes, and as the degradation target when no external CLI is on PATH or authenticated. Its presence is also what makes default_backend meaningful: reckon flight probes availability and can fall back.

§4 — Flight control

Routing must be tunable from one surface without editing a skill. Four layers resolve upward, each overriding the one below:

LayerLocationCommittedOwns
promptthe current user or coordinator instructionalways wins; per-task model, effort, concurrency
project<repo>/docs/state/<project>/flight.yamlyes, optionala repository whose work needs different defaults
host~/.config/reckon/flight.yamlnothis workstation's standing preferences
shippedgenerated model defaults inside the packagein the wheela fresh install works with zero config

The prompt layer winning is not a convenience — it is required by the user-space agent policy, which states that model family, concrete model, reasoning effort and worker concurrency are runtime choices for the current task and that skills must not prescribe them. That same policy permits application configuration, which is precisely what the lower three layers are.

A [tool.reckon] section in pyproject.toml was considered and rejected: pyproject.toml is not shipped in the wheel, so it would resolve only from a source checkout and would break reckon build and any wheel install. It also conflates build metadata with runtime routing. Shipped defaults therefore live in the generated model, which is in the wheel.

The schema follows the LinkML idiom already established in this codebase's sibling projects — nova, helix and imas-codex all declare it as an optional authoring extra whose generated Pydantic models and JSON-Schema export are committed, so runtime needs none of the toolchain. Reckon adopts the same shape: reckon/schema/flight.yaml is the LinkML source; reckon/_flight_schema.py and docs/_shared/flight.schema.json are generated and committed; schema = ["linkml>=1.9"] becomes an optional extra. Runtime gains exactly one dependency, pyyaml. Serving the JSON Schema at /_shared/flight.schema.json lets a yaml-language-server header give editor completion while tuning.

The surface itself is deliberately flat enough to hold in one screen — the whole ship tunes here:

version: 1
default_backend: codex                    # codex | claude | native

backends:
  codex:   { launch: cli, command: codex,  model: gpt-5.6-sol,
             effort: high,   sandbox: worktree-full, session_reuse: true,
             concurrency: 3, time_budget: 25m }
  claude:  { launch: cli, command: claude, model: claude-sonnet-5,
             effort: medium, sandbox: worktree-full, session_reuse: true,
             concurrency: 3, time_budget: 25m }
  native:  { launch: in-harness,
             concurrency: 4, time_budget: 25m, session_reuse: false }

roles:
  implement:   {}                         # inherits default_backend
  review:      { sandbox: read-only }     # writes only its manifest
  investigate: { sandbox: read-only }
  cleanup:     {}

gates:    { enforce: strict, require_evidence: true, on_fail: hold }
fences:   { time_budget: 25m, needs_help_after_failures: 2, manifest_required: true }
worktree: { cleanup: conservative }
summary:  { reflex: what-why-how-when, at: [dispatch, completion, micro-plan] }

Roles matter because a team is not only implementers. implement and cleanup produce commits; review and investigate produce a findings manifest and nothing else, so they need no write access beyond that file and drop to a read-only tier as a runtime choice rather than a policy change.

§5 — Ownership: the repo owns its implementation record

Run state has two natures that must not share a home. While a worker is in flight its record changes every few seconds and is worthless once the run ends. Once the run completes, the record is durable evidence of how the plan was implemented — and plans are repo-local and committed, so their implementation record should be too. Storing either in plan HTML is wrong: every progress tick would bump the server-owned version counter and fight optimistic concurrency.

Reckon already has the mechanism for the durable half. ~/.config/reckon/state/<project> is a symlink into <repo>/docs/state/<project>, which is how index.json is server-written and git-committed. A ledger placed beside it inherits exactly that property for free.

live pointer — config home run ledger — owning repository reckon server worktree · log · manifest · pid never committed roster · commit · gate verdict · outcome committed with the plan promote tails each pointer's log for phase, joins the ledger for identity
Three tiers. Nothing transient is ever committed; nothing durable is ever only in a cache. A stale pointer with a dead process and a terminal event in its log is a recoverable orphan, handled the same way as an unmerged worktree.

So: durable roster and completed-run records live in <repo>/docs/state/<project>/crew.json, committed. Ephemeral live pointers live in ~/.config/reckon/crew/live/<run-id>.json, never committed. On completion the orchestrator promotes the pointer into the ledger and deletes it. The server joins both to serve a live view across every mounted project without owning any of their data. The division of file formats follows the same logic reckon already uses — YAML for what humans tune, JSON for what machines record.

§6 — Gates as derived blockers

The gating reflex being hardened here is not new; it was developed in imas-ambix, where a plan declares an Evidence gates section as a two-column table of measure against required evidence before the work starts, with the explicit caveat that thresholds are hypotheses to measure rather than acceptance criteria to tune around. Downstream work stays visibly closed when a gate fails, and negative results stay on the page. Today that reflex is prose, so nothing but discipline enforces it and nothing else can read it.

Making it first-class state resolves a second, measured problem at the same time. Reckon derives a plan's blocked state dynamically — blocking comes from unresolved dependencies, and effective_status projects it over the persisted workflow status. But the projection is one-way: it can add blocked and never remove a stale one.

effective_status('blocked', [])        -> 'blocked'   # never clears
effective_status('active',  [{...}])   -> 'blocked'   # correctly derived
effective_status('active',  [])        -> 'active'

A plan authored plan-status="blocked" therefore reads blocked forever after every blocker has cleared. roadmap already detects this as an orphaned-blocked-status wiring finding — the system knows the state has rotted and files a report instead of resolving it. Two plans across the portfolio currently persist it, so the blast radius is small and the fix is cheap.

The same class of defect exists in sprint items, where it contradicts an explicit written contract. Sprint orchestration states that item lifecycle status and implementation fraction are derived from plan HTML and must never be persisted in the sprint — yet index.json carries a persisted status per item, and it is already stale: distributed-sprint-state reads pending in its sprint while its plan is shipped. roadmap derives the correct rollup, so only consumers of the raw index are misled, but that includes any agent reading project state directly.

One mechanism fixes all three. A gate becomes a declared element carrying its measure, required evidence, verdict and evidence link, anchored to a section and naming the sections it gates. An unpassed gate is a derived blocker, so it flows through the existing blockingeffective_status path. Consequences follow without new machinery: a failed gate renders downstream work blocked automatically, passing it unblocks automatically, no status edit is involved anywhere, and blocked becomes a state that is only ever derived and never stored — which the write path enforces by rejecting it, matching reckon's established reject-write-warn-doctor posture. Persisted sprint-item status goes the same way. A closing sweep across all mounted projects then migrates existing stale labels to the new pattern.

§7 — Worker fences and the escape hatch

Workers are locked into highly specified, time- and scope-limited, evidence-producing bursts. Each dispatch carries four fences and nothing else, because the live plan remains the semantic authority and copying plan prose into a prompt creates a second source of truth that drifts between workers:

FenceContent
Scopeexclusive write paths; no two concurrent workers share a file
Timean explicit budget; exceeding it means stop and report, never push on
Evidencethe gate's measure is the done-when, stated quantitatively
Deliverya named manifest path on disk; long output goes in the file and the reply is the path

Delivery is a fence rather than a convention because of a known failure mode: a background worker can finish its work and end its turn without delivering a report, at which point the runtime signals idle and the node looks failed when it is not. Re-asking often produces another bare idle signal and redispatching repeats work that already succeeded. Requiring the manifest on disk removes the failure mode for the cost of one write.

The escape hatch is structured, because a vague "I'm stuck" wastes as much time as confused thrashing. A worker stops and emits a report whose first line is NEEDS-HELP: followed by four required fields — tried: what was attempted and the observable result, options: two or three concrete paths it can see, leaning: which one and why, and cost-if-wrong: what must be redone if the wrong path is chosen. Those fields turn a plea into a decision brief the orchestrator can act on in one turn.

Knowing when to stop matters as much as how. Named triggers: the same command has failed twice with different fixes attempted; a decision the plan does not settle is required to proceed; the necessary change extends beyond the exclusive write scope; the gate's required evidence cannot be produced with the available tools or data; or the time budget is spent with the gate still closed. On receiving one, the orchestrator answers it itself by default, escalates only genuinely user-owned decisions such as scope trade-offs and irreversible choices, and resumes the same session with the advice — which is why session reuse is load-bearing rather than an optimisation.

§8 — The summary reflex

Dispatches are silent by design and workers report in their own idiom, so the orchestrator owes the lead a readable account at two moments — when work goes out, and when a batch comes back. The same four-axis shape is used when micro-planning the next step, so it is one habit rather than three formats. Four lines, one per axis, at most two lines each:

Dispatching wave 2 — 3 workers
WHAT   §3 /crew routes (impl-a) · §4 watcher tab (impl-b) · §5 MCP in_flight (impl-c)
WHY    §3 unblocks §4 and §5; all three read the §2 ledger; no shared files
HOW    codex gpt-5.6-sol @high, detached worktrees, scopes below, manifests on disk
WHEN   ~20 min each; gate g-live-run closes the wave — §6 stays shut until it passes

Wave 2 complete — 3/3 landed, gate g-live-run PASSED
WHAT   /crew routes + watcher tab + MCP in_flight (1a2b3c4, 5d6e7f8, 9a0b1c2)
WHY    gate evidence: GET /crew returned phase=working 1.8s after dispatch (<3s required)
HOW    all scoped clean on git show --stat; 41 tests green; no out-of-scope paths
WHEN   next §6 north-star — ready, nothing blocks it

Each axis has one job. WHAT names nodes and artifacts; WHY gives the causal reason this wave runs now; HOW carries runtime and isolation facts only; WHEN gives a duration estimate and names the gate that closes the wave. Nothing restates what the plan already says.

One discipline binds the reflex to the gating reflex and is the reason the format earns its place: at completion, WHY carries the gate evidence. That forces every wave report to be quantitative, and makes a wave that cannot state its gate evidence visibly incomplete rather than plausibly done.

§9 — Tool surface

The skill is the uniform surface; the command-line entry points are agent-callable primitives, not a human interface. That has consequences for their contract: output is JSON on stdout by default with human formatting as an afterthought, each call is atomic so an agent cannot perform half of it, nothing is ever interactive, and exit codes are branchable.

Actions belong to the CLI because dispatch must spawn a background process, which an MCP tool cannot usefully do. Reads belong to MCP, which keeps that surface at five tools and honours reckon's ongoing collapse of its tool count.

SurfaceEntry pointPurpose
CLIreckon flight [--project X]resolved config plus backend availability; the skill's first pre-flight step
CLIreckon crew dispatch …flight-resolve, create worktree, launch backend, write live pointer, return run id
CLIreckon crew attach --run R --task Tbind an in-harness dispatch to its live pointer
CLIreckon crew complete --run R …promote the pointer into the repo ledger with gate verdict, delete the pointer
CLIreckon crew recoverfind orphaned pointers — dead process, terminal event in log
MCPcrew(project, view=…)read-only: resolved flight config, roster, live runs, ledger
MCPread_plan / roadmapgain an in_flight block on plan and row payloads

The last row carries more weight than its size suggests. Folding in_flight into the tools agents already call means a second orchestrator session, or a worker reading its own plan, sees that a node is being worked without needing to know the crew tool exists. That is the cheapest available guard against double-dispatch, which for a node holding write scope risks a conflicting second commit.

§10 — Defects found while exercising the tools

These were measured while validating the design and are repaired as part of it. Reckon's own schema audit is clean at 17 of 17 conformant, so these are graph- and tool-surface faults rather than plan-state rot.

DefectEvidenceRepaired in
Persisted blocked never clears effective_status('blocked', []) returns blocked; already reported as orphaned-blocked-status derived-gate-state
Persisted sprint-item status contradicts its own contract and is stale distributed-sprint-state persists pending; its plan is shipped derived-gate-state
roadmap(project="*") unusable through MCP 176 KB response exceeded the token ceiling; read_plan and audit both take progressive view, roadmap takes none inflight-visibility
Portfolio graph faults 81 wiring findings, 69 in imas-efit, including 2 dependency cycles and 4 inactive-hard-dependency errors sweep in derived-gate-state
Skill installation drift risk ~/.claude/skills/reckon-roadmap is a real directory where the other six are symlinks; contents currently identical inflight-visibility
Placeholder passes schema validation as an identifier agent-plan-authoring carries milestone: "—" inflight-visibility

§11 — Delivery shape

Eight plans across three sprints, each sprint closed by one evidence gate. The sequencing is deliberate: the first sprint is built conventionally so that the second and third are executed by the worker team it creates, which dog-foods the whole mechanism on real work rather than a rehearsal.

Continuation is wired at three altitudes rather than one, because a chain that closes only at plan level leaves the other two ends dangling. A worker returns candidate follow-ons it was fenced out of, which the orchestrator folds into the wave or writes as a followup. A plan landing appends the next one-line invocation or records that the chain closes — a rule that exists today but is enforced only by discipline, so it becomes a validated condition of the writeback. A sprint close reports the downstream sprints its plans unblock, derived from the graph, which does not exist at all today. At every altitude, ending without either a next invocation or an explicit no-followup outcome is a failure.

Two measurement questions are answered by the ledger rather than by definition. Staleness is the simpler one: detection already exists in the audit command but never reaches the agent doing the work, so a worker can begin on a plan idle for months with no signal that its assumptions may have expired.

Effort is the deeper one. The scale is a relative doubling series consumed only for critical-path weighting, with nothing tying a letter to an observable — asserted, not measured, and every number built on it inherits that. Dispatching work supplies the missing measurement for free, so effort moves to neutral worker-hours: the expected wall-clock a capable worker takes, deliberately not anchored to a model tier, since a tier-anchored unit would put a routing claim into plan state and would drift silently as capability improved, rebasing every historical estimate. Improvement appears instead as a rising per-agent speed factor, leaving the size of the work unchanged. Read as hours the existing weights are already plausible, so the migration costs no re-estimation.

Feeding actuals back requires two separate loops, and this is where a naive build fails. Predicted-versus-actual mixes estimate error, agent speed and scope change; a single loop bakes worker slowness into the recorded size of the work and the number stops meaning what it claims. So normalised actuals update the plan estimate while aggregate ratios update agent speed, and a scope-changed run is excluded from both because it measures neither. The payoff is a figure that closes the dispatch problem with a measurement: success rate conditioned on task size gives each agent configuration a competence horizon — the size beyond which it starts failing — so a boss computes whether to split rather than judging it.

SprintPlansBuilt byClosing gate
S7 — uniform dispatch flight-control-config, uniform-worker-dispatch orchestrator, conventionally one real codex node lands in a detached worktree with its manifest on disk, dispatched through the uniform call, with both summaries emitted
S8 — execution substrate crew-run-ledger, derived-gate-state, budget-aware-dispatch worker team a dispatch is recorded and promoted; a failed gate blocks downstream work and passing it unblocks, proven by test; no persisted blocked, item status or sprint-activation state remains in any mounted project; a wave holds on exhausted budget and resumes itself
S9 — observation surface inflight-visibility, north-star-orientation, effort-calibration codex team a live run is visible in the SPA and through MCP; north-stars render with plans pointing at them; roadmap("*") returns a usable summary; effort reads in worker-hours with both calibration loops proven separable and competence horizons driving split decisions

The north-star work lands last but changes how everything above is read. A north-star is a durable direction rather than a dated target, defined per project in a small set — a handful at most — with plans declaring which one they serve. Its purpose is to make it visible when a plan points nowhere, and the majority of plans should point at one.