import { resolve } from "node:path";
import type { Command } from "commander";
import type { EmitContext } from "../commander.ts";
import {
adapterProofInputs,
createBuiltinAdapterRegistry,
probeBinaryVersion,
} from "../core/adapters/index.ts";
import { resolveCoordRoot } from "../core/agents/coord-client.ts";
import { workflowSubscriptionOnly } from "../core/config.ts";
import { findCompletedMissionGoverning, reopenGovernorMission } from "../core/governor/index.ts";
import type { PolicyIsolation } from "../core/policy/index.ts";
import { loadPolicyFile } from "../core/policy/index.ts";
import {
acceptWorkItem,
cancelWorkItem,
createWorkItem,
listWorkItemsWithWarnings,
readWorkItem,
reconcileAllWorkItems,
reconcileWorkItem,
reopenWorkItem,
runWorkItem,
type WorkAttempt,
type WorkRecord,
} from "../core/work/index.ts";
import { WorkflowParkedError } from "../core/workflow/index.ts";
interface WorkCreateOpts {
id?: string;
accept?: string[];
dependsOn?: string;
maxAttempts?: string;
maxUnchargedAttempts?: string;
sourceKind?: "human" | "workflow" | "external";
sourceRef?: string;
actor?: string;
json?: boolean;
}
interface WorkRunOpts {
adapter?: string;
maxAgents?: string;
concurrency?: string;
cwd?: string;
subscriptionOnly?: boolean;
allowApiBilling?: boolean;
policy?: string;
isolation?: PolicyIsolation;
workspaceRoot?: string;
approvalTo?: string;
actor?: string;
json?: boolean;
}
interface GovernanceOpts {
actor?: string;
reason?: string;
json?: boolean;
finding?: string[];
dispose?: string[];
}
const MAX_ATTEMPT_TRANSCRIPT_ERROR_RENDER = 240;
export function registerWorkCommand(program: Command, emit: EmitContext): void {
const registry = createBuiltinAdapterRegistry();
const adapters = registry.ids();
const work = program
.command("work")
.description("Track durable objectives across bounded workflow attempts.");
work
.command("create
")
.description("Create an immutable durable-work intent linked to a workflow script.")
.requiredOption("--objective ", "Objective that must survive execution attempts")
.option("--id ", "Stable work id (generated by default)")
.option("--accept ", "Acceptance statement; repeat for more than one", collect, [])
.option("--depends-on ", "Comma-separated prerequisite work ids")
.option("--max-attempts ", "Charged-attempt ceiling (default 3)")
.option(
"--max-uncharged-attempts ",
"Ceiling on consecutive uncharged upstream attempts (default 3)",
)
.option("--source-kind ", "human | workflow | external", "human")
.option("--source-ref [", "Bounded source reference")
.option("--actor ", "Actor recorded in the creation receipt")
.option("--json", "Emit the complete work record as JSON")
.action((title: string, workflowPath: string, opts: WorkCreateOpts & { objective: string }) => {
withWorkRoot(emit, (coordRoot) => {
const record = createWorkItem({
coordRoot,
id: opts.id,
title,
objective: opts.objective,
workflowPath,
acceptance: opts.accept,
dependencies: splitIds(opts.dependsOn),
maxAttempts: opts.maxAttempts ? Number.parseInt(opts.maxAttempts, 10) : undefined,
maxUnchargedAttempts: opts.maxUnchargedAttempts
? Number.parseInt(opts.maxUnchargedAttempts, 10)
: undefined,
source: opts.sourceKind ? { kind: opts.sourceKind, ref: opts.sourceRef } : undefined,
actor: opts.actor,
});
emitWork(record, opts.json, emit);
});
});
work
.command("list")
.description("List durable work with its currently derived state.")
.option("--state ", "Filter by derived state")
.option("--json", "Emit complete work records as JSON")
.action((opts: { state?: string; json?: boolean }) => {
withWorkRoot(emit, (coordRoot) => {
const result = listWorkItemsWithWarnings(coordRoot);
const records = result.records.filter(
(record) => !opts.state || record.projection.state === opts.state,
);
if (opts.json) {
emit.config({ format: "json" });
emit.data(records);
for (const warning of result.warnings) {
emit.log(renderWorkListWarning(warning.work_id, warning.reason), "warn");
}
} else if (records.length === 0 && result.warnings.length === 0) {
emit.text("no durable work\n");
} else {
emit.text(
`${[
...records.map(renderWorkRow),
...result.warnings.map((warning) =>
renderWorkListWarning(warning.work_id, warning.reason),
),
].join("\n")}\n`,
);
}
});
});
work
.command("show ")
.description("Show one work item, its attempts, and the next explicit action.")
.option("--json", "Emit the complete work record as JSON")
.action((workId: string, opts: { json?: boolean }) => {
withWorkRoot(emit, (coordRoot) => {
emitWork(readWorkItem(coordRoot, workId), opts.json, emit, true);
});
});
work
.command("reconcile [work-id]")
.description("Derive state from durable evidence without starting or retrying work.")
.option("--actor ", "Actor recorded on changed reconciliation receipts")
.option("--json", "Emit reconciled work records as JSON")
.action((workId: string | undefined, opts: { actor?: string; json?: boolean }) => {
withWorkRoot(emit, (coordRoot) => {
const records = workId
? [reconcileWorkItem(coordRoot, workId, opts.actor)]
: reconcileAllWorkItems(coordRoot, opts.actor);
if (opts.json) {
emit.config({ format: "json" });
emit.data(workId ? records[0] : records);
} else if (records.length === 0) {
emit.text("no durable work\n");
} else {
emit.text(`${records.map(renderWorkRow).join("\n")}\n`);
}
});
});
registerRunCommand(work, "run", false, registry, adapters, emit);
registerRunCommand(work, "retry", true, registry, adapters, emit);
registerGovernanceCommand(work, "accept", emit);
registerGovernanceCommand(work, "cancel", emit);
registerGovernanceCommand(work, "reopen", emit);
}
function registerRunCommand(
work: Command,
name: "run" | "retry",
retry: boolean,
registry: ReturnType,
adapters: string[],
emit: EmitContext,
): void {
work
.command(`${name} `)
.description(
retry
? "Start a new bounded attempt after a blocked run."
: "Start ready work or resume its resolved parked attempt.",
)
.option("--max-agents ", "Total-agent ceiling for the workflow")
.option("--concurrency ", "Concurrent-subagent cap")
.option("--cwd ", "Working directory children spawn in")
.option("--adapter ", `Default adapter: ${adapters.join(" | ")}`)
.option("--subscription-only", "Require stored adapter-login billing")
.option("--allow-api-billing", "Permit API-key override billing")
.option("--policy ", "Host policy JSON/JSONC")
.option("--isolation ", "shared | worktree | sandbox | remote")
.option("--workspace-root ", "Writable root the isolated workspace is allocated under")
.option("--approval-to ", "Address durable ASK requests")
.option("--actor ", "Actor recorded in attempt receipts")
.option("--json", "Emit the workflow report or parked result as JSON")
.action(async (workId: string, opts: WorkRunOpts) => {
await withWorkRootAsync(emit, async (coordRoot) => {
if (opts.adapter && !registry.get(opts.adapter)) {
throw new Error(`unknown adapter ${JSON.stringify(opts.adapter)}`);
}
if (opts.workspaceRoot && opts.isolation !== "worktree") {
throw new Error("--workspace-root requires --isolation worktree");
}
try {
// An isolated attempt needs a provider AND the writable root it may
// allocate under; the engine refuses to guess either. Without them a
// worktree request degrades to shared, which the proof records as
// requested vs effective isolation and the reporting below surfaces.
const { createLocalGitWorktreeProvider } = await import("../core/workflow/index.ts");
const workspace = opts.workspaceRoot
? {
provider: createLocalGitWorktreeProvider({ coordRoot }),
writableRoots: [resolve(opts.workspaceRoot)],
}
: undefined;
const report = await runWorkItem({
coordRoot,
workId,
retry,
actor: opts.actor,
engine: {
spawners: registry.spawners(),
defaultAdapter: opts.adapter,
maxAgents: opts.maxAgents ? Number.parseInt(opts.maxAgents, 10) : undefined,
concurrency: opts.concurrency ? Number.parseInt(opts.concurrency, 10) : undefined,
cwd: opts.cwd,
subscriptionOnly:
opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
allowApiBilling: opts.allowApiBilling,
...adapterProofInputs(
registry.list().map((adapter) => adapter.profile),
{ versionProbe: probeBinaryVersion },
),
policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
approvalMode: "park",
approvalAddressee: opts.approvalTo,
isolation: opts.isolation,
workspace,
networkAccess: "enabled",
},
});
if (opts.json) {
emit.config({ format: "json" });
emit.data(report);
} else {
// A requested isolation the run could not honour is recorded in the
// proof as requested vs effective, which nobody reads on a green run.
// Say it here instead, so a silent downgrade to shared cannot pass for
// an isolated attempt.
const degraded =
opts.isolation && opts.isolation !== "shared" && !report.workspaceBinding
? `note: ran shared; ${opts.isolation} isolation was requested but not allocated` +
`${opts.workspaceRoot ? "" : " (no --workspace-root given)"}\n`
: "";
emit.text(
`work ${workId}: run ${report.runId} finished\n${degraded}` +
`proof: ${report.proofPath}\n` +
`next: harn work reconcile ${workId}\n`,
);
}
} catch (error) {
if (error instanceof WorkflowParkedError) {
if (opts.json) {
emit.config({ format: "json" });
emit.data({
status: "parked",
workId,
runId: error.runId,
approvalId: error.approvalId,
transcriptPath: error.transcriptPath,
});
} else {
emit.text(
`work ${workId}: run ${error.runId} parked\napproval: ${error.approvalId}\n` +
`next: resolve the approval, then harn work run ${workId}\n`,
);
}
return;
}
throw error;
}
});
});
}
function registerGovernanceCommand(
work: Command,
name: "accept" | "cancel" | "reopen",
emit: EmitContext,
): void {
work
.command(`${name} `)
.description(
name === "accept"
? "Explicitly accept reviewed proof and complete the objective."
: name === "cancel"
? "Explicitly cancel unfinished durable work."
: "Explicitly reopen terminal or blocked work for a future attempt.",
)
.option("--actor ", "Actor recorded in the governance receipt")
.option("--reason ", "Bounded decision reason")
.option("--json", "Emit the complete work record as JSON");
const command = work.commands.at(-1) as Command;
if (name === "reopen") {
command.option(
"--finding ",
"Correction the next attempt must address (repeatable)",
collectValue,
[] as string[],
);
}
if (name === "accept") {
command.option(
"--dispose ",
"Dispose of an open finding: fixed, or deferred: (repeatable)",
collectValue,
[] as string[],
);
}
command.action((workId: string, opts: GovernanceOpts) => {
withWorkRoot(emit, (coordRoot) => {
const fn =
name === "accept" ? acceptWorkItem : name === "cancel" ? cancelWorkItem : reopenWorkItem;
// ADR 0050: a reopen under a mission that already succeeded has to reopen the
// mission too, or the item lands in ready_work that the governor will never
// dispatch. Resolve the goal BEFORE touching the work item so a refusal leaves
// nothing half-done.
const goalId =
name === "reopen"
? findCompletedMissionGoverning(coordRoot, workId, (warning) => {
emit.log(renderGovernorEnumerationWarning(warning.goal_id, warning.reason), "warn");
})
: undefined;
const record = fn(coordRoot, workId, {
...opts,
...(opts.finding?.length ? { findings: opts.finding } : {}),
...(opts.dispose?.length ? { dispositions: opts.dispose.map(parseDisposition) } : {}),
});
if (goalId) {
reopenGovernorMission({
coordRoot,
goalId,
actor: opts.actor,
reason: opts.reason?.trim() || `work ${workId} was reopened after mission completion`,
});
emit.log(
`mission ${goalId} had completed; its completion was reopened so ${workId} can be dispatched`,
"warn",
);
}
emitWork(record, opts.json, emit, true);
});
});
}
function collectValue(value: string, previous: string[]): string[] {
return [...previous, value];
}
/** `=fixed` or `=deferred:`. Deferring without a reason is
* rejected downstream, so a lapsed finding cannot look like a decision. */
function parseDisposition(input: string): {
id: string;
outcome: "fixed" | "deferred";
reason?: string;
} {
const split = input.indexOf("=");
if (split <= 0)
throw new Error(`invalid --dispose ${JSON.stringify(input)}; expected =`);
const id = input.slice(0, split).trim();
const rest = input.slice(split + 1).trim();
const [outcome, ...reasonParts] = rest.split(":");
if (outcome !== "fixed" && outcome !== "deferred") {
throw new Error(
`invalid --dispose outcome ${JSON.stringify(outcome)}; expected fixed or deferred`,
);
}
const reason = reasonParts.join(":").trim();
return { id, outcome, ...(reason ? { reason } : {}) };
}
function emitWork(
record: WorkRecord,
json: boolean | undefined,
emit: EmitContext,
detail = false,
) {
if (json) {
emit.config({ format: "json" });
emit.data(record);
return;
}
if (!detail) {
emit.text(`${renderWorkRow(record)}\n`);
return;
}
const projection = record.projection;
const lines = [
`${record.intent.id}: ${record.intent.title}`,
`state: ${projection.state}`,
`objective: ${record.intent.objective}`,
`reason: ${projection.reason}`,
`next: ${projection.next_action}`,
`attempts: ${renderAttemptBudget(record)}`,
];
if (record.intent.dependencies.length)
lines.push(`depends on: ${record.intent.dependencies.join(", ")}`);
if (record.intent.acceptance.length) {
lines.push("acceptance:", ...record.intent.acceptance.map((item) => ` - ${item}`));
}
if (projection.attempts.length) {
lines.push("attempts:", ...projection.attempts.map(renderAttemptRow));
}
if (projection.approval_id) lines.push(`approval: ${projection.approval_id}`);
if (projection.proof_path) lines.push(`proof: ${projection.proof_path}`);
emit.text(`${lines.join("\n")}\n`);
}
export function renderAttemptRow(attempt: WorkAttempt): string {
const statusDetail =
attempt.status === "transcript_unreadable"
? `: ${renderAttemptTranscriptError(attempt.transcript_error)}`
: "";
return ` ${attempt.number}. ${attempt.run_id} ${attempt.status}${statusDetail}`;
}
function renderAttemptTranscriptError(error: string | undefined): string {
const normalized = (error ?? "").replace(/\s+/g, " ").trim();
const value = normalized || "unknown transcript read error";
if (value.length <= MAX_ATTEMPT_TRANSCRIPT_ERROR_RENDER) return value;
return `${value.slice(0, MAX_ATTEMPT_TRANSCRIPT_ERROR_RENDER - 3).trimEnd()}...`;
}
function renderWorkRow(record: WorkRecord): string {
const projection = record.projection;
return `${record.intent.id} ${projection.state.padEnd(17)} ${renderAttemptBudget(record).padEnd(18)} ${projection.next_action.padEnd(21)} ${record.intent.title}`;
}
function renderWorkListWarning(workId: string, reason: string): string {
return `warning ${workId} unreadable: ${reason}`;
}
function renderGovernorEnumerationWarning(goalId: string, reason: string): string {
return `skipped unreadable governor ${goalId}: ${reason}`;
}
// `max_attempts` budgets CHARGED attempts (ADR 0046), so the budget is
// charged/max, not the raw attempt count. When uncharged environment/upstream
// attempts happened, surface them as a suffix rather than folding them into the
// numerator, where they would read as spent budget the item still has.
export function renderAttemptBudget(record: WorkRecord): string {
const { charged_attempts, attempts_used } = record.projection;
const uncharged = attempts_used - charged_attempts;
const base = `${charged_attempts}/${record.intent.max_attempts}`;
return uncharged > 0 ? `${base} (+${uncharged} uncharged)` : base;
}
function withWorkRoot(emit: EmitContext, fn: (coordRoot: string) => void): void {
const coordRoot = resolveCoordRoot();
if (!coordRoot) {
emit.error({
code: "no_coord_root",
message: "no .harnery/ coordination root found; run `init` first",
});
emit.setExitCode(1);
return;
}
try {
fn(coordRoot);
} catch (error) {
emit.error({ code: "work_failed", message: (error as Error).message });
emit.setExitCode(1);
}
}
async function withWorkRootAsync(
emit: EmitContext,
fn: (coordRoot: string) => Promise,
): Promise {
const coordRoot = resolveCoordRoot();
if (!coordRoot) {
emit.error({
code: "no_coord_root",
message: "no .harnery/ coordination root found; run `init` first",
});
emit.setExitCode(1);
return;
}
try {
await fn(coordRoot);
} catch (error) {
emit.error({ code: "work_failed", message: (error as Error).message });
emit.setExitCode(1);
}
}
function splitIds(value?: string): string[] {
return value
? value
.split(",")
.map((item) => item.trim())
.filter(Boolean)
: [];
}
function collect(value: string, previous: string[]): string[] {
return [...previous, value];
}
]