cascade-ai 0.18.0 → 0.19.1

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.cjs CHANGED
@@ -236,7 +236,7 @@ var init_audit_logger = __esm({
236
236
  });
237
237
 
238
238
  // src/constants.ts
239
- var CASCADE_VERSION = "0.18.0";
239
+ var CASCADE_VERSION = "0.19.1";
240
240
  var CASCADE_CONFIG_DIR = ".cascade";
241
241
  var CASCADE_MD_FILE = "CASCADE.md";
242
242
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -1039,6 +1039,23 @@ var OpenAIProvider = class extends BaseProvider {
1039
1039
  };
1040
1040
 
1041
1041
  // src/providers/azure.ts
1042
+ function azureModelForDeployment(cfg) {
1043
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
1044
+ const id = cfg.deploymentName.trim();
1045
+ return {
1046
+ id,
1047
+ name: cfg.label?.trim() || id,
1048
+ provider: "azure",
1049
+ contextWindow: 128e3,
1050
+ isVisionCapable: false,
1051
+ inputCostPer1kTokens: 25e-4,
1052
+ outputCostPer1kTokens: 0.01,
1053
+ maxOutputTokens: 16e3,
1054
+ supportsStreaming: true,
1055
+ isLocal: false,
1056
+ supportsToolUse: true
1057
+ };
1058
+ }
1042
1059
  var AzureOpenAIProvider = class extends OpenAIProvider {
1043
1060
  constructor(config, model) {
1044
1061
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -1059,17 +1076,31 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
1059
1076
  });
1060
1077
  }
1061
1078
  async listModels() {
1062
- return [this.model];
1079
+ const fromDeployment = azureModelForDeployment(this.config);
1080
+ return [fromDeployment ?? this.model];
1063
1081
  }
1064
1082
  async isAvailable() {
1083
+ const params = {
1084
+ model: this.model.id,
1085
+ messages: [{ role: "user", content: "ping" }],
1086
+ max_tokens: 1
1087
+ };
1065
1088
  try {
1066
- await this.client.chat.completions.create({
1067
- model: this.model.id,
1068
- messages: [{ role: "user", content: "ping" }],
1069
- max_tokens: 1
1070
- });
1089
+ await this.client.chat.completions.create(params);
1071
1090
  return true;
1072
- } catch {
1091
+ } catch (err) {
1092
+ if (err?.message && String(err.message).includes("max_completion_tokens")) {
1093
+ try {
1094
+ await this.client.chat.completions.create({
1095
+ model: this.model.id,
1096
+ messages: [{ role: "user", content: "ping" }],
1097
+ max_completion_tokens: 1
1098
+ });
1099
+ return true;
1100
+ } catch {
1101
+ return false;
1102
+ }
1103
+ }
1073
1104
  return false;
1074
1105
  }
1075
1106
  }
@@ -1811,6 +1842,10 @@ var ModelSelector = class {
1811
1842
  actualId = parts.slice(1).join(":");
1812
1843
  }
1813
1844
  }
1845
+ const registered = this.availableModels.get(actualId);
1846
+ if (registered && (!providerStr || registered.provider === providerStr)) {
1847
+ return registered;
1848
+ }
1814
1849
  if (!providerStr) {
1815
1850
  const lower = actualId.toLowerCase();
1816
1851
  if (lower.includes("claude")) providerStr = "anthropic";
@@ -2568,6 +2603,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2568
2603
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
2569
2604
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
2570
2605
  }
2606
+ if (availableProviders.has("azure")) {
2607
+ for (const cfg of config.providers) {
2608
+ const model = azureModelForDeployment(cfg);
2609
+ if (model) this.selector.addDynamicModel(model);
2610
+ }
2611
+ }
2571
2612
  for (const tier of ["T1", "T2", "T3"]) {
2572
2613
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
2573
2614
  if (!override || override === "auto") continue;
@@ -3174,7 +3215,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
3174
3215
  ensureProvider(model, configs) {
3175
3216
  const key = `${model.provider}:${model.id}`;
3176
3217
  if (this.providers.has(key)) return;
3177
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
3218
+ const cfg = (model.provider === "azure" ? configs.find((c) => c.type === "azure" && c.deploymentName === model.id) : void 0) ?? configs.find((c) => c.type === model.provider) ?? { type: model.provider };
3178
3219
  const provider = this.createProvider(cfg, model);
3179
3220
  this.providers.set(key, provider);
3180
3221
  }
@@ -3334,6 +3375,12 @@ var BaseTier = class extends EventEmitter__default.default {
3334
3375
  * which would interleave, are not tagged.
3335
3376
  */
3336
3377
  isPresenter = false;
3378
+ /**
3379
+ * The model actually serving this tier (`provider:id`), once resolved —
3380
+ * rides on every tier:status event so the desktop can show which model ran
3381
+ * which node (Cockpit node panel / Why panel).
3382
+ */
3383
+ servingModel;
3337
3384
  constructor(role, id, parentId) {
3338
3385
  super();
3339
3386
  this.role = role;
@@ -3358,11 +3405,16 @@ var BaseTier = class extends EventEmitter__default.default {
3358
3405
  label: this.label,
3359
3406
  status,
3360
3407
  timestamp,
3361
- output
3408
+ output,
3409
+ model: this.servingModel
3362
3410
  };
3363
3411
  this.emit("status", event);
3364
3412
  this.emit("tier:status", event);
3365
3413
  }
3414
+ /** Record the model serving this tier; future status events carry it. */
3415
+ setServingModel(model) {
3416
+ this.servingModel = model || void 0;
3417
+ }
3366
3418
  setLabel(label) {
3367
3419
  this.label = label;
3368
3420
  }
@@ -3385,7 +3437,8 @@ var BaseTier = class extends EventEmitter__default.default {
3385
3437
  currentAction: update.currentAction,
3386
3438
  progressPct: update.progressPct,
3387
3439
  timestamp,
3388
- output: update.output
3440
+ output: update.output,
3441
+ model: this.servingModel
3389
3442
  });
3390
3443
  }
3391
3444
  buildMessage(type, to, payload) {
@@ -3751,6 +3804,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
3751
3804
  When you have enough information, stop calling tools and write your final answer.`;
3752
3805
  }
3753
3806
 
3807
+ // src/utils/truncate.ts
3808
+ function truncateForContext(text, maxChars = 12e3) {
3809
+ if (text.length <= maxChars) return text;
3810
+ const headLen = Math.floor(maxChars * 0.75);
3811
+ const tailLen = maxChars - headLen;
3812
+ const head = text.slice(0, headLen);
3813
+ const tail = text.slice(-tailLen);
3814
+ const elided = text.length - headLen - tailLen;
3815
+ return `${head}
3816
+
3817
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
3818
+
3819
+ ${tail}`;
3820
+ }
3821
+
3754
3822
  // src/core/tiers/t3-worker.ts
3755
3823
  var CriticalToolError = class extends Error {
3756
3824
  constructor(message, toolName) {
@@ -4058,6 +4126,7 @@ Now execute your subtask using this context where relevant.`
4058
4126
  } catch {
4059
4127
  }
4060
4128
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
4129
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
4061
4130
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
4062
4131
  let sentFullTextContract = false;
4063
4132
  let textContractSignature = "";
@@ -4155,7 +4224,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4155
4224
  const toolResult = await this.executeTool(tc);
4156
4225
  await this.context.addMessage({
4157
4226
  role: "tool",
4158
- content: toolResult,
4227
+ content: truncateForContext(toolResult),
4159
4228
  toolCallId: tc.id
4160
4229
  });
4161
4230
  }
@@ -4412,15 +4481,17 @@ ${assignment.expectedOutput}`;
4412
4481
  };
4413
4482
  }
4414
4483
  requiresArtifact() {
4484
+ if (this.assignment?.files?.length) return true;
4415
4485
  const haystack = `${this.assignment?.description ?? ""}
4416
4486
  ${this.assignment?.expectedOutput ?? ""}`;
4417
4487
  return /\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|png|jpg|jpeg|svg|gif)\b/i.test(haystack) || /save (?:a|the)? file|create (?:a|the)? file|write (?:a|the)? file/i.test(haystack);
4418
4488
  }
4419
4489
  extractArtifactPaths(assignment) {
4490
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
4420
4491
  const haystack = `${assignment.description}
4421
4492
  ${assignment.expectedOutput}`;
4422
4493
  const matches = haystack.match(/\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|png|jpg|jpeg|svg|gif)\b/gi) ?? [];
4423
- return [...new Set(matches.map((m) => m.trim()))];
4494
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
4424
4495
  }
4425
4496
  async verifyArtifacts(assignment) {
4426
4497
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -4543,7 +4614,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4543
4614
  Assignment: ${assignment.description}
4544
4615
  Expected output: ${assignment.expectedOutput}
4545
4616
  Constraints: ${assignment.constraints.join("; ")}
4546
-
4617
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
4618
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4619
+ ` : ""}
4547
4620
  Output to test:
4548
4621
  ${output}
4549
4622
 
@@ -4632,17 +4705,27 @@ Your subtask:
4632
4705
  - Title: ${assignment.subtaskTitle}
4633
4706
  - Description: ${assignment.description}
4634
4707
  - Expected output: ${assignment.expectedOutput}
4635
- - Constraints: ${assignment.constraints.join("; ")}`;
4708
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
4709
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
4710
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
4636
4711
  }
4637
4712
  buildInitialPrompt(assignment) {
4638
4713
  return `Execute the following subtask completely:
4639
4714
 
4640
4715
  **${assignment.subtaskTitle}**
4641
-
4716
+ ${assignment.contextBrief ? `
4717
+ Context: ${assignment.contextBrief}
4718
+ ` : ""}
4642
4719
  ${assignment.description}
4643
4720
 
4644
4721
  Expected output: ${assignment.expectedOutput}
4645
-
4722
+ ${assignment.files?.length ? `
4723
+ Files you own (create or edit exactly these paths):
4724
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
4725
+ ` : ""}${assignment.acceptance?.length ? `
4726
+ Definition of done (your output must satisfy ALL of these):
4727
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4728
+ ` : ""}
4646
4729
  Constraints:
4647
4730
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
4648
4731
 
@@ -5123,6 +5206,8 @@ var T2Manager = class extends BaseTier {
5123
5206
  this.assignment = assignment;
5124
5207
  this.taskId = taskId;
5125
5208
  this.setLabel(assignment.sectionTitle);
5209
+ const m = this.router.getModelForTier("T2");
5210
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5126
5211
  this.setStatus("ACTIVE");
5127
5212
  this.sendStatusUpdate({
5128
5213
  progressPct: 0,
@@ -5209,7 +5294,7 @@ Guidance (must be followed): ${decision.note}`
5209
5294
  // ── Private ──────────────────────────────────
5210
5295
  async decomposeSection(assignment) {
5211
5296
  const peerPlans = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_PLAN_ANNOUNCEMENT").map((p) => `[Peer ${p.fromId} Plan]: ${p.content.sectionTitle} - ${p.content.subtaskTitles?.join(", ")}`).join("\n");
5212
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
5297
+ const prompt = `Decompose this section into 1-4 concrete subtasks for T3 workers \u2014 the FEWEST that fully cover it (one subtask is the correct answer for a small section).
5213
5298
 
5214
5299
  Section: ${assignment.sectionTitle}
5215
5300
  Description: ${assignment.description}
@@ -5228,6 +5313,9 @@ Return a JSON array of subtask objects, each with:
5228
5313
  - peerT3Ids: string[] (empty for now)
5229
5314
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
5230
5315
  - executionMode: "parallel|sequential" (default is parallel)
5316
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
5317
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
5318
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
5231
5319
 
5232
5320
  Return ONLY the JSON array.`;
5233
5321
  const messages = [{ role: "user", content: prompt }];
@@ -5825,6 +5913,8 @@ var T1Administrator = class extends BaseTier {
5825
5913
  this.signal = signal;
5826
5914
  this.taskId = crypto3.randomUUID();
5827
5915
  this.setLabel("Administrator");
5916
+ const m = this.router.getModelForTier("T1");
5917
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5828
5918
  this.setStatus("ACTIVE");
5829
5919
  this.taskGoal = userPrompt;
5830
5920
  this.sendStatusUpdate({
@@ -6071,10 +6161,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6071
6161
  "description": "Run npm init",
6072
6162
  "expectedOutput": "package.json created",
6073
6163
  "constraints": [],
6074
- "dependsOn": []
6164
+ "dependsOn": [],
6165
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
6166
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
6167
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
6075
6168
  }]
6076
6169
  }, {
6077
- "sectionId": "s2",
6170
+ "sectionId": "s2",
6078
6171
  "sectionTitle": "Write Tests",
6079
6172
  "description": "Write tests for the project",
6080
6173
  "expectedOutput": "Tests passing",
@@ -6084,7 +6177,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6084
6177
  }]
6085
6178
  }
6086
6179
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
6087
- Leave dependsOn empty for sections that can run immediately in parallel.`;
6180
+ Leave dependsOn empty for sections that can run immediately in parallel.
6181
+
6182
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
6183
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
6184
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
6185
+ - "contextBrief": 1-3 short sentences with the ONLY background the worker needs. It sees nothing else about the task, so make the brief self-sufficient \u2014 but never pad it.
6186
+ - RIGHT-SIZE the plan: use the FEWEST sections and workers that fully cover the task. One section with 1-2 subtasks is the CORRECT plan for a small task; padding a plan with filler sections wastes the user's money.`;
6088
6187
  const messages = [{ role: "user", content: decompositionPrompt }];
6089
6188
  const result = await this.router.generate("T1", {
6090
6189
  messages,
@@ -7386,30 +7485,56 @@ async function searchTavily(query, apiKey, maxResults) {
7386
7485
  engine: "tavily"
7387
7486
  }));
7388
7487
  }
7389
- async function searchDuckDuckGoLite(query, maxResults) {
7390
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
7391
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
7392
- signal: AbortSignal.timeout(1e4)
7393
- });
7394
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
7395
- const html = await resp.text();
7396
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
7397
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
7398
- const links = [];
7399
- const snippets = [];
7488
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
7489
+ function unwrapDdgRedirect(href) {
7490
+ try {
7491
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
7492
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
7493
+ const target = url.searchParams.get("uddg");
7494
+ if (target) return decodeURIComponent(target);
7495
+ }
7496
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
7497
+ } catch {
7498
+ return href;
7499
+ }
7500
+ }
7501
+ function stripTags(html) {
7502
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
7503
+ }
7504
+ function decodeEntities(text) {
7505
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
7506
+ }
7507
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
7508
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
7509
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
7510
+ const results = [];
7400
7511
  let m;
7401
- while ((m = linkPattern.exec(html)) !== null) {
7402
- links.push({ url: m[1], title: m[2].trim() });
7512
+ while ((m = anchorRe.exec(html)) !== null) {
7513
+ const tag = m[0];
7514
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
7515
+ const title = decodeEntities(stripTags(m[1] ?? ""));
7516
+ if (!href || !title) continue;
7517
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
7518
+ }
7519
+ const snippets = [];
7520
+ while ((m = snippetRe.exec(html)) !== null) {
7521
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
7403
7522
  }
7404
- while ((m = snippetPattern.exec(html)) !== null) {
7405
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
7523
+ for (let i = 0; i < results.length; i++) {
7524
+ if (snippets[i]) results[i].snippet = snippets[i];
7406
7525
  }
7407
- return links.slice(0, maxResults).map((link, i) => ({
7408
- title: link.title,
7409
- url: link.url,
7410
- snippet: snippets[i] ?? "",
7411
- engine: "duckduckgo-lite"
7412
- }));
7526
+ return results;
7527
+ }
7528
+ async function searchDuckDuckGo(query, maxResults, variant) {
7529
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
7530
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
7531
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
7532
+ signal: AbortSignal.timeout(1e4)
7533
+ });
7534
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
7535
+ const html = await resp.text();
7536
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
7537
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
7413
7538
  }
7414
7539
  var WebSearchTool = class extends BaseTool {
7415
7540
  name = "web_search";
@@ -7468,12 +7593,14 @@ var WebSearchTool = class extends BaseTool {
7468
7593
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
7469
7594
  }
7470
7595
  }
7471
- try {
7472
- results = await searchDuckDuckGoLite(query, maxResults);
7473
- if (results.length > 0) return this.formatResults(query, results);
7474
- errors.push("DuckDuckGo Lite: returned 0 results");
7475
- } catch (err) {
7476
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
7596
+ for (const variant of ["html", "lite"]) {
7597
+ try {
7598
+ results = await searchDuckDuckGo(query, maxResults, variant);
7599
+ if (results.length > 0) return this.formatResults(query, results);
7600
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
7601
+ } catch (err) {
7602
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
7603
+ }
7477
7604
  }
7478
7605
  const configHint = !this.config.searxngUrl && !this.config.braveApiKey && !this.config.tavilyApiKey ? "\nTip: Configure a search backend for better results:\n \u2022 Self-hosted: set SEARXNG_URL in your environment\n \u2022 Brave Search API: set BRAVE_SEARCH_API_KEY\n \u2022 Tavily API: set TAVILY_API_KEY" : "";
7479
7606
  return [
@@ -8417,8 +8544,11 @@ var CascadeConfigSchema = zod.z.object({
8417
8544
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
8418
8545
  * tier based on task type and complexity, overriding the static priority lists.
8419
8546
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
8547
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
8548
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
8549
+ * model pins are unaffected; disable via config/Settings → Advanced.
8420
8550
  */
8421
- cascadeAuto: zod.z.boolean().default(false),
8551
+ cascadeAuto: zod.z.boolean().default(true),
8422
8552
  /**
8423
8553
  * Cascade Auto trade-off bias when picking a model for a task:
8424
8554
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -10131,13 +10261,30 @@ ${last.partialOutput}` : "");
10131
10261
  * explicit multi-part structure, so ordinary single-file asks (handled as
10132
10262
  * Simple/Moderate) don't get over-escalated.
10133
10263
  */
10134
- looksClearlyComplex(prompt) {
10264
+ /** Shared build/scale signals for the complexity floors below. */
10265
+ buildSignals(prompt) {
10135
10266
  const p = prompt.trim();
10136
- if (p.length < 24) return false;
10267
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
10137
10268
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
10138
- const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
10269
+ const scaleCount = (p.match(/\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/gi) ?? []).length;
10139
10270
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
10140
- return buildVerb && (scaleNoun || multiPart);
10271
+ return { buildVerb, scaleCount, multiPart };
10272
+ }
10273
+ /**
10274
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
10275
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
10276
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
10277
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
10278
+ * × workers for a task one worker handles).
10279
+ */
10280
+ looksClearlyComplex(prompt) {
10281
+ const s = this.buildSignals(prompt);
10282
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
10283
+ }
10284
+ /** A small single-deliverable build — real work, but one manager's worth. */
10285
+ looksLikeModerateBuild(prompt) {
10286
+ const s = this.buildSignals(prompt);
10287
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
10141
10288
  }
10142
10289
  // Cache glob scan results per workspace path to avoid repeated I/O.
10143
10290
  static globCache = /* @__PURE__ */ new Map();
@@ -10225,6 +10372,9 @@ ${prompt}` : prompt;
10225
10372
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
10226
10373
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
10227
10374
  verdict = "Complex";
10375
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
10376
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
10377
+ verdict = "Moderate";
10228
10378
  } else {
10229
10379
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
10230
10380
  }