infinity-harness 2.8.9 → 2.8.10

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.10] — 2026-08-31
8
+
9
+ Thinking levels from pi, not inventoried; locals inherit correctly.
10
+
11
+ ### Fixed
12
+
13
+ - **Thinking menu was hard-coded, not model-aware.** Wizard now filters the thinking-level picker to exactly what pi reports for the picked model (“pi truth”): local reasoning-off models (lfm2.5/qwen3.8/granite) only offer “(inherit)” + “off”; reasoning locals offer off…high; full providers offer off…max. “/infinity:config” thinking settings do the same per-tier. Previously all 7 were always offered, so locals could be set to xhigh.
14
+ - **Locals died on “inherit” even with thinking blank.** Worker forced “inherit” (“”) to “medium” before “createAgentSession”, so even blank B/C/D sent reasoning_effort → max_completion_tokens:1. Changed to leave “” as undefined so pi uses its per-model default then clamps via model.thinkingLevelMap (off for reasoning:false locals). Fixed in daemon/worker, daemon/index, and core/modelRouter clampThinkingForProvider (now model-aware with provider-string fallback).
15
+ - **Provider-string clamp was model-blind.** ”lm-studio → medium” broke xhigh-capable locals and still sent medium to 2b QAD models. Replaced with pi-ai getSupportedThinkingLevels semantics: reasoning:false → only off; reasoning:true + empty map → off…high (no xhigh/max); map null → filtered. Config file stays on pi’s THINKING_LEVELS definition, nothing invented.
16
+
7
17
  ## [2.8.9] — 2026-08-31
8
18
 
9
19
  Locals no longer die on xhigh; DEFINE ladder re-consults.
@@ -540,6 +540,27 @@ export default function (pi: ExtensionAPI): void {
540
540
  * would produce a tier that fails at the first task rather than at setup.
541
541
  */
542
542
  const availableModels = (ctx: ExtensionContext): ModelChoice[] => {
543
+ // Helper: get pi-default thinking levels for a model (the truth — not our THINKING_LEVELS)
544
+ const supportedOf = (m: unknown): readonly string[] | null => {
545
+ try {
546
+ const mod = m as { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> };
547
+ if (!mod) return null;
548
+ // pi-ai getSupportedThinkingLevels: reasoning false => ["off"]
549
+ if (mod.reasoning === false) return ["off"] as const;
550
+ if (mod.reasoning === true) {
551
+ const map = mod.thinkingLevelMap ?? {};
552
+ const all: readonly string[] = ["off","minimal","low","medium","high","xhigh","max"];
553
+ const supported = all.filter(l => {
554
+ const v = (map as Record<string, string | null | undefined>)[l];
555
+ if (v === null) return false;
556
+ if ((l === "xhigh" || l === "max") && v === undefined) return false;
557
+ return true;
558
+ });
559
+ return supported.length ? supported : (["off"] as const);
560
+ }
561
+ return null;
562
+ } catch { return null; }
563
+ };
543
564
  try {
544
565
  const scoped = ctx.scopedModels ?? [];
545
566
  const models =
@@ -558,7 +579,8 @@ export default function (pi: ExtensionAPI): void {
558
579
  if (m.name && m.name !== m.id) bits.push(`· ${m.name}`);
559
580
  if (m.contextWindow) bits.push(`· ${Math.round(m.contextWindow / 1000)}k ctx`);
560
581
  if (m.reasoning) bits.push("· reasoning");
561
- out.push({ ref, label: bits.join(" ") });
582
+ const levels = supportedOf(m);
583
+ out.push({ ref, label: bits.join(" "), ...(levels ? { supportedThinkingLevels: levels } : {}) } as unknown as ModelChoice & { supportedThinkingLevels?: readonly string[] });
562
584
  }
563
585
  out.sort((a, b) => a.ref.localeCompare(b.ref));
564
586
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinity-harness",
3
- "version": "2.8.9",
3
+ "version": "2.8.10",
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,8 +13,30 @@ 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 | "" {
16
+ export function clampThinkingForProvider(provider: string, level: string | "" | undefined, modelForCheck?: unknown): string | "" {
17
17
  if (!level) return (level ?? "") as string | "";
18
+ // If we have the actual model, use pi-default truth (getSupportedThinkingLevels semantics)
19
+ try {
20
+ const m = modelForCheck as { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> } | null | undefined;
21
+ if (m && typeof m.reasoning === "boolean") {
22
+ if (m.reasoning === false) return level === "off" || level === "" ? (level as string | "") : "off";
23
+ const map = m.thinkingLevelMap ?? {};
24
+ const all = ["off","minimal","low","medium","high","xhigh","max"] as const;
25
+ const supported = all.filter(l => {
26
+ const v = (map as Record<string, string | null | undefined>)[l];
27
+ if (v === null) return false;
28
+ if ((l === "xhigh" || l === "max") && v === undefined) return false;
29
+ return true;
30
+ });
31
+ if (supported.includes(level as typeof all[number])) return level as string | "";
32
+ // clamp to nearest supported (prefer lower)
33
+ const idx = (all as readonly string[]).indexOf(level);
34
+ for (let i = idx; i >= 0; i--) if (supported.includes(all[i] as typeof all[number])) return all[i] as string | "";
35
+ for (let i = idx+1; i < all.length; i++) if (supported.includes(all[i] as typeof all[number])) return all[i] as string | "";
36
+ return supported[0] ?? "off";
37
+ }
38
+ } catch {}
39
+ // Fallback: old provider-string heuristic for when model not available (e.g. in config migration)
18
40
  const pp = String(provider || "").toLowerCase();
19
41
  const isLocal = pp === "lm-studio" || pp.startsWith("lm-studio/") || pp === "lmstudio";
20
42
  if (!isLocal) return level as string | "";
@@ -275,7 +275,7 @@ async function runLoop(targetDir: string): Promise<void> {
275
275
  model: askedModel,
276
276
  askedModel,
277
277
  servedModel: null,
278
- thinking: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "medium") || "medium",
278
+ thinking: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "") || undefined,
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: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "medium") || "medium" },
301
+ modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "") || undefined },
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: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "medium") || "medium" },
320
+ modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "") || undefined },
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,12 +110,35 @@ export async function createWorker(opts: CreateWorkerOpts): Promise<{
110
110
  ? SessionManager.create(opts.cwd, opts.sessionManagerDir)
111
111
  : SessionManager.inMemory(opts.cwd);
112
112
 
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)
113
+ // inherit means inherit don't force medium. Only clamp when explicitly set.
114
+ let thinkingLevel: string | undefined = (opts.thinkingLevel ?? opts.modelSpec.thinkingLevel ?? undefined) as unknown as string | undefined;
115
+ // capability-aware clamp: use pi-default supported levels for the actual model, not provider string
115
116
  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";
117
+ const mod = model as unknown as { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> } | null;
118
+ if (thinkingLevel) {
119
+ // inline getSupportedThinkingLevels semantics
120
+ if (mod && typeof mod.reasoning === "boolean" && mod.reasoning === false) {
121
+ if (thinkingLevel !== "off") thinkingLevel = "off";
122
+ } else if (mod && mod.reasoning === true) {
123
+ const map = mod.thinkingLevelMap ?? {};
124
+ const all = ["off","minimal","low","medium","high","xhigh","max"] as const;
125
+ const supported = all.filter(l => {
126
+ const v = (map as Record<string, string | null | undefined>)[l];
127
+ if (v === null) return false;
128
+ if ((l === "xhigh" || l === "max") && v === undefined) return false;
129
+ return true;
130
+ });
131
+ if (!supported.includes(thinkingLevel as typeof all[number])) {
132
+ const idx = (all as readonly string[]).indexOf(thinkingLevel);
133
+ let found: string | null = null;
134
+ for (let i = idx; i >= 0; i--) if (supported.includes(all[i] as typeof all[number])) { found = all[i] as string; break; }
135
+ if (!found) for (let i = idx+1; i < all.length; i++) if (supported.includes(all[i] as typeof all[number])) { found = all[i] as string; break; }
136
+ thinkingLevel = found ?? supported[0] ?? "off";
137
+ }
138
+ }
139
+ } else if (thinkingLevel === undefined) {
140
+ // inherit (undefined) — leave undefined so createAgentSession uses pi-default for that model
141
+ }
119
142
  } catch {}
120
143
  // B: debug trace of routed model/thinking (readable in harness/daemon.log)
121
144
  try { console.log("[worker] model=" + opts.modelSpec.provider + "/" + opts.modelSpec.id + " thinking=" + thinkingLevel + " askedModel=" + opts.askedModel); } catch {}
package/src/ui/config.ts CHANGED
@@ -39,6 +39,8 @@ export type ModelChoice = {
39
39
  ref: string;
40
40
  /** What the user sees. */
41
41
  label: string;
42
+ /** Pi-default supported thinking levels for this model, or null if unknown. */
43
+ supportedThinkingLevels?: readonly string[];
42
44
  };
43
45
 
44
46
  export type ConfigMenuOptions = {
@@ -167,7 +169,29 @@ async function editSetting(setting: Setting, options: ConfigMenuOptions): Promis
167
169
  break;
168
170
  }
169
171
  case "thinking": {
170
- const choices = ["(inherit)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
172
+ // capability-aware: if setting is tiers.X.thinkingLevel etc, filter to what the model actually supports
173
+ let choices: string[] = ["(inherit)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
174
+ try {
175
+ const m = setting.path.match(/^tiers.([A-Z]).(provider|id|thinkingLevel)/);
176
+ // For tiers.A/B/C/D/X thinkingLevel, look up that tier's model and its supported levels
177
+ const tierMatch = setting.path.match(/^tiers.([A-Z]).thinkingLevel/);
178
+ if (tierMatch) {
179
+ const tier = tierMatch[1]!;
180
+ const all = readAll(targetDir);
181
+ const tierSpec = (all.config as unknown as { tiers?: Record<string, { provider?: string; id?: string }> }).tiers?.[tier];
182
+ if (tierSpec?.provider && tierSpec?.id) {
183
+ const ref = tierSpec.provider + "/" + tierSpec.id;
184
+ const models = await options.models();
185
+ const found = models.find(x => x.ref === ref) as unknown as { supportedThinkingLevels?: readonly string[] } | undefined;
186
+ if (found?.supportedThinkingLevels?.length) {
187
+ choices = ["(inherit)", ...found.supportedThinkingLevels.filter(l => l !== "off")];
188
+ // ensure off is last fallback if model supports only off
189
+ if (!found.supportedThinkingLevels.includes("off")) choices = ["(inherit)", ...found.supportedThinkingLevels];
190
+ else if (choices.indexOf("off") === -1) choices.splice(1,0,"off");
191
+ }
192
+ }
193
+ }
194
+ } catch {}
171
195
  const picked = await prompt.select(`${setting.label} — ${setting.help}`, [...choices, BACK]);
172
196
  if (picked === undefined || picked === BACK) return false;
173
197
  raw = picked;
package/src/ui/wizard.ts CHANGED
@@ -153,8 +153,9 @@ async function pickModelChoice(prompt: Prompter, title: string, models: ModelCho
153
153
  return model?.ref;
154
154
  }
155
155
 
156
- async function pickThinkingLevel(prompt: Prompter, title: string): Promise<ThinkingLevel | "" | undefined> {
157
- const picked = await prompt.select(title, [THINK_INHERIT, ...THINKING_LEVELS]);
156
+ async function pickThinkingLevel(prompt: Prompter, title: string, supported?: readonly string[] | null): Promise<ThinkingLevel | "" | undefined> {
157
+ const levels = Array.isArray(supported) && supported.length ? supported as ThinkingLevel[] : THINKING_LEVELS;
158
+ const picked = await prompt.select(title, [THINK_INHERIT, ...levels]);
158
159
  if (picked === undefined) return undefined;
159
160
  if (picked === THINK_INHERIT) return "";
160
161
  return picked as ThinkingLevel;
@@ -214,21 +215,25 @@ async function pickModelsStep(
214
215
  const tiers = ["easy", "moderate", "difficult"] as const;
215
216
  const byDifficulty: Record<string, string> = {};
216
217
  const thinkingByDifficulty: Partial<Record<string, ThinkingLevel | "">> = {};
218
+ const supportedFor = (ref: string): readonly string[] | null => {
219
+ const m = models.find(x => x.ref === ref) as unknown as { supportedThinkingLevels?: readonly string[] } | undefined;
220
+ return (m as { supportedThinkingLevels?: readonly string[] } | undefined)?.supportedThinkingLevels ?? null;
221
+ };
217
222
  for (const tier of tiers) {
218
223
  const model = await pickModelChoice(prompt, `${tier.toUpperCase()} tier — model`, models, "");
219
224
  if (model === undefined) return undefined;
220
225
  byDifficulty[tier] = model;
221
- const thinking = await pickThinkingLevel(prompt, `${tier.toUpperCase()} tier — thinking level`);
226
+ const thinking = await pickThinkingLevel(prompt, `${tier.toUpperCase()} tier — thinking level`, model ? supportedFor(model) : null);
222
227
  if (thinking === undefined) return undefined;
223
228
  thinkingByDifficulty[tier] = thinking;
224
229
  }
225
230
  const masterModel = await pickModelChoice(prompt, "Consulting master — model (used only when the ladder is exhausted)", models, "");
226
231
  if (masterModel === undefined) return undefined;
227
- const masterThinking = await pickThinkingLevel(prompt, "Consulting master — thinking level");
232
+ const masterThinking = await pickThinkingLevel(prompt, "Consulting master — thinking level", masterModel ? supportedFor(masterModel) : null);
228
233
  if (masterThinking === undefined) return undefined;
229
234
  const defaultModel = await pickModelChoice(prompt, "Default (tier A) — general work + undifficult tasks — idle under task handoff when every task has difficulty (" + tierScopeNote(handoff) + ")", models, "");
230
235
  if (defaultModel === undefined) return undefined;
231
- const defaultThinking = await pickThinkingLevel(prompt, "Default — thinking level fallback");
236
+ const defaultThinking = await pickThinkingLevel(prompt, "Default — thinking level fallback", defaultModel ? supportedFor(defaultModel) : null);
232
237
  if (defaultThinking === undefined) return undefined;
233
238
  return {
234
239
  router: {