harnery 0.15.0 → 0.17.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/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +393 -4
- package/dist/core/supervisor/index.d.ts +4 -0
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/index.js +4 -0
- package/dist/core/supervisor/plan-api.d.ts +16 -0
- package/dist/core/supervisor/plan-api.d.ts.map +1 -0
- package/dist/core/supervisor/plan-api.js +34 -0
- package/dist/core/supervisor/plan-read.d.ts +4 -0
- package/dist/core/supervisor/plan-read.d.ts.map +1 -0
- package/dist/core/supervisor/plan-read.js +269 -0
- package/dist/core/supervisor/plan-types.d.ts +113 -0
- package/dist/core/supervisor/plan-types.d.ts.map +1 -0
- package/dist/core/supervisor/plan-types.js +14 -0
- package/dist/core/supervisor/planning.d.ts +26 -0
- package/dist/core/supervisor/planning.d.ts.map +1 -0
- package/dist/core/supervisor/planning.js +486 -0
- package/dist/core/supervisor/read.d.ts +4 -0
- package/dist/core/supervisor/read.d.ts.map +1 -1
- package/dist/core/supervisor/read.js +3 -0
- package/dist/core/supervisor/runner.d.ts +3 -0
- package/dist/core/supervisor/runner.d.ts.map +1 -1
- package/dist/core/supervisor/runner.js +37 -7
- package/dist/core/supervisor/service-read.d.ts +6 -0
- package/dist/core/supervisor/service-read.d.ts.map +1 -0
- package/dist/core/supervisor/service-read.js +127 -0
- package/dist/core/supervisor/service.d.ts +114 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +706 -0
- package/dist/core/supervisor/state.d.ts +11 -1
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +186 -7
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +6 -1
- package/package.json +1 -1
- package/src/commands/supervisor.ts +490 -3
- package/src/core/supervisor/index.ts +45 -0
- package/src/core/supervisor/plan-api.ts +47 -0
- package/src/core/supervisor/plan-read.ts +299 -0
- package/src/core/supervisor/plan-types.ts +144 -0
- package/src/core/supervisor/planning.ts +576 -0
- package/src/core/supervisor/read.ts +30 -0
- package/src/core/supervisor/runner.ts +39 -2
- package/src/core/supervisor/service-read.ts +144 -0
- package/src/core/supervisor/service.ts +927 -0
- package/src/core/supervisor/state.ts +235 -6
- package/src/lib/tunnel/gate.ts +7 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
SUPERVISOR_PLAN_SCHEMA_VERSION,
|
|
5
|
+
type SupervisorPlanEvent,
|
|
6
|
+
type SupervisorPlanEventType,
|
|
7
|
+
type SupervisorPlanHistory,
|
|
8
|
+
type SupervisorPlanProposal,
|
|
9
|
+
type SupervisorPlanRecord,
|
|
10
|
+
type SupervisorPlanRequest,
|
|
11
|
+
} from "./plan-types.ts";
|
|
12
|
+
|
|
13
|
+
const PLAN_ID = /^plan-[0-9]{4}-[a-f0-9]{8}$/;
|
|
14
|
+
const GOAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
15
|
+
const WORK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
16
|
+
const APPROVAL_ID = /^apr-[a-f0-9]{20}$/;
|
|
17
|
+
const PLAN_KEY = /^[a-z][a-z0-9-]{0,31}$/;
|
|
18
|
+
const TEMPLATE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
19
|
+
const MAX_PLANS = 100;
|
|
20
|
+
const MAX_RECORD_BYTES = 256 * 1024;
|
|
21
|
+
const MAX_EVENTS_BYTES = 512 * 1024;
|
|
22
|
+
const MAX_EVENTS = 100;
|
|
23
|
+
const EVENT_TYPES = new Set<SupervisorPlanEventType>([
|
|
24
|
+
"plan.awaiting_approval",
|
|
25
|
+
"plan.resumed",
|
|
26
|
+
"plan.proposed",
|
|
27
|
+
"plan.applied",
|
|
28
|
+
"plan.rejected",
|
|
29
|
+
"plan.attention",
|
|
30
|
+
"plan.failed",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export function readSupervisorPlans(
|
|
34
|
+
coordRootRaw: string,
|
|
35
|
+
goalId: string,
|
|
36
|
+
originalRootWorkId: string,
|
|
37
|
+
): SupervisorPlanHistory {
|
|
38
|
+
assertId(goalId, GOAL_ID, "supervisor id");
|
|
39
|
+
assertId(originalRootWorkId, WORK_ID, "work id");
|
|
40
|
+
const coordRoot = resolve(coordRootRaw);
|
|
41
|
+
const root = plansRoot(coordRoot, goalId);
|
|
42
|
+
if (!existsSync(root)) {
|
|
43
|
+
return {
|
|
44
|
+
plans: [],
|
|
45
|
+
active_root_work_id: originalRootWorkId,
|
|
46
|
+
generation: 0,
|
|
47
|
+
applied_work_ids: [],
|
|
48
|
+
materialized_work_ids: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const entries = readdirSync(root, { withFileTypes: true })
|
|
52
|
+
.filter((entry) => entry.isDirectory() && PLAN_ID.test(entry.name))
|
|
53
|
+
.map((entry) => entry.name);
|
|
54
|
+
if (entries.length > MAX_PLANS) throw new Error(`supervisor plan history exceeds ${MAX_PLANS}`);
|
|
55
|
+
const plans = entries
|
|
56
|
+
.map((planId) => readSupervisorPlan(coordRootRaw, goalId, planId))
|
|
57
|
+
.sort((left, right) => left.request.sequence - right.request.sequence);
|
|
58
|
+
const sequences = new Set<number>();
|
|
59
|
+
for (const plan of plans) {
|
|
60
|
+
if (sequences.has(plan.request.sequence)) {
|
|
61
|
+
throw new Error(`supervisor ${goalId} has duplicate plan sequence ${plan.request.sequence}`);
|
|
62
|
+
}
|
|
63
|
+
sequences.add(plan.request.sequence);
|
|
64
|
+
}
|
|
65
|
+
let activeRoot = originalRootWorkId;
|
|
66
|
+
let generation = 0;
|
|
67
|
+
const appliedWork = new Set<string>();
|
|
68
|
+
const materializedWork = new Set<string>();
|
|
69
|
+
for (const plan of plans) {
|
|
70
|
+
for (const spec of plan.proposal?.work ?? []) {
|
|
71
|
+
const workId = `${plan.request.id}-${spec.key}`;
|
|
72
|
+
if (existsSync(join(coordRoot, ".harnery", "work", workId, "intent.json"))) {
|
|
73
|
+
materializedWork.add(workId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (plan.status !== "applied" || !plan.root_work_id) continue;
|
|
77
|
+
activeRoot = plan.root_work_id;
|
|
78
|
+
generation++;
|
|
79
|
+
for (const workId of plan.work_ids) appliedWork.add(workId);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
plans,
|
|
83
|
+
active_root_work_id: activeRoot,
|
|
84
|
+
generation,
|
|
85
|
+
applied_work_ids: [...appliedWork],
|
|
86
|
+
materialized_work_ids: [...materializedWork],
|
|
87
|
+
latest: plans.at(-1),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readSupervisorPlan(
|
|
92
|
+
coordRootRaw: string,
|
|
93
|
+
goalId: string,
|
|
94
|
+
planId: string,
|
|
95
|
+
): SupervisorPlanRecord {
|
|
96
|
+
assertId(goalId, GOAL_ID, "supervisor id");
|
|
97
|
+
assertId(planId, PLAN_ID, "supervisor plan id");
|
|
98
|
+
const coordRoot = resolve(coordRootRaw);
|
|
99
|
+
const dir = planDir(coordRoot, goalId, planId);
|
|
100
|
+
const request = readJson<SupervisorPlanRequest>(join(dir, "request.json"), "plan request");
|
|
101
|
+
validateRequest(request, goalId, planId);
|
|
102
|
+
const proposalPath = join(dir, "proposal.json");
|
|
103
|
+
const proposal = existsSync(proposalPath)
|
|
104
|
+
? readJson<SupervisorPlanProposal>(proposalPath, "plan proposal")
|
|
105
|
+
: undefined;
|
|
106
|
+
if (proposal) validateProposalEnvelope(proposal, planId);
|
|
107
|
+
const events = readEvents(join(dir, "events.jsonl"), planId);
|
|
108
|
+
const derived = deriveStatus(coordRoot, events, proposal);
|
|
109
|
+
return { request, proposal, events, ...derived };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function deriveStatus(
|
|
113
|
+
coordRoot: string,
|
|
114
|
+
events: SupervisorPlanEvent[],
|
|
115
|
+
proposal: SupervisorPlanProposal | undefined,
|
|
116
|
+
): Pick<SupervisorPlanRecord, "status" | "approval_id" | "root_work_id" | "work_ids" | "reason"> {
|
|
117
|
+
const latest = events.at(-1);
|
|
118
|
+
const applied = [...events].reverse().find((event) => event.event === "plan.applied");
|
|
119
|
+
if (applied) {
|
|
120
|
+
return {
|
|
121
|
+
status: "applied",
|
|
122
|
+
root_work_id: applied.root_work_id,
|
|
123
|
+
work_ids: applied.work_ids ?? [],
|
|
124
|
+
reason: applied.reason,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (latest?.event === "plan.rejected") {
|
|
128
|
+
return { status: "rejected", work_ids: [], reason: latest.reason };
|
|
129
|
+
}
|
|
130
|
+
if (latest?.event === "plan.attention") {
|
|
131
|
+
return { status: "attention", work_ids: [], reason: latest.reason };
|
|
132
|
+
}
|
|
133
|
+
if (latest?.event === "plan.failed") {
|
|
134
|
+
return { status: "failed", work_ids: [], reason: latest.reason };
|
|
135
|
+
}
|
|
136
|
+
const parked = [...events].reverse().find((event) => event.event === "plan.awaiting_approval");
|
|
137
|
+
const resumedAfterPark =
|
|
138
|
+
parked !== undefined &&
|
|
139
|
+
events.some((event) => event.event === "plan.resumed" && event.seq > parked.seq);
|
|
140
|
+
if (parked?.approval_id && !resumedAfterPark) {
|
|
141
|
+
return {
|
|
142
|
+
status: approvalIsResolved(coordRoot, parked.approval_id) ? "resumable" : "awaiting_approval",
|
|
143
|
+
approval_id: parked.approval_id,
|
|
144
|
+
work_ids: [],
|
|
145
|
+
reason: parked.reason,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (proposal) return { status: "proposed", work_ids: [], reason: proposal.rationale };
|
|
149
|
+
return { status: "interrupted", work_ids: [], reason: latest?.reason };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readEvents(path: string, planId: string): SupervisorPlanEvent[] {
|
|
153
|
+
if (!existsSync(path)) return [];
|
|
154
|
+
const size = statSync(path).size;
|
|
155
|
+
if (size > MAX_EVENTS_BYTES) throw new Error("supervisor plan events exceed their byte limit");
|
|
156
|
+
const body = readFileSync(path, "utf8");
|
|
157
|
+
if (body && !body.endsWith("\n"))
|
|
158
|
+
throw new Error(`supervisor plan ${planId} has a partial event`);
|
|
159
|
+
const lines = body.split("\n").filter(Boolean);
|
|
160
|
+
if (lines.length > MAX_EVENTS) throw new Error(`supervisor plan exceeds ${MAX_EVENTS} events`);
|
|
161
|
+
return lines.map((line, index) => {
|
|
162
|
+
let event: SupervisorPlanEvent;
|
|
163
|
+
try {
|
|
164
|
+
event = JSON.parse(line) as SupervisorPlanEvent;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`cannot parse supervisor plan ${planId} event ${index + 1}: ${(error as Error).message}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
validateEvent(event, planId, index + 1);
|
|
171
|
+
return event;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateRequest(request: SupervisorPlanRequest, goalId: string, planId: string): void {
|
|
176
|
+
if (
|
|
177
|
+
request.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
|
|
178
|
+
request.id !== planId ||
|
|
179
|
+
request.goal_id !== goalId ||
|
|
180
|
+
!Number.isSafeInteger(request.sequence) ||
|
|
181
|
+
request.sequence < 1 ||
|
|
182
|
+
request.sequence > MAX_PLANS ||
|
|
183
|
+
typeof request.trigger_fingerprint !== "string" ||
|
|
184
|
+
request.trigger_fingerprint.length < 1 ||
|
|
185
|
+
request.trigger_fingerprint.length > 64_000 ||
|
|
186
|
+
!WORK_ID.test(request.prior_root_work_id) ||
|
|
187
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(request.workflow_run_id) ||
|
|
188
|
+
!validTimestamp(request.created_at)
|
|
189
|
+
) {
|
|
190
|
+
throw new Error(`supervisor plan request ${planId} has an unsupported schema`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateProposalEnvelope(proposal: SupervisorPlanProposal, planId: string): void {
|
|
195
|
+
if (
|
|
196
|
+
proposal.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
|
|
197
|
+
proposal.plan_id !== planId ||
|
|
198
|
+
!["apply", "attention"].includes(proposal.decision) ||
|
|
199
|
+
typeof proposal.rationale !== "string" ||
|
|
200
|
+
proposal.rationale.length < 1 ||
|
|
201
|
+
proposal.rationale.length > 2_000 ||
|
|
202
|
+
!Array.isArray(proposal.work) ||
|
|
203
|
+
proposal.work.length > 25 ||
|
|
204
|
+
typeof proposal.root !== "string" ||
|
|
205
|
+
proposal.root.length > 32 ||
|
|
206
|
+
!validTimestamp(proposal.proposed_at)
|
|
207
|
+
) {
|
|
208
|
+
throw new Error(`supervisor plan proposal ${planId} has an unsupported schema`);
|
|
209
|
+
}
|
|
210
|
+
for (const [index, spec] of proposal.work.entries()) {
|
|
211
|
+
if (
|
|
212
|
+
!spec ||
|
|
213
|
+
typeof spec !== "object" ||
|
|
214
|
+
!PLAN_KEY.test(spec.key) ||
|
|
215
|
+
typeof spec.title !== "string" ||
|
|
216
|
+
spec.title.length < 1 ||
|
|
217
|
+
spec.title.length > 200 ||
|
|
218
|
+
typeof spec.objective !== "string" ||
|
|
219
|
+
spec.objective.length < 1 ||
|
|
220
|
+
spec.objective.length > 4_000 ||
|
|
221
|
+
!Array.isArray(spec.acceptance) ||
|
|
222
|
+
spec.acceptance.length > 50 ||
|
|
223
|
+
spec.acceptance.some((value) => typeof value !== "string" || value.length > 500) ||
|
|
224
|
+
!Array.isArray(spec.dependencies) ||
|
|
225
|
+
spec.dependencies.length > 50 ||
|
|
226
|
+
spec.dependencies.some((value) => typeof value !== "string" || value.length > 100) ||
|
|
227
|
+
!TEMPLATE_ID.test(spec.template)
|
|
228
|
+
) {
|
|
229
|
+
throw new Error(`supervisor plan proposal ${planId} work[${index}] is invalid`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function validateEvent(event: SupervisorPlanEvent, planId: string, sequence: number): void {
|
|
235
|
+
if (
|
|
236
|
+
event.schema_version !== SUPERVISOR_PLAN_SCHEMA_VERSION ||
|
|
237
|
+
event.plan_id !== planId ||
|
|
238
|
+
event.seq !== sequence ||
|
|
239
|
+
!EVENT_TYPES.has(event.event) ||
|
|
240
|
+
!validTimestamp(event.ts) ||
|
|
241
|
+
typeof event.actor !== "string" ||
|
|
242
|
+
event.actor.length < 1 ||
|
|
243
|
+
event.actor.length > 200 ||
|
|
244
|
+
typeof event.reason !== "string" ||
|
|
245
|
+
event.reason.length < 1 ||
|
|
246
|
+
event.reason.length > 2_000
|
|
247
|
+
) {
|
|
248
|
+
throw new Error(`supervisor plan ${planId} event ${sequence} has an unsupported schema`);
|
|
249
|
+
}
|
|
250
|
+
if (event.approval_id !== undefined && !APPROVAL_ID.test(event.approval_id)) {
|
|
251
|
+
throw new Error(`supervisor plan ${planId} event ${sequence} has an invalid approval id`);
|
|
252
|
+
}
|
|
253
|
+
if (event.root_work_id !== undefined && !WORK_ID.test(event.root_work_id)) {
|
|
254
|
+
throw new Error(`supervisor plan ${planId} event ${sequence} has an invalid root work id`);
|
|
255
|
+
}
|
|
256
|
+
if (event.work_ids !== undefined) {
|
|
257
|
+
if (!Array.isArray(event.work_ids) || event.work_ids.some((id) => !WORK_ID.test(id))) {
|
|
258
|
+
throw new Error(`supervisor plan ${planId} event ${sequence} has invalid work ids`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function approvalIsResolved(coordRoot: string, approvalId: string): boolean {
|
|
264
|
+
const path = join(coordRoot, ".harnery", "approvals", approvalId, "decision.json");
|
|
265
|
+
if (!existsSync(path)) return false;
|
|
266
|
+
try {
|
|
267
|
+
const value = readJson<Record<string, unknown>>(path, "workflow approval decision");
|
|
268
|
+
return value.approval_id === approvalId && ["allow", "deny"].includes(String(value.verdict));
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readJson<T>(path: string, label: string): T {
|
|
275
|
+
if (!existsSync(path)) throw new Error(`${label} does not exist at ${path}`);
|
|
276
|
+
const size = statSync(path).size;
|
|
277
|
+
if (size <= 0 || size > MAX_RECORD_BYTES) throw new Error(`${label} has invalid size ${size}`);
|
|
278
|
+
try {
|
|
279
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw new Error(`cannot parse ${label} at ${path}: ${(error as Error).message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function plansRoot(coordRoot: string, goalId: string): string {
|
|
286
|
+
return join(coordRoot, ".harnery", "supervisors", goalId, "plans");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function planDir(coordRoot: string, goalId: string, planId: string): string {
|
|
290
|
+
return join(plansRoot(coordRoot, goalId), planId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function assertId(value: string, pattern: RegExp, label: string): void {
|
|
294
|
+
if (!pattern.test(value)) throw new Error(`invalid ${label} ${JSON.stringify(value)}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function validTimestamp(value: unknown): value is string {
|
|
298
|
+
return typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
|
|
299
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export const SUPERVISOR_PLAN_SCHEMA_VERSION = 1 as const;
|
|
2
|
+
|
|
3
|
+
export interface SupervisorPlanTemplate {
|
|
4
|
+
workflow: { path: string; sha256: string };
|
|
5
|
+
max_attempts: number;
|
|
6
|
+
root: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SupervisorReplanningPolicy {
|
|
10
|
+
planner_specialist: string;
|
|
11
|
+
auto_apply: boolean;
|
|
12
|
+
max_replans: number;
|
|
13
|
+
max_work_items_per_plan: number;
|
|
14
|
+
max_total_work_items: number;
|
|
15
|
+
templates: Record<string, SupervisorPlanTemplate>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateSupervisorPlanTemplateInput {
|
|
19
|
+
workflowPath: string;
|
|
20
|
+
maxAttempts?: number;
|
|
21
|
+
root?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CreateSupervisorReplanningInput {
|
|
25
|
+
plannerSpecialist: string;
|
|
26
|
+
autoApply?: boolean;
|
|
27
|
+
maxReplans?: number;
|
|
28
|
+
maxWorkItemsPerPlan?: number;
|
|
29
|
+
maxTotalWorkItems?: number;
|
|
30
|
+
templates: Readonly<Record<string, CreateSupervisorPlanTemplateInput>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SupervisorPlanRequest {
|
|
34
|
+
schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
|
|
35
|
+
id: string;
|
|
36
|
+
goal_id: string;
|
|
37
|
+
sequence: number;
|
|
38
|
+
trigger_fingerprint: string;
|
|
39
|
+
prior_root_work_id: string;
|
|
40
|
+
workflow_run_id: string;
|
|
41
|
+
created_at: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SupervisorPlanWorkSpec {
|
|
45
|
+
key: string;
|
|
46
|
+
title: string;
|
|
47
|
+
objective: string;
|
|
48
|
+
acceptance: string[];
|
|
49
|
+
dependencies: string[];
|
|
50
|
+
template: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SupervisorPlanProposal {
|
|
54
|
+
schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
|
|
55
|
+
plan_id: string;
|
|
56
|
+
decision: "apply" | "attention";
|
|
57
|
+
rationale: string;
|
|
58
|
+
root: string;
|
|
59
|
+
work: SupervisorPlanWorkSpec[];
|
|
60
|
+
proposed_at: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SupervisorPlanEventType =
|
|
64
|
+
| "plan.awaiting_approval"
|
|
65
|
+
| "plan.resumed"
|
|
66
|
+
| "plan.proposed"
|
|
67
|
+
| "plan.applied"
|
|
68
|
+
| "plan.rejected"
|
|
69
|
+
| "plan.attention"
|
|
70
|
+
| "plan.failed";
|
|
71
|
+
|
|
72
|
+
export interface SupervisorPlanEvent {
|
|
73
|
+
schema_version: typeof SUPERVISOR_PLAN_SCHEMA_VERSION;
|
|
74
|
+
plan_id: string;
|
|
75
|
+
seq: number;
|
|
76
|
+
ts: string;
|
|
77
|
+
event: SupervisorPlanEventType;
|
|
78
|
+
actor: string;
|
|
79
|
+
reason: string;
|
|
80
|
+
approval_id?: string;
|
|
81
|
+
root_work_id?: string;
|
|
82
|
+
work_ids?: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SupervisorPlanStatus =
|
|
86
|
+
| "interrupted"
|
|
87
|
+
| "awaiting_approval"
|
|
88
|
+
| "resumable"
|
|
89
|
+
| "proposed"
|
|
90
|
+
| "applied"
|
|
91
|
+
| "rejected"
|
|
92
|
+
| "attention"
|
|
93
|
+
| "failed";
|
|
94
|
+
|
|
95
|
+
export interface SupervisorPlanRecord {
|
|
96
|
+
request: SupervisorPlanRequest;
|
|
97
|
+
proposal?: SupervisorPlanProposal;
|
|
98
|
+
events: SupervisorPlanEvent[];
|
|
99
|
+
status: SupervisorPlanStatus;
|
|
100
|
+
approval_id?: string;
|
|
101
|
+
root_work_id?: string;
|
|
102
|
+
work_ids: string[];
|
|
103
|
+
reason?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface SupervisorPlanHistory {
|
|
107
|
+
plans: SupervisorPlanRecord[];
|
|
108
|
+
active_root_work_id: string;
|
|
109
|
+
generation: number;
|
|
110
|
+
applied_work_ids: string[];
|
|
111
|
+
materialized_work_ids: string[];
|
|
112
|
+
latest?: SupervisorPlanRecord;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SupervisorPlanOutcome {
|
|
116
|
+
plan_id: string;
|
|
117
|
+
status: SupervisorPlanStatus;
|
|
118
|
+
workflow_run_id: string;
|
|
119
|
+
reason?: string;
|
|
120
|
+
root_work_id?: string;
|
|
121
|
+
work_ids: string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function supervisorGraphFingerprint(input: {
|
|
125
|
+
rootWorkId: string;
|
|
126
|
+
generation: number;
|
|
127
|
+
work: ReadonlyArray<{
|
|
128
|
+
intent: { id: string };
|
|
129
|
+
projection: { state: string; next_action: string; attempts_used: number };
|
|
130
|
+
events: readonly unknown[];
|
|
131
|
+
}>;
|
|
132
|
+
}): string {
|
|
133
|
+
return JSON.stringify([
|
|
134
|
+
input.rootWorkId,
|
|
135
|
+
input.generation,
|
|
136
|
+
input.work.map((work) => [
|
|
137
|
+
work.intent.id,
|
|
138
|
+
work.projection.state,
|
|
139
|
+
work.projection.next_action,
|
|
140
|
+
work.projection.attempts_used,
|
|
141
|
+
work.events.length,
|
|
142
|
+
]),
|
|
143
|
+
]);
|
|
144
|
+
}
|