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 <workflow>") .description("Create an immutable durable-work intent linked to a workflow script.") .requiredOption("--objective <text>", "Objective that must survive execution attempts") .option("--id <work-id>", "Stable work id (generated by default)") .option("--accept <text>", "Acceptance statement; repeat for more than one", collect, []) .option("--depends-on <ids>", "Comma-separated prerequisite work ids") .option("--max-attempts <n>", "Charged-attempt ceiling (default 3)") .option( "--max-uncharged-attempts <n>", "Ceiling on consecutive uncharged upstream attempts (default 3)", ) .option("--source-kind <kind>", "human | workflow | external", "human") .option("--source-ref <ref>", "Bounded source reference") .option("--actor <name>", "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 <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 <work-id>") .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 <name>", "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<typeof createBuiltinAdapterRegistry>, adapters: string[], emit: EmitContext, ): void { work .command(`${name} <work-id>`) .description( retry ? "Start a new bounded attempt after a blocked run." : "Start ready work or resume its resolved parked attempt.", ) .option("--max-agents <n>", "Total-agent ceiling for the workflow") .option("--concurrency <n>", "Concurrent-subagent cap") .option("--cwd <dir>", "Working directory children spawn in") .option("--adapter <name>", `Default adapter: ${adapters.join(" | ")}`) .option("--subscription-only", "Require stored adapter-login billing") .option("--allow-api-billing", "Permit API-key override billing") .option("--policy <file>", "Host policy JSON/JSONC") .option("--isolation <mode>", "shared | worktree | sandbox | remote") .option("--workspace-root <dir>", "Writable root the isolated workspace is allocated under") .option("--approval-to <address>", "Address durable ASK requests") .option("--actor <name>", "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} <work-id>`) .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 <name>", "Actor recorded in the governance receipt") .option("--reason <text>", "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 <text>", "Correction the next attempt must address (repeatable)", collectValue, [] as string[], ); } if (name === "accept") { command.option( "--dispose <id=outcome[:reason]>", "Dispose of an open finding: fixed, or deferred:<reason> (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]; } /** `<id>=fixed` or `<id>=deferred:<reason>`. 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 <id>=<outcome>`); 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<void>, ): Promise<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 { 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]; }