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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/supervisor.d.ts.map +1 -1
  3. package/dist/commands/supervisor.js +393 -4
  4. package/dist/core/supervisor/index.d.ts +4 -0
  5. package/dist/core/supervisor/index.d.ts.map +1 -1
  6. package/dist/core/supervisor/index.js +4 -0
  7. package/dist/core/supervisor/plan-api.d.ts +16 -0
  8. package/dist/core/supervisor/plan-api.d.ts.map +1 -0
  9. package/dist/core/supervisor/plan-api.js +34 -0
  10. package/dist/core/supervisor/plan-read.d.ts +4 -0
  11. package/dist/core/supervisor/plan-read.d.ts.map +1 -0
  12. package/dist/core/supervisor/plan-read.js +269 -0
  13. package/dist/core/supervisor/plan-types.d.ts +113 -0
  14. package/dist/core/supervisor/plan-types.d.ts.map +1 -0
  15. package/dist/core/supervisor/plan-types.js +14 -0
  16. package/dist/core/supervisor/planning.d.ts +26 -0
  17. package/dist/core/supervisor/planning.d.ts.map +1 -0
  18. package/dist/core/supervisor/planning.js +486 -0
  19. package/dist/core/supervisor/read.d.ts +4 -0
  20. package/dist/core/supervisor/read.d.ts.map +1 -1
  21. package/dist/core/supervisor/read.js +3 -0
  22. package/dist/core/supervisor/runner.d.ts +3 -0
  23. package/dist/core/supervisor/runner.d.ts.map +1 -1
  24. package/dist/core/supervisor/runner.js +37 -7
  25. package/dist/core/supervisor/service-read.d.ts +6 -0
  26. package/dist/core/supervisor/service-read.d.ts.map +1 -0
  27. package/dist/core/supervisor/service-read.js +127 -0
  28. package/dist/core/supervisor/service.d.ts +114 -0
  29. package/dist/core/supervisor/service.d.ts.map +1 -0
  30. package/dist/core/supervisor/service.js +706 -0
  31. package/dist/core/supervisor/state.d.ts +11 -1
  32. package/dist/core/supervisor/state.d.ts.map +1 -1
  33. package/dist/core/supervisor/state.js +186 -7
  34. package/dist/lib/tunnel/gate.d.ts.map +1 -1
  35. package/dist/lib/tunnel/gate.js +6 -1
  36. package/package.json +1 -1
  37. package/src/commands/supervisor.ts +490 -3
  38. package/src/core/supervisor/index.ts +45 -0
  39. package/src/core/supervisor/plan-api.ts +47 -0
  40. package/src/core/supervisor/plan-read.ts +299 -0
  41. package/src/core/supervisor/plan-types.ts +144 -0
  42. package/src/core/supervisor/planning.ts +576 -0
  43. package/src/core/supervisor/read.ts +30 -0
  44. package/src/core/supervisor/runner.ts +39 -2
  45. package/src/core/supervisor/service-read.ts +144 -0
  46. package/src/core/supervisor/service.ts +927 -0
  47. package/src/core/supervisor/state.ts +235 -6
  48. package/src/lib/tunnel/gate.ts +7 -1
@@ -1,5 +1,6 @@
1
1
  import { type WorkRecord } from "../work/read.js";
2
2
  import type { WorkflowSpecialistProfile } from "../workflow/types.js";
3
+ import { type CreateSupervisorReplanningInput, type SupervisorPlanRecord, type SupervisorReplanningPolicy } from "./plan-types.js";
3
4
  export declare const SUPERVISOR_INTENT_SCHEMA_VERSION: 1;
4
5
  export interface SupervisorLimits {
5
6
  max_cycles: number;
@@ -22,10 +23,11 @@ export interface SupervisorIntent {
22
23
  specialists: Record<string, WorkflowSpecialistProfile>;
23
24
  limits: SupervisorLimits;
24
25
  automation: SupervisorAutomationPolicy;
26
+ replanning?: SupervisorReplanningPolicy;
25
27
  created_at: string;
26
28
  }
27
29
  export type SupervisorState = "ready" | "running" | "awaiting_attention" | "blocked" | "budget_exhausted" | "succeeded";
28
- export type SupervisorNextAction = "run" | "wait_for_run" | "resolve_approval" | "review" | "retry" | "none";
30
+ export type SupervisorNextAction = "run" | "wait_for_run" | "resolve_approval" | "review" | "retry" | "replan" | "review_plan" | "none";
29
31
  export interface SupervisorProjection {
30
32
  id: string;
31
33
  title: string;
@@ -41,11 +43,18 @@ export interface SupervisorProjection {
41
43
  attempts_used: number;
42
44
  attempts_remaining: number;
43
45
  specialists: string[];
46
+ plan_generation: number;
47
+ replans_used: number;
48
+ replans_remaining: number;
49
+ pending_plan_id?: string;
50
+ latest_plan_status?: SupervisorPlanRecord["status"];
51
+ governed_work_ids: string[];
44
52
  }
45
53
  export interface SupervisorRecord {
46
54
  intent: SupervisorIntent;
47
55
  projection: SupervisorProjection;
48
56
  work: WorkRecord[];
57
+ plans: SupervisorPlanRecord[];
49
58
  }
50
59
  export interface CreateSupervisorInput {
51
60
  coordRoot: string;
@@ -55,6 +64,7 @@ export interface CreateSupervisorInput {
55
64
  id?: string;
56
65
  limits?: Partial<SupervisorLimits>;
57
66
  automation?: Partial<SupervisorAutomationPolicy>;
67
+ replanning?: CreateSupervisorReplanningInput;
58
68
  }
59
69
  export declare function createSupervisor(input: CreateSupervisorInput): SupervisorRecord;
60
70
  export declare function readSupervisor(coordRoot: string, goalId: string): SupervisorRecord;
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/state.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/state.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,18 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { hostname } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { assertWorkId, readWorkItem } from "../work/read.js";
6
+ import { workflowScriptDigest } from "../workflow/run-state.js";
6
7
  import { normalizeWorkflowSpecialists } from "../workflow/specialists.js";
8
+ import { readSupervisorPlans } from "./plan-read.js";
9
+ import { supervisorGraphFingerprint, } from "./plan-types.js";
7
10
  export const SUPERVISOR_INTENT_SCHEMA_VERSION = 1;
8
11
  const GOAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
9
12
  const MAX_TITLE = 200;
10
13
  const MAX_INTENT_BYTES = 256 * 1024;
14
+ const MAX_TEMPLATES = 20;
15
+ const TEMPLATE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
11
16
  const FOREIGN_LEASE_STALE_MS = 24 * 60 * 60 * 1_000;
12
17
  export function createSupervisor(input) {
13
18
  const coordRoot = resolve(input.coordRoot);
@@ -15,6 +20,9 @@ export function createSupervisor(input) {
15
20
  const root = readWorkItem(coordRoot, input.rootWorkId);
16
21
  const id = input.id ?? newSupervisorId();
17
22
  assertSupervisorId(id);
23
+ const replanning = input.replanning
24
+ ? normalizeReplanning(input.replanning, input.specialists)
25
+ : undefined;
18
26
  const intent = {
19
27
  schema_version: SUPERVISOR_INTENT_SCHEMA_VERSION,
20
28
  id,
@@ -23,6 +31,7 @@ export function createSupervisor(input) {
23
31
  specialists: normalizeWorkflowSpecialists(input.specialists),
24
32
  limits: normalizeLimits(input.limits),
25
33
  automation: normalizeAutomation(input.automation),
34
+ ...(replanning ? { replanning } : {}),
26
35
  created_at: new Date().toISOString(),
27
36
  };
28
37
  const path = supervisorIntentPath(coordRoot, id);
@@ -126,18 +135,21 @@ export function acquireSupervisorLease(coordRoot, goalId) {
126
135
  function readSupervisorInternal(coordRootRaw, goalId, ignoreLease) {
127
136
  const coordRoot = resolve(coordRootRaw);
128
137
  const intent = readSupervisorIntent(coordRoot, goalId);
129
- const work = collectSupervisorWork(coordRoot, intent.root_work_id);
138
+ const plans = readSupervisorPlans(coordRoot, intent.id, intent.root_work_id);
139
+ const work = collectSupervisorWork(coordRoot, plans.active_root_work_id);
140
+ const governed = collectGovernedWork(coordRoot, intent.root_work_id, plans, work);
130
141
  return {
131
142
  intent,
132
143
  work,
133
- projection: deriveProjection(coordRoot, intent, work, ignoreLease),
144
+ plans: plans.plans,
145
+ projection: deriveProjection(coordRoot, intent, work, governed, plans, ignoreLease),
134
146
  };
135
147
  }
136
- function deriveProjection(coordRoot, intent, work, ignoreLease) {
137
- const root = work.find((record) => record.intent.id === intent.root_work_id);
148
+ function deriveProjection(coordRoot, intent, work, governed, plans, ignoreLease) {
149
+ const root = work.find((record) => record.intent.id === plans.active_root_work_id);
138
150
  if (!root)
139
151
  throw new Error(`supervisor ${intent.id} root work is missing`);
140
- const attemptsUsed = work.reduce((sum, record) => sum + record.projection.attempts_used, 0);
152
+ const attemptsUsed = governed.reduce((sum, record) => sum + record.projection.attempts_used, 0);
141
153
  const readyWork = work
142
154
  .filter((record) => record.projection.state === "ready")
143
155
  .map((record) => record.intent.id);
@@ -172,7 +184,7 @@ function deriveProjection(coordRoot, intent, work, ignoreLease) {
172
184
  const base = {
173
185
  id: intent.id,
174
186
  title: intent.title,
175
- root_work_id: intent.root_work_id,
187
+ root_work_id: plans.active_root_work_id,
176
188
  work_ids: work.map((record) => record.intent.id),
177
189
  ready_work: readyWork,
178
190
  resumable_work: resumableWork,
@@ -181,6 +193,12 @@ function deriveProjection(coordRoot, intent, work, ignoreLease) {
181
193
  attempts_used: attemptsUsed,
182
194
  attempts_remaining: Math.max(0, intent.limits.max_total_attempts - attemptsUsed),
183
195
  specialists: Object.keys(intent.specialists),
196
+ plan_generation: plans.generation,
197
+ replans_used: plans.plans.length,
198
+ replans_remaining: Math.max(0, (intent.replanning?.max_replans ?? 0) - plans.plans.length),
199
+ pending_plan_id: plans.latest?.status === "proposed" ? plans.latest.request.id : undefined,
200
+ latest_plan_status: plans.latest?.status,
201
+ governed_work_ids: governed.map((record) => record.intent.id),
184
202
  };
185
203
  if (!ignoreLease && supervisorLeaseIsLive(coordRoot, intent.id)) {
186
204
  return {
@@ -198,6 +216,30 @@ function deriveProjection(coordRoot, intent, work, ignoreLease) {
198
216
  next_action: "none",
199
217
  };
200
218
  }
219
+ if (plans.latest?.status === "proposed") {
220
+ return {
221
+ ...base,
222
+ state: "awaiting_attention",
223
+ reason: `replacement plan ${plans.latest.request.id} awaits explicit review`,
224
+ next_action: "review_plan",
225
+ };
226
+ }
227
+ if (plans.latest?.status === "awaiting_approval") {
228
+ return {
229
+ ...base,
230
+ state: "awaiting_attention",
231
+ reason: `planner workflow awaits approval ${plans.latest.approval_id}`,
232
+ next_action: "resolve_approval",
233
+ };
234
+ }
235
+ if (plans.latest?.status === "resumable") {
236
+ return {
237
+ ...base,
238
+ state: "ready",
239
+ reason: `planner workflow ${plans.latest.request.workflow_run_id} is resumable`,
240
+ next_action: "replan",
241
+ };
242
+ }
201
243
  const resumableDispatchable = intent.automation.resume_approved ? resumableWork : [];
202
244
  const attemptDispatchable = [
203
245
  ...readyWork,
@@ -268,6 +310,54 @@ function deriveProjection(coordRoot, intent, work, ignoreLease) {
268
310
  next_action: "retry",
269
311
  };
270
312
  }
313
+ if (attemptsUsed >= intent.limits.max_total_attempts && cancelled.length === 0) {
314
+ return {
315
+ ...base,
316
+ state: "budget_exhausted",
317
+ reason: `goal exhausted its ${intent.limits.max_total_attempts} total attempts`,
318
+ next_action: "none",
319
+ };
320
+ }
321
+ const triggerFingerprint = supervisorGraphFingerprint({
322
+ rootWorkId: plans.active_root_work_id,
323
+ generation: plans.generation,
324
+ work,
325
+ });
326
+ const latestHandledSameGraph = plans.latest?.request.trigger_fingerprint === triggerFingerprint &&
327
+ plans.latest.request.prior_root_work_id === plans.active_root_work_id &&
328
+ plans.latest.status === "attention";
329
+ const canReplan = intent.replanning !== undefined &&
330
+ plans.plans.length < intent.replanning.max_replans &&
331
+ cancelled.length === 0 &&
332
+ !latestHandledSameGraph;
333
+ if (canReplan) {
334
+ return {
335
+ ...base,
336
+ state: "ready",
337
+ reason: terminalBlocked.length
338
+ ? `${terminalBlocked.length} terminally blocked work item${terminalBlocked.length === 1 ? " can" : "s can"} be replanned`
339
+ : "the active graph has no legal progress action and may be replanned",
340
+ next_action: "replan",
341
+ };
342
+ }
343
+ if (intent.replanning &&
344
+ plans.plans.length >= intent.replanning.max_replans &&
345
+ cancelled.length === 0) {
346
+ return {
347
+ ...base,
348
+ state: "budget_exhausted",
349
+ reason: `goal exhausted its ${intent.replanning.max_replans} replans`,
350
+ next_action: "none",
351
+ };
352
+ }
353
+ if (latestHandledSameGraph && plans.latest) {
354
+ return {
355
+ ...base,
356
+ state: "awaiting_attention",
357
+ reason: plans.latest.reason ?? `plan ${plans.latest.request.id} requires attention`,
358
+ next_action: "none",
359
+ };
360
+ }
271
361
  return {
272
362
  ...base,
273
363
  state: "blocked",
@@ -314,6 +404,8 @@ function validateIntent(intent, goalId) {
314
404
  if (JSON.stringify(normalizeAutomation(intent.automation)) !== JSON.stringify(intent.automation)) {
315
405
  throw new Error(`supervisor intent ${goalId} automation policy is not canonical`);
316
406
  }
407
+ if (intent.replanning)
408
+ validateReplanning(intent.replanning, specialists, goalId);
317
409
  }
318
410
  function normalizeLimits(input) {
319
411
  return {
@@ -332,6 +424,93 @@ function normalizeAutomation(input) {
332
424
  retry_blocked: boolean(input?.retry_blocked ?? false, "retry_blocked"),
333
425
  };
334
426
  }
427
+ function normalizeReplanning(input, specialistsInput) {
428
+ const specialists = normalizeWorkflowSpecialists(specialistsInput);
429
+ const plannerSpecialist = bounded(input.plannerSpecialist, "planner specialist", 100);
430
+ if (!specialists[plannerSpecialist]) {
431
+ throw new Error(`planner specialist ${plannerSpecialist} is not present in the frozen team`);
432
+ }
433
+ const entries = Object.entries(input.templates);
434
+ if (entries.length < 1 || entries.length > MAX_TEMPLATES) {
435
+ throw new Error(`replanning templates must contain 1 to ${MAX_TEMPLATES} entries`);
436
+ }
437
+ const templates = Object.fromEntries(entries
438
+ .sort(([left], [right]) => left.localeCompare(right))
439
+ .map(([id, template]) => {
440
+ if (!TEMPLATE_ID.test(id))
441
+ throw new Error(`invalid replanning template id ${id}`);
442
+ const path = resolve(template.workflowPath);
443
+ if (!existsSync(path))
444
+ throw new Error(`replanning template ${id} does not exist at ${path}`);
445
+ const maxAttempts = positive(template.maxAttempts ?? 3, `replanning template ${id} maxAttempts`, 100);
446
+ return [
447
+ id,
448
+ {
449
+ workflow: { path, sha256: workflowScriptDigest(path) },
450
+ max_attempts: maxAttempts,
451
+ root: boolean(template.root ?? false, `replanning template ${id} root`),
452
+ },
453
+ ];
454
+ }));
455
+ if (!Object.values(templates).some((template) => template.root)) {
456
+ throw new Error("replanning requires at least one root-capable template");
457
+ }
458
+ const maxWorkItemsPerPlan = positive(input.maxWorkItemsPerPlan ?? 8, "replanning max_work_items_per_plan", 25);
459
+ const maxTotalWorkItems = positive(input.maxTotalWorkItems ?? 25, "replanning max_total_work_items", 100);
460
+ if (maxWorkItemsPerPlan > maxTotalWorkItems) {
461
+ throw new Error("replanning max_work_items_per_plan cannot exceed max_total_work_items");
462
+ }
463
+ return {
464
+ planner_specialist: plannerSpecialist,
465
+ auto_apply: boolean(input.autoApply ?? false, "replanning auto_apply"),
466
+ max_replans: positive(input.maxReplans ?? 5, "replanning max_replans", 100),
467
+ max_work_items_per_plan: maxWorkItemsPerPlan,
468
+ max_total_work_items: maxTotalWorkItems,
469
+ templates,
470
+ };
471
+ }
472
+ function validateReplanning(policy, specialists, goalId) {
473
+ if (!specialists[policy.planner_specialist]) {
474
+ throw new Error(`supervisor ${goalId} planner specialist is not in the frozen team`);
475
+ }
476
+ boolean(policy.auto_apply, "replanning auto_apply");
477
+ positive(policy.max_replans, "replanning max_replans", 100);
478
+ positive(policy.max_work_items_per_plan, "replanning max_work_items_per_plan", 25);
479
+ positive(policy.max_total_work_items, "replanning max_total_work_items", 100);
480
+ if (policy.max_work_items_per_plan > policy.max_total_work_items) {
481
+ throw new Error(`supervisor ${goalId} replanning work-item bounds are inconsistent`);
482
+ }
483
+ const entries = Object.entries(policy.templates ?? {});
484
+ if (entries.length < 1 || entries.length > MAX_TEMPLATES) {
485
+ throw new Error(`supervisor ${goalId} has invalid replanning templates`);
486
+ }
487
+ let rootCapable = false;
488
+ for (const [id, template] of entries) {
489
+ if (!TEMPLATE_ID.test(id) ||
490
+ !isAbsolute(template.workflow?.path) ||
491
+ !/^[a-f0-9]{64}$/.test(template.workflow?.sha256) ||
492
+ !Number.isSafeInteger(template.max_attempts) ||
493
+ template.max_attempts < 1 ||
494
+ template.max_attempts > 100 ||
495
+ typeof template.root !== "boolean") {
496
+ throw new Error(`supervisor ${goalId} replanning template ${id} is invalid`);
497
+ }
498
+ rootCapable ||= template.root;
499
+ }
500
+ if (!rootCapable)
501
+ throw new Error(`supervisor ${goalId} has no root-capable replanning template`);
502
+ }
503
+ function collectGovernedWork(coordRoot, originalRootWorkId, plans, active) {
504
+ const governed = new Map();
505
+ for (const record of collectSupervisorWork(coordRoot, originalRootWorkId)) {
506
+ governed.set(record.intent.id, record);
507
+ }
508
+ for (const workId of plans.applied_work_ids)
509
+ governed.set(workId, readWorkItem(coordRoot, workId));
510
+ for (const record of active)
511
+ governed.set(record.intent.id, record);
512
+ return [...governed.values()];
513
+ }
335
514
  function supervisorDir(coordRoot, goalId) {
336
515
  assertSupervisorId(goalId);
337
516
  return join(resolve(coordRoot), ".harnery", "supervisors", goalId);
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/gate.ts"],"names":[],"mappings":"AAiBA,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGlD;AAED,QAAA,MAAM,KAAK,aAKV,CAAC;AACF,QAAA,MAAM,MAAM,QAAwD,CAAC;AACrE,QAAA,MAAM,KAAK,QAAkD,CAAC;AAC9D,QAAA,MAAM,IAAI,QAA0E,CAAC;AACrF,QAAA,MAAM,MAAM,QAA8D,CAAC;AAE3E,QAAA,MAAM,aAAa,QAAqB,CAAC;AACzC,QAAA,MAAM,WAAW,QAAmB,CAAC;AAErC,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,QAAA,MAAM,MAAM,oBAsEV,CAAC"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/gate.ts"],"names":[],"mappings":"AAiBA,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGlD;AAED,QAAA,MAAM,KAAK,aAKV,CAAC;AACF,QAAA,MAAM,MAAM,QAAwD,CAAC;AACrE,QAAA,MAAM,KAAK,QAAkD,CAAC;AAC9D,QAAA,MAAM,IAAI,QAA0E,CAAC;AACrF,QAAA,MAAM,MAAM,QAA8D,CAAC;AAE3E,QAAA,MAAM,aAAa,QAAqB,CAAC;AACzC,QAAA,MAAM,WAAW,QAAmB,CAAC;AAErC,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,QAAA,MAAM,MAAM,oBA4EV,CAAC"}
@@ -38,13 +38,18 @@ const server = Bun.serve({
38
38
  // Bun's max, and beyond any realistic single-turn delay.
39
39
  idleTimeout: 255,
40
40
  async fetch(req, server) {
41
+ const url = new URL(req.url);
41
42
  if (ACCESS === "cloudflare-allowlist") {
42
43
  const ip = req.headers.get("cf-connecting-ip") ?? "";
43
44
  if (!ALLOW.has(ip)) {
45
+ // Log denials so operators can whitelist a phone/laptop that just
46
+ // hit 403 without asking the human to dig up their public IP.
47
+ console.log(
48
+ // lint-ok-emission: detached worker, see file note above
49
+ `deny: ${ip || "(missing-cf-connecting-ip)"} ${req.method} ${url.pathname}`);
44
50
  return new Response("403 Forbidden\n", { status: 403 });
45
51
  }
46
52
  }
47
- const url = new URL(req.url);
48
53
  if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
49
54
  if (server.upgrade(req, { data: { path: url.pathname + url.search } })) {
50
55
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",