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