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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/supervisor.d.ts.map +1 -1
  3. package/dist/commands/supervisor.js +223 -5
  4. package/dist/core/supervisor/index.d.ts +4 -1
  5. package/dist/core/supervisor/index.d.ts.map +1 -1
  6. package/dist/core/supervisor/index.js +3 -0
  7. package/dist/core/supervisor/plan-api.d.ts +16 -0
  8. package/dist/core/supervisor/plan-api.d.ts.map +1 -0
  9. package/dist/core/supervisor/plan-api.js +34 -0
  10. package/dist/core/supervisor/plan-read.d.ts +4 -0
  11. package/dist/core/supervisor/plan-read.d.ts.map +1 -0
  12. package/dist/core/supervisor/plan-read.js +315 -0
  13. package/dist/core/supervisor/plan-types.d.ts +123 -0
  14. package/dist/core/supervisor/plan-types.d.ts.map +1 -0
  15. package/dist/core/supervisor/plan-types.js +14 -0
  16. package/dist/core/supervisor/planning.d.ts +26 -0
  17. package/dist/core/supervisor/planning.d.ts.map +1 -0
  18. package/dist/core/supervisor/planning.js +579 -0
  19. package/dist/core/supervisor/read.d.ts +3 -1
  20. package/dist/core/supervisor/read.d.ts.map +1 -1
  21. package/dist/core/supervisor/read.js +2 -0
  22. package/dist/core/supervisor/runner.d.ts +3 -0
  23. package/dist/core/supervisor/runner.d.ts.map +1 -1
  24. package/dist/core/supervisor/runner.js +42 -7
  25. package/dist/core/supervisor/service.d.ts.map +1 -1
  26. package/dist/core/supervisor/service.js +10 -0
  27. package/dist/core/supervisor/state.d.ts +27 -2
  28. package/dist/core/supervisor/state.d.ts.map +1 -1
  29. package/dist/core/supervisor/state.js +302 -13
  30. package/package.json +1 -1
  31. package/src/commands/supervisor.ts +275 -5
  32. package/src/core/supervisor/index.ts +21 -0
  33. package/src/core/supervisor/plan-api.ts +47 -0
  34. package/src/core/supervisor/plan-read.ts +356 -0
  35. package/src/core/supervisor/plan-types.ts +157 -0
  36. package/src/core/supervisor/planning.ts +698 -0
  37. package/src/core/supervisor/read.ts +17 -0
  38. package/src/core/supervisor/runner.ts +46 -2
  39. package/src/core/supervisor/service.ts +10 -0
  40. 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: string;
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(input.title ?? root.intent.title, "supervisor title", MAX_TITLE),
112
- root_work_id: input.rootWorkId,
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 work = collectSupervisorWork(coordRoot, intent.root_work_id);
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
- projection: deriveProjection(coordRoot, intent, work, ignoreLease),
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 === intent.root_work_id);
239
- if (!root) throw new Error(`supervisor ${intent.id} root work is missing`);
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: intent.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);