Skip to content

harn work

harn work create <title> <workflow> --objective <text>
[--accept <text> ...] [--depends-on <ids>]
[--max-attempts <n>] [--source-kind <kind>] [--source-ref <ref>]
[--id <work-id>] [--actor <name>] [--json]
harn work list [--state <state>] [--json]
harn work show <work-id> [--json]
harn work run <work-id> [workflow options] [--json]
harn work retry <work-id> [workflow options] [--json]
harn work reconcile [work-id] [--actor <name>] [--json]
harn work accept|cancel|reopen <work-id> [--actor <name>] [--reason <text>] [--json]
harn work reopen <work-id> [--finding <text> ...]
harn work accept <work-id> [--dispose <id>=fixed|deferred:<reason> ...]

A work item is a durable objective. A workflow run is one bounded attempt to advance it. The distinction lets a failed attempt remain inspectable, a parked attempt resume without being counted twice, and a passing attempt wait for explicit review rather than claiming the objective is complete.

Terminal window
harn work create "Release the package" ./workflows/release.mjs \
--objective "Publish a verified package release" \
--accept "The full suite passes" \
--accept "The installed package smoke test passes" \
--max-attempts 3
harn work list
harn work show work-2026-07-22T12-00-00-000Z-a1b2c3

Creation freezes the workflow’s absolute path and SHA-256. Changing the script later is refused; create a new work item for a changed execution contract. Intent lives in a private intent.json, while transitions append to a private events.jsonl. Reads rebuild current state and validate the complete bounded history.

--depends-on accepts comma-separated work IDs. Dependencies must already exist, and every one must reach explicitly accepted succeeded before the new item becomes ready. Dependencies are immutable in the first schema.

Terminal window
harn work run <work-id> --adapter codex --subscription-only
harn work retry <work-id>

run starts ready work. It records an attempt and stable workflow run ID before loading the workflow script. Standard workflow options are available: --adapter, --max-agents, --concurrency, --cwd, --subscription-only, --allow-api-billing, --policy, --isolation, --workspace-root, and --approval-to.

Running an attempt in an isolated workspace

Section titled “Running an attempt in an isolated workspace”

--isolation worktree needs somewhere to put the workspace, so pass --workspace-root with a directory that contains both the checkout and its Git administrative directory. For a submodule or a linked worktree that means the enclosing repository, not the checkout itself:

Terminal window
harn work run <work-id> --isolation worktree --workspace-root /path/to/repo

The attempt then owns a real workspace binding, recorded in the run’s proof under execution.binding with owner.kind: "work_attempt".

Asking for isolation without a writable root does not fail the run. The attempt falls back to shared and says so:

note: ran shared; worktree isolation was requested but not allocated (no --workspace-root given)

--workspace-root without --isolation worktree is refused, since the engine only honours a writable root under worktree isolation.

The workflow receives the immutable assignment as ctx.work and its attempt identity as ctx.attempt:

export default async ({ work, attempt, agent }) => {
if (!work || !attempt) throw new Error("durable work context required");
const retry =
attempt.trigger === "retry"
? `\nPrior failure: ${attempt.prior.causes.join(", ")}`
: "";
return agent(
`Objective: ${work.objective}\nAcceptance:\n${work.acceptance.join("\n")}${retry}`,
);
};

Harnery freezes the work ID, title, objective, and acceptance criteria in the workflow run manifest before executing the script. The terminal proof carries the same value. It also freezes the one-based attempt number and whether the run is an initial attempt or retry. A retry adds the prior run ID, deterministic failure causes, bounded error, acceptance counts, and unresolved criteria. Harnery does not inject that data into child prompts automatically.

A parked attempt resumes from the manifest snapshot instead of rereading or synthesizing context. It remains the same attempt and retains its original ctx.attempt.

When host policy parks the workflow, the work item derives awaiting_approval. Resolve the request with harn approval, then run the work item again. Harnery resumes the same workflow run and does not charge another attempt. A failed or lost run derives blocked; only the explicit retry command creates a new attempt, and the immutable attempt ceiling still applies. The retry’s ctx.attempt.prior is derived from the preceding proof. Missing proof is reported as lost without guessing a cause.

--max-attempts budgets attempts that produced information about the work (ADR 0046). An attempt that never touched the work does not spend the budget, and the two ways that can happen get opposite handling:

  • Environment. The vendor binary was absent, so the run never started. Retrying an unchanged environment cannot help, so the item stops immediately: state: blocked, next: none, with a reason naming the missing precondition. Fix the precondition, then harn work retry, since the attempt was uncharged, so the budget is intact.
  • Upstream. The vendor was reached and refused (a 5xx or 429 status, or a circuit-open message). The attempt is uncharged and stays retryable, because the vendor may recover. Consecutive uncharged attempts are bounded by --max-uncharged-attempts (default 3); at the bound the item stops with a reason saying it is blocked waiting on an outside service, distinct from blocked on the work.

Anything not positively identified as environment or upstream is charged exactly as before. The attempts: line in harn work show reads charged/max, and appends (+N uncharged) when uncharged attempts have happened, so an uncharged retry never reads as spent budget:

attempts: 1/3 (+2 uncharged)
Terminal window
harn work reconcile <work-id>
harn work accept <work-id> --actor reviewer --reason "proof and artifacts reviewed"

Reconciliation compares the ledger with dependency state, workflow transcripts, approval records, leases, and proof. It records a changed state and next action but never executes that action. Running it repeatedly over unchanged evidence does not append duplicate events.

A passing workflow proof derives in_review. It does not derive succeeded. accept is the explicit completion decision. cancel is also explicit, and reopen retains every event and attempt while making a future attempt possible.

The local dashboard exposes the same projection at /work, with links to each attempt’s workflow transcript and current approval or proof.

Reviewers miss things. When you look at work sitting in in_review and find a defect the workflow’s own review passed, reopen it with a finding rather than accepting it or throwing the work away:

Terminal window
harn work reopen <work-id> --actor reviewer \
--finding "The bounded writer throws on a large result instead of truncating it."

--finding is repeatable. Each finding gets a stable id and is carried into the next attempt’s frozen context as attempt.findings, alongside the actor who raised it, so a workflow can read it and act on it:

export default async ({ agent, attempt }) => {
const corrections = (attempt?.findings ?? [])
.map((finding) => `- ${finding.statement}`)
.join("\n");
return await agent(`Address these operator findings first:\n${corrections}`);
};

Acceptance then fails closed until you say what happened to each one:

Terminal window
harn work accept <work-id> --dispose f1=fixed
harn work accept <work-id> --dispose f1=deferred:tracked separately, out of scope

Deferring requires a reason, and both the finding and its disposition stay in the event log. The team that missed a defect does not get to certify that it was fixed; that judgement stays with the operator who raised it. See ADR 0043.

A mission that reached succeeded is where you are most likely to read the output and find something, so reopening work beneath one is the ordinary case rather than an edge. It reopens the mission along with the item:

Terminal window
harn work reopen <work-id> --actor reviewer --finding "..."
# mission goal-ship-it had completed; its completion was reopened so <work-id>
# can be dispatched

The accepted completion stays in the plan log. A superseding plan.reopened event is appended beside it, the goal returns to ready, and harn governor run dispatches the reopened item ahead of any milestone reassessment. Run it twice and nothing further happens. See ADR 0050.