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
@@ -9,6 +9,7 @@ import type { PolicyIsolation } from "../core/policy/index.ts";
9
9
  import { loadPolicyFile } from "../core/policy/index.ts";
10
10
  import {
11
11
  approveSupervisorPlan,
12
+ type CreateSupervisorMissionInput,
12
13
  type CreateSupervisorReplanningInput,
13
14
  configureSupervisorService,
14
15
  createSupervisor,
@@ -46,6 +47,7 @@ interface CreateOpts {
46
47
  resumeApproved?: boolean;
47
48
  retryBlocked?: boolean;
48
49
  replanning?: string;
50
+ mission?: string;
49
51
  json?: boolean;
50
52
  }
51
53
 
@@ -75,8 +77,8 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
75
77
  .description("Run a bounded specialist team over a durable-work dependency graph.");
76
78
 
77
79
  supervisor
78
- .command("create <root-work-id>")
79
- .description("Freeze a goal, specialist team, automation policy, and execution limits.")
80
+ .command("create [root-work-id]")
81
+ .description("Freeze an existing-root goal or an objective-first bounded mission.")
80
82
  .requiredOption("--team <file>", "JSON object mapping specialist ids to profiles")
81
83
  .option("--id <goal-id>", "Stable goal id (generated by default)")
82
84
  .option("--title <text>", "Goal title (defaults to root work title)")
@@ -90,8 +92,9 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
90
92
  .option("--no-resume-approved", "Stop after an approval instead of resuming its parked run")
91
93
  .option("--retry-blocked", "Allow bounded retry of blocked work")
92
94
  .option("--replanning <file>", "Frozen planner policy and allowed workflow-template catalog")
95
+ .option("--mission <file>", "Frozen objective, acceptance criteria, and milestone bound")
93
96
  .option("--json", "Emit the complete supervisor record as JSON")
94
- .action((rootWorkId: string, opts: CreateOpts) => {
97
+ .action((rootWorkId: string | undefined, opts: CreateOpts) => {
95
98
  withSupervisorRoot(emit, (coordRoot) => {
96
99
  const specialists = readTeamFile(opts.team);
97
100
  for (const [id, profile] of Object.entries(specialists)) {
@@ -121,6 +124,7 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
121
124
  resume_approved: opts.resumeApproved,
122
125
  retry_blocked: opts.retryBlocked,
123
126
  },
127
+ mission: opts.mission ? readMissionFile(opts.mission) : undefined,
124
128
  replanning: opts.replanning ? readReplanningFile(opts.replanning) : undefined,
125
129
  }),
126
130
  opts.json,
@@ -613,6 +617,7 @@ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
613
617
  ];
614
618
  }),
615
619
  );
620
+ const review = readReplanningReview(config.review);
616
621
  return {
617
622
  plannerSpecialist: config.planner_specialist,
618
623
  autoApply: jsonBoolean(config.auto_apply, "replanning auto_apply"),
@@ -623,6 +628,57 @@ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
623
628
  ),
624
629
  maxTotalWorkItems: jsonInteger(config.max_total_work_items, "replanning max_total_work_items"),
625
630
  templates,
631
+ ...(review ? { review } : {}),
632
+ };
633
+ }
634
+
635
+ function readReplanningReview(raw: unknown): CreateSupervisorReplanningInput["review"] | undefined {
636
+ if (raw === undefined) return undefined;
637
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
638
+ throw new Error("supervisor replanning review must be a JSON object");
639
+ }
640
+ const review = raw as Record<string, unknown>;
641
+ if (
642
+ !Array.isArray(review.reviewer_specialists) ||
643
+ review.reviewer_specialists.some((item) => typeof item !== "string")
644
+ ) {
645
+ throw new Error(
646
+ "supervisor replanning review reviewer_specialists must be an array of strings",
647
+ );
648
+ }
649
+ return {
650
+ reviewerSpecialists: review.reviewer_specialists as string[],
651
+ maxRevisionRounds:
652
+ jsonNonNegativeInteger(review.max_revision_rounds, "replanning review max_revision_rounds") ??
653
+ 0,
654
+ };
655
+ }
656
+
657
+ function readMissionFile(path: string): CreateSupervisorMissionInput {
658
+ const absolute = resolve(path);
659
+ let value: unknown;
660
+ try {
661
+ value = JSON.parse(readFileSync(absolute, "utf8"));
662
+ } catch (error) {
663
+ throw new Error(`cannot read supervisor mission at ${absolute}: ${(error as Error).message}`);
664
+ }
665
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
666
+ throw new Error("supervisor mission must be a JSON object");
667
+ }
668
+ const config = value as Record<string, unknown>;
669
+ if (typeof config.objective !== "string") {
670
+ throw new Error("supervisor mission objective must be a string");
671
+ }
672
+ if (
673
+ !Array.isArray(config.acceptance) ||
674
+ config.acceptance.some((item) => typeof item !== "string")
675
+ ) {
676
+ throw new Error("supervisor mission acceptance must be an array of strings");
677
+ }
678
+ return {
679
+ objective: config.objective,
680
+ acceptance: config.acceptance as string[],
681
+ maxMilestones: jsonInteger(config.max_milestones, "mission max_milestones"),
626
682
  };
627
683
  }
628
684
 
@@ -645,7 +701,7 @@ function emitSupervisor(
645
701
  const lines = [
646
702
  `${record.intent.id}: ${record.intent.title}`,
647
703
  `state: ${projection.state}`,
648
- `root: ${projection.root_work_id}`,
704
+ `root: ${projection.root_materialized ? projection.root_work_id : "pending initial plan"}`,
649
705
  `reason: ${projection.reason}`,
650
706
  `next: ${projection.next_action}`,
651
707
  `work: ${projection.work_ids.length}`,
@@ -658,11 +714,23 @@ function emitSupervisor(
658
714
  lines.push(
659
715
  `replanning: generation=${projection.plan_generation}, used=${projection.replans_used}/${record.intent.replanning.max_replans}, auto_apply=${record.intent.replanning.auto_apply}`,
660
716
  );
717
+ if (record.intent.replanning.review) {
718
+ lines.push(
719
+ `plan review: reviewers=${record.intent.replanning.review.reviewer_specialists.join(", ")}, max_revision_rounds=${record.intent.replanning.review.max_revision_rounds}`,
720
+ );
721
+ }
661
722
  if (projection.root_work_id !== record.intent.root_work_id) {
662
723
  lines.push(`original root: ${record.intent.root_work_id}`);
663
724
  }
664
725
  if (projection.pending_plan_id) lines.push(`pending plan: ${projection.pending_plan_id}`);
665
726
  }
727
+ if (record.intent.mission) {
728
+ lines.push(`mission: ${record.intent.mission.objective}`);
729
+ lines.push(
730
+ `milestones: ${projection.milestones_completed}/${record.intent.mission.max_milestones}`,
731
+ );
732
+ lines.push(`mission acceptance: ${record.intent.mission.acceptance.length}`);
733
+ }
666
734
  if (projection.attention_work.length) {
667
735
  lines.push(`attention: ${projection.attention_work.join(", ")}`);
668
736
  }
@@ -708,6 +776,14 @@ function jsonInteger(value: unknown, field: string): number | undefined {
708
776
  return value as number;
709
777
  }
710
778
 
779
+ function jsonNonNegativeInteger(value: unknown, field: string): number | undefined {
780
+ if (value === undefined) return undefined;
781
+ if (!Number.isSafeInteger(value) || (value as number) < 0) {
782
+ throw new Error(`${field} must be a non-negative integer`);
783
+ }
784
+ return value as number;
785
+ }
786
+
711
787
  function jsonBoolean(value: unknown, field: string): boolean | undefined {
712
788
  if (value === undefined) return undefined;
713
789
  if (typeof value !== "boolean") throw new Error(`${field} must be boolean`);
@@ -724,6 +800,7 @@ function emitPlan(plan: SupervisorPlanRecord, json: boolean | undefined, emit: E
724
800
  `${plan.request.id}: ${plan.status}`,
725
801
  `goal: ${plan.request.goal_id}`,
726
802
  `sequence: ${plan.request.sequence}`,
803
+ `trigger: ${plan.request.trigger ?? "recovery"}`,
727
804
  `prior root: ${plan.request.prior_root_work_id}`,
728
805
  `planner run: ${plan.request.workflow_run_id}`,
729
806
  ];
@@ -731,6 +808,15 @@ function emitPlan(plan: SupervisorPlanRecord, json: boolean | undefined, emit: E
731
808
  lines.push(`decision: ${plan.proposal.decision}`);
732
809
  lines.push(`rationale: ${plan.proposal.rationale}`);
733
810
  lines.push(`proposed work: ${plan.proposal.work.length}`);
811
+ if (plan.proposal.milestone) {
812
+ lines.push(`milestone: ${plan.proposal.milestone.sequence} ${plan.proposal.milestone.title}`);
813
+ }
814
+ }
815
+ if (plan.review) {
816
+ lines.push(
817
+ `review: ${plan.review.status}, rounds=${plan.review.rounds}, blocking=${plan.review.blocking_findings}, advisory=${plan.review.advisory_findings}`,
818
+ );
819
+ lines.push(`review candidate: ${plan.review.candidate_sha256}`);
734
820
  }
735
821
  if (plan.root_work_id) lines.push(`applied root: ${plan.root_work_id}`);
736
822
  if (plan.approval_id) lines.push(`approval: ${plan.approval_id}`);
@@ -755,7 +841,8 @@ function emitPlanOutcome(
755
841
  }
756
842
 
757
843
  function renderPlanRow(plan: SupervisorPlanRecord): string {
758
- return `${plan.request.id} ${plan.status.padEnd(18)} ${plan.request.prior_root_work_id}`;
844
+ const review = plan.review ? ` review:${plan.review.status}` : "";
845
+ return `${plan.request.id} ${plan.status.padEnd(18)} ${plan.request.prior_root_work_id}${review}`;
759
846
  }
760
847
 
761
848
  function delay(ms: number): Promise<void> {
@@ -1,16 +1,30 @@
1
1
  export { approveSupervisorPlan, rejectSupervisorPlan } from "./plan-api.ts";
2
- export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.ts";
2
+ export {
3
+ readSupervisorPlan,
4
+ readSupervisorPlanReviewReceipt,
5
+ readSupervisorPlans,
6
+ } from "./plan-read.ts";
3
7
  export {
4
8
  type CreateSupervisorPlanTemplateInput,
5
9
  type CreateSupervisorReplanningInput,
10
+ MAX_SUPERVISOR_PLAN_REVIEWERS,
11
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
6
12
  SUPERVISOR_PLAN_SCHEMA_VERSION,
7
13
  type SupervisorPlanEvent,
8
14
  type SupervisorPlanEventType,
9
15
  type SupervisorPlanHistory,
16
+ type SupervisorPlanMilestone,
10
17
  type SupervisorPlanOutcome,
11
18
  type SupervisorPlanProposal,
12
19
  type SupervisorPlanRecord,
13
20
  type SupervisorPlanRequest,
21
+ type SupervisorPlanReviewFinding,
22
+ type SupervisorPlanReviewReceipt,
23
+ type SupervisorPlanReviewReviewer,
24
+ type SupervisorPlanReviewRound,
25
+ type SupervisorPlanReviewStatus,
26
+ type SupervisorPlanReviewSummary,
27
+ type SupervisorPlanReviewVerdict,
14
28
  type SupervisorPlanStatus,
15
29
  type SupervisorPlanTemplate,
16
30
  type SupervisorPlanWorkSpec,
@@ -53,6 +67,7 @@ export {
53
67
  export {
54
68
  assertSupervisorId,
55
69
  type CreateSupervisorInput,
70
+ type CreateSupervisorMissionInput,
56
71
  collectSupervisorWork,
57
72
  createSupervisor,
58
73
  listSupervisors,
@@ -62,6 +77,7 @@ export {
62
77
  type SupervisorAutomationPolicy,
63
78
  type SupervisorIntent,
64
79
  type SupervisorLimits,
80
+ type SupervisorMission,
65
81
  type SupervisorNextAction,
66
82
  type SupervisorProjection,
67
83
  type SupervisorRecord,
@@ -1,6 +1,10 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
3
  import { join, resolve } from "node:path";
4
+ import { readWorkItem } from "../work/read.ts";
3
5
  import {
6
+ MAX_SUPERVISOR_PLAN_REVIEWERS,
7
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
4
8
  SUPERVISOR_PLAN_SCHEMA_VERSION,
5
9
  type SupervisorPlanEvent,
6
10
  type SupervisorPlanEventType,
@@ -8,6 +12,13 @@ import {
8
12
  type SupervisorPlanProposal,
9
13
  type SupervisorPlanRecord,
10
14
  type SupervisorPlanRequest,
15
+ type SupervisorPlanReviewFinding,
16
+ type SupervisorPlanReviewReceipt,
17
+ type SupervisorPlanReviewReviewer,
18
+ type SupervisorPlanReviewRound,
19
+ type SupervisorPlanReviewSummary,
20
+ type SupervisorPlanReviewVerdict,
21
+ type SupervisorReplanningPolicy,
11
22
  } from "./plan-types.ts";
12
23
 
13
24
  const PLAN_ID = /^plan-[0-9]{4}-[a-f0-9]{8}$/;
@@ -23,8 +34,10 @@ const MAX_EVENTS = 100;
23
34
  const EVENT_TYPES = new Set<SupervisorPlanEventType>([
24
35
  "plan.awaiting_approval",
25
36
  "plan.resumed",
37
+ "plan.reviewed",
26
38
  "plan.proposed",
27
39
  "plan.applied",
40
+ "plan.completed",
28
41
  "plan.rejected",
29
42
  "plan.attention",
30
43
  "plan.failed",
@@ -46,6 +59,8 @@ export function readSupervisorPlans(
46
59
  generation: 0,
47
60
  applied_work_ids: [],
48
61
  materialized_work_ids: [],
62
+ milestones_completed: 0,
63
+ completed: false,
49
64
  };
50
65
  }
51
66
  const entries = readdirSync(root, { withFileTypes: true })
@@ -64,6 +79,8 @@ export function readSupervisorPlans(
64
79
  }
65
80
  let activeRoot = originalRootWorkId;
66
81
  let generation = 0;
82
+ let milestonesCompleted = 0;
83
+ let completed = false;
67
84
  const appliedWork = new Set<string>();
68
85
  const materializedWork = new Set<string>();
69
86
  for (const plan of plans) {
@@ -76,14 +93,30 @@ export function readSupervisorPlans(
76
93
  if (plan.status !== "applied" || !plan.root_work_id) continue;
77
94
  activeRoot = plan.root_work_id;
78
95
  generation++;
96
+ if (
97
+ plan.proposal?.milestone &&
98
+ readWorkItem(coordRoot, plan.root_work_id).projection.state === "succeeded"
99
+ ) {
100
+ milestonesCompleted++;
101
+ }
79
102
  for (const workId of plan.work_ids) appliedWork.add(workId);
80
103
  }
104
+ const completedPlans = plans.filter((plan) => plan.status === "completed");
105
+ if (
106
+ completedPlans.length > 1 ||
107
+ (completedPlans.length === 1 && completedPlans[0] !== plans.at(-1))
108
+ ) {
109
+ throw new Error(`supervisor ${goalId} has invalid completion history`);
110
+ }
111
+ completed = completedPlans.length === 1;
81
112
  return {
82
113
  plans,
83
114
  active_root_work_id: activeRoot,
84
115
  generation,
85
116
  applied_work_ids: [...appliedWork],
86
117
  materialized_work_ids: [...materializedWork],
118
+ milestones_completed: milestonesCompleted,
119
+ completed,
87
120
  latest: plans.at(-1),
88
121
  };
89
122
  }
@@ -104,9 +137,33 @@ export function readSupervisorPlan(
104
137
  ? readJson<SupervisorPlanProposal>(proposalPath, "plan proposal")
105
138
  : undefined;
106
139
  if (proposal) validateProposalEnvelope(proposal, planId);
140
+ const review = readSupervisorPlanReviewReceipt(coordRootRaw, goalId, planId);
107
141
  const events = readEvents(join(dir, "events.jsonl"), planId);
108
142
  const derived = deriveStatus(coordRoot, events, proposal);
109
- return { request, proposal, events, ...derived };
143
+ return {
144
+ request,
145
+ proposal,
146
+ review: review ? summarizeReview(review) : undefined,
147
+ events,
148
+ ...derived,
149
+ };
150
+ }
151
+
152
+ export function readSupervisorPlanReviewReceipt(
153
+ coordRootRaw: string,
154
+ goalId: string,
155
+ planId: string,
156
+ ): SupervisorPlanReviewReceipt | undefined {
157
+ assertId(goalId, GOAL_ID, "supervisor id");
158
+ assertId(planId, PLAN_ID, "supervisor plan id");
159
+ const coordRoot = resolve(coordRootRaw);
160
+ const path = join(planDir(coordRoot, goalId, planId), "review.json");
161
+ if (!existsSync(path)) return undefined;
162
+ const receipt = readJson<SupervisorPlanReviewReceipt>(path, "plan review receipt");
163
+ validateReviewReceipt(receipt, planId);
164
+ const reviewPolicy = readFrozenReviewPolicy(coordRoot, goalId);
165
+ if (reviewPolicy) validateReviewReceiptMatchesPolicy(receipt, planId, reviewPolicy);
166
+ return receipt;
110
167
  }
111
168
 
112
169
  function deriveStatus(
@@ -115,6 +172,10 @@ function deriveStatus(
115
172
  proposal: SupervisorPlanProposal | undefined,
116
173
  ): Pick<SupervisorPlanRecord, "status" | "approval_id" | "root_work_id" | "work_ids" | "reason"> {
117
174
  const latest = events.at(-1);
175
+ const completed = [...events].reverse().find((event) => event.event === "plan.completed");
176
+ if (completed) {
177
+ return { status: "completed", work_ids: [], reason: completed.reason };
178
+ }
118
179
  const applied = [...events].reverse().find((event) => event.event === "plan.applied");
119
180
  if (applied) {
120
181
  return {
@@ -180,6 +241,8 @@ function validateRequest(request: SupervisorPlanRequest, goalId: string, planId:
180
241
  !Number.isSafeInteger(request.sequence) ||
181
242
  request.sequence < 1 ||
182
243
  request.sequence > MAX_PLANS ||
244
+ (request.trigger !== undefined &&
245
+ !["initial", "recovery", "milestone"].includes(request.trigger)) ||
183
246
  typeof request.trigger_fingerprint !== "string" ||
184
247
  request.trigger_fingerprint.length < 1 ||
185
248
  request.trigger_fingerprint.length > 64_000 ||
@@ -195,7 +258,7 @@ function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: stri
195
258
  if (
196
259
  proposal.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
197
260
  proposal.plan_id !== planId ||
198
- !["apply", "attention"].includes(proposal.decision) ||
261
+ !["apply", "complete", "attention"].includes(proposal.decision) ||
199
262
  typeof proposal.rationale !== "string" ||
200
263
  proposal.rationale.length < 1 ||
201
264
  proposal.rationale.length > 2_000 ||
@@ -207,6 +270,35 @@ function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: stri
207
270
  ) {
208
271
  throw new Error(`supervisor plan proposal ${planId} has an unsupported schema`);
209
272
  }
273
+ if (proposal.milestone !== undefined) {
274
+ const milestone = proposal.milestone;
275
+ if (
276
+ !Number.isSafeInteger(milestone.sequence) ||
277
+ milestone.sequence < 1 ||
278
+ milestone.sequence > 20 ||
279
+ typeof milestone.title !== "string" ||
280
+ milestone.title.length < 1 ||
281
+ milestone.title.length > 200 ||
282
+ typeof milestone.objective !== "string" ||
283
+ milestone.objective.length < 1 ||
284
+ milestone.objective.length > 4_000 ||
285
+ !Array.isArray(milestone.acceptance) ||
286
+ milestone.acceptance.length < 1 ||
287
+ milestone.acceptance.length > 50 ||
288
+ milestone.acceptance.some(
289
+ (criterion) =>
290
+ typeof criterion !== "string" || criterion.length < 1 || criterion.length > 500,
291
+ )
292
+ ) {
293
+ throw new Error(`supervisor plan proposal ${planId} milestone is invalid`);
294
+ }
295
+ }
296
+ if (
297
+ (proposal.decision === "attention" || proposal.decision === "complete") &&
298
+ (proposal.root !== "" || proposal.work.length > 0 || proposal.milestone !== undefined)
299
+ ) {
300
+ throw new Error(`supervisor plan proposal ${planId} terminal decision contains work`);
301
+ }
210
302
  for (const [index, spec] of proposal.work.entries()) {
211
303
  if (
212
304
  !spec ||
@@ -231,6 +323,204 @@ function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: stri
231
323
  }
232
324
  }
233
325
 
326
+ function validateReviewReceipt(receipt: SupervisorPlanReviewReceipt, planId: string): void {
327
+ if (
328
+ !receipt ||
329
+ typeof receipt !== "object" ||
330
+ receipt.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
331
+ receipt.plan_id !== planId ||
332
+ !["passed", "revision_exhausted", "attention", "failed"].includes(receipt.status) ||
333
+ !/^[a-f0-9]{64}$/.test(receipt.candidate_sha256) ||
334
+ !Array.isArray(receipt.rounds) ||
335
+ receipt.rounds.length < 1 ||
336
+ receipt.rounds.length > MAX_SUPERVISOR_PLAN_REVISION_ROUNDS + 1
337
+ ) {
338
+ throw new Error(`supervisor plan review ${planId} has an unsupported schema`);
339
+ }
340
+ validateProposalEnvelope(receipt.final_candidate, planId);
341
+ if (receipt.candidate_sha256 !== candidateDigest(receipt.final_candidate)) {
342
+ throw new Error(`supervisor plan review ${planId} has invalid candidate digest`);
343
+ }
344
+ receipt.rounds.forEach((round, index) => {
345
+ validateReviewRound(round, planId, index + 1);
346
+ });
347
+ const finalRound = receipt.rounds.at(-1);
348
+ if (finalRound?.candidate_sha256 !== receipt.candidate_sha256) {
349
+ throw new Error(`supervisor plan review ${planId} final round does not match its candidate`);
350
+ }
351
+ if (receipt.status === "passed" && finalRound?.outcome !== "approved") {
352
+ throw new Error(`supervisor plan review ${planId} passed without approval`);
353
+ }
354
+ if (receipt.status === "attention" && finalRound?.outcome === "approved") {
355
+ throw new Error(`supervisor plan review ${planId} attention conflicts with approval`);
356
+ }
357
+ }
358
+
359
+ function readFrozenReviewPolicy(
360
+ coordRoot: string,
361
+ goalId: string,
362
+ ): SupervisorReplanningPolicy["review"] | undefined {
363
+ const path = join(coordRoot, ".harnery", "supervisors", goalId, "intent.json");
364
+ if (!existsSync(path)) return undefined;
365
+ const intent = readJson<{ replanning?: SupervisorReplanningPolicy }>(path, "supervisor intent");
366
+ return intent.replanning?.review;
367
+ }
368
+
369
+ function validateReviewReceiptMatchesPolicy(
370
+ receipt: SupervisorPlanReviewReceipt,
371
+ planId: string,
372
+ review: NonNullable<SupervisorReplanningPolicy["review"]>,
373
+ ): void {
374
+ if (receipt.rounds.length > review.max_revision_rounds + 1) {
375
+ throw new Error(`supervisor plan review ${planId} exceeds the frozen review policy`);
376
+ }
377
+ for (const round of receipt.rounds) {
378
+ if (round.reviewers.length !== review.reviewer_specialists.length) {
379
+ throw new Error(`supervisor plan review ${planId} does not match the frozen review policy`);
380
+ }
381
+ round.reviewers.forEach((reviewer, index) => {
382
+ if (reviewer.specialist !== review.reviewer_specialists[index]) {
383
+ throw new Error(`supervisor plan review ${planId} does not match the frozen review policy`);
384
+ }
385
+ });
386
+ const outcome = aggregateReviewers(round.reviewers);
387
+ if (round.outcome !== outcome) {
388
+ throw new Error(`supervisor plan review ${planId} has an invalid reviewer outcome`);
389
+ }
390
+ }
391
+ const finalRound = receipt.rounds.at(-1)!;
392
+ if (receipt.status === "passed" && finalRound.outcome !== "approved") {
393
+ throw new Error(`supervisor plan review ${planId} passed without policy approval`);
394
+ }
395
+ if (receipt.status === "revision_exhausted" && finalRound.round <= review.max_revision_rounds) {
396
+ throw new Error(`supervisor plan review ${planId} exhausted before the frozen review limit`);
397
+ }
398
+ }
399
+
400
+ function aggregateReviewers(
401
+ reviewers: readonly SupervisorPlanReviewReviewer[],
402
+ ): SupervisorPlanReviewRound["outcome"] {
403
+ if (reviewers.length < 1) return "failed";
404
+ if (reviewers.some((reviewer) => reviewer.verdict === "attention")) return "attention";
405
+ if (
406
+ reviewers.some(
407
+ (reviewer) =>
408
+ reviewer.verdict === "revise" ||
409
+ reviewer.findings.some((finding) => finding.severity === "blocking"),
410
+ )
411
+ ) {
412
+ return "revise";
413
+ }
414
+ return "approved";
415
+ }
416
+
417
+ function candidateDigest(candidate: SupervisorPlanProposal): string {
418
+ return createHash("sha256")
419
+ .update(JSON.stringify(canonicalCandidate(candidate)))
420
+ .digest("hex");
421
+ }
422
+
423
+ function canonicalCandidate(candidate: SupervisorPlanProposal): unknown {
424
+ return {
425
+ schema_version: candidate.schema_version,
426
+ plan_id: candidate.plan_id,
427
+ decision: candidate.decision,
428
+ rationale: candidate.rationale,
429
+ root: candidate.root,
430
+ work: candidate.work,
431
+ milestone: candidate.milestone,
432
+ };
433
+ }
434
+
435
+ function validateReviewRound(
436
+ round: SupervisorPlanReviewRound,
437
+ planId: string,
438
+ sequence: number,
439
+ ): void {
440
+ if (
441
+ !round ||
442
+ typeof round !== "object" ||
443
+ round.round !== sequence ||
444
+ !/^[a-f0-9]{64}$/.test(round.candidate_sha256) ||
445
+ !Array.isArray(round.reviewers) ||
446
+ round.reviewers.length < 1 ||
447
+ round.reviewers.length > MAX_SUPERVISOR_PLAN_REVIEWERS ||
448
+ !["approved", "revise", "attention", "failed"].includes(round.outcome) ||
449
+ (round.revision_workflow_run_id !== undefined &&
450
+ !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(round.revision_workflow_run_id))
451
+ ) {
452
+ throw new Error(`supervisor plan review ${planId} round ${sequence} is invalid`);
453
+ }
454
+ round.reviewers.forEach((reviewer, index) => {
455
+ validateReviewReviewer(reviewer, planId, sequence, index);
456
+ });
457
+ }
458
+
459
+ function validateReviewReviewer(
460
+ reviewer: SupervisorPlanReviewReviewer,
461
+ planId: string,
462
+ round: number,
463
+ index: number,
464
+ ): void {
465
+ if (
466
+ !reviewer ||
467
+ typeof reviewer !== "object" ||
468
+ typeof reviewer.specialist !== "string" ||
469
+ reviewer.specialist.length < 1 ||
470
+ reviewer.specialist.length > 100 ||
471
+ !["approve", "revise", "attention"].includes(reviewer.verdict as SupervisorPlanReviewVerdict) ||
472
+ typeof reviewer.rationale !== "string" ||
473
+ reviewer.rationale.length < 1 ||
474
+ reviewer.rationale.length > 2_000 ||
475
+ !Array.isArray(reviewer.findings) ||
476
+ reviewer.findings.length > 50
477
+ ) {
478
+ throw new Error(`supervisor plan review ${planId} round ${round} reviewer ${index} is invalid`);
479
+ }
480
+ reviewer.findings.forEach((finding, findingIndex) => {
481
+ validateReviewFinding(finding, planId, round, index, findingIndex);
482
+ });
483
+ }
484
+
485
+ function validateReviewFinding(
486
+ finding: SupervisorPlanReviewFinding,
487
+ planId: string,
488
+ round: number,
489
+ reviewerIndex: number,
490
+ findingIndex: number,
491
+ ): void {
492
+ if (
493
+ !finding ||
494
+ typeof finding !== "object" ||
495
+ typeof finding.code !== "string" ||
496
+ !/^[a-z][a-z0-9._-]{0,99}$/.test(finding.code) ||
497
+ !["blocking", "advisory"].includes(finding.severity) ||
498
+ typeof finding.summary !== "string" ||
499
+ finding.summary.length < 1 ||
500
+ finding.summary.length > 1_000 ||
501
+ typeof finding.recommendation !== "string" ||
502
+ finding.recommendation.length < 1 ||
503
+ finding.recommendation.length > 1_000
504
+ ) {
505
+ throw new Error(
506
+ `supervisor plan review ${planId} round ${round} reviewer ${reviewerIndex} finding ${findingIndex} is invalid`,
507
+ );
508
+ }
509
+ }
510
+
511
+ function summarizeReview(receipt: SupervisorPlanReviewReceipt): SupervisorPlanReviewSummary {
512
+ const findings = receipt.rounds.flatMap((round) =>
513
+ round.reviewers.flatMap((reviewer) => reviewer.findings),
514
+ );
515
+ return {
516
+ status: receipt.status,
517
+ candidate_sha256: receipt.candidate_sha256,
518
+ rounds: receipt.rounds.length,
519
+ blocking_findings: findings.filter((finding) => finding.severity === "blocking").length,
520
+ advisory_findings: findings.filter((finding) => finding.severity === "advisory").length,
521
+ };
522
+ }
523
+
234
524
  function validateEvent(event: SupervisorPlanEvent, planId: string, sequence: number): void {
235
525
  if (
236
526
  event.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||