§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.
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.
| Concern | codex | claude | native |
|---|---|---|---|
| Version probed | codex-cli 0.147.0 | Claude Code 2.1.228 | host harness |
| Launch | codex 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-permissions | harness policy |
| Read-only tier | --sandbox read-only | permission mode | harness policy |
| Session id source | thread.started event → thread_id | system/init event → session_id | none — ephemeral |
| Resume | resume <thread_id> | --resume <session_id> | not applicable |
| Terminal event | turn.completed | result with is_error, total_cost_usd | tool return |
| Final message | -o <file> | result.result field | tool return |
| Long prompt | stdin via trailing - | stdin via --input-format | tool 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:
| Layer | Location | Committed | Owns |
|---|---|---|---|
| prompt | the current user or coordinator instruction | — | always wins; per-task model, effort, concurrency |
| project | <repo>/docs/state/<project>/flight.yaml | yes, optional | a repository whose work needs different defaults |
| host | ~/.config/reckon/flight.yaml | no | this workstation's standing preferences |
| shipped | generated model defaults inside the package | in the wheel | a 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.
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 blocking → effective_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:
| Fence | Content |
|---|---|
| Scope | exclusive write paths; no two concurrent workers share a file |
| Time | an explicit budget; exceeding it means stop and report, never push on |
| Evidence | the gate's measure is the done-when, stated quantitatively |
| Delivery | a 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.
| Surface | Entry point | Purpose |
|---|---|---|
| CLI | reckon flight [--project X] | resolved config plus backend availability; the skill's first pre-flight step |
| CLI | reckon crew dispatch … | flight-resolve, create worktree, launch backend, write live pointer, return run id |
| CLI | reckon crew attach --run R --task T | bind an in-harness dispatch to its live pointer |
| CLI | reckon crew complete --run R … | promote the pointer into the repo ledger with gate verdict, delete the pointer |
| CLI | reckon crew recover | find orphaned pointers — dead process, terminal event in log |
| MCP | crew(project, view=…) | read-only: resolved flight config, roster, live runs, ledger |
| MCP | read_plan / roadmap | gain 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.
| Defect | Evidence | Repaired 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.
| Sprint | Plans | Built by | Closing 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.