cascade-ai 0.18.0 → 0.19.0

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/cli.cjs CHANGED
@@ -111,7 +111,7 @@ var __export = (target, all) => {
111
111
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_CREDENTIALS_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
112
112
  var init_constants = __esm({
113
113
  "src/constants.ts"() {
114
- CASCADE_VERSION = "0.18.0";
114
+ CASCADE_VERSION = "0.19.0";
115
115
  CASCADE_CONFIG_FILE = ".cascade/config.json";
116
116
  CASCADE_DB_FILE = ".cascade/memory.db";
117
117
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -937,8 +937,26 @@ var init_openai = __esm({
937
937
  // src/providers/azure.ts
938
938
  var azure_exports = {};
939
939
  __export(azure_exports, {
940
- AzureOpenAIProvider: () => AzureOpenAIProvider
940
+ AzureOpenAIProvider: () => AzureOpenAIProvider,
941
+ azureModelForDeployment: () => azureModelForDeployment
941
942
  });
943
+ function azureModelForDeployment(cfg) {
944
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
945
+ const id = cfg.deploymentName.trim();
946
+ return {
947
+ id,
948
+ name: cfg.label?.trim() || id,
949
+ provider: "azure",
950
+ contextWindow: 128e3,
951
+ isVisionCapable: false,
952
+ inputCostPer1kTokens: 25e-4,
953
+ outputCostPer1kTokens: 0.01,
954
+ maxOutputTokens: 16e3,
955
+ supportsStreaming: true,
956
+ isLocal: false,
957
+ supportsToolUse: true
958
+ };
959
+ }
942
960
  var AzureOpenAIProvider;
943
961
  var init_azure = __esm({
944
962
  "src/providers/azure.ts"() {
@@ -964,7 +982,8 @@ var init_azure = __esm({
964
982
  });
965
983
  }
966
984
  async listModels() {
967
- return [this.model];
985
+ const fromDeployment = azureModelForDeployment(this.config);
986
+ return [fromDeployment ?? this.model];
968
987
  }
969
988
  async isAvailable() {
970
989
  try {
@@ -3049,8 +3068,11 @@ var CascadeConfigSchema = zod.z.object({
3049
3068
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
3050
3069
  * tier based on task type and complexity, overriding the static priority lists.
3051
3070
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
3071
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
3072
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
3073
+ * model pins are unaffected; disable via config/Settings → Advanced.
3052
3074
  */
3053
- cascadeAuto: zod.z.boolean().default(false),
3075
+ cascadeAuto: zod.z.boolean().default(true),
3054
3076
  /**
3055
3077
  * Cascade Auto trade-off bias when picking a model for a task:
3056
3078
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -4604,6 +4626,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4604
4626
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
4605
4627
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
4606
4628
  }
4629
+ if (availableProviders.has("azure")) {
4630
+ for (const cfg of config.providers) {
4631
+ const model = azureModelForDeployment(cfg);
4632
+ if (model) this.selector.addDynamicModel(model);
4633
+ }
4634
+ }
4607
4635
  for (const tier of ["T1", "T2", "T3"]) {
4608
4636
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
4609
4637
  if (!override || override === "auto") continue;
@@ -5210,7 +5238,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5210
5238
  ensureProvider(model, configs) {
5211
5239
  const key = `${model.provider}:${model.id}`;
5212
5240
  if (this.providers.has(key)) return;
5213
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
5241
+ 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 };
5214
5242
  const provider = this.createProvider(cfg, model);
5215
5243
  this.providers.set(key, provider);
5216
5244
  }
@@ -5370,6 +5398,12 @@ var BaseTier = class extends EventEmitter__default.default {
5370
5398
  * which would interleave, are not tagged.
5371
5399
  */
5372
5400
  isPresenter = false;
5401
+ /**
5402
+ * The model actually serving this tier (`provider:id`), once resolved —
5403
+ * rides on every tier:status event so the desktop can show which model ran
5404
+ * which node (Cockpit node panel / Why panel).
5405
+ */
5406
+ servingModel;
5373
5407
  constructor(role, id, parentId) {
5374
5408
  super();
5375
5409
  this.role = role;
@@ -5394,11 +5428,16 @@ var BaseTier = class extends EventEmitter__default.default {
5394
5428
  label: this.label,
5395
5429
  status,
5396
5430
  timestamp,
5397
- output
5431
+ output,
5432
+ model: this.servingModel
5398
5433
  };
5399
5434
  this.emit("status", event);
5400
5435
  this.emit("tier:status", event);
5401
5436
  }
5437
+ /** Record the model serving this tier; future status events carry it. */
5438
+ setServingModel(model) {
5439
+ this.servingModel = model || void 0;
5440
+ }
5402
5441
  setLabel(label) {
5403
5442
  this.label = label;
5404
5443
  }
@@ -5421,7 +5460,8 @@ var BaseTier = class extends EventEmitter__default.default {
5421
5460
  currentAction: update.currentAction,
5422
5461
  progressPct: update.progressPct,
5423
5462
  timestamp,
5424
- output: update.output
5463
+ output: update.output,
5464
+ model: this.servingModel
5425
5465
  });
5426
5466
  }
5427
5467
  buildMessage(type, to, payload) {
@@ -5788,6 +5828,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
5788
5828
  When you have enough information, stop calling tools and write your final answer.`;
5789
5829
  }
5790
5830
 
5831
+ // src/utils/truncate.ts
5832
+ function truncateForContext(text, maxChars = 12e3) {
5833
+ if (text.length <= maxChars) return text;
5834
+ const headLen = Math.floor(maxChars * 0.75);
5835
+ const tailLen = maxChars - headLen;
5836
+ const head = text.slice(0, headLen);
5837
+ const tail = text.slice(-tailLen);
5838
+ const elided = text.length - headLen - tailLen;
5839
+ return `${head}
5840
+
5841
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
5842
+
5843
+ ${tail}`;
5844
+ }
5845
+
5791
5846
  // src/core/tiers/t3-worker.ts
5792
5847
  var CriticalToolError = class extends Error {
5793
5848
  constructor(message, toolName) {
@@ -6095,6 +6150,7 @@ Now execute your subtask using this context where relevant.`
6095
6150
  } catch {
6096
6151
  }
6097
6152
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
6153
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
6098
6154
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
6099
6155
  let sentFullTextContract = false;
6100
6156
  let textContractSignature = "";
@@ -6192,7 +6248,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6192
6248
  const toolResult = await this.executeTool(tc);
6193
6249
  await this.context.addMessage({
6194
6250
  role: "tool",
6195
- content: toolResult,
6251
+ content: truncateForContext(toolResult),
6196
6252
  toolCallId: tc.id
6197
6253
  });
6198
6254
  }
@@ -6449,15 +6505,17 @@ ${assignment.expectedOutput}`;
6449
6505
  };
6450
6506
  }
6451
6507
  requiresArtifact() {
6508
+ if (this.assignment?.files?.length) return true;
6452
6509
  const haystack = `${this.assignment?.description ?? ""}
6453
6510
  ${this.assignment?.expectedOutput ?? ""}`;
6454
6511
  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);
6455
6512
  }
6456
6513
  extractArtifactPaths(assignment) {
6514
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
6457
6515
  const haystack = `${assignment.description}
6458
6516
  ${assignment.expectedOutput}`;
6459
6517
  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) ?? [];
6460
- return [...new Set(matches.map((m) => m.trim()))];
6518
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
6461
6519
  }
6462
6520
  async verifyArtifacts(assignment) {
6463
6521
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -6580,7 +6638,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6580
6638
  Assignment: ${assignment.description}
6581
6639
  Expected output: ${assignment.expectedOutput}
6582
6640
  Constraints: ${assignment.constraints.join("; ")}
6583
-
6641
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
6642
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6643
+ ` : ""}
6584
6644
  Output to test:
6585
6645
  ${output}
6586
6646
 
@@ -6669,17 +6729,27 @@ Your subtask:
6669
6729
  - Title: ${assignment.subtaskTitle}
6670
6730
  - Description: ${assignment.description}
6671
6731
  - Expected output: ${assignment.expectedOutput}
6672
- - Constraints: ${assignment.constraints.join("; ")}`;
6732
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
6733
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
6734
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
6673
6735
  }
6674
6736
  buildInitialPrompt(assignment) {
6675
6737
  return `Execute the following subtask completely:
6676
6738
 
6677
6739
  **${assignment.subtaskTitle}**
6678
-
6740
+ ${assignment.contextBrief ? `
6741
+ Context: ${assignment.contextBrief}
6742
+ ` : ""}
6679
6743
  ${assignment.description}
6680
6744
 
6681
6745
  Expected output: ${assignment.expectedOutput}
6682
-
6746
+ ${assignment.files?.length ? `
6747
+ Files you own (create or edit exactly these paths):
6748
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
6749
+ ` : ""}${assignment.acceptance?.length ? `
6750
+ Definition of done (your output must satisfy ALL of these):
6751
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6752
+ ` : ""}
6683
6753
  Constraints:
6684
6754
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
6685
6755
 
@@ -7160,6 +7230,8 @@ var T2Manager = class extends BaseTier {
7160
7230
  this.assignment = assignment;
7161
7231
  this.taskId = taskId;
7162
7232
  this.setLabel(assignment.sectionTitle);
7233
+ const m = this.router.getModelForTier("T2");
7234
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7163
7235
  this.setStatus("ACTIVE");
7164
7236
  this.sendStatusUpdate({
7165
7237
  progressPct: 0,
@@ -7246,7 +7318,7 @@ Guidance (must be followed): ${decision.note}`
7246
7318
  // ── Private ──────────────────────────────────
7247
7319
  async decomposeSection(assignment) {
7248
7320
  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");
7249
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
7321
+ 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).
7250
7322
 
7251
7323
  Section: ${assignment.sectionTitle}
7252
7324
  Description: ${assignment.description}
@@ -7265,6 +7337,9 @@ Return a JSON array of subtask objects, each with:
7265
7337
  - peerT3Ids: string[] (empty for now)
7266
7338
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
7267
7339
  - executionMode: "parallel|sequential" (default is parallel)
7340
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
7341
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
7342
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
7268
7343
 
7269
7344
  Return ONLY the JSON array.`;
7270
7345
  const messages = [{ role: "user", content: prompt }];
@@ -7865,6 +7940,8 @@ var T1Administrator = class extends BaseTier {
7865
7940
  this.signal = signal;
7866
7941
  this.taskId = crypto.randomUUID();
7867
7942
  this.setLabel("Administrator");
7943
+ const m = this.router.getModelForTier("T1");
7944
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7868
7945
  this.setStatus("ACTIVE");
7869
7946
  this.taskGoal = userPrompt;
7870
7947
  this.sendStatusUpdate({
@@ -8111,10 +8188,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8111
8188
  "description": "Run npm init",
8112
8189
  "expectedOutput": "package.json created",
8113
8190
  "constraints": [],
8114
- "dependsOn": []
8191
+ "dependsOn": [],
8192
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
8193
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
8194
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
8115
8195
  }]
8116
8196
  }, {
8117
- "sectionId": "s2",
8197
+ "sectionId": "s2",
8118
8198
  "sectionTitle": "Write Tests",
8119
8199
  "description": "Write tests for the project",
8120
8200
  "expectedOutput": "Tests passing",
@@ -8124,7 +8204,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8124
8204
  }]
8125
8205
  }
8126
8206
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
8127
- Leave dependsOn empty for sections that can run immediately in parallel.`;
8207
+ Leave dependsOn empty for sections that can run immediately in parallel.
8208
+
8209
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
8210
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
8211
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
8212
+ - "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.
8213
+ - 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.`;
8128
8214
  const messages = [{ role: "user", content: decompositionPrompt }];
8129
8215
  const result = await this.router.generate("T1", {
8130
8216
  messages,
@@ -9432,30 +9518,56 @@ async function searchTavily(query, apiKey, maxResults) {
9432
9518
  engine: "tavily"
9433
9519
  }));
9434
9520
  }
9435
- async function searchDuckDuckGoLite(query, maxResults) {
9436
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
9437
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
9438
- signal: AbortSignal.timeout(1e4)
9439
- });
9440
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
9441
- const html = await resp.text();
9442
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
9443
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
9444
- const links = [];
9445
- const snippets = [];
9521
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
9522
+ function unwrapDdgRedirect(href) {
9523
+ try {
9524
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
9525
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
9526
+ const target = url.searchParams.get("uddg");
9527
+ if (target) return decodeURIComponent(target);
9528
+ }
9529
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
9530
+ } catch {
9531
+ return href;
9532
+ }
9533
+ }
9534
+ function stripTags(html) {
9535
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
9536
+ }
9537
+ function decodeEntities(text) {
9538
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
9539
+ }
9540
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
9541
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
9542
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
9543
+ const results = [];
9446
9544
  let m;
9447
- while ((m = linkPattern.exec(html)) !== null) {
9448
- links.push({ url: m[1], title: m[2].trim() });
9545
+ while ((m = anchorRe.exec(html)) !== null) {
9546
+ const tag = m[0];
9547
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
9548
+ const title = decodeEntities(stripTags(m[1] ?? ""));
9549
+ if (!href || !title) continue;
9550
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
9449
9551
  }
9450
- while ((m = snippetPattern.exec(html)) !== null) {
9451
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
9552
+ const snippets = [];
9553
+ while ((m = snippetRe.exec(html)) !== null) {
9554
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
9452
9555
  }
9453
- return links.slice(0, maxResults).map((link, i) => ({
9454
- title: link.title,
9455
- url: link.url,
9456
- snippet: snippets[i] ?? "",
9457
- engine: "duckduckgo-lite"
9458
- }));
9556
+ for (let i = 0; i < results.length; i++) {
9557
+ if (snippets[i]) results[i].snippet = snippets[i];
9558
+ }
9559
+ return results;
9560
+ }
9561
+ async function searchDuckDuckGo(query, maxResults, variant) {
9562
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
9563
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
9564
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
9565
+ signal: AbortSignal.timeout(1e4)
9566
+ });
9567
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
9568
+ const html = await resp.text();
9569
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
9570
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
9459
9571
  }
9460
9572
  var WebSearchTool = class extends BaseTool {
9461
9573
  name = "web_search";
@@ -9514,12 +9626,14 @@ var WebSearchTool = class extends BaseTool {
9514
9626
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
9515
9627
  }
9516
9628
  }
9517
- try {
9518
- results = await searchDuckDuckGoLite(query, maxResults);
9519
- if (results.length > 0) return this.formatResults(query, results);
9520
- errors.push("DuckDuckGo Lite: returned 0 results");
9521
- } catch (err) {
9522
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
9629
+ for (const variant of ["html", "lite"]) {
9630
+ try {
9631
+ results = await searchDuckDuckGo(query, maxResults, variant);
9632
+ if (results.length > 0) return this.formatResults(query, results);
9633
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
9634
+ } catch (err) {
9635
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
9636
+ }
9523
9637
  }
9524
9638
  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" : "";
9525
9639
  return [
@@ -11866,13 +11980,30 @@ ${last.partialOutput}` : "");
11866
11980
  * explicit multi-part structure, so ordinary single-file asks (handled as
11867
11981
  * Simple/Moderate) don't get over-escalated.
11868
11982
  */
11869
- looksClearlyComplex(prompt) {
11983
+ /** Shared build/scale signals for the complexity floors below. */
11984
+ buildSignals(prompt) {
11870
11985
  const p = prompt.trim();
11871
- if (p.length < 24) return false;
11986
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
11872
11987
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11873
- 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);
11988
+ 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;
11874
11989
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11875
- return buildVerb && (scaleNoun || multiPart);
11990
+ return { buildVerb, scaleCount, multiPart };
11991
+ }
11992
+ /**
11993
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
11994
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
11995
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
11996
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
11997
+ * × workers for a task one worker handles).
11998
+ */
11999
+ looksClearlyComplex(prompt) {
12000
+ const s = this.buildSignals(prompt);
12001
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
12002
+ }
12003
+ /** A small single-deliverable build — real work, but one manager's worth. */
12004
+ looksLikeModerateBuild(prompt) {
12005
+ const s = this.buildSignals(prompt);
12006
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
11876
12007
  }
11877
12008
  // Cache glob scan results per workspace path to avoid repeated I/O.
11878
12009
  static globCache = /* @__PURE__ */ new Map();
@@ -11960,6 +12091,9 @@ ${prompt}` : prompt;
11960
12091
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11961
12092
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11962
12093
  verdict = "Complex";
12094
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
12095
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
12096
+ verdict = "Moderate";
11963
12097
  } else {
11964
12098
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11965
12099
  }