harnery 0.16.0 → 0.18.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 +1 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +223 -5
- package/dist/core/supervisor/index.d.ts +4 -1
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/index.js +3 -0
- package/dist/core/supervisor/plan-api.d.ts +16 -0
- package/dist/core/supervisor/plan-api.d.ts.map +1 -0
- package/dist/core/supervisor/plan-api.js +34 -0
- package/dist/core/supervisor/plan-read.d.ts +4 -0
- package/dist/core/supervisor/plan-read.d.ts.map +1 -0
- package/dist/core/supervisor/plan-read.js +315 -0
- package/dist/core/supervisor/plan-types.d.ts +123 -0
- package/dist/core/supervisor/plan-types.d.ts.map +1 -0
- package/dist/core/supervisor/plan-types.js +14 -0
- package/dist/core/supervisor/planning.d.ts +26 -0
- package/dist/core/supervisor/planning.d.ts.map +1 -0
- package/dist/core/supervisor/planning.js +579 -0
- package/dist/core/supervisor/read.d.ts +3 -1
- package/dist/core/supervisor/read.d.ts.map +1 -1
- package/dist/core/supervisor/read.js +2 -0
- package/dist/core/supervisor/runner.d.ts +3 -0
- package/dist/core/supervisor/runner.d.ts.map +1 -1
- package/dist/core/supervisor/runner.js +42 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +10 -0
- package/dist/core/supervisor/state.d.ts +27 -2
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +302 -13
- package/package.json +1 -1
- package/src/commands/supervisor.ts +275 -5
- package/src/core/supervisor/index.ts +21 -0
- package/src/core/supervisor/plan-api.ts +47 -0
- package/src/core/supervisor/plan-read.ts +356 -0
- package/src/core/supervisor/plan-types.ts +157 -0
- package/src/core/supervisor/planning.ts +698 -0
- package/src/core/supervisor/read.ts +17 -0
- package/src/core/supervisor/runner.ts +46 -2
- package/src/core/supervisor/service.ts +10 -0
- package/src/core/supervisor/state.ts +389 -12
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.ts";
|
|
2
|
+
export {
|
|
3
|
+
SUPERVISOR_PLAN_SCHEMA_VERSION,
|
|
4
|
+
type SupervisorPlanEvent,
|
|
5
|
+
type SupervisorPlanEventType,
|
|
6
|
+
type SupervisorPlanHistory,
|
|
7
|
+
type SupervisorPlanMilestone,
|
|
8
|
+
type SupervisorPlanOutcome,
|
|
9
|
+
type SupervisorPlanProposal,
|
|
10
|
+
type SupervisorPlanRecord,
|
|
11
|
+
type SupervisorPlanRequest,
|
|
12
|
+
type SupervisorPlanStatus,
|
|
13
|
+
type SupervisorPlanTemplate,
|
|
14
|
+
type SupervisorPlanWorkSpec,
|
|
15
|
+
type SupervisorReplanningPolicy,
|
|
16
|
+
} from "./plan-types.ts";
|
|
1
17
|
export type {
|
|
2
18
|
SupervisorServiceConfig,
|
|
3
19
|
SupervisorServiceGoalRuntime,
|
|
@@ -22,6 +38,7 @@ export {
|
|
|
22
38
|
type SupervisorAutomationPolicy,
|
|
23
39
|
type SupervisorIntent,
|
|
24
40
|
type SupervisorLimits,
|
|
41
|
+
type SupervisorMission,
|
|
25
42
|
type SupervisorNextAction,
|
|
26
43
|
type SupervisorProjection,
|
|
27
44
|
type SupervisorRecord,
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
runWorkItem,
|
|
7
7
|
type WorkRecord,
|
|
8
8
|
} from "../work/index.ts";
|
|
9
|
+
import type { SupervisorPlanOutcome } from "./plan-types.ts";
|
|
10
|
+
import { runSupervisorPlanner } from "./planning.ts";
|
|
9
11
|
import {
|
|
10
12
|
acquireSupervisorLease,
|
|
11
13
|
readSupervisorIgnoringLease,
|
|
@@ -40,7 +42,9 @@ export interface SupervisorRunReport {
|
|
|
40
42
|
cycles: number;
|
|
41
43
|
dispatches: number;
|
|
42
44
|
acceptances: number;
|
|
45
|
+
replans: number;
|
|
43
46
|
outcomes: SupervisorDispatchOutcome[];
|
|
47
|
+
plan_outcomes: SupervisorPlanOutcome[];
|
|
44
48
|
projection: SupervisorProjection;
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -70,7 +74,9 @@ export async function runSupervisor(input: RunSupervisorInput): Promise<Supervis
|
|
|
70
74
|
let cycles = 0;
|
|
71
75
|
let dispatches = 0;
|
|
72
76
|
let acceptances = 0;
|
|
77
|
+
let replans = 0;
|
|
73
78
|
const outcomes: SupervisorDispatchOutcome[] = [];
|
|
79
|
+
const planOutcomes: SupervisorPlanOutcome[] = [];
|
|
74
80
|
try {
|
|
75
81
|
const initial = readSupervisorIgnoringLease(coordRoot, input.goalId);
|
|
76
82
|
const cycleLimit = mode === "tick" ? 1 : initial.intent.limits.max_cycles;
|
|
@@ -91,6 +97,39 @@ export async function runSupervisor(input: RunSupervisorInput): Promise<Supervis
|
|
|
91
97
|
|
|
92
98
|
const before = graphFingerprint(record);
|
|
93
99
|
let changed = false;
|
|
100
|
+
if (
|
|
101
|
+
record.projection.next_action === "replan" ||
|
|
102
|
+
record.projection.next_action === "plan_initial" ||
|
|
103
|
+
record.projection.next_action === "plan_milestone"
|
|
104
|
+
) {
|
|
105
|
+
log(
|
|
106
|
+
`[${record.intent.title}] ${record.projection.next_action} at graph generation ${record.projection.plan_generation}`,
|
|
107
|
+
);
|
|
108
|
+
const plan = await runSupervisorPlanner({
|
|
109
|
+
coordRoot,
|
|
110
|
+
record,
|
|
111
|
+
engine: input.engine,
|
|
112
|
+
actor,
|
|
113
|
+
onLog: input.onLog,
|
|
114
|
+
});
|
|
115
|
+
replans++;
|
|
116
|
+
planOutcomes.push(plan);
|
|
117
|
+
record = refreshGraph(coordRoot, input.goalId, actor);
|
|
118
|
+
if (mode === "tick") {
|
|
119
|
+
return report(
|
|
120
|
+
"tick_complete",
|
|
121
|
+
`planner produced ${plan.status} plan ${plan.plan_id}`,
|
|
122
|
+
record,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (record.projection.state === "succeeded") {
|
|
126
|
+
return report("succeeded", record.projection.reason, record);
|
|
127
|
+
}
|
|
128
|
+
if (record.projection.state !== "ready") {
|
|
129
|
+
return report(stopForProjection(record.projection), record.projection.reason, record);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
94
133
|
if (record.intent.automation.accept_passing_proof) {
|
|
95
134
|
for (const work of record.work) {
|
|
96
135
|
if (work.projection.state !== "in_review") continue;
|
|
@@ -206,7 +245,9 @@ export async function runSupervisor(input: RunSupervisorInput): Promise<Supervis
|
|
|
206
245
|
cycles,
|
|
207
246
|
dispatches,
|
|
208
247
|
acceptances,
|
|
248
|
+
replans,
|
|
209
249
|
outcomes,
|
|
250
|
+
plan_outcomes: planOutcomes,
|
|
210
251
|
projection: record.projection,
|
|
211
252
|
};
|
|
212
253
|
}
|
|
@@ -255,7 +296,10 @@ function selectWithinAttemptBudget(record: SupervisorRecord, candidates: Candida
|
|
|
255
296
|
}
|
|
256
297
|
|
|
257
298
|
function graphFingerprint(record: SupervisorRecord): string {
|
|
258
|
-
return JSON.stringify(
|
|
299
|
+
return JSON.stringify([
|
|
300
|
+
record.projection.root_work_id,
|
|
301
|
+
record.projection.plan_generation,
|
|
302
|
+
record.projection.latest_plan_status,
|
|
259
303
|
record.work.map((work) => [
|
|
260
304
|
work.intent.id,
|
|
261
305
|
work.projection.state,
|
|
@@ -263,7 +307,7 @@ function graphFingerprint(record: SupervisorRecord): string {
|
|
|
263
307
|
work.projection.attempts_used,
|
|
264
308
|
work.events.length,
|
|
265
309
|
]),
|
|
266
|
-
);
|
|
310
|
+
]);
|
|
267
311
|
}
|
|
268
312
|
|
|
269
313
|
function stopForProjection(projection: SupervisorProjection): SupervisorStopReason {
|
|
@@ -406,6 +406,7 @@ export async function runSupervisorServiceSweep(
|
|
|
406
406
|
cycles: report.cycles,
|
|
407
407
|
dispatches: report.dispatches,
|
|
408
408
|
acceptances: report.acceptances,
|
|
409
|
+
replans: report.replans,
|
|
409
410
|
});
|
|
410
411
|
persistRuntime(coordRoot, runtime, completedAt);
|
|
411
412
|
} catch (error) {
|
|
@@ -614,6 +615,15 @@ function projectionFingerprint(projection: SupervisorRecord["projection"]): stri
|
|
|
614
615
|
projection.next_action,
|
|
615
616
|
projection.attempts_used,
|
|
616
617
|
projection.attempts_remaining,
|
|
618
|
+
projection.root_work_id,
|
|
619
|
+
projection.root_materialized,
|
|
620
|
+
projection.plan_generation,
|
|
621
|
+
projection.replans_used,
|
|
622
|
+
projection.replans_remaining,
|
|
623
|
+
projection.milestones_completed,
|
|
624
|
+
projection.milestones_remaining,
|
|
625
|
+
projection.pending_plan_id,
|
|
626
|
+
projection.latest_plan_status,
|
|
617
627
|
projection.ready_work,
|
|
618
628
|
projection.resumable_work,
|
|
619
629
|
projection.retryable_work,
|
|
@@ -12,16 +12,29 @@ import {
|
|
|
12
12
|
writeFileSync,
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { hostname } from "node:os";
|
|
15
|
-
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
16
16
|
import { assertWorkId, readWorkItem, type WorkRecord } from "../work/read.ts";
|
|
17
|
+
import { workflowScriptDigest } from "../workflow/run-state.ts";
|
|
17
18
|
import { normalizeWorkflowSpecialists } from "../workflow/specialists.ts";
|
|
18
19
|
import type { WorkflowSpecialistProfile } from "../workflow/types.ts";
|
|
20
|
+
import { readSupervisorPlans } from "./plan-read.ts";
|
|
21
|
+
import {
|
|
22
|
+
type CreateSupervisorReplanningInput,
|
|
23
|
+
type SupervisorPlanHistory,
|
|
24
|
+
type SupervisorPlanRecord,
|
|
25
|
+
type SupervisorReplanningPolicy,
|
|
26
|
+
supervisorGraphFingerprint,
|
|
27
|
+
} from "./plan-types.ts";
|
|
19
28
|
|
|
20
29
|
export const SUPERVISOR_INTENT_SCHEMA_VERSION = 1 as const;
|
|
21
30
|
|
|
22
31
|
const GOAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
23
32
|
const MAX_TITLE = 200;
|
|
33
|
+
const MAX_OBJECTIVE = 4_000;
|
|
34
|
+
const MAX_ACCEPTANCE = 50;
|
|
24
35
|
const MAX_INTENT_BYTES = 256 * 1024;
|
|
36
|
+
const MAX_TEMPLATES = 20;
|
|
37
|
+
const TEMPLATE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
25
38
|
const FOREIGN_LEASE_STALE_MS = 24 * 60 * 60 * 1_000;
|
|
26
39
|
|
|
27
40
|
export interface SupervisorLimits {
|
|
@@ -39,6 +52,18 @@ export interface SupervisorAutomationPolicy {
|
|
|
39
52
|
retry_blocked: boolean;
|
|
40
53
|
}
|
|
41
54
|
|
|
55
|
+
export interface SupervisorMission {
|
|
56
|
+
objective: string;
|
|
57
|
+
acceptance: string[];
|
|
58
|
+
max_milestones: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CreateSupervisorMissionInput {
|
|
62
|
+
objective: string;
|
|
63
|
+
acceptance: readonly string[];
|
|
64
|
+
maxMilestones?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
export interface SupervisorIntent {
|
|
43
68
|
schema_version: typeof SUPERVISOR_INTENT_SCHEMA_VERSION;
|
|
44
69
|
id: string;
|
|
@@ -47,6 +72,8 @@ export interface SupervisorIntent {
|
|
|
47
72
|
specialists: Record<string, WorkflowSpecialistProfile>;
|
|
48
73
|
limits: SupervisorLimits;
|
|
49
74
|
automation: SupervisorAutomationPolicy;
|
|
75
|
+
mission?: SupervisorMission;
|
|
76
|
+
replanning?: SupervisorReplanningPolicy;
|
|
50
77
|
created_at: string;
|
|
51
78
|
}
|
|
52
79
|
|
|
@@ -64,12 +91,17 @@ export type SupervisorNextAction =
|
|
|
64
91
|
| "resolve_approval"
|
|
65
92
|
| "review"
|
|
66
93
|
| "retry"
|
|
94
|
+
| "replan"
|
|
95
|
+
| "plan_initial"
|
|
96
|
+
| "plan_milestone"
|
|
97
|
+
| "review_plan"
|
|
67
98
|
| "none";
|
|
68
99
|
|
|
69
100
|
export interface SupervisorProjection {
|
|
70
101
|
id: string;
|
|
71
102
|
title: string;
|
|
72
103
|
root_work_id: string;
|
|
104
|
+
root_materialized: boolean;
|
|
73
105
|
state: SupervisorState;
|
|
74
106
|
reason: string;
|
|
75
107
|
next_action: SupervisorNextAction;
|
|
@@ -81,38 +113,77 @@ export interface SupervisorProjection {
|
|
|
81
113
|
attempts_used: number;
|
|
82
114
|
attempts_remaining: number;
|
|
83
115
|
specialists: string[];
|
|
116
|
+
plan_generation: number;
|
|
117
|
+
replans_used: number;
|
|
118
|
+
replans_remaining: number;
|
|
119
|
+
milestones_completed: number;
|
|
120
|
+
milestones_remaining: number;
|
|
121
|
+
pending_plan_id?: string;
|
|
122
|
+
latest_plan_status?: SupervisorPlanRecord["status"];
|
|
123
|
+
governed_work_ids: string[];
|
|
84
124
|
}
|
|
85
125
|
|
|
86
126
|
export interface SupervisorRecord {
|
|
87
127
|
intent: SupervisorIntent;
|
|
88
128
|
projection: SupervisorProjection;
|
|
89
129
|
work: WorkRecord[];
|
|
130
|
+
plans: SupervisorPlanRecord[];
|
|
90
131
|
}
|
|
91
132
|
|
|
92
133
|
export interface CreateSupervisorInput {
|
|
93
134
|
coordRoot: string;
|
|
94
|
-
rootWorkId
|
|
135
|
+
rootWorkId?: string;
|
|
95
136
|
specialists: Readonly<Record<string, WorkflowSpecialistProfile>>;
|
|
96
137
|
title?: string;
|
|
97
138
|
id?: string;
|
|
98
139
|
limits?: Partial<SupervisorLimits>;
|
|
99
140
|
automation?: Partial<SupervisorAutomationPolicy>;
|
|
141
|
+
mission?: CreateSupervisorMissionInput;
|
|
142
|
+
replanning?: CreateSupervisorReplanningInput;
|
|
100
143
|
}
|
|
101
144
|
|
|
102
145
|
export function createSupervisor(input: CreateSupervisorInput): SupervisorRecord {
|
|
103
146
|
const coordRoot = resolve(input.coordRoot);
|
|
104
|
-
assertWorkId(input.rootWorkId);
|
|
105
|
-
const root = readWorkItem(coordRoot, input.rootWorkId);
|
|
106
147
|
const id = input.id ?? newSupervisorId();
|
|
107
148
|
assertSupervisorId(id);
|
|
149
|
+
const mission = input.mission ? normalizeMission(input.mission) : undefined;
|
|
150
|
+
if (mission && !input.replanning) {
|
|
151
|
+
throw new Error("supervisor mission requires a replanning policy");
|
|
152
|
+
}
|
|
153
|
+
if (!input.rootWorkId && !mission) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"supervisor creation without root work requires a mission and replanning policy",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (input.rootWorkId) assertWorkId(input.rootWorkId);
|
|
159
|
+
const root = input.rootWorkId ? readWorkItem(coordRoot, input.rootWorkId) : undefined;
|
|
160
|
+
const replanning = input.replanning
|
|
161
|
+
? normalizeReplanning(input.replanning, input.specialists)
|
|
162
|
+
: undefined;
|
|
163
|
+
if (
|
|
164
|
+
mission &&
|
|
165
|
+
!input.rootWorkId &&
|
|
166
|
+
replanning &&
|
|
167
|
+
replanning.max_replans <= mission.max_milestones
|
|
168
|
+
) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"objective-first mission max_replans must exceed max_milestones to reserve completion review",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
108
173
|
const intent: SupervisorIntent = {
|
|
109
174
|
schema_version: SUPERVISOR_INTENT_SCHEMA_VERSION,
|
|
110
175
|
id,
|
|
111
|
-
title: bounded(
|
|
112
|
-
|
|
176
|
+
title: bounded(
|
|
177
|
+
input.title ?? root?.intent.title ?? missionTitle(mission!),
|
|
178
|
+
"supervisor title",
|
|
179
|
+
MAX_TITLE,
|
|
180
|
+
),
|
|
181
|
+
root_work_id: input.rootWorkId ?? missionRootId(id),
|
|
113
182
|
specialists: normalizeWorkflowSpecialists(input.specialists),
|
|
114
183
|
limits: normalizeLimits(input.limits),
|
|
115
184
|
automation: normalizeAutomation(input.automation),
|
|
185
|
+
...(mission ? { mission } : {}),
|
|
186
|
+
...(replanning ? { replanning } : {}),
|
|
116
187
|
created_at: new Date().toISOString(),
|
|
117
188
|
};
|
|
118
189
|
const path = supervisorIntentPath(coordRoot, id);
|
|
@@ -221,11 +292,18 @@ function readSupervisorInternal(
|
|
|
221
292
|
): SupervisorRecord {
|
|
222
293
|
const coordRoot = resolve(coordRootRaw);
|
|
223
294
|
const intent = readSupervisorIntent(coordRoot, goalId);
|
|
224
|
-
const
|
|
295
|
+
const plans = readSupervisorPlans(coordRoot, intent.id, intent.root_work_id);
|
|
296
|
+
const activeRootExists = workIntentExists(coordRoot, plans.active_root_work_id);
|
|
297
|
+
if (!activeRootExists && (!intent.mission || plans.generation > 0)) {
|
|
298
|
+
throw new Error(`supervisor ${intent.id} root work is missing`);
|
|
299
|
+
}
|
|
300
|
+
const work = activeRootExists ? collectSupervisorWork(coordRoot, plans.active_root_work_id) : [];
|
|
301
|
+
const governed = collectGovernedWork(coordRoot, intent.root_work_id, plans, work);
|
|
225
302
|
return {
|
|
226
303
|
intent,
|
|
227
304
|
work,
|
|
228
|
-
|
|
305
|
+
plans: plans.plans,
|
|
306
|
+
projection: deriveProjection(coordRoot, intent, work, governed, plans, ignoreLease),
|
|
229
307
|
};
|
|
230
308
|
}
|
|
231
309
|
|
|
@@ -233,11 +311,12 @@ function deriveProjection(
|
|
|
233
311
|
coordRoot: string,
|
|
234
312
|
intent: SupervisorIntent,
|
|
235
313
|
work: WorkRecord[],
|
|
314
|
+
governed: WorkRecord[],
|
|
315
|
+
plans: SupervisorPlanHistory,
|
|
236
316
|
ignoreLease: boolean,
|
|
237
317
|
): SupervisorProjection {
|
|
238
|
-
const root = work.find((record) => record.intent.id ===
|
|
239
|
-
|
|
240
|
-
const attemptsUsed = work.reduce((sum, record) => sum + record.projection.attempts_used, 0);
|
|
318
|
+
const root = work.find((record) => record.intent.id === plans.active_root_work_id);
|
|
319
|
+
const attemptsUsed = governed.reduce((sum, record) => sum + record.projection.attempts_used, 0);
|
|
241
320
|
const readyWork = work
|
|
242
321
|
.filter((record) => record.projection.state === "ready")
|
|
243
322
|
.map((record) => record.intent.id);
|
|
@@ -280,10 +359,18 @@ function deriveProjection(
|
|
|
280
359
|
...cancelled,
|
|
281
360
|
...terminalBlocked,
|
|
282
361
|
]);
|
|
362
|
+
const initialRootAccepted =
|
|
363
|
+
intent.mission !== undefined &&
|
|
364
|
+
governed.some(
|
|
365
|
+
(record) =>
|
|
366
|
+
record.intent.id === intent.root_work_id && record.projection.state === "succeeded",
|
|
367
|
+
);
|
|
368
|
+
const milestonesCompleted = plans.milestones_completed + (initialRootAccepted ? 1 : 0);
|
|
283
369
|
const base = {
|
|
284
370
|
id: intent.id,
|
|
285
371
|
title: intent.title,
|
|
286
|
-
root_work_id:
|
|
372
|
+
root_work_id: plans.active_root_work_id,
|
|
373
|
+
root_materialized: root !== undefined,
|
|
287
374
|
work_ids: work.map((record) => record.intent.id),
|
|
288
375
|
ready_work: readyWork,
|
|
289
376
|
resumable_work: resumableWork,
|
|
@@ -292,6 +379,14 @@ function deriveProjection(
|
|
|
292
379
|
attempts_used: attemptsUsed,
|
|
293
380
|
attempts_remaining: Math.max(0, intent.limits.max_total_attempts - attemptsUsed),
|
|
294
381
|
specialists: Object.keys(intent.specialists),
|
|
382
|
+
plan_generation: plans.generation,
|
|
383
|
+
replans_used: plans.plans.length,
|
|
384
|
+
replans_remaining: Math.max(0, (intent.replanning?.max_replans ?? 0) - plans.plans.length),
|
|
385
|
+
milestones_completed: milestonesCompleted,
|
|
386
|
+
milestones_remaining: Math.max(0, (intent.mission?.max_milestones ?? 0) - milestonesCompleted),
|
|
387
|
+
pending_plan_id: plans.latest?.status === "proposed" ? plans.latest.request.id : undefined,
|
|
388
|
+
latest_plan_status: plans.latest?.status,
|
|
389
|
+
governed_work_ids: governed.map((record) => record.intent.id),
|
|
295
390
|
};
|
|
296
391
|
if (!ignoreLease && supervisorLeaseIsLive(coordRoot, intent.id)) {
|
|
297
392
|
return {
|
|
@@ -301,6 +396,88 @@ function deriveProjection(
|
|
|
301
396
|
next_action: "wait_for_run",
|
|
302
397
|
};
|
|
303
398
|
}
|
|
399
|
+
if (intent.mission && plans.completed) {
|
|
400
|
+
return {
|
|
401
|
+
...base,
|
|
402
|
+
state: "succeeded",
|
|
403
|
+
reason: "mission completion was explicitly accepted",
|
|
404
|
+
next_action: "none",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (plans.latest?.status === "proposed") {
|
|
408
|
+
return {
|
|
409
|
+
...base,
|
|
410
|
+
state: "awaiting_attention",
|
|
411
|
+
reason: `replacement plan ${plans.latest.request.id} awaits explicit review`,
|
|
412
|
+
next_action: "review_plan",
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (plans.latest?.status === "awaiting_approval") {
|
|
416
|
+
return {
|
|
417
|
+
...base,
|
|
418
|
+
state: "awaiting_attention",
|
|
419
|
+
reason: `planner workflow awaits approval ${plans.latest.approval_id}`,
|
|
420
|
+
next_action: "resolve_approval",
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (plans.latest?.status === "resumable") {
|
|
424
|
+
return {
|
|
425
|
+
...base,
|
|
426
|
+
state: "ready",
|
|
427
|
+
reason: `planner workflow ${plans.latest.request.workflow_run_id} is resumable`,
|
|
428
|
+
next_action: "replan",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const triggerFingerprint = supervisorGraphFingerprint({
|
|
432
|
+
rootWorkId: plans.active_root_work_id,
|
|
433
|
+
generation: plans.generation,
|
|
434
|
+
work,
|
|
435
|
+
});
|
|
436
|
+
const latestHandledSameGraph =
|
|
437
|
+
plans.latest?.request.trigger_fingerprint === triggerFingerprint &&
|
|
438
|
+
plans.latest.request.prior_root_work_id === plans.active_root_work_id &&
|
|
439
|
+
plans.latest.status === "attention";
|
|
440
|
+
if (latestHandledSameGraph && plans.latest) {
|
|
441
|
+
return {
|
|
442
|
+
...base,
|
|
443
|
+
state: "awaiting_attention",
|
|
444
|
+
reason: plans.latest.reason ?? `plan ${plans.latest.request.id} requires attention`,
|
|
445
|
+
next_action: "none",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (!root && intent.mission) {
|
|
449
|
+
if (!intent.replanning || plans.plans.length >= intent.replanning.max_replans) {
|
|
450
|
+
return {
|
|
451
|
+
...base,
|
|
452
|
+
state: "budget_exhausted",
|
|
453
|
+
reason: "mission exhausted its planning budget before creating an initial milestone",
|
|
454
|
+
next_action: "none",
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
...base,
|
|
459
|
+
state: "ready",
|
|
460
|
+
reason: "mission is ready for its initial bounded milestone plan",
|
|
461
|
+
next_action: "plan_initial",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (!root) throw new Error(`supervisor ${intent.id} root work is missing`);
|
|
465
|
+
if (root.projection.state === "succeeded" && intent.mission) {
|
|
466
|
+
if (!intent.replanning || plans.plans.length >= intent.replanning.max_replans) {
|
|
467
|
+
return {
|
|
468
|
+
...base,
|
|
469
|
+
state: "budget_exhausted",
|
|
470
|
+
reason: "mission exhausted its planning budget at a milestone boundary",
|
|
471
|
+
next_action: "none",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
...base,
|
|
476
|
+
state: "ready",
|
|
477
|
+
reason: `milestone ${plans.milestones_completed} is accepted and requires mission reassessment`,
|
|
478
|
+
next_action: "plan_milestone",
|
|
479
|
+
};
|
|
480
|
+
}
|
|
304
481
|
if (root.projection.state === "succeeded") {
|
|
305
482
|
return {
|
|
306
483
|
...base,
|
|
@@ -379,6 +556,41 @@ function deriveProjection(
|
|
|
379
556
|
next_action: "retry",
|
|
380
557
|
};
|
|
381
558
|
}
|
|
559
|
+
if (attemptsUsed >= intent.limits.max_total_attempts && cancelled.length === 0) {
|
|
560
|
+
return {
|
|
561
|
+
...base,
|
|
562
|
+
state: "budget_exhausted",
|
|
563
|
+
reason: `goal exhausted its ${intent.limits.max_total_attempts} total attempts`,
|
|
564
|
+
next_action: "none",
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const canReplan =
|
|
568
|
+
intent.replanning !== undefined &&
|
|
569
|
+
plans.plans.length < intent.replanning.max_replans &&
|
|
570
|
+
cancelled.length === 0 &&
|
|
571
|
+
!latestHandledSameGraph;
|
|
572
|
+
if (canReplan) {
|
|
573
|
+
return {
|
|
574
|
+
...base,
|
|
575
|
+
state: "ready",
|
|
576
|
+
reason: terminalBlocked.length
|
|
577
|
+
? `${terminalBlocked.length} terminally blocked work item${terminalBlocked.length === 1 ? " can" : "s can"} be replanned`
|
|
578
|
+
: "the active graph has no legal progress action and may be replanned",
|
|
579
|
+
next_action: "replan",
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (
|
|
583
|
+
intent.replanning &&
|
|
584
|
+
plans.plans.length >= intent.replanning.max_replans &&
|
|
585
|
+
cancelled.length === 0
|
|
586
|
+
) {
|
|
587
|
+
return {
|
|
588
|
+
...base,
|
|
589
|
+
state: "budget_exhausted",
|
|
590
|
+
reason: `goal exhausted its ${intent.replanning.max_replans} replans`,
|
|
591
|
+
next_action: "none",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
382
594
|
return {
|
|
383
595
|
...base,
|
|
384
596
|
state: "blocked",
|
|
@@ -418,6 +630,19 @@ function validateIntent(intent: SupervisorIntent, goalId: string): void {
|
|
|
418
630
|
}
|
|
419
631
|
bounded(intent.title, "supervisor title", MAX_TITLE);
|
|
420
632
|
assertWorkId(intent.root_work_id);
|
|
633
|
+
if (intent.mission) {
|
|
634
|
+
const normalizedMission = normalizeMission({
|
|
635
|
+
objective: intent.mission.objective,
|
|
636
|
+
acceptance: intent.mission.acceptance,
|
|
637
|
+
maxMilestones: intent.mission.max_milestones,
|
|
638
|
+
});
|
|
639
|
+
if (JSON.stringify(normalizedMission) !== JSON.stringify(intent.mission)) {
|
|
640
|
+
throw new Error(`supervisor intent ${goalId} mission is not canonical`);
|
|
641
|
+
}
|
|
642
|
+
if (!intent.replanning) {
|
|
643
|
+
throw new Error(`supervisor intent ${goalId} mission requires replanning`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
421
646
|
const specialists = normalizeWorkflowSpecialists(intent.specialists);
|
|
422
647
|
if (JSON.stringify(specialists) !== JSON.stringify(intent.specialists)) {
|
|
423
648
|
throw new Error(`supervisor intent ${goalId} specialists are not canonical`);
|
|
@@ -430,6 +655,33 @@ function validateIntent(intent: SupervisorIntent, goalId: string): void {
|
|
|
430
655
|
) {
|
|
431
656
|
throw new Error(`supervisor intent ${goalId} automation policy is not canonical`);
|
|
432
657
|
}
|
|
658
|
+
if (intent.replanning) validateReplanning(intent.replanning, specialists, goalId);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function normalizeMission(input: CreateSupervisorMissionInput): SupervisorMission {
|
|
662
|
+
if (!Array.isArray(input.acceptance)) {
|
|
663
|
+
throw new Error("supervisor mission acceptance must be an array");
|
|
664
|
+
}
|
|
665
|
+
if (input.acceptance.length < 1 || input.acceptance.length > MAX_ACCEPTANCE) {
|
|
666
|
+
throw new Error(`supervisor mission acceptance must contain 1 to ${MAX_ACCEPTANCE} criteria`);
|
|
667
|
+
}
|
|
668
|
+
const acceptance = input.acceptance.map((criterion, index) =>
|
|
669
|
+
bounded(criterion, `supervisor mission acceptance[${index}]`, 500),
|
|
670
|
+
);
|
|
671
|
+
if (new Set(acceptance).size !== acceptance.length) {
|
|
672
|
+
throw new Error("supervisor mission acceptance criteria must be unique");
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
objective: bounded(input.objective, "supervisor mission objective", MAX_OBJECTIVE),
|
|
676
|
+
acceptance,
|
|
677
|
+
max_milestones: positive(input.maxMilestones ?? 4, "supervisor mission max_milestones", 20),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function missionTitle(mission: SupervisorMission): string {
|
|
682
|
+
return mission.objective.length <= MAX_TITLE
|
|
683
|
+
? mission.objective
|
|
684
|
+
: `${mission.objective.slice(0, MAX_TITLE - 1).trimEnd()}…`;
|
|
433
685
|
}
|
|
434
686
|
|
|
435
687
|
function normalizeLimits(input: Partial<SupervisorLimits> | undefined): SupervisorLimits {
|
|
@@ -465,6 +717,131 @@ function normalizeAutomation(
|
|
|
465
717
|
};
|
|
466
718
|
}
|
|
467
719
|
|
|
720
|
+
function normalizeReplanning(
|
|
721
|
+
input: CreateSupervisorReplanningInput,
|
|
722
|
+
specialistsInput: Readonly<Record<string, WorkflowSpecialistProfile>>,
|
|
723
|
+
): SupervisorReplanningPolicy {
|
|
724
|
+
const specialists = normalizeWorkflowSpecialists(specialistsInput);
|
|
725
|
+
const plannerSpecialist = bounded(input.plannerSpecialist, "planner specialist", 100);
|
|
726
|
+
if (!specialists[plannerSpecialist]) {
|
|
727
|
+
throw new Error(`planner specialist ${plannerSpecialist} is not present in the frozen team`);
|
|
728
|
+
}
|
|
729
|
+
const entries = Object.entries(input.templates);
|
|
730
|
+
if (entries.length < 1 || entries.length > MAX_TEMPLATES) {
|
|
731
|
+
throw new Error(`replanning templates must contain 1 to ${MAX_TEMPLATES} entries`);
|
|
732
|
+
}
|
|
733
|
+
const templates = Object.fromEntries(
|
|
734
|
+
entries
|
|
735
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
736
|
+
.map(([id, template]) => {
|
|
737
|
+
if (!TEMPLATE_ID.test(id)) throw new Error(`invalid replanning template id ${id}`);
|
|
738
|
+
const path = resolve(template.workflowPath);
|
|
739
|
+
if (!existsSync(path))
|
|
740
|
+
throw new Error(`replanning template ${id} does not exist at ${path}`);
|
|
741
|
+
const maxAttempts = positive(
|
|
742
|
+
template.maxAttempts ?? 3,
|
|
743
|
+
`replanning template ${id} maxAttempts`,
|
|
744
|
+
100,
|
|
745
|
+
);
|
|
746
|
+
return [
|
|
747
|
+
id,
|
|
748
|
+
{
|
|
749
|
+
workflow: { path, sha256: workflowScriptDigest(path) },
|
|
750
|
+
max_attempts: maxAttempts,
|
|
751
|
+
root: boolean(template.root ?? false, `replanning template ${id} root`),
|
|
752
|
+
},
|
|
753
|
+
];
|
|
754
|
+
}),
|
|
755
|
+
);
|
|
756
|
+
if (!Object.values(templates).some((template) => template.root)) {
|
|
757
|
+
throw new Error("replanning requires at least one root-capable template");
|
|
758
|
+
}
|
|
759
|
+
const maxWorkItemsPerPlan = positive(
|
|
760
|
+
input.maxWorkItemsPerPlan ?? 8,
|
|
761
|
+
"replanning max_work_items_per_plan",
|
|
762
|
+
25,
|
|
763
|
+
);
|
|
764
|
+
const maxTotalWorkItems = positive(
|
|
765
|
+
input.maxTotalWorkItems ?? 25,
|
|
766
|
+
"replanning max_total_work_items",
|
|
767
|
+
100,
|
|
768
|
+
);
|
|
769
|
+
if (maxWorkItemsPerPlan > maxTotalWorkItems) {
|
|
770
|
+
throw new Error("replanning max_work_items_per_plan cannot exceed max_total_work_items");
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
planner_specialist: plannerSpecialist,
|
|
774
|
+
auto_apply: boolean(input.autoApply ?? false, "replanning auto_apply"),
|
|
775
|
+
max_replans: positive(input.maxReplans ?? 5, "replanning max_replans", 100),
|
|
776
|
+
max_work_items_per_plan: maxWorkItemsPerPlan,
|
|
777
|
+
max_total_work_items: maxTotalWorkItems,
|
|
778
|
+
templates,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function validateReplanning(
|
|
783
|
+
policy: SupervisorReplanningPolicy,
|
|
784
|
+
specialists: Record<string, WorkflowSpecialistProfile>,
|
|
785
|
+
goalId: string,
|
|
786
|
+
): void {
|
|
787
|
+
if (!specialists[policy.planner_specialist]) {
|
|
788
|
+
throw new Error(`supervisor ${goalId} planner specialist is not in the frozen team`);
|
|
789
|
+
}
|
|
790
|
+
boolean(policy.auto_apply, "replanning auto_apply");
|
|
791
|
+
positive(policy.max_replans, "replanning max_replans", 100);
|
|
792
|
+
positive(policy.max_work_items_per_plan, "replanning max_work_items_per_plan", 25);
|
|
793
|
+
positive(policy.max_total_work_items, "replanning max_total_work_items", 100);
|
|
794
|
+
if (policy.max_work_items_per_plan > policy.max_total_work_items) {
|
|
795
|
+
throw new Error(`supervisor ${goalId} replanning work-item bounds are inconsistent`);
|
|
796
|
+
}
|
|
797
|
+
const entries = Object.entries(policy.templates ?? {});
|
|
798
|
+
if (entries.length < 1 || entries.length > MAX_TEMPLATES) {
|
|
799
|
+
throw new Error(`supervisor ${goalId} has invalid replanning templates`);
|
|
800
|
+
}
|
|
801
|
+
let rootCapable = false;
|
|
802
|
+
for (const [id, template] of entries) {
|
|
803
|
+
if (
|
|
804
|
+
!TEMPLATE_ID.test(id) ||
|
|
805
|
+
!isAbsolute(template.workflow?.path) ||
|
|
806
|
+
!/^[a-f0-9]{64}$/.test(template.workflow?.sha256) ||
|
|
807
|
+
!Number.isSafeInteger(template.max_attempts) ||
|
|
808
|
+
template.max_attempts < 1 ||
|
|
809
|
+
template.max_attempts > 100 ||
|
|
810
|
+
typeof template.root !== "boolean"
|
|
811
|
+
) {
|
|
812
|
+
throw new Error(`supervisor ${goalId} replanning template ${id} is invalid`);
|
|
813
|
+
}
|
|
814
|
+
rootCapable ||= template.root;
|
|
815
|
+
}
|
|
816
|
+
if (!rootCapable) throw new Error(`supervisor ${goalId} has no root-capable replanning template`);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function collectGovernedWork(
|
|
820
|
+
coordRoot: string,
|
|
821
|
+
originalRootWorkId: string,
|
|
822
|
+
plans: SupervisorPlanHistory,
|
|
823
|
+
active: WorkRecord[],
|
|
824
|
+
): WorkRecord[] {
|
|
825
|
+
const governed = new Map<string, WorkRecord>();
|
|
826
|
+
if (workIntentExists(coordRoot, originalRootWorkId)) {
|
|
827
|
+
for (const record of collectSupervisorWork(coordRoot, originalRootWorkId)) {
|
|
828
|
+
governed.set(record.intent.id, record);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
for (const workId of plans.applied_work_ids)
|
|
832
|
+
governed.set(workId, readWorkItem(coordRoot, workId));
|
|
833
|
+
for (const record of active) governed.set(record.intent.id, record);
|
|
834
|
+
return [...governed.values()];
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function workIntentExists(coordRoot: string, workId: string): boolean {
|
|
838
|
+
return existsSync(join(resolve(coordRoot), ".harnery", "work", workId, "intent.json"));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function missionRootId(goalId: string): string {
|
|
842
|
+
return `${goalId.slice(0, 87)}-mission-root`;
|
|
843
|
+
}
|
|
844
|
+
|
|
468
845
|
function supervisorDir(coordRoot: string, goalId: string): string {
|
|
469
846
|
assertSupervisorId(goalId);
|
|
470
847
|
return join(resolve(coordRoot), ".harnery", "supervisors", goalId);
|