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.
@@ -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(coordRoot, input.record, history.plans.length + 1, triggerFingerprint);
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 (plan.status !== "proposed" || !plan.proposal || plan.proposal.decision !== "apply") {
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(record: SupervisorRecord, planId: string): string {
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 a bounded replacement plan for a durable supervised goal.",
302
- "The current graph is blocked and cannot make a legal scheduling step.",
303
- "Return decision=apply with a new immutable root graph, or decision=attention when human judgment is required.",
304
- "Never claim work is complete. Never request a workflow outside the frozen template catalog.",
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: { type: "string", enum: ["apply", "attention"] },
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
- ' stage("Replan blocked goal");',
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) throw new Error("attention plan must not contain work");
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 (record.projection.next_action === "replan") {
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}] replan active graph generation ${record.projection.plan_generation}`,
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,