infinity-harness 2.8.8 → 2.8.9

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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to this project are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.8.9] — 2026-08-31
8
+
9
+ Locals no longer die on xhigh; DEFINE ladder re-consults.
10
+
11
+ ### Fixed
12
+
13
+ - **Locals cut off at 1 token on xhigh.** LM Studio (lm-studio/*) forwarded reasoning_effort xhigh/max as max_completion_tokens:1. Added provider-aware clamp: lm-studio tiers (B/C/D/X when local) never get xhigh/max/high — they are clamped to medium at the router, in daemon/index and in worker createWorker, with a [worker] trace in harness/daemon.log. master (muse-spark) still gets xhigh. Verified: no more length=1 on lfm/qwen/granite.
14
+
15
+ - **DEFINE never re-consulted after RESEARCH spent the ladder.** perLevelEscalation[subtask] kept consultedCount/masterUsed across phase advance (research/r1 → define/d1), so consult/master appeared spent. decideNext now resets perLevelEscalation when retryLevel changes or when tree moves; escalation instruction for feature-criteria now explicitly says to use infinity_plan to add criteria.
16
+
7
17
  ## [2.8.8] — 2026-08-31
8
18
 
9
19
  Routing is one source; default honestly idle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinity-harness",
3
- "version": "2.8.8",
3
+ "version": "2.8.9",
4
4
  "description": "A pi agent extension that runs a gated build pipeline unattended \u2014 enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -13,6 +13,15 @@ import type { TierMap, TierSpec, HarnessConfig, FeatureList, Phase, Difficulty }
13
13
  export type ThinkingLevel = import("./types.ts").ThinkingLevel;
14
14
  export type TierId = import("./types.ts").TierId;
15
15
 
16
+ export function clampThinkingForProvider(provider: string, level: string | "" | undefined): string | "" {
17
+ if (!level) return (level ?? "") as string | "";
18
+ const pp = String(provider || "").toLowerCase();
19
+ const isLocal = pp === "lm-studio" || pp.startsWith("lm-studio/") || pp === "lmstudio";
20
+ if (!isLocal) return level as string | "";
21
+ if (level === "xhigh" || level === "max" || level === "high") return "medium";
22
+ return level as string | "";
23
+ }
24
+
16
25
  const DIFF_TO_TIER: Record<Difficulty, import("./types.ts").TierId> = {
17
26
  easy: "B",
18
27
  moderate: "C",
@@ -26,7 +26,7 @@ import { saveSupervisor, appendActivity, type SupervisorWorker } from "./supervi
26
26
  import { runPreflight } from "./preflight.ts";
27
27
  import { addUsageForTier, isCapExceeded, hasXLeak, xLeakReason, type Tier } from "./budget.ts";
28
28
  import { createWorker, promptWorker, type TurnResult } from "./worker.ts";
29
- import { routeModel, effectiveDifficultyForTask as effectiveDifficulty } from "../core/modelRouter.ts";
29
+ import { routeModel, effectiveDifficultyForTask as effectiveDifficulty, clampThinkingForProvider } from "../core/modelRouter.ts";
30
30
  import { dirname } from "node:path";
31
31
  import type { Server } from "node:http";
32
32
 
@@ -275,7 +275,7 @@ async function runLoop(targetDir: string): Promise<void> {
275
275
  model: askedModel,
276
276
  askedModel,
277
277
  servedModel: null,
278
- thinking: (tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel ?? "medium",
278
+ thinking: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "medium") || "medium",
279
279
  state: "starting",
280
280
  doing: "starting",
281
281
  startedAt: new Date().toISOString(),
@@ -298,7 +298,7 @@ async function runLoop(targetDir: string): Promise<void> {
298
298
  const workerFactory = await import("./worker.ts");
299
299
  workerHandle = await workerFactory.createWorker({
300
300
  cwd: targetDir,
301
- modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: (tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel ?? "medium" },
301
+ modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "medium") || "medium" },
302
302
  askedModel,
303
303
  sessionManagerDir: `harness/sessions/${workerLabel.replace(/[^a-z0-9._-]/gi, "-")}`,
304
304
  customTools: (await import("./isolation.ts")).harnessToolsForWorker() as unknown[],
@@ -317,7 +317,7 @@ async function runLoop(targetDir: string): Promise<void> {
317
317
  if (!workerHandle) {
318
318
  workerHandle = await workerFactory.createWorker({
319
319
  cwd: targetDir,
320
- modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: (tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel ?? "medium" },
320
+ modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "medium") || "medium" },
321
321
  askedModel,
322
322
  sessionManagerDir: `harness/sessions/${workerLabel.replace(/[^a-z0-9._-]/gi, "-")}`,
323
323
  customTools: (await import("./isolation.ts")).harnessToolsForWorker() as unknown[],
@@ -110,7 +110,15 @@ export async function createWorker(opts: CreateWorkerOpts): Promise<{
110
110
  ? SessionManager.create(opts.cwd, opts.sessionManagerDir)
111
111
  : SessionManager.inMemory(opts.cwd);
112
112
 
113
- const thinkingLevel = (opts.thinkingLevel ?? opts.modelSpec.thinkingLevel ?? "medium") as unknown as string;
113
+ let thinkingLevel = (opts.thinkingLevel ?? opts.modelSpec.thinkingLevel ?? "medium") as unknown as string;
114
+ // A: clamp for lm-studio (xhigh/max not supported, caused max_completion_tokens:1)
115
+ try {
116
+ const p = String(opts.modelSpec.provider || "").toLowerCase();
117
+ const isLocal = p === "lm-studio" || p.startsWith("lm-studio/") || p === "lmstudio";
118
+ if (isLocal && (thinkingLevel === "xhigh" || thinkingLevel === "max" || thinkingLevel === "high")) thinkingLevel = "medium";
119
+ } catch {}
120
+ // B: debug trace of routed model/thinking (readable in harness/daemon.log)
121
+ try { console.log("[worker] model=" + opts.modelSpec.provider + "/" + opts.modelSpec.id + " thinking=" + thinkingLevel + " askedModel=" + opts.askedModel); } catch {}
114
122
 
115
123
  const agentTools = opts.customTools as unknown as import("@earendil-works/pi-coding-agent").ToolDefinition[] | undefined;
116
124
 
package/src/loop.ts CHANGED
@@ -486,11 +486,18 @@ export async function decideNext(options: DecideOptions): Promise<{ decision: Lo
486
486
  // budgets in rework.json and replan.json still bound the run across
487
487
  // stalls, but a rung spent on a problem that resolved should not be
488
488
  // missing when a different problem appears.
489
- state.escalation = { ...state.escalation, tried: [] };
490
- const lev = retryLevel
491
- ? escalationStateForLevel(state, retryLevel)
492
- : null;
493
- if (lev) setEscalationForLevel(state, retryLevel, { ...lev, tried: [] });
489
+ // C: also reset per-level escalation when the active unit changes (define/d1 vs research/r1),
490
+ // otherwise consult/master stays marked spent across unrelated tasks.
491
+ const lastLev = state.escalations.length ? state.escalations[state.escalations.length-1]?.level as string | undefined : undefined;
492
+ if (lastLev && lastLev !== (retryLevel ?? "task")) {
493
+ setEscalationForLevel(state, retryLevel ?? "task", { consultedCount: 0, masterUsed: false, lastUnstuckAt: null, fingerprints: [], tried: [] } as unknown as import("./escalate.ts").EscalationState);
494
+ } else {
495
+ state.escalation = { ...state.escalation, tried: [] };
496
+ const lev = retryLevel
497
+ ? escalationStateForLevel(state, retryLevel)
498
+ : null;
499
+ if (lev) setEscalationForLevel(state, retryLevel, { ...lev, tried: [] });
500
+ }
494
501
  } else {
495
502
  state.noProgressStreak += 1;
496
503
  }
@@ -597,6 +604,13 @@ export async function decideNext(options: DecideOptions): Promise<{ decision: Lo
597
604
  const task = nextActionableTask(list);
598
605
  const focus = task ? `\nCurrent task: ${task.compositeKey} — ${task.description}` : "";
599
606
 
607
+ // C: feature-criteria guard — when DEFINE stalls on feature-criteria, the plan is wrong, not the model.
608
+ if (escalation && escalation.strategy === "consult" && gate && (gate.failures as string[]).some((f: string) => String(f).toLowerCase().includes("feature-criteria"))) {
609
+ try {
610
+ const msg = "The " + phase.toUpperCase() + " gate is blocked on feature-criteria: every feature needs acceptance criteria. This is a plan gap — use infinity_plan to add criteria, not just a model switch. Escalating to " + (escalation.model ?? "master") + " for help writing them.";
611
+ escalation = { ...escalation, instruction: msg + "\n" + (escalation.instruction ?? "") } as unknown as typeof escalation;
612
+ } catch {}
613
+ }
600
614
  // An escalation replaces the standard "fix these" nudge, because repeating
601
615
  // that nudge is exactly what the ladder exists to interrupt.
602
616
  const head = standingRejection