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.js CHANGED
@@ -59,7 +59,7 @@ var __export = (target, all) => {
59
59
  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;
60
60
  var init_constants = __esm({
61
61
  "src/constants.ts"() {
62
- CASCADE_VERSION = "0.18.0";
62
+ CASCADE_VERSION = "0.19.0";
63
63
  CASCADE_CONFIG_FILE = ".cascade/config.json";
64
64
  CASCADE_DB_FILE = ".cascade/memory.db";
65
65
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -885,8 +885,26 @@ var init_openai = __esm({
885
885
  // src/providers/azure.ts
886
886
  var azure_exports = {};
887
887
  __export(azure_exports, {
888
- AzureOpenAIProvider: () => AzureOpenAIProvider
888
+ AzureOpenAIProvider: () => AzureOpenAIProvider,
889
+ azureModelForDeployment: () => azureModelForDeployment
889
890
  });
891
+ function azureModelForDeployment(cfg) {
892
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
893
+ const id = cfg.deploymentName.trim();
894
+ return {
895
+ id,
896
+ name: cfg.label?.trim() || id,
897
+ provider: "azure",
898
+ contextWindow: 128e3,
899
+ isVisionCapable: false,
900
+ inputCostPer1kTokens: 25e-4,
901
+ outputCostPer1kTokens: 0.01,
902
+ maxOutputTokens: 16e3,
903
+ supportsStreaming: true,
904
+ isLocal: false,
905
+ supportsToolUse: true
906
+ };
907
+ }
890
908
  var AzureOpenAIProvider;
891
909
  var init_azure = __esm({
892
910
  "src/providers/azure.ts"() {
@@ -912,7 +930,8 @@ var init_azure = __esm({
912
930
  });
913
931
  }
914
932
  async listModels() {
915
- return [this.model];
933
+ const fromDeployment = azureModelForDeployment(this.config);
934
+ return [fromDeployment ?? this.model];
916
935
  }
917
936
  async isAvailable() {
918
937
  try {
@@ -2997,8 +3016,11 @@ var CascadeConfigSchema = z.object({
2997
3016
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
2998
3017
  * tier based on task type and complexity, overriding the static priority lists.
2999
3018
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
3019
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
3020
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
3021
+ * model pins are unaffected; disable via config/Settings → Advanced.
3000
3022
  */
3001
- cascadeAuto: z.boolean().default(false),
3023
+ cascadeAuto: z.boolean().default(true),
3002
3024
  /**
3003
3025
  * Cascade Auto trade-off bias when picking a model for a task:
3004
3026
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -4552,6 +4574,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4552
4574
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
4553
4575
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
4554
4576
  }
4577
+ if (availableProviders.has("azure")) {
4578
+ for (const cfg of config.providers) {
4579
+ const model = azureModelForDeployment(cfg);
4580
+ if (model) this.selector.addDynamicModel(model);
4581
+ }
4582
+ }
4555
4583
  for (const tier of ["T1", "T2", "T3"]) {
4556
4584
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
4557
4585
  if (!override || override === "auto") continue;
@@ -5158,7 +5186,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5158
5186
  ensureProvider(model, configs) {
5159
5187
  const key = `${model.provider}:${model.id}`;
5160
5188
  if (this.providers.has(key)) return;
5161
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
5189
+ 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 };
5162
5190
  const provider = this.createProvider(cfg, model);
5163
5191
  this.providers.set(key, provider);
5164
5192
  }
@@ -5318,6 +5346,12 @@ var BaseTier = class extends EventEmitter {
5318
5346
  * which would interleave, are not tagged.
5319
5347
  */
5320
5348
  isPresenter = false;
5349
+ /**
5350
+ * The model actually serving this tier (`provider:id`), once resolved —
5351
+ * rides on every tier:status event so the desktop can show which model ran
5352
+ * which node (Cockpit node panel / Why panel).
5353
+ */
5354
+ servingModel;
5321
5355
  constructor(role, id, parentId) {
5322
5356
  super();
5323
5357
  this.role = role;
@@ -5342,11 +5376,16 @@ var BaseTier = class extends EventEmitter {
5342
5376
  label: this.label,
5343
5377
  status,
5344
5378
  timestamp,
5345
- output
5379
+ output,
5380
+ model: this.servingModel
5346
5381
  };
5347
5382
  this.emit("status", event);
5348
5383
  this.emit("tier:status", event);
5349
5384
  }
5385
+ /** Record the model serving this tier; future status events carry it. */
5386
+ setServingModel(model) {
5387
+ this.servingModel = model || void 0;
5388
+ }
5350
5389
  setLabel(label) {
5351
5390
  this.label = label;
5352
5391
  }
@@ -5369,7 +5408,8 @@ var BaseTier = class extends EventEmitter {
5369
5408
  currentAction: update.currentAction,
5370
5409
  progressPct: update.progressPct,
5371
5410
  timestamp,
5372
- output: update.output
5411
+ output: update.output,
5412
+ model: this.servingModel
5373
5413
  });
5374
5414
  }
5375
5415
  buildMessage(type, to, payload) {
@@ -5736,6 +5776,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
5736
5776
  When you have enough information, stop calling tools and write your final answer.`;
5737
5777
  }
5738
5778
 
5779
+ // src/utils/truncate.ts
5780
+ function truncateForContext(text, maxChars = 12e3) {
5781
+ if (text.length <= maxChars) return text;
5782
+ const headLen = Math.floor(maxChars * 0.75);
5783
+ const tailLen = maxChars - headLen;
5784
+ const head = text.slice(0, headLen);
5785
+ const tail = text.slice(-tailLen);
5786
+ const elided = text.length - headLen - tailLen;
5787
+ return `${head}
5788
+
5789
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
5790
+
5791
+ ${tail}`;
5792
+ }
5793
+
5739
5794
  // src/core/tiers/t3-worker.ts
5740
5795
  var CriticalToolError = class extends Error {
5741
5796
  constructor(message, toolName) {
@@ -6043,6 +6098,7 @@ Now execute your subtask using this context where relevant.`
6043
6098
  } catch {
6044
6099
  }
6045
6100
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
6101
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
6046
6102
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
6047
6103
  let sentFullTextContract = false;
6048
6104
  let textContractSignature = "";
@@ -6140,7 +6196,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6140
6196
  const toolResult = await this.executeTool(tc);
6141
6197
  await this.context.addMessage({
6142
6198
  role: "tool",
6143
- content: toolResult,
6199
+ content: truncateForContext(toolResult),
6144
6200
  toolCallId: tc.id
6145
6201
  });
6146
6202
  }
@@ -6397,15 +6453,17 @@ ${assignment.expectedOutput}`;
6397
6453
  };
6398
6454
  }
6399
6455
  requiresArtifact() {
6456
+ if (this.assignment?.files?.length) return true;
6400
6457
  const haystack = `${this.assignment?.description ?? ""}
6401
6458
  ${this.assignment?.expectedOutput ?? ""}`;
6402
6459
  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);
6403
6460
  }
6404
6461
  extractArtifactPaths(assignment) {
6462
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
6405
6463
  const haystack = `${assignment.description}
6406
6464
  ${assignment.expectedOutput}`;
6407
6465
  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) ?? [];
6408
- return [...new Set(matches.map((m) => m.trim()))];
6466
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
6409
6467
  }
6410
6468
  async verifyArtifacts(assignment) {
6411
6469
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -6528,7 +6586,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6528
6586
  Assignment: ${assignment.description}
6529
6587
  Expected output: ${assignment.expectedOutput}
6530
6588
  Constraints: ${assignment.constraints.join("; ")}
6531
-
6589
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
6590
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6591
+ ` : ""}
6532
6592
  Output to test:
6533
6593
  ${output}
6534
6594
 
@@ -6617,17 +6677,27 @@ Your subtask:
6617
6677
  - Title: ${assignment.subtaskTitle}
6618
6678
  - Description: ${assignment.description}
6619
6679
  - Expected output: ${assignment.expectedOutput}
6620
- - Constraints: ${assignment.constraints.join("; ")}`;
6680
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
6681
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
6682
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
6621
6683
  }
6622
6684
  buildInitialPrompt(assignment) {
6623
6685
  return `Execute the following subtask completely:
6624
6686
 
6625
6687
  **${assignment.subtaskTitle}**
6626
-
6688
+ ${assignment.contextBrief ? `
6689
+ Context: ${assignment.contextBrief}
6690
+ ` : ""}
6627
6691
  ${assignment.description}
6628
6692
 
6629
6693
  Expected output: ${assignment.expectedOutput}
6630
-
6694
+ ${assignment.files?.length ? `
6695
+ Files you own (create or edit exactly these paths):
6696
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
6697
+ ` : ""}${assignment.acceptance?.length ? `
6698
+ Definition of done (your output must satisfy ALL of these):
6699
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6700
+ ` : ""}
6631
6701
  Constraints:
6632
6702
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
6633
6703
 
@@ -7108,6 +7178,8 @@ var T2Manager = class extends BaseTier {
7108
7178
  this.assignment = assignment;
7109
7179
  this.taskId = taskId;
7110
7180
  this.setLabel(assignment.sectionTitle);
7181
+ const m = this.router.getModelForTier("T2");
7182
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7111
7183
  this.setStatus("ACTIVE");
7112
7184
  this.sendStatusUpdate({
7113
7185
  progressPct: 0,
@@ -7194,7 +7266,7 @@ Guidance (must be followed): ${decision.note}`
7194
7266
  // ── Private ──────────────────────────────────
7195
7267
  async decomposeSection(assignment) {
7196
7268
  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");
7197
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
7269
+ 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).
7198
7270
 
7199
7271
  Section: ${assignment.sectionTitle}
7200
7272
  Description: ${assignment.description}
@@ -7213,6 +7285,9 @@ Return a JSON array of subtask objects, each with:
7213
7285
  - peerT3Ids: string[] (empty for now)
7214
7286
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
7215
7287
  - executionMode: "parallel|sequential" (default is parallel)
7288
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
7289
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
7290
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
7216
7291
 
7217
7292
  Return ONLY the JSON array.`;
7218
7293
  const messages = [{ role: "user", content: prompt }];
@@ -7813,6 +7888,8 @@ var T1Administrator = class extends BaseTier {
7813
7888
  this.signal = signal;
7814
7889
  this.taskId = randomUUID();
7815
7890
  this.setLabel("Administrator");
7891
+ const m = this.router.getModelForTier("T1");
7892
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7816
7893
  this.setStatus("ACTIVE");
7817
7894
  this.taskGoal = userPrompt;
7818
7895
  this.sendStatusUpdate({
@@ -8059,10 +8136,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8059
8136
  "description": "Run npm init",
8060
8137
  "expectedOutput": "package.json created",
8061
8138
  "constraints": [],
8062
- "dependsOn": []
8139
+ "dependsOn": [],
8140
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
8141
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
8142
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
8063
8143
  }]
8064
8144
  }, {
8065
- "sectionId": "s2",
8145
+ "sectionId": "s2",
8066
8146
  "sectionTitle": "Write Tests",
8067
8147
  "description": "Write tests for the project",
8068
8148
  "expectedOutput": "Tests passing",
@@ -8072,7 +8152,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8072
8152
  }]
8073
8153
  }
8074
8154
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
8075
- Leave dependsOn empty for sections that can run immediately in parallel.`;
8155
+ Leave dependsOn empty for sections that can run immediately in parallel.
8156
+
8157
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
8158
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
8159
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
8160
+ - "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.
8161
+ - 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.`;
8076
8162
  const messages = [{ role: "user", content: decompositionPrompt }];
8077
8163
  const result = await this.router.generate("T1", {
8078
8164
  messages,
@@ -9380,30 +9466,56 @@ async function searchTavily(query, apiKey, maxResults) {
9380
9466
  engine: "tavily"
9381
9467
  }));
9382
9468
  }
9383
- async function searchDuckDuckGoLite(query, maxResults) {
9384
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
9385
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
9386
- signal: AbortSignal.timeout(1e4)
9387
- });
9388
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
9389
- const html = await resp.text();
9390
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
9391
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
9392
- const links = [];
9393
- const snippets = [];
9469
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
9470
+ function unwrapDdgRedirect(href) {
9471
+ try {
9472
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
9473
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
9474
+ const target = url.searchParams.get("uddg");
9475
+ if (target) return decodeURIComponent(target);
9476
+ }
9477
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
9478
+ } catch {
9479
+ return href;
9480
+ }
9481
+ }
9482
+ function stripTags(html) {
9483
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
9484
+ }
9485
+ function decodeEntities(text) {
9486
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
9487
+ }
9488
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
9489
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
9490
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
9491
+ const results = [];
9394
9492
  let m;
9395
- while ((m = linkPattern.exec(html)) !== null) {
9396
- links.push({ url: m[1], title: m[2].trim() });
9493
+ while ((m = anchorRe.exec(html)) !== null) {
9494
+ const tag = m[0];
9495
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
9496
+ const title = decodeEntities(stripTags(m[1] ?? ""));
9497
+ if (!href || !title) continue;
9498
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
9397
9499
  }
9398
- while ((m = snippetPattern.exec(html)) !== null) {
9399
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
9500
+ const snippets = [];
9501
+ while ((m = snippetRe.exec(html)) !== null) {
9502
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
9400
9503
  }
9401
- return links.slice(0, maxResults).map((link, i) => ({
9402
- title: link.title,
9403
- url: link.url,
9404
- snippet: snippets[i] ?? "",
9405
- engine: "duckduckgo-lite"
9406
- }));
9504
+ for (let i = 0; i < results.length; i++) {
9505
+ if (snippets[i]) results[i].snippet = snippets[i];
9506
+ }
9507
+ return results;
9508
+ }
9509
+ async function searchDuckDuckGo(query, maxResults, variant) {
9510
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
9511
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
9512
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
9513
+ signal: AbortSignal.timeout(1e4)
9514
+ });
9515
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
9516
+ const html = await resp.text();
9517
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
9518
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
9407
9519
  }
9408
9520
  var WebSearchTool = class extends BaseTool {
9409
9521
  name = "web_search";
@@ -9462,12 +9574,14 @@ var WebSearchTool = class extends BaseTool {
9462
9574
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
9463
9575
  }
9464
9576
  }
9465
- try {
9466
- results = await searchDuckDuckGoLite(query, maxResults);
9467
- if (results.length > 0) return this.formatResults(query, results);
9468
- errors.push("DuckDuckGo Lite: returned 0 results");
9469
- } catch (err) {
9470
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
9577
+ for (const variant of ["html", "lite"]) {
9578
+ try {
9579
+ results = await searchDuckDuckGo(query, maxResults, variant);
9580
+ if (results.length > 0) return this.formatResults(query, results);
9581
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
9582
+ } catch (err) {
9583
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
9584
+ }
9471
9585
  }
9472
9586
  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" : "";
9473
9587
  return [
@@ -11814,13 +11928,30 @@ ${last.partialOutput}` : "");
11814
11928
  * explicit multi-part structure, so ordinary single-file asks (handled as
11815
11929
  * Simple/Moderate) don't get over-escalated.
11816
11930
  */
11817
- looksClearlyComplex(prompt) {
11931
+ /** Shared build/scale signals for the complexity floors below. */
11932
+ buildSignals(prompt) {
11818
11933
  const p = prompt.trim();
11819
- if (p.length < 24) return false;
11934
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
11820
11935
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11821
- 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);
11936
+ 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;
11822
11937
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11823
- return buildVerb && (scaleNoun || multiPart);
11938
+ return { buildVerb, scaleCount, multiPart };
11939
+ }
11940
+ /**
11941
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
11942
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
11943
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
11944
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
11945
+ * × workers for a task one worker handles).
11946
+ */
11947
+ looksClearlyComplex(prompt) {
11948
+ const s = this.buildSignals(prompt);
11949
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
11950
+ }
11951
+ /** A small single-deliverable build — real work, but one manager's worth. */
11952
+ looksLikeModerateBuild(prompt) {
11953
+ const s = this.buildSignals(prompt);
11954
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
11824
11955
  }
11825
11956
  // Cache glob scan results per workspace path to avoid repeated I/O.
11826
11957
  static globCache = /* @__PURE__ */ new Map();
@@ -11908,6 +12039,9 @@ ${prompt}` : prompt;
11908
12039
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11909
12040
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11910
12041
  verdict = "Complex";
12042
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
12043
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
12044
+ verdict = "Moderate";
11911
12045
  } else {
11912
12046
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11913
12047
  }