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.
- package/README.md +1 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +187 -4
- package/dist/commands/supervisor.js +36 -1
- package/dist/core/supervisor/index.d.ts +2 -2
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/index.js +2 -2
- package/dist/core/supervisor/plan-read.d.ts +2 -1
- package/dist/core/supervisor/plan-read.d.ts.map +1 -1
- package/dist/core/supervisor/plan-read.js +175 -2
- package/dist/core/supervisor/plan-types.d.ts +48 -1
- package/dist/core/supervisor/plan-types.d.ts.map +1 -1
- package/dist/core/supervisor/plan-types.js +2 -0
- package/dist/core/supervisor/planning.d.ts.map +1 -1
- package/dist/core/supervisor/planning.js +762 -60
- package/dist/core/supervisor/read.d.ts +2 -2
- package/dist/core/supervisor/read.d.ts.map +1 -1
- package/dist/core/supervisor/read.js +2 -2
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +74 -6
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +24 -6
- package/dist/core/workflow/types.d.ts +9 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +5 -3
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +53 -4
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +39 -0
- package/dist/lib/browser/geometry.d.ts +146 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -0
- package/dist/lib/browser/geometry.js +525 -0
- package/dist/lib/browser/index.d.ts +2 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/target-size.d.ts +65 -0
- package/dist/lib/browser/target-size.d.ts.map +1 -0
- package/dist/lib/browser/target-size.js +108 -0
- package/package.json +7 -1
- package/src/commands/browse.ts +319 -0
- package/src/commands/supervisor.ts +45 -1
- package/src/core/supervisor/index.ts +14 -1
- package/src/core/supervisor/plan-read.ts +234 -1
- package/src/core/supervisor/plan-types.ts +55 -0
- package/src/core/supervisor/planning.ts +954 -65
- package/src/core/supervisor/read.ts +14 -1
- package/src/core/supervisor/state.ts +96 -3
- package/src/core/workflow/engine.ts +29 -6
- package/src/core/workflow/types.ts +9 -0
- package/src/core/workflow/validate.ts +59 -4
- package/src/lib/browser/client.ts +62 -0
- package/src/lib/browser/geometry.ts +695 -0
- package/src/lib/browser/index.ts +25 -0
- 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.
|
|
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;
|
|
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"}
|
package/dist/commands/browse.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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":"
|
|
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 {
|
|
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 ||
|