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.
@@ -222595,7 +222595,7 @@ Anthropic.Models = Models2;
222595
222595
  Anthropic.Beta = Beta;
222596
222596
 
222597
222597
  // src/constants.ts
222598
- var CASCADE_VERSION = "0.18.0";
222598
+ var CASCADE_VERSION = "0.19.1";
222599
222599
  var CASCADE_CONFIG_DIR = ".cascade";
222600
222600
  var CASCADE_MD_FILE = "CASCADE.md";
222601
222601
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -231271,6 +231271,23 @@ var OpenAIProvider = class extends BaseProvider {
231271
231271
  };
231272
231272
 
231273
231273
  // src/providers/azure.ts
231274
+ function azureModelForDeployment(cfg) {
231275
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
231276
+ const id = cfg.deploymentName.trim();
231277
+ return {
231278
+ id,
231279
+ name: cfg.label?.trim() || id,
231280
+ provider: "azure",
231281
+ contextWindow: 128e3,
231282
+ isVisionCapable: false,
231283
+ inputCostPer1kTokens: 25e-4,
231284
+ outputCostPer1kTokens: 0.01,
231285
+ maxOutputTokens: 16e3,
231286
+ supportsStreaming: true,
231287
+ isLocal: false,
231288
+ supportsToolUse: true
231289
+ };
231290
+ }
231274
231291
  var AzureOpenAIProvider = class extends OpenAIProvider {
231275
231292
  constructor(config2, model) {
231276
231293
  const rawUrl = config2.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -231291,17 +231308,31 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
231291
231308
  });
231292
231309
  }
231293
231310
  async listModels() {
231294
- return [this.model];
231311
+ const fromDeployment = azureModelForDeployment(this.config);
231312
+ return [fromDeployment ?? this.model];
231295
231313
  }
231296
231314
  async isAvailable() {
231315
+ const params = {
231316
+ model: this.model.id,
231317
+ messages: [{ role: "user", content: "ping" }],
231318
+ max_tokens: 1
231319
+ };
231297
231320
  try {
231298
- await this.client.chat.completions.create({
231299
- model: this.model.id,
231300
- messages: [{ role: "user", content: "ping" }],
231301
- max_tokens: 1
231302
- });
231321
+ await this.client.chat.completions.create(params);
231303
231322
  return true;
231304
- } catch {
231323
+ } catch (err) {
231324
+ if (err?.message && String(err.message).includes("max_completion_tokens")) {
231325
+ try {
231326
+ await this.client.chat.completions.create({
231327
+ model: this.model.id,
231328
+ messages: [{ role: "user", content: "ping" }],
231329
+ max_completion_tokens: 1
231330
+ });
231331
+ return true;
231332
+ } catch {
231333
+ return false;
231334
+ }
231335
+ }
231305
231336
  return false;
231306
231337
  }
231307
231338
  }
@@ -250075,6 +250106,10 @@ var ModelSelector = class {
250075
250106
  actualId = parts.slice(1).join(":");
250076
250107
  }
250077
250108
  }
250109
+ const registered = this.availableModels.get(actualId);
250110
+ if (registered && (!providerStr || registered.provider === providerStr)) {
250111
+ return registered;
250112
+ }
250078
250113
  if (!providerStr) {
250079
250114
  const lower2 = actualId.toLowerCase();
250080
250115
  if (lower2.includes("claude")) providerStr = "anthropic";
@@ -250832,6 +250867,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
250832
250867
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
250833
250868
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
250834
250869
  }
250870
+ if (availableProviders.has("azure")) {
250871
+ for (const cfg of config2.providers) {
250872
+ const model = azureModelForDeployment(cfg);
250873
+ if (model) this.selector.addDynamicModel(model);
250874
+ }
250875
+ }
250835
250876
  for (const tier of ["T1", "T2", "T3"]) {
250836
250877
  const override = tier === "T1" ? config2.models.t1 : tier === "T2" ? config2.models.t2 : config2.models.t3;
250837
250878
  if (!override || override === "auto") continue;
@@ -251438,7 +251479,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
251438
251479
  ensureProvider(model, configs) {
251439
251480
  const key = `${model.provider}:${model.id}`;
251440
251481
  if (this.providers.has(key)) return;
251441
- const cfg = configs.find((c4) => c4.type === model.provider) ?? { type: model.provider };
251482
+ const cfg = (model.provider === "azure" ? configs.find((c4) => c4.type === "azure" && c4.deploymentName === model.id) : void 0) ?? configs.find((c4) => c4.type === model.provider) ?? { type: model.provider };
251442
251483
  const provider = this.createProvider(cfg, model);
251443
251484
  this.providers.set(key, provider);
251444
251485
  }
@@ -251598,6 +251639,12 @@ var BaseTier = class extends EventEmitter__default.default {
251598
251639
  * which would interleave, are not tagged.
251599
251640
  */
251600
251641
  isPresenter = false;
251642
+ /**
251643
+ * The model actually serving this tier (`provider:id`), once resolved —
251644
+ * rides on every tier:status event so the desktop can show which model ran
251645
+ * which node (Cockpit node panel / Why panel).
251646
+ */
251647
+ servingModel;
251601
251648
  constructor(role, id, parentId) {
251602
251649
  super();
251603
251650
  this.role = role;
@@ -251622,11 +251669,16 @@ var BaseTier = class extends EventEmitter__default.default {
251622
251669
  label: this.label,
251623
251670
  status,
251624
251671
  timestamp,
251625
- output
251672
+ output,
251673
+ model: this.servingModel
251626
251674
  };
251627
251675
  this.emit("status", event);
251628
251676
  this.emit("tier:status", event);
251629
251677
  }
251678
+ /** Record the model serving this tier; future status events carry it. */
251679
+ setServingModel(model) {
251680
+ this.servingModel = model || void 0;
251681
+ }
251630
251682
  setLabel(label) {
251631
251683
  this.label = label;
251632
251684
  }
@@ -251649,7 +251701,8 @@ var BaseTier = class extends EventEmitter__default.default {
251649
251701
  currentAction: update.currentAction,
251650
251702
  progressPct: update.progressPct,
251651
251703
  timestamp,
251652
- output: update.output
251704
+ output: update.output,
251705
+ model: this.servingModel
251653
251706
  });
251654
251707
  }
251655
251708
  buildMessage(type, to, payload) {
@@ -252015,6 +252068,21 @@ Available tools: ${tools.map((t4) => t4.name).join(", ")}.
252015
252068
  When you have enough information, stop calling tools and write your final answer.`;
252016
252069
  }
252017
252070
 
252071
+ // src/utils/truncate.ts
252072
+ function truncateForContext(text, maxChars = 12e3) {
252073
+ if (text.length <= maxChars) return text;
252074
+ const headLen = Math.floor(maxChars * 0.75);
252075
+ const tailLen = maxChars - headLen;
252076
+ const head = text.slice(0, headLen);
252077
+ const tail = text.slice(-tailLen);
252078
+ const elided = text.length - headLen - tailLen;
252079
+ return `${head}
252080
+
252081
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
252082
+
252083
+ ${tail}`;
252084
+ }
252085
+
252018
252086
  // src/core/tiers/t3-worker.ts
252019
252087
  var CriticalToolError = class extends Error {
252020
252088
  constructor(message, toolName) {
@@ -252322,6 +252390,7 @@ Now execute your subtask using this context where relevant.`
252322
252390
  } catch {
252323
252391
  }
252324
252392
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
252393
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
252325
252394
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
252326
252395
  let sentFullTextContract = false;
252327
252396
  let textContractSignature = "";
@@ -252419,7 +252488,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
252419
252488
  const toolResult = await this.executeTool(tc);
252420
252489
  await this.context.addMessage({
252421
252490
  role: "tool",
252422
- content: toolResult,
252491
+ content: truncateForContext(toolResult),
252423
252492
  toolCallId: tc.id
252424
252493
  });
252425
252494
  }
@@ -252676,15 +252745,17 @@ ${assignment.expectedOutput}`;
252676
252745
  };
252677
252746
  }
252678
252747
  requiresArtifact() {
252748
+ if (this.assignment?.files?.length) return true;
252679
252749
  const haystack = `${this.assignment?.description ?? ""}
252680
252750
  ${this.assignment?.expectedOutput ?? ""}`;
252681
252751
  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);
252682
252752
  }
252683
252753
  extractArtifactPaths(assignment) {
252754
+ const declared = (assignment.files ?? []).map((f4) => f4.trim()).filter((f4) => f4.includes("."));
252684
252755
  const haystack = `${assignment.description}
252685
252756
  ${assignment.expectedOutput}`;
252686
252757
  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) ?? [];
252687
- return [...new Set(matches.map((m3) => m3.trim()))];
252758
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m3) => m3.trim())])];
252688
252759
  }
252689
252760
  async verifyArtifacts(assignment) {
252690
252761
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -252807,7 +252878,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
252807
252878
  Assignment: ${assignment.description}
252808
252879
  Expected output: ${assignment.expectedOutput}
252809
252880
  Constraints: ${assignment.constraints.join("; ")}
252810
-
252881
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
252882
+ ${assignment.acceptance.map((a2) => `- ${a2}`).join("\n")}
252883
+ ` : ""}
252811
252884
  Output to test:
252812
252885
  ${output}
252813
252886
 
@@ -252896,17 +252969,27 @@ Your subtask:
252896
252969
  - Title: ${assignment.subtaskTitle}
252897
252970
  - Description: ${assignment.description}
252898
252971
  - Expected output: ${assignment.expectedOutput}
252899
- - Constraints: ${assignment.constraints.join("; ")}`;
252972
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
252973
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
252974
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
252900
252975
  }
252901
252976
  buildInitialPrompt(assignment) {
252902
252977
  return `Execute the following subtask completely:
252903
252978
 
252904
252979
  **${assignment.subtaskTitle}**
252905
-
252980
+ ${assignment.contextBrief ? `
252981
+ Context: ${assignment.contextBrief}
252982
+ ` : ""}
252906
252983
  ${assignment.description}
252907
252984
 
252908
252985
  Expected output: ${assignment.expectedOutput}
252909
-
252986
+ ${assignment.files?.length ? `
252987
+ Files you own (create or edit exactly these paths):
252988
+ ${assignment.files.map((f4) => `- ${f4}`).join("\n")}
252989
+ ` : ""}${assignment.acceptance?.length ? `
252990
+ Definition of done (your output must satisfy ALL of these):
252991
+ ${assignment.acceptance.map((a2) => `- ${a2}`).join("\n")}
252992
+ ` : ""}
252910
252993
  Constraints:
252911
252994
  ${assignment.constraints.map((c4) => `- ${c4}`).join("\n")}
252912
252995
 
@@ -253387,6 +253470,8 @@ var T2Manager = class extends BaseTier {
253387
253470
  this.assignment = assignment;
253388
253471
  this.taskId = taskId;
253389
253472
  this.setLabel(assignment.sectionTitle);
253473
+ const m3 = this.router.getModelForTier("T2");
253474
+ if (m3) this.setServingModel(`${m3.provider}:${m3.id}`);
253390
253475
  this.setStatus("ACTIVE");
253391
253476
  this.sendStatusUpdate({
253392
253477
  progressPct: 0,
@@ -253473,7 +253558,7 @@ Guidance (must be followed): ${decision.note}`
253473
253558
  // ── Private ──────────────────────────────────
253474
253559
  async decomposeSection(assignment) {
253475
253560
  const peerPlans = this.peerSyncBuffer.filter((p3) => p3.content?.type === "T2_PLAN_ANNOUNCEMENT").map((p3) => `[Peer ${p3.fromId} Plan]: ${p3.content.sectionTitle} - ${p3.content.subtaskTitles?.join(", ")}`).join("\n");
253476
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
253561
+ 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).
253477
253562
 
253478
253563
  Section: ${assignment.sectionTitle}
253479
253564
  Description: ${assignment.description}
@@ -253492,6 +253577,9 @@ Return a JSON array of subtask objects, each with:
253492
253577
  - peerT3Ids: string[] (empty for now)
253493
253578
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
253494
253579
  - executionMode: "parallel|sequential" (default is parallel)
253580
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
253581
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
253582
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
253495
253583
 
253496
253584
  Return ONLY the JSON array.`;
253497
253585
  const messages = [{ role: "user", content: prompt }];
@@ -254089,6 +254177,8 @@ var T1Administrator = class extends BaseTier {
254089
254177
  this.signal = signal;
254090
254178
  this.taskId = crypto4.randomUUID();
254091
254179
  this.setLabel("Administrator");
254180
+ const m3 = this.router.getModelForTier("T1");
254181
+ if (m3) this.setServingModel(`${m3.provider}:${m3.id}`);
254092
254182
  this.setStatus("ACTIVE");
254093
254183
  this.taskGoal = userPrompt;
254094
254184
  this.sendStatusUpdate({
@@ -254335,10 +254425,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
254335
254425
  "description": "Run npm init",
254336
254426
  "expectedOutput": "package.json created",
254337
254427
  "constraints": [],
254338
- "dependsOn": []
254428
+ "dependsOn": [],
254429
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
254430
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
254431
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
254339
254432
  }]
254340
254433
  }, {
254341
- "sectionId": "s2",
254434
+ "sectionId": "s2",
254342
254435
  "sectionTitle": "Write Tests",
254343
254436
  "description": "Write tests for the project",
254344
254437
  "expectedOutput": "Tests passing",
@@ -254348,7 +254441,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
254348
254441
  }]
254349
254442
  }
254350
254443
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
254351
- Leave dependsOn empty for sections that can run immediately in parallel.`;
254444
+ Leave dependsOn empty for sections that can run immediately in parallel.
254445
+
254446
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
254447
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
254448
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
254449
+ - "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.
254450
+ - 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.`;
254352
254451
  const messages = [{ role: "user", content: decompositionPrompt }];
254353
254452
  const result = await this.router.generate("T1", {
254354
254453
  messages,
@@ -282539,30 +282638,56 @@ async function searchTavily(query, apiKey, maxResults) {
282539
282638
  engine: "tavily"
282540
282639
  }));
282541
282640
  }
282542
- async function searchDuckDuckGoLite(query, maxResults) {
282543
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
282544
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
282545
- signal: AbortSignal.timeout(1e4)
282546
- });
282547
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
282548
- const html = await resp.text();
282549
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
282550
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
282551
- const links = [];
282552
- const snippets = [];
282641
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
282642
+ function unwrapDdgRedirect(href) {
282643
+ try {
282644
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
282645
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
282646
+ const target = url.searchParams.get("uddg");
282647
+ if (target) return decodeURIComponent(target);
282648
+ }
282649
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
282650
+ } catch {
282651
+ return href;
282652
+ }
282653
+ }
282654
+ function stripTags(html) {
282655
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
282656
+ }
282657
+ function decodeEntities(text) {
282658
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
282659
+ }
282660
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
282661
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
282662
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
282663
+ const results = [];
282553
282664
  let m3;
282554
- while ((m3 = linkPattern.exec(html)) !== null) {
282555
- links.push({ url: m3[1], title: m3[2].trim() });
282665
+ while ((m3 = anchorRe.exec(html)) !== null) {
282666
+ const tag = m3[0];
282667
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
282668
+ const title = decodeEntities(stripTags(m3[1] ?? ""));
282669
+ if (!href || !title) continue;
282670
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
282556
282671
  }
282557
- while ((m3 = snippetPattern.exec(html)) !== null) {
282558
- snippets.push(m3[1].replace(/<[^>]+>/g, "").trim());
282672
+ const snippets = [];
282673
+ while ((m3 = snippetRe.exec(html)) !== null) {
282674
+ snippets.push(decodeEntities(stripTags(m3[1] ?? "")));
282559
282675
  }
282560
- return links.slice(0, maxResults).map((link, i4) => ({
282561
- title: link.title,
282562
- url: link.url,
282563
- snippet: snippets[i4] ?? "",
282564
- engine: "duckduckgo-lite"
282565
- }));
282676
+ for (let i4 = 0; i4 < results.length; i4++) {
282677
+ if (snippets[i4]) results[i4].snippet = snippets[i4];
282678
+ }
282679
+ return results;
282680
+ }
282681
+ async function searchDuckDuckGo(query, maxResults, variant) {
282682
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
282683
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
282684
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
282685
+ signal: AbortSignal.timeout(1e4)
282686
+ });
282687
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
282688
+ const html = await resp.text();
282689
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
282690
+ return parsed.slice(0, maxResults).map((r4) => ({ ...r4, engine: `duckduckgo-${variant}` }));
282566
282691
  }
282567
282692
  var WebSearchTool = class extends BaseTool {
282568
282693
  name = "web_search";
@@ -282621,12 +282746,14 @@ var WebSearchTool = class extends BaseTool {
282621
282746
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
282622
282747
  }
282623
282748
  }
282624
- try {
282625
- results = await searchDuckDuckGoLite(query, maxResults);
282626
- if (results.length > 0) return this.formatResults(query, results);
282627
- errors.push("DuckDuckGo Lite: returned 0 results");
282628
- } catch (err) {
282629
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
282749
+ for (const variant of ["html", "lite"]) {
282750
+ try {
282751
+ results = await searchDuckDuckGo(query, maxResults, variant);
282752
+ if (results.length > 0) return this.formatResults(query, results);
282753
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
282754
+ } catch (err) {
282755
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
282756
+ }
282630
282757
  }
282631
282758
  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" : "";
282632
282759
  return [
@@ -294855,8 +294982,11 @@ var CascadeConfigSchema = external_exports.object({
294855
294982
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
294856
294983
  * tier based on task type and complexity, overriding the static priority lists.
294857
294984
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
294985
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
294986
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
294987
+ * model pins are unaffected; disable via config/Settings → Advanced.
294858
294988
  */
294859
- cascadeAuto: external_exports.boolean().default(false),
294989
+ cascadeAuto: external_exports.boolean().default(true),
294860
294990
  /**
294861
294991
  * Cascade Auto trade-off bias when picking a model for a task:
294862
294992
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -296572,13 +296702,30 @@ ${last3.partialOutput}` : "");
296572
296702
  * explicit multi-part structure, so ordinary single-file asks (handled as
296573
296703
  * Simple/Moderate) don't get over-escalated.
296574
296704
  */
296575
- looksClearlyComplex(prompt) {
296705
+ /** Shared build/scale signals for the complexity floors below. */
296706
+ buildSignals(prompt) {
296576
296707
  const p3 = prompt.trim();
296577
- if (p3.length < 24) return false;
296708
+ if (p3.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
296578
296709
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p3);
296579
- 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(p3);
296710
+ const scaleCount = (p3.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;
296580
296711
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p3);
296581
- return buildVerb && (scaleNoun || multiPart);
296712
+ return { buildVerb, scaleCount, multiPart };
296713
+ }
296714
+ /**
296715
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
296716
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
296717
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
296718
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
296719
+ * × workers for a task one worker handles).
296720
+ */
296721
+ looksClearlyComplex(prompt) {
296722
+ const s3 = this.buildSignals(prompt);
296723
+ return s3.buildVerb && (s3.scaleCount >= 2 || s3.scaleCount >= 1 && s3.multiPart);
296724
+ }
296725
+ /** A small single-deliverable build — real work, but one manager's worth. */
296726
+ looksLikeModerateBuild(prompt) {
296727
+ const s3 = this.buildSignals(prompt);
296728
+ return s3.buildVerb && (s3.scaleCount >= 1 || s3.multiPart);
296582
296729
  }
296583
296730
  // Cache glob scan results per workspace path to avoid repeated I/O.
296584
296731
  static globCache = /* @__PURE__ */ new Map();
@@ -296666,6 +296813,9 @@ ${prompt}` : prompt;
296666
296813
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
296667
296814
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
296668
296815
  verdict = "Complex";
296816
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
296817
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
296818
+ verdict = "Moderate";
296669
296819
  } else {
296670
296820
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
296671
296821
  }