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
|
@@ -617,6 +617,7 @@ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
|
|
|
617
617
|
];
|
|
618
618
|
}),
|
|
619
619
|
);
|
|
620
|
+
const review = readReplanningReview(config.review);
|
|
620
621
|
return {
|
|
621
622
|
plannerSpecialist: config.planner_specialist,
|
|
622
623
|
autoApply: jsonBoolean(config.auto_apply, "replanning auto_apply"),
|
|
@@ -627,6 +628,29 @@ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
|
|
|
627
628
|
),
|
|
628
629
|
maxTotalWorkItems: jsonInteger(config.max_total_work_items, "replanning max_total_work_items"),
|
|
629
630
|
templates,
|
|
631
|
+
...(review ? { review } : {}),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function readReplanningReview(raw: unknown): CreateSupervisorReplanningInput["review"] | undefined {
|
|
636
|
+
if (raw === undefined) return undefined;
|
|
637
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
638
|
+
throw new Error("supervisor replanning review must be a JSON object");
|
|
639
|
+
}
|
|
640
|
+
const review = raw as Record<string, unknown>;
|
|
641
|
+
if (
|
|
642
|
+
!Array.isArray(review.reviewer_specialists) ||
|
|
643
|
+
review.reviewer_specialists.some((item) => typeof item !== "string")
|
|
644
|
+
) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"supervisor replanning review reviewer_specialists must be an array of strings",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
reviewerSpecialists: review.reviewer_specialists as string[],
|
|
651
|
+
maxRevisionRounds:
|
|
652
|
+
jsonNonNegativeInteger(review.max_revision_rounds, "replanning review max_revision_rounds") ??
|
|
653
|
+
0,
|
|
630
654
|
};
|
|
631
655
|
}
|
|
632
656
|
|
|
@@ -690,6 +714,11 @@ function emitSupervisor(
|
|
|
690
714
|
lines.push(
|
|
691
715
|
`replanning: generation=${projection.plan_generation}, used=${projection.replans_used}/${record.intent.replanning.max_replans}, auto_apply=${record.intent.replanning.auto_apply}`,
|
|
692
716
|
);
|
|
717
|
+
if (record.intent.replanning.review) {
|
|
718
|
+
lines.push(
|
|
719
|
+
`plan review: reviewers=${record.intent.replanning.review.reviewer_specialists.join(", ")}, max_revision_rounds=${record.intent.replanning.review.max_revision_rounds}`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
693
722
|
if (projection.root_work_id !== record.intent.root_work_id) {
|
|
694
723
|
lines.push(`original root: ${record.intent.root_work_id}`);
|
|
695
724
|
}
|
|
@@ -747,6 +776,14 @@ function jsonInteger(value: unknown, field: string): number | undefined {
|
|
|
747
776
|
return value as number;
|
|
748
777
|
}
|
|
749
778
|
|
|
779
|
+
function jsonNonNegativeInteger(value: unknown, field: string): number | undefined {
|
|
780
|
+
if (value === undefined) return undefined;
|
|
781
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
782
|
+
throw new Error(`${field} must be a non-negative integer`);
|
|
783
|
+
}
|
|
784
|
+
return value as number;
|
|
785
|
+
}
|
|
786
|
+
|
|
750
787
|
function jsonBoolean(value: unknown, field: string): boolean | undefined {
|
|
751
788
|
if (value === undefined) return undefined;
|
|
752
789
|
if (typeof value !== "boolean") throw new Error(`${field} must be boolean`);
|
|
@@ -775,6 +812,12 @@ function emitPlan(plan: SupervisorPlanRecord, json: boolean | undefined, emit: E
|
|
|
775
812
|
lines.push(`milestone: ${plan.proposal.milestone.sequence} ${plan.proposal.milestone.title}`);
|
|
776
813
|
}
|
|
777
814
|
}
|
|
815
|
+
if (plan.review) {
|
|
816
|
+
lines.push(
|
|
817
|
+
`review: ${plan.review.status}, rounds=${plan.review.rounds}, blocking=${plan.review.blocking_findings}, advisory=${plan.review.advisory_findings}`,
|
|
818
|
+
);
|
|
819
|
+
lines.push(`review candidate: ${plan.review.candidate_sha256}`);
|
|
820
|
+
}
|
|
778
821
|
if (plan.root_work_id) lines.push(`applied root: ${plan.root_work_id}`);
|
|
779
822
|
if (plan.approval_id) lines.push(`approval: ${plan.approval_id}`);
|
|
780
823
|
emit.text(`${lines.join("\n")}\n`);
|
|
@@ -798,7 +841,8 @@ function emitPlanOutcome(
|
|
|
798
841
|
}
|
|
799
842
|
|
|
800
843
|
function renderPlanRow(plan: SupervisorPlanRecord): string {
|
|
801
|
-
|
|
844
|
+
const review = plan.review ? ` review:${plan.review.status}` : "";
|
|
845
|
+
return `${plan.request.id} ${plan.status.padEnd(18)} ${plan.request.prior_root_work_id}${review}`;
|
|
802
846
|
}
|
|
803
847
|
|
|
804
848
|
function delay(ms: number): Promise<void> {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
export { approveSupervisorPlan, rejectSupervisorPlan } from "./plan-api.ts";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
readSupervisorPlan,
|
|
4
|
+
readSupervisorPlanReviewReceipt,
|
|
5
|
+
readSupervisorPlans,
|
|
6
|
+
} from "./plan-read.ts";
|
|
3
7
|
export {
|
|
4
8
|
type CreateSupervisorPlanTemplateInput,
|
|
5
9
|
type CreateSupervisorReplanningInput,
|
|
10
|
+
MAX_SUPERVISOR_PLAN_REVIEWERS,
|
|
11
|
+
MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
|
|
6
12
|
SUPERVISOR_PLAN_SCHEMA_VERSION,
|
|
7
13
|
type SupervisorPlanEvent,
|
|
8
14
|
type SupervisorPlanEventType,
|
|
@@ -12,6 +18,13 @@ export {
|
|
|
12
18
|
type SupervisorPlanProposal,
|
|
13
19
|
type SupervisorPlanRecord,
|
|
14
20
|
type SupervisorPlanRequest,
|
|
21
|
+
type SupervisorPlanReviewFinding,
|
|
22
|
+
type SupervisorPlanReviewReceipt,
|
|
23
|
+
type SupervisorPlanReviewReviewer,
|
|
24
|
+
type SupervisorPlanReviewRound,
|
|
25
|
+
type SupervisorPlanReviewStatus,
|
|
26
|
+
type SupervisorPlanReviewSummary,
|
|
27
|
+
type SupervisorPlanReviewVerdict,
|
|
15
28
|
type SupervisorPlanStatus,
|
|
16
29
|
type SupervisorPlanTemplate,
|
|
17
30
|
type SupervisorPlanWorkSpec,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { readWorkItem } from "../work/read.ts";
|
|
4
5
|
import {
|
|
6
|
+
MAX_SUPERVISOR_PLAN_REVIEWERS,
|
|
7
|
+
MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
|
|
5
8
|
SUPERVISOR_PLAN_SCHEMA_VERSION,
|
|
6
9
|
type SupervisorPlanEvent,
|
|
7
10
|
type SupervisorPlanEventType,
|
|
@@ -9,6 +12,13 @@ import {
|
|
|
9
12
|
type SupervisorPlanProposal,
|
|
10
13
|
type SupervisorPlanRecord,
|
|
11
14
|
type SupervisorPlanRequest,
|
|
15
|
+
type SupervisorPlanReviewFinding,
|
|
16
|
+
type SupervisorPlanReviewReceipt,
|
|
17
|
+
type SupervisorPlanReviewReviewer,
|
|
18
|
+
type SupervisorPlanReviewRound,
|
|
19
|
+
type SupervisorPlanReviewSummary,
|
|
20
|
+
type SupervisorPlanReviewVerdict,
|
|
21
|
+
type SupervisorReplanningPolicy,
|
|
12
22
|
} from "./plan-types.ts";
|
|
13
23
|
|
|
14
24
|
const PLAN_ID = /^plan-[0-9]{4}-[a-f0-9]{8}$/;
|
|
@@ -24,6 +34,7 @@ const MAX_EVENTS = 100;
|
|
|
24
34
|
const EVENT_TYPES = new Set<SupervisorPlanEventType>([
|
|
25
35
|
"plan.awaiting_approval",
|
|
26
36
|
"plan.resumed",
|
|
37
|
+
"plan.reviewed",
|
|
27
38
|
"plan.proposed",
|
|
28
39
|
"plan.applied",
|
|
29
40
|
"plan.completed",
|
|
@@ -126,9 +137,33 @@ export function readSupervisorPlan(
|
|
|
126
137
|
? readJson<SupervisorPlanProposal>(proposalPath, "plan proposal")
|
|
127
138
|
: undefined;
|
|
128
139
|
if (proposal) validateProposalEnvelope(proposal, planId);
|
|
140
|
+
const review = readSupervisorPlanReviewReceipt(coordRootRaw, goalId, planId);
|
|
129
141
|
const events = readEvents(join(dir, "events.jsonl"), planId);
|
|
130
142
|
const derived = deriveStatus(coordRoot, events, proposal);
|
|
131
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
request,
|
|
145
|
+
proposal,
|
|
146
|
+
review: review ? summarizeReview(review) : undefined,
|
|
147
|
+
events,
|
|
148
|
+
...derived,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function readSupervisorPlanReviewReceipt(
|
|
153
|
+
coordRootRaw: string,
|
|
154
|
+
goalId: string,
|
|
155
|
+
planId: string,
|
|
156
|
+
): SupervisorPlanReviewReceipt | undefined {
|
|
157
|
+
assertId(goalId, GOAL_ID, "supervisor id");
|
|
158
|
+
assertId(planId, PLAN_ID, "supervisor plan id");
|
|
159
|
+
const coordRoot = resolve(coordRootRaw);
|
|
160
|
+
const path = join(planDir(coordRoot, goalId, planId), "review.json");
|
|
161
|
+
if (!existsSync(path)) return undefined;
|
|
162
|
+
const receipt = readJson<SupervisorPlanReviewReceipt>(path, "plan review receipt");
|
|
163
|
+
validateReviewReceipt(receipt, planId);
|
|
164
|
+
const reviewPolicy = readFrozenReviewPolicy(coordRoot, goalId);
|
|
165
|
+
if (reviewPolicy) validateReviewReceiptMatchesPolicy(receipt, planId, reviewPolicy);
|
|
166
|
+
return receipt;
|
|
132
167
|
}
|
|
133
168
|
|
|
134
169
|
function deriveStatus(
|
|
@@ -288,6 +323,204 @@ function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: stri
|
|
|
288
323
|
}
|
|
289
324
|
}
|
|
290
325
|
|
|
326
|
+
function validateReviewReceipt(receipt: SupervisorPlanReviewReceipt, planId: string): void {
|
|
327
|
+
if (
|
|
328
|
+
!receipt ||
|
|
329
|
+
typeof receipt !== "object" ||
|
|
330
|
+
receipt.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
|
|
331
|
+
receipt.plan_id !== planId ||
|
|
332
|
+
!["passed", "revision_exhausted", "attention", "failed"].includes(receipt.status) ||
|
|
333
|
+
!/^[a-f0-9]{64}$/.test(receipt.candidate_sha256) ||
|
|
334
|
+
!Array.isArray(receipt.rounds) ||
|
|
335
|
+
receipt.rounds.length < 1 ||
|
|
336
|
+
receipt.rounds.length > MAX_SUPERVISOR_PLAN_REVISION_ROUNDS + 1
|
|
337
|
+
) {
|
|
338
|
+
throw new Error(`supervisor plan review ${planId} has an unsupported schema`);
|
|
339
|
+
}
|
|
340
|
+
validateProposalEnvelope(receipt.final_candidate, planId);
|
|
341
|
+
if (receipt.candidate_sha256 !== candidateDigest(receipt.final_candidate)) {
|
|
342
|
+
throw new Error(`supervisor plan review ${planId} has invalid candidate digest`);
|
|
343
|
+
}
|
|
344
|
+
receipt.rounds.forEach((round, index) => {
|
|
345
|
+
validateReviewRound(round, planId, index + 1);
|
|
346
|
+
});
|
|
347
|
+
const finalRound = receipt.rounds.at(-1);
|
|
348
|
+
if (finalRound?.candidate_sha256 !== receipt.candidate_sha256) {
|
|
349
|
+
throw new Error(`supervisor plan review ${planId} final round does not match its candidate`);
|
|
350
|
+
}
|
|
351
|
+
if (receipt.status === "passed" && finalRound?.outcome !== "approved") {
|
|
352
|
+
throw new Error(`supervisor plan review ${planId} passed without approval`);
|
|
353
|
+
}
|
|
354
|
+
if (receipt.status === "attention" && finalRound?.outcome === "approved") {
|
|
355
|
+
throw new Error(`supervisor plan review ${planId} attention conflicts with approval`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function readFrozenReviewPolicy(
|
|
360
|
+
coordRoot: string,
|
|
361
|
+
goalId: string,
|
|
362
|
+
): SupervisorReplanningPolicy["review"] | undefined {
|
|
363
|
+
const path = join(coordRoot, ".harnery", "supervisors", goalId, "intent.json");
|
|
364
|
+
if (!existsSync(path)) return undefined;
|
|
365
|
+
const intent = readJson<{ replanning?: SupervisorReplanningPolicy }>(path, "supervisor intent");
|
|
366
|
+
return intent.replanning?.review;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function validateReviewReceiptMatchesPolicy(
|
|
370
|
+
receipt: SupervisorPlanReviewReceipt,
|
|
371
|
+
planId: string,
|
|
372
|
+
review: NonNullable<SupervisorReplanningPolicy["review"]>,
|
|
373
|
+
): void {
|
|
374
|
+
if (receipt.rounds.length > review.max_revision_rounds + 1) {
|
|
375
|
+
throw new Error(`supervisor plan review ${planId} exceeds the frozen review policy`);
|
|
376
|
+
}
|
|
377
|
+
for (const round of receipt.rounds) {
|
|
378
|
+
if (round.reviewers.length !== review.reviewer_specialists.length) {
|
|
379
|
+
throw new Error(`supervisor plan review ${planId} does not match the frozen review policy`);
|
|
380
|
+
}
|
|
381
|
+
round.reviewers.forEach((reviewer, index) => {
|
|
382
|
+
if (reviewer.specialist !== review.reviewer_specialists[index]) {
|
|
383
|
+
throw new Error(`supervisor plan review ${planId} does not match the frozen review policy`);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
const outcome = aggregateReviewers(round.reviewers);
|
|
387
|
+
if (round.outcome !== outcome) {
|
|
388
|
+
throw new Error(`supervisor plan review ${planId} has an invalid reviewer outcome`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const finalRound = receipt.rounds.at(-1)!;
|
|
392
|
+
if (receipt.status === "passed" && finalRound.outcome !== "approved") {
|
|
393
|
+
throw new Error(`supervisor plan review ${planId} passed without policy approval`);
|
|
394
|
+
}
|
|
395
|
+
if (receipt.status === "revision_exhausted" && finalRound.round <= review.max_revision_rounds) {
|
|
396
|
+
throw new Error(`supervisor plan review ${planId} exhausted before the frozen review limit`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function aggregateReviewers(
|
|
401
|
+
reviewers: readonly SupervisorPlanReviewReviewer[],
|
|
402
|
+
): SupervisorPlanReviewRound["outcome"] {
|
|
403
|
+
if (reviewers.length < 1) return "failed";
|
|
404
|
+
if (reviewers.some((reviewer) => reviewer.verdict === "attention")) return "attention";
|
|
405
|
+
if (
|
|
406
|
+
reviewers.some(
|
|
407
|
+
(reviewer) =>
|
|
408
|
+
reviewer.verdict === "revise" ||
|
|
409
|
+
reviewer.findings.some((finding) => finding.severity === "blocking"),
|
|
410
|
+
)
|
|
411
|
+
) {
|
|
412
|
+
return "revise";
|
|
413
|
+
}
|
|
414
|
+
return "approved";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function candidateDigest(candidate: SupervisorPlanProposal): string {
|
|
418
|
+
return createHash("sha256")
|
|
419
|
+
.update(JSON.stringify(canonicalCandidate(candidate)))
|
|
420
|
+
.digest("hex");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function canonicalCandidate(candidate: SupervisorPlanProposal): unknown {
|
|
424
|
+
return {
|
|
425
|
+
schema_version: candidate.schema_version,
|
|
426
|
+
plan_id: candidate.plan_id,
|
|
427
|
+
decision: candidate.decision,
|
|
428
|
+
rationale: candidate.rationale,
|
|
429
|
+
root: candidate.root,
|
|
430
|
+
work: candidate.work,
|
|
431
|
+
milestone: candidate.milestone,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function validateReviewRound(
|
|
436
|
+
round: SupervisorPlanReviewRound,
|
|
437
|
+
planId: string,
|
|
438
|
+
sequence: number,
|
|
439
|
+
): void {
|
|
440
|
+
if (
|
|
441
|
+
!round ||
|
|
442
|
+
typeof round !== "object" ||
|
|
443
|
+
round.round !== sequence ||
|
|
444
|
+
!/^[a-f0-9]{64}$/.test(round.candidate_sha256) ||
|
|
445
|
+
!Array.isArray(round.reviewers) ||
|
|
446
|
+
round.reviewers.length < 1 ||
|
|
447
|
+
round.reviewers.length > MAX_SUPERVISOR_PLAN_REVIEWERS ||
|
|
448
|
+
!["approved", "revise", "attention", "failed"].includes(round.outcome) ||
|
|
449
|
+
(round.revision_workflow_run_id !== undefined &&
|
|
450
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(round.revision_workflow_run_id))
|
|
451
|
+
) {
|
|
452
|
+
throw new Error(`supervisor plan review ${planId} round ${sequence} is invalid`);
|
|
453
|
+
}
|
|
454
|
+
round.reviewers.forEach((reviewer, index) => {
|
|
455
|
+
validateReviewReviewer(reviewer, planId, sequence, index);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function validateReviewReviewer(
|
|
460
|
+
reviewer: SupervisorPlanReviewReviewer,
|
|
461
|
+
planId: string,
|
|
462
|
+
round: number,
|
|
463
|
+
index: number,
|
|
464
|
+
): void {
|
|
465
|
+
if (
|
|
466
|
+
!reviewer ||
|
|
467
|
+
typeof reviewer !== "object" ||
|
|
468
|
+
typeof reviewer.specialist !== "string" ||
|
|
469
|
+
reviewer.specialist.length < 1 ||
|
|
470
|
+
reviewer.specialist.length > 100 ||
|
|
471
|
+
!["approve", "revise", "attention"].includes(reviewer.verdict as SupervisorPlanReviewVerdict) ||
|
|
472
|
+
typeof reviewer.rationale !== "string" ||
|
|
473
|
+
reviewer.rationale.length < 1 ||
|
|
474
|
+
reviewer.rationale.length > 2_000 ||
|
|
475
|
+
!Array.isArray(reviewer.findings) ||
|
|
476
|
+
reviewer.findings.length > 50
|
|
477
|
+
) {
|
|
478
|
+
throw new Error(`supervisor plan review ${planId} round ${round} reviewer ${index} is invalid`);
|
|
479
|
+
}
|
|
480
|
+
reviewer.findings.forEach((finding, findingIndex) => {
|
|
481
|
+
validateReviewFinding(finding, planId, round, index, findingIndex);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function validateReviewFinding(
|
|
486
|
+
finding: SupervisorPlanReviewFinding,
|
|
487
|
+
planId: string,
|
|
488
|
+
round: number,
|
|
489
|
+
reviewerIndex: number,
|
|
490
|
+
findingIndex: number,
|
|
491
|
+
): void {
|
|
492
|
+
if (
|
|
493
|
+
!finding ||
|
|
494
|
+
typeof finding !== "object" ||
|
|
495
|
+
typeof finding.code !== "string" ||
|
|
496
|
+
!/^[a-z][a-z0-9._-]{0,99}$/.test(finding.code) ||
|
|
497
|
+
!["blocking", "advisory"].includes(finding.severity) ||
|
|
498
|
+
typeof finding.summary !== "string" ||
|
|
499
|
+
finding.summary.length < 1 ||
|
|
500
|
+
finding.summary.length > 1_000 ||
|
|
501
|
+
typeof finding.recommendation !== "string" ||
|
|
502
|
+
finding.recommendation.length < 1 ||
|
|
503
|
+
finding.recommendation.length > 1_000
|
|
504
|
+
) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`supervisor plan review ${planId} round ${round} reviewer ${reviewerIndex} finding ${findingIndex} is invalid`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function summarizeReview(receipt: SupervisorPlanReviewReceipt): SupervisorPlanReviewSummary {
|
|
512
|
+
const findings = receipt.rounds.flatMap((round) =>
|
|
513
|
+
round.reviewers.flatMap((reviewer) => reviewer.findings),
|
|
514
|
+
);
|
|
515
|
+
return {
|
|
516
|
+
status: receipt.status,
|
|
517
|
+
candidate_sha256: receipt.candidate_sha256,
|
|
518
|
+
rounds: receipt.rounds.length,
|
|
519
|
+
blocking_findings: findings.filter((finding) => finding.severity === "blocking").length,
|
|
520
|
+
advisory_findings: findings.filter((finding) => finding.severity === "advisory").length,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
291
524
|
function validateEvent(event: SupervisorPlanEvent, planId: string, sequence: number): void {
|
|
292
525
|
if (
|
|
293
526
|
event.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const SUPERVISOR_PLAN_SCHEMA_VERSION = 1 as const;
|
|
2
|
+
export const MAX_SUPERVISOR_PLAN_REVIEWERS = 5 as const;
|
|
3
|
+
export const MAX_SUPERVISOR_PLAN_REVISION_ROUNDS = 3 as const;
|
|
2
4
|
|
|
3
5
|
export interface SupervisorPlanTemplate {
|
|
4
6
|
workflow: { path: string; sha256: string };
|
|
@@ -13,6 +15,10 @@ export interface SupervisorReplanningPolicy {
|
|
|
13
15
|
max_work_items_per_plan: number;
|
|
14
16
|
max_total_work_items: number;
|
|
15
17
|
templates: Record<string, SupervisorPlanTemplate>;
|
|
18
|
+
review?: {
|
|
19
|
+
reviewer_specialists: string[];
|
|
20
|
+
max_revision_rounds: number;
|
|
21
|
+
};
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export interface CreateSupervisorPlanTemplateInput {
|
|
@@ -28,6 +34,10 @@ export interface CreateSupervisorReplanningInput {
|
|
|
28
34
|
maxWorkItemsPerPlan?: number;
|
|
29
35
|
maxTotalWorkItems?: number;
|
|
30
36
|
templates: Readonly<Record<string, CreateSupervisorPlanTemplateInput>>;
|
|
37
|
+
review?: {
|
|
38
|
+
reviewerSpecialists: readonly string[];
|
|
39
|
+
maxRevisionRounds: number;
|
|
40
|
+
};
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export interface SupervisorPlanRequest {
|
|
@@ -72,6 +82,7 @@ export interface SupervisorPlanProposal {
|
|
|
72
82
|
export type SupervisorPlanEventType =
|
|
73
83
|
| "plan.awaiting_approval"
|
|
74
84
|
| "plan.resumed"
|
|
85
|
+
| "plan.reviewed"
|
|
75
86
|
| "plan.proposed"
|
|
76
87
|
| "plan.applied"
|
|
77
88
|
| "plan.completed"
|
|
@@ -79,6 +90,49 @@ export type SupervisorPlanEventType =
|
|
|
79
90
|
| "plan.attention"
|
|
80
91
|
| "plan.failed";
|
|
81
92
|
|
|
93
|
+
export type SupervisorPlanReviewStatus = "passed" | "revision_exhausted" | "attention" | "failed";
|
|
94
|
+
|
|
95
|
+
export type SupervisorPlanReviewVerdict = "approve" | "revise" | "attention";
|
|
96
|
+
|
|
97
|
+
export interface SupervisorPlanReviewFinding {
|
|
98
|
+
code: string;
|
|
99
|
+
severity: "blocking" | "advisory";
|
|
100
|
+
summary: string;
|
|
101
|
+
recommendation: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SupervisorPlanReviewReviewer {
|
|
105
|
+
specialist: string;
|
|
106
|
+
verdict: SupervisorPlanReviewVerdict;
|
|
107
|
+
rationale: string;
|
|
108
|
+
findings: SupervisorPlanReviewFinding[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface SupervisorPlanReviewRound {
|
|
112
|
+
round: number;
|
|
113
|
+
candidate_sha256: string;
|
|
114
|
+
reviewers: SupervisorPlanReviewReviewer[];
|
|
115
|
+
outcome: "approved" | "revise" | "attention" | "failed";
|
|
116
|
+
revision_workflow_run_id?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface SupervisorPlanReviewReceipt {
|
|
120
|
+
schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
|
|
121
|
+
plan_id: string;
|
|
122
|
+
status: SupervisorPlanReviewStatus;
|
|
123
|
+
candidate_sha256: string;
|
|
124
|
+
final_candidate: SupervisorPlanProposal;
|
|
125
|
+
rounds: SupervisorPlanReviewRound[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface SupervisorPlanReviewSummary {
|
|
129
|
+
status: SupervisorPlanReviewStatus;
|
|
130
|
+
candidate_sha256: string;
|
|
131
|
+
rounds: number;
|
|
132
|
+
blocking_findings: number;
|
|
133
|
+
advisory_findings: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
82
136
|
export interface SupervisorPlanEvent {
|
|
83
137
|
schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
|
|
84
138
|
plan_id: string;
|
|
@@ -106,6 +160,7 @@ export type SupervisorPlanStatus =
|
|
|
106
160
|
export interface SupervisorPlanRecord {
|
|
107
161
|
request: SupervisorPlanRequest;
|
|
108
162
|
proposal?: SupervisorPlanProposal;
|
|
163
|
+
review?: SupervisorPlanReviewSummary;
|
|
109
164
|
events: SupervisorPlanEvent[];
|
|
110
165
|
status: SupervisorPlanStatus;
|
|
111
166
|
approval_id?: string;
|