@rulvar/plan 1.234.0 → 1.236.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/dist/index.d.ts CHANGED
@@ -984,6 +984,32 @@ interface PlanRunnerOptions {
984
984
  * enabling it changes toolsetHash by design. Default false.
985
985
  */
986
986
  kbPropose?: boolean;
987
+ /**
988
+ * Disarms the finish gate (RV3202). By default the coordination
989
+ * finish REFUSES while any plan node is ready or running, naming the
990
+ * stragglers, because the plan is the extension's authority: a root
991
+ * that finishes over a running node used to settle a bare ok while
992
+ * the exit barrier cancelled the node (the 2026-08-11 experiment's
993
+ * blocker). Opting out restores that pre-RV3202 behavior for hosts
994
+ * whose acceptance policy already owns the boundary. Default false.
995
+ */
996
+ allowEarlyFinish?: boolean;
997
+ /**
998
+ * The resume posture toward a drifted profile registry (RV3203, the
999
+ * 2026-08-11 experiment's resume blocker). The registry identity
1000
+ * frozen in `termination.init` (profile names mapped to ladder
1001
+ * lengths) is recomputed from the LIVE profiles on every resume:
1002
+ * under `'refuse'` (the default) a mismatch terminates the resumed
1003
+ * run typed BEFORE any model call, because ladders are live values
1004
+ * the journal cannot rebuild and "the journal wins" is not honorable
1005
+ * for them; `'warn'` downgrades the mismatch to the
1006
+ * `termination:config-drift` event and proceeds under the live
1007
+ * registry. Either way the mismatch is reported; a journal recorded
1008
+ * before the hash shipped skips the check (absence means NOT
1009
+ * RECORDED). The projection covers profile names and ladder lengths,
1010
+ * not the models inside same-length rungs.
1011
+ */
1012
+ profileDrift?: "refuse" | "warn";
987
1013
  }
988
1014
  /**
989
1015
  * Builds the PlanRunner orchestrator extension.
package/dist/index.js CHANGED
@@ -1996,6 +1996,7 @@ const TERMINATION_DENIED_KEY_KIND = "termination.denied";
1996
1996
  * the `orchestratePlanned` convenience surface.
1997
1997
  */
1998
1998
  function planRunner(options) {
1999
+ if (options?.profileDrift !== void 0 && options.profileDrift !== "refuse" && options.profileDrift !== "warn") throw new ConfigError(`PlanRunnerOptions.profileDrift must be 'refuse' or 'warn'; got ` + JSON.stringify(options.profileDrift));
1999
2000
  let io;
2000
2001
  let planScope = "plan";
2001
2002
  let rootScope = "";
@@ -3684,6 +3685,11 @@ function planRunner(options) {
3684
3685
  "plan_revise (add_task, amend_task, park_task, unpark_task, cancel_task,",
3685
3686
  "reprioritize, rewire_deps, waive_dep), inspect it with plan_view, and sleep",
3686
3687
  "with wait_for_events; the ENGINE schedules ready plan nodes for you.",
3688
+ ...options?.allowEarlyFinish === true ? [] : [
3689
+ "finish is refused while any plan node is ready or running: wait for them",
3690
+ "with wait_for_events, or close them deliberately with plan_revise",
3691
+ "(cancel_task or park_task) before finishing."
3692
+ ],
3687
3693
  ...options?.kbPropose === true ? ["kb_propose records ONE quarantined model observation about a ladder tier of a", "journaled lineage; it renders into no prompt and commits nothing during the run."] : []
3688
3694
  ],
3689
3695
  boot: async (bound) => {
@@ -3711,6 +3717,36 @@ function planRunner(options) {
3711
3717
  liveValue
3712
3718
  });
3713
3719
  }
3720
+ const liveUsd = {
3721
+ runBudgetUsdCeiling: io.runCeilingUsd ?? 0,
3722
+ orchestratorCapUsd: io.orchestratorCapUsd ?? 0,
3723
+ finalizeReserveUsd: io.finalizeReserveUsd ?? 0
3724
+ };
3725
+ for (const [field, liveValue] of Object.entries(liveUsd)) {
3726
+ const frozenValue = frozen[field];
3727
+ if (typeof frozenValue === "number" && frozenValue !== 0 && frozenValue !== liveValue) io.emit({
3728
+ type: "termination:config-drift",
3729
+ field,
3730
+ frozenValue,
3731
+ liveValue
3732
+ });
3733
+ }
3734
+ const frozenRegistryHash = folded.init.profileRegistrySnapshotHash;
3735
+ if (typeof frozenRegistryHash === "string") {
3736
+ const liveRegistryHash = profileRegistrySnapshotHash(io.profiles);
3737
+ if (frozenRegistryHash !== liveRegistryHash) {
3738
+ io.emit({
3739
+ type: "termination:config-drift",
3740
+ field: "profileRegistrySnapshotHash",
3741
+ frozenValue: frozenRegistryHash,
3742
+ liveValue: liveRegistryHash
3743
+ });
3744
+ if (options?.profileDrift !== "warn") {
3745
+ io.terminate?.(new ConfigError(`resume: the profile registry drifted since run '${io.runId}' started (frozen ${frozenRegistryHash.slice(0, 12)}, live ${liveRegistryHash.slice(0, 12)}): profile names or ladder lengths changed, and the journaled plan cannot be honored under a different registry. Resume with the original profiles, or pass PlanRunnerOptions.profileDrift: 'warn' to proceed under the live registry deliberately`));
3746
+ return;
3747
+ }
3748
+ }
3749
+ }
3714
3750
  } else {
3715
3751
  const limits = validateTerminationLimits({
3716
3752
  maxRevisionsPerRun: options?.maxRevisionsPerRun ?? 32,
@@ -3787,6 +3823,17 @@ function planRunner(options) {
3787
3823
  absorbPlan();
3788
3824
  return !Object.values(fold.plan.nodes).some((node) => node.status === "ready" || node.status === "running");
3789
3825
  },
3826
+ finishGate: () => {
3827
+ if (options?.allowEarlyFinish === true) return { ok: true };
3828
+ absorbPlan();
3829
+ const live = Object.values(fold.plan.nodes).filter((node) => node.status === "ready" || node.status === "running").sort((a, b) => a.nodeId.localeCompare(b.nodeId));
3830
+ if (live.length === 0) return { ok: true };
3831
+ const named = live.slice(0, 8).map((node) => `${node.nodeId} (${node.logicalTaskId}, ${node.status})`).join(", ");
3832
+ return {
3833
+ ok: false,
3834
+ reason: `finish refused: the plan still has ${String(live.length)} node(s) ready or running: ${named}${live.length > 8 ? ", ..." : ""}. Wait for them with wait_for_events, or close them deliberately with plan_revise (cancel_task or park_task), then call finish again`
3835
+ };
3836
+ },
3790
3837
  digestExtras: () => {
3791
3838
  absorbPlan();
3792
3839
  const spend = DedupIndex.fold(io.snapshot(), { priceUsd: (servedBy, usage) => io.priceUsd(servedBy, usage) }).abandonedSpend();
@@ -4172,6 +4219,15 @@ async function runOscillationFreeze(options) {
4172
4219
  prompt: "flip flop"
4173
4220
  }
4174
4221
  }], `oscillation ${String(phase)}`);
4222
+ if (phase === 7) return { toolCall: {
4223
+ name: "finish",
4224
+ args: { result: "frozen" }
4225
+ } };
4226
+ if (phase === 8) return revise([{
4227
+ op: "cancel_task",
4228
+ nodeId: liveNode ?? "MISSING",
4229
+ reason: "frozen: close out"
4230
+ }], "close the straggler");
4175
4231
  return { toolCall: {
4176
4232
  name: "finish",
4177
4233
  args: { result: "frozen" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/plan",
3
- "version": "1.234.0",
3
+ "version": "1.236.0",
4
4
  "description": "Rulvar adaptive orchestration extension: PlanRunner, RunLedger, escalation extensions, ModelLadder configuration. Replans during the run; not the plan-writing hybrid mode, which is @rulvar/planner.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,13 +22,13 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.234.0"
25
+ "@rulvar/core": "1.236.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.20.1",
29
29
  "tsdown": "^0.22.14",
30
30
  "typescript": "~6.0.3",
31
- "@rulvar/store-sqlite": "1.234.0"
31
+ "@rulvar/store-sqlite": "1.236.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",