harnery 0.17.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 +40 -3
- package/dist/core/supervisor/index.d.ts +2 -2
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/plan-read.d.ts.map +1 -1
- package/dist/core/supervisor/plan-read.js +47 -1
- package/dist/core/supervisor/plan-types.d.ts +13 -3
- package/dist/core/supervisor/plan-types.d.ts.map +1 -1
- package/dist/core/supervisor/planning.d.ts.map +1 -1
- package/dist/core/supervisor/planning.js +113 -20
- package/dist/core/supervisor/read.d.ts +2 -2
- package/dist/core/supervisor/read.d.ts.map +1 -1
- package/dist/core/supervisor/runner.d.ts.map +1 -1
- package/dist/core/supervisor/runner.js +7 -2
- package/dist/core/supervisor/service.js +3 -0
- package/dist/core/supervisor/state.d.ts +17 -2
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +137 -27
- package/package.json +1 -1
- package/src/commands/supervisor.ts +47 -4
- package/src/core/supervisor/index.ts +3 -0
- package/src/core/supervisor/plan-read.ts +58 -1
- package/src/core/supervisor/plan-types.ts +14 -1
- package/src/core/supervisor/planning.ts +136 -14
- package/src/core/supervisor/read.ts +2 -0
- package/src/core/supervisor/runner.ts +9 -2
- package/src/core/supervisor/service.ts +3 -0
- package/src/core/supervisor/state.ts +176 -28
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { readWorkItem } from "../work/read.ts";
|
|
3
4
|
import {
|
|
4
5
|
SUPERVISOR_PLAN_SCHEMA_VERSION,
|
|
5
6
|
type SupervisorPlanEvent,
|
|
@@ -25,6 +26,7 @@ const EVENT_TYPES = new Set<SupervisorPlanEventType>([
|
|
|
25
26
|
"plan.resumed",
|
|
26
27
|
"plan.proposed",
|
|
27
28
|
"plan.applied",
|
|
29
|
+
"plan.completed",
|
|
28
30
|
"plan.rejected",
|
|
29
31
|
"plan.attention",
|
|
30
32
|
"plan.failed",
|
|
@@ -46,6 +48,8 @@ export function readSupervisorPlans(
|
|
|
46
48
|
generation: 0,
|
|
47
49
|
applied_work_ids: [],
|
|
48
50
|
materialized_work_ids: [],
|
|
51
|
+
milestones_completed: 0,
|
|
52
|
+
completed: false,
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
const entries = readdirSync(root, { withFileTypes: true })
|
|
@@ -64,6 +68,8 @@ export function readSupervisorPlans(
|
|
|
64
68
|
}
|
|
65
69
|
let activeRoot = originalRootWorkId;
|
|
66
70
|
let generation = 0;
|
|
71
|
+
let milestonesCompleted = 0;
|
|
72
|
+
let completed = false;
|
|
67
73
|
const appliedWork = new Set<string>();
|
|
68
74
|
const materializedWork = new Set<string>();
|
|
69
75
|
for (const plan of plans) {
|
|
@@ -76,14 +82,30 @@ export function readSupervisorPlans(
|
|
|
76
82
|
if (plan.status !== "applied" || !plan.root_work_id) continue;
|
|
77
83
|
activeRoot = plan.root_work_id;
|
|
78
84
|
generation++;
|
|
85
|
+
if (
|
|
86
|
+
plan.proposal?.milestone &&
|
|
87
|
+
readWorkItem(coordRoot, plan.root_work_id).projection.state === "succeeded"
|
|
88
|
+
) {
|
|
89
|
+
milestonesCompleted++;
|
|
90
|
+
}
|
|
79
91
|
for (const workId of plan.work_ids) appliedWork.add(workId);
|
|
80
92
|
}
|
|
93
|
+
const completedPlans = plans.filter((plan) => plan.status === "completed");
|
|
94
|
+
if (
|
|
95
|
+
completedPlans.length > 1 ||
|
|
96
|
+
(completedPlans.length === 1 && completedPlans[0] !== plans.at(-1))
|
|
97
|
+
) {
|
|
98
|
+
throw new Error(`supervisor ${goalId} has invalid completion history`);
|
|
99
|
+
}
|
|
100
|
+
completed = completedPlans.length === 1;
|
|
81
101
|
return {
|
|
82
102
|
plans,
|
|
83
103
|
active_root_work_id: activeRoot,
|
|
84
104
|
generation,
|
|
85
105
|
applied_work_ids: [...appliedWork],
|
|
86
106
|
materialized_work_ids: [...materializedWork],
|
|
107
|
+
milestones_completed: milestonesCompleted,
|
|
108
|
+
completed,
|
|
87
109
|
latest: plans.at(-1),
|
|
88
110
|
};
|
|
89
111
|
}
|
|
@@ -115,6 +137,10 @@ function deriveStatus(
|
|
|
115
137
|
proposal: SupervisorPlanProposal | undefined,
|
|
116
138
|
): Pick<SupervisorPlanRecord, "status" | "approval_id" | "root_work_id" | "work_ids" | "reason"> {
|
|
117
139
|
const latest = events.at(-1);
|
|
140
|
+
const completed = [...events].reverse().find((event) => event.event === "plan.completed");
|
|
141
|
+
if (completed) {
|
|
142
|
+
return { status: "completed", work_ids: [], reason: completed.reason };
|
|
143
|
+
}
|
|
118
144
|
const applied = [...events].reverse().find((event) => event.event === "plan.applied");
|
|
119
145
|
if (applied) {
|
|
120
146
|
return {
|
|
@@ -180,6 +206,8 @@ function validateRequest(request: SupervisorPlanRequest, goalId: string, planId:
|
|
|
180
206
|
!Number.isSafeInteger(request.sequence) ||
|
|
181
207
|
request.sequence < 1 ||
|
|
182
208
|
request.sequence > MAX_PLANS ||
|
|
209
|
+
(request.trigger !== undefined &&
|
|
210
|
+
!["initial", "recovery", "milestone"].includes(request.trigger)) ||
|
|
183
211
|
typeof request.trigger_fingerprint !== "string" ||
|
|
184
212
|
request.trigger_fingerprint.length < 1 ||
|
|
185
213
|
request.trigger_fingerprint.length > 64_000 ||
|
|
@@ -195,7 +223,7 @@ function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: stri
|
|
|
195
223
|
if (
|
|
196
224
|
proposal.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
|
|
197
225
|
proposal.plan_id !== planId ||
|
|
198
|
-
!["apply", "attention"].includes(proposal.decision) ||
|
|
226
|
+
!["apply", "complete", "attention"].includes(proposal.decision) ||
|
|
199
227
|
typeof proposal.rationale !== "string" ||
|
|
200
228
|
proposal.rationale.length < 1 ||
|
|
201
229
|
proposal.rationale.length > 2_000 ||
|
|
@@ -207,6 +235,35 @@ function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: stri
|
|
|
207
235
|
) {
|
|
208
236
|
throw new Error(`supervisor plan proposal ${planId} has an unsupported schema`);
|
|
209
237
|
}
|
|
238
|
+
if (proposal.milestone !== undefined) {
|
|
239
|
+
const milestone = proposal.milestone;
|
|
240
|
+
if (
|
|
241
|
+
!Number.isSafeInteger(milestone.sequence) ||
|
|
242
|
+
milestone.sequence < 1 ||
|
|
243
|
+
milestone.sequence > 20 ||
|
|
244
|
+
typeof milestone.title !== "string" ||
|
|
245
|
+
milestone.title.length < 1 ||
|
|
246
|
+
milestone.title.length > 200 ||
|
|
247
|
+
typeof milestone.objective !== "string" ||
|
|
248
|
+
milestone.objective.length < 1 ||
|
|
249
|
+
milestone.objective.length > 4_000 ||
|
|
250
|
+
!Array.isArray(milestone.acceptance) ||
|
|
251
|
+
milestone.acceptance.length < 1 ||
|
|
252
|
+
milestone.acceptance.length > 50 ||
|
|
253
|
+
milestone.acceptance.some(
|
|
254
|
+
(criterion) =>
|
|
255
|
+
typeof criterion !== "string" || criterion.length < 1 || criterion.length > 500,
|
|
256
|
+
)
|
|
257
|
+
) {
|
|
258
|
+
throw new Error(`supervisor plan proposal ${planId} milestone is invalid`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
(proposal.decision === "attention" || proposal.decision === "complete") &&
|
|
263
|
+
(proposal.root !== "" || proposal.work.length > 0 || proposal.milestone !== undefined)
|
|
264
|
+
) {
|
|
265
|
+
throw new Error(`supervisor plan proposal ${planId} terminal decision contains work`);
|
|
266
|
+
}
|
|
210
267
|
for (const [index, spec] of proposal.work.entries()) {
|
|
211
268
|
if (
|
|
212
269
|
!spec ||
|
|
@@ -35,12 +35,20 @@ export interface SupervisorPlanRequest {
|
|
|
35
35
|
id: string;
|
|
36
36
|
goal_id: string;
|
|
37
37
|
sequence: number;
|
|
38
|
+
trigger?: "initial" | "recovery" | "milestone";
|
|
38
39
|
trigger_fingerprint: string;
|
|
39
40
|
prior_root_work_id: string;
|
|
40
41
|
workflow_run_id: string;
|
|
41
42
|
created_at: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
export interface SupervisorPlanMilestone {
|
|
46
|
+
sequence: number;
|
|
47
|
+
title: string;
|
|
48
|
+
objective: string;
|
|
49
|
+
acceptance: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
export interface SupervisorPlanWorkSpec {
|
|
45
53
|
key: string;
|
|
46
54
|
title: string;
|
|
@@ -53,10 +61,11 @@ export interface SupervisorPlanWorkSpec {
|
|
|
53
61
|
export interface SupervisorPlanProposal {
|
|
54
62
|
schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
|
|
55
63
|
plan_id: string;
|
|
56
|
-
decision: "apply" | "attention";
|
|
64
|
+
decision: "apply" | "complete" | "attention";
|
|
57
65
|
rationale: string;
|
|
58
66
|
root: string;
|
|
59
67
|
work: SupervisorPlanWorkSpec[];
|
|
68
|
+
milestone?: SupervisorPlanMilestone;
|
|
60
69
|
proposed_at: string;
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -65,6 +74,7 @@ export type SupervisorPlanEventType =
|
|
|
65
74
|
| "plan.resumed"
|
|
66
75
|
| "plan.proposed"
|
|
67
76
|
| "plan.applied"
|
|
77
|
+
| "plan.completed"
|
|
68
78
|
| "plan.rejected"
|
|
69
79
|
| "plan.attention"
|
|
70
80
|
| "plan.failed";
|
|
@@ -88,6 +98,7 @@ export type SupervisorPlanStatus =
|
|
|
88
98
|
| "resumable"
|
|
89
99
|
| "proposed"
|
|
90
100
|
| "applied"
|
|
101
|
+
| "completed"
|
|
91
102
|
| "rejected"
|
|
92
103
|
| "attention"
|
|
93
104
|
| "failed";
|
|
@@ -109,6 +120,8 @@ export interface SupervisorPlanHistory {
|
|
|
109
120
|
generation: number;
|
|
110
121
|
applied_work_ids: string[];
|
|
111
122
|
materialized_work_ids: string[];
|
|
123
|
+
milestones_completed: number;
|
|
124
|
+
completed: boolean;
|
|
112
125
|
latest?: SupervisorPlanRecord;
|
|
113
126
|
}
|
|
114
127
|
|
|
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
14
14
|
import { createWorkItem, readWorkItem } from "../work/index.ts";
|
|
15
15
|
import {
|
|
16
16
|
type EngineOpts,
|
|
17
|
+
readWorkflowProof,
|
|
17
18
|
runWorkflow,
|
|
18
19
|
WorkflowParkedError,
|
|
19
20
|
workflowScriptDigest,
|
|
@@ -22,6 +23,7 @@ import { readSupervisorPlan, readSupervisorPlans } from "./plan-read.ts";
|
|
|
22
23
|
import {
|
|
23
24
|
SUPERVISOR_PLAN_SCHEMA_VERSION,
|
|
24
25
|
type SupervisorPlanEvent,
|
|
26
|
+
type SupervisorPlanMilestone,
|
|
25
27
|
type SupervisorPlanOutcome,
|
|
26
28
|
type SupervisorPlanProposal,
|
|
27
29
|
type SupervisorPlanRequest,
|
|
@@ -60,6 +62,7 @@ export async function runSupervisorPlanner(
|
|
|
60
62
|
generation: input.record.projection.plan_generation,
|
|
61
63
|
work: input.record.work,
|
|
62
64
|
});
|
|
65
|
+
const trigger = planTrigger(input.record);
|
|
63
66
|
const resumable =
|
|
64
67
|
history.latest?.status === "resumable" &&
|
|
65
68
|
history.latest.request.trigger_fingerprint === triggerFingerprint &&
|
|
@@ -68,7 +71,13 @@ export async function runSupervisorPlanner(
|
|
|
68
71
|
: undefined;
|
|
69
72
|
const request = resumable
|
|
70
73
|
? resumable.request
|
|
71
|
-
: createPlanRequest(
|
|
74
|
+
: createPlanRequest(
|
|
75
|
+
coordRoot,
|
|
76
|
+
input.record,
|
|
77
|
+
history.plans.length + 1,
|
|
78
|
+
triggerFingerprint,
|
|
79
|
+
trigger,
|
|
80
|
+
);
|
|
72
81
|
const scriptPath = planScriptPath(coordRoot, input.record.intent.id, request.id);
|
|
73
82
|
if (resumable) {
|
|
74
83
|
appendPlanEvent(coordRoot, input.record.intent.id, request.id, {
|
|
@@ -96,6 +105,7 @@ export async function runSupervisorPlanner(
|
|
|
96
105
|
report.result,
|
|
97
106
|
input.record,
|
|
98
107
|
history.materialized_work_ids.length,
|
|
108
|
+
request.trigger ?? "recovery",
|
|
99
109
|
);
|
|
100
110
|
writeExclusiveJson(
|
|
101
111
|
planProposalPath(coordRoot, input.record.intent.id, request.id),
|
|
@@ -157,8 +167,12 @@ export function applySupervisorPlanProposal(input: {
|
|
|
157
167
|
const policy = input.record.intent.replanning;
|
|
158
168
|
if (!policy) throw new Error(`supervisor ${input.record.intent.id} does not allow replanning`);
|
|
159
169
|
const plan = readSupervisorPlan(coordRoot, input.record.intent.id, input.planId);
|
|
160
|
-
if (plan.status === "applied") return outcome(plan);
|
|
161
|
-
if (
|
|
170
|
+
if (plan.status === "applied" || plan.status === "completed") return outcome(plan);
|
|
171
|
+
if (
|
|
172
|
+
plan.status !== "proposed" ||
|
|
173
|
+
!plan.proposal ||
|
|
174
|
+
!["apply", "complete"].includes(plan.proposal.decision)
|
|
175
|
+
) {
|
|
162
176
|
throw new Error(`supervisor plan ${input.planId} cannot be applied from ${plan.status}`);
|
|
163
177
|
}
|
|
164
178
|
if (plan.request.prior_root_work_id !== input.record.projection.root_work_id) {
|
|
@@ -176,7 +190,16 @@ export function applySupervisorPlanProposal(input: {
|
|
|
176
190
|
plan.proposal,
|
|
177
191
|
input.record,
|
|
178
192
|
history.materialized_work_ids.filter((workId) => !workId.startsWith(`${input.planId}-`)).length,
|
|
193
|
+
plan.request.trigger ?? "recovery",
|
|
179
194
|
);
|
|
195
|
+
if (proposal.decision === "complete") {
|
|
196
|
+
appendPlanEvent(coordRoot, input.record.intent.id, input.planId, {
|
|
197
|
+
event: "plan.completed",
|
|
198
|
+
actor: input.actor,
|
|
199
|
+
reason: input.reason ?? proposal.rationale,
|
|
200
|
+
});
|
|
201
|
+
return outcome(readSupervisorPlan(coordRoot, input.record.intent.id, input.planId));
|
|
202
|
+
}
|
|
180
203
|
const ids = new Map<string, string>();
|
|
181
204
|
const created: string[] = [];
|
|
182
205
|
for (const spec of proposal.work) {
|
|
@@ -268,6 +291,7 @@ function createPlanRequest(
|
|
|
268
291
|
record: SupervisorRecord,
|
|
269
292
|
sequence: number,
|
|
270
293
|
triggerFingerprint: string,
|
|
294
|
+
trigger: NonNullable<SupervisorPlanRequest["trigger"]>,
|
|
271
295
|
): SupervisorPlanRequest {
|
|
272
296
|
const policy = record.intent.replanning;
|
|
273
297
|
if (!policy) throw new Error(`supervisor ${record.intent.id} does not allow replanning`);
|
|
@@ -280,36 +304,47 @@ function createPlanRequest(
|
|
|
280
304
|
id: planId,
|
|
281
305
|
goal_id: record.intent.id,
|
|
282
306
|
sequence,
|
|
307
|
+
trigger,
|
|
283
308
|
trigger_fingerprint: triggerFingerprint,
|
|
284
309
|
prior_root_work_id: record.projection.root_work_id,
|
|
285
310
|
workflow_run_id: `${planId}-workflow`,
|
|
286
311
|
created_at: new Date().toISOString(),
|
|
287
312
|
};
|
|
313
|
+
const script = plannerScript(coordRoot, record, planId, trigger);
|
|
288
314
|
const dir = planDir(coordRoot, record.intent.id, planId);
|
|
289
315
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
290
316
|
chmodSync(dir, 0o700);
|
|
291
317
|
writeExclusiveJson(join(dir, "request.json"), request, "supervisor plan request");
|
|
292
|
-
const script = plannerScript(record, planId);
|
|
293
318
|
writeExclusive(join(dir, "planner.mjs"), script, "supervisor planner script");
|
|
294
319
|
return request;
|
|
295
320
|
}
|
|
296
321
|
|
|
297
|
-
function plannerScript(
|
|
322
|
+
function plannerScript(
|
|
323
|
+
coordRoot: string,
|
|
324
|
+
record: SupervisorRecord,
|
|
325
|
+
planId: string,
|
|
326
|
+
trigger: NonNullable<SupervisorPlanRequest["trigger"]>,
|
|
327
|
+
): string {
|
|
298
328
|
const policy = record.intent.replanning;
|
|
299
329
|
if (!policy) throw new Error(`supervisor ${record.intent.id} does not allow replanning`);
|
|
300
330
|
const prompt = [
|
|
301
|
-
"You are producing
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
331
|
+
"You are producing one bounded replacement plan or milestone decision for a durable supervised goal.",
|
|
332
|
+
record.intent.mission
|
|
333
|
+
? "Return decision=apply with the next immutable milestone graph, decision=complete only when mission acceptance is met, or decision=attention when human judgment is required."
|
|
334
|
+
: "Return decision=apply with a replacement immutable root graph, or decision=attention when human judgment is required.",
|
|
335
|
+
"Never claim mission completion unless the frozen mission acceptance is met. Never request a workflow outside the frozen template catalog.",
|
|
305
336
|
"Dependencies may name an active work ID or an earlier key in your proposed work array.",
|
|
306
337
|
"Every proposed item must be reachable from root. The root must be a proposed key using a root-capable template.",
|
|
307
338
|
`Plan id: ${planId}`,
|
|
339
|
+
`Planning trigger: ${trigger}`,
|
|
308
340
|
`Goal: ${record.intent.title}`,
|
|
341
|
+
`Mission: ${JSON.stringify(record.intent.mission ?? null)}`,
|
|
309
342
|
`Original root: ${record.intent.root_work_id}`,
|
|
310
343
|
`Active root: ${record.projection.root_work_id}`,
|
|
311
344
|
`Remaining work attempts: ${record.projection.attempts_remaining}`,
|
|
312
345
|
`Remaining replans: ${record.projection.replans_remaining}`,
|
|
346
|
+
`Completed milestones: ${record.projection.milestones_completed}`,
|
|
347
|
+
`Remaining milestones: ${record.projection.milestones_remaining}`,
|
|
313
348
|
`Latest planner outcome: ${JSON.stringify(
|
|
314
349
|
record.plans.at(-1)
|
|
315
350
|
? {
|
|
@@ -328,13 +363,32 @@ function plannerScript(record: SupervisorRecord, planId: string): string {
|
|
|
328
363
|
dependencies: work.intent.dependencies,
|
|
329
364
|
state: work.projection.state,
|
|
330
365
|
reason: work.projection.reason,
|
|
366
|
+
proof: work.projection.proof_path
|
|
367
|
+
? (() => {
|
|
368
|
+
const proof = readWorkflowProof(coordRoot, work.projection.latest_run_id!);
|
|
369
|
+
return {
|
|
370
|
+
run_id: proof.run.id,
|
|
371
|
+
status: proof.run.status,
|
|
372
|
+
acceptance: proof.acceptance.summary,
|
|
373
|
+
repository: {
|
|
374
|
+
head_changed: proof.repository.drift.head_changed,
|
|
375
|
+
dirty_paths_added: proof.repository.drift.dirty_paths_added,
|
|
376
|
+
incomplete: proof.repository.drift.incomplete,
|
|
377
|
+
},
|
|
378
|
+
unknowns: proof.unknowns.map((unknown) => unknown.code),
|
|
379
|
+
};
|
|
380
|
+
})()
|
|
381
|
+
: null,
|
|
331
382
|
})),
|
|
332
383
|
)}`,
|
|
333
384
|
].join("\n\n");
|
|
334
385
|
const schema = {
|
|
335
386
|
type: "object",
|
|
336
387
|
properties: {
|
|
337
|
-
decision: {
|
|
388
|
+
decision: {
|
|
389
|
+
type: "string",
|
|
390
|
+
enum: record.intent.mission ? ["apply", "complete", "attention"] : ["apply", "attention"],
|
|
391
|
+
},
|
|
338
392
|
rationale: { type: "string" },
|
|
339
393
|
root: { type: "string" },
|
|
340
394
|
work: {
|
|
@@ -352,13 +406,29 @@ function plannerScript(record: SupervisorRecord, planId: string): string {
|
|
|
352
406
|
required: ["key", "title", "objective", "acceptance", "dependencies", "template"],
|
|
353
407
|
},
|
|
354
408
|
},
|
|
409
|
+
milestone: {
|
|
410
|
+
type: "object",
|
|
411
|
+
properties: {
|
|
412
|
+
sequence: { type: "number" },
|
|
413
|
+
title: { type: "string" },
|
|
414
|
+
objective: { type: "string" },
|
|
415
|
+
acceptance: { type: "array", items: { type: "string" } },
|
|
416
|
+
},
|
|
417
|
+
required: ["sequence", "title", "objective", "acceptance"],
|
|
418
|
+
},
|
|
355
419
|
},
|
|
356
420
|
required: ["decision", "rationale", "root", "work"],
|
|
357
421
|
};
|
|
358
422
|
return [
|
|
359
423
|
`export const meta = ${JSON.stringify({ name: `replan-${planId}` })};`,
|
|
360
424
|
"export default async ({ agent, stage }) => {",
|
|
361
|
-
|
|
425
|
+
` stage(${JSON.stringify(
|
|
426
|
+
trigger === "initial"
|
|
427
|
+
? "Plan initial mission milestone"
|
|
428
|
+
: trigger === "milestone"
|
|
429
|
+
? "Reassess completed milestone"
|
|
430
|
+
: "Replan blocked goal",
|
|
431
|
+
)});`,
|
|
362
432
|
` return await agent(${JSON.stringify(prompt)}, ${JSON.stringify({
|
|
363
433
|
specialist: policy.planner_specialist,
|
|
364
434
|
schema,
|
|
@@ -374,6 +444,7 @@ function normalizeProposal(
|
|
|
374
444
|
raw: unknown,
|
|
375
445
|
record: SupervisorRecord,
|
|
376
446
|
previouslyApplied: number,
|
|
447
|
+
trigger: NonNullable<SupervisorPlanRequest["trigger"]>,
|
|
377
448
|
): SupervisorPlanProposal {
|
|
378
449
|
const policy = record.intent.replanning;
|
|
379
450
|
if (!policy) throw new Error(`supervisor ${record.intent.id} does not allow replanning`);
|
|
@@ -381,12 +452,17 @@ function normalizeProposal(
|
|
|
381
452
|
throw new Error("planner result must be an object");
|
|
382
453
|
}
|
|
383
454
|
const value = raw as Record<string, unknown>;
|
|
384
|
-
const decision = enumValue(value.decision, ["apply", "attention"], "plan decision");
|
|
455
|
+
const decision = enumValue(value.decision, ["apply", "complete", "attention"], "plan decision");
|
|
385
456
|
const rationale = bounded(value.rationale, "plan rationale", MAX_REASON);
|
|
386
457
|
const root = typeof value.root === "string" ? value.root.trim() : "";
|
|
387
458
|
if (!Array.isArray(value.work)) throw new Error("plan work must be an array");
|
|
388
|
-
if (decision === "attention") {
|
|
389
|
-
if (root || value.work.length > 0
|
|
459
|
+
if (decision === "attention" || decision === "complete") {
|
|
460
|
+
if (root || value.work.length > 0 || value.milestone !== undefined) {
|
|
461
|
+
throw new Error(`${decision} plan must not contain work or milestone data`);
|
|
462
|
+
}
|
|
463
|
+
if (decision === "complete" && (!record.intent.mission || trigger !== "milestone")) {
|
|
464
|
+
throw new Error("mission completion may be proposed only at a milestone boundary");
|
|
465
|
+
}
|
|
390
466
|
return {
|
|
391
467
|
schema_version: SUPERVISOR_PLAN_SCHEMA_VERSION,
|
|
392
468
|
plan_id: planId,
|
|
@@ -397,6 +473,17 @@ function normalizeProposal(
|
|
|
397
473
|
proposed_at: new Date().toISOString(),
|
|
398
474
|
};
|
|
399
475
|
}
|
|
476
|
+
const milestone = record.intent.mission
|
|
477
|
+
? normalizeMilestone(value.milestone, record.projection.milestones_completed + 1)
|
|
478
|
+
: undefined;
|
|
479
|
+
if (!record.intent.mission && value.milestone !== undefined) {
|
|
480
|
+
throw new Error("non-mission replacement plan must not contain milestone data");
|
|
481
|
+
}
|
|
482
|
+
if (milestone && milestone.sequence > record.intent.mission!.max_milestones) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`milestone ${milestone.sequence} exceeds mission limit ${record.intent.mission!.max_milestones}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
400
487
|
if (value.work.length < 1 || value.work.length > policy.max_work_items_per_plan) {
|
|
401
488
|
throw new Error(`plan work must contain 1 to ${policy.max_work_items_per_plan} items`);
|
|
402
489
|
}
|
|
@@ -477,10 +564,45 @@ function normalizeProposal(
|
|
|
477
564
|
rationale,
|
|
478
565
|
root,
|
|
479
566
|
work,
|
|
567
|
+
...(milestone ? { milestone } : {}),
|
|
480
568
|
proposed_at: new Date().toISOString(),
|
|
481
569
|
};
|
|
482
570
|
}
|
|
483
571
|
|
|
572
|
+
function normalizeMilestone(raw: unknown, expectedSequence: number): SupervisorPlanMilestone {
|
|
573
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
574
|
+
throw new Error("mission plan must contain a milestone object");
|
|
575
|
+
}
|
|
576
|
+
const value = raw as Record<string, unknown>;
|
|
577
|
+
if (value.sequence !== expectedSequence) {
|
|
578
|
+
throw new Error(`milestone sequence must be ${expectedSequence}`);
|
|
579
|
+
}
|
|
580
|
+
if (!Array.isArray(value.acceptance) || value.acceptance.length < 1) {
|
|
581
|
+
throw new Error("milestone acceptance must be a non-empty array");
|
|
582
|
+
}
|
|
583
|
+
if (value.acceptance.length > 50) throw new Error("milestone acceptance exceeds 50 criteria");
|
|
584
|
+
return {
|
|
585
|
+
sequence: expectedSequence,
|
|
586
|
+
title: bounded(value.title, "milestone title", 200),
|
|
587
|
+
objective: bounded(value.objective, "milestone objective", 4_000),
|
|
588
|
+
acceptance: value.acceptance.map((criterion, index) =>
|
|
589
|
+
bounded(criterion, `milestone acceptance[${index}]`, 500),
|
|
590
|
+
),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function planTrigger(record: SupervisorRecord): NonNullable<SupervisorPlanRequest["trigger"]> {
|
|
595
|
+
if (
|
|
596
|
+
record.intent.mission &&
|
|
597
|
+
record.projection.plan_generation === 0 &&
|
|
598
|
+
record.work.length === 0
|
|
599
|
+
) {
|
|
600
|
+
return "initial";
|
|
601
|
+
}
|
|
602
|
+
const root = record.work.find((work) => work.intent.id === record.projection.root_work_id);
|
|
603
|
+
return record.intent.mission && root?.projection.state === "succeeded" ? "milestone" : "recovery";
|
|
604
|
+
}
|
|
605
|
+
|
|
484
606
|
function appendPlanEvent(
|
|
485
607
|
coordRootRaw: string,
|
|
486
608
|
goalId: string,
|
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
type SupervisorPlanEvent,
|
|
5
5
|
type SupervisorPlanEventType,
|
|
6
6
|
type SupervisorPlanHistory,
|
|
7
|
+
type SupervisorPlanMilestone,
|
|
7
8
|
type SupervisorPlanOutcome,
|
|
8
9
|
type SupervisorPlanProposal,
|
|
9
10
|
type SupervisorPlanRecord,
|
|
@@ -37,6 +38,7 @@ export {
|
|
|
37
38
|
type SupervisorAutomationPolicy,
|
|
38
39
|
type SupervisorIntent,
|
|
39
40
|
type SupervisorLimits,
|
|
41
|
+
type SupervisorMission,
|
|
40
42
|
type SupervisorNextAction,
|
|
41
43
|
type SupervisorProjection,
|
|
42
44
|
type SupervisorRecord,
|
|
@@ -97,9 +97,13 @@ export async function runSupervisor(input: RunSupervisorInput): Promise<Supervis
|
|
|
97
97
|
|
|
98
98
|
const before = graphFingerprint(record);
|
|
99
99
|
let changed = false;
|
|
100
|
-
if (
|
|
100
|
+
if (
|
|
101
|
+
record.projection.next_action === "replan" ||
|
|
102
|
+
record.projection.next_action === "plan_initial" ||
|
|
103
|
+
record.projection.next_action === "plan_milestone"
|
|
104
|
+
) {
|
|
101
105
|
log(
|
|
102
|
-
`[${record.intent.title}]
|
|
106
|
+
`[${record.intent.title}] ${record.projection.next_action} at graph generation ${record.projection.plan_generation}`,
|
|
103
107
|
);
|
|
104
108
|
const plan = await runSupervisorPlanner({
|
|
105
109
|
coordRoot,
|
|
@@ -118,6 +122,9 @@ export async function runSupervisor(input: RunSupervisorInput): Promise<Supervis
|
|
|
118
122
|
record,
|
|
119
123
|
);
|
|
120
124
|
}
|
|
125
|
+
if (record.projection.state === "succeeded") {
|
|
126
|
+
return report("succeeded", record.projection.reason, record);
|
|
127
|
+
}
|
|
121
128
|
if (record.projection.state !== "ready") {
|
|
122
129
|
return report(stopForProjection(record.projection), record.projection.reason, record);
|
|
123
130
|
}
|
|
@@ -616,9 +616,12 @@ function projectionFingerprint(projection: SupervisorRecord["projection"]): stri
|
|
|
616
616
|
projection.attempts_used,
|
|
617
617
|
projection.attempts_remaining,
|
|
618
618
|
projection.root_work_id,
|
|
619
|
+
projection.root_materialized,
|
|
619
620
|
projection.plan_generation,
|
|
620
621
|
projection.replans_used,
|
|
621
622
|
projection.replans_remaining,
|
|
623
|
+
projection.milestones_completed,
|
|
624
|
+
projection.milestones_remaining,
|
|
622
625
|
projection.pending_plan_id,
|
|
623
626
|
projection.latest_plan_status,
|
|
624
627
|
projection.ready_work,
|