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.
@@ -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.0";
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,7 +231308,8 @@ 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() {
231297
231315
  try {
@@ -250832,6 +250850,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
250832
250850
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
250833
250851
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
250834
250852
  }
250853
+ if (availableProviders.has("azure")) {
250854
+ for (const cfg of config2.providers) {
250855
+ const model = azureModelForDeployment(cfg);
250856
+ if (model) this.selector.addDynamicModel(model);
250857
+ }
250858
+ }
250835
250859
  for (const tier of ["T1", "T2", "T3"]) {
250836
250860
  const override = tier === "T1" ? config2.models.t1 : tier === "T2" ? config2.models.t2 : config2.models.t3;
250837
250861
  if (!override || override === "auto") continue;
@@ -251438,7 +251462,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
251438
251462
  ensureProvider(model, configs) {
251439
251463
  const key = `${model.provider}:${model.id}`;
251440
251464
  if (this.providers.has(key)) return;
251441
- const cfg = configs.find((c4) => c4.type === model.provider) ?? { type: model.provider };
251465
+ 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
251466
  const provider = this.createProvider(cfg, model);
251443
251467
  this.providers.set(key, provider);
251444
251468
  }
@@ -251598,6 +251622,12 @@ var BaseTier = class extends EventEmitter__default.default {
251598
251622
  * which would interleave, are not tagged.
251599
251623
  */
251600
251624
  isPresenter = false;
251625
+ /**
251626
+ * The model actually serving this tier (`provider:id`), once resolved —
251627
+ * rides on every tier:status event so the desktop can show which model ran
251628
+ * which node (Cockpit node panel / Why panel).
251629
+ */
251630
+ servingModel;
251601
251631
  constructor(role, id, parentId) {
251602
251632
  super();
251603
251633
  this.role = role;
@@ -251622,11 +251652,16 @@ var BaseTier = class extends EventEmitter__default.default {
251622
251652
  label: this.label,
251623
251653
  status,
251624
251654
  timestamp,
251625
- output
251655
+ output,
251656
+ model: this.servingModel
251626
251657
  };
251627
251658
  this.emit("status", event);
251628
251659
  this.emit("tier:status", event);
251629
251660
  }
251661
+ /** Record the model serving this tier; future status events carry it. */
251662
+ setServingModel(model) {
251663
+ this.servingModel = model || void 0;
251664
+ }
251630
251665
  setLabel(label) {
251631
251666
  this.label = label;
251632
251667
  }
@@ -251649,7 +251684,8 @@ var BaseTier = class extends EventEmitter__default.default {
251649
251684
  currentAction: update.currentAction,
251650
251685
  progressPct: update.progressPct,
251651
251686
  timestamp,
251652
- output: update.output
251687
+ output: update.output,
251688
+ model: this.servingModel
251653
251689
  });
251654
251690
  }
251655
251691
  buildMessage(type, to, payload) {
@@ -252015,6 +252051,21 @@ Available tools: ${tools.map((t4) => t4.name).join(", ")}.
252015
252051
  When you have enough information, stop calling tools and write your final answer.`;
252016
252052
  }
252017
252053
 
252054
+ // src/utils/truncate.ts
252055
+ function truncateForContext(text, maxChars = 12e3) {
252056
+ if (text.length <= maxChars) return text;
252057
+ const headLen = Math.floor(maxChars * 0.75);
252058
+ const tailLen = maxChars - headLen;
252059
+ const head = text.slice(0, headLen);
252060
+ const tail = text.slice(-tailLen);
252061
+ const elided = text.length - headLen - tailLen;
252062
+ return `${head}
252063
+
252064
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
252065
+
252066
+ ${tail}`;
252067
+ }
252068
+
252018
252069
  // src/core/tiers/t3-worker.ts
252019
252070
  var CriticalToolError = class extends Error {
252020
252071
  constructor(message, toolName) {
@@ -252322,6 +252373,7 @@ Now execute your subtask using this context where relevant.`
252322
252373
  } catch {
252323
252374
  }
252324
252375
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
252376
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
252325
252377
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
252326
252378
  let sentFullTextContract = false;
252327
252379
  let textContractSignature = "";
@@ -252419,7 +252471,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
252419
252471
  const toolResult = await this.executeTool(tc);
252420
252472
  await this.context.addMessage({
252421
252473
  role: "tool",
252422
- content: toolResult,
252474
+ content: truncateForContext(toolResult),
252423
252475
  toolCallId: tc.id
252424
252476
  });
252425
252477
  }
@@ -252676,15 +252728,17 @@ ${assignment.expectedOutput}`;
252676
252728
  };
252677
252729
  }
252678
252730
  requiresArtifact() {
252731
+ if (this.assignment?.files?.length) return true;
252679
252732
  const haystack = `${this.assignment?.description ?? ""}
252680
252733
  ${this.assignment?.expectedOutput ?? ""}`;
252681
252734
  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
252735
  }
252683
252736
  extractArtifactPaths(assignment) {
252737
+ const declared = (assignment.files ?? []).map((f4) => f4.trim()).filter((f4) => f4.includes("."));
252684
252738
  const haystack = `${assignment.description}
252685
252739
  ${assignment.expectedOutput}`;
252686
252740
  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()))];
252741
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m3) => m3.trim())])];
252688
252742
  }
252689
252743
  async verifyArtifacts(assignment) {
252690
252744
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -252807,7 +252861,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
252807
252861
  Assignment: ${assignment.description}
252808
252862
  Expected output: ${assignment.expectedOutput}
252809
252863
  Constraints: ${assignment.constraints.join("; ")}
252810
-
252864
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
252865
+ ${assignment.acceptance.map((a2) => `- ${a2}`).join("\n")}
252866
+ ` : ""}
252811
252867
  Output to test:
252812
252868
  ${output}
252813
252869
 
@@ -252896,17 +252952,27 @@ Your subtask:
252896
252952
  - Title: ${assignment.subtaskTitle}
252897
252953
  - Description: ${assignment.description}
252898
252954
  - Expected output: ${assignment.expectedOutput}
252899
- - Constraints: ${assignment.constraints.join("; ")}`;
252955
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
252956
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
252957
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
252900
252958
  }
252901
252959
  buildInitialPrompt(assignment) {
252902
252960
  return `Execute the following subtask completely:
252903
252961
 
252904
252962
  **${assignment.subtaskTitle}**
252905
-
252963
+ ${assignment.contextBrief ? `
252964
+ Context: ${assignment.contextBrief}
252965
+ ` : ""}
252906
252966
  ${assignment.description}
252907
252967
 
252908
252968
  Expected output: ${assignment.expectedOutput}
252909
-
252969
+ ${assignment.files?.length ? `
252970
+ Files you own (create or edit exactly these paths):
252971
+ ${assignment.files.map((f4) => `- ${f4}`).join("\n")}
252972
+ ` : ""}${assignment.acceptance?.length ? `
252973
+ Definition of done (your output must satisfy ALL of these):
252974
+ ${assignment.acceptance.map((a2) => `- ${a2}`).join("\n")}
252975
+ ` : ""}
252910
252976
  Constraints:
252911
252977
  ${assignment.constraints.map((c4) => `- ${c4}`).join("\n")}
252912
252978
 
@@ -253387,6 +253453,8 @@ var T2Manager = class extends BaseTier {
253387
253453
  this.assignment = assignment;
253388
253454
  this.taskId = taskId;
253389
253455
  this.setLabel(assignment.sectionTitle);
253456
+ const m3 = this.router.getModelForTier("T2");
253457
+ if (m3) this.setServingModel(`${m3.provider}:${m3.id}`);
253390
253458
  this.setStatus("ACTIVE");
253391
253459
  this.sendStatusUpdate({
253392
253460
  progressPct: 0,
@@ -253473,7 +253541,7 @@ Guidance (must be followed): ${decision.note}`
253473
253541
  // ── Private ──────────────────────────────────
253474
253542
  async decomposeSection(assignment) {
253475
253543
  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.
253544
+ 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
253545
 
253478
253546
  Section: ${assignment.sectionTitle}
253479
253547
  Description: ${assignment.description}
@@ -253492,6 +253560,9 @@ Return a JSON array of subtask objects, each with:
253492
253560
  - peerT3Ids: string[] (empty for now)
253493
253561
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
253494
253562
  - executionMode: "parallel|sequential" (default is parallel)
253563
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
253564
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
253565
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
253495
253566
 
253496
253567
  Return ONLY the JSON array.`;
253497
253568
  const messages = [{ role: "user", content: prompt }];
@@ -254089,6 +254160,8 @@ var T1Administrator = class extends BaseTier {
254089
254160
  this.signal = signal;
254090
254161
  this.taskId = crypto4.randomUUID();
254091
254162
  this.setLabel("Administrator");
254163
+ const m3 = this.router.getModelForTier("T1");
254164
+ if (m3) this.setServingModel(`${m3.provider}:${m3.id}`);
254092
254165
  this.setStatus("ACTIVE");
254093
254166
  this.taskGoal = userPrompt;
254094
254167
  this.sendStatusUpdate({
@@ -254335,10 +254408,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
254335
254408
  "description": "Run npm init",
254336
254409
  "expectedOutput": "package.json created",
254337
254410
  "constraints": [],
254338
- "dependsOn": []
254411
+ "dependsOn": [],
254412
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
254413
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
254414
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
254339
254415
  }]
254340
254416
  }, {
254341
- "sectionId": "s2",
254417
+ "sectionId": "s2",
254342
254418
  "sectionTitle": "Write Tests",
254343
254419
  "description": "Write tests for the project",
254344
254420
  "expectedOutput": "Tests passing",
@@ -254348,7 +254424,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
254348
254424
  }]
254349
254425
  }
254350
254426
  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.`;
254427
+ Leave dependsOn empty for sections that can run immediately in parallel.
254428
+
254429
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
254430
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
254431
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
254432
+ - "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.
254433
+ - 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
254434
  const messages = [{ role: "user", content: decompositionPrompt }];
254353
254435
  const result = await this.router.generate("T1", {
254354
254436
  messages,
@@ -282539,30 +282621,56 @@ async function searchTavily(query, apiKey, maxResults) {
282539
282621
  engine: "tavily"
282540
282622
  }));
282541
282623
  }
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 = [];
282624
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
282625
+ function unwrapDdgRedirect(href) {
282626
+ try {
282627
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
282628
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
282629
+ const target = url.searchParams.get("uddg");
282630
+ if (target) return decodeURIComponent(target);
282631
+ }
282632
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
282633
+ } catch {
282634
+ return href;
282635
+ }
282636
+ }
282637
+ function stripTags(html) {
282638
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
282639
+ }
282640
+ function decodeEntities(text) {
282641
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
282642
+ }
282643
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
282644
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
282645
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
282646
+ const results = [];
282553
282647
  let m3;
282554
- while ((m3 = linkPattern.exec(html)) !== null) {
282555
- links.push({ url: m3[1], title: m3[2].trim() });
282648
+ while ((m3 = anchorRe.exec(html)) !== null) {
282649
+ const tag = m3[0];
282650
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
282651
+ const title = decodeEntities(stripTags(m3[1] ?? ""));
282652
+ if (!href || !title) continue;
282653
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
282654
+ }
282655
+ const snippets = [];
282656
+ while ((m3 = snippetRe.exec(html)) !== null) {
282657
+ snippets.push(decodeEntities(stripTags(m3[1] ?? "")));
282556
282658
  }
282557
- while ((m3 = snippetPattern.exec(html)) !== null) {
282558
- snippets.push(m3[1].replace(/<[^>]+>/g, "").trim());
282659
+ for (let i4 = 0; i4 < results.length; i4++) {
282660
+ if (snippets[i4]) results[i4].snippet = snippets[i4];
282559
282661
  }
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
- }));
282662
+ return results;
282663
+ }
282664
+ async function searchDuckDuckGo(query, maxResults, variant) {
282665
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
282666
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
282667
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
282668
+ signal: AbortSignal.timeout(1e4)
282669
+ });
282670
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
282671
+ const html = await resp.text();
282672
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
282673
+ return parsed.slice(0, maxResults).map((r4) => ({ ...r4, engine: `duckduckgo-${variant}` }));
282566
282674
  }
282567
282675
  var WebSearchTool = class extends BaseTool {
282568
282676
  name = "web_search";
@@ -282621,12 +282729,14 @@ var WebSearchTool = class extends BaseTool {
282621
282729
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
282622
282730
  }
282623
282731
  }
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)}`);
282732
+ for (const variant of ["html", "lite"]) {
282733
+ try {
282734
+ results = await searchDuckDuckGo(query, maxResults, variant);
282735
+ if (results.length > 0) return this.formatResults(query, results);
282736
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
282737
+ } catch (err) {
282738
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
282739
+ }
282630
282740
  }
282631
282741
  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
282742
  return [
@@ -294855,8 +294965,11 @@ var CascadeConfigSchema = external_exports.object({
294855
294965
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
294856
294966
  * tier based on task type and complexity, overriding the static priority lists.
294857
294967
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
294968
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
294969
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
294970
+ * model pins are unaffected; disable via config/Settings → Advanced.
294858
294971
  */
294859
- cascadeAuto: external_exports.boolean().default(false),
294972
+ cascadeAuto: external_exports.boolean().default(true),
294860
294973
  /**
294861
294974
  * Cascade Auto trade-off bias when picking a model for a task:
294862
294975
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -296572,13 +296685,30 @@ ${last3.partialOutput}` : "");
296572
296685
  * explicit multi-part structure, so ordinary single-file asks (handled as
296573
296686
  * Simple/Moderate) don't get over-escalated.
296574
296687
  */
296575
- looksClearlyComplex(prompt) {
296688
+ /** Shared build/scale signals for the complexity floors below. */
296689
+ buildSignals(prompt) {
296576
296690
  const p3 = prompt.trim();
296577
- if (p3.length < 24) return false;
296691
+ if (p3.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
296578
296692
  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);
296693
+ 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
296694
  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);
296695
+ return { buildVerb, scaleCount, multiPart };
296696
+ }
296697
+ /**
296698
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
296699
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
296700
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
296701
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
296702
+ * × workers for a task one worker handles).
296703
+ */
296704
+ looksClearlyComplex(prompt) {
296705
+ const s3 = this.buildSignals(prompt);
296706
+ return s3.buildVerb && (s3.scaleCount >= 2 || s3.scaleCount >= 1 && s3.multiPart);
296707
+ }
296708
+ /** A small single-deliverable build — real work, but one manager's worth. */
296709
+ looksLikeModerateBuild(prompt) {
296710
+ const s3 = this.buildSignals(prompt);
296711
+ return s3.buildVerb && (s3.scaleCount >= 1 || s3.multiPart);
296582
296712
  }
296583
296713
  // Cache glob scan results per workspace path to avoid repeated I/O.
296584
296714
  static globCache = /* @__PURE__ */ new Map();
@@ -296666,6 +296796,9 @@ ${prompt}` : prompt;
296666
296796
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
296667
296797
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
296668
296798
  verdict = "Complex";
296799
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
296800
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
296801
+ verdict = "Moderate";
296669
296802
  } else {
296670
296803
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
296671
296804
  }