harnery 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +238 -0
- package/dist/commands/work.d.ts +4 -0
- package/dist/commands/work.d.ts.map +1 -0
- package/dist/commands/work.js +281 -0
- package/dist/core/supervisor/index.d.ts +3 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +2 -0
- package/dist/core/supervisor/read.d.ts +2 -0
- package/dist/core/supervisor/read.d.ts.map +1 -0
- package/dist/core/supervisor/read.js +1 -0
- package/dist/core/supervisor/runner.d.ts +34 -0
- package/dist/core/supervisor/runner.d.ts.map +1 -0
- package/dist/core/supervisor/runner.js +198 -0
- package/dist/core/supervisor/state.d.ts +69 -0
- package/dist/core/supervisor/state.d.ts.map +1 -0
- package/dist/core/supervisor/state.js +413 -0
- package/dist/core/work/index.d.ts +3 -0
- package/dist/core/work/index.d.ts.map +1 -0
- package/dist/core/work/index.js +2 -0
- package/dist/core/work/read.d.ts +2 -0
- package/dist/core/work/read.d.ts.map +1 -0
- package/dist/core/work/read.js +1 -0
- package/dist/core/work/runner.d.ts +10 -0
- package/dist/core/work/runner.d.ts.map +1 -0
- package/dist/core/work/runner.js +63 -0
- package/dist/core/work/state.d.ts +109 -0
- package/dist/core/work/state.d.ts.map +1 -0
- package/dist/core/work/state.js +698 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +41 -9
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +1 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -0
- package/dist/core/workflow/run-state.d.ts +3 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +14 -0
- package/dist/core/workflow/specialists.d.ts +7 -0
- package/dist/core/workflow/specialists.d.ts.map +1 -0
- package/dist/core/workflow/specialists.js +78 -0
- package/dist/core/workflow/types.d.ts +24 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/package.json +21 -1
- package/src/commander.ts +4 -0
- package/src/commands/supervisor.ts +315 -0
- package/src/commands/work.ts +367 -0
- package/src/core/supervisor/index.ts +24 -0
- package/src/core/supervisor/read.ts +14 -0
- package/src/core/supervisor/runner.ts +278 -0
- package/src/core/supervisor/state.ts +552 -0
- package/src/core/work/index.ts +24 -0
- package/src/core/work/read.ts +15 -0
- package/src/core/work/runner.ts +81 -0
- package/src/core/work/state.ts +900 -0
- package/src/core/workflow/engine.ts +42 -9
- package/src/core/workflow/index.ts +1 -0
- package/src/core/workflow/proof.ts +3 -0
- package/src/core/workflow/run-state.ts +20 -0
- package/src/core/workflow/specialists.ts +97 -0
- package/src/core/workflow/types.ts +25 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import type { EmitContext } from "../commander.ts";
|
|
5
|
+
import { workflowSubscriptionOnly } from "../core/config.ts";
|
|
6
|
+
import { createBuiltinHarnessRegistry } from "../core/harnesses/index.ts";
|
|
7
|
+
import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
|
|
8
|
+
import type { PolicyIsolation } from "../core/policy/index.ts";
|
|
9
|
+
import { loadPolicyFile } from "../core/policy/index.ts";
|
|
10
|
+
import {
|
|
11
|
+
createSupervisor,
|
|
12
|
+
listSupervisors,
|
|
13
|
+
readSupervisor,
|
|
14
|
+
runSupervisor,
|
|
15
|
+
type SupervisorRecord,
|
|
16
|
+
type SupervisorRunReport,
|
|
17
|
+
} from "../core/supervisor/index.ts";
|
|
18
|
+
import type { WorkflowSpecialistProfile } from "../core/workflow/index.ts";
|
|
19
|
+
|
|
20
|
+
interface CreateOpts {
|
|
21
|
+
team: string;
|
|
22
|
+
id?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
maxCycles?: string;
|
|
25
|
+
maxRuntimeMs?: string;
|
|
26
|
+
maxParallelWork?: string;
|
|
27
|
+
maxTotalAttempts?: string;
|
|
28
|
+
maxAgentsPerWork?: string;
|
|
29
|
+
agentConcurrency?: string;
|
|
30
|
+
acceptPassingProof?: boolean;
|
|
31
|
+
resumeApproved?: boolean;
|
|
32
|
+
retryBlocked?: boolean;
|
|
33
|
+
json?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RunOpts {
|
|
37
|
+
harness?: string;
|
|
38
|
+
cwd?: string;
|
|
39
|
+
subscriptionOnly?: boolean;
|
|
40
|
+
allowApiBilling?: boolean;
|
|
41
|
+
policy?: string;
|
|
42
|
+
isolation?: PolicyIsolation;
|
|
43
|
+
approvalTo?: string;
|
|
44
|
+
actor?: string;
|
|
45
|
+
json?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function registerSupervisorCommand(program: Command, emit: EmitContext): void {
|
|
49
|
+
const registry = createBuiltinHarnessRegistry();
|
|
50
|
+
const supervisor = program
|
|
51
|
+
.command("supervisor")
|
|
52
|
+
.description("Run a bounded specialist team over a durable-work dependency graph.");
|
|
53
|
+
|
|
54
|
+
supervisor
|
|
55
|
+
.command("create <root-work-id>")
|
|
56
|
+
.description("Freeze a goal, specialist team, automation policy, and execution limits.")
|
|
57
|
+
.requiredOption("--team <file>", "JSON object mapping specialist ids to profiles")
|
|
58
|
+
.option("--id <goal-id>", "Stable goal id (generated by default)")
|
|
59
|
+
.option("--title <text>", "Goal title (defaults to root work title)")
|
|
60
|
+
.option("--max-cycles <n>", "Foreground cycle ceiling (default 50)")
|
|
61
|
+
.option("--max-runtime-ms <n>", "Foreground wall-time ceiling (default 14400000)")
|
|
62
|
+
.option("--max-parallel-work <n>", "Concurrent work-item ceiling (default 1)")
|
|
63
|
+
.option("--max-total-attempts <n>", "Graph-wide attempt ceiling (default 100)")
|
|
64
|
+
.option("--max-agents-per-work <n>", "Child-agent ceiling per work item (default 20)")
|
|
65
|
+
.option("--agent-concurrency <n>", "Child concurrency per work item (default 4)")
|
|
66
|
+
.option("--accept-passing-proof", "Allow the supervisor to explicitly accept passing proof")
|
|
67
|
+
.option("--no-resume-approved", "Stop after an approval instead of resuming its parked run")
|
|
68
|
+
.option("--retry-blocked", "Allow bounded retry of blocked work")
|
|
69
|
+
.option("--json", "Emit the complete supervisor record as JSON")
|
|
70
|
+
.action((rootWorkId: string, opts: CreateOpts) => {
|
|
71
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
72
|
+
const specialists = readTeamFile(opts.team);
|
|
73
|
+
for (const [id, profile] of Object.entries(specialists)) {
|
|
74
|
+
if (profile.harness && !registry.get(profile.harness)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`specialist ${id} names unknown harness ${JSON.stringify(profile.harness)}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
emitSupervisor(
|
|
81
|
+
createSupervisor({
|
|
82
|
+
coordRoot,
|
|
83
|
+
rootWorkId,
|
|
84
|
+
specialists,
|
|
85
|
+
id: opts.id,
|
|
86
|
+
title: opts.title,
|
|
87
|
+
limits: {
|
|
88
|
+
max_cycles: integer(opts.maxCycles),
|
|
89
|
+
max_runtime_ms: integer(opts.maxRuntimeMs),
|
|
90
|
+
max_parallel_work: integer(opts.maxParallelWork),
|
|
91
|
+
max_total_attempts: integer(opts.maxTotalAttempts),
|
|
92
|
+
max_agents_per_work: integer(opts.maxAgentsPerWork),
|
|
93
|
+
agent_concurrency: integer(opts.agentConcurrency),
|
|
94
|
+
},
|
|
95
|
+
automation: {
|
|
96
|
+
accept_passing_proof: opts.acceptPassingProof,
|
|
97
|
+
resume_approved: opts.resumeApproved,
|
|
98
|
+
retry_blocked: opts.retryBlocked,
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
opts.json,
|
|
102
|
+
emit,
|
|
103
|
+
true,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
supervisor
|
|
109
|
+
.command("list")
|
|
110
|
+
.description("List durable goals and their derived next action.")
|
|
111
|
+
.option("--json", "Emit complete supervisor records as JSON")
|
|
112
|
+
.action((opts: { json?: boolean }) => {
|
|
113
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
114
|
+
const records = listSupervisors(coordRoot);
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
emit.config({ format: "json" });
|
|
117
|
+
emit.data(records);
|
|
118
|
+
} else if (records.length === 0) {
|
|
119
|
+
emit.text("no durable supervisors\n");
|
|
120
|
+
} else {
|
|
121
|
+
emit.text(`${records.map(renderSupervisorRow).join("\n")}\n`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
supervisor
|
|
127
|
+
.command("show <goal-id>")
|
|
128
|
+
.description("Show one goal, frozen team, bounds, graph state, and next action.")
|
|
129
|
+
.option("--json", "Emit the complete supervisor record as JSON")
|
|
130
|
+
.action((goalId: string, opts: { json?: boolean }) => {
|
|
131
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
132
|
+
emitSupervisor(readSupervisor(coordRoot, goalId), opts.json, emit, true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
registerRunCommand(supervisor, "tick", registry, emit);
|
|
137
|
+
registerRunCommand(supervisor, "run", registry, emit);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function registerRunCommand(
|
|
141
|
+
supervisor: Command,
|
|
142
|
+
mode: "tick" | "run",
|
|
143
|
+
registry: ReturnType<typeof createBuiltinHarnessRegistry>,
|
|
144
|
+
emit: EmitContext,
|
|
145
|
+
): void {
|
|
146
|
+
supervisor
|
|
147
|
+
.command(`${mode} <goal-id>`)
|
|
148
|
+
.description(
|
|
149
|
+
mode === "tick"
|
|
150
|
+
? "Perform at most one bounded scheduling cycle."
|
|
151
|
+
: "Run bounded cycles until success, attention, no progress, or budget exhaustion.",
|
|
152
|
+
)
|
|
153
|
+
.option("--harness <name>", "Fallback harness for agent calls without a specialist")
|
|
154
|
+
.option("--cwd <dir>", "Working directory for child agents")
|
|
155
|
+
.option("--subscription-only", "Require stored harness-login billing")
|
|
156
|
+
.option("--allow-api-billing", "Permit API-key override billing")
|
|
157
|
+
.option("--policy <file>", "Host policy JSON/JSONC")
|
|
158
|
+
.option("--isolation <mode>", "shared | worktree | sandbox | remote")
|
|
159
|
+
.option("--approval-to <address>", "Address durable ASK requests")
|
|
160
|
+
.option("--actor <name>", "Actor recorded on supervisor work decisions")
|
|
161
|
+
.option("--json", "Emit the complete run report as JSON")
|
|
162
|
+
.action(async (goalId: string, opts: RunOpts) => {
|
|
163
|
+
await withSupervisorRootAsync(emit, async (coordRoot) => {
|
|
164
|
+
if (opts.harness && !registry.get(opts.harness)) {
|
|
165
|
+
throw new Error(`unknown harness ${JSON.stringify(opts.harness)}`);
|
|
166
|
+
}
|
|
167
|
+
const report = await runSupervisor({
|
|
168
|
+
coordRoot,
|
|
169
|
+
goalId,
|
|
170
|
+
mode,
|
|
171
|
+
actor: opts.actor,
|
|
172
|
+
onLog: opts.json ? undefined : (line) => emit.text(`${line}\n`),
|
|
173
|
+
engine: {
|
|
174
|
+
spawners: registry.spawners(),
|
|
175
|
+
defaultHarness: opts.harness,
|
|
176
|
+
cwd: opts.cwd,
|
|
177
|
+
subscriptionOnly:
|
|
178
|
+
opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
|
|
179
|
+
allowApiBilling: opts.allowApiBilling,
|
|
180
|
+
harnessEvidence: Object.fromEntries(
|
|
181
|
+
registry
|
|
182
|
+
.list()
|
|
183
|
+
.map((adapter) => [
|
|
184
|
+
adapter.profile.id,
|
|
185
|
+
{ toolEvidence: adapter.profile.capabilities.toolEvidence },
|
|
186
|
+
]),
|
|
187
|
+
),
|
|
188
|
+
policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
|
|
189
|
+
approvalMode: "park",
|
|
190
|
+
approvalAddressee: opts.approvalTo,
|
|
191
|
+
isolation: opts.isolation,
|
|
192
|
+
networkAccess: "enabled",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
emitSupervisorReport(report, opts.json, emit);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function readTeamFile(path: string): Record<string, WorkflowSpecialistProfile> {
|
|
201
|
+
const absolute = resolve(path);
|
|
202
|
+
let value: unknown;
|
|
203
|
+
try {
|
|
204
|
+
value = JSON.parse(readFileSync(absolute, "utf8"));
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw new Error(`cannot read supervisor team at ${absolute}: ${(error as Error).message}`);
|
|
207
|
+
}
|
|
208
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
209
|
+
throw new Error("supervisor team must be a JSON object keyed by specialist id");
|
|
210
|
+
}
|
|
211
|
+
return value as Record<string, WorkflowSpecialistProfile>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function emitSupervisor(
|
|
215
|
+
record: SupervisorRecord,
|
|
216
|
+
json: boolean | undefined,
|
|
217
|
+
emit: EmitContext,
|
|
218
|
+
detail = false,
|
|
219
|
+
): void {
|
|
220
|
+
if (json) {
|
|
221
|
+
emit.config({ format: "json" });
|
|
222
|
+
emit.data(record);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!detail) {
|
|
226
|
+
emit.text(`${renderSupervisorRow(record)}\n`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const projection = record.projection;
|
|
230
|
+
const lines = [
|
|
231
|
+
`${record.intent.id}: ${record.intent.title}`,
|
|
232
|
+
`state: ${projection.state}`,
|
|
233
|
+
`root: ${record.intent.root_work_id}`,
|
|
234
|
+
`reason: ${projection.reason}`,
|
|
235
|
+
`next: ${projection.next_action}`,
|
|
236
|
+
`work: ${projection.work_ids.length}`,
|
|
237
|
+
`attempts: ${projection.attempts_used}/${record.intent.limits.max_total_attempts}`,
|
|
238
|
+
`specialists: ${projection.specialists.join(", ") || "none"}`,
|
|
239
|
+
`automation: accept=${record.intent.automation.accept_passing_proof}, ` +
|
|
240
|
+
`resume=${record.intent.automation.resume_approved}, retry=${record.intent.automation.retry_blocked}`,
|
|
241
|
+
];
|
|
242
|
+
if (projection.attention_work.length) {
|
|
243
|
+
lines.push(`attention: ${projection.attention_work.join(", ")}`);
|
|
244
|
+
}
|
|
245
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function emitSupervisorReport(
|
|
249
|
+
report: SupervisorRunReport,
|
|
250
|
+
json: boolean | undefined,
|
|
251
|
+
emit: EmitContext,
|
|
252
|
+
): void {
|
|
253
|
+
if (json) {
|
|
254
|
+
emit.config({ format: "json" });
|
|
255
|
+
emit.data(report);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
emit.text(
|
|
259
|
+
`supervisor ${report.goal_id}: ${report.stop_reason}\n` +
|
|
260
|
+
`reason: ${report.reason}\n` +
|
|
261
|
+
`cycles: ${report.cycles}; dispatches: ${report.dispatches}; acceptances: ${report.acceptances}\n` +
|
|
262
|
+
`state: ${report.projection.state}; next: ${report.projection.next_action}\n`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderSupervisorRow(record: SupervisorRecord): string {
|
|
267
|
+
const projection = record.projection;
|
|
268
|
+
return `${record.intent.id} ${projection.state.padEnd(20)} ${projection.attempts_used}/${record.intent.limits.max_total_attempts} ${projection.next_action.padEnd(18)} ${record.intent.title}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function integer(value: string | undefined): number | undefined {
|
|
272
|
+
if (value === undefined) return undefined;
|
|
273
|
+
if (!/^\d+$/.test(value))
|
|
274
|
+
throw new Error(`expected a positive integer, got ${JSON.stringify(value)}`);
|
|
275
|
+
return Number.parseInt(value, 10);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function withSupervisorRoot(emit: EmitContext, fn: (coordRoot: string) => void): void {
|
|
279
|
+
const coordRoot = findCoordRoot();
|
|
280
|
+
if (!coordRoot) {
|
|
281
|
+
emit.error({
|
|
282
|
+
code: "no_coord_root",
|
|
283
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
284
|
+
});
|
|
285
|
+
emit.setExitCode(1);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
fn(coordRoot);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
emit.error({ code: "supervisor_failed", message: (error as Error).message });
|
|
292
|
+
emit.setExitCode(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function withSupervisorRootAsync(
|
|
297
|
+
emit: EmitContext,
|
|
298
|
+
fn: (coordRoot: string) => Promise<void>,
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
const coordRoot = findCoordRoot();
|
|
301
|
+
if (!coordRoot) {
|
|
302
|
+
emit.error({
|
|
303
|
+
code: "no_coord_root",
|
|
304
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
305
|
+
});
|
|
306
|
+
emit.setExitCode(1);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
await fn(coordRoot);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
emit.error({ code: "supervisor_failed", message: (error as Error).message });
|
|
313
|
+
emit.setExitCode(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { EmitContext } from "../commander.ts";
|
|
3
|
+
import { workflowSubscriptionOnly } from "../core/config.ts";
|
|
4
|
+
import { createBuiltinHarnessRegistry } from "../core/harnesses/index.ts";
|
|
5
|
+
import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
|
|
6
|
+
import type { PolicyIsolation } from "../core/policy/index.ts";
|
|
7
|
+
import { loadPolicyFile } from "../core/policy/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
acceptWorkItem,
|
|
10
|
+
cancelWorkItem,
|
|
11
|
+
createWorkItem,
|
|
12
|
+
listWorkItems,
|
|
13
|
+
readWorkItem,
|
|
14
|
+
reconcileAllWorkItems,
|
|
15
|
+
reconcileWorkItem,
|
|
16
|
+
reopenWorkItem,
|
|
17
|
+
runWorkItem,
|
|
18
|
+
type WorkRecord,
|
|
19
|
+
} from "../core/work/index.ts";
|
|
20
|
+
import { WorkflowParkedError } from "../core/workflow/index.ts";
|
|
21
|
+
|
|
22
|
+
interface WorkCreateOpts {
|
|
23
|
+
id?: string;
|
|
24
|
+
accept?: string[];
|
|
25
|
+
dependsOn?: string;
|
|
26
|
+
maxAttempts?: string;
|
|
27
|
+
sourceKind?: "human" | "workflow" | "external";
|
|
28
|
+
sourceRef?: string;
|
|
29
|
+
actor?: string;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WorkRunOpts {
|
|
34
|
+
harness?: string;
|
|
35
|
+
maxAgents?: string;
|
|
36
|
+
concurrency?: string;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
subscriptionOnly?: boolean;
|
|
39
|
+
allowApiBilling?: boolean;
|
|
40
|
+
policy?: string;
|
|
41
|
+
isolation?: PolicyIsolation;
|
|
42
|
+
approvalTo?: string;
|
|
43
|
+
actor?: string;
|
|
44
|
+
json?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface GovernanceOpts {
|
|
48
|
+
actor?: string;
|
|
49
|
+
reason?: string;
|
|
50
|
+
json?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerWorkCommand(program: Command, emit: EmitContext): void {
|
|
54
|
+
const registry = createBuiltinHarnessRegistry();
|
|
55
|
+
const harnesses = registry.ids();
|
|
56
|
+
const work = program
|
|
57
|
+
.command("work")
|
|
58
|
+
.description("Track durable objectives across bounded workflow attempts.");
|
|
59
|
+
|
|
60
|
+
work
|
|
61
|
+
.command("create <title> <workflow>")
|
|
62
|
+
.description("Create an immutable durable-work intent linked to a workflow script.")
|
|
63
|
+
.requiredOption("--objective <text>", "Objective that must survive execution attempts")
|
|
64
|
+
.option("--id <work-id>", "Stable work id (generated by default)")
|
|
65
|
+
.option("--accept <text>", "Acceptance statement; repeat for more than one", collect, [])
|
|
66
|
+
.option("--depends-on <ids>", "Comma-separated prerequisite work ids")
|
|
67
|
+
.option("--max-attempts <n>", "Attempt ceiling (default 3)")
|
|
68
|
+
.option("--source-kind <kind>", "human | workflow | external", "human")
|
|
69
|
+
.option("--source-ref <ref>", "Bounded source reference")
|
|
70
|
+
.option("--actor <name>", "Actor recorded in the creation receipt")
|
|
71
|
+
.option("--json", "Emit the complete work record as JSON")
|
|
72
|
+
.action((title: string, workflowPath: string, opts: WorkCreateOpts & { objective: string }) => {
|
|
73
|
+
withWorkRoot(emit, (coordRoot) => {
|
|
74
|
+
const record = createWorkItem({
|
|
75
|
+
coordRoot,
|
|
76
|
+
id: opts.id,
|
|
77
|
+
title,
|
|
78
|
+
objective: opts.objective,
|
|
79
|
+
workflowPath,
|
|
80
|
+
acceptance: opts.accept,
|
|
81
|
+
dependencies: splitIds(opts.dependsOn),
|
|
82
|
+
maxAttempts: opts.maxAttempts ? Number.parseInt(opts.maxAttempts, 10) : undefined,
|
|
83
|
+
source: opts.sourceKind ? { kind: opts.sourceKind, ref: opts.sourceRef } : undefined,
|
|
84
|
+
actor: opts.actor,
|
|
85
|
+
});
|
|
86
|
+
emitWork(record, opts.json, emit);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
work
|
|
91
|
+
.command("list")
|
|
92
|
+
.description("List durable work with its currently derived state.")
|
|
93
|
+
.option("--state <state>", "Filter by derived state")
|
|
94
|
+
.option("--json", "Emit complete work records as JSON")
|
|
95
|
+
.action((opts: { state?: string; json?: boolean }) => {
|
|
96
|
+
withWorkRoot(emit, (coordRoot) => {
|
|
97
|
+
const records = listWorkItems(coordRoot).filter(
|
|
98
|
+
(record) => !opts.state || record.projection.state === opts.state,
|
|
99
|
+
);
|
|
100
|
+
if (opts.json) {
|
|
101
|
+
emit.config({ format: "json" });
|
|
102
|
+
emit.data(records);
|
|
103
|
+
} else if (records.length === 0) {
|
|
104
|
+
emit.text("no durable work\n");
|
|
105
|
+
} else {
|
|
106
|
+
emit.text(`${records.map(renderWorkRow).join("\n")}\n`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
work
|
|
112
|
+
.command("show <work-id>")
|
|
113
|
+
.description("Show one work item, its attempts, and the next explicit action.")
|
|
114
|
+
.option("--json", "Emit the complete work record as JSON")
|
|
115
|
+
.action((workId: string, opts: { json?: boolean }) => {
|
|
116
|
+
withWorkRoot(emit, (coordRoot) => {
|
|
117
|
+
emitWork(readWorkItem(coordRoot, workId), opts.json, emit, true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
work
|
|
122
|
+
.command("reconcile [work-id]")
|
|
123
|
+
.description("Derive state from durable evidence without starting or retrying work.")
|
|
124
|
+
.option("--actor <name>", "Actor recorded on changed reconciliation receipts")
|
|
125
|
+
.option("--json", "Emit reconciled work records as JSON")
|
|
126
|
+
.action((workId: string | undefined, opts: { actor?: string; json?: boolean }) => {
|
|
127
|
+
withWorkRoot(emit, (coordRoot) => {
|
|
128
|
+
const records = workId
|
|
129
|
+
? [reconcileWorkItem(coordRoot, workId, opts.actor)]
|
|
130
|
+
: reconcileAllWorkItems(coordRoot, opts.actor);
|
|
131
|
+
if (opts.json) {
|
|
132
|
+
emit.config({ format: "json" });
|
|
133
|
+
emit.data(workId ? records[0] : records);
|
|
134
|
+
} else if (records.length === 0) {
|
|
135
|
+
emit.text("no durable work\n");
|
|
136
|
+
} else {
|
|
137
|
+
emit.text(`${records.map(renderWorkRow).join("\n")}\n`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
registerRunCommand(work, "run", false, registry, harnesses, emit);
|
|
143
|
+
registerRunCommand(work, "retry", true, registry, harnesses, emit);
|
|
144
|
+
registerGovernanceCommand(work, "accept", emit);
|
|
145
|
+
registerGovernanceCommand(work, "cancel", emit);
|
|
146
|
+
registerGovernanceCommand(work, "reopen", emit);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function registerRunCommand(
|
|
150
|
+
work: Command,
|
|
151
|
+
name: "run" | "retry",
|
|
152
|
+
retry: boolean,
|
|
153
|
+
registry: ReturnType<typeof createBuiltinHarnessRegistry>,
|
|
154
|
+
harnesses: string[],
|
|
155
|
+
emit: EmitContext,
|
|
156
|
+
): void {
|
|
157
|
+
work
|
|
158
|
+
.command(`${name} <work-id>`)
|
|
159
|
+
.description(
|
|
160
|
+
retry
|
|
161
|
+
? "Start a new bounded attempt after a blocked run."
|
|
162
|
+
: "Start ready work or resume its resolved parked attempt.",
|
|
163
|
+
)
|
|
164
|
+
.option("--max-agents <n>", "Total-agent ceiling for the workflow")
|
|
165
|
+
.option("--concurrency <n>", "Concurrent-subagent cap")
|
|
166
|
+
.option("--cwd <dir>", "Working directory children spawn in")
|
|
167
|
+
.option("--harness <name>", `Default harness: ${harnesses.join(" | ")}`)
|
|
168
|
+
.option("--subscription-only", "Require stored harness-login billing")
|
|
169
|
+
.option("--allow-api-billing", "Permit API-key override billing")
|
|
170
|
+
.option("--policy <file>", "Host policy JSON/JSONC")
|
|
171
|
+
.option("--isolation <mode>", "shared | worktree | sandbox | remote")
|
|
172
|
+
.option("--approval-to <address>", "Address durable ASK requests")
|
|
173
|
+
.option("--actor <name>", "Actor recorded in attempt receipts")
|
|
174
|
+
.option("--json", "Emit the workflow report or parked result as JSON")
|
|
175
|
+
.action(async (workId: string, opts: WorkRunOpts) => {
|
|
176
|
+
await withWorkRootAsync(emit, async (coordRoot) => {
|
|
177
|
+
if (opts.harness && !registry.get(opts.harness)) {
|
|
178
|
+
throw new Error(`unknown harness ${JSON.stringify(opts.harness)}`);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const report = await runWorkItem({
|
|
182
|
+
coordRoot,
|
|
183
|
+
workId,
|
|
184
|
+
retry,
|
|
185
|
+
actor: opts.actor,
|
|
186
|
+
engine: {
|
|
187
|
+
spawners: registry.spawners(),
|
|
188
|
+
defaultHarness: opts.harness,
|
|
189
|
+
maxAgents: opts.maxAgents ? Number.parseInt(opts.maxAgents, 10) : undefined,
|
|
190
|
+
concurrency: opts.concurrency ? Number.parseInt(opts.concurrency, 10) : undefined,
|
|
191
|
+
cwd: opts.cwd,
|
|
192
|
+
subscriptionOnly:
|
|
193
|
+
opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
|
|
194
|
+
allowApiBilling: opts.allowApiBilling,
|
|
195
|
+
harnessEvidence: Object.fromEntries(
|
|
196
|
+
registry
|
|
197
|
+
.list()
|
|
198
|
+
.map((adapter) => [
|
|
199
|
+
adapter.profile.id,
|
|
200
|
+
{ toolEvidence: adapter.profile.capabilities.toolEvidence },
|
|
201
|
+
]),
|
|
202
|
+
),
|
|
203
|
+
policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
|
|
204
|
+
approvalMode: "park",
|
|
205
|
+
approvalAddressee: opts.approvalTo,
|
|
206
|
+
isolation: opts.isolation,
|
|
207
|
+
networkAccess: "enabled",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
if (opts.json) {
|
|
211
|
+
emit.config({ format: "json" });
|
|
212
|
+
emit.data(report);
|
|
213
|
+
} else {
|
|
214
|
+
emit.text(
|
|
215
|
+
`work ${workId}: run ${report.runId} finished\nproof: ${report.proofPath}\n` +
|
|
216
|
+
`next: harn work reconcile ${workId}\n`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof WorkflowParkedError) {
|
|
221
|
+
if (opts.json) {
|
|
222
|
+
emit.config({ format: "json" });
|
|
223
|
+
emit.data({
|
|
224
|
+
status: "parked",
|
|
225
|
+
workId,
|
|
226
|
+
runId: error.runId,
|
|
227
|
+
approvalId: error.approvalId,
|
|
228
|
+
journalPath: error.journalPath,
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
emit.text(
|
|
232
|
+
`work ${workId}: run ${error.runId} parked\napproval: ${error.approvalId}\n` +
|
|
233
|
+
`next: resolve the approval, then harn work run ${workId}\n`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function registerGovernanceCommand(
|
|
245
|
+
work: Command,
|
|
246
|
+
name: "accept" | "cancel" | "reopen",
|
|
247
|
+
emit: EmitContext,
|
|
248
|
+
): void {
|
|
249
|
+
work
|
|
250
|
+
.command(`${name} <work-id>`)
|
|
251
|
+
.description(
|
|
252
|
+
name === "accept"
|
|
253
|
+
? "Explicitly accept reviewed proof and complete the objective."
|
|
254
|
+
: name === "cancel"
|
|
255
|
+
? "Explicitly cancel unfinished durable work."
|
|
256
|
+
: "Explicitly reopen terminal or blocked work for a future attempt.",
|
|
257
|
+
)
|
|
258
|
+
.option("--actor <name>", "Actor recorded in the governance receipt")
|
|
259
|
+
.option("--reason <text>", "Bounded decision reason")
|
|
260
|
+
.option("--json", "Emit the complete work record as JSON")
|
|
261
|
+
.action((workId: string, opts: GovernanceOpts) => {
|
|
262
|
+
withWorkRoot(emit, (coordRoot) => {
|
|
263
|
+
const fn =
|
|
264
|
+
name === "accept" ? acceptWorkItem : name === "cancel" ? cancelWorkItem : reopenWorkItem;
|
|
265
|
+
emitWork(fn(coordRoot, workId, opts), opts.json, emit, true);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function emitWork(
|
|
271
|
+
record: WorkRecord,
|
|
272
|
+
json: boolean | undefined,
|
|
273
|
+
emit: EmitContext,
|
|
274
|
+
detail = false,
|
|
275
|
+
) {
|
|
276
|
+
if (json) {
|
|
277
|
+
emit.config({ format: "json" });
|
|
278
|
+
emit.data(record);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!detail) {
|
|
282
|
+
emit.text(`${renderWorkRow(record)}\n`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const projection = record.projection;
|
|
286
|
+
const lines = [
|
|
287
|
+
`${record.intent.id}: ${record.intent.title}`,
|
|
288
|
+
`state: ${projection.state}`,
|
|
289
|
+
`objective: ${record.intent.objective}`,
|
|
290
|
+
`reason: ${projection.reason}`,
|
|
291
|
+
`next: ${projection.next_action}`,
|
|
292
|
+
`attempts: ${projection.attempts_used}/${record.intent.max_attempts}`,
|
|
293
|
+
];
|
|
294
|
+
if (record.intent.dependencies.length)
|
|
295
|
+
lines.push(`depends on: ${record.intent.dependencies.join(", ")}`);
|
|
296
|
+
if (record.intent.acceptance.length) {
|
|
297
|
+
lines.push("acceptance:", ...record.intent.acceptance.map((item) => ` - ${item}`));
|
|
298
|
+
}
|
|
299
|
+
if (projection.attempts.length) {
|
|
300
|
+
lines.push(
|
|
301
|
+
"attempts:",
|
|
302
|
+
...projection.attempts.map(
|
|
303
|
+
(attempt) => ` ${attempt.number}. ${attempt.run_id} ${attempt.status}`,
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (projection.approval_id) lines.push(`approval: ${projection.approval_id}`);
|
|
308
|
+
if (projection.proof_path) lines.push(`proof: ${projection.proof_path}`);
|
|
309
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function renderWorkRow(record: WorkRecord): string {
|
|
313
|
+
const projection = record.projection;
|
|
314
|
+
return `${record.intent.id} ${projection.state.padEnd(17)} ${projection.attempts_used}/${record.intent.max_attempts} ${projection.next_action.padEnd(21)} ${record.intent.title}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function withWorkRoot(emit: EmitContext, fn: (coordRoot: string) => void): void {
|
|
318
|
+
const coordRoot = findCoordRoot();
|
|
319
|
+
if (!coordRoot) {
|
|
320
|
+
emit.error({
|
|
321
|
+
code: "no_coord_root",
|
|
322
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
323
|
+
});
|
|
324
|
+
emit.setExitCode(1);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
fn(coordRoot);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
emit.error({ code: "work_failed", message: (error as Error).message });
|
|
331
|
+
emit.setExitCode(1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function withWorkRootAsync(
|
|
336
|
+
emit: EmitContext,
|
|
337
|
+
fn: (coordRoot: string) => Promise<void>,
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const coordRoot = findCoordRoot();
|
|
340
|
+
if (!coordRoot) {
|
|
341
|
+
emit.error({
|
|
342
|
+
code: "no_coord_root",
|
|
343
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
344
|
+
});
|
|
345
|
+
emit.setExitCode(1);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
await fn(coordRoot);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
emit.error({ code: "work_failed", message: (error as Error).message });
|
|
352
|
+
emit.setExitCode(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function splitIds(value?: string): string[] {
|
|
357
|
+
return value
|
|
358
|
+
? value
|
|
359
|
+
.split(",")
|
|
360
|
+
.map((item) => item.trim())
|
|
361
|
+
.filter(Boolean)
|
|
362
|
+
: [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function collect(value: string, previous: string[]): string[] {
|
|
366
|
+
return [...previous, value];
|
|
367
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type RunSupervisorInput,
|
|
3
|
+
runSupervisor,
|
|
4
|
+
type SupervisorDispatchOutcome,
|
|
5
|
+
type SupervisorRunReport,
|
|
6
|
+
type SupervisorStopReason,
|
|
7
|
+
} from "./runner.ts";
|
|
8
|
+
export {
|
|
9
|
+
assertSupervisorId,
|
|
10
|
+
type CreateSupervisorInput,
|
|
11
|
+
collectSupervisorWork,
|
|
12
|
+
createSupervisor,
|
|
13
|
+
listSupervisors,
|
|
14
|
+
newSupervisorId,
|
|
15
|
+
readSupervisor,
|
|
16
|
+
SUPERVISOR_INTENT_SCHEMA_VERSION,
|
|
17
|
+
type SupervisorAutomationPolicy,
|
|
18
|
+
type SupervisorIntent,
|
|
19
|
+
type SupervisorLimits,
|
|
20
|
+
type SupervisorNextAction,
|
|
21
|
+
type SupervisorProjection,
|
|
22
|
+
type SupervisorRecord,
|
|
23
|
+
type SupervisorState,
|
|
24
|
+
} from "./state.ts";
|