infinity-harness 2.8.7 → 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,28 @@ 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
+
17
+ ## [2.8.8] — 2026-08-31
18
+
19
+ Routing is one source; default honestly idle.
20
+
21
+ ### Changed
22
+
23
+ - **Default (tier A) is now honest.** Wizard title is “Default (tier A) — general work + undifficult tasks — idle under task handoff when every task has difficulty” with the live “tierScopeNote(handoff)”. Widget appends “Default nemotron (idle — all tasks have B/C/D, task handoff) · Model per task …” when every lane has B/C/D under task/subtask handoff so “never called” reads as “idle by design”.
24
+
25
+ ### Fixed
26
+
27
+ - **Single source of truth for model routing.** “harness/model-router.json” migrates into “harness/config.json:tiers” (“A=default, B=easy, C=moderate, D=difficult, X=master” + thinkingLevel) on first “loadConfig” when “tiers:{}” and as “initHarness” writes both files for new projects. Daemon and supervisor now agree: “supervisor.ts” prefers “core/modelRouter:routeModel” via “config.tiers” when present, falling back to legacy “src/modelRouter:resolveModel”. Existing projects like “bakr_test_2.8.6” auto-heal without hand-editing.
28
+
7
29
  ## [2.8.7] — 2026-08-31
8
30
 
9
31
  Widget stays live after /infinity:run; daemon found in installed package.
@@ -219,7 +219,7 @@ export default function (pi: ExtensionAPI): void {
219
219
 
220
220
  // -- widget ---------------------------------------------------------------
221
221
 
222
- const handoffNoteFor = (h: import("../../src/core/types.ts").HandoffGranularity): string | null => {
222
+ const handoffNoteFor = (h: import("../../src/core/types.ts").HandoffGranularity, hasDefaultIdle?: boolean, defaultRef?: string | null): string | null => {
223
223
  const map: Record<string, string> = {
224
224
  off: "Model per run (off/goal) — the whole run shares its hardest model; finer per-task routing requires task/subtask handoff",
225
225
  goal: "Model per run (off/goal) — the whole run shares its hardest model; finer per-task routing requires task/subtask handoff",
@@ -236,7 +236,9 @@ export default function (pi: ExtensionAPI): void {
236
236
  try {
237
237
  const { list } = loadFeatureList(dir);
238
238
  const { config } = loadConfig(dir);
239
- const handoffModelNote: string | null = handoffNoteFor((config.session?.handoff as import("../../src/core/types.ts").HandoffGranularity) ?? "task");
239
+ const _routerForWidget = (()=>{ try{ const {loadRouterConfig: _lrc} = require("../../src/modelRouter.ts"); return _lrc(dir); }catch{ return null as unknown as {default?: string; byDifficulty?: Record<string,string>}; }})() as {default?: string; byDifficulty?: Record<string,string>} | null;
240
+ const hasIdle = ((): boolean =>{ try{ if(!_routerForWidget?.default?.trim()) return false; const h=(config.session?.handoff as string) ?? "task"; if(h!=="task" && h!=="subtask") return false; let lanes: Array<{difficulty?: string}> = []; try{ lanes = flattenTasks(list) as unknown as Array<{difficulty?: string}>; }catch{ lanes = []; } if(!lanes.length) return false; return lanes.every(x=>{ const d=(x as {difficulty?: string}).difficulty; return d==="easy"||d==="moderate"||d==="difficult"; }); }catch{return false;} })();
241
+ const handoffModelNote: string | null = handoffNoteFor((config.session?.handoff as import("../../src/core/types.ts").HandoffGranularity) ?? "task", hasIdle, _routerForWidget?.default ?? null);
240
242
  const spent = escalationSummary(dir);
241
243
  // v3 viewState: widget must read daemon.json before rendering — deriveViewState is that check.
242
244
  let viewState: WidgetState["viewState"] = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infinity-harness",
3
- "version": "2.8.7",
4
- "description": "A pi agent extension that runs a gated build pipeline unattended enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
3
+ "version": "2.8.9",
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": [
7
7
  "pi-package",
@@ -10,7 +10,7 @@
10
10
  import type { HarnessConfig, GateHistoryEntry, Phase, Role } from "./types.ts";
11
11
  import { DEFAULT_ENABLED_PHASES, PHASE_ROLE } from "./types.ts";
12
12
  import { defaultDisplay, normalizeDisplay } from "../ui/display.ts";
13
- import { configPath } from "./paths.ts";
13
+ import { configPath, modelRouterPath } from "./paths.ts";
14
14
  import { readJson, writeJsonAtomic, backupOnce, fileExists } from "./fsx.ts";
15
15
  import { existsSync, readFileSync } from "node:fs";
16
16
 
@@ -181,10 +181,17 @@ function migrate(config: HarnessConfig, stored: Partial<HarnessConfig>): Harness
181
181
  }
182
182
  }
183
183
  // tiers: validate + migrate from model-router.json if empty
184
+ // C: single source — config.tiers is truth; legacy file migrates in.
185
+ // When config.tiers is {} and harness/model-router.json exists with
186
+ // byDifficulty/master/default, populate A=default, B=easy, C=moderate, D=difficult, X=master.
184
187
  {
185
188
  const t = normalizeTiers((out as Record<string, unknown>).tiers);
186
- if (t !== undefined) (out as Record<string, unknown>).tiers = t;
187
- else (out as Record<string, unknown>).tiers = {};
189
+ if (t !== undefined && Object.keys(t).length > 0) {
190
+ (out as Record<string, unknown>).tiers = t;
191
+ } else {
192
+ // targetDir not available in migrate(); migration happens in loadConfig where targetDir is known.
193
+ (out as Record<string, unknown>).tiers = t ?? {};
194
+ }
188
195
  }
189
196
 
190
197
  // The signal is what the *file* had, not what the merge produced: defaults
@@ -255,7 +262,61 @@ export function loadConfig(targetDir: string): LoadResult {
255
262
  if (raw === null) {
256
263
  return { ok: false, config: defaultConfig(), error: "harness/config.json is empty", seeded: true };
257
264
  }
258
- return { ok: true, config: migrate(deepMerge(defaultConfig(), raw), raw), error: null, seeded: false };
265
+ let cfg = migrate(deepMerge(defaultConfig(), raw), raw);
266
+ // C: migrate legacy harness/model-router.json -> config.tiers when empty.
267
+ // In-memory only until next saveConfig; legacy file kept for one release.
268
+ try {
269
+ const tiersNow = (cfg as unknown as { tiers?: Record<string, unknown> }).tiers ?? {};
270
+ if (Object.keys(tiersNow).length === 0) {
271
+
272
+ const mrPath = modelRouterPath(targetDir);
273
+ if (existsSync(mrPath)) {
274
+ const rawMr = JSON.parse(readFileSync(mrPath, "utf-8"));
275
+ if (typeof rawMr?.enabled === "boolean" && rawMr.enabled) {
276
+ const byDiff = (rawMr as { byDifficulty?: Record<string,string> }).byDifficulty ?? {};
277
+ const toTier = (ref: string | undefined): { provider: string; id: string } | null => {
278
+ if (!ref || !ref.trim()) return null;
279
+ const s = ref.trim();
280
+ const slash = s.indexOf("/");
281
+ if (slash <= 0 || slash === s.length-1) return null;
282
+ return { provider: s.slice(0, slash), id: s.slice(slash+1) };
283
+ };
284
+ const next: Record<string, { provider: string; id: string; thinkingLevel?: string }> = {};
285
+ const thinkByDiff = (rawMr as { thinkingByDifficulty?: Record<string,string> }).thinkingByDifficulty ?? {};
286
+ const tLvl = (k: string): string | undefined => {
287
+ const v = (thinkByDiff as Record<string,string>)[k];
288
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
289
+ };
290
+ // A = default
291
+ const a = toTier(rawMr.default as string | undefined);
292
+ if (a) next["A"] = { ...a, ...(tLvl("__default__") || (rawMr as { thinkingDefault?: string }).thinkingDefault ? { thinkingLevel: ((rawMr as { thinkingDefault?: string }).thinkingDefault || tLvl("__default__") || undefined) } : {}) };
293
+ // B/C/D from byDifficulty
294
+ const want: Array<[string,string]> = [["B","easy"],["C","moderate"],["D","difficult"]];
295
+ for (const [tier, diff] of want) {
296
+ const m = toTier(byDiff[diff]);
297
+ if (m) {
298
+ const tl = tLvl(diff) || undefined;
299
+ next[tier] = tl ? { ...m, thinkingLevel: tl as string } : m;
300
+ }
301
+ }
302
+ // X = master
303
+ const x = toTier(rawMr.master as string | undefined);
304
+ if (x) {
305
+ const masterTl = (rawMr as { thinkingMaster?: string }).thinkingMaster;
306
+ next["X"] = masterTl ? { ...x, thinkingLevel: masterTl } : x;
307
+ }
308
+ // also fill A thinkingDefault correctly (override above if needed)
309
+ if (next["A"] && (rawMr as { thinkingDefault?: string }).thinkingDefault) {
310
+ (next["A"] as { thinkingLevel?: string }).thinkingLevel = (rawMr as { thinkingDefault?: string }).thinkingDefault as string;
311
+ }
312
+ if (Object.keys(next).length > 0) {
313
+ (cfg as unknown as { tiers: unknown }).tiers = next;
314
+ }
315
+ }
316
+ }
317
+ }
318
+ } catch {}
319
+ return { ok: true, config: cfg, error: null, seeded: false };
259
320
  } catch (e) {
260
321
  const msg = e instanceof Error ? e.message : String(e);
261
322
  return { ok: false, config: defaultConfig(), error: msg, seeded: false };
package/src/core/init.ts CHANGED
@@ -264,6 +264,39 @@ export function initHarness(targetDir: string, options: InitOptions = {}): InitR
264
264
  merged.thinkingByDifficulty = { ...((existing.thinkingByDifficulty as Record<string,string>) ?? {}), ...(incoming.thinkingByDifficulty as Record<string,string>) };
265
265
  }
266
266
  writeFileSync(routerPath, JSON.stringify(merged, null, 2), "utf-8");
267
+ // C: also materialize config.tiers so daemon + supervisor share truth.
268
+ // Keep legacy file for one release (read fallback still honours it).
269
+ try {
270
+ const toTier = (ref: string | undefined): { provider: string; id: string } | null => {
271
+ if (!ref || !ref.trim()) return null;
272
+ const s = ref.trim(); const slash = s.indexOf("/");
273
+ if (slash <= 0 || slash === s.length-1) return null;
274
+ return { provider: s.slice(0, slash), id: s.slice(slash+1) };
275
+ };
276
+ const next: Record<string, { provider: string; id: string; thinkingLevel?: string }> = {};
277
+ const thinkByDiff = (incoming as { thinkingByDifficulty?: Record<string,string> }).thinkingByDifficulty ?? {};
278
+ const tLvl2 = (k: string): string | undefined => {
279
+ const v = (thinkByDiff as Record<string,string>)[k];
280
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
281
+ };
282
+ const a2 = toTier(incoming.default as string | undefined);
283
+ if (a2) {
284
+ const td = (incoming as { thinkingDefault?: string }).thinkingDefault;
285
+ next["A"] = td ? { ...a2, thinkingLevel: td } : a2;
286
+ }
287
+ for (const [tier, diff] of [["B","easy"],["C","moderate"],["D","difficult"]] as Array<[string,string]>) {
288
+ const m = toTier((incoming as Record<string,Record<string,string>>)?.byDifficulty?.[diff]);
289
+ if (m) { const tl = tLvl2(diff); next[tier] = tl ? { ...m, thinkingLevel: tl } : m; }
290
+ }
291
+ const x2 = toTier(incoming.master as string | undefined);
292
+ if (x2) {
293
+ const mt = (incoming as { thinkingMaster?: string }).thinkingMaster;
294
+ next["X"] = mt ? { ...x2, thinkingLevel: mt } : x2;
295
+ }
296
+ if (Object.keys(next).length > 0) {
297
+ (config as unknown as { tiers: unknown }).tiers = { ...((config as unknown as { tiers?: Record<string,unknown> })?.tiers ?? {}), ...next };
298
+ }
299
+ } catch {}
267
300
  } catch { /* best-effort */ }
268
301
  }
269
302
 
@@ -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
package/src/supervisor.ts CHANGED
@@ -50,7 +50,8 @@ import { readJsonSafe, writeJsonAtomic, fileExists } from "./core/fsx.ts";
50
50
  import { decideNext, stopFilePath, type LoopDecision } from "./loop.ts";
51
51
  import { loadRunState, countSession, disarmRun } from "./runState.ts";
52
52
  import { effectiveDifficultyForTask } from "./scheduler.ts";
53
- import { resolveModel, resolveThinking, loadRouterConfig } from "./modelRouter.ts";
53
+ import { resolveModel as legacyResolveModel, resolveThinking as legacyResolveThinking, loadRouterConfig } from "./modelRouter.ts";
54
+ import { routeModel as coreRouteModel, effectiveDifficultyForTask as coreEffectiveDifficulty } from "./core/modelRouter.ts";
54
55
  import {
55
56
  WorkerSession,
56
57
  WORKER_DIRECTIVE,
@@ -371,7 +372,9 @@ export function currentUnit(targetDir: string, baseModel?: string | null): WorkU
371
372
  })();
372
373
  if (!identity) return null;
373
374
 
374
- const routed = resolveModel({
375
+ // C: prefer config.tiers (core/modelRouter) — single source. Fallback to legacy model-router.json.
376
+ const useCore = (()=>{ try{ return Object.keys((config as unknown as { tiers?: Record<string,unknown> }).tiers ?? {}).length>0; }catch{return false;}})();
377
+ const routedLegacy = legacyResolveModel({
375
378
  projectDir: targetDir,
376
379
  task: task
377
380
  ? ({
@@ -386,12 +389,24 @@ export function currentUnit(targetDir: string, baseModel?: string | null): WorkU
386
389
  phase: phase ?? undefined,
387
390
  role: (config.currentRole ?? undefined) as string | undefined,
388
391
  });
389
- const thinking = resolveThinking({
392
+ const thinkingLegacy = legacyResolveThinking({
390
393
  projectDir: targetDir,
391
394
  task: difficulty ? ({ difficulty } as never) : undefined,
392
395
  feature: (feature ?? undefined) as never,
393
396
  sprint: (sprint ?? undefined) as never,
394
397
  });
398
+ let routed = routedLegacy;
399
+ let thinking = thinkingLegacy;
400
+ if (useCore) {
401
+ try {
402
+ const mod = require("./runState.ts") as { loadRunState: (d:string)=>{ baseModel?: {provider:string;id:string}}|null };
403
+ const rs2 = mod.loadRunState(targetDir);
404
+ const coreRes = coreRouteModel({ difficulty: difficulty ?? undefined, config, runState: rs2 as unknown as { baseModel?: {provider:string;id:string}} | null });
405
+ routed = coreRes.provider + "/" + coreRes.id;
406
+ const tierThinking = (()=>{ try{ const tiers=(config as unknown as { tiers?: Record<string,{thinkingLevel?: string}> }).tiers ?? {}; const tk = ({easy:"B",moderate:"C",difficult:"D"} as Record<string,string>)[String(difficulty ?? "")] ?? "A"; const th = (tiers as Record<string,{thinkingLevel?: string}>)[tk]?.thinkingLevel; return typeof th==="string"&&th ? th : undefined; }catch{return undefined;}})();
407
+ if (tierThinking) thinking = tierThinking as unknown as typeof thinking;
408
+ } catch {}
409
+ }
395
410
 
396
411
  return {
397
412
  level,
package/src/ui/wizard.ts CHANGED
@@ -226,7 +226,7 @@ async function pickModelsStep(
226
226
  if (masterModel === undefined) return undefined;
227
227
  const masterThinking = await pickThinkingLevel(prompt, "Consulting master — thinking level");
228
228
  if (masterThinking === undefined) return undefined;
229
- const defaultModel = await pickModelChoice(prompt, "Default — fallback when nothing more specific matches", models, "");
229
+ 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
230
  if (defaultModel === undefined) return undefined;
231
231
  const defaultThinking = await pickThinkingLevel(prompt, "Default — thinking level fallback");
232
232
  if (defaultThinking === undefined) return undefined;