0015: Portable coordination-aware workflows
Status: accepted (2026-07-16) · Scope: product tier (harn workflow, src/core/workflow/)
Context
Section titled “Context”Harness-native subagent orchestration is fragmented. Claude Code ships an excellent workflow primitive (bounded stages, schema-gated outputs, code-decides routing) — but it is Claude Code-only. Codex’s subagent modes spawn recursively with no terminus and are widely reported as a token furnace. Cursor has no orchestration at all. And none of them is coordination-aware: a harness-native workflow’s subagents cannot see peers outside their own run, so in a shared checkout a workflow child and an unrelated agent (or human) can silently clobber each other’s files.
harnery already owns the layer that solves the second problem — heartbeats, file claims, the commit guard, the canonical event stream, the web dashboard. The question was whether to expand from coordination (keeping independent agents from colliding) into orchestration (one process driving many agents toward a goal).
Decision
Section titled “Decision”Build harn workflow: a deterministic engine that runs a small throwaway JS script (export default async ({agent, parallel, stage, log}) => …) whose stages fan out to subagents spawned as headless harness-CLI subprocesses (claude -p --output-format json first; codex/cursor behind the same Spawner signature later). The engine guarantees:
- Bounded — hard total-agent ceiling (default 50) + a shared concurrency pool (default 4). Subagents are leaf processes; there is no recursive self-spawning path.
- Terminating — the run ends when the script returns.
- Schema-gated — with
schema, a stage’s reply must strict-parse and validate (tiny built-in subset validator; no ajv dependency); failures re-prompt with the validation errors appended, bounded bymaxAttempts. Deterministic script code, not a model, routes between stages. - Journaled — every stage/agent start+end lands in
.harnery/workflows/<run-id>/journal.jsonlwith cost, duration, and child session id. - Coordination-registered — children run with the host’s hooks ON, so they emit the full session lifecycle into the canonical event stream and stay visible to peers.
Two empirical findings from the spike are load-bearing in the spawn adapter:
- Inherited
CLAUDE*env must be deleted, not blanked — a nested CLI under a live session otherwise exits 1 with empty output; an empty-string var still reads as set. - The end-of-turn ritual must exempt workflow children, not lose them. With hooks on, the coordination Stop hook blocked headless children for skipping the status-box ritual, burning their turn budget on re-prompts (
error_max_turns). Disabling all hooks fixes the block but destroys coord capture. Children therefore run withHARNERY_WORKFLOW_CHILD=1and the stop-hook rule returnsstop-hook.workflow_childallow — ritual skipped, heartbeats + events kept. Verified live: child sessions showsession.start → user_prompt.submit → turn.stop → session.endwith zero rule blocks.
Alternatives considered
Section titled “Alternatives considered”- Defer to Claude Code’s built-in workflows. Rejected: CC-only, and blind to cross-session peers — the collision risk harnery exists to prevent.
- Enhance
harn councilwith conditional round-routing instead. Rejected as the primary move: councils solve structured decisions, not general work fan-out, and don’t spawn headless children. (Still open as a follow-on.) - Orchestrate only human-launched, coord-registered peers (no spawning). Rejected: loses the one-command fan-out that makes workflows useful; kept as a degraded mode idea if headless CLIs prove unstable.
- Full JSON Schema validation via ajv. Rejected: a dependency for validation depth stage gates don’t need; the subset (type/properties/required/items/enum) covers routing discriminants.
Consequences
Section titled “Consequences”- harnery’s public identity grows from “coordination” to “coordination + a portable orchestration surface.” Coordination remains the pitch; the engine is sold as coordination-aware orchestration, which no harness-native implementation offers.
- Orchestration is inherently token-hungry. The engine’s job is to make the burn bounded and visible (caps, per-agent cost in the journal and RunReport), not to hide it. Headless children in a repo with a large instructions file additionally pay that file’s cache-write per child (~72K tokens in the reference host) — surfacing projected per-stage cost is Phase 3+ work.
- Per-harness headless contracts (
codex exec,cursor-agent) drift; adapters isolate that maintenance to one file each. The sharedbuildChildEnvscrub covers all three env families (CLAUDE*/CODEX*/CURSOR*). - Shipped in the same arc (2026-07-16): the codex + cursor adapters behind a spawner registry with per-agent
harnessselection;--resume-fromreplay keyed on (stage, harness, model, maxTurns, schema, original prompt) with results captured inagent.end; the per-child context-cost heads-up (contextTokensPerChildEstimate); and the journal-driven/workflowsweb view (list + stages→agents detail). The codex adapter was live-verified same-day against codex-cli 0.144.5 (schema-gated triage + text stages via--harness codex, API-key auth). Child-heartbeat linkage landed 2026-07-16:session.startfor a workflow child now carriesworkflow_run_id(read from the env the spawn adapters stamp), the heartbeat projector copies it onto the row (including writeHeartbeat’s persisted-field allowlist — the field is dropped on disk-write without that), and the/workflows/[runId]page badges journal rows whose child session is still live in.harnery/active/. Verified live on the event stream; heartbeat persistence locked by a projector unit test (short-lived children end before a by-hand check can catch the file). Cursor was live-verified 2026-07-17 against cursor-agent 2026.07.16 (schema triage + text stages via--harness cursor, subscription auth), closing the last open adapter; the verification surfaced that headless cursor requires--trustfor the workspace (now in the adapter argv — same posture as codex’s--sandbox workspace-write). All three adapters are verified end-to-end. Auth posture, verified live 2026-07-17: children inherit the harness CLI’s own login (claude = OAuth subscription seat, codex = ChatGPT login) — no API-key billing is required, and the child-env scrub touches session vars only, never credentials. An exportedANTHROPIC_API_KEY/OPENAI_API_KEYin the parent shell would override the stored login (harness-CLI precedence) — so billing safeguards shipped 2026-07-17: a per-harness billing probe on first spawn (subscription / api-key / api-key-override), hard refusal of the silent-override state unless--allow-api-billing, and--subscription-only(flag,workflow.subscriptionOnlyconfig pin, orHARNERY_WORKFLOW_SUBSCRIPTION_ONLYenv) which scrubs every key var from child envs and fails loud on a provably-absent login. Key-only hosts (CI) keep working: api-key mode proceeds with a notice, andCURSOR_API_KEYis carved out of theCURSOR*session scrub (it’s a credential — previously it was deleted, which broke key-only cursor hosts). Login detection is heuristic (credential-file locations) with anunknownstate that never hard-fails; the harness CLI stays the final authority. Verified live: a fakeANTHROPIC_API_KEYalongside the real login is refused pre-spawn, and the same poisoned env under--subscription-onlycompletes on the subscription login.