harnery 0.17.0 → 0.19.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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/browse.d.ts.map +1 -1
  3. package/dist/commands/browse.js +187 -4
  4. package/dist/commands/supervisor.d.ts.map +1 -1
  5. package/dist/commands/supervisor.js +76 -4
  6. package/dist/core/supervisor/index.d.ts +3 -3
  7. package/dist/core/supervisor/index.d.ts.map +1 -1
  8. package/dist/core/supervisor/index.js +2 -2
  9. package/dist/core/supervisor/plan-read.d.ts +2 -1
  10. package/dist/core/supervisor/plan-read.d.ts.map +1 -1
  11. package/dist/core/supervisor/plan-read.js +222 -3
  12. package/dist/core/supervisor/plan-types.d.ts +60 -3
  13. package/dist/core/supervisor/plan-types.d.ts.map +1 -1
  14. package/dist/core/supervisor/plan-types.js +2 -0
  15. package/dist/core/supervisor/planning.d.ts.map +1 -1
  16. package/dist/core/supervisor/planning.js +804 -56
  17. package/dist/core/supervisor/read.d.ts +3 -3
  18. package/dist/core/supervisor/read.d.ts.map +1 -1
  19. package/dist/core/supervisor/read.js +2 -2
  20. package/dist/core/supervisor/runner.d.ts.map +1 -1
  21. package/dist/core/supervisor/runner.js +7 -2
  22. package/dist/core/supervisor/service.js +3 -0
  23. package/dist/core/supervisor/state.d.ts +17 -2
  24. package/dist/core/supervisor/state.d.ts.map +1 -1
  25. package/dist/core/supervisor/state.js +211 -33
  26. package/dist/core/workflow/engine.d.ts.map +1 -1
  27. package/dist/core/workflow/engine.js +24 -6
  28. package/dist/core/workflow/types.d.ts +6 -0
  29. package/dist/core/workflow/types.d.ts.map +1 -1
  30. package/dist/core/workflow/validate.d.ts +3 -2
  31. package/dist/core/workflow/validate.d.ts.map +1 -1
  32. package/dist/core/workflow/validate.js +31 -3
  33. package/dist/lib/browser/client.d.ts +18 -0
  34. package/dist/lib/browser/client.d.ts.map +1 -1
  35. package/dist/lib/browser/client.js +39 -0
  36. package/dist/lib/browser/geometry.d.ts +146 -0
  37. package/dist/lib/browser/geometry.d.ts.map +1 -0
  38. package/dist/lib/browser/geometry.js +525 -0
  39. package/dist/lib/browser/index.d.ts +2 -0
  40. package/dist/lib/browser/index.d.ts.map +1 -1
  41. package/dist/lib/browser/target-size.d.ts +65 -0
  42. package/dist/lib/browser/target-size.d.ts.map +1 -0
  43. package/dist/lib/browser/target-size.js +108 -0
  44. package/package.json +7 -1
  45. package/src/commands/browse.ts +319 -0
  46. package/src/commands/supervisor.ts +92 -5
  47. package/src/core/supervisor/index.ts +17 -1
  48. package/src/core/supervisor/plan-read.ts +292 -2
  49. package/src/core/supervisor/plan-types.ts +69 -1
  50. package/src/core/supervisor/planning.ts +1012 -51
  51. package/src/core/supervisor/read.ts +16 -1
  52. package/src/core/supervisor/runner.ts +9 -2
  53. package/src/core/supervisor/service.ts +3 -0
  54. package/src/core/supervisor/state.ts +272 -31
  55. package/src/core/workflow/engine.ts +29 -6
  56. package/src/core/workflow/types.ts +6 -0
  57. package/src/core/workflow/validate.ts +34 -3
  58. package/src/lib/browser/client.ts +62 -0
  59. package/src/lib/browser/geometry.ts +695 -0
  60. package/src/lib/browser/index.ts +25 -0
  61. package/src/lib/browser/target-size.ts +180 -0
@@ -1,13 +1,27 @@
1
- export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.ts";
2
1
  export {
2
+ readSupervisorPlan,
3
+ readSupervisorPlanReviewReceipt,
4
+ readSupervisorPlans,
5
+ } from "./plan-read.ts";
6
+ export {
7
+ MAX_SUPERVISOR_PLAN_REVIEWERS,
8
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
3
9
  SUPERVISOR_PLAN_SCHEMA_VERSION,
4
10
  type SupervisorPlanEvent,
5
11
  type SupervisorPlanEventType,
6
12
  type SupervisorPlanHistory,
13
+ type SupervisorPlanMilestone,
7
14
  type SupervisorPlanOutcome,
8
15
  type SupervisorPlanProposal,
9
16
  type SupervisorPlanRecord,
10
17
  type SupervisorPlanRequest,
18
+ type SupervisorPlanReviewFinding,
19
+ type SupervisorPlanReviewReceipt,
20
+ type SupervisorPlanReviewReviewer,
21
+ type SupervisorPlanReviewRound,
22
+ type SupervisorPlanReviewStatus,
23
+ type SupervisorPlanReviewSummary,
24
+ type SupervisorPlanReviewVerdict,
11
25
  type SupervisorPlanStatus,
12
26
  type SupervisorPlanTemplate,
13
27
  type SupervisorPlanWorkSpec,
@@ -37,6 +51,7 @@ export {
37
51
  type SupervisorAutomationPolicy,
38
52
  type SupervisorIntent,
39
53
  type SupervisorLimits,
54
+ type SupervisorMission,
40
55
  type SupervisorNextAction,
41
56
  type SupervisorProjection,
42
57
  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,
@@ -20,6 +20,8 @@ import type { WorkflowSpecialistProfile } from "../workflow/types.ts";
20
20
  import { readSupervisorPlans } from "./plan-read.ts";
21
21
  import {
22
22
  type CreateSupervisorReplanningInput,
23
+ MAX_SUPERVISOR_PLAN_REVIEWERS,
24
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
23
25
  type SupervisorPlanHistory,
24
26
  type SupervisorPlanRecord,
25
27
  type SupervisorReplanningPolicy,
@@ -30,6 +32,8 @@ export const SUPERVISOR_INTENT_SCHEMA_VERSION = 1 as const;
30
32
 
31
33
  const GOAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
32
34
  const MAX_TITLE = 200;
35
+ const MAX_OBJECTIVE = 4_000;
36
+ const MAX_ACCEPTANCE = 50;
33
37
  const MAX_INTENT_BYTES = 256 * 1024;
34
38
  const MAX_TEMPLATES = 20;
35
39
  const TEMPLATE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -50,6 +54,18 @@ export interface SupervisorAutomationPolicy {
50
54
  retry_blocked: boolean;
51
55
  }
52
56
 
57
+ export interface SupervisorMission {
58
+ objective: string;
59
+ acceptance: string[];
60
+ max_milestones: number;
61
+ }
62
+
63
+ export interface CreateSupervisorMissionInput {
64
+ objective: string;
65
+ acceptance: readonly string[];
66
+ maxMilestones?: number;
67
+ }
68
+
53
69
  export interface SupervisorIntent {
54
70
  schema_version: typeof SUPERVISOR_INTENT_SCHEMA_VERSION;
55
71
  id: string;
@@ -58,6 +74,7 @@ export interface SupervisorIntent {
58
74
  specialists: Record<string, WorkflowSpecialistProfile>;
59
75
  limits: SupervisorLimits;
60
76
  automation: SupervisorAutomationPolicy;
77
+ mission?: SupervisorMission;
61
78
  replanning?: SupervisorReplanningPolicy;
62
79
  created_at: string;
63
80
  }
@@ -77,6 +94,8 @@ export type SupervisorNextAction =
77
94
  | "review"
78
95
  | "retry"
79
96
  | "replan"
97
+ | "plan_initial"
98
+ | "plan_milestone"
80
99
  | "review_plan"
81
100
  | "none";
82
101
 
@@ -84,6 +103,7 @@ export interface SupervisorProjection {
84
103
  id: string;
85
104
  title: string;
86
105
  root_work_id: string;
106
+ root_materialized: boolean;
87
107
  state: SupervisorState;
88
108
  reason: string;
89
109
  next_action: SupervisorNextAction;
@@ -98,6 +118,8 @@ export interface SupervisorProjection {
98
118
  plan_generation: number;
99
119
  replans_used: number;
100
120
  replans_remaining: number;
121
+ milestones_completed: number;
122
+ milestones_remaining: number;
101
123
  pending_plan_id?: string;
102
124
  latest_plan_status?: SupervisorPlanRecord["status"];
103
125
  governed_work_ids: string[];
@@ -112,32 +134,58 @@ export interface SupervisorRecord {
112
134
 
113
135
  export interface CreateSupervisorInput {
114
136
  coordRoot: string;
115
- rootWorkId: string;
137
+ rootWorkId?: string;
116
138
  specialists: Readonly<Record<string, WorkflowSpecialistProfile>>;
117
139
  title?: string;
118
140
  id?: string;
119
141
  limits?: Partial<SupervisorLimits>;
120
142
  automation?: Partial<SupervisorAutomationPolicy>;
143
+ mission?: CreateSupervisorMissionInput;
121
144
  replanning?: CreateSupervisorReplanningInput;
122
145
  }
123
146
 
124
147
  export function createSupervisor(input: CreateSupervisorInput): SupervisorRecord {
125
148
  const coordRoot = resolve(input.coordRoot);
126
- assertWorkId(input.rootWorkId);
127
- const root = readWorkItem(coordRoot, input.rootWorkId);
128
149
  const id = input.id ?? newSupervisorId();
129
150
  assertSupervisorId(id);
151
+ const mission = input.mission ? normalizeMission(input.mission) : undefined;
152
+ if (mission && !input.replanning) {
153
+ throw new Error("supervisor mission requires a replanning policy");
154
+ }
155
+ if (!input.rootWorkId && !mission) {
156
+ throw new Error(
157
+ "supervisor creation without root work requires a mission and replanning policy",
158
+ );
159
+ }
160
+ if (input.rootWorkId) assertWorkId(input.rootWorkId);
161
+ const root = input.rootWorkId ? readWorkItem(coordRoot, input.rootWorkId) : undefined;
162
+ const limits = normalizeLimits(input.limits);
130
163
  const replanning = input.replanning
131
- ? normalizeReplanning(input.replanning, input.specialists)
164
+ ? normalizeReplanning(input.replanning, input.specialists, limits)
132
165
  : undefined;
166
+ if (
167
+ mission &&
168
+ !input.rootWorkId &&
169
+ replanning &&
170
+ replanning.max_replans <= mission.max_milestones
171
+ ) {
172
+ throw new Error(
173
+ "objective-first mission max_replans must exceed max_milestones to reserve completion review",
174
+ );
175
+ }
133
176
  const intent: SupervisorIntent = {
134
177
  schema_version: SUPERVISOR_INTENT_SCHEMA_VERSION,
135
178
  id,
136
- title: bounded(input.title ?? root.intent.title, "supervisor title", MAX_TITLE),
137
- root_work_id: input.rootWorkId,
179
+ title: bounded(
180
+ input.title ?? root?.intent.title ?? missionTitle(mission!),
181
+ "supervisor title",
182
+ MAX_TITLE,
183
+ ),
184
+ root_work_id: input.rootWorkId ?? missionRootId(id),
138
185
  specialists: normalizeWorkflowSpecialists(input.specialists),
139
- limits: normalizeLimits(input.limits),
186
+ limits,
140
187
  automation: normalizeAutomation(input.automation),
188
+ ...(mission ? { mission } : {}),
141
189
  ...(replanning ? { replanning } : {}),
142
190
  created_at: new Date().toISOString(),
143
191
  };
@@ -248,7 +296,11 @@ function readSupervisorInternal(
248
296
  const coordRoot = resolve(coordRootRaw);
249
297
  const intent = readSupervisorIntent(coordRoot, goalId);
250
298
  const plans = readSupervisorPlans(coordRoot, intent.id, intent.root_work_id);
251
- const work = collectSupervisorWork(coordRoot, plans.active_root_work_id);
299
+ const activeRootExists = workIntentExists(coordRoot, plans.active_root_work_id);
300
+ if (!activeRootExists && (!intent.mission || plans.generation > 0)) {
301
+ throw new Error(`supervisor ${intent.id} root work is missing`);
302
+ }
303
+ const work = activeRootExists ? collectSupervisorWork(coordRoot, plans.active_root_work_id) : [];
252
304
  const governed = collectGovernedWork(coordRoot, intent.root_work_id, plans, work);
253
305
  return {
254
306
  intent,
@@ -267,7 +319,6 @@ function deriveProjection(
267
319
  ignoreLease: boolean,
268
320
  ): SupervisorProjection {
269
321
  const root = work.find((record) => record.intent.id === plans.active_root_work_id);
270
- if (!root) throw new Error(`supervisor ${intent.id} root work is missing`);
271
322
  const attemptsUsed = governed.reduce((sum, record) => sum + record.projection.attempts_used, 0);
272
323
  const readyWork = work
273
324
  .filter((record) => record.projection.state === "ready")
@@ -311,10 +362,18 @@ function deriveProjection(
311
362
  ...cancelled,
312
363
  ...terminalBlocked,
313
364
  ]);
365
+ const initialRootAccepted =
366
+ intent.mission !== undefined &&
367
+ governed.some(
368
+ (record) =>
369
+ record.intent.id === intent.root_work_id && record.projection.state === "succeeded",
370
+ );
371
+ const milestonesCompleted = plans.milestones_completed + (initialRootAccepted ? 1 : 0);
314
372
  const base = {
315
373
  id: intent.id,
316
374
  title: intent.title,
317
375
  root_work_id: plans.active_root_work_id,
376
+ root_materialized: root !== undefined,
318
377
  work_ids: work.map((record) => record.intent.id),
319
378
  ready_work: readyWork,
320
379
  resumable_work: resumableWork,
@@ -326,6 +385,8 @@ function deriveProjection(
326
385
  plan_generation: plans.generation,
327
386
  replans_used: plans.plans.length,
328
387
  replans_remaining: Math.max(0, (intent.replanning?.max_replans ?? 0) - plans.plans.length),
388
+ milestones_completed: milestonesCompleted,
389
+ milestones_remaining: Math.max(0, (intent.mission?.max_milestones ?? 0) - milestonesCompleted),
329
390
  pending_plan_id: plans.latest?.status === "proposed" ? plans.latest.request.id : undefined,
330
391
  latest_plan_status: plans.latest?.status,
331
392
  governed_work_ids: governed.map((record) => record.intent.id),
@@ -338,11 +399,11 @@ function deriveProjection(
338
399
  next_action: "wait_for_run",
339
400
  };
340
401
  }
341
- if (root.projection.state === "succeeded") {
402
+ if (intent.mission && plans.completed) {
342
403
  return {
343
404
  ...base,
344
405
  state: "succeeded",
345
- reason: "root work was explicitly accepted",
406
+ reason: "mission completion was explicitly accepted",
346
407
  next_action: "none",
347
408
  };
348
409
  }
@@ -370,6 +431,64 @@ function deriveProjection(
370
431
  next_action: "replan",
371
432
  };
372
433
  }
434
+ const triggerFingerprint = supervisorGraphFingerprint({
435
+ rootWorkId: plans.active_root_work_id,
436
+ generation: plans.generation,
437
+ work,
438
+ });
439
+ const latestHandledSameGraph =
440
+ plans.latest?.request.trigger_fingerprint === triggerFingerprint &&
441
+ plans.latest.request.prior_root_work_id === plans.active_root_work_id &&
442
+ plans.latest.status === "attention";
443
+ if (latestHandledSameGraph && plans.latest) {
444
+ return {
445
+ ...base,
446
+ state: "awaiting_attention",
447
+ reason: plans.latest.reason ?? `plan ${plans.latest.request.id} requires attention`,
448
+ next_action: "none",
449
+ };
450
+ }
451
+ if (!root && intent.mission) {
452
+ if (!intent.replanning || plans.plans.length >= intent.replanning.max_replans) {
453
+ return {
454
+ ...base,
455
+ state: "budget_exhausted",
456
+ reason: "mission exhausted its planning budget before creating an initial milestone",
457
+ next_action: "none",
458
+ };
459
+ }
460
+ return {
461
+ ...base,
462
+ state: "ready",
463
+ reason: "mission is ready for its initial bounded milestone plan",
464
+ next_action: "plan_initial",
465
+ };
466
+ }
467
+ if (!root) throw new Error(`supervisor ${intent.id} root work is missing`);
468
+ if (root.projection.state === "succeeded" && intent.mission) {
469
+ if (!intent.replanning || plans.plans.length >= intent.replanning.max_replans) {
470
+ return {
471
+ ...base,
472
+ state: "budget_exhausted",
473
+ reason: "mission exhausted its planning budget at a milestone boundary",
474
+ next_action: "none",
475
+ };
476
+ }
477
+ return {
478
+ ...base,
479
+ state: "ready",
480
+ reason: `milestone ${plans.milestones_completed} is accepted and requires mission reassessment`,
481
+ next_action: "plan_milestone",
482
+ };
483
+ }
484
+ if (root.projection.state === "succeeded") {
485
+ return {
486
+ ...base,
487
+ state: "succeeded",
488
+ reason: "root work was explicitly accepted",
489
+ next_action: "none",
490
+ };
491
+ }
373
492
  const resumableDispatchable = intent.automation.resume_approved ? resumableWork : [];
374
493
  const attemptDispatchable = [
375
494
  ...readyWork,
@@ -448,15 +567,6 @@ function deriveProjection(
448
567
  next_action: "none",
449
568
  };
450
569
  }
451
- const triggerFingerprint = supervisorGraphFingerprint({
452
- rootWorkId: plans.active_root_work_id,
453
- generation: plans.generation,
454
- work,
455
- });
456
- const latestHandledSameGraph =
457
- plans.latest?.request.trigger_fingerprint === triggerFingerprint &&
458
- plans.latest.request.prior_root_work_id === plans.active_root_work_id &&
459
- plans.latest.status === "attention";
460
570
  const canReplan =
461
571
  intent.replanning !== undefined &&
462
572
  plans.plans.length < intent.replanning.max_replans &&
@@ -484,14 +594,6 @@ function deriveProjection(
484
594
  next_action: "none",
485
595
  };
486
596
  }
487
- if (latestHandledSameGraph && plans.latest) {
488
- return {
489
- ...base,
490
- state: "awaiting_attention",
491
- reason: plans.latest.reason ?? `plan ${plans.latest.request.id} requires attention`,
492
- next_action: "none",
493
- };
494
- }
495
597
  return {
496
598
  ...base,
497
599
  state: "blocked",
@@ -531,6 +633,19 @@ function validateIntent(intent: SupervisorIntent, goalId: string): void {
531
633
  }
532
634
  bounded(intent.title, "supervisor title", MAX_TITLE);
533
635
  assertWorkId(intent.root_work_id);
636
+ if (intent.mission) {
637
+ const normalizedMission = normalizeMission({
638
+ objective: intent.mission.objective,
639
+ acceptance: intent.mission.acceptance,
640
+ maxMilestones: intent.mission.max_milestones,
641
+ });
642
+ if (JSON.stringify(normalizedMission) !== JSON.stringify(intent.mission)) {
643
+ throw new Error(`supervisor intent ${goalId} mission is not canonical`);
644
+ }
645
+ if (!intent.replanning) {
646
+ throw new Error(`supervisor intent ${goalId} mission requires replanning`);
647
+ }
648
+ }
534
649
  const specialists = normalizeWorkflowSpecialists(intent.specialists);
535
650
  if (JSON.stringify(specialists) !== JSON.stringify(intent.specialists)) {
536
651
  throw new Error(`supervisor intent ${goalId} specialists are not canonical`);
@@ -543,7 +658,33 @@ function validateIntent(intent: SupervisorIntent, goalId: string): void {
543
658
  ) {
544
659
  throw new Error(`supervisor intent ${goalId} automation policy is not canonical`);
545
660
  }
546
- if (intent.replanning) validateReplanning(intent.replanning, specialists, goalId);
661
+ if (intent.replanning) validateReplanning(intent.replanning, specialists, intent.limits, goalId);
662
+ }
663
+
664
+ function normalizeMission(input: CreateSupervisorMissionInput): SupervisorMission {
665
+ if (!Array.isArray(input.acceptance)) {
666
+ throw new Error("supervisor mission acceptance must be an array");
667
+ }
668
+ if (input.acceptance.length < 1 || input.acceptance.length > MAX_ACCEPTANCE) {
669
+ throw new Error(`supervisor mission acceptance must contain 1 to ${MAX_ACCEPTANCE} criteria`);
670
+ }
671
+ const acceptance = input.acceptance.map((criterion, index) =>
672
+ bounded(criterion, `supervisor mission acceptance[${index}]`, 500),
673
+ );
674
+ if (new Set(acceptance).size !== acceptance.length) {
675
+ throw new Error("supervisor mission acceptance criteria must be unique");
676
+ }
677
+ return {
678
+ objective: bounded(input.objective, "supervisor mission objective", MAX_OBJECTIVE),
679
+ acceptance,
680
+ max_milestones: positive(input.maxMilestones ?? 4, "supervisor mission max_milestones", 20),
681
+ };
682
+ }
683
+
684
+ function missionTitle(mission: SupervisorMission): string {
685
+ return mission.objective.length <= MAX_TITLE
686
+ ? mission.objective
687
+ : `${mission.objective.slice(0, MAX_TITLE - 1).trimEnd()}…`;
547
688
  }
548
689
 
549
690
  function normalizeLimits(input: Partial<SupervisorLimits> | undefined): SupervisorLimits {
@@ -582,6 +723,7 @@ function normalizeAutomation(
582
723
  function normalizeReplanning(
583
724
  input: CreateSupervisorReplanningInput,
584
725
  specialistsInput: Readonly<Record<string, WorkflowSpecialistProfile>>,
726
+ limits: SupervisorLimits,
585
727
  ): SupervisorReplanningPolicy {
586
728
  const specialists = normalizeWorkflowSpecialists(specialistsInput);
587
729
  const plannerSpecialist = bounded(input.plannerSpecialist, "planner specialist", 100);
@@ -631,6 +773,9 @@ function normalizeReplanning(
631
773
  if (maxWorkItemsPerPlan > maxTotalWorkItems) {
632
774
  throw new Error("replanning max_work_items_per_plan cannot exceed max_total_work_items");
633
775
  }
776
+ const review = input.review
777
+ ? normalizeReviewPolicy(input.review, specialists, plannerSpecialist, limits)
778
+ : undefined;
634
779
  return {
635
780
  planner_specialist: plannerSpecialist,
636
781
  auto_apply: boolean(input.autoApply ?? false, "replanning auto_apply"),
@@ -638,12 +783,60 @@ function normalizeReplanning(
638
783
  max_work_items_per_plan: maxWorkItemsPerPlan,
639
784
  max_total_work_items: maxTotalWorkItems,
640
785
  templates,
786
+ ...(review ? { review } : {}),
787
+ };
788
+ }
789
+
790
+ function normalizeReviewPolicy(
791
+ input: NonNullable<CreateSupervisorReplanningInput["review"]>,
792
+ specialists: Record<string, WorkflowSpecialistProfile>,
793
+ plannerSpecialist: string,
794
+ limits: SupervisorLimits,
795
+ ): NonNullable<SupervisorReplanningPolicy["review"]> {
796
+ if (!Array.isArray(input.reviewerSpecialists) || input.reviewerSpecialists.length < 1) {
797
+ throw new Error("replanning review requires at least one reviewer specialist");
798
+ }
799
+ if (input.reviewerSpecialists.length > MAX_SUPERVISOR_PLAN_REVIEWERS) {
800
+ throw new Error(
801
+ `replanning review cannot exceed ${MAX_SUPERVISOR_PLAN_REVIEWERS} reviewer specialists`,
802
+ );
803
+ }
804
+ const reviewerSpecialists = input.reviewerSpecialists.map((specialist, index) =>
805
+ bounded(specialist, `reviewer specialist[${index}]`, 100),
806
+ );
807
+ if (new Set(reviewerSpecialists).size !== reviewerSpecialists.length) {
808
+ throw new Error("replanning reviewer specialists must be unique");
809
+ }
810
+ for (const specialist of reviewerSpecialists) {
811
+ if (!specialists[specialist]) {
812
+ throw new Error(`reviewer specialist ${specialist} is not present in the frozen team`);
813
+ }
814
+ if (specialist === plannerSpecialist) {
815
+ throw new Error("reviewer specialist cannot be the planner specialist");
816
+ }
817
+ }
818
+ const maxRevisionRounds = nonNegative(
819
+ input.maxRevisionRounds,
820
+ "replanning review max_revision_rounds",
821
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
822
+ );
823
+ const worstCaseAgents =
824
+ 1 + reviewerSpecialists.length * (maxRevisionRounds + 1) + maxRevisionRounds;
825
+ if (worstCaseAgents > limits.max_agents_per_work) {
826
+ throw new Error(
827
+ `replanning review worst-case ${worstCaseAgents} agents exceeds max_agents_per_work ${limits.max_agents_per_work}`,
828
+ );
829
+ }
830
+ return {
831
+ reviewer_specialists: reviewerSpecialists,
832
+ max_revision_rounds: maxRevisionRounds,
641
833
  };
642
834
  }
643
835
 
644
836
  function validateReplanning(
645
837
  policy: SupervisorReplanningPolicy,
646
838
  specialists: Record<string, WorkflowSpecialistProfile>,
839
+ limits: SupervisorLimits,
647
840
  goalId: string,
648
841
  ): void {
649
842
  if (!specialists[policy.planner_specialist]) {
@@ -676,6 +869,37 @@ function validateReplanning(
676
869
  rootCapable ||= template.root;
677
870
  }
678
871
  if (!rootCapable) throw new Error(`supervisor ${goalId} has no root-capable replanning template`);
872
+ if (policy.review !== undefined) {
873
+ const review = policy.review;
874
+ if (
875
+ !Array.isArray(review.reviewer_specialists) ||
876
+ review.reviewer_specialists.length < 1 ||
877
+ review.reviewer_specialists.length > MAX_SUPERVISOR_PLAN_REVIEWERS ||
878
+ !Number.isSafeInteger(review.max_revision_rounds) ||
879
+ review.max_revision_rounds < 0 ||
880
+ review.max_revision_rounds > MAX_SUPERVISOR_PLAN_REVISION_ROUNDS
881
+ ) {
882
+ throw new Error(`supervisor ${goalId} replanning review policy is invalid`);
883
+ }
884
+ if (new Set(review.reviewer_specialists).size !== review.reviewer_specialists.length) {
885
+ throw new Error(`supervisor ${goalId} replanning reviewers are not unique`);
886
+ }
887
+ for (const specialist of review.reviewer_specialists) {
888
+ if (!specialists[specialist]) {
889
+ throw new Error(`supervisor ${goalId} reviewer specialist is not in the frozen team`);
890
+ }
891
+ if (specialist === policy.planner_specialist) {
892
+ throw new Error(`supervisor ${goalId} reviewer specialist cannot be planner`);
893
+ }
894
+ }
895
+ const worstCaseAgents =
896
+ 1 +
897
+ review.reviewer_specialists.length * (review.max_revision_rounds + 1) +
898
+ review.max_revision_rounds;
899
+ if (worstCaseAgents > limits.max_agents_per_work) {
900
+ throw new Error(`supervisor ${goalId} review policy exceeds the agent budget`);
901
+ }
902
+ }
679
903
  }
680
904
 
681
905
  function collectGovernedWork(
@@ -685,8 +909,10 @@ function collectGovernedWork(
685
909
  active: WorkRecord[],
686
910
  ): WorkRecord[] {
687
911
  const governed = new Map<string, WorkRecord>();
688
- for (const record of collectSupervisorWork(coordRoot, originalRootWorkId)) {
689
- governed.set(record.intent.id, record);
912
+ if (workIntentExists(coordRoot, originalRootWorkId)) {
913
+ for (const record of collectSupervisorWork(coordRoot, originalRootWorkId)) {
914
+ governed.set(record.intent.id, record);
915
+ }
690
916
  }
691
917
  for (const workId of plans.applied_work_ids)
692
918
  governed.set(workId, readWorkItem(coordRoot, workId));
@@ -694,6 +920,14 @@ function collectGovernedWork(
694
920
  return [...governed.values()];
695
921
  }
696
922
 
923
+ function workIntentExists(coordRoot: string, workId: string): boolean {
924
+ return existsSync(join(resolve(coordRoot), ".harnery", "work", workId, "intent.json"));
925
+ }
926
+
927
+ function missionRootId(goalId: string): string {
928
+ return `${goalId.slice(0, 87)}-mission-root`;
929
+ }
930
+
697
931
  function supervisorDir(coordRoot: string, goalId: string): string {
698
932
  assertSupervisorId(goalId);
699
933
  return join(resolve(coordRoot), ".harnery", "supervisors", goalId);
@@ -767,6 +1001,13 @@ function positive(value: unknown, field: string, max: number): number {
767
1001
  return value as number;
768
1002
  }
769
1003
 
1004
+ function nonNegative(value: unknown, field: string, max: number): number {
1005
+ if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > max) {
1006
+ throw new Error(`${field} must be an integer between 0 and ${max}`);
1007
+ }
1008
+ return value as number;
1009
+ }
1010
+
770
1011
  function boolean(value: unknown, field: string): boolean {
771
1012
  if (typeof value !== "boolean") throw new Error(`supervisor ${field} must be boolean`);
772
1013
  return value;
@@ -58,6 +58,7 @@ import type {
58
58
  HarnessName,
59
59
  RunReport,
60
60
  SpawnResult,
61
+ StageSchema,
61
62
  WorkflowAgentProof,
62
63
  WorkflowContext,
63
64
  WorkflowEvidenceRecord,
@@ -423,6 +424,9 @@ async function executeWorkflow(
423
424
  const assignment = resolveSpecialistAssignment(specialists, prompt, requestedOpts);
424
425
  const agentOpts = assignment.opts;
425
426
  const assignmentPrompt = assignment.prompt;
427
+ const dispatchPrompt = agentOpts.schema
428
+ ? withSchemaContract(assignmentPrompt, agentOpts.schema)
429
+ : assignmentPrompt;
426
430
  let reservedForDispatch = 0;
427
431
  let spawnCountClaimed = false;
428
432
  const harness = agentOpts.harness ?? defaultHarness;
@@ -487,7 +491,7 @@ async function executeWorkflow(
487
491
  max_attempts: maxAttempts,
488
492
  max_turns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
489
493
  timeout_ms: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
490
- prompt_bytes: Buffer.byteLength(assignmentPrompt),
494
+ prompt_bytes: Buffer.byteLength(dispatchPrompt),
491
495
  isolation,
492
496
  network_access: networkAccess,
493
497
  current_cost_usd: round4(costUsd + reservedCostUsd),
@@ -609,7 +613,7 @@ async function executeWorkflow(
609
613
  });
610
614
  log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${harness}] ${label}`);
611
615
 
612
- let attemptPrompt = assignmentPrompt;
616
+ let attemptPrompt = dispatchPrompt;
613
617
  let last: SpawnResult | null = null;
614
618
  let agentCostUsd = 0;
615
619
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -678,12 +682,16 @@ async function executeWorkflow(
678
682
  }
679
683
 
680
684
  journal("agent.schema_retry", { id, attempt, problems });
681
- // Feed the validation failure back verbatim — the retry prompt carries
682
- // exactly what was wrong, which is what makes bounded retry converge.
685
+ // Keep the original cache identity while making the actual output
686
+ // contract explicit. A full bounded copy of the rejected value lets the
687
+ // child repair omissions instead of reconstructing an unseen object.
683
688
  attemptPrompt =
684
- `${assignmentPrompt}\n\nYour previous reply failed validation:\n` +
689
+ `${dispatchPrompt}\n\nYour previous reply failed validation. ` +
690
+ `Return a complete replacement value, not a patch.\n\n` +
691
+ `Previous reply (JSON string): ${JSON.stringify(boundedSchemaReply(last.text))}\n\n` +
692
+ `Validation problems:\n` +
685
693
  `${problems.map((p) => ` - ${p}`).join("\n")}\n` +
686
- `Reply with ONLY the corrected JSON object. No prose, no code fences.`;
694
+ `Reply with ONLY the corrected JSON value. No prose, no code fences.`;
687
695
  }
688
696
 
689
697
  const reason = last?.ok
@@ -892,6 +900,21 @@ async function executeWorkflow(
892
900
  }
893
901
  }
894
902
 
903
+ const MAX_SCHEMA_RETRY_REPLY = 16_000;
904
+
905
+ function withSchemaContract(prompt: string, schema: StageSchema): string {
906
+ return (
907
+ `${prompt}\n\nOutput contract:\n` +
908
+ `Return ONLY one JSON value matching this schema. Do not use prose or code fences.\n` +
909
+ `${JSON.stringify(schema)}`
910
+ );
911
+ }
912
+
913
+ function boundedSchemaReply(value: string): string {
914
+ if (value.length <= MAX_SCHEMA_RETRY_REPLY) return value;
915
+ return `${value.slice(0, MAX_SCHEMA_RETRY_REPLY)}\n[truncated by Harnery]`;
916
+ }
917
+
895
918
  /** Stable identity for one agent() call, for the resume cache. The ORIGINAL
896
919
  * prompt (never a retry-mutated one) plus everything that changes behavior. */
897
920
  function agentCallKey(
@@ -206,6 +206,12 @@ export interface StageSchema {
206
206
  required?: string[];
207
207
  /** type=array */
208
208
  items?: StageSchema;
209
+ minItems?: number;
210
+ maxItems?: number;
211
+ /** type=string */
212
+ minLength?: number;
213
+ maxLength?: number;
214
+ pattern?: string;
209
215
  /** any type: closed value set (compared with ===) */
210
216
  enum?: Array<string | number | boolean>;
211
217
  }