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.
Files changed (66) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +4 -0
  4. package/dist/commands/supervisor.d.ts +4 -0
  5. package/dist/commands/supervisor.d.ts.map +1 -0
  6. package/dist/commands/supervisor.js +238 -0
  7. package/dist/commands/work.d.ts +4 -0
  8. package/dist/commands/work.d.ts.map +1 -0
  9. package/dist/commands/work.js +281 -0
  10. package/dist/core/supervisor/index.d.ts +3 -0
  11. package/dist/core/supervisor/index.d.ts.map +1 -0
  12. package/dist/core/supervisor/index.js +2 -0
  13. package/dist/core/supervisor/read.d.ts +2 -0
  14. package/dist/core/supervisor/read.d.ts.map +1 -0
  15. package/dist/core/supervisor/read.js +1 -0
  16. package/dist/core/supervisor/runner.d.ts +34 -0
  17. package/dist/core/supervisor/runner.d.ts.map +1 -0
  18. package/dist/core/supervisor/runner.js +198 -0
  19. package/dist/core/supervisor/state.d.ts +69 -0
  20. package/dist/core/supervisor/state.d.ts.map +1 -0
  21. package/dist/core/supervisor/state.js +413 -0
  22. package/dist/core/work/index.d.ts +3 -0
  23. package/dist/core/work/index.d.ts.map +1 -0
  24. package/dist/core/work/index.js +2 -0
  25. package/dist/core/work/read.d.ts +2 -0
  26. package/dist/core/work/read.d.ts.map +1 -0
  27. package/dist/core/work/read.js +1 -0
  28. package/dist/core/work/runner.d.ts +10 -0
  29. package/dist/core/work/runner.d.ts.map +1 -0
  30. package/dist/core/work/runner.js +63 -0
  31. package/dist/core/work/state.d.ts +109 -0
  32. package/dist/core/work/state.d.ts.map +1 -0
  33. package/dist/core/work/state.js +698 -0
  34. package/dist/core/workflow/engine.d.ts.map +1 -1
  35. package/dist/core/workflow/engine.js +41 -9
  36. package/dist/core/workflow/index.d.ts +1 -1
  37. package/dist/core/workflow/index.d.ts.map +1 -1
  38. package/dist/core/workflow/proof.d.ts +1 -0
  39. package/dist/core/workflow/proof.d.ts.map +1 -1
  40. package/dist/core/workflow/proof.js +2 -0
  41. package/dist/core/workflow/run-state.d.ts +3 -0
  42. package/dist/core/workflow/run-state.d.ts.map +1 -1
  43. package/dist/core/workflow/run-state.js +14 -0
  44. package/dist/core/workflow/specialists.d.ts +7 -0
  45. package/dist/core/workflow/specialists.d.ts.map +1 -0
  46. package/dist/core/workflow/specialists.js +78 -0
  47. package/dist/core/workflow/types.d.ts +24 -0
  48. package/dist/core/workflow/types.d.ts.map +1 -1
  49. package/package.json +21 -1
  50. package/src/commander.ts +4 -0
  51. package/src/commands/supervisor.ts +315 -0
  52. package/src/commands/work.ts +367 -0
  53. package/src/core/supervisor/index.ts +24 -0
  54. package/src/core/supervisor/read.ts +14 -0
  55. package/src/core/supervisor/runner.ts +278 -0
  56. package/src/core/supervisor/state.ts +552 -0
  57. package/src/core/work/index.ts +24 -0
  58. package/src/core/work/read.ts +15 -0
  59. package/src/core/work/runner.ts +81 -0
  60. package/src/core/work/state.ts +900 -0
  61. package/src/core/workflow/engine.ts +42 -9
  62. package/src/core/workflow/index.ts +1 -0
  63. package/src/core/workflow/proof.ts +3 -0
  64. package/src/core/workflow/run-state.ts +20 -0
  65. package/src/core/workflow/specialists.ts +97 -0
  66. package/src/core/workflow/types.ts +25 -0
@@ -0,0 +1,198 @@
1
+ import { resolve } from "node:path";
2
+ import { acceptWorkItem, reconcileWorkItem, runWorkItem, } from "../work/index.js";
3
+ import { acquireSupervisorLease, readSupervisorIgnoringLease, } from "./state.js";
4
+ export async function runSupervisor(input) {
5
+ const coordRoot = resolve(input.coordRoot);
6
+ const mode = input.mode ?? "run";
7
+ const actor = input.actor?.trim() || `supervisor:${input.goalId}`;
8
+ const log = input.onLog ?? (() => undefined);
9
+ const startedAt = new Date().toISOString();
10
+ const startedMs = Date.now();
11
+ const release = acquireSupervisorLease(coordRoot, input.goalId);
12
+ let cycles = 0;
13
+ let dispatches = 0;
14
+ let acceptances = 0;
15
+ const outcomes = [];
16
+ try {
17
+ const initial = readSupervisorIgnoringLease(coordRoot, input.goalId);
18
+ const cycleLimit = mode === "tick" ? 1 : initial.intent.limits.max_cycles;
19
+ const deadline = startedMs + initial.intent.limits.max_runtime_ms;
20
+ while (cycles < cycleLimit) {
21
+ cycles++;
22
+ let record = refreshGraph(coordRoot, input.goalId, actor);
23
+ log(`[${record.intent.title}] cycle ${cycles}: ${record.projection.state}; ` +
24
+ `${record.projection.ready_work.length} ready, ${record.projection.attention_work.length} attention`);
25
+ if (record.projection.state === "succeeded") {
26
+ return report("succeeded", record.projection.reason, record);
27
+ }
28
+ if (Date.now() >= deadline) {
29
+ return report("budget_exhausted", "supervisor wall-time limit reached", record);
30
+ }
31
+ const before = graphFingerprint(record);
32
+ let changed = false;
33
+ if (record.intent.automation.accept_passing_proof) {
34
+ for (const work of record.work) {
35
+ if (work.projection.state !== "in_review")
36
+ continue;
37
+ acceptWorkItem(coordRoot, work.intent.id, {
38
+ actor,
39
+ reason: "passing proof accepted by frozen supervisor policy",
40
+ });
41
+ acceptances++;
42
+ changed = true;
43
+ log(`[${record.intent.title}] accepted ${work.intent.id} from passing proof`);
44
+ }
45
+ if (changed)
46
+ record = refreshGraph(coordRoot, input.goalId, actor);
47
+ if (record.projection.state === "succeeded") {
48
+ return report("succeeded", record.projection.reason, record);
49
+ }
50
+ }
51
+ const candidates = selectCandidates(record);
52
+ const selected = selectWithinAttemptBudget(record, candidates).slice(0, record.intent.limits.max_parallel_work);
53
+ if (selected.length > 0 && Date.now() < deadline) {
54
+ const settled = await Promise.all(selected.map(async (candidate) => {
55
+ dispatches++;
56
+ log(`[${record.intent.title}] ${candidate.action} ${candidate.record.intent.id}`);
57
+ try {
58
+ const result = await runWorkItem({
59
+ coordRoot,
60
+ workId: candidate.record.intent.id,
61
+ retry: candidate.action === "retry",
62
+ actor,
63
+ engine: {
64
+ ...input.engine,
65
+ specialists: record.intent.specialists,
66
+ maxAgents: record.intent.limits.max_agents_per_work,
67
+ concurrency: record.intent.limits.agent_concurrency,
68
+ },
69
+ });
70
+ return {
71
+ work_id: candidate.record.intent.id,
72
+ action: candidate.action,
73
+ status: "completed",
74
+ run_id: result.runId,
75
+ };
76
+ }
77
+ catch (error) {
78
+ const latest = reconcileWorkItem(coordRoot, candidate.record.intent.id, actor);
79
+ return {
80
+ work_id: candidate.record.intent.id,
81
+ action: candidate.action,
82
+ status: latest.projection.state === "awaiting_approval" ? "parked" : "failed",
83
+ run_id: latest.projection.latest_run_id,
84
+ error: boundedError(error.message),
85
+ };
86
+ }
87
+ }));
88
+ outcomes.push(...settled);
89
+ changed = true;
90
+ }
91
+ record = refreshGraph(coordRoot, input.goalId, actor);
92
+ const after = graphFingerprint(record);
93
+ if (record.projection.state === "succeeded") {
94
+ return report("succeeded", record.projection.reason, record);
95
+ }
96
+ if (mode === "tick") {
97
+ return report(before === after ? "no_progress" : "tick_complete", before === after
98
+ ? "the tick found no legal progress action"
99
+ : "one bounded supervisor cycle completed", record);
100
+ }
101
+ if (Date.now() >= deadline) {
102
+ return report("budget_exhausted", "supervisor wall-time limit reached", record);
103
+ }
104
+ if (record.projection.state === "budget_exhausted") {
105
+ return report("budget_exhausted", record.projection.reason, record);
106
+ }
107
+ if (selected.length === 0 && !changed) {
108
+ return report(stopForProjection(record.projection), record.projection.reason, record);
109
+ }
110
+ if (selected.length === 0 && record.projection.state !== "ready") {
111
+ return report(stopForProjection(record.projection), record.projection.reason, record);
112
+ }
113
+ if (before === after) {
114
+ return report("no_progress", "a supervisor cycle produced no durable state change", record);
115
+ }
116
+ }
117
+ const final = refreshGraph(coordRoot, input.goalId, actor);
118
+ return report("budget_exhausted", `supervisor cycle limit reached (${cycleLimit})`, final);
119
+ }
120
+ finally {
121
+ release();
122
+ }
123
+ function report(stopReason, reason, record) {
124
+ const endedAt = new Date().toISOString();
125
+ return {
126
+ goal_id: input.goalId,
127
+ mode,
128
+ stop_reason: stopReason,
129
+ reason,
130
+ started_at: startedAt,
131
+ ended_at: endedAt,
132
+ duration_ms: Date.now() - startedMs,
133
+ cycles,
134
+ dispatches,
135
+ acceptances,
136
+ outcomes,
137
+ projection: record.projection,
138
+ };
139
+ }
140
+ }
141
+ function refreshGraph(coordRoot, goalId, actor) {
142
+ const before = readSupervisorIgnoringLease(coordRoot, goalId);
143
+ for (const work of before.work)
144
+ reconcileWorkItem(coordRoot, work.intent.id, actor);
145
+ return readSupervisorIgnoringLease(coordRoot, goalId);
146
+ }
147
+ function selectCandidates(record) {
148
+ const candidates = [];
149
+ for (const work of record.work) {
150
+ if (work.projection.state === "ready") {
151
+ candidates.push({ record: work, action: "run", consumesAttempt: true });
152
+ continue;
153
+ }
154
+ if (work.projection.state === "awaiting_approval" &&
155
+ work.projection.next_action === "resume" &&
156
+ record.intent.automation.resume_approved) {
157
+ candidates.push({ record: work, action: "resume", consumesAttempt: false });
158
+ continue;
159
+ }
160
+ if (work.projection.state === "blocked" &&
161
+ work.projection.next_action === "retry" &&
162
+ record.intent.automation.retry_blocked) {
163
+ candidates.push({ record: work, action: "retry", consumesAttempt: true });
164
+ }
165
+ }
166
+ return candidates;
167
+ }
168
+ function selectWithinAttemptBudget(record, candidates) {
169
+ let remaining = record.projection.attempts_remaining;
170
+ return candidates.filter((candidate) => {
171
+ if (!candidate.consumesAttempt)
172
+ return true;
173
+ if (remaining <= 0)
174
+ return false;
175
+ remaining--;
176
+ return true;
177
+ });
178
+ }
179
+ function graphFingerprint(record) {
180
+ return JSON.stringify(record.work.map((work) => [
181
+ work.intent.id,
182
+ work.projection.state,
183
+ work.projection.next_action,
184
+ work.projection.attempts_used,
185
+ work.events.length,
186
+ ]));
187
+ }
188
+ function stopForProjection(projection) {
189
+ if (projection.state === "budget_exhausted")
190
+ return "budget_exhausted";
191
+ if (projection.state === "blocked")
192
+ return "blocked";
193
+ return "awaiting_attention";
194
+ }
195
+ function boundedError(message) {
196
+ const normalized = message.replace(/\s+/g, " ").trim();
197
+ return normalized.length > 2_000 ? `${normalized.slice(0, 1_999)}…` : normalized;
198
+ }
@@ -0,0 +1,69 @@
1
+ import { type WorkRecord } from "../work/read.js";
2
+ import type { WorkflowSpecialistProfile } from "../workflow/types.js";
3
+ export declare const SUPERVISOR_INTENT_SCHEMA_VERSION: 1;
4
+ export interface SupervisorLimits {
5
+ max_cycles: number;
6
+ max_runtime_ms: number;
7
+ max_parallel_work: number;
8
+ max_total_attempts: number;
9
+ max_agents_per_work: number;
10
+ agent_concurrency: number;
11
+ }
12
+ export interface SupervisorAutomationPolicy {
13
+ accept_passing_proof: boolean;
14
+ resume_approved: boolean;
15
+ retry_blocked: boolean;
16
+ }
17
+ export interface SupervisorIntent {
18
+ schema_version: typeof SUPERVISOR_INTENT_SCHEMA_VERSION;
19
+ id: string;
20
+ title: string;
21
+ root_work_id: string;
22
+ specialists: Record<string, WorkflowSpecialistProfile>;
23
+ limits: SupervisorLimits;
24
+ automation: SupervisorAutomationPolicy;
25
+ created_at: string;
26
+ }
27
+ export type SupervisorState = "ready" | "running" | "awaiting_attention" | "blocked" | "budget_exhausted" | "succeeded";
28
+ export type SupervisorNextAction = "run" | "wait_for_run" | "resolve_approval" | "review" | "retry" | "none";
29
+ export interface SupervisorProjection {
30
+ id: string;
31
+ title: string;
32
+ root_work_id: string;
33
+ state: SupervisorState;
34
+ reason: string;
35
+ next_action: SupervisorNextAction;
36
+ work_ids: string[];
37
+ ready_work: string[];
38
+ resumable_work: string[];
39
+ retryable_work: string[];
40
+ attention_work: string[];
41
+ attempts_used: number;
42
+ attempts_remaining: number;
43
+ specialists: string[];
44
+ }
45
+ export interface SupervisorRecord {
46
+ intent: SupervisorIntent;
47
+ projection: SupervisorProjection;
48
+ work: WorkRecord[];
49
+ }
50
+ export interface CreateSupervisorInput {
51
+ coordRoot: string;
52
+ rootWorkId: string;
53
+ specialists: Readonly<Record<string, WorkflowSpecialistProfile>>;
54
+ title?: string;
55
+ id?: string;
56
+ limits?: Partial<SupervisorLimits>;
57
+ automation?: Partial<SupervisorAutomationPolicy>;
58
+ }
59
+ export declare function createSupervisor(input: CreateSupervisorInput): SupervisorRecord;
60
+ export declare function readSupervisor(coordRoot: string, goalId: string): SupervisorRecord;
61
+ /** @internal Runner seam; ignores the lease held by this supervisor process. */
62
+ export declare function readSupervisorIgnoringLease(coordRoot: string, goalId: string): SupervisorRecord;
63
+ export declare function listSupervisors(coordRoot: string): SupervisorRecord[];
64
+ export declare function collectSupervisorWork(coordRoot: string, rootWorkId: string): WorkRecord[];
65
+ export declare function assertSupervisorId(goalId: string): void;
66
+ export declare function newSupervisorId(): string;
67
+ /** @internal Runner seam; one exclusive crash-recoverable supervisor lease. */
68
+ export declare function acquireSupervisorLease(coordRoot: string, goalId: string): () => void;
69
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,413 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { hostname } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { assertWorkId, readWorkItem } from "../work/read.js";
6
+ import { normalizeWorkflowSpecialists } from "../workflow/specialists.js";
7
+ export const SUPERVISOR_INTENT_SCHEMA_VERSION = 1;
8
+ const GOAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
9
+ const MAX_TITLE = 200;
10
+ const MAX_INTENT_BYTES = 256 * 1024;
11
+ const FOREIGN_LEASE_STALE_MS = 24 * 60 * 60 * 1_000;
12
+ export function createSupervisor(input) {
13
+ const coordRoot = resolve(input.coordRoot);
14
+ assertWorkId(input.rootWorkId);
15
+ const root = readWorkItem(coordRoot, input.rootWorkId);
16
+ const id = input.id ?? newSupervisorId();
17
+ assertSupervisorId(id);
18
+ const intent = {
19
+ schema_version: SUPERVISOR_INTENT_SCHEMA_VERSION,
20
+ id,
21
+ title: bounded(input.title ?? root.intent.title, "supervisor title", MAX_TITLE),
22
+ root_work_id: input.rootWorkId,
23
+ specialists: normalizeWorkflowSpecialists(input.specialists),
24
+ limits: normalizeLimits(input.limits),
25
+ automation: normalizeAutomation(input.automation),
26
+ created_at: new Date().toISOString(),
27
+ };
28
+ const path = supervisorIntentPath(coordRoot, id);
29
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
30
+ chmodSync(dirname(path), 0o700);
31
+ writePrivateJson(path, intent);
32
+ return readSupervisor(coordRoot, id);
33
+ }
34
+ export function readSupervisor(coordRoot, goalId) {
35
+ return readSupervisorInternal(coordRoot, goalId, false);
36
+ }
37
+ /** @internal Runner seam; ignores the lease held by this supervisor process. */
38
+ export function readSupervisorIgnoringLease(coordRoot, goalId) {
39
+ return readSupervisorInternal(coordRoot, goalId, true);
40
+ }
41
+ export function listSupervisors(coordRoot) {
42
+ const base = join(resolve(coordRoot), ".harnery", "supervisors");
43
+ if (!existsSync(base))
44
+ return [];
45
+ const records = [];
46
+ for (const name of readdirSync(base)) {
47
+ if (!GOAL_ID.test(name) || !existsSync(supervisorIntentPath(coordRoot, name)))
48
+ continue;
49
+ records.push(readSupervisor(coordRoot, name));
50
+ }
51
+ return records.sort((left, right) => right.intent.created_at.localeCompare(left.intent.created_at));
52
+ }
53
+ export function collectSupervisorWork(coordRoot, rootWorkId) {
54
+ const records = new Map();
55
+ const visiting = new Set();
56
+ const visit = (workId) => {
57
+ if (records.has(workId))
58
+ return;
59
+ if (visiting.has(workId))
60
+ throw new Error(`supervisor work graph contains a cycle at ${workId}`);
61
+ visiting.add(workId);
62
+ const record = readWorkItem(coordRoot, workId);
63
+ for (const dependency of record.intent.dependencies)
64
+ visit(dependency);
65
+ visiting.delete(workId);
66
+ records.set(workId, record);
67
+ };
68
+ visit(rootWorkId);
69
+ return Array.from(records.values());
70
+ }
71
+ export function assertSupervisorId(goalId) {
72
+ if (!GOAL_ID.test(goalId))
73
+ throw new Error(`invalid supervisor id ${JSON.stringify(goalId)}`);
74
+ }
75
+ export function newSupervisorId() {
76
+ return `goal-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
77
+ }
78
+ /** @internal Runner seam; one exclusive crash-recoverable supervisor lease. */
79
+ export function acquireSupervisorLease(coordRoot, goalId) {
80
+ readSupervisorIntent(coordRoot, goalId);
81
+ const path = join(supervisorDir(coordRoot, goalId), "lease.json");
82
+ const owner = {
83
+ pid: process.pid,
84
+ host: hostname(),
85
+ created_at: new Date().toISOString(),
86
+ nonce: randomUUID(),
87
+ };
88
+ const acquire = () => {
89
+ try {
90
+ const fd = openSync(path, "wx", 0o600);
91
+ try {
92
+ writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
93
+ }
94
+ finally {
95
+ closeSync(fd);
96
+ }
97
+ chmodSync(path, 0o600);
98
+ return true;
99
+ }
100
+ catch (error) {
101
+ if (error.code !== "EEXIST")
102
+ throw error;
103
+ return false;
104
+ }
105
+ };
106
+ if (!acquire()) {
107
+ const existing = readLease(path);
108
+ if (existing && leaseIsLive(existing)) {
109
+ throw new Error(`supervisor ${goalId} is already running under pid ${existing.pid} on ${existing.host}`);
110
+ }
111
+ unlinkSync(path);
112
+ if (!acquire())
113
+ throw new Error(`supervisor ${goalId} lease raced with another process`);
114
+ }
115
+ return () => {
116
+ try {
117
+ const existing = readLease(path);
118
+ if (existing?.nonce === owner.nonce)
119
+ unlinkSync(path);
120
+ }
121
+ catch {
122
+ // A stale private lease is recoverable by the next explicit run.
123
+ }
124
+ };
125
+ }
126
+ function readSupervisorInternal(coordRootRaw, goalId, ignoreLease) {
127
+ const coordRoot = resolve(coordRootRaw);
128
+ const intent = readSupervisorIntent(coordRoot, goalId);
129
+ const work = collectSupervisorWork(coordRoot, intent.root_work_id);
130
+ return {
131
+ intent,
132
+ work,
133
+ projection: deriveProjection(coordRoot, intent, work, ignoreLease),
134
+ };
135
+ }
136
+ function deriveProjection(coordRoot, intent, work, ignoreLease) {
137
+ const root = work.find((record) => record.intent.id === intent.root_work_id);
138
+ if (!root)
139
+ throw new Error(`supervisor ${intent.id} root work is missing`);
140
+ const attemptsUsed = work.reduce((sum, record) => sum + record.projection.attempts_used, 0);
141
+ const readyWork = work
142
+ .filter((record) => record.projection.state === "ready")
143
+ .map((record) => record.intent.id);
144
+ const resumableWork = work
145
+ .filter((record) => record.projection.state === "awaiting_approval" &&
146
+ record.projection.next_action === "resume")
147
+ .map((record) => record.intent.id);
148
+ const retryableWork = work
149
+ .filter((record) => record.projection.state === "blocked" && record.projection.next_action === "retry")
150
+ .map((record) => record.intent.id);
151
+ const pendingApproval = work
152
+ .filter((record) => record.projection.state === "awaiting_approval" &&
153
+ record.projection.next_action === "resolve_approval")
154
+ .map((record) => record.intent.id);
155
+ const reviews = work
156
+ .filter((record) => record.projection.state === "in_review")
157
+ .map((record) => record.intent.id);
158
+ const cancelled = work
159
+ .filter((record) => record.projection.state === "cancelled")
160
+ .map((record) => record.intent.id);
161
+ const terminalBlocked = work
162
+ .filter((record) => record.projection.state === "blocked" && record.projection.next_action === "none")
163
+ .map((record) => record.intent.id);
164
+ const attentionWork = unique([
165
+ ...pendingApproval,
166
+ ...(intent.automation.accept_passing_proof ? [] : reviews),
167
+ ...(intent.automation.resume_approved ? [] : resumableWork),
168
+ ...(intent.automation.retry_blocked ? [] : retryableWork),
169
+ ...cancelled,
170
+ ...terminalBlocked,
171
+ ]);
172
+ const base = {
173
+ id: intent.id,
174
+ title: intent.title,
175
+ root_work_id: intent.root_work_id,
176
+ work_ids: work.map((record) => record.intent.id),
177
+ ready_work: readyWork,
178
+ resumable_work: resumableWork,
179
+ retryable_work: retryableWork,
180
+ attention_work: attentionWork,
181
+ attempts_used: attemptsUsed,
182
+ attempts_remaining: Math.max(0, intent.limits.max_total_attempts - attemptsUsed),
183
+ specialists: Object.keys(intent.specialists),
184
+ };
185
+ if (!ignoreLease && supervisorLeaseIsLive(coordRoot, intent.id)) {
186
+ return {
187
+ ...base,
188
+ state: "running",
189
+ reason: "the foreground supervisor holds the goal lease",
190
+ next_action: "wait_for_run",
191
+ };
192
+ }
193
+ if (root.projection.state === "succeeded") {
194
+ return {
195
+ ...base,
196
+ state: "succeeded",
197
+ reason: "root work was explicitly accepted",
198
+ next_action: "none",
199
+ };
200
+ }
201
+ const resumableDispatchable = intent.automation.resume_approved ? resumableWork : [];
202
+ const attemptDispatchable = [
203
+ ...readyWork,
204
+ ...(intent.automation.retry_blocked ? retryableWork : []),
205
+ ];
206
+ const dispatchable = [...resumableDispatchable, ...attemptDispatchable];
207
+ if (intent.automation.accept_passing_proof && reviews.length > 0) {
208
+ return {
209
+ ...base,
210
+ state: "ready",
211
+ reason: `${reviews.length} passing work item${reviews.length === 1 ? "" : "s"} may be accepted by frozen policy`,
212
+ next_action: "run",
213
+ };
214
+ }
215
+ if (resumableDispatchable.length > 0) {
216
+ return {
217
+ ...base,
218
+ state: "ready",
219
+ reason: `${resumableDispatchable.length} parked run${resumableDispatchable.length === 1 ? " is" : "s are"} resumable`,
220
+ next_action: "run",
221
+ };
222
+ }
223
+ if (attemptsUsed >= intent.limits.max_total_attempts && attemptDispatchable.length > 0) {
224
+ return {
225
+ ...base,
226
+ state: "budget_exhausted",
227
+ reason: `goal exhausted its ${intent.limits.max_total_attempts} total attempts`,
228
+ next_action: "none",
229
+ };
230
+ }
231
+ if (dispatchable.length > 0) {
232
+ return {
233
+ ...base,
234
+ state: "ready",
235
+ reason: `${dispatchable.length} work item${dispatchable.length === 1 ? " is" : "s are"} dispatchable`,
236
+ next_action: "run",
237
+ };
238
+ }
239
+ if (pendingApproval.length > 0) {
240
+ return {
241
+ ...base,
242
+ state: "awaiting_attention",
243
+ reason: `${pendingApproval.length} work item${pendingApproval.length === 1 ? " needs" : "s need"} approval`,
244
+ next_action: "resolve_approval",
245
+ };
246
+ }
247
+ if (reviews.length > 0) {
248
+ return {
249
+ ...base,
250
+ state: "awaiting_attention",
251
+ reason: `${reviews.length} work item${reviews.length === 1 ? " awaits" : "s await"} explicit review`,
252
+ next_action: "review",
253
+ };
254
+ }
255
+ if (resumableWork.length > 0) {
256
+ return {
257
+ ...base,
258
+ state: "awaiting_attention",
259
+ reason: `${resumableWork.length} resolved approval${resumableWork.length === 1 ? " requires" : "s require"} explicit resume`,
260
+ next_action: "run",
261
+ };
262
+ }
263
+ if (retryableWork.length > 0) {
264
+ return {
265
+ ...base,
266
+ state: "blocked",
267
+ reason: `${retryableWork.length} blocked work item${retryableWork.length === 1 ? " requires" : "s require"} explicit retry`,
268
+ next_action: "retry",
269
+ };
270
+ }
271
+ return {
272
+ ...base,
273
+ state: "blocked",
274
+ reason: attentionWork.length > 0
275
+ ? `${attentionWork.length} work item${attentionWork.length === 1 ? " needs" : "s need"} intervention`
276
+ : "goal graph has no legal progress action",
277
+ next_action: "none",
278
+ };
279
+ }
280
+ function readSupervisorIntent(coordRoot, goalId) {
281
+ assertSupervisorId(goalId);
282
+ const path = supervisorIntentPath(coordRoot, goalId);
283
+ if (!existsSync(path))
284
+ throw new Error(`supervisor ${goalId} does not exist`);
285
+ const size = statSync(path).size;
286
+ if (size <= 0 || size > MAX_INTENT_BYTES) {
287
+ throw new Error(`supervisor intent has invalid size ${size}`);
288
+ }
289
+ let intent;
290
+ try {
291
+ intent = JSON.parse(readFileSync(path, "utf8"));
292
+ }
293
+ catch (error) {
294
+ throw new Error(`cannot parse supervisor intent ${goalId}: ${error.message}`);
295
+ }
296
+ validateIntent(intent, goalId);
297
+ return intent;
298
+ }
299
+ function validateIntent(intent, goalId) {
300
+ if (intent.schema_version !== SUPERVISOR_INTENT_SCHEMA_VERSION ||
301
+ intent.id !== goalId ||
302
+ !validTimestamp(intent.created_at)) {
303
+ throw new Error(`supervisor intent ${goalId} has an unsupported or mismatched schema`);
304
+ }
305
+ bounded(intent.title, "supervisor title", MAX_TITLE);
306
+ assertWorkId(intent.root_work_id);
307
+ const specialists = normalizeWorkflowSpecialists(intent.specialists);
308
+ if (JSON.stringify(specialists) !== JSON.stringify(intent.specialists)) {
309
+ throw new Error(`supervisor intent ${goalId} specialists are not canonical`);
310
+ }
311
+ if (JSON.stringify(normalizeLimits(intent.limits)) !== JSON.stringify(intent.limits)) {
312
+ throw new Error(`supervisor intent ${goalId} limits are not canonical`);
313
+ }
314
+ if (JSON.stringify(normalizeAutomation(intent.automation)) !== JSON.stringify(intent.automation)) {
315
+ throw new Error(`supervisor intent ${goalId} automation policy is not canonical`);
316
+ }
317
+ }
318
+ function normalizeLimits(input) {
319
+ return {
320
+ max_cycles: positive(input?.max_cycles ?? 50, "supervisor max_cycles", 1_000),
321
+ max_runtime_ms: positive(input?.max_runtime_ms ?? 4 * 60 * 60 * 1_000, "supervisor max_runtime_ms", 7 * 24 * 60 * 60 * 1_000),
322
+ max_parallel_work: positive(input?.max_parallel_work ?? 1, "supervisor max_parallel_work", 20),
323
+ max_total_attempts: positive(input?.max_total_attempts ?? 100, "supervisor max_total_attempts", 10_000),
324
+ max_agents_per_work: positive(input?.max_agents_per_work ?? 20, "supervisor max_agents_per_work", 1_000),
325
+ agent_concurrency: positive(input?.agent_concurrency ?? 4, "supervisor agent_concurrency", 100),
326
+ };
327
+ }
328
+ function normalizeAutomation(input) {
329
+ return {
330
+ accept_passing_proof: boolean(input?.accept_passing_proof ?? false, "accept_passing_proof"),
331
+ resume_approved: boolean(input?.resume_approved ?? true, "resume_approved"),
332
+ retry_blocked: boolean(input?.retry_blocked ?? false, "retry_blocked"),
333
+ };
334
+ }
335
+ function supervisorDir(coordRoot, goalId) {
336
+ assertSupervisorId(goalId);
337
+ return join(resolve(coordRoot), ".harnery", "supervisors", goalId);
338
+ }
339
+ function supervisorIntentPath(coordRoot, goalId) {
340
+ return join(supervisorDir(coordRoot, goalId), "intent.json");
341
+ }
342
+ function supervisorLeaseIsLive(coordRoot, goalId) {
343
+ const lease = readLease(join(supervisorDir(coordRoot, goalId), "lease.json"));
344
+ return lease ? leaseIsLive(lease) : false;
345
+ }
346
+ function readLease(path) {
347
+ try {
348
+ const value = JSON.parse(readFileSync(path, "utf8"));
349
+ return typeof value.pid === "number" &&
350
+ Number.isSafeInteger(value.pid) &&
351
+ value.pid > 0 &&
352
+ typeof value.host === "string" &&
353
+ typeof value.created_at === "string"
354
+ ? {
355
+ pid: value.pid,
356
+ host: value.host,
357
+ created_at: value.created_at,
358
+ nonce: typeof value.nonce === "string" ? value.nonce : undefined,
359
+ }
360
+ : null;
361
+ }
362
+ catch {
363
+ return null;
364
+ }
365
+ }
366
+ function leaseIsLive(lease) {
367
+ if (lease.host !== hostname()) {
368
+ const age = Date.now() - Date.parse(lease.created_at);
369
+ return Number.isFinite(age) && age < FOREIGN_LEASE_STALE_MS;
370
+ }
371
+ try {
372
+ process.kill(lease.pid, 0);
373
+ return true;
374
+ }
375
+ catch (error) {
376
+ return error.code === "EPERM";
377
+ }
378
+ }
379
+ function writePrivateJson(path, value) {
380
+ const body = `${JSON.stringify(value, null, 2)}\n`;
381
+ if (Buffer.byteLength(body) > MAX_INTENT_BYTES) {
382
+ throw new Error(`supervisor intent exceeds ${MAX_INTENT_BYTES} bytes`);
383
+ }
384
+ writeFileSync(path, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
385
+ chmodSync(path, 0o600);
386
+ }
387
+ function bounded(value, field, max) {
388
+ if (typeof value !== "string")
389
+ throw new Error(`${field} must be a string`);
390
+ const normalized = value.trim();
391
+ if (!normalized)
392
+ throw new Error(`${field} must not be empty`);
393
+ if (normalized.length > max)
394
+ throw new Error(`${field} exceeds ${max} characters`);
395
+ return normalized;
396
+ }
397
+ function positive(value, field, max) {
398
+ if (!Number.isSafeInteger(value) || value < 1 || value > max) {
399
+ throw new Error(`${field} must be an integer from 1 to ${max}`);
400
+ }
401
+ return value;
402
+ }
403
+ function boolean(value, field) {
404
+ if (typeof value !== "boolean")
405
+ throw new Error(`supervisor ${field} must be boolean`);
406
+ return value;
407
+ }
408
+ function validTimestamp(value) {
409
+ return typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
410
+ }
411
+ function unique(values) {
412
+ return [...new Set(values)];
413
+ }