cascade-ai 0.67.0 → 0.68.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/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var Database = require('better-sqlite3');
4
- var path25 = require('path');
5
- var fs24 = require('fs');
4
+ var path26 = require('path');
5
+ var fs25 = require('fs');
6
6
  var os6 = require('os');
7
7
  var crypto4 = require('crypto');
8
8
  var EventEmitter = require('events');
@@ -61,8 +61,8 @@ function _interopNamespace(e) {
61
61
  }
62
62
 
63
63
  var Database__default = /*#__PURE__*/_interopDefault(Database);
64
- var path25__default = /*#__PURE__*/_interopDefault(path25);
65
- var fs24__default = /*#__PURE__*/_interopDefault(fs24);
64
+ var path26__default = /*#__PURE__*/_interopDefault(path26);
65
+ var fs25__default = /*#__PURE__*/_interopDefault(fs25);
66
66
  var os6__default = /*#__PURE__*/_interopDefault(os6);
67
67
  var crypto4__default = /*#__PURE__*/_interopDefault(crypto4);
68
68
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
@@ -107,12 +107,12 @@ var init_audit_logger = __esm({
107
107
  constructor(workspacePath, debugMode = false) {
108
108
  this.workspacePath = workspacePath;
109
109
  this.debugMode = debugMode;
110
- const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
111
- if (!fs24__default.default.existsSync(cascadeDir)) {
112
- fs24__default.default.mkdirSync(cascadeDir, { recursive: true });
110
+ const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
111
+ if (!fs25__default.default.existsSync(cascadeDir)) {
112
+ fs25__default.default.mkdirSync(cascadeDir, { recursive: true });
113
113
  }
114
- this.keyPath = path25__default.default.join(cascadeDir, "audit_log.key");
115
- this.dbPath = path25__default.default.join(cascadeDir, "audit_log.db");
114
+ this.keyPath = path26__default.default.join(cascadeDir, "audit_log.key");
115
+ this.dbPath = path26__default.default.join(cascadeDir, "audit_log.db");
116
116
  this.initEncryptionKey();
117
117
  this.db = new Database__default.default(this.dbPath);
118
118
  this.db.pragma("journal_mode = WAL");
@@ -142,11 +142,11 @@ var init_audit_logger = __esm({
142
142
  return crypto4__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
143
143
  }
144
144
  initEncryptionKey() {
145
- if (fs24__default.default.existsSync(this.keyPath)) {
146
- this.encryptionKey = fs24__default.default.readFileSync(this.keyPath);
145
+ if (fs25__default.default.existsSync(this.keyPath)) {
146
+ this.encryptionKey = fs25__default.default.readFileSync(this.keyPath);
147
147
  } else {
148
148
  this.encryptionKey = crypto4__default.default.randomBytes(32);
149
- fs24__default.default.writeFileSync(this.keyPath, this.encryptionKey);
149
+ fs25__default.default.writeFileSync(this.keyPath, this.encryptionKey);
150
150
  }
151
151
  }
152
152
  encrypt(text) {
@@ -224,9 +224,9 @@ var init_audit_logger = __esm({
224
224
  dumpDebugIfNeeded() {
225
225
  if (!this.debugMode) return;
226
226
  try {
227
- const dumpPath = path25__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
227
+ const dumpPath = path26__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
228
228
  const entries = this.getAllLogs();
229
- fs24__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
229
+ fs25__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
230
230
  } catch (err) {
231
231
  console.error("Failed to dump debug audit logs", err);
232
232
  }
@@ -1908,7 +1908,7 @@ function reconcilePrice(model, live) {
1908
1908
  }
1909
1909
 
1910
1910
  // src/constants.ts
1911
- var CASCADE_VERSION = "0.67.0";
1911
+ var CASCADE_VERSION = "0.68.0";
1912
1912
  var CASCADE_CONFIG_DIR = ".cascade";
1913
1913
  var CASCADE_MD_FILE = "CASCADE.md";
1914
1914
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -4494,13 +4494,13 @@ var DeadModelStore = class {
4494
4494
  function fileDeadModelPersistence(filePath) {
4495
4495
  return {
4496
4496
  load() {
4497
- if (!fs24.existsSync(filePath)) return [];
4498
- const raw = JSON.parse(fs24.readFileSync(filePath, "utf-8"));
4497
+ if (!fs25.existsSync(filePath)) return [];
4498
+ const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
4499
4499
  return Array.isArray(raw) ? raw : [];
4500
4500
  },
4501
4501
  save(records) {
4502
- fs24.mkdirSync(path25.dirname(filePath), { recursive: true });
4503
- fs24.writeFileSync(filePath, JSON.stringify(records, null, 2));
4502
+ fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
4503
+ fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
4504
4504
  }
4505
4505
  };
4506
4506
  }
@@ -4663,7 +4663,7 @@ function computeDelegationSavings(stats, t1Model) {
4663
4663
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
4664
4664
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
4665
4665
  var FETCH_TIMEOUT_MS = 8e3;
4666
- var DEFAULT_CACHE_FILE = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4666
+ var DEFAULT_CACHE_FILE = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4667
4667
  function normalizeModelId2(id) {
4668
4668
  let s = id.toLowerCase();
4669
4669
  const slash = s.lastIndexOf("/");
@@ -4810,7 +4810,7 @@ var LiveDataProvider = class {
4810
4810
  }
4811
4811
  async saveCache() {
4812
4812
  try {
4813
- await fs5__default.default.mkdir(path25__default.default.dirname(this.opts.cacheFile), { recursive: true });
4813
+ await fs5__default.default.mkdir(path26__default.default.dirname(this.opts.cacheFile), { recursive: true });
4814
4814
  const cache = {
4815
4815
  fetchedAt: this.fetchedAt,
4816
4816
  snapshot: this.snapshot ?? void 0,
@@ -6602,6 +6602,79 @@ function describeProviderError(c, modelId) {
6602
6602
  }
6603
6603
  }
6604
6604
 
6605
+ // src/core/verification/acceptance.ts
6606
+ var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
6607
+ var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
6608
+ var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
6609
+ var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
6610
+ var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
6611
+ function targetFile(criterion, ownedFiles) {
6612
+ const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
6613
+ if (mentioned.length > 1) return null;
6614
+ if (mentioned.length === 1) {
6615
+ const candidate = mentioned[0];
6616
+ return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
6617
+ }
6618
+ return ownedFiles.length === 1 ? ownedFiles[0] : null;
6619
+ }
6620
+ async function evaluateAcceptance(criteria, ownedFiles, probe) {
6621
+ const results = [];
6622
+ for (const criterion of criteria) {
6623
+ const text = criterion.trim();
6624
+ if (!text) {
6625
+ results.push({ criterion, verdict: "undecidable" });
6626
+ continue;
6627
+ }
6628
+ if (NEGATION.test(text)) {
6629
+ results.push({ criterion, verdict: "undecidable" });
6630
+ continue;
6631
+ }
6632
+ const needle = text.match(QUOTED_NEEDLE)?.[1];
6633
+ const wantsExists = EXISTS.test(text);
6634
+ const wantsNonEmpty = NON_EMPTY.test(text);
6635
+ if (!needle && !wantsExists && !wantsNonEmpty) {
6636
+ results.push({ criterion, verdict: "undecidable" });
6637
+ continue;
6638
+ }
6639
+ const file = targetFile(text, ownedFiles);
6640
+ if (!file) {
6641
+ results.push({ criterion, verdict: "undecidable" });
6642
+ continue;
6643
+ }
6644
+ const stat = await probe.stat(file);
6645
+ if (!stat) {
6646
+ results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
6647
+ continue;
6648
+ }
6649
+ if (wantsNonEmpty && stat.size === 0) {
6650
+ results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
6651
+ continue;
6652
+ }
6653
+ if (needle) {
6654
+ const content = await probe.read(file);
6655
+ if (content === null) {
6656
+ results.push({ criterion, verdict: "undecidable" });
6657
+ continue;
6658
+ }
6659
+ const found = content.includes(needle);
6660
+ results.push({
6661
+ criterion,
6662
+ verdict: found ? "passed" : "failed",
6663
+ detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
6664
+ });
6665
+ continue;
6666
+ }
6667
+ results.push({ criterion, verdict: "passed", detail: `${file} exists` });
6668
+ }
6669
+ return results;
6670
+ }
6671
+ function undecided(results) {
6672
+ return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
6673
+ }
6674
+ function failures(results) {
6675
+ return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
6676
+ }
6677
+
6605
6678
  // src/core/tiers/t3-worker.ts
6606
6679
  var CriticalToolError = class extends Error {
6607
6680
  constructor(message, toolName) {
@@ -6918,7 +6991,7 @@ Now execute your subtask using this context where relevant.`
6918
6991
  }
6919
6992
  const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
6920
6993
  if (!artifactCheck.ok) {
6921
- correctionAttempts = 1;
6994
+ correctionAttempts++;
6922
6995
  issues.push(...artifactCheck.issues);
6923
6996
  output = await this.correctOutput(output, artifactCheck.issues);
6924
6997
  const retryArtifactCheck = await this.verifyArtifacts(assignment);
@@ -6936,18 +7009,40 @@ Now execute your subtask using this context where relevant.`
6936
7009
  this.tools.map((t) => t.name)
6937
7010
  );
6938
7011
  if (visualIssue) {
6939
- correctionAttempts = 1;
7012
+ correctionAttempts++;
6940
7013
  issues.push(visualIssue);
6941
7014
  this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
6942
7015
  output = await this.correctOutput(output, [visualIssue]);
6943
7016
  }
7017
+ const acceptanceResults = await this.checkAcceptance(assignment);
7018
+ const acceptanceFailures = failures(acceptanceResults);
7019
+ if (acceptanceFailures.length > 0) {
7020
+ correctionAttempts++;
7021
+ issues.push(...acceptanceFailures);
7022
+ this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
7023
+ output = await this.correctOutput(output, acceptanceFailures);
7024
+ const recheck = failures(await this.checkAcceptance(assignment));
7025
+ if (recheck.length > 0) {
7026
+ issues.push(...recheck);
7027
+ checksRun.push(...acceptanceResults.map((r) => r.criterion));
7028
+ failed.push(...recheck);
7029
+ this.setStatus("FAILED");
7030
+ this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
7031
+ return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
7032
+ }
7033
+ }
7034
+ for (const result2 of acceptanceResults) {
7035
+ if (result2.verdict === "undecidable") continue;
7036
+ checksRun.push(result2.criterion);
7037
+ if (result2.verdict === "passed") passed.push(result2.criterion);
7038
+ }
6944
7039
  this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
6945
- const testResult = await this.selfTest(assignment, output);
7040
+ const testResult = await this.selfTest(assignment, output, undecided(acceptanceResults));
6946
7041
  checksRun.push(...testResult.checksRun);
6947
7042
  passed.push(...testResult.passed);
6948
7043
  failed.push(...testResult.failed);
6949
7044
  if (testResult.failed.length > 0) {
6950
- correctionAttempts = 1;
7045
+ correctionAttempts++;
6951
7046
  issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
6952
7047
  const corrected = await this.correctOutput(output, testResult.failed);
6953
7048
  output = corrected;
@@ -7255,8 +7350,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
7255
7350
  // Media generation can run for a minute; without this a cancelled run
7256
7351
  // still pays for an image nobody will see.
7257
7352
  ...this.signal ? { signal: this.signal } : {},
7258
- saveSnapshot: async (path27, content) => {
7259
- this.store?.addFileSnapshot(this.taskId, path27, content);
7353
+ saveSnapshot: async (path28, content) => {
7354
+ this.store?.addFileSnapshot(this.taskId, path28, content);
7260
7355
  },
7261
7356
  sendPeerSync: (to, syncType, content) => {
7262
7357
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -7443,7 +7538,7 @@ ${assignment.expectedOutput}`;
7443
7538
  const { promisify: promisify4 } = await import('util');
7444
7539
  const execAsync2 = promisify4(exec2);
7445
7540
  for (const artifactPath of artifactPaths) {
7446
- const absolutePath = path25__default.default.resolve(process.cwd(), artifactPath);
7541
+ const absolutePath = path26__default.default.resolve(process.cwd(), artifactPath);
7447
7542
  try {
7448
7543
  const stat = await fs5__default.default.stat(absolutePath);
7449
7544
  if (!stat.isFile()) {
@@ -7478,7 +7573,7 @@ ${assignment.expectedOutput}`;
7478
7573
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
7479
7574
  continue;
7480
7575
  }
7481
- const ext = path25__default.default.extname(absolutePath).toLowerCase();
7576
+ const ext = path26__default.default.extname(absolutePath).toLowerCase();
7482
7577
  try {
7483
7578
  if (ext === ".ts" || ext === ".tsx") {
7484
7579
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -7564,14 +7659,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7564
7659
  }
7565
7660
  return current;
7566
7661
  }
7567
- async selfTest(assignment, output) {
7662
+ /**
7663
+ * Run the acceptance criteria that can be settled by looking at the workspace.
7664
+ * Reads go through the same workspace-relative resolution verifyArtifacts uses.
7665
+ */
7666
+ async checkAcceptance(assignment) {
7667
+ const criteria = assignment.acceptance ?? [];
7668
+ if (!criteria.length) return [];
7669
+ const resolve2 = (target) => path26__default.default.resolve(process.cwd(), target);
7670
+ return evaluateAcceptance(criteria, assignment.files ?? [], {
7671
+ stat: async (target) => {
7672
+ try {
7673
+ const stat = await fs5__default.default.stat(resolve2(target));
7674
+ return stat.isFile() ? { size: stat.size } : null;
7675
+ } catch {
7676
+ return null;
7677
+ }
7678
+ },
7679
+ read: async (target) => {
7680
+ try {
7681
+ const content = await fs5__default.default.readFile(resolve2(target), "utf-8");
7682
+ return content.includes("\uFFFD") ? null : content;
7683
+ } catch {
7684
+ return null;
7685
+ }
7686
+ }
7687
+ });
7688
+ }
7689
+ async selfTest(assignment, output, pendingAcceptance) {
7690
+ const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
7568
7691
  const prompt = `Self-test this output against the assignment requirements.
7569
7692
 
7570
7693
  Assignment: ${assignment.description}
7571
7694
  Expected output: ${assignment.expectedOutput}
7572
7695
  Constraints: ${assignment.constraints.join("; ")}
7573
- ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
7574
- ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
7696
+ ${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
7697
+ ${acceptance.map((a) => `- ${a}`).join("\n")}
7575
7698
  ` : ""}
7576
7699
  Output to test:
7577
7700
  ${output}
@@ -7638,8 +7761,8 @@ ${output.slice(0, 4e3)}`;
7638
7761
  } catch {
7639
7762
  }
7640
7763
  }
7641
- async correctOutput(originalOutput, failures) {
7642
- const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
7764
+ async correctOutput(originalOutput, failures2) {
7765
+ const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
7643
7766
 
7644
7767
  Original output:
7645
7768
  ${originalOutput}
@@ -8300,12 +8423,220 @@ function describeGenerationForPlanner(has) {
8300
8423
  return lines.join("\n");
8301
8424
  }
8302
8425
 
8426
+ // src/core/orchestration/task-graph.ts
8427
+ var TaskGraphValidationError = class extends Error {
8428
+ constructor(issues) {
8429
+ super(issues.map((issue) => issue.message).join("; "));
8430
+ this.issues = issues;
8431
+ this.name = "TaskGraphValidationError";
8432
+ }
8433
+ issues;
8434
+ };
8435
+ function normalizeDependencies(input) {
8436
+ if (!input?.length) return [];
8437
+ return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
8438
+ }
8439
+ function cycleNodes(nodes) {
8440
+ const adjacency = new Map(
8441
+ nodes.map((node) => [node.id, node.dependsOn])
8442
+ );
8443
+ const index = /* @__PURE__ */ new Map();
8444
+ const lowlink = /* @__PURE__ */ new Map();
8445
+ const onStack = /* @__PURE__ */ new Set();
8446
+ const stack = [];
8447
+ const inCycle = /* @__PURE__ */ new Set();
8448
+ let counter = 0;
8449
+ const open = (id) => {
8450
+ index.set(id, counter);
8451
+ lowlink.set(id, counter);
8452
+ counter++;
8453
+ stack.push(id);
8454
+ onStack.add(id);
8455
+ };
8456
+ for (const root of nodes) {
8457
+ if (index.has(root.id)) continue;
8458
+ open(root.id);
8459
+ const frames = [{ id: root.id, edge: 0 }];
8460
+ while (frames.length > 0) {
8461
+ const frame = frames[frames.length - 1];
8462
+ const neighbours = adjacency.get(frame.id) ?? [];
8463
+ if (frame.edge < neighbours.length) {
8464
+ const next = neighbours[frame.edge];
8465
+ frame.edge++;
8466
+ if (!adjacency.has(next)) continue;
8467
+ if (!index.has(next)) {
8468
+ open(next);
8469
+ frames.push({ id: next, edge: 0 });
8470
+ } else if (onStack.has(next)) {
8471
+ lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
8472
+ }
8473
+ continue;
8474
+ }
8475
+ frames.pop();
8476
+ const parent = frames[frames.length - 1];
8477
+ if (parent) {
8478
+ lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
8479
+ }
8480
+ if (lowlink.get(frame.id) === index.get(frame.id)) {
8481
+ const component = [];
8482
+ for (; ; ) {
8483
+ const member = stack.pop();
8484
+ onStack.delete(member);
8485
+ component.push(member);
8486
+ if (member === frame.id) break;
8487
+ }
8488
+ const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
8489
+ if (cyclic) for (const member of component) inCycle.add(member);
8490
+ }
8491
+ }
8492
+ }
8493
+ return inCycle;
8494
+ }
8495
+ function throwIfStrict(mode, issues) {
8496
+ if (mode === "strict" && issues.length > 0) {
8497
+ throw new TaskGraphValidationError(issues);
8498
+ }
8499
+ }
8500
+ function compileTaskGraph(inputNodes, options = {}) {
8501
+ const mode = options.mode ?? "strict";
8502
+ const issues = [];
8503
+ if (inputNodes.length === 0) {
8504
+ issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
8505
+ throwIfStrict(mode, issues);
8506
+ }
8507
+ const seen = /* @__PURE__ */ new Set();
8508
+ const nodes = [];
8509
+ for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
8510
+ const input = inputNodes[ordinal];
8511
+ const id = input.id?.trim();
8512
+ const title = input.title?.trim();
8513
+ if (!id || !title) {
8514
+ issues.push({
8515
+ code: "INVALID_NODE",
8516
+ nodeId: id || void 0,
8517
+ message: `Task graph node at index ${ordinal} requires a non-empty id and title`
8518
+ });
8519
+ continue;
8520
+ }
8521
+ if (seen.has(id)) {
8522
+ issues.push({
8523
+ code: "DUPLICATE_NODE_ID",
8524
+ nodeId: id,
8525
+ message: `Duplicate task graph node id: ${id}`
8526
+ });
8527
+ continue;
8528
+ }
8529
+ seen.add(id);
8530
+ nodes.push({
8531
+ id,
8532
+ title,
8533
+ kind: input.kind,
8534
+ dependsOn: normalizeDependencies(input.dependsOn),
8535
+ payload: input.payload,
8536
+ ordinal
8537
+ });
8538
+ }
8539
+ throwIfStrict(mode, issues);
8540
+ const ids = new Set(nodes.map((node) => node.id));
8541
+ const sanitized = nodes.map((node) => {
8542
+ const dependsOn = [];
8543
+ for (const dependencyId of node.dependsOn) {
8544
+ if (dependencyId === node.id) {
8545
+ issues.push({
8546
+ code: "SELF_DEPENDENCY",
8547
+ nodeId: node.id,
8548
+ dependencyId,
8549
+ message: `Task graph node ${node.id} cannot depend on itself`
8550
+ });
8551
+ continue;
8552
+ }
8553
+ if (!ids.has(dependencyId)) {
8554
+ issues.push({
8555
+ code: "UNKNOWN_DEPENDENCY",
8556
+ nodeId: node.id,
8557
+ dependencyId,
8558
+ message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
8559
+ });
8560
+ continue;
8561
+ }
8562
+ dependsOn.push(dependencyId);
8563
+ }
8564
+ return { ...node, dependsOn };
8565
+ });
8566
+ throwIfStrict(mode, issues);
8567
+ const cyclic = cycleNodes(sanitized);
8568
+ let repaired = sanitized;
8569
+ if (cyclic.size > 0) {
8570
+ const cycleIssues = [];
8571
+ repaired = sanitized.map((node) => {
8572
+ if (!cyclic.has(node.id)) return node;
8573
+ const dependsOn = node.dependsOn.filter((dependencyId) => {
8574
+ if (!cyclic.has(dependencyId)) return true;
8575
+ cycleIssues.push({
8576
+ code: "CYCLE_EDGE_REMOVED",
8577
+ nodeId: node.id,
8578
+ dependencyId,
8579
+ message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
8580
+ });
8581
+ return false;
8582
+ });
8583
+ return { ...node, dependsOn };
8584
+ });
8585
+ issues.push(...cycleIssues);
8586
+ throwIfStrict(mode, cycleIssues);
8587
+ }
8588
+ const remainingCycle = cycleNodes(repaired);
8589
+ if (remainingCycle.size > 0) {
8590
+ throw new TaskGraphValidationError([
8591
+ {
8592
+ code: "INVALID_NODE",
8593
+ message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
8594
+ }
8595
+ ]);
8596
+ }
8597
+ return {
8598
+ graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
8599
+ issues
8600
+ };
8601
+ }
8602
+
8603
+ // src/core/orchestration/adapters.ts
8604
+ function compileSectionGraph(sections) {
8605
+ return compileTaskGraph(
8606
+ sections.map((section) => ({
8607
+ id: section.sectionId,
8608
+ title: section.sectionTitle,
8609
+ kind: "section",
8610
+ dependsOn: section.dependsOn,
8611
+ payload: section
8612
+ })),
8613
+ { mode: "repair" }
8614
+ );
8615
+ }
8616
+ function compileSubtaskGraph(subtasks) {
8617
+ return compileTaskGraph(
8618
+ subtasks.map((subtask) => ({
8619
+ id: subtask.subtaskId,
8620
+ title: subtask.subtaskTitle,
8621
+ kind: "subtask",
8622
+ dependsOn: subtask.dependsOn,
8623
+ payload: subtask
8624
+ })),
8625
+ { mode: "repair" }
8626
+ );
8627
+ }
8628
+
8303
8629
  // src/core/tiers/t2-manager.ts
8304
8630
  function buildT2SystemPrompt(has) {
8305
8631
  const generation = describeGenerationForPlanner(has);
8306
8632
  return [
8307
8633
  "You are a T2 Manager agent in the Cascade AI system.",
8308
- "Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
8634
+ // Must agree with the decomposition prompt in decomposeSection(), which is
8635
+ // the one carrying the actual instruction. It said 1-4 and "the FEWEST that
8636
+ // fully cover it"; this said 2-5. The model received both, so its floor was
8637
+ // simultaneously one and two, and the cheaper reading — pad to two — costs a
8638
+ // real worker call on every small section.
8639
+ "Your role is to analyze a section of a task and decompose it into 1-4 discrete subtasks for T3 Workers \u2014 the fewest that fully cover it.",
8309
8640
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
8310
8641
  has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
8311
8642
  "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
@@ -8701,21 +9032,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8701
9032
  * then reduce in-degrees of their dependents and repeat.
8702
9033
  */
8703
9034
  async runWithDependencies(assignments, workerMap, taskId) {
9035
+ const compiled = compileSubtaskGraph(assignments);
9036
+ for (const issue of compiled.issues) {
9037
+ this.log(`\u26A0 Subtask graph: ${issue.message}`);
9038
+ }
9039
+ const sanitizedAssignments = compiled.graph.nodes.map((node) => {
9040
+ const assignment = node.payload;
9041
+ const original = assignment.dependsOn ?? [];
9042
+ const repaired = node.dependsOn;
9043
+ const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
9044
+ return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
9045
+ });
8704
9046
  const adj = /* @__PURE__ */ new Map();
8705
9047
  const inDegree = /* @__PURE__ */ new Map();
8706
9048
  const resultMap = /* @__PURE__ */ new Map();
8707
- for (const a of assignments) {
9049
+ for (const a of sanitizedAssignments) {
8708
9050
  if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
8709
9051
  inDegree.set(a.subtaskId, 0);
8710
9052
  }
8711
- for (const a of assignments) {
8712
- const deps = a.dependsOn ?? [];
8713
- for (const dep of deps) {
9053
+ for (const a of sanitizedAssignments) {
9054
+ for (const dep of a.dependsOn ?? []) {
8714
9055
  adj.get(dep).add(a.subtaskId);
8715
9056
  inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
8716
9057
  }
8717
9058
  }
8718
- const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
8719
9059
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
8720
9060
  let wave = 0;
8721
9061
  let respawnBudget = 1;
@@ -8922,49 +9262,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8922
9262
  }
8923
9263
  return [...resultMap.values()];
8924
9264
  }
8925
- /**
8926
- * Detects cyclic dependencies using Kahn's algorithm and breaks them
8927
- * by removing back-edges. Returns a sanitized copy of assignments.
8928
- *
8929
- * A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
8930
- * meaning t3 will start without waiting for t1, preventing deadlock.
8931
- */
8932
- breakCycles(assignments, adj, inDegree) {
8933
- const degree = new Map(inDegree);
8934
- const queue = [];
8935
- const visited = /* @__PURE__ */ new Set();
8936
- for (const [id, d] of degree) {
8937
- if (d === 0) queue.push(id);
8938
- }
8939
- while (queue.length > 0) {
8940
- const id = queue.shift();
8941
- visited.add(id);
8942
- for (const dep of adj.get(id) ?? []) {
8943
- const newDeg = (degree.get(dep) ?? 1) - 1;
8944
- degree.set(dep, newDeg);
8945
- if (newDeg === 0) queue.push(dep);
8946
- }
8947
- }
8948
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
8949
- if (cycleNodes.length === 0) return assignments;
8950
- this.log(
8951
- `\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
8952
- );
8953
- return assignments.map((a) => {
8954
- if (!cycleNodes.includes(a.subtaskId)) return a;
8955
- const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
8956
- if (safeDeps.length !== (a.dependsOn ?? []).length) {
8957
- this.log(
8958
- ` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
8959
- );
8960
- for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
8961
- inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
8962
- adj.get(removed)?.delete(a.subtaskId);
8963
- }
8964
- }
8965
- return { ...a, dependsOn: safeDeps };
8966
- });
8967
- }
8968
9265
  async retryT3(assignment, taskId) {
8969
9266
  this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
8970
9267
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
@@ -9100,6 +9397,57 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
9100
9397
  }
9101
9398
  };
9102
9399
 
9400
+ // src/core/orchestration/scheduler.ts
9401
+ var DependencyScheduler = class {
9402
+ constructor(graph, hooks, options = {}) {
9403
+ this.graph = graph;
9404
+ this.hooks = hooks;
9405
+ this.options = options;
9406
+ }
9407
+ graph;
9408
+ hooks;
9409
+ options;
9410
+ async run() {
9411
+ const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
9412
+ const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
9413
+ const dependents = /* @__PURE__ */ new Map();
9414
+ for (const node of nodes) dependents.set(node.id, []);
9415
+ for (const node of nodes) {
9416
+ for (const dependencyId of node.dependsOn) {
9417
+ dependents.get(dependencyId)?.push(node.id);
9418
+ }
9419
+ }
9420
+ const completed = /* @__PURE__ */ new Set();
9421
+ const results = /* @__PURE__ */ new Map();
9422
+ const waves = [];
9423
+ let wave = 0;
9424
+ while (completed.size < nodes.length) {
9425
+ const ready = nodes.filter((node) => !completed.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
9426
+ if (ready.length === 0) {
9427
+ throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
9428
+ }
9429
+ waves.push(ready.map((node) => node.id));
9430
+ await this.hooks.onWaveStart?.(ready, wave);
9431
+ const runOne = async (node) => {
9432
+ const result = await this.hooks.execute(node);
9433
+ results.set(node.id, result);
9434
+ completed.add(node.id);
9435
+ for (const dependentId of dependents.get(node.id) ?? []) {
9436
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
9437
+ }
9438
+ await this.hooks.onNodeComplete?.(node, result);
9439
+ };
9440
+ if ((this.options.mode ?? "parallel") === "sequential") {
9441
+ for (const node of ready) await runOne(node);
9442
+ } else {
9443
+ await Promise.all(ready.map(runOne));
9444
+ }
9445
+ wave++;
9446
+ }
9447
+ return { results, waves };
9448
+ }
9449
+ };
9450
+
9103
9451
  // src/utils/json-extract.ts
9104
9452
  function extractFirstJsonObject(input) {
9105
9453
  if (!input) return null;
@@ -9171,7 +9519,7 @@ function buildT1SystemPrompt(has) {
9171
9519
  Your responsibilities:
9172
9520
  1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
9173
9521
  2. Decompose the task into logical sections (one per T2 Manager)
9174
- 3. For each section, define 2-5 subtasks for T3 Workers
9522
+ 3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
9175
9523
  4. Return a structured plan as JSON
9176
9524
 
9177
9525
  CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
@@ -9586,7 +9934,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9586
9934
  }
9587
9935
  }
9588
9936
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
9589
- if (plan.sections.length < min) ;
9937
+ if (plan.sections.length < min || plan.sections.length > max) {
9938
+ this.log(
9939
+ `\u26A0 Plan has ${plan.sections.length} section(s) but complexity "${plan.complexity}" expects ${min}-${max}. Keeping the plan as written \u2014 a right-sized plan is better than one padded to hit a floor.`
9940
+ );
9941
+ }
9590
9942
  }
9591
9943
  async dispatchT2Managers(sections) {
9592
9944
  for (const section of sections) {
@@ -9698,114 +10050,91 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9698
10050
  return t2Results;
9699
10051
  }
9700
10052
  /**
9701
- * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
10053
+ * Runs T2 managers respecting dependsOn declarations.
10054
+ *
10055
+ * Graph construction, validation and cycle repair now live in
10056
+ * compileSectionGraph (orchestration/adapters.ts), and wave execution in
10057
+ * DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
10058
+ * ordered by one implementation rather than two that drifted.
10059
+ *
10060
+ * The hand-rolled Kahn pass this replaces treated every node it could not
10061
+ * reach as part of a cycle. That set also contains everything DOWNSTREAM of a
10062
+ * cycle, whose in-degree can never fall to zero while its dependency is stuck,
10063
+ * so breaking a cycle here also cut the dependencies of innocent later
10064
+ * sections and started them immediately — before the sections they consume.
10065
+ * compileTaskGraph uses strongly connected components, so only real cycle
10066
+ * members are touched.
9702
10067
  */
9703
10068
  async runT2sWithDependencies(sections, managers, taskId) {
9704
- const adj = /* @__PURE__ */ new Map();
9705
- const inDegree = /* @__PURE__ */ new Map();
9706
- const resultMap = /* @__PURE__ */ new Map();
9707
- const allKeys = new Set(sections.map((s) => s.sectionId));
9708
- for (const s of sections) {
9709
- if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
9710
- inDegree.set(s.sectionId, 0);
9711
- s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
10069
+ const compiled = compileSectionGraph(sections);
10070
+ for (const issue of compiled.issues) {
10071
+ this.log(`\u26A0 Section graph: ${issue.message}`);
9712
10072
  }
9713
- for (const s of sections) {
9714
- for (const dep of s.dependsOn ?? []) {
9715
- adj.get(dep).add(s.sectionId);
9716
- inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
9717
- }
9718
- }
9719
- const queue = [];
9720
- const degree = new Map(inDegree);
9721
- for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
9722
- const visited = /* @__PURE__ */ new Set();
9723
- while (queue.length > 0) {
9724
- const u = queue.shift();
9725
- visited.add(u);
9726
- for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
9727
- const newDeg = (degree.get(v) ?? 1) - 1;
9728
- degree.set(v, newDeg);
9729
- if (newDeg === 0) queue.push(v);
9730
- }
9731
- }
9732
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
9733
- if (cycleNodes.length > 0) {
9734
- this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
9735
- for (const s of sections) {
9736
- if (cycleNodes.includes(s.sectionId)) {
9737
- const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
9738
- for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
9739
- inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
9740
- adj.get(removed)?.delete(s.sectionId);
9741
- }
9742
- s.dependsOn = safeDeps;
9743
- }
9744
- }
10073
+ const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
10074
+ for (const section of sections) {
10075
+ const node = byId.get(section.sectionId);
10076
+ if (node) section.dependsOn = [...node.dependsOn];
9745
10077
  }
9746
- const totalSections = sections.length;
10078
+ const resultMap = /* @__PURE__ */ new Map();
10079
+ const totalSections = compiled.graph.nodes.length;
9747
10080
  let completedSections = 0;
9748
- const executeWave = async () => {
9749
- const readyIds = [];
9750
- for (const [id, deg] of inDegree.entries()) {
9751
- if (deg === 0 && !resultMap.has(id)) {
9752
- readyIds.push(id);
9753
- }
9754
- }
9755
- if (readyIds.length === 0) return;
9756
- const runOne = async (id) => {
9757
- resultMap.set(id, null);
9758
- const index = sections.findIndex((s) => s.sectionId === id);
9759
- const section = sections[index];
9760
- const manager = managers[index];
9761
- const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
9762
- this.sendStatusUpdate({
9763
- progressPct,
9764
- currentAction: `T2 working on: ${section.sectionTitle}`,
9765
- status: "IN_PROGRESS"
9766
- });
9767
- this.throwIfCancelled();
9768
- let result;
9769
- try {
9770
- result = await manager.execute(section, taskId, this.signal);
9771
- manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
9772
- if (result.status === "ESCALATED") {
9773
- this.escalations.push({
9774
- raisedBy: `T2_${section.sectionId}`,
10081
+ const scheduler = new DependencyScheduler(
10082
+ compiled.graph,
10083
+ {
10084
+ execute: async (node) => {
10085
+ const section = node.payload;
10086
+ const manager = managers[node.ordinal];
10087
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
10088
+ this.sendStatusUpdate({
10089
+ progressPct,
10090
+ currentAction: `T2 working on: ${section.sectionTitle}`,
10091
+ status: "IN_PROGRESS"
10092
+ });
10093
+ this.throwIfCancelled();
10094
+ try {
10095
+ const result = await manager.execute(section, taskId, this.signal);
10096
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
10097
+ if (result.status === "ESCALATED") {
10098
+ this.escalations.push({
10099
+ raisedBy: `T2_${section.sectionId}`,
10100
+ sectionId: section.sectionId,
10101
+ attempted: result.issues,
10102
+ blocker: result.issues.join("; "),
10103
+ needs: "Human review required"
10104
+ });
10105
+ }
10106
+ return result;
10107
+ } catch (err) {
10108
+ return {
9775
10109
  sectionId: section.sectionId,
9776
- attempted: result.issues,
9777
- blocker: result.issues.join("; "),
9778
- needs: "Human review required"
9779
- });
10110
+ sectionTitle: section.sectionTitle,
10111
+ status: "FAILED",
10112
+ t3Results: [],
10113
+ sectionSummary: "",
10114
+ issues: [err instanceof Error ? err.message : String(err)]
10115
+ };
9780
10116
  }
9781
- } catch (err) {
9782
- result = {
9783
- sectionId: section.sectionId,
9784
- sectionTitle: section.sectionTitle,
9785
- status: "FAILED",
9786
- t3Results: [],
9787
- sectionSummary: "",
9788
- issues: [err instanceof Error ? err.message : String(err)]
9789
- };
9790
- }
9791
- resultMap.set(id, result);
9792
- completedSections++;
9793
- for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
9794
- inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
9795
- }
9796
- };
9797
- if (this.router.getT3ExecutionMode?.() === "sequential") {
9798
- for (const id of readyIds) {
9799
- await runOne(id);
10117
+ },
10118
+ onNodeComplete: (node, result) => {
10119
+ resultMap.set(node.id, result);
10120
+ completedSections++;
10121
+ this.emit("section:complete", {
10122
+ id: node.id,
10123
+ title: result.sectionTitle,
10124
+ status: result.status,
10125
+ output: result.sectionSummary
10126
+ });
9800
10127
  }
9801
- } else {
9802
- await Promise.all(readyIds.map(runOne));
9803
- }
9804
- if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
9805
- await executeWave();
10128
+ },
10129
+ {
10130
+ // Cross-section concurrency respects the same t3Execution mode as the
10131
+ // intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
10132
+ // serialized T3 workers within a single section while every section's
10133
+ // T2Manager (and its T3 workers) still ran in parallel here.
10134
+ mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
9806
10135
  }
9807
- };
9808
- await executeWave();
10136
+ );
10137
+ await scheduler.run();
9809
10138
  return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
9810
10139
  }
9811
10140
  async compileFinalOutput(originalPrompt, plan, t2Results) {
@@ -10435,16 +10764,16 @@ function resolveInWorkspace(workspaceRoot, input) {
10435
10764
  if (typeof input !== "string" || input.length === 0) {
10436
10765
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
10437
10766
  }
10438
- const root = path25__default.default.resolve(workspaceRoot);
10439
- const abs = path25__default.default.isAbsolute(input) ? path25__default.default.resolve(input) : path25__default.default.resolve(root, input);
10440
- const rel = path25__default.default.relative(root, abs);
10441
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25__default.default.isAbsolute(rel)) {
10767
+ const root = path26__default.default.resolve(workspaceRoot);
10768
+ const abs = path26__default.default.isAbsolute(input) ? path26__default.default.resolve(input) : path26__default.default.resolve(root, input);
10769
+ const rel = path26__default.default.relative(root, abs);
10770
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26__default.default.isAbsolute(rel)) {
10442
10771
  throw new WorkspaceSandboxError(input, root);
10443
10772
  }
10444
10773
  try {
10445
- const real = fs24__default.default.realpathSync(abs);
10446
- const realRel = path25__default.default.relative(root, real);
10447
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25__default.default.isAbsolute(realRel))) {
10774
+ const real = fs25__default.default.realpathSync(abs);
10775
+ const realRel = path26__default.default.relative(root, real);
10776
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26__default.default.isAbsolute(realRel))) {
10448
10777
  throw new WorkspaceSandboxError(input, root);
10449
10778
  }
10450
10779
  } catch (e) {
@@ -11102,7 +11431,7 @@ var GenerateDocumentTool = class extends BaseTool {
11102
11431
  }
11103
11432
  }
11104
11433
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
11105
- await fs5__default.default.mkdir(path25__default.default.dirname(absPath), { recursive: true });
11434
+ await fs5__default.default.mkdir(path26__default.default.dirname(absPath), { recursive: true });
11106
11435
  await fs5__default.default.writeFile(absPath, bytes);
11107
11436
  const charts = countCharts(content);
11108
11437
  return [
@@ -11497,7 +11826,7 @@ var FileWriteTool = class extends BaseTool {
11497
11826
  } catch {
11498
11827
  }
11499
11828
  }
11500
- await fs5__default.default.mkdir(path25__default.default.dirname(absPath), { recursive: true });
11829
+ await fs5__default.default.mkdir(path26__default.default.dirname(absPath), { recursive: true });
11501
11830
  await fs5__default.default.writeFile(absPath, content, "utf-8");
11502
11831
  return `Written ${content.length} characters to ${filePath}`;
11503
11832
  }
@@ -11995,7 +12324,7 @@ var ImageAnalyzeTool = class extends BaseTool {
11995
12324
  };
11996
12325
  async function fileToImageAttachment(filePath) {
11997
12326
  const data = await fs5__default.default.readFile(filePath);
11998
- const ext = path25__default.default.extname(filePath).toLowerCase();
12327
+ const ext = path26__default.default.extname(filePath).toLowerCase();
11999
12328
  const mimeMap = {
12000
12329
  ".jpg": "image/jpeg",
12001
12330
  ".jpeg": "image/jpeg",
@@ -12029,14 +12358,14 @@ var PDFCreateTool = class extends BaseTool {
12029
12358
  const filePath = input["path"];
12030
12359
  const content = input["content"];
12031
12360
  const title = input["title"];
12032
- const dir = path25__default.default.dirname(filePath);
12033
- if (!fs24__default.default.existsSync(dir)) {
12034
- fs24__default.default.mkdirSync(dir, { recursive: true });
12361
+ const dir = path26__default.default.dirname(filePath);
12362
+ if (!fs25__default.default.existsSync(dir)) {
12363
+ fs25__default.default.mkdirSync(dir, { recursive: true });
12035
12364
  }
12036
12365
  return new Promise((resolve2, reject) => {
12037
12366
  try {
12038
12367
  const doc = new PDFDocument__default.default({ margin: 50 });
12039
- const stream = fs24__default.default.createWriteStream(filePath);
12368
+ const stream = fs25__default.default.createWriteStream(filePath);
12040
12369
  doc.pipe(stream);
12041
12370
  if (title) {
12042
12371
  doc.info["Title"] = title;
@@ -12114,22 +12443,22 @@ var CodeInterpreterTool = class extends BaseTool {
12114
12443
  }
12115
12444
  cmdPrefix = NODE_CMD;
12116
12445
  }
12117
- const tmpDir = path25__default.default.join(this.workspaceRoot, ".cascade", "tmp");
12118
- if (!fs24__default.default.existsSync(tmpDir)) {
12119
- fs24__default.default.mkdirSync(tmpDir, { recursive: true });
12446
+ const tmpDir = path26__default.default.join(this.workspaceRoot, ".cascade", "tmp");
12447
+ if (!fs25__default.default.existsSync(tmpDir)) {
12448
+ fs25__default.default.mkdirSync(tmpDir, { recursive: true });
12120
12449
  }
12121
12450
  const extension = language === "python" ? "py" : "js";
12122
12451
  const fileName = `intp_${crypto4.randomUUID().slice(0, 8)}.${extension}`;
12123
- const filePath = path25__default.default.join(tmpDir, fileName);
12124
- fs24__default.default.writeFileSync(filePath, code, "utf-8");
12452
+ const filePath = path26__default.default.join(tmpDir, fileName);
12453
+ fs25__default.default.writeFileSync(filePath, code, "utf-8");
12125
12454
  const execArgs = [filePath, ...args];
12126
12455
  return new Promise((resolve2) => {
12127
12456
  const startMs = Date.now();
12128
12457
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
12129
12458
  const duration = Date.now() - startMs;
12130
12459
  try {
12131
- if (fs24__default.default.existsSync(filePath)) {
12132
- fs24__default.default.unlinkSync(filePath);
12460
+ if (fs25__default.default.existsSync(filePath)) {
12461
+ fs25__default.default.unlinkSync(filePath);
12133
12462
  }
12134
12463
  } catch (cleanupErr) {
12135
12464
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -12441,7 +12770,7 @@ var GlobTool = class extends BaseTool {
12441
12770
  };
12442
12771
  async execute(input, _options) {
12443
12772
  const pattern = input["pattern"];
12444
- const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12773
+ const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12445
12774
  const matches = await glob.glob(pattern, {
12446
12775
  cwd: searchPath,
12447
12776
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -12454,7 +12783,7 @@ var GlobTool = class extends BaseTool {
12454
12783
  const withMtime = await Promise.all(
12455
12784
  matches.map(async (rel) => {
12456
12785
  try {
12457
- const stat = await fs5__default.default.stat(path25__default.default.join(searchPath, rel));
12786
+ const stat = await fs5__default.default.stat(path26__default.default.join(searchPath, rel));
12458
12787
  return { rel, mtime: stat.mtimeMs };
12459
12788
  } catch {
12460
12789
  return { rel, mtime: 0 };
@@ -12503,7 +12832,7 @@ var GrepTool = class extends BaseTool {
12503
12832
  };
12504
12833
  async execute(input, _options) {
12505
12834
  const pattern = input["pattern"];
12506
- const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12835
+ const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12507
12836
  const globPattern = input["glob"];
12508
12837
  const outputMode = input["output_mode"] ?? "content";
12509
12838
  const context = input["context"] ?? 0;
@@ -12557,12 +12886,12 @@ var GrepTool = class extends BaseTool {
12557
12886
  nodir: true
12558
12887
  });
12559
12888
  } catch {
12560
- files = [path25__default.default.relative(searchPath, searchPath) || "."];
12889
+ files = [path26__default.default.relative(searchPath, searchPath) || "."];
12561
12890
  }
12562
12891
  const results = [];
12563
12892
  let totalCount = 0;
12564
12893
  for (const rel of files) {
12565
- const abs = path25__default.default.join(searchPath, rel);
12894
+ const abs = path26__default.default.join(searchPath, rel);
12566
12895
  let content;
12567
12896
  try {
12568
12897
  content = await fs5__default.default.readFile(abs, "utf-8");
@@ -12949,10 +13278,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
12949
13278
  }
12950
13279
  isIgnored(filePath) {
12951
13280
  if (!filePath) return false;
12952
- const abs = path25__default.default.resolve(this.workspaceRoot, filePath);
12953
- const rel = path25__default.default.relative(this.workspaceRoot, abs);
12954
- if (!rel || rel.startsWith("..") || path25__default.default.isAbsolute(rel)) return true;
12955
- const posixRel = rel.split(path25__default.default.sep).join("/");
13281
+ const abs = path26__default.default.resolve(this.workspaceRoot, filePath);
13282
+ const rel = path26__default.default.relative(this.workspaceRoot, abs);
13283
+ if (!rel || rel.startsWith("..") || path26__default.default.isAbsolute(rel)) return true;
13284
+ const posixRel = rel.split(path26__default.default.sep).join("/");
12956
13285
  return this.ignoreMatcher.ignores(posixRel);
12957
13286
  }
12958
13287
  };
@@ -13186,22 +13515,22 @@ var FileMcpOAuthStore = class {
13186
13515
  filePath;
13187
13516
  load() {
13188
13517
  try {
13189
- return JSON.parse(fs24__default.default.readFileSync(this.filePath, "utf-8"));
13518
+ return JSON.parse(fs25__default.default.readFileSync(this.filePath, "utf-8"));
13190
13519
  } catch {
13191
13520
  return void 0;
13192
13521
  }
13193
13522
  }
13194
13523
  save(state) {
13195
- fs24__default.default.mkdirSync(path25__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
13196
- fs24__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
13524
+ fs25__default.default.mkdirSync(path26__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
13525
+ fs25__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
13197
13526
  try {
13198
- fs24__default.default.chmodSync(this.filePath, 384);
13527
+ fs25__default.default.chmodSync(this.filePath, 384);
13199
13528
  } catch {
13200
13529
  }
13201
13530
  }
13202
13531
  clear() {
13203
13532
  try {
13204
- fs24__default.default.rmSync(this.filePath);
13533
+ fs25__default.default.rmSync(this.filePath);
13205
13534
  } catch {
13206
13535
  }
13207
13536
  }
@@ -14098,6 +14427,14 @@ function heuristicAnalyze(prompt) {
14098
14427
  const estimatedTokens = wordCount * 5;
14099
14428
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
14100
14429
  }
14430
+ function hashPrompt(prompt) {
14431
+ let hash = 2166136261;
14432
+ for (let index = 0; index < prompt.length; index++) {
14433
+ hash ^= prompt.charCodeAt(index);
14434
+ hash = Math.imul(hash, 16777619) >>> 0;
14435
+ }
14436
+ return `${prompt.length}:${hash.toString(36)}`;
14437
+ }
14101
14438
  var analysisCache = /* @__PURE__ */ new Map();
14102
14439
  var TASK_TYPE_TAGS = {
14103
14440
  code: ["code", "instruction"],
@@ -14141,7 +14478,7 @@ var TaskAnalyzer = class {
14141
14478
  * Low confidence prompts fall back to a conservative mixed/moderate profile.
14142
14479
  */
14143
14480
  async analyze(prompt) {
14144
- const cacheKey = prompt.slice(0, 200);
14481
+ const cacheKey = hashPrompt(prompt);
14145
14482
  const cached = analysisCache.get(cacheKey);
14146
14483
  if (cached) {
14147
14484
  this.lastProfile = cached;
@@ -14240,7 +14577,7 @@ var TaskAnalyzer = class {
14240
14577
  analysisCache.clear();
14241
14578
  }
14242
14579
  };
14243
- var DEFAULT_STATS_FILE = path25__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
14580
+ var DEFAULT_STATS_FILE = path26__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
14244
14581
  var ModelPerformanceTracker = class {
14245
14582
  stats = /* @__PURE__ */ new Map();
14246
14583
  featureStats = /* @__PURE__ */ new Map();
@@ -14280,7 +14617,7 @@ var ModelPerformanceTracker = class {
14280
14617
  async save() {
14281
14618
  if (this.readOnly) return;
14282
14619
  try {
14283
- await fs5__default.default.mkdir(path25__default.default.dirname(this.statsFile), { recursive: true });
14620
+ await fs5__default.default.mkdir(path26__default.default.dirname(this.statsFile), { recursive: true });
14284
14621
  const modelsObj = {};
14285
14622
  const featuresObj = {};
14286
14623
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -14828,7 +15165,7 @@ Required capability: ${description.slice(0, 300)}`;
14828
15165
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
14829
15166
  async loadPersistedTools() {
14830
15167
  if (!this.workspacePath || !this.persistEnabled) return;
14831
- const file = path25__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
15168
+ const file = path26__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
14832
15169
  try {
14833
15170
  const raw = await fs5__default.default.readFile(file, "utf-8");
14834
15171
  const specs = JSON.parse(raw);
@@ -14852,8 +15189,8 @@ Required capability: ${description.slice(0, 300)}`;
14852
15189
  }
14853
15190
  async persist() {
14854
15191
  if (!this.workspacePath || !this.persistEnabled) return;
14855
- const dir = path25__default.default.join(this.workspacePath, ".cascade");
14856
- const file = path25__default.default.join(dir, DYNAMIC_TOOLS_FILE);
15192
+ const dir = path26__default.default.join(this.workspacePath, ".cascade");
15193
+ const file = path26__default.default.join(dir, DYNAMIC_TOOLS_FILE);
14857
15194
  try {
14858
15195
  await fs5__default.default.mkdir(dir, { recursive: true });
14859
15196
  await fs5__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -14873,12 +15210,12 @@ var WorldStateDB = class {
14873
15210
  constructor(workspacePath, debugMode = false) {
14874
15211
  this.workspacePath = workspacePath;
14875
15212
  this.debugMode = debugMode;
14876
- const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
14877
- if (!fs24__default.default.existsSync(cascadeDir)) {
14878
- fs24__default.default.mkdirSync(cascadeDir, { recursive: true });
15213
+ const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
15214
+ if (!fs25__default.default.existsSync(cascadeDir)) {
15215
+ fs25__default.default.mkdirSync(cascadeDir, { recursive: true });
14879
15216
  }
14880
- this.keyPath = path25__default.default.join(cascadeDir, "world_state.key");
14881
- this.dbPath = path25__default.default.join(cascadeDir, "world_state.db");
15217
+ this.keyPath = path26__default.default.join(cascadeDir, "world_state.key");
15218
+ this.dbPath = path26__default.default.join(cascadeDir, "world_state.db");
14882
15219
  this.initEncryptionKey();
14883
15220
  this.db = new Database__default.default(this.dbPath);
14884
15221
  this.db.pragma("journal_mode = WAL");
@@ -14935,11 +15272,11 @@ var WorldStateDB = class {
14935
15272
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
14936
15273
  }
14937
15274
  initEncryptionKey() {
14938
- if (fs24__default.default.existsSync(this.keyPath)) {
14939
- this.encryptionKey = fs24__default.default.readFileSync(this.keyPath);
15275
+ if (fs25__default.default.existsSync(this.keyPath)) {
15276
+ this.encryptionKey = fs25__default.default.readFileSync(this.keyPath);
14940
15277
  } else {
14941
15278
  this.encryptionKey = crypto4__default.default.randomBytes(32);
14942
- fs24__default.default.writeFileSync(this.keyPath, this.encryptionKey);
15279
+ fs25__default.default.writeFileSync(this.keyPath, this.encryptionKey);
14943
15280
  }
14944
15281
  }
14945
15282
  encrypt(text) {
@@ -15225,9 +15562,9 @@ var WorldStateDB = class {
15225
15562
  dumpDebugIfNeeded() {
15226
15563
  if (!this.debugMode) return;
15227
15564
  try {
15228
- const dumpPath = path25__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
15565
+ const dumpPath = path26__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
15229
15566
  const entries = this.getAllEntries();
15230
- fs24__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15567
+ fs25__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15231
15568
  } catch (err) {
15232
15569
  console.error("Failed to dump debug world state", err);
15233
15570
  }
@@ -15236,6 +15573,128 @@ var WorldStateDB = class {
15236
15573
  this.db.close();
15237
15574
  }
15238
15575
  };
15576
+ var RESUME_SCHEMA_VERSION = 1;
15577
+ var DEFAULT_MAX_CHECKPOINTS = 5;
15578
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
15579
+ var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
15580
+ function safeId(taskId) {
15581
+ const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
15582
+ return cleaned.slice(0, 100) || "run";
15583
+ }
15584
+ function isCheckpoint(value) {
15585
+ if (typeof value !== "object" || value === null) return false;
15586
+ const c = value;
15587
+ return c.version === RESUME_SCHEMA_VERSION && typeof c.taskId === "string" && typeof c.prompt === "string" && typeof c.createdAt === "string" && typeof c.partialOutput === "string" && Array.isArray(c.completed);
15588
+ }
15589
+ var ResumeStore = class {
15590
+ dir;
15591
+ maxCheckpoints;
15592
+ maxAgeMs;
15593
+ now;
15594
+ constructor(options) {
15595
+ this.dir = options.dir;
15596
+ this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
15597
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
15598
+ this.now = options.now ?? Date.now;
15599
+ }
15600
+ /**
15601
+ * Persist a checkpoint and prune. Never throws: failing to save a resume file
15602
+ * must not turn a recoverable stop into a crash, which would be a strictly
15603
+ * worse outcome than the one this store exists to improve.
15604
+ */
15605
+ async save(checkpoint) {
15606
+ try {
15607
+ const timestamp = this.now();
15608
+ const record = {
15609
+ ...checkpoint,
15610
+ version: RESUME_SCHEMA_VERSION,
15611
+ createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
15612
+ };
15613
+ await fs25.promises.mkdir(this.dir, { recursive: true });
15614
+ const finalPath = path26__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
15615
+ const tempPath = `${finalPath}.${process.pid}.tmp`;
15616
+ await fs25.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
15617
+ await fs25.promises.rename(tempPath, finalPath);
15618
+ await this.prune();
15619
+ } catch {
15620
+ }
15621
+ }
15622
+ /** Valid, unexpired checkpoints, newest first. */
15623
+ async list() {
15624
+ let entries;
15625
+ try {
15626
+ entries = await fs25.promises.readdir(this.dir);
15627
+ } catch {
15628
+ return [];
15629
+ }
15630
+ const cutoff = this.now() - this.maxAgeMs;
15631
+ const candidates = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).filter((e) => e.timestamp >= cutoff).sort((a, b) => b.timestamp - a.timestamp);
15632
+ const checkpoints = [];
15633
+ for (const candidate of candidates) {
15634
+ try {
15635
+ const raw = await fs25.promises.readFile(path26__default.default.join(this.dir, candidate.name), "utf-8");
15636
+ const parsed = JSON.parse(raw);
15637
+ if (isCheckpoint(parsed)) checkpoints.push(parsed);
15638
+ } catch {
15639
+ continue;
15640
+ }
15641
+ }
15642
+ return checkpoints;
15643
+ }
15644
+ /** The most recent resumable run, or null. */
15645
+ async latest() {
15646
+ return (await this.list())[0] ?? null;
15647
+ }
15648
+ /**
15649
+ * Load a checkpoint and delete it in the same step. Resuming twice from one
15650
+ * checkpoint would re-run restored work, so consumption is part of reading.
15651
+ */
15652
+ async consume(taskId) {
15653
+ const all = await this.list();
15654
+ const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
15655
+ if (!target) return null;
15656
+ await this.delete(target.taskId);
15657
+ return target;
15658
+ }
15659
+ /** Remove every checkpoint for a task id. */
15660
+ async delete(taskId) {
15661
+ try {
15662
+ const entries = await fs25.promises.readdir(this.dir);
15663
+ const wanted = safeId(taskId);
15664
+ await Promise.all(entries.map(async (name) => {
15665
+ const match = FILE_PATTERN.exec(name);
15666
+ if (match?.[2] === wanted) {
15667
+ await fs25.promises.rm(path26__default.default.join(this.dir, name), { force: true });
15668
+ }
15669
+ }));
15670
+ } catch {
15671
+ }
15672
+ }
15673
+ /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
15674
+ async prune() {
15675
+ try {
15676
+ const entries = await fs25.promises.readdir(this.dir);
15677
+ const cutoff = this.now() - this.maxAgeMs;
15678
+ const files = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).sort((a, b) => b.timestamp - a.timestamp);
15679
+ const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
15680
+ const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
15681
+ await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
15682
+ (name) => fs25.promises.rm(path26__default.default.join(this.dir, name), { force: true })
15683
+ ));
15684
+ } catch {
15685
+ }
15686
+ }
15687
+ };
15688
+ function summarizeCompleted(completed, perNodeChars = 500) {
15689
+ if (!completed.length) return "";
15690
+ const lines = completed.map((node) => {
15691
+ const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
15692
+ return `- [${node.status}] ${node.title}
15693
+ ${body.replace(/\n/g, "\n ")}`;
15694
+ });
15695
+ return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
15696
+ ${lines.join("\n")}`;
15697
+ }
15239
15698
  var ignore2 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
15240
15699
  var PrivacyPaths = class {
15241
15700
  localOnly;
@@ -15282,7 +15741,7 @@ var CascadeIgnore = class {
15282
15741
  ]);
15283
15742
  }
15284
15743
  async load(workspacePath) {
15285
- const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
15744
+ const filePath = path26__default.default.join(workspacePath, ".cascadeignore");
15286
15745
  try {
15287
15746
  const content = await fs5__default.default.readFile(filePath, "utf-8");
15288
15747
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -15293,7 +15752,7 @@ var CascadeIgnore = class {
15293
15752
  }
15294
15753
  isIgnored(filePath, workspacePath) {
15295
15754
  try {
15296
- const relative = workspacePath ? path25__default.default.relative(workspacePath, filePath) : filePath;
15755
+ const relative = workspacePath ? path26__default.default.relative(workspacePath, filePath) : filePath;
15297
15756
  return this.ig.ignores(relative);
15298
15757
  } catch {
15299
15758
  return false;
@@ -15636,7 +16095,7 @@ var WorkspaceIndex = class {
15636
16095
  isIgnored;
15637
16096
  chunker;
15638
16097
  constructor(opts) {
15639
- this.root = path25__default.default.resolve(opts.root);
16098
+ this.root = path26__default.default.resolve(opts.root);
15640
16099
  this.db = opts.db;
15641
16100
  this.namespace = opts.namespace ?? "code";
15642
16101
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -15668,14 +16127,14 @@ var WorkspaceIndex = class {
15668
16127
  return;
15669
16128
  }
15670
16129
  for (const entry of entries) {
15671
- const abs = path25__default.default.join(dir, entry.name);
16130
+ const abs = path26__default.default.join(dir, entry.name);
15672
16131
  if (entry.isDirectory()) {
15673
16132
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
15674
16133
  await walk(abs);
15675
16134
  } else if (entry.isFile()) {
15676
- const ext = path25__default.default.extname(entry.name).slice(1).toLowerCase();
16135
+ const ext = path26__default.default.extname(entry.name).slice(1).toLowerCase();
15677
16136
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
15678
- out.push(path25__default.default.relative(this.root, abs));
16137
+ out.push(path26__default.default.relative(this.root, abs));
15679
16138
  }
15680
16139
  }
15681
16140
  };
@@ -15705,7 +16164,7 @@ var WorkspaceIndex = class {
15705
16164
  const entries = [];
15706
16165
  for (const rel of rels) {
15707
16166
  try {
15708
- const abs = path25__default.default.join(this.root, rel);
16167
+ const abs = path26__default.default.join(this.root, rel);
15709
16168
  const stat = await fs5__default.default.stat(abs);
15710
16169
  if (stat.size > this.maxFileBytes) continue;
15711
16170
  const buf = await fs5__default.default.readFile(abs);
@@ -16243,8 +16702,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16243
16702
  /** Orchestration decisions for the CURRENT run — cleared on each run(). */
16244
16703
  decisionLog = [];
16245
16704
  initialized = false;
16246
- /** Last task that stopped at the budget cap — powers /continue (resumeRun). */
16705
+ /**
16706
+ * Last interrupted task — powers /continue (resumeRun). Kept in memory as a
16707
+ * fast path for the same-process case; the durable copy lives in `resumeStore`
16708
+ * so a crash or an exit no longer throws away everything already finished.
16709
+ */
16247
16710
  lastInterruptedRun;
16711
+ /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
16712
+ resumeStore;
16713
+ /** Section results of the CURRENT run, so an interruption can checkpoint them. */
16714
+ currentRunCompleted = [];
16248
16715
  initPromise;
16249
16716
  store;
16250
16717
  audit;
@@ -16277,10 +16744,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16277
16744
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16278
16745
  try {
16279
16746
  this.router.setDeadModelStore(new DeadModelStore(
16280
- fileDeadModelPersistence(path25__default.default.join(workspacePath, ".cascade", "dead-models.json"))
16747
+ fileDeadModelPersistence(path26__default.default.join(workspacePath, ".cascade", "dead-models.json"))
16281
16748
  ));
16282
16749
  } catch {
16283
16750
  }
16751
+ try {
16752
+ this.resumeStore = new ResumeStore({
16753
+ dir: path26__default.default.join(workspacePath, ".cascade", "resume")
16754
+ });
16755
+ } catch {
16756
+ }
16284
16757
  this.multimodal = new MultimodalRegistry(
16285
16758
  (this.config.providers ?? []).map((p) => p.type)
16286
16759
  );
@@ -16365,8 +16838,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16365
16838
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
16366
16839
  return;
16367
16840
  }
16368
- const dbPath = ci.dbPath || path25__default.default.join(this.workspacePath, ".cascade", "code-index.db");
16369
- fs24__default.default.mkdirSync(path25__default.default.dirname(dbPath), { recursive: true });
16841
+ const dbPath = ci.dbPath || path26__default.default.join(this.workspacePath, ".cascade", "code-index.db");
16842
+ fs25__default.default.mkdirSync(path26__default.default.dirname(dbPath), { recursive: true });
16370
16843
  const db = new Database__default.default(dbPath);
16371
16844
  db.pragma("journal_mode = WAL");
16372
16845
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -16746,10 +17219,43 @@ ${input}` }],
16746
17219
  setFeedbackSource(source) {
16747
17220
  this.router.setFeedbackSource(source);
16748
17221
  }
16749
- /** True when a task stopped at the budget cap and can be resumed via /continue. */
17222
+ /**
17223
+ * Write a durable checkpoint for an interrupted run.
17224
+ *
17225
+ * Best-effort throughout: a run is already ending badly on every path that
17226
+ * calls this, and failing to record a resume file must not make that ending
17227
+ * worse. Nothing here is allowed to throw.
17228
+ */
17229
+ async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
17230
+ if (!this.currentRunCompleted.length && !partialOutput) return;
17231
+ try {
17232
+ await this.resumeStore?.save({
17233
+ taskId,
17234
+ prompt,
17235
+ reason,
17236
+ ...detail ? { detail } : {},
17237
+ partialOutput,
17238
+ completed: [...this.currentRunCompleted]
17239
+ });
17240
+ } catch {
17241
+ }
17242
+ }
17243
+ /** True when a task can be resumed via /continue (this process or a previous one). */
16750
17244
  hasResumableRun() {
16751
17245
  return this.lastInterruptedRun != null;
16752
17246
  }
17247
+ /**
17248
+ * True when a checkpoint from ANY process is resumable. Async because the
17249
+ * durable check touches disk; `hasResumableRun` remains the sync in-memory
17250
+ * answer for callers already on a hot path.
17251
+ */
17252
+ async hasDurableResume() {
17253
+ return await this.resumeStore?.latest() != null;
17254
+ }
17255
+ /** Interrupted runs available to resume, newest first. */
17256
+ async listResumableRuns() {
17257
+ return await this.resumeStore?.list() ?? [];
17258
+ }
16753
17259
  /**
16754
17260
  * Raise the per-run token budget for a resume and return the continuation
16755
17261
  * prompt (or null when nothing is resumable). Consumes the interrupted-run
@@ -16760,22 +17266,54 @@ ${input}` }],
16760
17266
  const last = this.lastInterruptedRun;
16761
17267
  if (!last) return null;
16762
17268
  this.lastInterruptedRun = void 0;
16763
- const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
17269
+ this.raiseBudgetForResume(opts.maxTokens);
17270
+ return this.buildResumePrompt(last.prompt, last.partialOutput, []);
17271
+ }
17272
+ /**
17273
+ * Durable counterpart to prepareResume: recovers a run interrupted in ANY
17274
+ * process, including one that crashed before it could return. Consumes the
17275
+ * checkpoint, so the same finished work is never restored into two runs.
17276
+ */
17277
+ async prepareDurableResume(opts = {}) {
17278
+ const checkpoint = await this.resumeStore?.consume(opts.taskId);
17279
+ if (!checkpoint) return null;
17280
+ this.lastInterruptedRun = void 0;
17281
+ this.raiseBudgetForResume(opts.maxTokens);
17282
+ return this.buildResumePrompt(checkpoint.prompt, checkpoint.partialOutput, checkpoint.completed);
17283
+ }
17284
+ /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
17285
+ raiseBudgetForResume(maxTokens) {
17286
+ const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
16764
17287
  this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
16765
17288
  this.router.setMaxTokensPerRun(raised);
16766
- return `Continue and FINISH this task. A previous attempt was interrupted before completion; any files already created are on disk \u2014 build on them, do NOT recreate them. Complete only the remaining work.
16767
-
16768
- Original task: ${last.prompt}` + (last.partialOutput ? `
16769
-
17289
+ }
17290
+ /**
17291
+ * The resume prompt. Completed sections are named explicitly rather than
17292
+ * gestured at: "do not recreate the files" is an instruction a planner can
17293
+ * only guess at, whereas a list of what is already done, and what it produced,
17294
+ * is something it can act on. That difference is the entire reason node
17295
+ * results are kept — a re-plan that cannot see the finished work will plan the
17296
+ * finished work again, and the user pays for it a second time.
17297
+ */
17298
+ buildResumePrompt(prompt, partialOutput, completed) {
17299
+ const done = summarizeCompleted(completed);
17300
+ return [
17301
+ "Continue and FINISH this task. A previous attempt was interrupted before completion; any files already created are on disk \u2014 build on them, do NOT recreate them. Plan ONLY the remaining work.",
17302
+ "",
17303
+ `Original task: ${prompt}`,
17304
+ done ? `
17305
+ ${done}` : "",
17306
+ partialOutput ? `
16770
17307
  Partial result so far:
16771
- ${last.partialOutput}` : "");
17308
+ ${partialOutput}` : ""
17309
+ ].filter(Boolean).join("\n");
16772
17310
  }
16773
17311
  /**
16774
17312
  * Resume the last budget-capped task with a raised budget (SDK/headless).
16775
17313
  * Returns null when there is nothing to resume.
16776
17314
  */
16777
17315
  async resumeRun(opts = {}) {
16778
- const prompt = this.prepareResume(opts);
17316
+ const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
16779
17317
  if (!prompt) return null;
16780
17318
  return this.run({ prompt });
16781
17319
  }
@@ -17146,6 +17684,7 @@ ${prompt}` : prompt;
17146
17684
  const startMs = Date.now();
17147
17685
  const taskId = crypto4.randomUUID();
17148
17686
  this.decisionLog = [];
17687
+ this.currentRunCompleted = [];
17149
17688
  if (options.fastAnswer) {
17150
17689
  return this.runFastAnswer(options, startMs, taskId);
17151
17690
  }
@@ -17260,6 +17799,11 @@ ${prompt}` : prompt;
17260
17799
  });
17261
17800
  tier.on("log", (e) => this.emit("log", e));
17262
17801
  tier.on("tier:status", (e) => this.emit("tier:status", e));
17802
+ tier.on("section:complete", (e) => {
17803
+ if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
17804
+ this.currentRunCompleted.push(e);
17805
+ this.emit("section:complete", e);
17806
+ });
17263
17807
  tier.on("tool:call", (e) => this.emit("tool:call", e));
17264
17808
  tier.on("tool:result", (e) => this.emit("tool:result", e));
17265
17809
  tier.on("tool:approval-request", async (request) => {
@@ -17406,6 +17950,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
17406
17950
  if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
17407
17951
  const trip = runBreaker.reason();
17408
17952
  if (trip) {
17953
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
17409
17954
  finalOutput = [
17410
17955
  `\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
17411
17956
  "",
@@ -17425,6 +17970,13 @@ ${finalOutput}` : ""
17425
17970
  reason: err instanceof Error ? err.message : "Task cancelled",
17426
17971
  partialOutput: finalOutput || ""
17427
17972
  });
17973
+ await this.checkpointRun(
17974
+ taskId,
17975
+ options.prompt,
17976
+ finalOutput || "",
17977
+ "cancelled",
17978
+ err instanceof Error ? err.message : "Task cancelled"
17979
+ );
17428
17980
  runError = null;
17429
17981
  } else if (err instanceof Error && err.name === "BudgetExceededError") {
17430
17982
  this.emit("run:budget-exceeded", {
@@ -17433,9 +17985,17 @@ ${finalOutput}` : ""
17433
17985
  partialOutput: finalOutput || ""
17434
17986
  });
17435
17987
  this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
17988
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
17436
17989
  if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
17437
17990
  runError = null;
17438
17991
  } else {
17992
+ await this.checkpointRun(
17993
+ taskId,
17994
+ options.prompt,
17995
+ finalOutput || "",
17996
+ "error",
17997
+ err instanceof Error ? err.message : String(err)
17998
+ );
17439
17999
  runError = err;
17440
18000
  throw err;
17441
18001
  }
@@ -17683,7 +18243,7 @@ var Keystore = class {
17683
18243
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
17684
18244
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
17685
18245
  this.backend = "keytar";
17686
- if (password && fs24__default.default.existsSync(this.storePath)) {
18246
+ if (password && fs25__default.default.existsSync(this.storePath)) {
17687
18247
  try {
17688
18248
  const fileEntries = this.decryptFile(password);
17689
18249
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -17702,7 +18262,7 @@ var Keystore = class {
17702
18262
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
17703
18263
  );
17704
18264
  }
17705
- if (!fs24__default.default.existsSync(this.storePath)) {
18265
+ if (!fs25__default.default.existsSync(this.storePath)) {
17706
18266
  const salt = crypto4__default.default.randomBytes(SALT_LEN);
17707
18267
  this.masterKey = this.deriveKey(password, salt);
17708
18268
  this.writeWithSalt({}, salt);
@@ -17716,7 +18276,7 @@ var Keystore = class {
17716
18276
  }
17717
18277
  /** Synchronous legacy unlock kept for AES-only environments. */
17718
18278
  unlockSync(password) {
17719
- if (!fs24__default.default.existsSync(this.storePath)) {
18279
+ if (!fs25__default.default.existsSync(this.storePath)) {
17720
18280
  const salt = crypto4__default.default.randomBytes(SALT_LEN);
17721
18281
  this.masterKey = this.deriveKey(password, salt);
17722
18282
  this.writeWithSalt({}, salt);
@@ -17774,7 +18334,7 @@ var Keystore = class {
17774
18334
  }
17775
18335
  }
17776
18336
  decryptFile(password, knownSalt) {
17777
- if (!fs24__default.default.existsSync(this.storePath)) return {};
18337
+ if (!fs25__default.default.existsSync(this.storePath)) return {};
17778
18338
  try {
17779
18339
  const { salt, ciphertext, iv, tag } = this.readRaw();
17780
18340
  const useSalt = knownSalt ?? salt;
@@ -17796,8 +18356,8 @@ var Keystore = class {
17796
18356
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17797
18357
  const tag = cipher.getAuthTag();
17798
18358
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
17799
- fs24__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
17800
- fs24__default.default.writeFileSync(this.storePath, out, { mode: 384 });
18359
+ fs25__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
18360
+ fs25__default.default.writeFileSync(this.storePath, out, { mode: 384 });
17801
18361
  }
17802
18362
  writeWithSalt(data, salt) {
17803
18363
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -17807,11 +18367,11 @@ var Keystore = class {
17807
18367
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17808
18368
  const tag = cipher.getAuthTag();
17809
18369
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
17810
- fs24__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
17811
- fs24__default.default.writeFileSync(this.storePath, out, { mode: 384 });
18370
+ fs25__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
18371
+ fs25__default.default.writeFileSync(this.storePath, out, { mode: 384 });
17812
18372
  }
17813
18373
  readRaw() {
17814
- const buf = fs24__default.default.readFileSync(this.storePath);
18374
+ const buf = fs25__default.default.readFileSync(this.storePath);
17815
18375
  let offset = 0;
17816
18376
  const salt = buf.subarray(offset, offset + SALT_LEN);
17817
18377
  offset += SALT_LEN;
@@ -17827,7 +18387,7 @@ var Keystore = class {
17827
18387
  }
17828
18388
  };
17829
18389
  async function loadCascadeMd(workspacePath) {
17830
- const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
18390
+ const filePath = path26__default.default.join(workspacePath, "CASCADE.md");
17831
18391
  try {
17832
18392
  const raw = await fs5__default.default.readFile(filePath, "utf-8");
17833
18393
  return parseCascadeMd(raw);
@@ -17858,7 +18418,7 @@ ${raw.trim()}`;
17858
18418
  var MemoryStore = class _MemoryStore {
17859
18419
  db;
17860
18420
  constructor(dbPath) {
17861
- fs24__default.default.mkdirSync(path25__default.default.dirname(dbPath), { recursive: true });
18421
+ fs25__default.default.mkdirSync(path26__default.default.dirname(dbPath), { recursive: true });
17862
18422
  try {
17863
18423
  this.db = new Database__default.default(dbPath, { timeout: 5e3 });
17864
18424
  this.db.pragma("journal_mode = WAL");
@@ -18694,7 +19254,7 @@ Original error: ${err.message}`
18694
19254
  }
18695
19255
  };
18696
19256
  function credentialsPath(globalDir2) {
18697
- return path25__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
19257
+ return path26__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
18698
19258
  }
18699
19259
  function providerKey(p) {
18700
19260
  if (p.type === "azure") {
@@ -18707,7 +19267,7 @@ function isPersistable(p) {
18707
19267
  }
18708
19268
  function loadGlobalCredentials(globalDir2) {
18709
19269
  try {
18710
- const raw = fs24__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
19270
+ const raw = fs25__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
18711
19271
  const parsed = JSON.parse(raw);
18712
19272
  if (!Array.isArray(parsed.providers)) return [];
18713
19273
  return parsed.providers.filter(
@@ -18719,13 +19279,13 @@ function loadGlobalCredentials(globalDir2) {
18719
19279
  }
18720
19280
  function saveGlobalCredentials(globalDir2, providers) {
18721
19281
  const filePath = credentialsPath(globalDir2);
18722
- fs24__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
19282
+ fs25__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
18723
19283
  const body = { version: 1, providers: providers.filter(isPersistable) };
18724
19284
  const tmp = `${filePath}.tmp`;
18725
- fs24__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
18726
- fs24__default.default.renameSync(tmp, filePath);
19285
+ fs25__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
19286
+ fs25__default.default.renameSync(tmp, filePath);
18727
19287
  try {
18728
- fs24__default.default.chmodSync(filePath, 384);
19288
+ fs25__default.default.chmodSync(filePath, 384);
18729
19289
  } catch {
18730
19290
  }
18731
19291
  }
@@ -18760,7 +19320,7 @@ var ConfigManager = class {
18760
19320
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
18761
19321
  constructor(workspacePath = process.cwd(), globalDirOverride) {
18762
19322
  this.workspacePath = workspacePath;
18763
- this.globalDir = globalDirOverride ?? path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
19323
+ this.globalDir = globalDirOverride ?? path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
18764
19324
  }
18765
19325
  async load() {
18766
19326
  this.config = await this.loadConfig();
@@ -18774,8 +19334,8 @@ var ConfigManager = class {
18774
19334
  this.ignore = new CascadeIgnore();
18775
19335
  await this.ignore.load(this.workspacePath);
18776
19336
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
18777
- this.keystore = new Keystore(path25__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
18778
- this.store = new MemoryStore(path25__default.default.join(this.workspacePath, CASCADE_DB_FILE));
19337
+ this.keystore = new Keystore(path26__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
19338
+ this.store = new MemoryStore(path26__default.default.join(this.workspacePath, CASCADE_DB_FILE));
18779
19339
  await this.injectEnvKeys();
18780
19340
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
18781
19341
  await this.ensureDefaultIdentity();
@@ -18800,8 +19360,8 @@ var ConfigManager = class {
18800
19360
  return this.workspacePath;
18801
19361
  }
18802
19362
  async save() {
18803
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
18804
- await fs5__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
19363
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
19364
+ await fs5__default.default.mkdir(path26__default.default.dirname(configPath), { recursive: true });
18805
19365
  await fs5__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
18806
19366
  try {
18807
19367
  saveGlobalCredentials(this.globalDir, this.config.providers);
@@ -18837,7 +19397,7 @@ var ConfigManager = class {
18837
19397
  return configProvider?.apiKey;
18838
19398
  }
18839
19399
  async loadConfig() {
18840
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
19400
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
18841
19401
  try {
18842
19402
  const raw = await fs5__default.default.readFile(configPath, "utf-8");
18843
19403
  return validateConfig(JSON.parse(raw));
@@ -19369,7 +19929,7 @@ function aggregateCostStats(sessions, opts = {}) {
19369
19929
  }
19370
19930
 
19371
19931
  // src/dashboard/server.ts
19372
- var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
19932
+ var __dirname$1 = path26__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
19373
19933
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
19374
19934
  var DashboardServer = class {
19375
19935
  app;
@@ -19649,14 +20209,14 @@ var DashboardServer = class {
19649
20209
  */
19650
20210
  persistConfig() {
19651
20211
  try {
19652
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
19653
- fs24__default.default.mkdirSync(path25__default.default.dirname(configPath), { recursive: true });
19654
- fs24__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
20212
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
20213
+ fs25__default.default.mkdirSync(path26__default.default.dirname(configPath), { recursive: true });
20214
+ fs25__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
19655
20215
  } catch (err) {
19656
20216
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
19657
20217
  }
19658
20218
  try {
19659
- saveGlobalCredentials(path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
20219
+ saveGlobalCredentials(path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
19660
20220
  } catch (err) {
19661
20221
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
19662
20222
  }
@@ -19671,15 +20231,15 @@ var DashboardServer = class {
19671
20231
  resolveDashboardSecret() {
19672
20232
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
19673
20233
  if (fromConfig) return fromConfig;
19674
- const secretPath = path25__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
20234
+ const secretPath = path26__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
19675
20235
  try {
19676
- if (fs24__default.default.existsSync(secretPath)) {
19677
- const existing = fs24__default.default.readFileSync(secretPath, "utf-8").trim();
20236
+ if (fs25__default.default.existsSync(secretPath)) {
20237
+ const existing = fs25__default.default.readFileSync(secretPath, "utf-8").trim();
19678
20238
  if (existing.length >= 16) return existing;
19679
20239
  }
19680
20240
  const generated = crypto4.randomUUID();
19681
- fs24__default.default.mkdirSync(path25__default.default.dirname(secretPath), { recursive: true });
19682
- fs24__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
20241
+ fs25__default.default.mkdirSync(path26__default.default.dirname(secretPath), { recursive: true });
20242
+ fs25__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
19683
20243
  if (this.config.dashboard.auth) {
19684
20244
  console.warn(
19685
20245
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -19706,7 +20266,7 @@ var DashboardServer = class {
19706
20266
  // ── Setup ─────────────────────────────────────
19707
20267
  getGlobalStore() {
19708
20268
  if (!this.globalStore) {
19709
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20269
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
19710
20270
  this.globalStore = new MemoryStore(globalDbPath);
19711
20271
  }
19712
20272
  return this.globalStore;
@@ -20086,12 +20646,12 @@ ${prompt}`;
20086
20646
  }
20087
20647
  }
20088
20648
  watchRuntimeChanges() {
20089
- const workspaceDbPath = path25__default.default.join(this.workspacePath, CASCADE_DB_FILE);
20090
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20649
+ const workspaceDbPath = path26__default.default.join(this.workspacePath, CASCADE_DB_FILE);
20650
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20091
20651
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
20092
20652
  for (const watchPath of watchPaths) {
20093
- if (!fs24__default.default.existsSync(watchPath)) continue;
20094
- fs24__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
20653
+ if (!fs25__default.default.existsSync(watchPath)) continue;
20654
+ fs25__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
20095
20655
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
20096
20656
  });
20097
20657
  }
@@ -20213,7 +20773,7 @@ ${prompt}`;
20213
20773
  const sessionId = req.params.id;
20214
20774
  this.store.deleteSession(sessionId);
20215
20775
  this.store.deleteRuntimeSession(sessionId);
20216
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20776
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20217
20777
  const globalStore = new MemoryStore(globalDbPath);
20218
20778
  try {
20219
20779
  globalStore.deleteRuntimeSession(sessionId);
@@ -20306,7 +20866,7 @@ ${prompt}`;
20306
20866
  });
20307
20867
  this.app.delete("/api/sessions", auth2, (req, res) => {
20308
20868
  const body = req.body;
20309
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20869
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20310
20870
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
20311
20871
  const globalStore = new MemoryStore(globalDbPath);
20312
20872
  try {
@@ -20329,7 +20889,7 @@ ${prompt}`;
20329
20889
  });
20330
20890
  this.app.delete("/api/runtime", auth2, (_req, res) => {
20331
20891
  this.store.deleteAllRuntimeNodes();
20332
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20892
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20333
20893
  const globalStore = new MemoryStore(globalDbPath);
20334
20894
  try {
20335
20895
  globalStore.deleteAllRuntimeNodes();
@@ -20415,12 +20975,12 @@ ${prompt}`;
20415
20975
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
20416
20976
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
20417
20977
  try {
20418
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
20419
- const existing = fs24__default.default.existsSync(configPath) ? JSON.parse(fs24__default.default.readFileSync(configPath, "utf-8")) : {};
20978
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
20979
+ const existing = fs25__default.default.existsSync(configPath) ? JSON.parse(fs25__default.default.readFileSync(configPath, "utf-8")) : {};
20420
20980
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
20421
20981
  const tmp = configPath + ".tmp";
20422
- fs24__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
20423
- fs24__default.default.renameSync(tmp, configPath);
20982
+ fs25__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
20983
+ fs25__default.default.renameSync(tmp, configPath);
20424
20984
  res.json({ ok: true });
20425
20985
  } catch (err) {
20426
20986
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -20448,7 +21008,7 @@ ${prompt}`;
20448
21008
  this.app.get("/api/runtime", auth2, (req, res) => {
20449
21009
  const scope = req.query["scope"] ?? "workspace";
20450
21010
  if (scope === "global") {
20451
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21011
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20452
21012
  const globalStore = new MemoryStore(globalDbPath);
20453
21013
  try {
20454
21014
  res.json({
@@ -20789,13 +21349,13 @@ ${prompt}`;
20789
21349
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
20790
21350
  }
20791
21351
  });
20792
- const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
20793
- const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
20794
- const webDistPath = fs24__default.default.existsSync(prodPath) ? prodPath : devPath;
20795
- if (fs24__default.default.existsSync(webDistPath)) {
21352
+ const prodPath = path26__default.default.resolve(__dirname$1, "../web/dist");
21353
+ const devPath = path26__default.default.resolve(__dirname$1, "../../web/dist");
21354
+ const webDistPath = fs25__default.default.existsSync(prodPath) ? prodPath : devPath;
21355
+ if (fs25__default.default.existsSync(webDistPath)) {
20796
21356
  this.app.use(express__default.default.static(webDistPath));
20797
21357
  this.app.get("*", (_req, res) => {
20798
- res.sendFile(path25__default.default.join(webDistPath, "index.html"));
21358
+ res.sendFile(path26__default.default.join(webDistPath, "index.html"));
20799
21359
  });
20800
21360
  } else {
20801
21361
  this.app.get("/", (_req, res) => {
@@ -20982,14 +21542,14 @@ function planRetrieval(ctx) {
20982
21542
  }
20983
21543
  var CLOUD_SESSION_FILE = "cloud-session.json";
20984
21544
  function globalDir(homeDir = os6__default.default.homedir()) {
20985
- return path25__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
21545
+ return path26__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
20986
21546
  }
20987
21547
  function cloudSessionPath(dir = globalDir()) {
20988
- return path25__default.default.join(dir, CLOUD_SESSION_FILE);
21548
+ return path26__default.default.join(dir, CLOUD_SESSION_FILE);
20989
21549
  }
20990
21550
  function loadCloudSession(dir = globalDir()) {
20991
21551
  try {
20992
- const raw = fs24__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
21552
+ const raw = fs25__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
20993
21553
  const parsed = JSON.parse(raw);
20994
21554
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20995
21555
  return parsed;
@@ -20998,17 +21558,17 @@ function loadCloudSession(dir = globalDir()) {
20998
21558
  }
20999
21559
  }
21000
21560
  function saveCloudSession(session, dir = globalDir()) {
21001
- fs24__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
21561
+ fs25__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
21002
21562
  const file = cloudSessionPath(dir);
21003
- fs24__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21563
+ fs25__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21004
21564
  try {
21005
- fs24__default.default.chmodSync(file, 384);
21565
+ fs25__default.default.chmodSync(file, 384);
21006
21566
  } catch {
21007
21567
  }
21008
21568
  }
21009
21569
  function clearCloudSession(dir = globalDir()) {
21010
21570
  try {
21011
- fs24__default.default.rmSync(cloudSessionPath(dir));
21571
+ fs25__default.default.rmSync(cloudSessionPath(dir));
21012
21572
  } catch {
21013
21573
  }
21014
21574
  }
@@ -21180,16 +21740,16 @@ var CloudClient = class _CloudClient {
21180
21740
  return (await this.refresh()).accessToken;
21181
21741
  }
21182
21742
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
21183
- async authed(path27, init = {}) {
21184
- const call = (token) => fetch(this.url(path27), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21743
+ async authed(path28, init = {}) {
21744
+ const call = (token) => fetch(this.url(path28), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21185
21745
  let res = await call(await this.accessToken());
21186
21746
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
21187
21747
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
21188
21748
  const text = await res.text();
21189
21749
  return text ? JSON.parse(text) : {};
21190
21750
  }
21191
- authedGet(path27) {
21192
- return this.authed(path27);
21751
+ authedGet(path28) {
21752
+ return this.authed(path28);
21193
21753
  }
21194
21754
  async fetchMe(token) {
21195
21755
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21560,6 +22120,7 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
21560
22120
  exports.DEFAULT_THEME = DEFAULT_THEME;
21561
22121
  exports.DOCUMENT_MIME = DOCUMENT_MIME;
21562
22122
  exports.DashboardServer = DashboardServer;
22123
+ exports.DependencyScheduler = DependencyScheduler;
21563
22124
  exports.FileMcpOAuthStore = FileMcpOAuthStore;
21564
22125
  exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
21565
22126
  exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
@@ -21585,6 +22146,8 @@ exports.MemoryStore = MemoryStore;
21585
22146
  exports.OLLAMA_BASE_URL = OLLAMA_BASE_URL;
21586
22147
  exports.OpenAICompatibleEmbedder = OpenAICompatibleEmbedder;
21587
22148
  exports.PROVIDER_DISPLAY_NAMES = PROVIDER_DISPLAY_NAMES;
22149
+ exports.RESUME_SCHEMA_VERSION = RESUME_SCHEMA_VERSION;
22150
+ exports.ResumeStore = ResumeStore;
21588
22151
  exports.Retriever = Retriever;
21589
22152
  exports.SqliteVectorStore = SqliteVectorStore;
21590
22153
  exports.T1Administrator = T1Administrator;
@@ -21595,6 +22158,7 @@ exports.T3Worker = T3Worker;
21595
22158
  exports.T3_MODEL_PRIORITY = T3_MODEL_PRIORITY;
21596
22159
  exports.THEME_NAMES = THEME_NAMES;
21597
22160
  exports.TOOL_NAMES = TOOL_NAMES;
22161
+ exports.TaskGraphValidationError = TaskGraphValidationError;
21598
22162
  exports.TaskScheduler = TaskScheduler;
21599
22163
  exports.Telemetry = Telemetry;
21600
22164
  exports.ToolRegistry = ToolRegistry;
@@ -21613,6 +22177,9 @@ exports.chartToTableRows = chartToTableRows;
21613
22177
  exports.chatCompleterFromProviders = chatCompleterFromProviders;
21614
22178
  exports.chunkCode = chunkCode;
21615
22179
  exports.chunkText = chunkText2;
22180
+ exports.compileSectionGraph = compileSectionGraph;
22181
+ exports.compileSubtaskGraph = compileSubtaskGraph;
22182
+ exports.compileTaskGraph = compileTaskGraph;
21616
22183
  exports.completeMcpOAuth = completeMcpOAuth;
21617
22184
  exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
21618
22185
  exports.createCascade = createCascade;
@@ -21624,7 +22191,9 @@ exports.discoverMcpTools = discoverMcpTools;
21624
22191
  exports.distillSessionFacts = distillSessionFacts;
21625
22192
  exports.embedderFromProviders = embedderFromProviders;
21626
22193
  exports.encryptSyncBlob = encryptJSON;
22194
+ exports.evaluateAcceptance = evaluateAcceptance;
21627
22195
  exports.extractCharts = extractCharts;
22196
+ exports.failures = failures;
21628
22197
  exports.fileExt = fileExt;
21629
22198
  exports.fileOAuthProvider = fileOAuthProvider;
21630
22199
  exports.gatherSyncBundle = gatherSyncBundle;
@@ -21658,6 +22227,8 @@ exports.sniffImage = sniffImage;
21658
22227
  exports.splitSlides = splitSlides;
21659
22228
  exports.streamCascade = streamCascade;
21660
22229
  exports.stripInline = stripInline;
22230
+ exports.summarizeCompleted = summarizeCompleted;
22231
+ exports.undecided = undecided;
21661
22232
  exports.uniqueMcpServerName = uniqueMcpServerName;
21662
22233
  //# sourceMappingURL=index.cjs.map
21663
22234
  //# sourceMappingURL=index.cjs.map