harnery 0.16.0 → 0.18.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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/supervisor.d.ts.map +1 -1
  3. package/dist/commands/supervisor.js +223 -5
  4. package/dist/core/supervisor/index.d.ts +4 -1
  5. package/dist/core/supervisor/index.d.ts.map +1 -1
  6. package/dist/core/supervisor/index.js +3 -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 +315 -0
  13. package/dist/core/supervisor/plan-types.d.ts +123 -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 +579 -0
  19. package/dist/core/supervisor/read.d.ts +3 -1
  20. package/dist/core/supervisor/read.d.ts.map +1 -1
  21. package/dist/core/supervisor/read.js +2 -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 +42 -7
  25. package/dist/core/supervisor/service.d.ts.map +1 -1
  26. package/dist/core/supervisor/service.js +10 -0
  27. package/dist/core/supervisor/state.d.ts +27 -2
  28. package/dist/core/supervisor/state.d.ts.map +1 -1
  29. package/dist/core/supervisor/state.js +302 -13
  30. package/package.json +1 -1
  31. package/src/commands/supervisor.ts +275 -5
  32. package/src/core/supervisor/index.ts +21 -0
  33. package/src/core/supervisor/plan-api.ts +47 -0
  34. package/src/core/supervisor/plan-read.ts +356 -0
  35. package/src/core/supervisor/plan-types.ts +157 -0
  36. package/src/core/supervisor/planning.ts +698 -0
  37. package/src/core/supervisor/read.ts +17 -0
  38. package/src/core/supervisor/runner.ts +46 -2
  39. package/src/core/supervisor/service.ts +10 -0
  40. package/src/core/supervisor/state.ts +389 -12
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
3
  import type { Command } from "commander";
4
4
  import type { EmitContext } from "../commander.ts";
5
5
  import { workflowSubscriptionOnly } from "../core/config.ts";
@@ -8,15 +8,22 @@ import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
8
8
  import type { PolicyIsolation } from "../core/policy/index.ts";
9
9
  import { loadPolicyFile } from "../core/policy/index.ts";
10
10
  import {
11
+ approveSupervisorPlan,
12
+ type CreateSupervisorMissionInput,
13
+ type CreateSupervisorReplanningInput,
11
14
  configureSupervisorService,
12
15
  createSupervisor,
13
16
  listSupervisors,
14
17
  readSupervisor,
18
+ readSupervisorPlan,
15
19
  readSupervisorServiceConfig,
16
20
  readSupervisorServiceStatus,
21
+ rejectSupervisorPlan,
17
22
  requestSupervisorServiceStop,
18
23
  runSupervisor,
19
24
  runSupervisorServiceDaemon,
25
+ type SupervisorPlanOutcome,
26
+ type SupervisorPlanRecord,
20
27
  type SupervisorRecord,
21
28
  type SupervisorRunReport,
22
29
  type SupervisorServiceConfig,
@@ -39,6 +46,8 @@ interface CreateOpts {
39
46
  acceptPassingProof?: boolean;
40
47
  resumeApproved?: boolean;
41
48
  retryBlocked?: boolean;
49
+ replanning?: string;
50
+ mission?: string;
42
51
  json?: boolean;
43
52
  }
44
53
 
@@ -68,8 +77,8 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
68
77
  .description("Run a bounded specialist team over a durable-work dependency graph.");
69
78
 
70
79
  supervisor
71
- .command("create <root-work-id>")
72
- .description("Freeze a goal, specialist team, automation policy, and execution limits.")
80
+ .command("create [root-work-id]")
81
+ .description("Freeze an existing-root goal or an objective-first bounded mission.")
73
82
  .requiredOption("--team <file>", "JSON object mapping specialist ids to profiles")
74
83
  .option("--id <goal-id>", "Stable goal id (generated by default)")
75
84
  .option("--title <text>", "Goal title (defaults to root work title)")
@@ -82,8 +91,10 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
82
91
  .option("--accept-passing-proof", "Allow the supervisor to explicitly accept passing proof")
83
92
  .option("--no-resume-approved", "Stop after an approval instead of resuming its parked run")
84
93
  .option("--retry-blocked", "Allow bounded retry of blocked work")
94
+ .option("--replanning <file>", "Frozen planner policy and allowed workflow-template catalog")
95
+ .option("--mission <file>", "Frozen objective, acceptance criteria, and milestone bound")
85
96
  .option("--json", "Emit the complete supervisor record as JSON")
86
- .action((rootWorkId: string, opts: CreateOpts) => {
97
+ .action((rootWorkId: string | undefined, opts: CreateOpts) => {
87
98
  withSupervisorRoot(emit, (coordRoot) => {
88
99
  const specialists = readTeamFile(opts.team);
89
100
  for (const [id, profile] of Object.entries(specialists)) {
@@ -113,6 +124,8 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
113
124
  resume_approved: opts.resumeApproved,
114
125
  retry_blocked: opts.retryBlocked,
115
126
  },
127
+ mission: opts.mission ? readMissionFile(opts.mission) : undefined,
128
+ replanning: opts.replanning ? readReplanningFile(opts.replanning) : undefined,
116
129
  }),
117
130
  opts.json,
118
131
  emit,
@@ -149,11 +162,102 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
149
162
  });
150
163
  });
151
164
 
165
+ registerPlanCommand(supervisor, emit);
152
166
  registerServiceCommand(supervisor, registry, emit);
153
167
  registerRunCommand(supervisor, "tick", registry, emit);
154
168
  registerRunCommand(supervisor, "run", registry, emit);
155
169
  }
156
170
 
171
+ function registerPlanCommand(supervisor: Command, emit: EmitContext): void {
172
+ const plan = supervisor
173
+ .command("plan")
174
+ .description("Inspect and resolve bounded planner proposals for a durable goal.");
175
+
176
+ plan
177
+ .command("list <goal-id>")
178
+ .description("List append-only replanning attempts for one goal.")
179
+ .option("--json", "Emit complete plan records as JSON")
180
+ .action((goalId: string, opts: { json?: boolean }) => {
181
+ withSupervisorRoot(emit, (coordRoot) => {
182
+ const plans = readSupervisor(coordRoot, goalId).plans;
183
+ if (opts.json) {
184
+ emit.config({ format: "json" });
185
+ emit.data(plans);
186
+ } else if (plans.length === 0) {
187
+ emit.text(`supervisor ${goalId} has no replanning attempts\n`);
188
+ } else {
189
+ emit.text(`${plans.map(renderPlanRow).join("\n")}\n`);
190
+ }
191
+ });
192
+ });
193
+
194
+ plan
195
+ .command("show <goal-id> <plan-id>")
196
+ .description("Show one planner request, schema-gated proposal, and audit events.")
197
+ .option("--json", "Emit the complete plan record as JSON")
198
+ .action((goalId: string, planId: string, opts: { json?: boolean }) => {
199
+ withSupervisorRoot(emit, (coordRoot) => {
200
+ emitPlan(readSupervisorPlan(coordRoot, goalId, planId), opts.json, emit);
201
+ });
202
+ });
203
+
204
+ plan
205
+ .command("approve <goal-id> <plan-id>")
206
+ .description("Materialize a reviewed proposal and advance the active immutable root.")
207
+ .option("--actor <name>", "Actor recorded on the plan decision")
208
+ .option("--reason <text>", "Reason for approval")
209
+ .option("--json", "Emit the plan outcome as JSON")
210
+ .action(
211
+ (
212
+ goalId: string,
213
+ planId: string,
214
+ opts: { actor?: string; reason?: string; json?: boolean },
215
+ ) => {
216
+ withSupervisorRoot(emit, (coordRoot) => {
217
+ emitPlanOutcome(
218
+ approveSupervisorPlan({
219
+ coordRoot,
220
+ goalId,
221
+ planId,
222
+ actor: opts.actor,
223
+ reason: opts.reason,
224
+ }),
225
+ opts.json,
226
+ emit,
227
+ );
228
+ });
229
+ },
230
+ );
231
+
232
+ plan
233
+ .command("reject <goal-id> <plan-id>")
234
+ .description("Reject a pending proposal without mutating durable work.")
235
+ .requiredOption("--reason <text>", "Reason the proposal is rejected")
236
+ .option("--actor <name>", "Actor recorded on the plan decision")
237
+ .option("--json", "Emit the plan outcome as JSON")
238
+ .action(
239
+ (
240
+ goalId: string,
241
+ planId: string,
242
+ opts: { actor?: string; reason: string; json?: boolean },
243
+ ) => {
244
+ withSupervisorRoot(emit, (coordRoot) => {
245
+ emitPlanOutcome(
246
+ rejectSupervisorPlan({
247
+ coordRoot,
248
+ goalId,
249
+ planId,
250
+ actor: opts.actor,
251
+ reason: opts.reason,
252
+ }),
253
+ opts.json,
254
+ emit,
255
+ );
256
+ });
257
+ },
258
+ );
259
+ }
260
+
157
261
  function registerServiceCommand(
158
262
  supervisor: Command,
159
263
  registry: ReturnType<typeof createBuiltinHarnessRegistry>,
@@ -467,6 +571,93 @@ function readTeamFile(path: string): Record<string, WorkflowSpecialistProfile> {
467
571
  return value as Record<string, WorkflowSpecialistProfile>;
468
572
  }
469
573
 
574
+ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
575
+ const absolute = resolve(path);
576
+ let value: unknown;
577
+ try {
578
+ value = JSON.parse(readFileSync(absolute, "utf8"));
579
+ } catch (error) {
580
+ throw new Error(
581
+ `cannot read supervisor replanning policy at ${absolute}: ${(error as Error).message}`,
582
+ );
583
+ }
584
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
585
+ throw new Error("supervisor replanning policy must be a JSON object");
586
+ }
587
+ const config = value as Record<string, unknown>;
588
+ if (typeof config.planner_specialist !== "string") {
589
+ throw new Error("supervisor replanning planner_specialist must be a string");
590
+ }
591
+ if (
592
+ !config.templates ||
593
+ typeof config.templates !== "object" ||
594
+ Array.isArray(config.templates)
595
+ ) {
596
+ throw new Error("supervisor replanning templates must be an object");
597
+ }
598
+ const base = dirname(absolute);
599
+ const templates = Object.fromEntries(
600
+ Object.entries(config.templates as Record<string, unknown>).map(([id, raw]) => {
601
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
602
+ throw new Error(`supervisor replanning template ${id} must be an object`);
603
+ }
604
+ const template = raw as Record<string, unknown>;
605
+ if (typeof template.workflow !== "string") {
606
+ throw new Error(`supervisor replanning template ${id} workflow must be a string`);
607
+ }
608
+ return [
609
+ id,
610
+ {
611
+ workflowPath: isAbsolute(template.workflow)
612
+ ? template.workflow
613
+ : resolve(base, template.workflow),
614
+ maxAttempts: jsonInteger(template.max_attempts, `template ${id} max_attempts`),
615
+ root: jsonBoolean(template.root, `template ${id} root`),
616
+ },
617
+ ];
618
+ }),
619
+ );
620
+ return {
621
+ plannerSpecialist: config.planner_specialist,
622
+ autoApply: jsonBoolean(config.auto_apply, "replanning auto_apply"),
623
+ maxReplans: jsonInteger(config.max_replans, "replanning max_replans"),
624
+ maxWorkItemsPerPlan: jsonInteger(
625
+ config.max_work_items_per_plan,
626
+ "replanning max_work_items_per_plan",
627
+ ),
628
+ maxTotalWorkItems: jsonInteger(config.max_total_work_items, "replanning max_total_work_items"),
629
+ templates,
630
+ };
631
+ }
632
+
633
+ function readMissionFile(path: string): CreateSupervisorMissionInput {
634
+ const absolute = resolve(path);
635
+ let value: unknown;
636
+ try {
637
+ value = JSON.parse(readFileSync(absolute, "utf8"));
638
+ } catch (error) {
639
+ throw new Error(`cannot read supervisor mission at ${absolute}: ${(error as Error).message}`);
640
+ }
641
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
642
+ throw new Error("supervisor mission must be a JSON object");
643
+ }
644
+ const config = value as Record<string, unknown>;
645
+ if (typeof config.objective !== "string") {
646
+ throw new Error("supervisor mission objective must be a string");
647
+ }
648
+ if (
649
+ !Array.isArray(config.acceptance) ||
650
+ config.acceptance.some((item) => typeof item !== "string")
651
+ ) {
652
+ throw new Error("supervisor mission acceptance must be an array of strings");
653
+ }
654
+ return {
655
+ objective: config.objective,
656
+ acceptance: config.acceptance as string[],
657
+ maxMilestones: jsonInteger(config.max_milestones, "mission max_milestones"),
658
+ };
659
+ }
660
+
470
661
  function emitSupervisor(
471
662
  record: SupervisorRecord,
472
663
  json: boolean | undefined,
@@ -486,7 +677,7 @@ function emitSupervisor(
486
677
  const lines = [
487
678
  `${record.intent.id}: ${record.intent.title}`,
488
679
  `state: ${projection.state}`,
489
- `root: ${record.intent.root_work_id}`,
680
+ `root: ${projection.root_materialized ? projection.root_work_id : "pending initial plan"}`,
490
681
  `reason: ${projection.reason}`,
491
682
  `next: ${projection.next_action}`,
492
683
  `work: ${projection.work_ids.length}`,
@@ -495,6 +686,22 @@ function emitSupervisor(
495
686
  `automation: accept=${record.intent.automation.accept_passing_proof}, ` +
496
687
  `resume=${record.intent.automation.resume_approved}, retry=${record.intent.automation.retry_blocked}`,
497
688
  ];
689
+ if (record.intent.replanning) {
690
+ lines.push(
691
+ `replanning: generation=${projection.plan_generation}, used=${projection.replans_used}/${record.intent.replanning.max_replans}, auto_apply=${record.intent.replanning.auto_apply}`,
692
+ );
693
+ if (projection.root_work_id !== record.intent.root_work_id) {
694
+ lines.push(`original root: ${record.intent.root_work_id}`);
695
+ }
696
+ if (projection.pending_plan_id) lines.push(`pending plan: ${projection.pending_plan_id}`);
697
+ }
698
+ if (record.intent.mission) {
699
+ lines.push(`mission: ${record.intent.mission.objective}`);
700
+ lines.push(
701
+ `milestones: ${projection.milestones_completed}/${record.intent.mission.max_milestones}`,
702
+ );
703
+ lines.push(`mission acceptance: ${record.intent.mission.acceptance.length}`);
704
+ }
498
705
  if (projection.attention_work.length) {
499
706
  lines.push(`attention: ${projection.attention_work.join(", ")}`);
500
707
  }
@@ -515,6 +722,7 @@ function emitSupervisorReport(
515
722
  `supervisor ${report.goal_id}: ${report.stop_reason}\n` +
516
723
  `reason: ${report.reason}\n` +
517
724
  `cycles: ${report.cycles}; dispatches: ${report.dispatches}; acceptances: ${report.acceptances}\n` +
725
+ `replans: ${report.replans}\n` +
518
726
  `state: ${report.projection.state}; next: ${report.projection.next_action}\n`,
519
727
  );
520
728
  }
@@ -531,6 +739,68 @@ function integer(value: string | undefined): number | undefined {
531
739
  return Number.parseInt(value, 10);
532
740
  }
533
741
 
742
+ function jsonInteger(value: unknown, field: string): number | undefined {
743
+ if (value === undefined) return undefined;
744
+ if (!Number.isSafeInteger(value) || (value as number) < 1) {
745
+ throw new Error(`${field} must be a positive integer`);
746
+ }
747
+ return value as number;
748
+ }
749
+
750
+ function jsonBoolean(value: unknown, field: string): boolean | undefined {
751
+ if (value === undefined) return undefined;
752
+ if (typeof value !== "boolean") throw new Error(`${field} must be boolean`);
753
+ return value;
754
+ }
755
+
756
+ function emitPlan(plan: SupervisorPlanRecord, json: boolean | undefined, emit: EmitContext): void {
757
+ if (json) {
758
+ emit.config({ format: "json" });
759
+ emit.data(plan);
760
+ return;
761
+ }
762
+ const lines = [
763
+ `${plan.request.id}: ${plan.status}`,
764
+ `goal: ${plan.request.goal_id}`,
765
+ `sequence: ${plan.request.sequence}`,
766
+ `trigger: ${plan.request.trigger ?? "recovery"}`,
767
+ `prior root: ${plan.request.prior_root_work_id}`,
768
+ `planner run: ${plan.request.workflow_run_id}`,
769
+ ];
770
+ if (plan.proposal) {
771
+ lines.push(`decision: ${plan.proposal.decision}`);
772
+ lines.push(`rationale: ${plan.proposal.rationale}`);
773
+ lines.push(`proposed work: ${plan.proposal.work.length}`);
774
+ if (plan.proposal.milestone) {
775
+ lines.push(`milestone: ${plan.proposal.milestone.sequence} ${plan.proposal.milestone.title}`);
776
+ }
777
+ }
778
+ if (plan.root_work_id) lines.push(`applied root: ${plan.root_work_id}`);
779
+ if (plan.approval_id) lines.push(`approval: ${plan.approval_id}`);
780
+ emit.text(`${lines.join("\n")}\n`);
781
+ }
782
+
783
+ function emitPlanOutcome(
784
+ outcome: SupervisorPlanOutcome,
785
+ json: boolean | undefined,
786
+ emit: EmitContext,
787
+ ): void {
788
+ if (json) {
789
+ emit.config({ format: "json" });
790
+ emit.data(outcome);
791
+ return;
792
+ }
793
+ emit.text(
794
+ `supervisor plan ${outcome.plan_id}: ${outcome.status}\n` +
795
+ `${outcome.reason ? `reason: ${outcome.reason}\n` : ""}` +
796
+ `${outcome.root_work_id ? `root: ${outcome.root_work_id}\n` : ""}`,
797
+ );
798
+ }
799
+
800
+ function renderPlanRow(plan: SupervisorPlanRecord): string {
801
+ return `${plan.request.id} ${plan.status.padEnd(18)} ${plan.request.prior_root_work_id}`;
802
+ }
803
+
534
804
  function delay(ms: number): Promise<void> {
535
805
  return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
536
806
  }
@@ -1,3 +1,22 @@
1
+ export { approveSupervisorPlan, rejectSupervisorPlan } from "./plan-api.ts";
2
+ export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.ts";
3
+ export {
4
+ type CreateSupervisorPlanTemplateInput,
5
+ type CreateSupervisorReplanningInput,
6
+ SUPERVISOR_PLAN_SCHEMA_VERSION,
7
+ type SupervisorPlanEvent,
8
+ type SupervisorPlanEventType,
9
+ type SupervisorPlanHistory,
10
+ type SupervisorPlanMilestone,
11
+ type SupervisorPlanOutcome,
12
+ type SupervisorPlanProposal,
13
+ type SupervisorPlanRecord,
14
+ type SupervisorPlanRequest,
15
+ type SupervisorPlanStatus,
16
+ type SupervisorPlanTemplate,
17
+ type SupervisorPlanWorkSpec,
18
+ type SupervisorReplanningPolicy,
19
+ } from "./plan-types.ts";
1
20
  export {
2
21
  type RunSupervisorInput,
3
22
  runSupervisor,
@@ -35,6 +54,7 @@ export {
35
54
  export {
36
55
  assertSupervisorId,
37
56
  type CreateSupervisorInput,
57
+ type CreateSupervisorMissionInput,
38
58
  collectSupervisorWork,
39
59
  createSupervisor,
40
60
  listSupervisors,
@@ -44,6 +64,7 @@ export {
44
64
  type SupervisorAutomationPolicy,
45
65
  type SupervisorIntent,
46
66
  type SupervisorLimits,
67
+ type SupervisorMission,
47
68
  type SupervisorNextAction,
48
69
  type SupervisorProjection,
49
70
  type SupervisorRecord,
@@ -0,0 +1,47 @@
1
+ import type { SupervisorPlanOutcome } from "./plan-types.ts";
2
+ import { applySupervisorPlanProposal, rejectSupervisorPlanProposal } from "./planning.ts";
3
+ import { acquireSupervisorLease, readSupervisorIgnoringLease } from "./state.ts";
4
+
5
+ export function approveSupervisorPlan(input: {
6
+ coordRoot: string;
7
+ goalId: string;
8
+ planId: string;
9
+ actor?: string;
10
+ reason?: string;
11
+ }): SupervisorPlanOutcome {
12
+ const actor = input.actor?.trim() || `supervisor:${input.goalId}`;
13
+ const release = acquireSupervisorLease(input.coordRoot, input.goalId);
14
+ try {
15
+ return applySupervisorPlanProposal({
16
+ coordRoot: input.coordRoot,
17
+ record: readSupervisorIgnoringLease(input.coordRoot, input.goalId),
18
+ planId: input.planId,
19
+ actor,
20
+ reason: input.reason,
21
+ });
22
+ } finally {
23
+ release();
24
+ }
25
+ }
26
+
27
+ export function rejectSupervisorPlan(input: {
28
+ coordRoot: string;
29
+ goalId: string;
30
+ planId: string;
31
+ actor?: string;
32
+ reason: string;
33
+ }): SupervisorPlanOutcome {
34
+ const actor = input.actor?.trim() || `supervisor:${input.goalId}`;
35
+ const release = acquireSupervisorLease(input.coordRoot, input.goalId);
36
+ try {
37
+ return rejectSupervisorPlanProposal({
38
+ coordRoot: input.coordRoot,
39
+ goalId: input.goalId,
40
+ planId: input.planId,
41
+ actor,
42
+ reason: input.reason,
43
+ });
44
+ } finally {
45
+ release();
46
+ }
47
+ }