harnery 0.18.0 → 0.19.1

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 (54) 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.js +36 -1
  5. package/dist/core/supervisor/index.d.ts +2 -2
  6. package/dist/core/supervisor/index.d.ts.map +1 -1
  7. package/dist/core/supervisor/index.js +2 -2
  8. package/dist/core/supervisor/plan-read.d.ts +2 -1
  9. package/dist/core/supervisor/plan-read.d.ts.map +1 -1
  10. package/dist/core/supervisor/plan-read.js +175 -2
  11. package/dist/core/supervisor/plan-types.d.ts +48 -1
  12. package/dist/core/supervisor/plan-types.d.ts.map +1 -1
  13. package/dist/core/supervisor/plan-types.js +2 -0
  14. package/dist/core/supervisor/planning.d.ts.map +1 -1
  15. package/dist/core/supervisor/planning.js +762 -60
  16. package/dist/core/supervisor/read.d.ts +2 -2
  17. package/dist/core/supervisor/read.d.ts.map +1 -1
  18. package/dist/core/supervisor/read.js +2 -2
  19. package/dist/core/supervisor/state.d.ts.map +1 -1
  20. package/dist/core/supervisor/state.js +74 -6
  21. package/dist/core/workflow/engine.d.ts.map +1 -1
  22. package/dist/core/workflow/engine.js +24 -6
  23. package/dist/core/workflow/types.d.ts +9 -0
  24. package/dist/core/workflow/types.d.ts.map +1 -1
  25. package/dist/core/workflow/validate.d.ts +5 -3
  26. package/dist/core/workflow/validate.d.ts.map +1 -1
  27. package/dist/core/workflow/validate.js +53 -4
  28. package/dist/lib/browser/client.d.ts +18 -0
  29. package/dist/lib/browser/client.d.ts.map +1 -1
  30. package/dist/lib/browser/client.js +39 -0
  31. package/dist/lib/browser/geometry.d.ts +146 -0
  32. package/dist/lib/browser/geometry.d.ts.map +1 -0
  33. package/dist/lib/browser/geometry.js +525 -0
  34. package/dist/lib/browser/index.d.ts +2 -0
  35. package/dist/lib/browser/index.d.ts.map +1 -1
  36. package/dist/lib/browser/target-size.d.ts +65 -0
  37. package/dist/lib/browser/target-size.d.ts.map +1 -0
  38. package/dist/lib/browser/target-size.js +108 -0
  39. package/package.json +7 -1
  40. package/src/commands/browse.ts +319 -0
  41. package/src/commands/supervisor.ts +45 -1
  42. package/src/core/supervisor/index.ts +14 -1
  43. package/src/core/supervisor/plan-read.ts +234 -1
  44. package/src/core/supervisor/plan-types.ts +55 -0
  45. package/src/core/supervisor/planning.ts +954 -65
  46. package/src/core/supervisor/read.ts +14 -1
  47. package/src/core/supervisor/state.ts +96 -3
  48. package/src/core/workflow/engine.ts +29 -6
  49. package/src/core/workflow/types.ts +9 -0
  50. package/src/core/workflow/validate.ts +59 -4
  51. package/src/lib/browser/client.ts +62 -0
  52. package/src/lib/browser/geometry.ts +695 -0
  53. package/src/lib/browser/index.ts +25 -0
  54. package/src/lib/browser/target-size.ts +180 -0
@@ -617,6 +617,7 @@ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
617
617
  ];
618
618
  }),
619
619
  );
620
+ const review = readReplanningReview(config.review);
620
621
  return {
621
622
  plannerSpecialist: config.planner_specialist,
622
623
  autoApply: jsonBoolean(config.auto_apply, "replanning auto_apply"),
@@ -627,6 +628,29 @@ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
627
628
  ),
628
629
  maxTotalWorkItems: jsonInteger(config.max_total_work_items, "replanning max_total_work_items"),
629
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,
630
654
  };
631
655
  }
632
656
 
@@ -690,6 +714,11 @@ function emitSupervisor(
690
714
  lines.push(
691
715
  `replanning: generation=${projection.plan_generation}, used=${projection.replans_used}/${record.intent.replanning.max_replans}, auto_apply=${record.intent.replanning.auto_apply}`,
692
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
+ }
693
722
  if (projection.root_work_id !== record.intent.root_work_id) {
694
723
  lines.push(`original root: ${record.intent.root_work_id}`);
695
724
  }
@@ -747,6 +776,14 @@ function jsonInteger(value: unknown, field: string): number | undefined {
747
776
  return value as number;
748
777
  }
749
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
+
750
787
  function jsonBoolean(value: unknown, field: string): boolean | undefined {
751
788
  if (value === undefined) return undefined;
752
789
  if (typeof value !== "boolean") throw new Error(`${field} must be boolean`);
@@ -775,6 +812,12 @@ function emitPlan(plan: SupervisorPlanRecord, json: boolean | undefined, emit: E
775
812
  lines.push(`milestone: ${plan.proposal.milestone.sequence} ${plan.proposal.milestone.title}`);
776
813
  }
777
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}`);
820
+ }
778
821
  if (plan.root_work_id) lines.push(`applied root: ${plan.root_work_id}`);
779
822
  if (plan.approval_id) lines.push(`approval: ${plan.approval_id}`);
780
823
  emit.text(`${lines.join("\n")}\n`);
@@ -798,7 +841,8 @@ function emitPlanOutcome(
798
841
  }
799
842
 
800
843
  function renderPlanRow(plan: SupervisorPlanRecord): string {
801
- 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}`;
802
846
  }
803
847
 
804
848
  function delay(ms: number): Promise<void> {
@@ -1,8 +1,14 @@
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,
@@ -12,6 +18,13 @@ export {
12
18
  type SupervisorPlanProposal,
13
19
  type SupervisorPlanRecord,
14
20
  type SupervisorPlanRequest,
21
+ type SupervisorPlanReviewFinding,
22
+ type SupervisorPlanReviewReceipt,
23
+ type SupervisorPlanReviewReviewer,
24
+ type SupervisorPlanReviewRound,
25
+ type SupervisorPlanReviewStatus,
26
+ type SupervisorPlanReviewSummary,
27
+ type SupervisorPlanReviewVerdict,
15
28
  type SupervisorPlanStatus,
16
29
  type SupervisorPlanTemplate,
17
30
  type SupervisorPlanWorkSpec,
@@ -1,7 +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";
3
4
  import { readWorkItem } from "../work/read.ts";
4
5
  import {
6
+ MAX_SUPERVISOR_PLAN_REVIEWERS,
7
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
5
8
  SUPERVISOR_PLAN_SCHEMA_VERSION,
6
9
  type SupervisorPlanEvent,
7
10
  type SupervisorPlanEventType,
@@ -9,6 +12,13 @@ import {
9
12
  type SupervisorPlanProposal,
10
13
  type SupervisorPlanRecord,
11
14
  type SupervisorPlanRequest,
15
+ type SupervisorPlanReviewFinding,
16
+ type SupervisorPlanReviewReceipt,
17
+ type SupervisorPlanReviewReviewer,
18
+ type SupervisorPlanReviewRound,
19
+ type SupervisorPlanReviewSummary,
20
+ type SupervisorPlanReviewVerdict,
21
+ type SupervisorReplanningPolicy,
12
22
  } from "./plan-types.ts";
13
23
 
14
24
  const PLAN_ID = /^plan-[0-9]{4}-[a-f0-9]{8}$/;
@@ -24,6 +34,7 @@ const MAX_EVENTS = 100;
24
34
  const EVENT_TYPES = new Set<SupervisorPlanEventType>([
25
35
  "plan.awaiting_approval",
26
36
  "plan.resumed",
37
+ "plan.reviewed",
27
38
  "plan.proposed",
28
39
  "plan.applied",
29
40
  "plan.completed",
@@ -126,9 +137,33 @@ export function readSupervisorPlan(
126
137
  ? readJson<SupervisorPlanProposal>(proposalPath, "plan proposal")
127
138
  : undefined;
128
139
  if (proposal) validateProposalEnvelope(proposal, planId);
140
+ const review = readSupervisorPlanReviewReceipt(coordRootRaw, goalId, planId);
129
141
  const events = readEvents(join(dir, "events.jsonl"), planId);
130
142
  const derived = deriveStatus(coordRoot, events, proposal);
131
- 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;
132
167
  }
133
168
 
134
169
  function deriveStatus(
@@ -288,6 +323,204 @@ function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: stri
288
323
  }
289
324
  }
290
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
+
291
524
  function validateEvent(event: SupervisorPlanEvent, planId: string, sequence: number): void {
292
525
  if (
293
526
  event.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
@@ -1,4 +1,6 @@
1
1
  export const SUPERVISOR_PLAN_SCHEMA_VERSION = 1 as const;
2
+ export const MAX_SUPERVISOR_PLAN_REVIEWERS = 5 as const;
3
+ export const MAX_SUPERVISOR_PLAN_REVISION_ROUNDS = 3 as const;
2
4
 
3
5
  export interface SupervisorPlanTemplate {
4
6
  workflow: { path: string; sha256: string };
@@ -13,6 +15,10 @@ export interface SupervisorReplanningPolicy {
13
15
  max_work_items_per_plan: number;
14
16
  max_total_work_items: number;
15
17
  templates: Record<string, SupervisorPlanTemplate>;
18
+ review?: {
19
+ reviewer_specialists: string[];
20
+ max_revision_rounds: number;
21
+ };
16
22
  }
17
23
 
18
24
  export interface CreateSupervisorPlanTemplateInput {
@@ -28,6 +34,10 @@ export interface CreateSupervisorReplanningInput {
28
34
  maxWorkItemsPerPlan?: number;
29
35
  maxTotalWorkItems?: number;
30
36
  templates: Readonly<Record<string, CreateSupervisorPlanTemplateInput>>;
37
+ review?: {
38
+ reviewerSpecialists: readonly string[];
39
+ maxRevisionRounds: number;
40
+ };
31
41
  }
32
42
 
33
43
  export interface SupervisorPlanRequest {
@@ -72,6 +82,7 @@ export interface SupervisorPlanProposal {
72
82
  export type SupervisorPlanEventType =
73
83
  | "plan.awaiting_approval"
74
84
  | "plan.resumed"
85
+ | "plan.reviewed"
75
86
  | "plan.proposed"
76
87
  | "plan.applied"
77
88
  | "plan.completed"
@@ -79,6 +90,49 @@ export type SupervisorPlanEventType =
79
90
  | "plan.attention"
80
91
  | "plan.failed";
81
92
 
93
+ export type SupervisorPlanReviewStatus = "passed" | "revision_exhausted" | "attention" | "failed";
94
+
95
+ export type SupervisorPlanReviewVerdict = "approve" | "revise" | "attention";
96
+
97
+ export interface SupervisorPlanReviewFinding {
98
+ code: string;
99
+ severity: "blocking" | "advisory";
100
+ summary: string;
101
+ recommendation: string;
102
+ }
103
+
104
+ export interface SupervisorPlanReviewReviewer {
105
+ specialist: string;
106
+ verdict: SupervisorPlanReviewVerdict;
107
+ rationale: string;
108
+ findings: SupervisorPlanReviewFinding[];
109
+ }
110
+
111
+ export interface SupervisorPlanReviewRound {
112
+ round: number;
113
+ candidate_sha256: string;
114
+ reviewers: SupervisorPlanReviewReviewer[];
115
+ outcome: "approved" | "revise" | "attention" | "failed";
116
+ revision_workflow_run_id?: string;
117
+ }
118
+
119
+ export interface SupervisorPlanReviewReceipt {
120
+ schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
121
+ plan_id: string;
122
+ status: SupervisorPlanReviewStatus;
123
+ candidate_sha256: string;
124
+ final_candidate: SupervisorPlanProposal;
125
+ rounds: SupervisorPlanReviewRound[];
126
+ }
127
+
128
+ export interface SupervisorPlanReviewSummary {
129
+ status: SupervisorPlanReviewStatus;
130
+ candidate_sha256: string;
131
+ rounds: number;
132
+ blocking_findings: number;
133
+ advisory_findings: number;
134
+ }
135
+
82
136
  export interface SupervisorPlanEvent {
83
137
  schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
84
138
  plan_id: string;
@@ -106,6 +160,7 @@ export type SupervisorPlanStatus =
106
160
  export interface SupervisorPlanRecord {
107
161
  request: SupervisorPlanRequest;
108
162
  proposal?: SupervisorPlanProposal;
163
+ review?: SupervisorPlanReviewSummary;
109
164
  events: SupervisorPlanEvent[];
110
165
  status: SupervisorPlanStatus;
111
166
  approval_id?: string;