harnery 0.18.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 (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 +705 -50
  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 +6 -0
  24. package/dist/core/workflow/types.d.ts.map +1 -1
  25. package/dist/core/workflow/validate.d.ts +3 -2
  26. package/dist/core/workflow/validate.d.ts.map +1 -1
  27. package/dist/core/workflow/validate.js +31 -3
  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 +896 -57
  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 +6 -0
  50. package/src/core/workflow/validate.ts +34 -3
  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
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
  - **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
24
24
  - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with `ALLOW`, `ASK`, and `DENY`; CLI `ASK` requests park durably for an explicit approve/deny and same-run resume, while library callers remain fail-closed by default.
25
25
  - **Durable work above attempts:** `harn work` preserves an objective across bounded workflow attempts, dependencies, approval parks, failures, and review. Its one-shot reconciler derives state from append-only local evidence and reports the next explicit action without silently spawning, retrying, or accepting work.
26
- - **Bounded goal supervision:** `harn supervisor` freezes a mission or existing root graph, specialist team, and safety limits, then runs scheduling cycles until reviewed mission success, human attention, no progress, or budget exhaustion. Its optional planner can create the first milestone, reassess accepted milestone proof, or recover a terminal graph using only a frozen workflow-template catalog. Proposals require review by default, old generations remain immutable evidence, and all attempts still consume the goal budget. Use explicit foreground ticks or enroll goals in the optional heartbeat-backed background service.
26
+ - **Bounded goal supervision:** `harn supervisor` freezes a mission or existing root graph, specialist team, and safety limits, then runs scheduling cycles until reviewed mission success, human attention, no progress, or budget exhaustion. Its optional planner can create the first milestone, reassess accepted milestone proof, or recover a terminal graph using only a frozen workflow-template catalog. Plans can require independent bounded specialist review before ordinary approval or auto-apply authority, old generations remain immutable evidence, and all attempts still consume the goal budget. Use explicit foreground ticks or enroll goals in the optional heartbeat-backed background service.
27
27
  - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
28
28
  - **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
29
29
 
@@ -1 +1 @@
1
- {"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAmH1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAoLN"}
1
+ {"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAgJ1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA6PN"}
@@ -105,6 +105,32 @@ export function registerBrowseCommand(program, emitParam, context) {
105
105
  .option("--check-runts-min-chars <n>", "Minimum block text length to scan (default 40; smaller labels can't meaningfully wrap).", "40")
106
106
  .option("--check-runts-fail", "Exit non-zero if any runt is detected.")
107
107
  .option("--no-check-runts-annotate", "Skip drawing runt boxes on the screenshot (JSON still emitted).")
108
+ .option("--check-align <selector>", "Check rendered child-content alignment inside this container (repeatable).", (value, previous = []) => [...previous, value], [])
109
+ .option("--check-align-axis <axis>", "Alignment axis: auto | x | y (default auto).", "auto")
110
+ .option("--check-align-threshold <px>", "Maximum rendered-center drift in CSS pixels (default 2).", "2")
111
+ .option("--check-align-fail", "Exit 2 when an alignment target fails or is missing.")
112
+ .option("--no-check-align-annotate", "Skip alignment screenshot annotations.")
113
+ .option("--check-gap <selector>", "Check adjacent rendered gaps inside this container (repeatable).", (value, previous = []) => [...previous, value], [])
114
+ .option("--check-gap-axis <axis>", "Gap axis: auto | x | y (default auto).", "auto")
115
+ .option("--check-gap-expected <px>", "Expected gap in CSS pixels. Without it, groups of 3+ infer the median gap.")
116
+ .option("--check-gap-threshold <px>", "Maximum gap deviation in CSS pixels (default 2).", "2")
117
+ .option("--check-gap-fail", "Exit 2 when a gap target fails or is missing.")
118
+ .option("--no-check-gap-annotate", "Skip gap screenshot annotations.")
119
+ .option("--check-clip <selector>", "Check descendants against this container's rectangular clipping chain (repeatable).", (value, previous = []) => [...previous, value], [])
120
+ .option("--check-clip-threshold <px>", "Allowed rectangular overrun in CSS pixels (default 0).", "0")
121
+ .option("--check-clip-fail", "Exit 2 when a clip target fails or is missing.")
122
+ .option("--no-check-clip-annotate", "Skip clip screenshot annotations.")
123
+ .option("--check-overlap <selector>", "Check measurable in-flow siblings for unintended overlap (repeatable).", (value, previous = []) => [...previous, value], [])
124
+ .option("--check-overlap-threshold <px>", "Allowed overlap depth on either axis in CSS pixels (default 0).", "0")
125
+ .option("--check-overlap-fail", "Exit 2 when an overlap target fails or is missing.")
126
+ .option("--no-check-overlap-annotate", "Skip overlap screenshot annotations.")
127
+ .option("--check-hit [selector]", "Check pointer-target size and spacing in the document or optional scope (repeatable).", (value, previous = []) => [
128
+ ...previous,
129
+ typeof value === "string" ? value : null,
130
+ ], [])
131
+ .option("--check-hit-profile <profile>", "Target profile: wcag-aa (24px) | comfortable (44px).", "wcag-aa")
132
+ .option("--check-hit-fail", "Exit 2 when target sizing fails, is incomplete, or a scope is missing.")
133
+ .option("--no-check-hit-annotate", "Skip target-size screenshot annotations.")
108
134
  .option("--baseline <name>", "Save the captured screenshot as a named baseline at " +
109
135
  "~/.cache/harnery/visual-baselines/<name>.png. Use --diff <name> later to " +
110
136
  "compare future captures against it (visual-regression check).")
@@ -127,6 +153,16 @@ export function registerBrowseCommand(program, emitParam, context) {
127
153
  });
128
154
  }
129
155
  async function runBrowse(url, opts, context) {
156
+ const alignAxis = parseLayoutAxis(opts.checkAlignAxis ?? "auto", "--check-align-axis");
157
+ const gapAxis = parseLayoutAxis(opts.checkGapAxis ?? "auto", "--check-gap-axis");
158
+ const alignThreshold = parseNonNegativeNumber(opts.checkAlignThreshold ?? "2", "--check-align-threshold");
159
+ const gapThreshold = parseNonNegativeNumber(opts.checkGapThreshold ?? "2", "--check-gap-threshold");
160
+ const gapExpected = opts.checkGapExpected === undefined
161
+ ? null
162
+ : parseNonNegativeNumber(opts.checkGapExpected, "--check-gap-expected");
163
+ const clipThreshold = parseNonNegativeNumber(opts.checkClipThreshold ?? "0", "--check-clip-threshold");
164
+ const overlapThreshold = parseNonNegativeNumber(opts.checkOverlapThreshold ?? "0", "--check-overlap-threshold");
165
+ const hitProfile = parseTargetSizeProfile(opts.checkHitProfile ?? "wcag-aa");
130
166
  // Commander: `--no-cookies` turns `opts.cookies` into `false`. Default is `true`.
131
167
  const jar = opts.cookies === false
132
168
  ? null
@@ -198,6 +234,38 @@ async function runBrowse(url, opts, context) {
198
234
  minChars: Number.parseInt(opts.checkRuntsMinChars ?? "40", 10),
199
235
  });
200
236
  }
237
+ const hasLayoutLint = (opts.checkAlign?.length ?? 0) > 0 ||
238
+ (opts.checkGap?.length ?? 0) > 0 ||
239
+ (opts.checkClip?.length ?? 0) > 0 ||
240
+ (opts.checkOverlap?.length ?? 0) > 0;
241
+ let layoutLint;
242
+ if (hasLayoutLint) {
243
+ layoutLint = await browser.checkLayoutLint({
244
+ align: (opts.checkAlign ?? []).map((selector) => ({
245
+ selector,
246
+ axis: alignAxis,
247
+ tolerancePx: alignThreshold,
248
+ })),
249
+ gap: (opts.checkGap ?? []).map((selector) => ({
250
+ selector,
251
+ axis: gapAxis,
252
+ tolerancePx: gapThreshold,
253
+ expectedGapPx: gapExpected,
254
+ })),
255
+ clip: (opts.checkClip ?? []).map((selector) => ({
256
+ selector,
257
+ tolerancePx: clipThreshold,
258
+ })),
259
+ overlap: (opts.checkOverlap ?? []).map((selector) => ({
260
+ selector,
261
+ tolerancePx: overlapThreshold,
262
+ })),
263
+ });
264
+ }
265
+ let hit;
266
+ if (opts.checkHit && opts.checkHit.length > 0) {
267
+ hit = await browser.checkTargetSize(opts.checkHit, hitProfile);
268
+ }
201
269
  const widthThreshold = Number.parseFloat(opts.checkWidthThreshold ?? "0.9");
202
270
  const annotateWidth = widths && opts.checkWidthAnnotate !== false;
203
271
  const annotateOverflow = overflow && opts.checkOverflowAnnotate !== false;
@@ -212,6 +280,25 @@ async function runBrowse(url, opts, context) {
212
280
  if (annotateRunts) {
213
281
  await browser.annotateRunts(runts);
214
282
  }
283
+ const annotateLayoutLint = layoutLint
284
+ ? {
285
+ align: opts.checkAlignAnnotate === false ? [] : layoutLint.align,
286
+ gap: opts.checkGapAnnotate === false ? [] : layoutLint.gap,
287
+ clip: opts.checkClipAnnotate === false ? [] : layoutLint.clip,
288
+ overlap: opts.checkOverlapAnnotate === false ? [] : layoutLint.overlap,
289
+ }
290
+ : undefined;
291
+ if (annotateLayoutLint &&
292
+ (annotateLayoutLint.align.length > 0 ||
293
+ annotateLayoutLint.gap.length > 0 ||
294
+ annotateLayoutLint.clip.length > 0 ||
295
+ annotateLayoutLint.overlap.length > 0)) {
296
+ await browser.annotateLayoutLint(annotateLayoutLint);
297
+ }
298
+ const annotateHit = hit && opts.checkHitAnnotate !== false;
299
+ if (annotateHit) {
300
+ await browser.annotateTargetSize(hit);
301
+ }
215
302
  if (opts.login) {
216
303
  await new Promise((res) => {
217
304
  emit.log("[--login] Headed Chromium is open. Walk through your auth flow now. Press Enter here to close + persist cookies into the profile.", "info");
@@ -228,10 +315,10 @@ async function runBrowse(url, opts, context) {
228
315
  if (opts.baseline || opts.diff) {
229
316
  throw new Error("--baseline / --diff require trio mode (screenshot file). Remove --snapshot/--html/--json or capture the screenshot first.");
230
317
  }
231
- await runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, devOverlay, batchResult);
318
+ await runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, devOverlay, batchResult);
232
319
  }
233
320
  else {
234
- await runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, devOverlay, batchResult);
321
+ await runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, devOverlay, batchResult);
235
322
  }
236
323
  if (visibility && opts.checkVisibleAnnotate !== false) {
237
324
  await browser.clearVisibilityAnnotations();
@@ -242,6 +329,12 @@ async function runBrowse(url, opts, context) {
242
329
  if (annotateRunts) {
243
330
  await browser.clearRuntsAnnotations();
244
331
  }
332
+ if (annotateLayoutLint) {
333
+ await browser.clearLayoutLintAnnotations();
334
+ }
335
+ if (annotateHit) {
336
+ await browser.clearTargetSizeAnnotations();
337
+ }
245
338
  if (opts.checkVisibleFail && visibility) {
246
339
  const threshold = Number.parseFloat(opts.checkVisibleThreshold ?? "0.9");
247
340
  const failed = visibility.filter((r) => !r.found || !r.cssVisible || r.visibleRatio < threshold);
@@ -291,6 +384,7 @@ async function runBrowse(url, opts, context) {
291
384
  emit.log(`check-overflow FAIL: documentScrollWidth ${overflow.documentScrollWidth}px > viewport ${overflow.viewport.width}px (+${overflow.overflowPx}px)${detail}`, "warn");
292
385
  process.exitCode = 2;
293
386
  }
387
+ applyLayoutLintFailGates(opts, layoutLint, hit);
294
388
  }
295
389
  finally {
296
390
  await browser.close();
@@ -390,7 +484,7 @@ function splitBatchSteps(input) {
390
484
  // ---------------------------------------------------------------------------
391
485
  // Print mode (--snapshot / --html / --json): stdout, no file writes
392
486
  // ---------------------------------------------------------------------------
393
- async function runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, devOverlay, batchResult) {
487
+ async function runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, devOverlay, batchResult) {
394
488
  let body = null;
395
489
  if (opts.html) {
396
490
  body = await browser.htmlContent(opts.selector);
@@ -419,6 +513,14 @@ async function runPrintMode(browser, navResult, opts, evalResult, visibility, wi
419
513
  result.overflow = overflow;
420
514
  if (runts)
421
515
  result.runts = runts;
516
+ if (layoutLint) {
517
+ result.align = layoutLint.align;
518
+ result.gap = layoutLint.gap;
519
+ result.clip = layoutLint.clip;
520
+ result.overlap = layoutLint.overlap;
521
+ }
522
+ if (hit)
523
+ result.hit = hit;
422
524
  if (devOverlay)
423
525
  result.devOverlay = devOverlay;
424
526
  if (batchResult && batchResult.clipboardReads.length > 0) {
@@ -442,7 +544,7 @@ async function runPrintMode(browser, navResult, opts, evalResult, visibility, wi
442
544
  // ---------------------------------------------------------------------------
443
545
  // Trio mode (default): write <prefix>.{png,html,json}
444
546
  // ---------------------------------------------------------------------------
445
- async function runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, devOverlay, batchResult) {
547
+ async function runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, devOverlay, batchResult) {
446
548
  const prefix = resolveOutPrefix(opts.out);
447
549
  mkdirSync(dirname(prefix), { recursive: true });
448
550
  const written = [];
@@ -485,6 +587,14 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
485
587
  envelope.overflow = overflow;
486
588
  if (runts)
487
589
  envelope.runts = runts;
590
+ if (layoutLint) {
591
+ envelope.align = layoutLint.align;
592
+ envelope.gap = layoutLint.gap;
593
+ envelope.clip = layoutLint.clip;
594
+ envelope.overlap = layoutLint.overlap;
595
+ }
596
+ if (hit)
597
+ envelope.hit = hit;
488
598
  if (devOverlay)
489
599
  envelope.devOverlay = devOverlay;
490
600
  if (batchResult && batchResult.clipboardReads.length > 0) {
@@ -572,6 +682,17 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
572
682
  : ""}`;
573
683
  emit.log(`check-overflow: [${ok ? "OK" : "FAIL"}] ${detail}`, ok ? "info" : "warn");
574
684
  }
685
+ if (layoutLint) {
686
+ logLayoutLintSummary(layoutLint);
687
+ }
688
+ if (hit) {
689
+ const lines = hit.map((result) => {
690
+ const failed = result.nodes.filter((node) => node.outcome === "fail").length;
691
+ const unknown = result.nodes.filter((node) => node.outcome === "unknown").length;
692
+ return ` [${result.outcome.toUpperCase()}] ${result.selector ?? "document"}: ${failed} failed, ${unknown} unknown, ${result.nodes.length} evaluated (${result.minSizePx}px)`;
693
+ });
694
+ emit.log(`check-hit:\n${lines.join("\n")}`, "info");
695
+ }
575
696
  if (savedBaseline) {
576
697
  emit.log(`baseline saved: ${savedBaseline.name} → ${savedBaseline.path} (${savedBaseline.bytes} bytes)`, "info");
577
698
  }
@@ -593,6 +714,68 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
593
714
  // ---------------------------------------------------------------------------
594
715
  // Helpers
595
716
  // ---------------------------------------------------------------------------
717
+ function parseNonNegativeNumber(value, flag) {
718
+ const parsed = Number(value);
719
+ if (!Number.isFinite(parsed) || parsed < 0) {
720
+ throw new Error(`${flag} must be a finite non-negative number (got: ${value})`);
721
+ }
722
+ return parsed;
723
+ }
724
+ function parseLayoutAxis(value, flag) {
725
+ if (value === "auto" || value === "x" || value === "y")
726
+ return value;
727
+ throw new Error(`${flag} must be auto, x, or y (got: ${value})`);
728
+ }
729
+ function parseTargetSizeProfile(value) {
730
+ if (value === "wcag-aa" || value === "comfortable")
731
+ return value;
732
+ throw new Error(`--check-hit-profile must be wcag-aa or comfortable (got: ${value})`);
733
+ }
734
+ function logLayoutLintSummary(result) {
735
+ const lines = [];
736
+ for (const check of result.align) {
737
+ const worst = Math.max(0, ...check.clusters.flatMap((cluster) => cluster.children.map((child) => Math.abs(child.deltaPx))));
738
+ lines.push(` [${check.outcome.toUpperCase()}] align ${check.selector}: worst ${worst.toFixed(1)}px (${check.axis})`);
739
+ }
740
+ for (const check of result.gap) {
741
+ const worst = Math.max(0, ...check.clusters.flatMap((cluster) => cluster.pairs.map((pair) => Math.abs(pair.deltaPx ?? 0))));
742
+ lines.push(` [${check.outcome.toUpperCase()}] gap ${check.selector}: worst ${worst.toFixed(1)}px (${check.axis})`);
743
+ }
744
+ for (const check of result.clip) {
745
+ const worst = Math.max(0, ...check.issues.map((issue) => issue.maxOverrunPx));
746
+ lines.push(` [${check.outcome.toUpperCase()}] clip ${check.selector}: ${check.issues.length} issues, worst ${worst.toFixed(1)}px, ${check.unsupported.length} unknown shapes`);
747
+ }
748
+ for (const check of result.overlap) {
749
+ const worst = Math.max(0, ...check.issues.map((issue) => issue.areaPx));
750
+ lines.push(` [${check.outcome.toUpperCase()}] overlap ${check.selector}: ${check.issues.length} collisions, worst ${worst.toFixed(0)}px²`);
751
+ }
752
+ if (lines.length > 0)
753
+ emit.log(`layout-lint:\n${lines.join("\n")}`, "info");
754
+ }
755
+ function applyLayoutLintFailGates(opts, layoutLint, hit) {
756
+ const gate = (enabled, rule, results) => {
757
+ if (!enabled || !results)
758
+ return;
759
+ const failed = results.filter((result) => !result.found || result.outcome === "fail");
760
+ for (const result of failed) {
761
+ emit.log(`check-${rule} FAIL ${result.selector}: ${result.found ? "rendered relation failed" : "element not found"}`, "warn");
762
+ }
763
+ if (failed.length > 0)
764
+ process.exitCode = 2;
765
+ };
766
+ gate(opts.checkAlignFail, "align", layoutLint?.align);
767
+ gate(opts.checkGapFail, "gap", layoutLint?.gap);
768
+ gate(opts.checkClipFail, "clip", layoutLint?.clip);
769
+ gate(opts.checkOverlapFail, "overlap", layoutLint?.overlap);
770
+ if (opts.checkHitFail && hit) {
771
+ const failed = hit.filter((result) => !result.found || result.outcome === "fail" || result.outcome === "unknown");
772
+ for (const result of failed) {
773
+ emit.log(`check-hit FAIL ${result.selector ?? "document"}: ${result.found ? result.outcome : "element not found"}`, "warn");
774
+ }
775
+ if (failed.length > 0)
776
+ process.exitCode = 2;
777
+ }
778
+ }
596
779
  function summarizeDiagnostics(diag) {
597
780
  return {
598
781
  viewport: diag.viewport,
@@ -468,6 +468,7 @@ function readReplanningFile(path) {
468
468
  },
469
469
  ];
470
470
  }));
471
+ const review = readReplanningReview(config.review);
471
472
  return {
472
473
  plannerSpecialist: config.planner_specialist,
473
474
  autoApply: jsonBoolean(config.auto_apply, "replanning auto_apply"),
@@ -475,6 +476,24 @@ function readReplanningFile(path) {
475
476
  maxWorkItemsPerPlan: jsonInteger(config.max_work_items_per_plan, "replanning max_work_items_per_plan"),
476
477
  maxTotalWorkItems: jsonInteger(config.max_total_work_items, "replanning max_total_work_items"),
477
478
  templates,
479
+ ...(review ? { review } : {}),
480
+ };
481
+ }
482
+ function readReplanningReview(raw) {
483
+ if (raw === undefined)
484
+ return undefined;
485
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
486
+ throw new Error("supervisor replanning review must be a JSON object");
487
+ }
488
+ const review = raw;
489
+ if (!Array.isArray(review.reviewer_specialists) ||
490
+ review.reviewer_specialists.some((item) => typeof item !== "string")) {
491
+ throw new Error("supervisor replanning review reviewer_specialists must be an array of strings");
492
+ }
493
+ return {
494
+ reviewerSpecialists: review.reviewer_specialists,
495
+ maxRevisionRounds: jsonNonNegativeInteger(review.max_revision_rounds, "replanning review max_revision_rounds") ??
496
+ 0,
478
497
  };
479
498
  }
480
499
  function readMissionFile(path) {
@@ -528,6 +547,9 @@ function emitSupervisor(record, json, emit, detail = false) {
528
547
  ];
529
548
  if (record.intent.replanning) {
530
549
  lines.push(`replanning: generation=${projection.plan_generation}, used=${projection.replans_used}/${record.intent.replanning.max_replans}, auto_apply=${record.intent.replanning.auto_apply}`);
550
+ if (record.intent.replanning.review) {
551
+ lines.push(`plan review: reviewers=${record.intent.replanning.review.reviewer_specialists.join(", ")}, max_revision_rounds=${record.intent.replanning.review.max_revision_rounds}`);
552
+ }
531
553
  if (projection.root_work_id !== record.intent.root_work_id) {
532
554
  lines.push(`original root: ${record.intent.root_work_id}`);
533
555
  }
@@ -575,6 +597,14 @@ function jsonInteger(value, field) {
575
597
  }
576
598
  return value;
577
599
  }
600
+ function jsonNonNegativeInteger(value, field) {
601
+ if (value === undefined)
602
+ return undefined;
603
+ if (!Number.isSafeInteger(value) || value < 0) {
604
+ throw new Error(`${field} must be a non-negative integer`);
605
+ }
606
+ return value;
607
+ }
578
608
  function jsonBoolean(value, field) {
579
609
  if (value === undefined)
580
610
  return undefined;
@@ -604,6 +634,10 @@ function emitPlan(plan, json, emit) {
604
634
  lines.push(`milestone: ${plan.proposal.milestone.sequence} ${plan.proposal.milestone.title}`);
605
635
  }
606
636
  }
637
+ if (plan.review) {
638
+ lines.push(`review: ${plan.review.status}, rounds=${plan.review.rounds}, blocking=${plan.review.blocking_findings}, advisory=${plan.review.advisory_findings}`);
639
+ lines.push(`review candidate: ${plan.review.candidate_sha256}`);
640
+ }
607
641
  if (plan.root_work_id)
608
642
  lines.push(`applied root: ${plan.root_work_id}`);
609
643
  if (plan.approval_id)
@@ -621,7 +655,8 @@ function emitPlanOutcome(outcome, json, emit) {
621
655
  `${outcome.root_work_id ? `root: ${outcome.root_work_id}\n` : ""}`);
622
656
  }
623
657
  function renderPlanRow(plan) {
624
- return `${plan.request.id} ${plan.status.padEnd(18)} ${plan.request.prior_root_work_id}`;
658
+ const review = plan.review ? ` review:${plan.review.status}` : "";
659
+ return `${plan.request.id} ${plan.status.padEnd(18)} ${plan.request.prior_root_work_id}${review}`;
625
660
  }
626
661
  function delay(ms) {
627
662
  return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
@@ -1,6 +1,6 @@
1
1
  export { approveSupervisorPlan, rejectSupervisorPlan } from "./plan-api.js";
2
- export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.js";
3
- export { type CreateSupervisorPlanTemplateInput, type CreateSupervisorReplanningInput, SUPERVISOR_PLAN_SCHEMA_VERSION, type SupervisorPlanEvent, type SupervisorPlanEventType, type SupervisorPlanHistory, type SupervisorPlanMilestone, type SupervisorPlanOutcome, type SupervisorPlanProposal, type SupervisorPlanRecord, type SupervisorPlanRequest, type SupervisorPlanStatus, type SupervisorPlanTemplate, type SupervisorPlanWorkSpec, type SupervisorReplanningPolicy, } from "./plan-types.js";
2
+ export { readSupervisorPlan, readSupervisorPlanReviewReceipt, readSupervisorPlans, } from "./plan-read.js";
3
+ export { type CreateSupervisorPlanTemplateInput, type CreateSupervisorReplanningInput, MAX_SUPERVISOR_PLAN_REVIEWERS, MAX_SUPERVISOR_PLAN_REVISION_ROUNDS, SUPERVISOR_PLAN_SCHEMA_VERSION, type SupervisorPlanEvent, type SupervisorPlanEventType, type SupervisorPlanHistory, type SupervisorPlanMilestone, type SupervisorPlanOutcome, type SupervisorPlanProposal, type SupervisorPlanRecord, type SupervisorPlanRequest, type SupervisorPlanReviewFinding, type SupervisorPlanReviewReceipt, type SupervisorPlanReviewReviewer, type SupervisorPlanReviewRound, type SupervisorPlanReviewStatus, type SupervisorPlanReviewSummary, type SupervisorPlanReviewVerdict, type SupervisorPlanStatus, type SupervisorPlanTemplate, type SupervisorPlanWorkSpec, type SupervisorReplanningPolicy, } from "./plan-types.js";
4
4
  export { type RunSupervisorInput, runSupervisor, type SupervisorDispatchOutcome, type SupervisorRunReport, type SupervisorStopReason, } from "./runner.js";
5
5
  export { type ConfigureSupervisorServiceInput, configureSupervisorService, type RunSupervisorServiceDaemonInput, type RunSupervisorServiceSweepInput, readSupervisorServiceConfig, readSupervisorServiceRuntime, readSupervisorServiceStatus, requestSupervisorServiceStop, runSupervisorServiceDaemon, runSupervisorServiceSweep, SUPERVISOR_SERVICE_CONFIG_SCHEMA_VERSION, SUPERVISOR_SERVICE_RUNTIME_SCHEMA_VERSION, SUPERVISOR_SERVICE_STATUS_SCHEMA_VERSION, type SupervisorServiceConfig, type SupervisorServiceEngineConfig, type SupervisorServiceGoalRuntime, type SupervisorServiceGoalState, type SupervisorServiceProcessState, type SupervisorServiceRuntime, type SupervisorServiceStatus, type SupervisorServiceStatusRecord, type SupervisorServiceSweepOutcome, type SupervisorServiceSweepReport, spawnSupervisorService, supervisorServiceLogPath, } from "./service.js";
6
6
  export { assertSupervisorId, type CreateSupervisorInput, type CreateSupervisorMissionInput, collectSupervisorWork, createSupervisor, listSupervisors, newSupervisorId, readSupervisor, SUPERVISOR_INTENT_SCHEMA_VERSION, type SupervisorAutomationPolicy, type SupervisorIntent, type SupervisorLimits, type SupervisorMission, type SupervisorNextAction, type SupervisorProjection, type SupervisorRecord, type SupervisorState, } from "./state.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,EACL,KAAK,iCAAiC,EACtC,KAAK,+BAA+B,EACpC,8BAA8B,EAC9B,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,kBAAkB,EACvB,aAAa,EACb,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,+BAA+B,EACpC,0BAA0B,EAC1B,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,EACnC,2BAA2B,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,EAC5B,0BAA0B,EAC1B,yBAAyB,EACzB,wCAAwC,EACxC,yCAAyC,EACzC,wCAAwC,EACxC,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAC/B,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,EACjC,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,cAAc,EACd,gCAAgC,EAChC,KAAK,0BAA0B,EAC/B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EACL,kBAAkB,EAClB,+BAA+B,EAC/B,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,iCAAiC,EACtC,KAAK,+BAA+B,EACpC,6BAA6B,EAC7B,mCAAmC,EACnC,8BAA8B,EAC9B,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,kBAAkB,EACvB,aAAa,EACb,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,+BAA+B,EACpC,0BAA0B,EAC1B,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,EACnC,2BAA2B,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,EAC5B,0BAA0B,EAC1B,yBAAyB,EACzB,wCAAwC,EACxC,yCAAyC,EACzC,wCAAwC,EACxC,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAC/B,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,EACjC,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,cAAc,EACd,gCAAgC,EAChC,KAAK,0BAA0B,EAC/B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC"}
@@ -1,6 +1,6 @@
1
1
  export { approveSupervisorPlan, rejectSupervisorPlan } from "./plan-api.js";
2
- export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.js";
3
- export { SUPERVISOR_PLAN_SCHEMA_VERSION, } from "./plan-types.js";
2
+ export { readSupervisorPlan, readSupervisorPlanReviewReceipt, readSupervisorPlans, } from "./plan-read.js";
3
+ export { MAX_SUPERVISOR_PLAN_REVIEWERS, MAX_SUPERVISOR_PLAN_REVISION_ROUNDS, SUPERVISOR_PLAN_SCHEMA_VERSION, } from "./plan-types.js";
4
4
  export { runSupervisor, } from "./runner.js";
5
5
  export { configureSupervisorService, readSupervisorServiceConfig, readSupervisorServiceRuntime, readSupervisorServiceStatus, requestSupervisorServiceStop, runSupervisorServiceDaemon, runSupervisorServiceSweep, SUPERVISOR_SERVICE_CONFIG_SCHEMA_VERSION, SUPERVISOR_SERVICE_RUNTIME_SCHEMA_VERSION, SUPERVISOR_SERVICE_STATUS_SCHEMA_VERSION, spawnSupervisorService, supervisorServiceLogPath, } from "./service.js";
6
6
  export { assertSupervisorId, collectSupervisorWork, createSupervisor, listSupervisors, newSupervisorId, readSupervisor, SUPERVISOR_INTENT_SCHEMA_VERSION, } from "./state.js";
@@ -1,4 +1,5 @@
1
- import { type SupervisorPlanHistory, type SupervisorPlanRecord } from "./plan-types.js";
1
+ import { type SupervisorPlanHistory, type SupervisorPlanRecord, type SupervisorPlanReviewReceipt } from "./plan-types.js";
2
2
  export declare function readSupervisorPlans(coordRootRaw: string, goalId: string, originalRootWorkId: string): SupervisorPlanHistory;
3
3
  export declare function readSupervisorPlan(coordRootRaw: string, goalId: string, planId: string): SupervisorPlanRecord;
4
+ export declare function readSupervisorPlanReviewReceipt(coordRootRaw: string, goalId: string, planId: string): SupervisorPlanReviewReceipt | undefined;
4
5
  //# sourceMappingURL=plan-read.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plan-read.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/plan-read.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EAE1B,MAAM,iBAAiB,CAAC;AAuBzB,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,MAAM,GACzB,qBAAqB,CAwEvB;AAED,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,oBAAoB,CAetB"}
1
+ {"version":3,"file":"plan-read.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/plan-read.ts"],"names":[],"mappings":"AAIA,OAAO,EAML,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EAGzB,KAAK,2BAA2B,EAMjC,MAAM,iBAAiB,CAAC;AAwBzB,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,MAAM,GACzB,qBAAqB,CAwEvB;AAED,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,oBAAoB,CAsBtB;AAED,wBAAgB,+BAA+B,CAC7C,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,2BAA2B,GAAG,SAAS,CAWzC"}
@@ -1,7 +1,8 @@
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.js";
4
- import { SUPERVISOR_PLAN_SCHEMA_VERSION, } from "./plan-types.js";
5
+ import { MAX_SUPERVISOR_PLAN_REVIEWERS, MAX_SUPERVISOR_PLAN_REVISION_ROUNDS, SUPERVISOR_PLAN_SCHEMA_VERSION, } from "./plan-types.js";
5
6
  const PLAN_ID = /^plan-[0-9]{4}-[a-f0-9]{8}$/;
6
7
  const GOAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
7
8
  const WORK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
@@ -15,6 +16,7 @@ const MAX_EVENTS = 100;
15
16
  const EVENT_TYPES = new Set([
16
17
  "plan.awaiting_approval",
17
18
  "plan.resumed",
19
+ "plan.reviewed",
18
20
  "plan.proposed",
19
21
  "plan.applied",
20
22
  "plan.completed",
@@ -107,9 +109,30 @@ export function readSupervisorPlan(coordRootRaw, goalId, planId) {
107
109
  : undefined;
108
110
  if (proposal)
109
111
  validateProposalEnvelope(proposal, planId);
112
+ const review = readSupervisorPlanReviewReceipt(coordRootRaw, goalId, planId);
110
113
  const events = readEvents(join(dir, "events.jsonl"), planId);
111
114
  const derived = deriveStatus(coordRoot, events, proposal);
112
- return { request, proposal, events, ...derived };
115
+ return {
116
+ request,
117
+ proposal,
118
+ review: review ? summarizeReview(review) : undefined,
119
+ events,
120
+ ...derived,
121
+ };
122
+ }
123
+ export function readSupervisorPlanReviewReceipt(coordRootRaw, goalId, planId) {
124
+ assertId(goalId, GOAL_ID, "supervisor id");
125
+ assertId(planId, PLAN_ID, "supervisor plan id");
126
+ const coordRoot = resolve(coordRootRaw);
127
+ const path = join(planDir(coordRoot, goalId, planId), "review.json");
128
+ if (!existsSync(path))
129
+ return undefined;
130
+ const receipt = readJson(path, "plan review receipt");
131
+ validateReviewReceipt(receipt, planId);
132
+ const reviewPolicy = readFrozenReviewPolicy(coordRoot, goalId);
133
+ if (reviewPolicy)
134
+ validateReviewReceiptMatchesPolicy(receipt, planId, reviewPolicy);
135
+ return receipt;
113
136
  }
114
137
  function deriveStatus(coordRoot, events, proposal) {
115
138
  const latest = events.at(-1);
@@ -249,6 +272,156 @@ function validateProposalEnvelope(proposal, planId) {
249
272
  }
250
273
  }
251
274
  }
275
+ function validateReviewReceipt(receipt, planId) {
276
+ if (!receipt ||
277
+ typeof receipt !== "object" ||
278
+ receipt.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
279
+ receipt.plan_id !== planId ||
280
+ !["passed", "revision_exhausted", "attention", "failed"].includes(receipt.status) ||
281
+ !/^[a-f0-9]{64}$/.test(receipt.candidate_sha256) ||
282
+ !Array.isArray(receipt.rounds) ||
283
+ receipt.rounds.length < 1 ||
284
+ receipt.rounds.length > MAX_SUPERVISOR_PLAN_REVISION_ROUNDS + 1) {
285
+ throw new Error(`supervisor plan review ${planId} has an unsupported schema`);
286
+ }
287
+ validateProposalEnvelope(receipt.final_candidate, planId);
288
+ if (receipt.candidate_sha256 !== candidateDigest(receipt.final_candidate)) {
289
+ throw new Error(`supervisor plan review ${planId} has invalid candidate digest`);
290
+ }
291
+ receipt.rounds.forEach((round, index) => {
292
+ validateReviewRound(round, planId, index + 1);
293
+ });
294
+ const finalRound = receipt.rounds.at(-1);
295
+ if (finalRound?.candidate_sha256 !== receipt.candidate_sha256) {
296
+ throw new Error(`supervisor plan review ${planId} final round does not match its candidate`);
297
+ }
298
+ if (receipt.status === "passed" && finalRound?.outcome !== "approved") {
299
+ throw new Error(`supervisor plan review ${planId} passed without approval`);
300
+ }
301
+ if (receipt.status === "attention" && finalRound?.outcome === "approved") {
302
+ throw new Error(`supervisor plan review ${planId} attention conflicts with approval`);
303
+ }
304
+ }
305
+ function readFrozenReviewPolicy(coordRoot, goalId) {
306
+ const path = join(coordRoot, ".harnery", "supervisors", goalId, "intent.json");
307
+ if (!existsSync(path))
308
+ return undefined;
309
+ const intent = readJson(path, "supervisor intent");
310
+ return intent.replanning?.review;
311
+ }
312
+ function validateReviewReceiptMatchesPolicy(receipt, planId, review) {
313
+ if (receipt.rounds.length > review.max_revision_rounds + 1) {
314
+ throw new Error(`supervisor plan review ${planId} exceeds the frozen review policy`);
315
+ }
316
+ for (const round of receipt.rounds) {
317
+ if (round.reviewers.length !== review.reviewer_specialists.length) {
318
+ throw new Error(`supervisor plan review ${planId} does not match the frozen review policy`);
319
+ }
320
+ round.reviewers.forEach((reviewer, index) => {
321
+ if (reviewer.specialist !== review.reviewer_specialists[index]) {
322
+ throw new Error(`supervisor plan review ${planId} does not match the frozen review policy`);
323
+ }
324
+ });
325
+ const outcome = aggregateReviewers(round.reviewers);
326
+ if (round.outcome !== outcome) {
327
+ throw new Error(`supervisor plan review ${planId} has an invalid reviewer outcome`);
328
+ }
329
+ }
330
+ const finalRound = receipt.rounds.at(-1);
331
+ if (receipt.status === "passed" && finalRound.outcome !== "approved") {
332
+ throw new Error(`supervisor plan review ${planId} passed without policy approval`);
333
+ }
334
+ if (receipt.status === "revision_exhausted" && finalRound.round <= review.max_revision_rounds) {
335
+ throw new Error(`supervisor plan review ${planId} exhausted before the frozen review limit`);
336
+ }
337
+ }
338
+ function aggregateReviewers(reviewers) {
339
+ if (reviewers.length < 1)
340
+ return "failed";
341
+ if (reviewers.some((reviewer) => reviewer.verdict === "attention"))
342
+ return "attention";
343
+ if (reviewers.some((reviewer) => reviewer.verdict === "revise" ||
344
+ reviewer.findings.some((finding) => finding.severity === "blocking"))) {
345
+ return "revise";
346
+ }
347
+ return "approved";
348
+ }
349
+ function candidateDigest(candidate) {
350
+ return createHash("sha256")
351
+ .update(JSON.stringify(canonicalCandidate(candidate)))
352
+ .digest("hex");
353
+ }
354
+ function canonicalCandidate(candidate) {
355
+ return {
356
+ schema_version: candidate.schema_version,
357
+ plan_id: candidate.plan_id,
358
+ decision: candidate.decision,
359
+ rationale: candidate.rationale,
360
+ root: candidate.root,
361
+ work: candidate.work,
362
+ milestone: candidate.milestone,
363
+ };
364
+ }
365
+ function validateReviewRound(round, planId, sequence) {
366
+ if (!round ||
367
+ typeof round !== "object" ||
368
+ round.round !== sequence ||
369
+ !/^[a-f0-9]{64}$/.test(round.candidate_sha256) ||
370
+ !Array.isArray(round.reviewers) ||
371
+ round.reviewers.length < 1 ||
372
+ round.reviewers.length > MAX_SUPERVISOR_PLAN_REVIEWERS ||
373
+ !["approved", "revise", "attention", "failed"].includes(round.outcome) ||
374
+ (round.revision_workflow_run_id !== undefined &&
375
+ !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(round.revision_workflow_run_id))) {
376
+ throw new Error(`supervisor plan review ${planId} round ${sequence} is invalid`);
377
+ }
378
+ round.reviewers.forEach((reviewer, index) => {
379
+ validateReviewReviewer(reviewer, planId, sequence, index);
380
+ });
381
+ }
382
+ function validateReviewReviewer(reviewer, planId, round, index) {
383
+ if (!reviewer ||
384
+ typeof reviewer !== "object" ||
385
+ typeof reviewer.specialist !== "string" ||
386
+ reviewer.specialist.length < 1 ||
387
+ reviewer.specialist.length > 100 ||
388
+ !["approve", "revise", "attention"].includes(reviewer.verdict) ||
389
+ typeof reviewer.rationale !== "string" ||
390
+ reviewer.rationale.length < 1 ||
391
+ reviewer.rationale.length > 2_000 ||
392
+ !Array.isArray(reviewer.findings) ||
393
+ reviewer.findings.length > 50) {
394
+ throw new Error(`supervisor plan review ${planId} round ${round} reviewer ${index} is invalid`);
395
+ }
396
+ reviewer.findings.forEach((finding, findingIndex) => {
397
+ validateReviewFinding(finding, planId, round, index, findingIndex);
398
+ });
399
+ }
400
+ function validateReviewFinding(finding, planId, round, reviewerIndex, findingIndex) {
401
+ if (!finding ||
402
+ typeof finding !== "object" ||
403
+ typeof finding.code !== "string" ||
404
+ !/^[a-z][a-z0-9._-]{0,99}$/.test(finding.code) ||
405
+ !["blocking", "advisory"].includes(finding.severity) ||
406
+ typeof finding.summary !== "string" ||
407
+ finding.summary.length < 1 ||
408
+ finding.summary.length > 1_000 ||
409
+ typeof finding.recommendation !== "string" ||
410
+ finding.recommendation.length < 1 ||
411
+ finding.recommendation.length > 1_000) {
412
+ throw new Error(`supervisor plan review ${planId} round ${round} reviewer ${reviewerIndex} finding ${findingIndex} is invalid`);
413
+ }
414
+ }
415
+ function summarizeReview(receipt) {
416
+ const findings = receipt.rounds.flatMap((round) => round.reviewers.flatMap((reviewer) => reviewer.findings));
417
+ return {
418
+ status: receipt.status,
419
+ candidate_sha256: receipt.candidate_sha256,
420
+ rounds: receipt.rounds.length,
421
+ blocking_findings: findings.filter((finding) => finding.severity === "blocking").length,
422
+ advisory_findings: findings.filter((finding) => finding.severity === "advisory").length,
423
+ };
424
+ }
252
425
  function validateEvent(event, planId, sequence) {
253
426
  if (event.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
254
427
  event.plan_id !== planId ||