cascade-ai 0.67.0 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -9,8 +9,8 @@ var https = require('https');
9
9
  var zlib = require('zlib');
10
10
  var stream = require('stream');
11
11
  var Database2 = require('better-sqlite3');
12
- var path33 = require('path');
13
- var fs30 = require('fs');
12
+ var path34 = require('path');
13
+ var fs31 = require('fs');
14
14
  var os10 = require('os');
15
15
  var crypto = require('crypto');
16
16
  var ink = require('ink');
@@ -77,8 +77,8 @@ var http__default = /*#__PURE__*/_interopDefault(http);
77
77
  var https__default = /*#__PURE__*/_interopDefault(https);
78
78
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
79
79
  var Database2__default = /*#__PURE__*/_interopDefault(Database2);
80
- var path33__default = /*#__PURE__*/_interopDefault(path33);
81
- var fs30__default = /*#__PURE__*/_interopDefault(fs30);
80
+ var path34__default = /*#__PURE__*/_interopDefault(path34);
81
+ var fs31__default = /*#__PURE__*/_interopDefault(fs31);
82
82
  var os10__default = /*#__PURE__*/_interopDefault(os10);
83
83
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
84
84
  var React2__default = /*#__PURE__*/_interopDefault(React2);
@@ -1810,7 +1810,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1810
1810
  var init_constants = __esm({
1811
1811
  "src/constants.ts"() {
1812
1812
  init_pricing();
1813
- CASCADE_VERSION = "0.67.0";
1813
+ CASCADE_VERSION = "0.69.0";
1814
1814
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1815
1815
  CASCADE_DB_FILE = ".cascade/memory.db";
1816
1816
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -3932,12 +3932,12 @@ var init_audit_logger = __esm({
3932
3932
  constructor(workspacePath, debugMode = false) {
3933
3933
  this.workspacePath = workspacePath;
3934
3934
  this.debugMode = debugMode;
3935
- const cascadeDir = path33__default.default.join(workspacePath, ".cascade");
3936
- if (!fs30__default.default.existsSync(cascadeDir)) {
3937
- fs30__default.default.mkdirSync(cascadeDir, { recursive: true });
3935
+ const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
3936
+ if (!fs31__default.default.existsSync(cascadeDir)) {
3937
+ fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
3938
3938
  }
3939
- this.keyPath = path33__default.default.join(cascadeDir, "audit_log.key");
3940
- this.dbPath = path33__default.default.join(cascadeDir, "audit_log.db");
3939
+ this.keyPath = path34__default.default.join(cascadeDir, "audit_log.key");
3940
+ this.dbPath = path34__default.default.join(cascadeDir, "audit_log.db");
3941
3941
  this.initEncryptionKey();
3942
3942
  this.db = new Database2__default.default(this.dbPath);
3943
3943
  this.db.pragma("journal_mode = WAL");
@@ -3967,11 +3967,11 @@ var init_audit_logger = __esm({
3967
3967
  return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
3968
3968
  }
3969
3969
  initEncryptionKey() {
3970
- if (fs30__default.default.existsSync(this.keyPath)) {
3971
- this.encryptionKey = fs30__default.default.readFileSync(this.keyPath);
3970
+ if (fs31__default.default.existsSync(this.keyPath)) {
3971
+ this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
3972
3972
  } else {
3973
3973
  this.encryptionKey = crypto__default.default.randomBytes(32);
3974
- fs30__default.default.writeFileSync(this.keyPath, this.encryptionKey);
3974
+ fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
3975
3975
  }
3976
3976
  }
3977
3977
  encrypt(text) {
@@ -4049,9 +4049,9 @@ var init_audit_logger = __esm({
4049
4049
  dumpDebugIfNeeded() {
4050
4050
  if (!this.debugMode) return;
4051
4051
  try {
4052
- const dumpPath = path33__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
4052
+ const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
4053
4053
  const entries = this.getAllLogs();
4054
- fs30__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4054
+ fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4055
4055
  } catch (err) {
4056
4056
  console.error("Failed to dump debug audit logs", err);
4057
4057
  }
@@ -4106,7 +4106,7 @@ var Keystore = class {
4106
4106
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
4107
4107
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
4108
4108
  this.backend = "keytar";
4109
- if (password && fs30__default.default.existsSync(this.storePath)) {
4109
+ if (password && fs31__default.default.existsSync(this.storePath)) {
4110
4110
  try {
4111
4111
  const fileEntries = this.decryptFile(password);
4112
4112
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -4125,7 +4125,7 @@ var Keystore = class {
4125
4125
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
4126
4126
  );
4127
4127
  }
4128
- if (!fs30__default.default.existsSync(this.storePath)) {
4128
+ if (!fs31__default.default.existsSync(this.storePath)) {
4129
4129
  const salt = crypto__default.default.randomBytes(SALT_LEN);
4130
4130
  this.masterKey = this.deriveKey(password, salt);
4131
4131
  this.writeWithSalt({}, salt);
@@ -4139,7 +4139,7 @@ var Keystore = class {
4139
4139
  }
4140
4140
  /** Synchronous legacy unlock kept for AES-only environments. */
4141
4141
  unlockSync(password) {
4142
- if (!fs30__default.default.existsSync(this.storePath)) {
4142
+ if (!fs31__default.default.existsSync(this.storePath)) {
4143
4143
  const salt = crypto__default.default.randomBytes(SALT_LEN);
4144
4144
  this.masterKey = this.deriveKey(password, salt);
4145
4145
  this.writeWithSalt({}, salt);
@@ -4197,7 +4197,7 @@ var Keystore = class {
4197
4197
  }
4198
4198
  }
4199
4199
  decryptFile(password, knownSalt) {
4200
- if (!fs30__default.default.existsSync(this.storePath)) return {};
4200
+ if (!fs31__default.default.existsSync(this.storePath)) return {};
4201
4201
  try {
4202
4202
  const { salt, ciphertext, iv, tag } = this.readRaw();
4203
4203
  const useSalt = knownSalt ?? salt;
@@ -4219,8 +4219,8 @@ var Keystore = class {
4219
4219
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4220
4220
  const tag = cipher.getAuthTag();
4221
4221
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
4222
- fs30__default.default.mkdirSync(path33__default.default.dirname(this.storePath), { recursive: true });
4223
- fs30__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4222
+ fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4223
+ fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4224
4224
  }
4225
4225
  writeWithSalt(data, salt) {
4226
4226
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -4230,11 +4230,11 @@ var Keystore = class {
4230
4230
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4231
4231
  const tag = cipher.getAuthTag();
4232
4232
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
4233
- fs30__default.default.mkdirSync(path33__default.default.dirname(this.storePath), { recursive: true });
4234
- fs30__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4233
+ fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4234
+ fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4235
4235
  }
4236
4236
  readRaw() {
4237
- const buf = fs30__default.default.readFileSync(this.storePath);
4237
+ const buf = fs31__default.default.readFileSync(this.storePath);
4238
4238
  let offset = 0;
4239
4239
  const salt = buf.subarray(offset, offset + SALT_LEN);
4240
4240
  offset += SALT_LEN;
@@ -4267,7 +4267,7 @@ var CascadeIgnore = class {
4267
4267
  ]);
4268
4268
  }
4269
4269
  async load(workspacePath) {
4270
- const filePath = path33__default.default.join(workspacePath, ".cascadeignore");
4270
+ const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4271
4271
  try {
4272
4272
  const content = await fs11__default.default.readFile(filePath, "utf-8");
4273
4273
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -4278,7 +4278,7 @@ var CascadeIgnore = class {
4278
4278
  }
4279
4279
  isIgnored(filePath, workspacePath) {
4280
4280
  try {
4281
- const relative = workspacePath ? path33__default.default.relative(workspacePath, filePath) : filePath;
4281
+ const relative = workspacePath ? path34__default.default.relative(workspacePath, filePath) : filePath;
4282
4282
  return this.ig.ignores(relative);
4283
4283
  } catch {
4284
4284
  return false;
@@ -4289,7 +4289,7 @@ var CascadeIgnore = class {
4289
4289
  }
4290
4290
  };
4291
4291
  async function createDefaultIgnoreFile(workspacePath) {
4292
- const filePath = path33__default.default.join(workspacePath, ".cascadeignore");
4292
+ const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4293
4293
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
4294
4294
  # Syntax identical to .gitignore
4295
4295
 
@@ -4319,7 +4319,7 @@ Thumbs.db
4319
4319
  await fs11__default.default.writeFile(filePath, content, "utf-8");
4320
4320
  }
4321
4321
  async function loadCascadeMd(workspacePath) {
4322
- const filePath = path33__default.default.join(workspacePath, "CASCADE.md");
4322
+ const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4323
4323
  try {
4324
4324
  const raw = await fs11__default.default.readFile(filePath, "utf-8");
4325
4325
  return parseCascadeMd(raw);
@@ -4348,7 +4348,7 @@ ${raw.trim()}`;
4348
4348
  return { raw, sections, systemPrompt };
4349
4349
  }
4350
4350
  async function createDefaultCascadeMd(workspacePath) {
4351
- const filePath = path33__default.default.join(workspacePath, "CASCADE.md");
4351
+ const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4352
4352
  const content = `# Cascade Project Instructions
4353
4353
 
4354
4354
  This file contains project-specific instructions for the Cascade AI agent.
@@ -4383,7 +4383,7 @@ List any areas the agent should not touch.
4383
4383
  var MemoryStore = class _MemoryStore {
4384
4384
  db;
4385
4385
  constructor(dbPath) {
4386
- fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
4386
+ fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
4387
4387
  try {
4388
4388
  this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
4389
4389
  this.db.pragma("journal_mode = WAL");
@@ -5635,7 +5635,7 @@ function validateConfig(raw) {
5635
5635
  // src/config/global-credentials.ts
5636
5636
  init_constants();
5637
5637
  function credentialsPath(globalDir2) {
5638
- return path33__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5638
+ return path34__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5639
5639
  }
5640
5640
  function providerKey(p) {
5641
5641
  if (p.type === "azure") {
@@ -5648,7 +5648,7 @@ function isPersistable(p) {
5648
5648
  }
5649
5649
  function loadGlobalCredentials(globalDir2) {
5650
5650
  try {
5651
- const raw = fs30__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5651
+ const raw = fs31__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5652
5652
  const parsed = JSON.parse(raw);
5653
5653
  if (!Array.isArray(parsed.providers)) return [];
5654
5654
  return parsed.providers.filter(
@@ -5660,13 +5660,13 @@ function loadGlobalCredentials(globalDir2) {
5660
5660
  }
5661
5661
  function saveGlobalCredentials(globalDir2, providers) {
5662
5662
  const filePath = credentialsPath(globalDir2);
5663
- fs30__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5663
+ fs31__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5664
5664
  const body = { version: 1, providers: providers.filter(isPersistable) };
5665
5665
  const tmp = `${filePath}.tmp`;
5666
- fs30__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5667
- fs30__default.default.renameSync(tmp, filePath);
5666
+ fs31__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5667
+ fs31__default.default.renameSync(tmp, filePath);
5668
5668
  try {
5669
- fs30__default.default.chmodSync(filePath, 384);
5669
+ fs31__default.default.chmodSync(filePath, 384);
5670
5670
  } catch {
5671
5671
  }
5672
5672
  }
@@ -5884,7 +5884,7 @@ var ConfigManager = class {
5884
5884
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
5885
5885
  constructor(workspacePath = process.cwd(), globalDirOverride) {
5886
5886
  this.workspacePath = workspacePath;
5887
- this.globalDir = globalDirOverride ?? path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
5887
+ this.globalDir = globalDirOverride ?? path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
5888
5888
  }
5889
5889
  async load() {
5890
5890
  this.config = await this.loadConfig();
@@ -5898,8 +5898,8 @@ var ConfigManager = class {
5898
5898
  this.ignore = new CascadeIgnore();
5899
5899
  await this.ignore.load(this.workspacePath);
5900
5900
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
5901
- this.keystore = new Keystore(path33__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5902
- this.store = new MemoryStore(path33__default.default.join(this.workspacePath, CASCADE_DB_FILE));
5901
+ this.keystore = new Keystore(path34__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5902
+ this.store = new MemoryStore(path34__default.default.join(this.workspacePath, CASCADE_DB_FILE));
5903
5903
  await this.injectEnvKeys();
5904
5904
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
5905
5905
  await this.ensureDefaultIdentity();
@@ -5924,8 +5924,8 @@ var ConfigManager = class {
5924
5924
  return this.workspacePath;
5925
5925
  }
5926
5926
  async save() {
5927
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5928
- await fs11__default.default.mkdir(path33__default.default.dirname(configPath), { recursive: true });
5927
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5928
+ await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
5929
5929
  await fs11__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
5930
5930
  try {
5931
5931
  saveGlobalCredentials(this.globalDir, this.config.providers);
@@ -5961,7 +5961,7 @@ var ConfigManager = class {
5961
5961
  return configProvider?.apiKey;
5962
5962
  }
5963
5963
  async loadConfig() {
5964
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5964
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5965
5965
  try {
5966
5966
  const raw = await fs11__default.default.readFile(configPath, "utf-8");
5967
5967
  return validateConfig(JSON.parse(raw));
@@ -6963,13 +6963,13 @@ var DeadModelStore = class {
6963
6963
  function fileDeadModelPersistence(filePath) {
6964
6964
  return {
6965
6965
  load() {
6966
- if (!fs30.existsSync(filePath)) return [];
6967
- const raw = JSON.parse(fs30.readFileSync(filePath, "utf-8"));
6966
+ if (!fs31.existsSync(filePath)) return [];
6967
+ const raw = JSON.parse(fs31.readFileSync(filePath, "utf-8"));
6968
6968
  return Array.isArray(raw) ? raw : [];
6969
6969
  },
6970
6970
  save(records) {
6971
- fs30.mkdirSync(path33.dirname(filePath), { recursive: true });
6972
- fs30.writeFileSync(filePath, JSON.stringify(records, null, 2));
6971
+ fs31.mkdirSync(path34.dirname(filePath), { recursive: true });
6972
+ fs31.writeFileSync(filePath, JSON.stringify(records, null, 2));
6973
6973
  }
6974
6974
  };
6975
6975
  }
@@ -7128,7 +7128,7 @@ init_pricing();
7128
7128
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
7129
7129
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7130
7130
  var FETCH_TIMEOUT_MS = 8e3;
7131
- var DEFAULT_CACHE_FILE = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7131
+ var DEFAULT_CACHE_FILE = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7132
7132
  function normalizeModelId2(id) {
7133
7133
  let s = id.toLowerCase();
7134
7134
  const slash = s.lastIndexOf("/");
@@ -7275,7 +7275,7 @@ var LiveDataProvider = class {
7275
7275
  }
7276
7276
  async saveCache() {
7277
7277
  try {
7278
- await fs11__default.default.mkdir(path33__default.default.dirname(this.opts.cacheFile), { recursive: true });
7278
+ await fs11__default.default.mkdir(path34__default.default.dirname(this.opts.cacheFile), { recursive: true });
7279
7279
  const cache = {
7280
7280
  fetchedAt: this.fetchedAt,
7281
7281
  snapshot: this.snapshot ?? void 0,
@@ -8535,6 +8535,21 @@ var BaseTier = class extends EventEmitter__default.default {
8535
8535
  * which node (Cockpit node panel / Why panel).
8536
8536
  */
8537
8537
  servingModel;
8538
+ /**
8539
+ * This tier's identity IN THE TASK GRAPH — the section or subtask id the
8540
+ * planner assigned — plus where it sat in the dependency order.
8541
+ *
8542
+ * `id` and `graphNodeId` are deliberately different id spaces, and both are
8543
+ * needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
8544
+ * construction); `graphNodeId` identifies the WORK ("s1"), which is what
8545
+ * dependency edges point at. Without the distinction a graph view cannot be
8546
+ * drawn at all: `dependsOn` lists section ids, while every running tier
8547
+ * reports a generated tier id, so every edge would name a node that never
8548
+ * appears in the stream.
8549
+ */
8550
+ graphNodeId;
8551
+ graphDependsOn;
8552
+ graphWave;
8538
8553
  constructor(role, id, parentId) {
8539
8554
  super();
8540
8555
  this.role = role;
@@ -8546,6 +8561,33 @@ var BaseTier = class extends EventEmitter__default.default {
8546
8561
  setPresenter(on = true) {
8547
8562
  this.isPresenter = on;
8548
8563
  }
8564
+ /**
8565
+ * Record where this tier sits in the compiled task graph.
8566
+ *
8567
+ * Called by whichever tier scheduled this one: the id and edges are known at
8568
+ * construction, the wave only once the scheduler reaches it, so the fields
8569
+ * are set independently rather than all at once.
8570
+ */
8571
+ setGraphPosition(position) {
8572
+ if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
8573
+ if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
8574
+ if (position.wave !== void 0) this.graphWave = position.wave;
8575
+ }
8576
+ /**
8577
+ * The graph fields, for spreading into a status payload.
8578
+ *
8579
+ * One helper rather than two literals because `tier:status` is emitted from
8580
+ * two different call sites with two different payload shapes; adding a field
8581
+ * to only one of them makes it arrive intermittently, which is worse for a
8582
+ * consumer than never arriving at all.
8583
+ */
8584
+ graphFields() {
8585
+ return {
8586
+ nodeId: this.graphNodeId,
8587
+ dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
8588
+ waveId: this.graphWave
8589
+ };
8590
+ }
8549
8591
  getStatus() {
8550
8592
  return this.status;
8551
8593
  }
@@ -8560,7 +8602,8 @@ var BaseTier = class extends EventEmitter__default.default {
8560
8602
  status,
8561
8603
  timestamp,
8562
8604
  output,
8563
- model: this.servingModel
8605
+ model: this.servingModel,
8606
+ ...this.graphFields()
8564
8607
  };
8565
8608
  this.emit("status", event);
8566
8609
  this.emit("tier:status", event);
@@ -8600,7 +8643,8 @@ var BaseTier = class extends EventEmitter__default.default {
8600
8643
  progressPct: update.progressPct,
8601
8644
  timestamp,
8602
8645
  output: update.output,
8603
- model: this.servingModel
8646
+ model: this.servingModel,
8647
+ ...this.graphFields()
8604
8648
  });
8605
8649
  }
8606
8650
  buildMessage(type, to, payload) {
@@ -9068,6 +9112,79 @@ function describeProviderError(c, modelId) {
9068
9112
  }
9069
9113
  }
9070
9114
 
9115
+ // src/core/verification/acceptance.ts
9116
+ var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
9117
+ var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
9118
+ var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
9119
+ var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
9120
+ var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
9121
+ function targetFile(criterion, ownedFiles) {
9122
+ const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
9123
+ if (mentioned.length > 1) return null;
9124
+ if (mentioned.length === 1) {
9125
+ const candidate = mentioned[0];
9126
+ return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
9127
+ }
9128
+ return ownedFiles.length === 1 ? ownedFiles[0] : null;
9129
+ }
9130
+ async function evaluateAcceptance(criteria, ownedFiles, probe) {
9131
+ const results = [];
9132
+ for (const criterion of criteria) {
9133
+ const text = criterion.trim();
9134
+ if (!text) {
9135
+ results.push({ criterion, verdict: "undecidable" });
9136
+ continue;
9137
+ }
9138
+ if (NEGATION.test(text)) {
9139
+ results.push({ criterion, verdict: "undecidable" });
9140
+ continue;
9141
+ }
9142
+ const needle = text.match(QUOTED_NEEDLE)?.[1];
9143
+ const wantsExists = EXISTS.test(text);
9144
+ const wantsNonEmpty = NON_EMPTY.test(text);
9145
+ if (!needle && !wantsExists && !wantsNonEmpty) {
9146
+ results.push({ criterion, verdict: "undecidable" });
9147
+ continue;
9148
+ }
9149
+ const file = targetFile(text, ownedFiles);
9150
+ if (!file) {
9151
+ results.push({ criterion, verdict: "undecidable" });
9152
+ continue;
9153
+ }
9154
+ const stat = await probe.stat(file);
9155
+ if (!stat) {
9156
+ results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
9157
+ continue;
9158
+ }
9159
+ if (wantsNonEmpty && stat.size === 0) {
9160
+ results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
9161
+ continue;
9162
+ }
9163
+ if (needle) {
9164
+ const content = await probe.read(file);
9165
+ if (content === null) {
9166
+ results.push({ criterion, verdict: "undecidable" });
9167
+ continue;
9168
+ }
9169
+ const found = content.includes(needle);
9170
+ results.push({
9171
+ criterion,
9172
+ verdict: found ? "passed" : "failed",
9173
+ detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
9174
+ });
9175
+ continue;
9176
+ }
9177
+ results.push({ criterion, verdict: "passed", detail: `${file} exists` });
9178
+ }
9179
+ return results;
9180
+ }
9181
+ function undecided(results) {
9182
+ return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
9183
+ }
9184
+ function failures(results) {
9185
+ return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
9186
+ }
9187
+
9071
9188
  // src/core/tiers/t3-worker.ts
9072
9189
  var CriticalToolError = class extends Error {
9073
9190
  constructor(message, toolName) {
@@ -9384,7 +9501,7 @@ Now execute your subtask using this context where relevant.`
9384
9501
  }
9385
9502
  const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
9386
9503
  if (!artifactCheck.ok) {
9387
- correctionAttempts = 1;
9504
+ correctionAttempts++;
9388
9505
  issues.push(...artifactCheck.issues);
9389
9506
  output = await this.correctOutput(output, artifactCheck.issues);
9390
9507
  const retryArtifactCheck = await this.verifyArtifacts(assignment);
@@ -9402,22 +9519,45 @@ Now execute your subtask using this context where relevant.`
9402
9519
  this.tools.map((t) => t.name)
9403
9520
  );
9404
9521
  if (visualIssue) {
9405
- correctionAttempts = 1;
9522
+ correctionAttempts++;
9406
9523
  issues.push(visualIssue);
9407
9524
  this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
9408
9525
  output = await this.correctOutput(output, [visualIssue]);
9409
9526
  }
9527
+ const acceptanceResults = await this.checkAcceptance(assignment);
9528
+ const acceptanceFailures = failures(acceptanceResults);
9529
+ if (acceptanceFailures.length > 0) {
9530
+ correctionAttempts++;
9531
+ issues.push(...acceptanceFailures);
9532
+ this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
9533
+ output = await this.correctOutput(output, acceptanceFailures);
9534
+ const recheck = failures(await this.checkAcceptance(assignment));
9535
+ if (recheck.length > 0) {
9536
+ issues.push(...recheck);
9537
+ checksRun.push(...acceptanceResults.map((r) => r.criterion));
9538
+ failed.push(...recheck);
9539
+ this.setStatus("FAILED");
9540
+ this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
9541
+ return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
9542
+ }
9543
+ }
9544
+ for (const result2 of acceptanceResults) {
9545
+ if (result2.verdict === "undecidable") continue;
9546
+ checksRun.push(result2.criterion);
9547
+ if (result2.verdict === "passed") passed.push(result2.criterion);
9548
+ }
9410
9549
  this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
9411
- const testResult = await this.selfTest(assignment, output);
9550
+ const pendingAcceptance = undecided(acceptanceResults);
9551
+ const testResult = await this.selfTest(assignment, output, pendingAcceptance);
9412
9552
  checksRun.push(...testResult.checksRun);
9413
9553
  passed.push(...testResult.passed);
9414
9554
  failed.push(...testResult.failed);
9415
9555
  if (testResult.failed.length > 0) {
9416
- correctionAttempts = 1;
9556
+ correctionAttempts++;
9417
9557
  issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
9418
9558
  const corrected = await this.correctOutput(output, testResult.failed);
9419
9559
  output = corrected;
9420
- const retest = await this.selfTest(assignment, output);
9560
+ const retest = await this.selfTest(assignment, output, pendingAcceptance);
9421
9561
  passed.push(...retest.passed);
9422
9562
  if (retest.failed.length > 0) {
9423
9563
  failed.push(...retest.failed);
@@ -9721,8 +9861,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
9721
9861
  // Media generation can run for a minute; without this a cancelled run
9722
9862
  // still pays for an image nobody will see.
9723
9863
  ...this.signal ? { signal: this.signal } : {},
9724
- saveSnapshot: async (path38, content) => {
9725
- this.store?.addFileSnapshot(this.taskId, path38, content);
9864
+ saveSnapshot: async (path39, content) => {
9865
+ this.store?.addFileSnapshot(this.taskId, path39, content);
9726
9866
  },
9727
9867
  sendPeerSync: (to, syncType, content) => {
9728
9868
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -9909,7 +10049,7 @@ ${assignment.expectedOutput}`;
9909
10049
  const { promisify: promisify4 } = await import('util');
9910
10050
  const execAsync3 = promisify4(exec3);
9911
10051
  for (const artifactPath of artifactPaths) {
9912
- const absolutePath = path33__default.default.resolve(process.cwd(), artifactPath);
10052
+ const absolutePath = path34__default.default.resolve(process.cwd(), artifactPath);
9913
10053
  try {
9914
10054
  const stat = await fs11__default.default.stat(absolutePath);
9915
10055
  if (!stat.isFile()) {
@@ -9944,7 +10084,7 @@ ${assignment.expectedOutput}`;
9944
10084
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
9945
10085
  continue;
9946
10086
  }
9947
- const ext = path33__default.default.extname(absolutePath).toLowerCase();
10087
+ const ext = path34__default.default.extname(absolutePath).toLowerCase();
9948
10088
  try {
9949
10089
  if (ext === ".ts" || ext === ".tsx") {
9950
10090
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -10030,14 +10170,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10030
10170
  }
10031
10171
  return current;
10032
10172
  }
10033
- async selfTest(assignment, output) {
10173
+ /**
10174
+ * Run the acceptance criteria that can be settled by looking at the workspace.
10175
+ * Reads go through the same workspace-relative resolution verifyArtifacts uses.
10176
+ */
10177
+ async checkAcceptance(assignment) {
10178
+ const criteria = assignment.acceptance ?? [];
10179
+ if (!criteria.length) return [];
10180
+ const resolve2 = (target) => path34__default.default.resolve(process.cwd(), target);
10181
+ return evaluateAcceptance(criteria, assignment.files ?? [], {
10182
+ stat: async (target) => {
10183
+ try {
10184
+ const stat = await fs11__default.default.stat(resolve2(target));
10185
+ return stat.isFile() ? { size: stat.size } : null;
10186
+ } catch {
10187
+ return null;
10188
+ }
10189
+ },
10190
+ read: async (target) => {
10191
+ try {
10192
+ const content = await fs11__default.default.readFile(resolve2(target), "utf-8");
10193
+ return content.includes("\uFFFD") ? null : content;
10194
+ } catch {
10195
+ return null;
10196
+ }
10197
+ }
10198
+ });
10199
+ }
10200
+ async selfTest(assignment, output, pendingAcceptance) {
10201
+ const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
10034
10202
  const prompt = `Self-test this output against the assignment requirements.
10035
10203
 
10036
10204
  Assignment: ${assignment.description}
10037
10205
  Expected output: ${assignment.expectedOutput}
10038
10206
  Constraints: ${assignment.constraints.join("; ")}
10039
- ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
10040
- ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
10207
+ ${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
10208
+ ${acceptance.map((a) => `- ${a}`).join("\n")}
10041
10209
  ` : ""}
10042
10210
  Output to test:
10043
10211
  ${output}
@@ -10104,8 +10272,8 @@ ${output.slice(0, 4e3)}`;
10104
10272
  } catch {
10105
10273
  }
10106
10274
  }
10107
- async correctOutput(originalOutput, failures) {
10108
- const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
10275
+ async correctOutput(originalOutput, failures2) {
10276
+ const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
10109
10277
 
10110
10278
  Original output:
10111
10279
  ${originalOutput}
@@ -10767,12 +10935,220 @@ function describeGenerationForPlanner(has) {
10767
10935
  return lines.join("\n");
10768
10936
  }
10769
10937
 
10938
+ // src/core/orchestration/task-graph.ts
10939
+ var TaskGraphValidationError = class extends Error {
10940
+ constructor(issues) {
10941
+ super(issues.map((issue) => issue.message).join("; "));
10942
+ this.issues = issues;
10943
+ this.name = "TaskGraphValidationError";
10944
+ }
10945
+ issues;
10946
+ };
10947
+ function normalizeDependencies(input) {
10948
+ if (!input?.length) return [];
10949
+ return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
10950
+ }
10951
+ function cycleNodes(nodes) {
10952
+ const adjacency = new Map(
10953
+ nodes.map((node) => [node.id, node.dependsOn])
10954
+ );
10955
+ const index = /* @__PURE__ */ new Map();
10956
+ const lowlink = /* @__PURE__ */ new Map();
10957
+ const onStack = /* @__PURE__ */ new Set();
10958
+ const stack = [];
10959
+ const inCycle = /* @__PURE__ */ new Set();
10960
+ let counter = 0;
10961
+ const open = (id) => {
10962
+ index.set(id, counter);
10963
+ lowlink.set(id, counter);
10964
+ counter++;
10965
+ stack.push(id);
10966
+ onStack.add(id);
10967
+ };
10968
+ for (const root of nodes) {
10969
+ if (index.has(root.id)) continue;
10970
+ open(root.id);
10971
+ const frames = [{ id: root.id, edge: 0 }];
10972
+ while (frames.length > 0) {
10973
+ const frame = frames[frames.length - 1];
10974
+ const neighbours = adjacency.get(frame.id) ?? [];
10975
+ if (frame.edge < neighbours.length) {
10976
+ const next = neighbours[frame.edge];
10977
+ frame.edge++;
10978
+ if (!adjacency.has(next)) continue;
10979
+ if (!index.has(next)) {
10980
+ open(next);
10981
+ frames.push({ id: next, edge: 0 });
10982
+ } else if (onStack.has(next)) {
10983
+ lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
10984
+ }
10985
+ continue;
10986
+ }
10987
+ frames.pop();
10988
+ const parent = frames[frames.length - 1];
10989
+ if (parent) {
10990
+ lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
10991
+ }
10992
+ if (lowlink.get(frame.id) === index.get(frame.id)) {
10993
+ const component = [];
10994
+ for (; ; ) {
10995
+ const member = stack.pop();
10996
+ onStack.delete(member);
10997
+ component.push(member);
10998
+ if (member === frame.id) break;
10999
+ }
11000
+ const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
11001
+ if (cyclic) for (const member of component) inCycle.add(member);
11002
+ }
11003
+ }
11004
+ }
11005
+ return inCycle;
11006
+ }
11007
+ function throwIfStrict(mode, issues) {
11008
+ if (mode === "strict" && issues.length > 0) {
11009
+ throw new TaskGraphValidationError(issues);
11010
+ }
11011
+ }
11012
+ function compileTaskGraph(inputNodes, options = {}) {
11013
+ const mode = options.mode ?? "strict";
11014
+ const issues = [];
11015
+ if (inputNodes.length === 0) {
11016
+ issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
11017
+ throwIfStrict(mode, issues);
11018
+ }
11019
+ const seen = /* @__PURE__ */ new Set();
11020
+ const nodes = [];
11021
+ for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
11022
+ const input = inputNodes[ordinal];
11023
+ const id = input.id?.trim();
11024
+ const title = input.title?.trim();
11025
+ if (!id || !title) {
11026
+ issues.push({
11027
+ code: "INVALID_NODE",
11028
+ nodeId: id || void 0,
11029
+ message: `Task graph node at index ${ordinal} requires a non-empty id and title`
11030
+ });
11031
+ continue;
11032
+ }
11033
+ if (seen.has(id)) {
11034
+ issues.push({
11035
+ code: "DUPLICATE_NODE_ID",
11036
+ nodeId: id,
11037
+ message: `Duplicate task graph node id: ${id}`
11038
+ });
11039
+ continue;
11040
+ }
11041
+ seen.add(id);
11042
+ nodes.push({
11043
+ id,
11044
+ title,
11045
+ kind: input.kind,
11046
+ dependsOn: normalizeDependencies(input.dependsOn),
11047
+ payload: input.payload,
11048
+ ordinal
11049
+ });
11050
+ }
11051
+ throwIfStrict(mode, issues);
11052
+ const ids = new Set(nodes.map((node) => node.id));
11053
+ const sanitized = nodes.map((node) => {
11054
+ const dependsOn = [];
11055
+ for (const dependencyId of node.dependsOn) {
11056
+ if (dependencyId === node.id) {
11057
+ issues.push({
11058
+ code: "SELF_DEPENDENCY",
11059
+ nodeId: node.id,
11060
+ dependencyId,
11061
+ message: `Task graph node ${node.id} cannot depend on itself`
11062
+ });
11063
+ continue;
11064
+ }
11065
+ if (!ids.has(dependencyId)) {
11066
+ issues.push({
11067
+ code: "UNKNOWN_DEPENDENCY",
11068
+ nodeId: node.id,
11069
+ dependencyId,
11070
+ message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
11071
+ });
11072
+ continue;
11073
+ }
11074
+ dependsOn.push(dependencyId);
11075
+ }
11076
+ return { ...node, dependsOn };
11077
+ });
11078
+ throwIfStrict(mode, issues);
11079
+ const cyclic = cycleNodes(sanitized);
11080
+ let repaired = sanitized;
11081
+ if (cyclic.size > 0) {
11082
+ const cycleIssues = [];
11083
+ repaired = sanitized.map((node) => {
11084
+ if (!cyclic.has(node.id)) return node;
11085
+ const dependsOn = node.dependsOn.filter((dependencyId) => {
11086
+ if (!cyclic.has(dependencyId)) return true;
11087
+ cycleIssues.push({
11088
+ code: "CYCLE_EDGE_REMOVED",
11089
+ nodeId: node.id,
11090
+ dependencyId,
11091
+ message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
11092
+ });
11093
+ return false;
11094
+ });
11095
+ return { ...node, dependsOn };
11096
+ });
11097
+ issues.push(...cycleIssues);
11098
+ throwIfStrict(mode, cycleIssues);
11099
+ }
11100
+ const remainingCycle = cycleNodes(repaired);
11101
+ if (remainingCycle.size > 0) {
11102
+ throw new TaskGraphValidationError([
11103
+ {
11104
+ code: "INVALID_NODE",
11105
+ message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
11106
+ }
11107
+ ]);
11108
+ }
11109
+ return {
11110
+ graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
11111
+ issues
11112
+ };
11113
+ }
11114
+
11115
+ // src/core/orchestration/adapters.ts
11116
+ function compileSectionGraph(sections) {
11117
+ return compileTaskGraph(
11118
+ sections.map((section) => ({
11119
+ id: section.sectionId,
11120
+ title: section.sectionTitle,
11121
+ kind: "section",
11122
+ dependsOn: section.dependsOn,
11123
+ payload: section
11124
+ })),
11125
+ { mode: "repair" }
11126
+ );
11127
+ }
11128
+ function compileSubtaskGraph(subtasks) {
11129
+ return compileTaskGraph(
11130
+ subtasks.map((subtask) => ({
11131
+ id: subtask.subtaskId,
11132
+ title: subtask.subtaskTitle,
11133
+ kind: "subtask",
11134
+ dependsOn: subtask.dependsOn,
11135
+ payload: subtask
11136
+ })),
11137
+ { mode: "repair" }
11138
+ );
11139
+ }
11140
+
10770
11141
  // src/core/tiers/t2-manager.ts
10771
11142
  function buildT2SystemPrompt(has) {
10772
11143
  const generation = describeGenerationForPlanner(has);
10773
11144
  return [
10774
11145
  "You are a T2 Manager agent in the Cascade AI system.",
10775
- "Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
11146
+ // Must agree with the decomposition prompt in decomposeSection(), which is
11147
+ // the one carrying the actual instruction. It said 1-4 and "the FEWEST that
11148
+ // fully cover it"; this said 2-5. The model received both, so its floor was
11149
+ // simultaneously one and two, and the cheaper reading — pad to two — costs a
11150
+ // real worker call on every small section.
11151
+ "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.",
10776
11152
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
10777
11153
  has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
10778
11154
  "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
@@ -11106,6 +11482,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11106
11482
  for (const a of assignments) {
11107
11483
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
11108
11484
  if (this.store) worker.setStore(this.store, taskId);
11485
+ worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
11109
11486
  worker.setPeerBus(this.t3PeerBus);
11110
11487
  if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
11111
11488
  if (this.toolCreator) worker.setToolCreator(this.toolCreator);
@@ -11168,21 +11545,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11168
11545
  * then reduce in-degrees of their dependents and repeat.
11169
11546
  */
11170
11547
  async runWithDependencies(assignments, workerMap, taskId) {
11548
+ const compiled = compileSubtaskGraph(assignments);
11549
+ for (const issue of compiled.issues) {
11550
+ this.log(`\u26A0 Subtask graph: ${issue.message}`);
11551
+ }
11552
+ const sanitizedAssignments = compiled.graph.nodes.map((node) => {
11553
+ const assignment = node.payload;
11554
+ const original = assignment.dependsOn ?? [];
11555
+ const repaired = node.dependsOn;
11556
+ const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
11557
+ return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
11558
+ });
11171
11559
  const adj = /* @__PURE__ */ new Map();
11172
11560
  const inDegree = /* @__PURE__ */ new Map();
11173
11561
  const resultMap = /* @__PURE__ */ new Map();
11174
- for (const a of assignments) {
11562
+ for (const a of sanitizedAssignments) {
11175
11563
  if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
11176
11564
  inDegree.set(a.subtaskId, 0);
11177
11565
  }
11178
- for (const a of assignments) {
11179
- const deps = a.dependsOn ?? [];
11180
- for (const dep of deps) {
11566
+ for (const a of sanitizedAssignments) {
11567
+ for (const dep of a.dependsOn ?? []) {
11181
11568
  adj.get(dep).add(a.subtaskId);
11182
11569
  inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
11183
11570
  }
11184
11571
  }
11185
- const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
11186
11572
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
11187
11573
  let wave = 0;
11188
11574
  let respawnBudget = 1;
@@ -11217,6 +11603,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11217
11603
  runnableIds.push(fallbackId);
11218
11604
  }
11219
11605
  wave++;
11606
+ for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
11220
11607
  this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
11221
11608
  this.sendStatusUpdate({
11222
11609
  progressPct: 20 + Math.min(wave * 10, 60),
@@ -11389,49 +11776,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11389
11776
  }
11390
11777
  return [...resultMap.values()];
11391
11778
  }
11392
- /**
11393
- * Detects cyclic dependencies using Kahn's algorithm and breaks them
11394
- * by removing back-edges. Returns a sanitized copy of assignments.
11395
- *
11396
- * A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
11397
- * meaning t3 will start without waiting for t1, preventing deadlock.
11398
- */
11399
- breakCycles(assignments, adj, inDegree) {
11400
- const degree = new Map(inDegree);
11401
- const queue = [];
11402
- const visited = /* @__PURE__ */ new Set();
11403
- for (const [id, d] of degree) {
11404
- if (d === 0) queue.push(id);
11405
- }
11406
- while (queue.length > 0) {
11407
- const id = queue.shift();
11408
- visited.add(id);
11409
- for (const dep of adj.get(id) ?? []) {
11410
- const newDeg = (degree.get(dep) ?? 1) - 1;
11411
- degree.set(dep, newDeg);
11412
- if (newDeg === 0) queue.push(dep);
11413
- }
11414
- }
11415
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
11416
- if (cycleNodes.length === 0) return assignments;
11417
- this.log(
11418
- `\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
11419
- );
11420
- return assignments.map((a) => {
11421
- if (!cycleNodes.includes(a.subtaskId)) return a;
11422
- const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
11423
- if (safeDeps.length !== (a.dependsOn ?? []).length) {
11424
- this.log(
11425
- ` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
11426
- );
11427
- for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
11428
- inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
11429
- adj.get(removed)?.delete(a.subtaskId);
11430
- }
11431
- }
11432
- return { ...a, dependsOn: safeDeps };
11433
- });
11434
- }
11435
11779
  async retryT3(assignment, taskId) {
11436
11780
  this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
11437
11781
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
@@ -11570,6 +11914,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11570
11914
  // src/core/tiers/t1-administrator.ts
11571
11915
  init_constants();
11572
11916
 
11917
+ // src/core/orchestration/scheduler.ts
11918
+ var DependencyScheduler = class {
11919
+ constructor(graph, hooks, options = {}) {
11920
+ this.graph = graph;
11921
+ this.hooks = hooks;
11922
+ this.options = options;
11923
+ }
11924
+ graph;
11925
+ hooks;
11926
+ options;
11927
+ async run() {
11928
+ const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
11929
+ const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
11930
+ const dependents = /* @__PURE__ */ new Map();
11931
+ for (const node of nodes) dependents.set(node.id, []);
11932
+ for (const node of nodes) {
11933
+ for (const dependencyId of node.dependsOn) {
11934
+ dependents.get(dependencyId)?.push(node.id);
11935
+ }
11936
+ }
11937
+ const policy = this.options.onUpstreamFailure ?? "continue";
11938
+ const settled = /* @__PURE__ */ new Set();
11939
+ const results = /* @__PURE__ */ new Map();
11940
+ const blocked = /* @__PURE__ */ new Map();
11941
+ const waves = [];
11942
+ let wave = 0;
11943
+ const blockDescendants = async (failedIds) => {
11944
+ const rootsFor = /* @__PURE__ */ new Map();
11945
+ const queue = [];
11946
+ for (const rootId of failedIds) {
11947
+ for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
11948
+ }
11949
+ while (queue.length > 0) {
11950
+ const { id, root } = queue.shift();
11951
+ if (settled.has(id)) continue;
11952
+ const known = rootsFor.get(id);
11953
+ if (known) {
11954
+ if (!known.has(root)) known.add(root);
11955
+ continue;
11956
+ }
11957
+ rootsFor.set(id, /* @__PURE__ */ new Set([root]));
11958
+ for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
11959
+ }
11960
+ for (const node of nodes) {
11961
+ const roots = rootsFor.get(node.id);
11962
+ if (!roots || settled.has(node.id)) continue;
11963
+ const causes = nodes.filter((n) => roots.has(n.id));
11964
+ const blockedBy = causes.map((n) => n.id);
11965
+ const blockedByTitles = causes.map((n) => n.title || n.id);
11966
+ const record = {
11967
+ blockedBy,
11968
+ blockedByTitles,
11969
+ reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
11970
+ };
11971
+ blocked.set(node.id, record);
11972
+ settled.add(node.id);
11973
+ await this.hooks.onNodeBlocked?.(node, record);
11974
+ }
11975
+ };
11976
+ while (settled.size < nodes.length) {
11977
+ const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
11978
+ if (ready.length === 0) {
11979
+ throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
11980
+ }
11981
+ waves.push(ready.map((node) => node.id));
11982
+ await this.hooks.onWaveStart?.(ready, wave);
11983
+ const outcomes = /* @__PURE__ */ new Map();
11984
+ const runOne = async (node) => {
11985
+ const result = await this.hooks.execute(node);
11986
+ results.set(node.id, result);
11987
+ settled.add(node.id);
11988
+ const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
11989
+ outcomes.set(node.id, outcome);
11990
+ if (outcome === "failed" && policy === "block") ; else {
11991
+ for (const dependentId of dependents.get(node.id) ?? []) {
11992
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
11993
+ }
11994
+ }
11995
+ await this.hooks.onNodeComplete?.(node, result);
11996
+ };
11997
+ if ((this.options.mode ?? "parallel") === "sequential") {
11998
+ for (const node of ready) await runOne(node);
11999
+ } else {
12000
+ await Promise.all(ready.map(runOne));
12001
+ }
12002
+ const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
12003
+ if (policy === "block" && failedThisWave.length > 0) {
12004
+ await blockDescendants(failedThisWave);
12005
+ }
12006
+ wave++;
12007
+ }
12008
+ return { results, waves, blocked };
12009
+ }
12010
+ };
12011
+
11573
12012
  // src/utils/json-extract.ts
11574
12013
  function extractFirstJsonObject(input) {
11575
12014
  if (!input) return null;
@@ -11641,7 +12080,7 @@ function buildT1SystemPrompt(has) {
11641
12080
  Your responsibilities:
11642
12081
  1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
11643
12082
  2. Decompose the task into logical sections (one per T2 Manager)
11644
- 3. For each section, define 2-5 subtasks for T3 Workers
12083
+ 3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
11645
12084
  4. Return a structured plan as JSON
11646
12085
 
11647
12086
  CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
@@ -11667,6 +12106,9 @@ QUALITY RULES:
11667
12106
 
11668
12107
  ${generation}` : ""}`;
11669
12108
  }
12109
+ function producedOutput(result) {
12110
+ return result.status === "COMPLETED" || result.status === "PARTIAL";
12111
+ }
11670
12112
  var T1Administrator = class extends BaseTier {
11671
12113
  router;
11672
12114
  toolRegistry;
@@ -11801,7 +12243,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
11801
12243
  let allT2Results = await this.dispatchT2Managers(plan.sections);
11802
12244
  let pass = 1;
11803
12245
  const maxReplanPasses = this.config.maxReplanPasses ?? 2;
11804
- const okCount = (rs) => rs.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL").length;
12246
+ const okCount = (rs) => rs.filter(producedOutput).length;
11805
12247
  while (pass <= maxReplanPasses) {
11806
12248
  const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
11807
12249
  if (reviewResult.approved) {
@@ -11932,7 +12374,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
11932
12374
  /** Structured, grounded summary of what's already done — used to keep
11933
12375
  * corrective replan passes from re-emitting completed sections. */
11934
12376
  summarizeCompletedSections(results) {
11935
- const done = results.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL");
12377
+ const done = results.filter(producedOutput);
11936
12378
  if (done.length === 0) return "";
11937
12379
  const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
11938
12380
  return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
@@ -12056,7 +12498,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12056
12498
  }
12057
12499
  }
12058
12500
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
12059
- if (plan.sections.length < min) ;
12501
+ if (plan.sections.length < min || plan.sections.length > max) {
12502
+ this.log(
12503
+ `\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.`
12504
+ );
12505
+ }
12060
12506
  }
12061
12507
  async dispatchT2Managers(sections) {
12062
12508
  for (const section of sections) {
@@ -12073,6 +12519,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12073
12519
  if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
12074
12520
  if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
12075
12521
  manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
12522
+ manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
12076
12523
  if (this.store) {
12077
12524
  manager.setStore(this.store);
12078
12525
  }
@@ -12168,118 +12615,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12168
12615
  return t2Results;
12169
12616
  }
12170
12617
  /**
12171
- * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
12618
+ * Runs T2 managers respecting dependsOn declarations.
12619
+ *
12620
+ * Graph construction, validation and cycle repair now live in
12621
+ * compileSectionGraph (orchestration/adapters.ts), and wave execution in
12622
+ * DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
12623
+ * ordered by one implementation rather than two that drifted.
12624
+ *
12625
+ * The hand-rolled Kahn pass this replaces treated every node it could not
12626
+ * reach as part of a cycle. That set also contains everything DOWNSTREAM of a
12627
+ * cycle, whose in-degree can never fall to zero while its dependency is stuck,
12628
+ * so breaking a cycle here also cut the dependencies of innocent later
12629
+ * sections and started them immediately — before the sections they consume.
12630
+ * compileTaskGraph uses strongly connected components, so only real cycle
12631
+ * members are touched.
12172
12632
  */
12173
12633
  async runT2sWithDependencies(sections, managers, taskId) {
12174
- const adj = /* @__PURE__ */ new Map();
12175
- const inDegree = /* @__PURE__ */ new Map();
12176
- const resultMap = /* @__PURE__ */ new Map();
12177
- const allKeys = new Set(sections.map((s) => s.sectionId));
12178
- for (const s of sections) {
12179
- if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
12180
- inDegree.set(s.sectionId, 0);
12181
- s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
12182
- }
12183
- for (const s of sections) {
12184
- for (const dep of s.dependsOn ?? []) {
12185
- adj.get(dep).add(s.sectionId);
12186
- inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
12187
- }
12188
- }
12189
- const queue = [];
12190
- const degree = new Map(inDegree);
12191
- for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
12192
- const visited = /* @__PURE__ */ new Set();
12193
- while (queue.length > 0) {
12194
- const u = queue.shift();
12195
- visited.add(u);
12196
- for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
12197
- const newDeg = (degree.get(v) ?? 1) - 1;
12198
- degree.set(v, newDeg);
12199
- if (newDeg === 0) queue.push(v);
12200
- }
12201
- }
12202
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
12203
- if (cycleNodes.length > 0) {
12204
- this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
12205
- for (const s of sections) {
12206
- if (cycleNodes.includes(s.sectionId)) {
12207
- const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
12208
- for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
12209
- inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
12210
- adj.get(removed)?.delete(s.sectionId);
12211
- }
12212
- s.dependsOn = safeDeps;
12213
- }
12214
- }
12634
+ const compiled = compileSectionGraph(sections);
12635
+ for (const issue of compiled.issues) {
12636
+ this.log(`\u26A0 Section graph: ${issue.message}`);
12215
12637
  }
12216
- const totalSections = sections.length;
12638
+ const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
12639
+ for (const section of sections) {
12640
+ const node = byId.get(section.sectionId);
12641
+ if (node) section.dependsOn = [...node.dependsOn];
12642
+ }
12643
+ const resultMap = /* @__PURE__ */ new Map();
12644
+ const totalSections = compiled.graph.nodes.length;
12217
12645
  let completedSections = 0;
12218
- const executeWave = async () => {
12219
- const readyIds = [];
12220
- for (const [id, deg] of inDegree.entries()) {
12221
- if (deg === 0 && !resultMap.has(id)) {
12222
- readyIds.push(id);
12223
- }
12224
- }
12225
- if (readyIds.length === 0) return;
12226
- const runOne = async (id) => {
12227
- resultMap.set(id, null);
12228
- const index = sections.findIndex((s) => s.sectionId === id);
12229
- const section = sections[index];
12230
- const manager = managers[index];
12231
- const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
12232
- this.sendStatusUpdate({
12233
- progressPct,
12234
- currentAction: `T2 working on: ${section.sectionTitle}`,
12235
- status: "IN_PROGRESS"
12236
- });
12237
- this.throwIfCancelled();
12238
- let result;
12239
- try {
12240
- result = await manager.execute(section, taskId, this.signal);
12241
- manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
12242
- if (result.status === "ESCALATED") {
12243
- this.escalations.push({
12244
- raisedBy: `T2_${section.sectionId}`,
12646
+ const scheduler = new DependencyScheduler(
12647
+ compiled.graph,
12648
+ {
12649
+ // The wave is the scheduler's own counter, and it is the only place the
12650
+ // parallelism is visible: nodes sharing a wave ran at the same time.
12651
+ // Stamped before the wave executes so the first status event a node
12652
+ // emits already carries it.
12653
+ onWaveStart: (nodes, wave) => {
12654
+ for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
12655
+ },
12656
+ execute: async (node) => {
12657
+ const section = node.payload;
12658
+ const manager = managers[node.ordinal];
12659
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
12660
+ this.sendStatusUpdate({
12661
+ progressPct,
12662
+ currentAction: `T2 working on: ${section.sectionTitle}`,
12663
+ status: "IN_PROGRESS"
12664
+ });
12665
+ this.throwIfCancelled();
12666
+ try {
12667
+ const result = await manager.execute(section, taskId, this.signal);
12668
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
12669
+ if (result.status === "ESCALATED") {
12670
+ this.escalations.push({
12671
+ raisedBy: `T2_${section.sectionId}`,
12672
+ sectionId: section.sectionId,
12673
+ attempted: result.issues,
12674
+ blocker: result.issues.join("; "),
12675
+ needs: "Human review required"
12676
+ });
12677
+ }
12678
+ return result;
12679
+ } catch (err) {
12680
+ return {
12245
12681
  sectionId: section.sectionId,
12246
- attempted: result.issues,
12247
- blocker: result.issues.join("; "),
12248
- needs: "Human review required"
12249
- });
12682
+ sectionTitle: section.sectionTitle,
12683
+ status: "FAILED",
12684
+ t3Results: [],
12685
+ sectionSummary: "",
12686
+ issues: [err instanceof Error ? err.message : String(err)]
12687
+ };
12250
12688
  }
12251
- } catch (err) {
12252
- result = {
12689
+ },
12690
+ // PARTIAL is deliberately not a failure: a degraded-but-real section
12691
+ // can still feed the one after it, and cancelling that work would cost
12692
+ // the user more than letting it try.
12693
+ // BLOCKED cannot reach here today — the scheduler settles blocked nodes
12694
+ // through onNodeBlocked and never executes them, so classify only ever
12695
+ // sees results that ran. It is listed anyway because the failure mode if
12696
+ // that ever changes is silent and expensive: a blocked section would
12697
+ // classify as 'succeeded' and release its own dependents, so work that
12698
+ // should have been skipped would run and be billed.
12699
+ classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
12700
+ onNodeBlocked: (node, blockedInfo) => {
12701
+ const section = node.payload;
12702
+ this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
12703
+ resultMap.set(node.id, {
12253
12704
  sectionId: section.sectionId,
12254
12705
  sectionTitle: section.sectionTitle,
12255
- status: "FAILED",
12706
+ // BLOCKED, not FAILED. This section was never attempted — saying it
12707
+ // failed reports a breakage that did not happen, and hides the one
12708
+ // that did (the upstream section named below).
12709
+ status: "BLOCKED",
12256
12710
  t3Results: [],
12257
12711
  sectionSummary: "",
12258
- issues: [err instanceof Error ? err.message : String(err)]
12259
- };
12260
- }
12261
- resultMap.set(id, result);
12262
- completedSections++;
12263
- for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
12264
- inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
12265
- }
12266
- };
12267
- if (this.router.getT3ExecutionMode?.() === "sequential") {
12268
- for (const id of readyIds) {
12269
- await runOne(id);
12712
+ issues: [
12713
+ `${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
12714
+ ],
12715
+ blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
12716
+ });
12717
+ completedSections++;
12718
+ this.sendStatusUpdate({
12719
+ progressPct: 10 + Math.floor(completedSections / totalSections * 85),
12720
+ currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
12721
+ status: "IN_PROGRESS"
12722
+ });
12723
+ this.emit("tier:status", {
12724
+ tierId: section.sectionId,
12725
+ // A blocked section has no tier instance — it was never
12726
+ // constructed — so its runtime id and its graph id are the same
12727
+ // thing here. Emitting nodeId anyway keeps every node in the
12728
+ // stream addressable in the graph's id space, which is what
12729
+ // dependsOn edges point at.
12730
+ nodeId: section.sectionId,
12731
+ dependsOn: [...section.dependsOn ?? []],
12732
+ role: "T2",
12733
+ label: section.sectionTitle,
12734
+ status: "BLOCKED",
12735
+ blockedBy: [...blockedInfo.blockedByTitles],
12736
+ output: blockedInfo.reason
12737
+ });
12738
+ this.emit("section:complete", {
12739
+ id: node.id,
12740
+ title: section.sectionTitle,
12741
+ status: "BLOCKED",
12742
+ blockedBy: [...blockedInfo.blockedByTitles],
12743
+ output: ""
12744
+ });
12745
+ },
12746
+ onNodeComplete: (node, result) => {
12747
+ resultMap.set(node.id, result);
12748
+ completedSections++;
12749
+ this.emit("section:complete", {
12750
+ id: node.id,
12751
+ title: result.sectionTitle,
12752
+ status: result.status,
12753
+ output: result.sectionSummary
12754
+ });
12270
12755
  }
12271
- } else {
12272
- await Promise.all(readyIds.map(runOne));
12273
- }
12274
- if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
12275
- await executeWave();
12756
+ },
12757
+ {
12758
+ // Cross-section concurrency respects the same t3Execution mode as the
12759
+ // intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
12760
+ // serialized T3 workers within a single section while every section's
12761
+ // T2Manager (and its T3 workers) still ran in parallel here.
12762
+ mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
12763
+ // A section that declared `dependsOn` said it needs that section's
12764
+ // output. Running it after the dependency failed produces a second
12765
+ // failure, billed, whose cause is the first one — so don't.
12766
+ onUpstreamFailure: "block"
12276
12767
  }
12277
- };
12278
- await executeWave();
12768
+ );
12769
+ await scheduler.run();
12279
12770
  return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
12280
12771
  }
12281
12772
  async compileFinalOutput(originalPrompt, plan, t2Results) {
12282
- const completedSections = t2Results.filter((r) => r.status !== "FAILED");
12773
+ const completedSections = t2Results.filter(producedOutput);
12283
12774
  if (!completedSections.length) {
12284
12775
  const allIssues = [
12285
12776
  ...t2Results.flatMap((r) => r.issues ?? []),
@@ -12299,7 +12790,6 @@ ${r.sectionSummary}
12299
12790
  Outputs:
12300
12791
  ${r.t3Results.filter((t) => t.status === "COMPLETED").map((t) => `\u2022 ${typeof t.output === "string" ? t.output : JSON.stringify(t.output)}`).join("\n")}`).join("\n\n---\n\n");
12301
12792
  const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
12302
- t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
12303
12793
  const compilePrompt = `Compile a final, coherent response to the user's original request.
12304
12794
 
12305
12795
  Original request: ${originalPrompt}
@@ -12794,6 +13284,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
12794
13284
  };
12795
13285
  var DEFAULT_VIDEO_SECONDS = 5;
12796
13286
  var MAX_VIDEO_SECONDS = 8;
13287
+ var MIN_VIDEO_SECONDS = 4;
12797
13288
  var GenerateVideoTool = class extends BaseTool {
12798
13289
  constructor(deps) {
12799
13290
  super();
@@ -12810,7 +13301,7 @@ var GenerateVideoTool = class extends BaseTool {
12810
13301
  type: "object",
12811
13302
  properties: {
12812
13303
  prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
12813
- seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
13304
+ seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
12814
13305
  aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
12815
13306
  model: { type: "string", description: "Optional: force a specific video model." }
12816
13307
  },
@@ -12822,7 +13313,7 @@ var GenerateVideoTool = class extends BaseTool {
12822
13313
  const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
12823
13314
  if (!r.ok) return r.error;
12824
13315
  const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
12825
- const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
13316
+ const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
12826
13317
  const asset = await generateVideo(
12827
13318
  r.selected.capability,
12828
13319
  r.cfg,
@@ -12839,7 +13330,7 @@ var GenerateVideoTool = class extends BaseTool {
12839
13330
  const location = await this.deps.sink(asset);
12840
13331
  const unit = r.selected.cost;
12841
13332
  const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
12842
- const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
13333
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
12843
13334
  return `Video generated and saved to ${location}.
12844
13335
  Model: ${asset.modelId}.${estimate}${clamped}`;
12845
13336
  }
@@ -12908,16 +13399,16 @@ function resolveInWorkspace(workspaceRoot, input) {
12908
13399
  if (typeof input !== "string" || input.length === 0) {
12909
13400
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
12910
13401
  }
12911
- const root = path33__default.default.resolve(workspaceRoot);
12912
- const abs = path33__default.default.isAbsolute(input) ? path33__default.default.resolve(input) : path33__default.default.resolve(root, input);
12913
- const rel = path33__default.default.relative(root, abs);
12914
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path33__default.default.isAbsolute(rel)) {
13402
+ const root = path34__default.default.resolve(workspaceRoot);
13403
+ const abs = path34__default.default.isAbsolute(input) ? path34__default.default.resolve(input) : path34__default.default.resolve(root, input);
13404
+ const rel = path34__default.default.relative(root, abs);
13405
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34__default.default.isAbsolute(rel)) {
12915
13406
  throw new WorkspaceSandboxError(input, root);
12916
13407
  }
12917
13408
  try {
12918
- const real = fs30__default.default.realpathSync(abs);
12919
- const realRel = path33__default.default.relative(root, real);
12920
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path33__default.default.isAbsolute(realRel))) {
13409
+ const real = fs31__default.default.realpathSync(abs);
13410
+ const realRel = path34__default.default.relative(root, real);
13411
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34__default.default.isAbsolute(realRel))) {
12921
13412
  throw new WorkspaceSandboxError(input, root);
12922
13413
  }
12923
13414
  } catch (e) {
@@ -13570,7 +14061,7 @@ var GenerateDocumentTool = class extends BaseTool {
13570
14061
  }
13571
14062
  }
13572
14063
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
13573
- await fs11__default.default.mkdir(path33__default.default.dirname(absPath), { recursive: true });
14064
+ await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
13574
14065
  await fs11__default.default.writeFile(absPath, bytes);
13575
14066
  const charts = countCharts(content);
13576
14067
  return [
@@ -13791,7 +14282,7 @@ var FileWriteTool = class extends BaseTool {
13791
14282
  } catch {
13792
14283
  }
13793
14284
  }
13794
- await fs11__default.default.mkdir(path33__default.default.dirname(absPath), { recursive: true });
14285
+ await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
13795
14286
  await fs11__default.default.writeFile(absPath, content, "utf-8");
13796
14287
  return `Written ${content.length} characters to ${filePath}`;
13797
14288
  }
@@ -14289,7 +14780,7 @@ var ImageAnalyzeTool = class extends BaseTool {
14289
14780
  };
14290
14781
  async function fileToImageAttachment(filePath) {
14291
14782
  const data = await fs11__default.default.readFile(filePath);
14292
- const ext = path33__default.default.extname(filePath).toLowerCase();
14783
+ const ext = path34__default.default.extname(filePath).toLowerCase();
14293
14784
  const mimeMap = {
14294
14785
  ".jpg": "image/jpeg",
14295
14786
  ".jpeg": "image/jpeg",
@@ -14323,14 +14814,14 @@ var PDFCreateTool = class extends BaseTool {
14323
14814
  const filePath = input["path"];
14324
14815
  const content = input["content"];
14325
14816
  const title = input["title"];
14326
- const dir = path33__default.default.dirname(filePath);
14327
- if (!fs30__default.default.existsSync(dir)) {
14328
- fs30__default.default.mkdirSync(dir, { recursive: true });
14817
+ const dir = path34__default.default.dirname(filePath);
14818
+ if (!fs31__default.default.existsSync(dir)) {
14819
+ fs31__default.default.mkdirSync(dir, { recursive: true });
14329
14820
  }
14330
14821
  return new Promise((resolve2, reject) => {
14331
14822
  try {
14332
14823
  const doc = new PDFDocument__default.default({ margin: 50 });
14333
- const stream = fs30__default.default.createWriteStream(filePath);
14824
+ const stream = fs31__default.default.createWriteStream(filePath);
14334
14825
  doc.pipe(stream);
14335
14826
  if (title) {
14336
14827
  doc.info["Title"] = title;
@@ -14408,22 +14899,22 @@ var CodeInterpreterTool = class extends BaseTool {
14408
14899
  }
14409
14900
  cmdPrefix = NODE_CMD;
14410
14901
  }
14411
- const tmpDir = path33__default.default.join(this.workspaceRoot, ".cascade", "tmp");
14412
- if (!fs30__default.default.existsSync(tmpDir)) {
14413
- fs30__default.default.mkdirSync(tmpDir, { recursive: true });
14902
+ const tmpDir = path34__default.default.join(this.workspaceRoot, ".cascade", "tmp");
14903
+ if (!fs31__default.default.existsSync(tmpDir)) {
14904
+ fs31__default.default.mkdirSync(tmpDir, { recursive: true });
14414
14905
  }
14415
14906
  const extension = language === "python" ? "py" : "js";
14416
14907
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
14417
- const filePath = path33__default.default.join(tmpDir, fileName);
14418
- fs30__default.default.writeFileSync(filePath, code, "utf-8");
14908
+ const filePath = path34__default.default.join(tmpDir, fileName);
14909
+ fs31__default.default.writeFileSync(filePath, code, "utf-8");
14419
14910
  const execArgs = [filePath, ...args];
14420
14911
  return new Promise((resolve2) => {
14421
14912
  const startMs = Date.now();
14422
14913
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
14423
14914
  const duration = Date.now() - startMs;
14424
14915
  try {
14425
- if (fs30__default.default.existsSync(filePath)) {
14426
- fs30__default.default.unlinkSync(filePath);
14916
+ if (fs31__default.default.existsSync(filePath)) {
14917
+ fs31__default.default.unlinkSync(filePath);
14427
14918
  }
14428
14919
  } catch (cleanupErr) {
14429
14920
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -14735,7 +15226,7 @@ var GlobTool = class extends BaseTool {
14735
15226
  };
14736
15227
  async execute(input, _options) {
14737
15228
  const pattern = input["pattern"];
14738
- const searchPath = input["path"] ? path33__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15229
+ const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
14739
15230
  const matches = await glob.glob(pattern, {
14740
15231
  cwd: searchPath,
14741
15232
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -14748,7 +15239,7 @@ var GlobTool = class extends BaseTool {
14748
15239
  const withMtime = await Promise.all(
14749
15240
  matches.map(async (rel) => {
14750
15241
  try {
14751
- const stat = await fs11__default.default.stat(path33__default.default.join(searchPath, rel));
15242
+ const stat = await fs11__default.default.stat(path34__default.default.join(searchPath, rel));
14752
15243
  return { rel, mtime: stat.mtimeMs };
14753
15244
  } catch {
14754
15245
  return { rel, mtime: 0 };
@@ -14797,7 +15288,7 @@ var GrepTool = class extends BaseTool {
14797
15288
  };
14798
15289
  async execute(input, _options) {
14799
15290
  const pattern = input["pattern"];
14800
- const searchPath = input["path"] ? path33__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15291
+ const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
14801
15292
  const globPattern = input["glob"];
14802
15293
  const outputMode = input["output_mode"] ?? "content";
14803
15294
  const context = input["context"] ?? 0;
@@ -14851,12 +15342,12 @@ var GrepTool = class extends BaseTool {
14851
15342
  nodir: true
14852
15343
  });
14853
15344
  } catch {
14854
- files = [path33__default.default.relative(searchPath, searchPath) || "."];
15345
+ files = [path34__default.default.relative(searchPath, searchPath) || "."];
14855
15346
  }
14856
15347
  const results = [];
14857
15348
  let totalCount = 0;
14858
15349
  for (const rel of files) {
14859
- const abs = path33__default.default.join(searchPath, rel);
15350
+ const abs = path34__default.default.join(searchPath, rel);
14860
15351
  let content;
14861
15352
  try {
14862
15353
  content = await fs11__default.default.readFile(abs, "utf-8");
@@ -15243,10 +15734,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
15243
15734
  }
15244
15735
  isIgnored(filePath) {
15245
15736
  if (!filePath) return false;
15246
- const abs = path33__default.default.resolve(this.workspaceRoot, filePath);
15247
- const rel = path33__default.default.relative(this.workspaceRoot, abs);
15248
- if (!rel || rel.startsWith("..") || path33__default.default.isAbsolute(rel)) return true;
15249
- const posixRel = rel.split(path33__default.default.sep).join("/");
15737
+ const abs = path34__default.default.resolve(this.workspaceRoot, filePath);
15738
+ const rel = path34__default.default.relative(this.workspaceRoot, abs);
15739
+ if (!rel || rel.startsWith("..") || path34__default.default.isAbsolute(rel)) return true;
15740
+ const posixRel = rel.split(path34__default.default.sep).join("/");
15250
15741
  return this.ignoreMatcher.ignores(posixRel);
15251
15742
  }
15252
15743
  };
@@ -15480,22 +15971,22 @@ var FileMcpOAuthStore = class {
15480
15971
  filePath;
15481
15972
  load() {
15482
15973
  try {
15483
- return JSON.parse(fs30__default.default.readFileSync(this.filePath, "utf-8"));
15974
+ return JSON.parse(fs31__default.default.readFileSync(this.filePath, "utf-8"));
15484
15975
  } catch {
15485
15976
  return void 0;
15486
15977
  }
15487
15978
  }
15488
15979
  save(state) {
15489
- fs30__default.default.mkdirSync(path33__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
15490
- fs30__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
15980
+ fs31__default.default.mkdirSync(path34__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
15981
+ fs31__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
15491
15982
  try {
15492
- fs30__default.default.chmodSync(this.filePath, 384);
15983
+ fs31__default.default.chmodSync(this.filePath, 384);
15493
15984
  } catch {
15494
15985
  }
15495
15986
  }
15496
15987
  clear() {
15497
15988
  try {
15498
- fs30__default.default.rmSync(this.filePath);
15989
+ fs31__default.default.rmSync(this.filePath);
15499
15990
  } catch {
15500
15991
  }
15501
15992
  }
@@ -15958,6 +16449,14 @@ function heuristicAnalyze(prompt) {
15958
16449
  const estimatedTokens = wordCount * 5;
15959
16450
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
15960
16451
  }
16452
+ function hashPrompt(prompt) {
16453
+ let hash = 2166136261;
16454
+ for (let index = 0; index < prompt.length; index++) {
16455
+ hash ^= prompt.charCodeAt(index);
16456
+ hash = Math.imul(hash, 16777619) >>> 0;
16457
+ }
16458
+ return `${prompt.length}:${hash.toString(36)}`;
16459
+ }
15961
16460
  var analysisCache = /* @__PURE__ */ new Map();
15962
16461
  var TASK_TYPE_TAGS = {
15963
16462
  code: ["code", "instruction"],
@@ -15972,7 +16471,24 @@ var TaskAnalyzer = class {
15972
16471
  feedback;
15973
16472
  bias;
15974
16473
  lastProfile = null;
15975
- lastSelectedModels = /* @__PURE__ */ new Map();
16474
+ /** Models chosen by the run currently in flight. Cleared when it completes. */
16475
+ currentRunSelections = /* @__PURE__ */ new Map();
16476
+ /**
16477
+ * Immutable snapshot of the last COMPLETED run: its selections AND the task
16478
+ * type they were chosen for.
16479
+ *
16480
+ * The task type has to travel WITH the selections. `lastProfile` is overwritten
16481
+ * by the next analyze(), so if run B started before the user rated run A, A's
16482
+ * models were credited under B's task type — teaching the router that a coding
16483
+ * model is good at creative writing, from a rating that never said so.
16484
+ *
16485
+ * Explicit ratings arrive after the run finishes — rateLastRun() is by nature
16486
+ * a reaction to a finished answer — but the run finalizer calls
16487
+ * recordRunOutcome() first, and that used to clear the only map there was. So
16488
+ * every explicit rating iterated an empty map, recorded nothing, and returned
16489
+ * false: the 3x-weighted user signal never reached the tracker at all.
16490
+ */
16491
+ lastCompletedRun;
15976
16492
  constructor(tracker, bias = "balanced") {
15977
16493
  this.tracker = tracker;
15978
16494
  this.bias = bias;
@@ -16001,7 +16517,7 @@ var TaskAnalyzer = class {
16001
16517
  * Low confidence prompts fall back to a conservative mixed/moderate profile.
16002
16518
  */
16003
16519
  async analyze(prompt) {
16004
- const cacheKey = prompt.slice(0, 200);
16520
+ const cacheKey = hashPrompt(prompt);
16005
16521
  const cached = analysisCache.get(cacheKey);
16006
16522
  if (cached) {
16007
16523
  this.lastProfile = cached;
@@ -16019,11 +16535,15 @@ var TaskAnalyzer = class {
16019
16535
  */
16020
16536
  async selectModel(prompt, tier, selector, opts) {
16021
16537
  const profile = await this.analyze(prompt);
16538
+ const recordSelection = (model) => {
16539
+ if (model) this.currentRunSelections.set(tier, model);
16540
+ return model;
16541
+ };
16022
16542
  if (profile.requiresVision) {
16023
- return selector.selectVisionModel();
16543
+ return recordSelection(selector.selectVisionModel());
16024
16544
  }
16025
16545
  let candidates2 = selector.getCandidatesForTier(tier);
16026
- if (candidates2.length === 0) return selector.selectForTier(tier);
16546
+ if (candidates2.length === 0) return recordSelection(selector.selectForTier(tier));
16027
16547
  if (opts?.requiresToolUse) {
16028
16548
  const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
16029
16549
  if (toolCapable.length > 0) candidates2 = toolCapable;
@@ -16033,9 +16553,7 @@ var TaskAnalyzer = class {
16033
16553
  score: this.scoreModel(m, profile)
16034
16554
  }));
16035
16555
  scored.sort((a, b) => b.score - a.score);
16036
- const best = scored[0]?.model ?? selector.selectForTier(tier);
16037
- if (best) this.lastSelectedModels.set(tier, best);
16038
- return best;
16556
+ return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
16039
16557
  }
16040
16558
  /**
16041
16559
  * Record the outcome of a completed run across all tiers that were selected
@@ -16044,25 +16562,38 @@ var TaskAnalyzer = class {
16044
16562
  recordRunOutcome(outcome, costByTier, contextTokens = 0) {
16045
16563
  if (!this.tracker || !this.lastProfile) return;
16046
16564
  const taskType = this.lastProfile.type;
16047
- for (const [tier, model] of this.lastSelectedModels) {
16565
+ for (const [tier, model] of this.currentRunSelections) {
16048
16566
  const cost = costByTier[tier] ?? 0;
16049
16567
  this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
16050
16568
  }
16051
- this.lastSelectedModels.clear();
16569
+ this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
16570
+ this.currentRunSelections.clear();
16052
16571
  void this.tracker.save();
16053
16572
  }
16054
16573
  /**
16055
- * Record an explicit user rating (good/bad) for the last run's selected models.
16574
+ * Record an explicit user rating (good/bad) for the last COMPLETED run's models.
16056
16575
  * Explicit ratings carry 3× the weight of auto-detected outcomes.
16057
- * Does NOT clear lastSelectedModels — the auto record already did that.
16576
+ *
16577
+ * Reads the completed-run snapshot, not the in-flight map: by the time a user
16578
+ * can rate an answer, the finalizer has already run and the in-flight map is
16579
+ * empty.
16580
+ *
16581
+ * The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
16582
+ * three samples, so a double submit would inject six — a stutter on the
16583
+ * button would count as two opinions. Consuming makes a second call a no-op
16584
+ * that reports false, which is what "rate this run" should mean.
16058
16585
  */
16059
16586
  recordExplicitRating(rating) {
16060
- if (!this.tracker || !this.lastProfile) return false;
16061
- const taskType = this.lastProfile.type;
16062
- for (const [, model] of this.lastSelectedModels) {
16063
- this.tracker.recordExplicit(model.id, taskType, rating, 0);
16064
- }
16065
- return this.lastSelectedModels.size > 0;
16587
+ const snapshot = this.lastCompletedRun;
16588
+ if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
16589
+ const ratedModels = /* @__PURE__ */ new Set();
16590
+ for (const [, model] of snapshot.selections) {
16591
+ if (ratedModels.has(model.id)) continue;
16592
+ ratedModels.add(model.id);
16593
+ this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
16594
+ }
16595
+ this.lastCompletedRun = void 0;
16596
+ return true;
16066
16597
  }
16067
16598
  scoreModel(model, profile) {
16068
16599
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
@@ -16103,7 +16634,7 @@ var TaskAnalyzer = class {
16103
16634
 
16104
16635
  // src/core/router/model-performance-tracker.ts
16105
16636
  init_pricing();
16106
- var DEFAULT_STATS_FILE = path33__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
16637
+ var DEFAULT_STATS_FILE = path34__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
16107
16638
  var ModelPerformanceTracker = class {
16108
16639
  stats = /* @__PURE__ */ new Map();
16109
16640
  featureStats = /* @__PURE__ */ new Map();
@@ -16143,7 +16674,7 @@ var ModelPerformanceTracker = class {
16143
16674
  async save() {
16144
16675
  if (this.readOnly) return;
16145
16676
  try {
16146
- await fs11__default.default.mkdir(path33__default.default.dirname(this.statsFile), { recursive: true });
16677
+ await fs11__default.default.mkdir(path34__default.default.dirname(this.statsFile), { recursive: true });
16147
16678
  const modelsObj = {};
16148
16679
  const featuresObj = {};
16149
16680
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -16691,7 +17222,7 @@ Required capability: ${description.slice(0, 300)}`;
16691
17222
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
16692
17223
  async loadPersistedTools() {
16693
17224
  if (!this.workspacePath || !this.persistEnabled) return;
16694
- const file = path33__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
17225
+ const file = path34__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
16695
17226
  try {
16696
17227
  const raw = await fs11__default.default.readFile(file, "utf-8");
16697
17228
  const specs = JSON.parse(raw);
@@ -16715,8 +17246,8 @@ Required capability: ${description.slice(0, 300)}`;
16715
17246
  }
16716
17247
  async persist() {
16717
17248
  if (!this.workspacePath || !this.persistEnabled) return;
16718
- const dir = path33__default.default.join(this.workspacePath, ".cascade");
16719
- const file = path33__default.default.join(dir, DYNAMIC_TOOLS_FILE);
17249
+ const dir = path34__default.default.join(this.workspacePath, ".cascade");
17250
+ const file = path34__default.default.join(dir, DYNAMIC_TOOLS_FILE);
16720
17251
  try {
16721
17252
  await fs11__default.default.mkdir(dir, { recursive: true });
16722
17253
  await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -16736,12 +17267,12 @@ var WorldStateDB = class {
16736
17267
  constructor(workspacePath, debugMode = false) {
16737
17268
  this.workspacePath = workspacePath;
16738
17269
  this.debugMode = debugMode;
16739
- const cascadeDir = path33__default.default.join(workspacePath, ".cascade");
16740
- if (!fs30__default.default.existsSync(cascadeDir)) {
16741
- fs30__default.default.mkdirSync(cascadeDir, { recursive: true });
17270
+ const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
17271
+ if (!fs31__default.default.existsSync(cascadeDir)) {
17272
+ fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
16742
17273
  }
16743
- this.keyPath = path33__default.default.join(cascadeDir, "world_state.key");
16744
- this.dbPath = path33__default.default.join(cascadeDir, "world_state.db");
17274
+ this.keyPath = path34__default.default.join(cascadeDir, "world_state.key");
17275
+ this.dbPath = path34__default.default.join(cascadeDir, "world_state.db");
16745
17276
  this.initEncryptionKey();
16746
17277
  this.db = new Database2__default.default(this.dbPath);
16747
17278
  this.db.pragma("journal_mode = WAL");
@@ -16798,11 +17329,11 @@ var WorldStateDB = class {
16798
17329
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
16799
17330
  }
16800
17331
  initEncryptionKey() {
16801
- if (fs30__default.default.existsSync(this.keyPath)) {
16802
- this.encryptionKey = fs30__default.default.readFileSync(this.keyPath);
17332
+ if (fs31__default.default.existsSync(this.keyPath)) {
17333
+ this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
16803
17334
  } else {
16804
17335
  this.encryptionKey = crypto__default.default.randomBytes(32);
16805
- fs30__default.default.writeFileSync(this.keyPath, this.encryptionKey);
17336
+ fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
16806
17337
  }
16807
17338
  }
16808
17339
  encrypt(text) {
@@ -17088,9 +17619,9 @@ var WorldStateDB = class {
17088
17619
  dumpDebugIfNeeded() {
17089
17620
  if (!this.debugMode) return;
17090
17621
  try {
17091
- const dumpPath = path33__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
17622
+ const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
17092
17623
  const entries = this.getAllEntries();
17093
- fs30__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
17624
+ fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
17094
17625
  } catch (err) {
17095
17626
  console.error("Failed to dump debug world state", err);
17096
17627
  }
@@ -17099,6 +17630,212 @@ var WorldStateDB = class {
17099
17630
  this.db.close();
17100
17631
  }
17101
17632
  };
17633
+ var RESUME_SCHEMA_VERSION = 1;
17634
+ var DEFAULT_MAX_CHECKPOINTS = 5;
17635
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
17636
+ var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
17637
+ var CLAIM_SUFFIX = ".claimed";
17638
+ var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
17639
+ function safeId(taskId) {
17640
+ const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
17641
+ return cleaned.slice(0, 100) || "run";
17642
+ }
17643
+ function isCheckpoint(value) {
17644
+ if (typeof value !== "object" || value === null) return false;
17645
+ const c = value;
17646
+ 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);
17647
+ }
17648
+ var ResumeStore = class {
17649
+ dir;
17650
+ maxCheckpoints;
17651
+ maxAgeMs;
17652
+ now;
17653
+ claimTimeoutMs;
17654
+ constructor(options) {
17655
+ this.dir = options.dir;
17656
+ this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
17657
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
17658
+ this.now = options.now ?? Date.now;
17659
+ this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
17660
+ }
17661
+ /**
17662
+ * Persist a checkpoint and prune.
17663
+ *
17664
+ * Never throws — failing to save a resume file must not turn a recoverable
17665
+ * stop into a crash — but DOES report whether the atomic rename landed.
17666
+ * Callers decide whether an original claim is safe to discard based on that
17667
+ * answer, so "I swallowed the error" and "the checkpoint is on disk" must not
17668
+ * look alike to them.
17669
+ *
17670
+ * @returns true only when the checkpoint is durably on disk.
17671
+ */
17672
+ async save(checkpoint) {
17673
+ try {
17674
+ const timestamp = this.now();
17675
+ const record = {
17676
+ ...checkpoint,
17677
+ version: RESUME_SCHEMA_VERSION,
17678
+ createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
17679
+ };
17680
+ await fs31.promises.mkdir(this.dir, { recursive: true });
17681
+ const finalPath = path34__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
17682
+ const tempPath = `${finalPath}.${process.pid}.tmp`;
17683
+ await fs31.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
17684
+ await fs31.promises.rename(tempPath, finalPath);
17685
+ await this.prune();
17686
+ return true;
17687
+ } catch {
17688
+ return false;
17689
+ }
17690
+ }
17691
+ /** Valid, unexpired checkpoints, newest first. */
17692
+ async list() {
17693
+ let entries;
17694
+ try {
17695
+ entries = await fs31.promises.readdir(this.dir);
17696
+ } catch {
17697
+ return [];
17698
+ }
17699
+ const cutoff = this.now() - this.maxAgeMs;
17700
+ const candidates2 = 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);
17701
+ const checkpoints = [];
17702
+ for (const candidate of candidates2) {
17703
+ try {
17704
+ const raw = await fs31.promises.readFile(path34__default.default.join(this.dir, candidate.name), "utf-8");
17705
+ const parsed = JSON.parse(raw);
17706
+ if (isCheckpoint(parsed)) checkpoints.push(parsed);
17707
+ } catch {
17708
+ continue;
17709
+ }
17710
+ }
17711
+ return checkpoints;
17712
+ }
17713
+ /** The most recent resumable run, or null. */
17714
+ async latest() {
17715
+ return (await this.list())[0] ?? null;
17716
+ }
17717
+ /**
17718
+ * Claim a checkpoint for a resume attempt.
17719
+ *
17720
+ * This used to DELETE the file and return it, which opened a window with no
17721
+ * recovery record at all: between preparing the continuation and the resumed
17722
+ * run writing its own checkpoint, a crash lost the only copy — and a crash is
17723
+ * precisely what this store exists to survive. Deleting on read optimised for
17724
+ * "never resume twice" at the cost of "never lose the work", which is the
17725
+ * wrong trade for a recovery mechanism.
17726
+ *
17727
+ * Instead the file is RENAMED to `.claimed`, which still hides it from
17728
+ * `list()` (so a concurrent resume cannot take the same work) but keeps it on
17729
+ * disk. The caller then either `release`s it — restoring it for another
17730
+ * attempt — or `settle`s it once the resumed run is safely under way. A claim
17731
+ * abandoned by a dead process is reclaimed after `claimTimeoutMs`.
17732
+ */
17733
+ async claim(taskId) {
17734
+ const all = await this.list();
17735
+ const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
17736
+ if (!target) return null;
17737
+ try {
17738
+ const entries = await fs31.promises.readdir(this.dir);
17739
+ const wanted = safeId(target.taskId);
17740
+ const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
17741
+ if (!source) return null;
17742
+ const claimId = `${this.now()}-${safeId(target.taskId)}`;
17743
+ await fs31.promises.rename(path34__default.default.join(this.dir, source), path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
17744
+ return { checkpoint: target, claimId };
17745
+ } catch {
17746
+ return null;
17747
+ }
17748
+ }
17749
+ /** Give a claim back, so the work stays recoverable after a failed start. */
17750
+ async release(claimId) {
17751
+ try {
17752
+ await fs31.promises.rename(
17753
+ path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
17754
+ path34__default.default.join(this.dir, `${claimId}.json`)
17755
+ );
17756
+ } catch {
17757
+ }
17758
+ }
17759
+ /** Discard a claim for good — the resumed run has taken ownership. */
17760
+ async settle(claimId) {
17761
+ try {
17762
+ await fs31.promises.rm(path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
17763
+ } catch {
17764
+ }
17765
+ }
17766
+ /**
17767
+ * Claim-and-discard in one step. Kept for callers that genuinely want the old
17768
+ * semantics; prefer claim/settle so a crash mid-resume stays recoverable.
17769
+ */
17770
+ async consume(taskId) {
17771
+ const claimed = await this.claim(taskId);
17772
+ if (!claimed) return null;
17773
+ await this.settle(claimed.claimId);
17774
+ return claimed.checkpoint;
17775
+ }
17776
+ /**
17777
+ * Return claims abandoned by a process that died mid-resume. Without this a
17778
+ * crash between claim and settle would strand the checkpoint invisibly —
17779
+ * trading one loss window for another.
17780
+ */
17781
+ async reclaimStale() {
17782
+ let reclaimed = 0;
17783
+ try {
17784
+ const entries = await fs31.promises.readdir(this.dir);
17785
+ const cutoff = this.now() - this.claimTimeoutMs;
17786
+ for (const name of entries) {
17787
+ if (!name.endsWith(CLAIM_SUFFIX)) continue;
17788
+ const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
17789
+ if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
17790
+ await fs31.promises.rename(
17791
+ path34__default.default.join(this.dir, name),
17792
+ path34__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
17793
+ );
17794
+ reclaimed++;
17795
+ }
17796
+ } catch {
17797
+ }
17798
+ return reclaimed;
17799
+ }
17800
+ /** Remove every checkpoint for a task id. */
17801
+ async delete(taskId) {
17802
+ try {
17803
+ const entries = await fs31.promises.readdir(this.dir);
17804
+ const wanted = safeId(taskId);
17805
+ await Promise.all(entries.map(async (name) => {
17806
+ const match = FILE_PATTERN.exec(name);
17807
+ if (match?.[2] === wanted) {
17808
+ await fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true });
17809
+ }
17810
+ }));
17811
+ } catch {
17812
+ }
17813
+ }
17814
+ /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
17815
+ async prune() {
17816
+ try {
17817
+ const entries = await fs31.promises.readdir(this.dir);
17818
+ const cutoff = this.now() - this.maxAgeMs;
17819
+ 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);
17820
+ const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
17821
+ const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
17822
+ await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
17823
+ (name) => fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true })
17824
+ ));
17825
+ } catch {
17826
+ }
17827
+ }
17828
+ };
17829
+ function summarizeCompleted(completed, perNodeChars = 500) {
17830
+ if (!completed.length) return "";
17831
+ const lines = completed.map((node) => {
17832
+ const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
17833
+ return `- [${node.status}] ${node.title}
17834
+ ${body.replace(/\n/g, "\n ")}`;
17835
+ });
17836
+ return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
17837
+ ${lines.join("\n")}`;
17838
+ }
17102
17839
  var ignore3 = _ignoreModule__namespace.default ?? _ignoreModule__namespace;
17103
17840
  var PrivacyPaths = class {
17104
17841
  localOnly;
@@ -17460,7 +18197,7 @@ var WorkspaceIndex = class {
17460
18197
  isIgnored;
17461
18198
  chunker;
17462
18199
  constructor(opts) {
17463
- this.root = path33__default.default.resolve(opts.root);
18200
+ this.root = path34__default.default.resolve(opts.root);
17464
18201
  this.db = opts.db;
17465
18202
  this.namespace = opts.namespace ?? "code";
17466
18203
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -17492,14 +18229,14 @@ var WorkspaceIndex = class {
17492
18229
  return;
17493
18230
  }
17494
18231
  for (const entry of entries) {
17495
- const abs = path33__default.default.join(dir, entry.name);
18232
+ const abs = path34__default.default.join(dir, entry.name);
17496
18233
  if (entry.isDirectory()) {
17497
18234
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
17498
18235
  await walk(abs);
17499
18236
  } else if (entry.isFile()) {
17500
- const ext = path33__default.default.extname(entry.name).slice(1).toLowerCase();
18237
+ const ext = path34__default.default.extname(entry.name).slice(1).toLowerCase();
17501
18238
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
17502
- out.push(path33__default.default.relative(this.root, abs));
18239
+ out.push(path34__default.default.relative(this.root, abs));
17503
18240
  }
17504
18241
  }
17505
18242
  };
@@ -17529,7 +18266,7 @@ var WorkspaceIndex = class {
17529
18266
  const entries = [];
17530
18267
  for (const rel of rels) {
17531
18268
  try {
17532
- const abs = path33__default.default.join(this.root, rel);
18269
+ const abs = path34__default.default.join(this.root, rel);
17533
18270
  const stat = await fs11__default.default.stat(abs);
17534
18271
  if (stat.size > this.maxFileBytes) continue;
17535
18272
  const buf = await fs11__default.default.readFile(abs);
@@ -18067,8 +18804,35 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
18067
18804
  /** Orchestration decisions for the CURRENT run — cleared on each run(). */
18068
18805
  decisionLog = [];
18069
18806
  initialized = false;
18070
- /** Last task that stopped at the budget cap — powers /continue (resumeRun). */
18807
+ /**
18808
+ * Last interrupted task — powers /continue (resumeRun). Kept in memory as a
18809
+ * fast path for the same-process case; the durable copy lives in `resumeStore`
18810
+ * so a crash or an exit no longer throws away everything already finished.
18811
+ */
18071
18812
  lastInterruptedRun;
18813
+ /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
18814
+ resumeStore;
18815
+ /** Claim held by an in-flight resume, settled once the run owns the work. */
18816
+ claimedResumeId;
18817
+ /**
18818
+ * Facts inherited from the checkpoint this run is resuming, so its own
18819
+ * checkpoint is cumulative rather than only covering this attempt.
18820
+ */
18821
+ resumedFrom;
18822
+ /** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
18823
+ wroteCheckpointThisRun = false;
18824
+ /**
18825
+ * Whether the run reached the end of its normal path.
18826
+ *
18827
+ * NOT the same as `runError == null`: the cancellation and budget handlers
18828
+ * both null `runError` deliberately (an intentional stop is not a telemetry
18829
+ * error) and the breaker path never sets it at all. Treating that as success
18830
+ * meant an immediate cancel — no output, no replacement checkpoint — deleted
18831
+ * the original claim and the finished sections with it.
18832
+ */
18833
+ completedNormally = false;
18834
+ /** Section results of the CURRENT run, so an interruption can checkpoint them. */
18835
+ currentRunCompleted = [];
18072
18836
  initPromise;
18073
18837
  store;
18074
18838
  audit;
@@ -18101,10 +18865,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
18101
18865
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
18102
18866
  try {
18103
18867
  this.router.setDeadModelStore(new DeadModelStore(
18104
- fileDeadModelPersistence(path33__default.default.join(workspacePath, ".cascade", "dead-models.json"))
18868
+ fileDeadModelPersistence(path34__default.default.join(workspacePath, ".cascade", "dead-models.json"))
18105
18869
  ));
18106
18870
  } catch {
18107
18871
  }
18872
+ try {
18873
+ this.resumeStore = new ResumeStore({
18874
+ dir: path34__default.default.join(workspacePath, ".cascade", "resume")
18875
+ });
18876
+ } catch {
18877
+ }
18108
18878
  this.multimodal = new MultimodalRegistry(
18109
18879
  (this.config.providers ?? []).map((p) => p.type)
18110
18880
  );
@@ -18189,8 +18959,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
18189
18959
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
18190
18960
  return;
18191
18961
  }
18192
- const dbPath = ci.dbPath || path33__default.default.join(this.workspacePath, ".cascade", "code-index.db");
18193
- fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
18962
+ const dbPath = ci.dbPath || path34__default.default.join(this.workspacePath, ".cascade", "code-index.db");
18963
+ fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
18194
18964
  const db = new Database2__default.default(dbPath);
18195
18965
  db.pragma("journal_mode = WAL");
18196
18966
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -18570,10 +19340,57 @@ ${input}` }],
18570
19340
  setFeedbackSource(source) {
18571
19341
  this.router.setFeedbackSource(source);
18572
19342
  }
18573
- /** True when a task stopped at the budget cap and can be resumed via /continue. */
19343
+ /**
19344
+ * Write a durable checkpoint for an interrupted run.
19345
+ *
19346
+ * Best-effort throughout: a run is already ending badly on every path that
19347
+ * calls this, and failing to record a resume file must not make that ending
19348
+ * worse. Nothing here is allowed to throw.
19349
+ */
19350
+ async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
19351
+ const inherited = this.resumedFrom?.completed ?? [];
19352
+ const merged = /* @__PURE__ */ new Map();
19353
+ for (const node of inherited) merged.set(node.id, node);
19354
+ for (const node of this.currentRunCompleted) {
19355
+ const existing = merged.get(node.id);
19356
+ if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
19357
+ merged.set(node.id, node);
19358
+ }
19359
+ const completed = [...merged.values()];
19360
+ const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
19361
+ const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
19362
+ if (!completed.length && !carriedPartial) return false;
19363
+ try {
19364
+ const saved = await this.resumeStore?.save({
19365
+ taskId,
19366
+ prompt: canonicalPrompt,
19367
+ reason,
19368
+ ...detail ? { detail } : {},
19369
+ partialOutput: carriedPartial,
19370
+ completed
19371
+ });
19372
+ this.wroteCheckpointThisRun = saved === true;
19373
+ return saved === true;
19374
+ } catch {
19375
+ return false;
19376
+ }
19377
+ }
19378
+ /** True when a task can be resumed via /continue (this process or a previous one). */
18574
19379
  hasResumableRun() {
18575
19380
  return this.lastInterruptedRun != null;
18576
19381
  }
19382
+ /**
19383
+ * True when a checkpoint from ANY process is resumable. Async because the
19384
+ * durable check touches disk; `hasResumableRun` remains the sync in-memory
19385
+ * answer for callers already on a hot path.
19386
+ */
19387
+ async hasDurableResume() {
19388
+ return await this.resumeStore?.latest() != null;
19389
+ }
19390
+ /** Interrupted runs available to resume, newest first. */
19391
+ async listResumableRuns() {
19392
+ return await this.resumeStore?.list() ?? [];
19393
+ }
18577
19394
  /**
18578
19395
  * Raise the per-run token budget for a resume and return the continuation
18579
19396
  * prompt (or null when nothing is resumable). Consumes the interrupted-run
@@ -18584,24 +19401,105 @@ ${input}` }],
18584
19401
  const last = this.lastInterruptedRun;
18585
19402
  if (!last) return null;
18586
19403
  this.lastInterruptedRun = void 0;
18587
- const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
19404
+ this.raiseBudgetForResume(opts.maxTokens);
19405
+ return this.buildResumePrompt(last.prompt, last.partialOutput, []);
19406
+ }
19407
+ /**
19408
+ * Durable counterpart to prepareResume: recovers a run interrupted in ANY
19409
+ * process, including one that crashed before it could return. Consumes the
19410
+ * checkpoint, so the same finished work is never restored into two runs.
19411
+ */
19412
+ async prepareDurableResume(opts = {}) {
19413
+ await this.resumeStore?.reclaimStale();
19414
+ const claimed = await this.resumeStore?.claim(opts.taskId);
19415
+ if (!claimed) return null;
19416
+ try {
19417
+ this.lastInterruptedRun = void 0;
19418
+ this.raiseBudgetForResume(opts.maxTokens);
19419
+ const prompt = this.buildResumePrompt(
19420
+ claimed.checkpoint.prompt,
19421
+ claimed.checkpoint.partialOutput,
19422
+ claimed.checkpoint.completed
19423
+ );
19424
+ this.resumedFrom = {
19425
+ prompt: claimed.checkpoint.prompt,
19426
+ completed: claimed.checkpoint.completed,
19427
+ partialOutput: claimed.checkpoint.partialOutput
19428
+ };
19429
+ this.claimedResumeId = claimed.claimId;
19430
+ return prompt;
19431
+ } catch (err) {
19432
+ await this.resumeStore?.release(claimed.claimId);
19433
+ throw err;
19434
+ }
19435
+ }
19436
+ /**
19437
+ * Discard the claimed checkpoint once the resumed run has taken ownership of
19438
+ * the work (it has produced its own checkpoint, or finished). Called by the
19439
+ * run loop; safe to call when nothing is claimed.
19440
+ */
19441
+ async settleClaimedResume(succeeded) {
19442
+ const claimId = this.claimedResumeId;
19443
+ if (!claimId) return;
19444
+ this.claimedResumeId = void 0;
19445
+ if (succeeded || this.wroteCheckpointThisRun) {
19446
+ await this.resumeStore?.settle(claimId);
19447
+ } else {
19448
+ await this.resumeStore?.release(claimId);
19449
+ }
19450
+ }
19451
+ /**
19452
+ * Hand back a claim taken by prepareDurableResume() when the caller never
19453
+ * gets as far as run(). The REPL prepares the continuation and submits it
19454
+ * separately, so a failure in between would otherwise strand the checkpoint
19455
+ * for the whole lease timeout before reclaimStale() could recover it.
19456
+ */
19457
+ async releaseClaimedResume() {
19458
+ const claimId = this.claimedResumeId;
19459
+ if (!claimId) return;
19460
+ this.claimedResumeId = void 0;
19461
+ await this.resumeStore?.release(claimId);
19462
+ }
19463
+ /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
19464
+ raiseBudgetForResume(maxTokens) {
19465
+ const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
18588
19466
  this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
18589
19467
  this.router.setMaxTokensPerRun(raised);
18590
- 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.
18591
-
18592
- Original task: ${last.prompt}` + (last.partialOutput ? `
18593
-
19468
+ }
19469
+ /**
19470
+ * The resume prompt. Completed sections are named explicitly rather than
19471
+ * gestured at: "do not recreate the files" is an instruction a planner can
19472
+ * only guess at, whereas a list of what is already done, and what it produced,
19473
+ * is something it can act on. That difference is the entire reason node
19474
+ * results are kept — a re-plan that cannot see the finished work will plan the
19475
+ * finished work again, and the user pays for it a second time.
19476
+ */
19477
+ buildResumePrompt(prompt, partialOutput, completed) {
19478
+ const done = summarizeCompleted(completed);
19479
+ return [
19480
+ "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.",
19481
+ "",
19482
+ `Original task: ${prompt}`,
19483
+ done ? `
19484
+ ${done}` : "",
19485
+ partialOutput ? `
18594
19486
  Partial result so far:
18595
- ${last.partialOutput}` : "");
19487
+ ${partialOutput}` : ""
19488
+ ].filter(Boolean).join("\n");
18596
19489
  }
18597
19490
  /**
18598
19491
  * Resume the last budget-capped task with a raised budget (SDK/headless).
18599
19492
  * Returns null when there is nothing to resume.
18600
19493
  */
18601
19494
  async resumeRun(opts = {}) {
18602
- const prompt = this.prepareResume(opts);
19495
+ const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
18603
19496
  if (!prompt) return null;
18604
- return this.run({ prompt });
19497
+ try {
19498
+ return await this.run({ prompt });
19499
+ } catch (err) {
19500
+ await this.releaseClaimedResume();
19501
+ throw err;
19502
+ }
18605
19503
  }
18606
19504
  getWorkspacePath() {
18607
19505
  return this.workspacePath;
@@ -18970,6 +19868,10 @@ ${prompt}` : prompt;
18970
19868
  const startMs = Date.now();
18971
19869
  const taskId = crypto.randomUUID();
18972
19870
  this.decisionLog = [];
19871
+ this.currentRunCompleted = [];
19872
+ this.wroteCheckpointThisRun = false;
19873
+ this.completedNormally = false;
19874
+ if (!this.claimedResumeId) this.resumedFrom = void 0;
18973
19875
  if (options.fastAnswer) {
18974
19876
  return this.runFastAnswer(options, startMs, taskId);
18975
19877
  }
@@ -19084,6 +19986,11 @@ ${prompt}` : prompt;
19084
19986
  });
19085
19987
  tier.on("log", (e) => this.emit("log", e));
19086
19988
  tier.on("tier:status", (e) => this.emit("tier:status", e));
19989
+ tier.on("section:complete", (e) => {
19990
+ if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
19991
+ this.currentRunCompleted.push(e);
19992
+ this.emit("section:complete", e);
19993
+ });
19087
19994
  tier.on("tool:call", (e) => this.emit("tool:call", e));
19088
19995
  tier.on("tool:result", (e) => this.emit("tool:result", e));
19089
19996
  tier.on("tool:approval-request", async (request) => {
@@ -19230,6 +20137,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
19230
20137
  if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
19231
20138
  const trip = runBreaker.reason();
19232
20139
  if (trip) {
20140
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
19233
20141
  finalOutput = [
19234
20142
  `\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
19235
20143
  "",
@@ -19242,6 +20150,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
19242
20150
  ${finalOutput}` : ""
19243
20151
  ].join("\n").trimEnd();
19244
20152
  }
20153
+ this.completedNormally = trip == null;
19245
20154
  } catch (err) {
19246
20155
  if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
19247
20156
  this.emit("run:cancelled", {
@@ -19249,6 +20158,13 @@ ${finalOutput}` : ""
19249
20158
  reason: err instanceof Error ? err.message : "Task cancelled",
19250
20159
  partialOutput: finalOutput || ""
19251
20160
  });
20161
+ await this.checkpointRun(
20162
+ taskId,
20163
+ options.prompt,
20164
+ finalOutput || "",
20165
+ "cancelled",
20166
+ err instanceof Error ? err.message : "Task cancelled"
20167
+ );
19252
20168
  runError = null;
19253
20169
  } else if (err instanceof Error && err.name === "BudgetExceededError") {
19254
20170
  this.emit("run:budget-exceeded", {
@@ -19257,9 +20173,17 @@ ${finalOutput}` : ""
19257
20173
  partialOutput: finalOutput || ""
19258
20174
  });
19259
20175
  this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
20176
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
19260
20177
  if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
19261
20178
  runError = null;
19262
20179
  } else {
20180
+ await this.checkpointRun(
20181
+ taskId,
20182
+ options.prompt,
20183
+ finalOutput || "",
20184
+ "error",
20185
+ err instanceof Error ? err.message : String(err)
20186
+ );
19263
20187
  runError = err;
19264
20188
  throw err;
19265
20189
  }
@@ -19268,6 +20192,10 @@ ${finalOutput}` : ""
19268
20192
  escalator.cancelAllPending();
19269
20193
  } catch {
19270
20194
  }
20195
+ try {
20196
+ await this.settleClaimedResume(this.completedNormally);
20197
+ } catch {
20198
+ }
19271
20199
  try {
19272
20200
  this.releasePendingEscalations();
19273
20201
  } catch {
@@ -19471,14 +20399,14 @@ ${finalOutput}` : ""
19471
20399
  init_constants();
19472
20400
  var CLOUD_SESSION_FILE = "cloud-session.json";
19473
20401
  function globalDir(homeDir = os10__default.default.homedir()) {
19474
- return path33__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
20402
+ return path34__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
19475
20403
  }
19476
20404
  function cloudSessionPath(dir = globalDir()) {
19477
- return path33__default.default.join(dir, CLOUD_SESSION_FILE);
20405
+ return path34__default.default.join(dir, CLOUD_SESSION_FILE);
19478
20406
  }
19479
20407
  function loadCloudSession(dir = globalDir()) {
19480
20408
  try {
19481
- const raw = fs30__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
20409
+ const raw = fs31__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
19482
20410
  const parsed = JSON.parse(raw);
19483
20411
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
19484
20412
  return parsed;
@@ -19487,17 +20415,17 @@ function loadCloudSession(dir = globalDir()) {
19487
20415
  }
19488
20416
  }
19489
20417
  function saveCloudSession(session, dir = globalDir()) {
19490
- fs30__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
20418
+ fs31__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
19491
20419
  const file = cloudSessionPath(dir);
19492
- fs30__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20420
+ fs31__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
19493
20421
  try {
19494
- fs30__default.default.chmodSync(file, 384);
20422
+ fs31__default.default.chmodSync(file, 384);
19495
20423
  } catch {
19496
20424
  }
19497
20425
  }
19498
20426
  function clearCloudSession(dir = globalDir()) {
19499
20427
  try {
19500
- fs30__default.default.rmSync(cloudSessionPath(dir));
20428
+ fs31__default.default.rmSync(cloudSessionPath(dir));
19501
20429
  } catch {
19502
20430
  }
19503
20431
  }
@@ -19669,16 +20597,16 @@ var CloudClient = class _CloudClient {
19669
20597
  return (await this.refresh()).accessToken;
19670
20598
  }
19671
20599
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
19672
- async authed(path38, init = {}) {
19673
- const call = (token) => fetch(this.url(path38), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
20600
+ async authed(path39, init = {}) {
20601
+ const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
19674
20602
  let res = await call(await this.accessToken());
19675
20603
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
19676
20604
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
19677
20605
  const text = await res.text();
19678
20606
  return text ? JSON.parse(text) : {};
19679
20607
  }
19680
- authedGet(path38) {
19681
- return this.authed(path38);
20608
+ authedGet(path39) {
20609
+ return this.authed(path39);
19682
20610
  }
19683
20611
  async fetchMe(token) {
19684
20612
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -20458,7 +21386,8 @@ function AgentTreeInternal({
20458
21386
  ] }),
20459
21387
  node.status === "COMPLETED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.success, children: " \u2714" }),
20460
21388
  node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" }),
20461
- node.status === "ESCALATED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.warning, children: " \u25B2" })
21389
+ node.status === "ESCALATED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.warning, children: " \u25B2" }),
21390
+ node.status === "BLOCKED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: " \u292B skipped" })
20462
21391
  ] }, `t2-${node.id}-${i}`);
20463
21392
  }
20464
21393
  if (row.type === "t3") {
@@ -20483,7 +21412,8 @@ function AgentTreeInternal({
20483
21412
  /* @__PURE__ */ jsxRuntime.jsx(Spinner__default.default, { type: "dots" })
20484
21413
  ] }),
20485
21414
  node.status === "COMPLETED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.success, children: " \u2714" }),
20486
- node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" })
21415
+ node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" }),
21416
+ node.status === "BLOCKED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: " \u292B skipped" })
20487
21417
  ] }, `t3-${node.id}-${i}`);
20488
21418
  }
20489
21419
  return null;
@@ -21453,9 +22383,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21453
22383
  }
21454
22384
  } catch {
21455
22385
  }
21456
- const configPath = path33__default.default.join(workspacePath, ".cascade", "config.json");
22386
+ const configPath = path34__default.default.join(workspacePath, ".cascade", "config.json");
21457
22387
  try {
21458
- await fs11__default.default.mkdir(path33__default.default.dirname(configPath), { recursive: true });
22388
+ await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
21459
22389
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
21460
22390
  } catch (err) {
21461
22391
  const msg = err instanceof Error ? err.message : String(err);
@@ -21482,7 +22412,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21482
22412
  }, []);
21483
22413
  const recordNodeEvent = React2.useCallback((event) => {
21484
22414
  const existing = treeNodesRef.current.get(event.tierId);
21485
- const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, children: [] };
22415
+ const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, nodeId: event.nodeId ?? existing?.nodeId, dependsOn: event.dependsOn ?? existing?.dependsOn, waveId: event.waveId ?? existing?.waveId, blockedBy: event.blockedBy ?? existing?.blockedBy, children: [] };
21486
22416
  treeNodesRef.current.set(event.tierId, node);
21487
22417
  const store = storeRef.current;
21488
22418
  if (store) {
@@ -21507,9 +22437,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21507
22437
  if (msg.includes("non-text parts")) return;
21508
22438
  originalLog(...args);
21509
22439
  };
21510
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
22440
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
21511
22441
  storeRef.current = store;
21512
- globalStoreRef.current = new MemoryStore(path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
22442
+ globalStoreRef.current = new MemoryStore(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
21513
22443
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
21514
22444
  setIdentities(identityRows);
21515
22445
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -21577,7 +22507,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21577
22507
  onThemeChange: (name) => setTheme(getTheme(name)),
21578
22508
  onExport: async (fmt) => {
21579
22509
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
21580
- const exportPath = path33__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
22510
+ const exportPath = path34__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
21581
22511
  if (fmt === "json") {
21582
22512
  await fs11__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
21583
22513
  } else {
@@ -21819,11 +22749,21 @@ ${lastUser.content}`;
21819
22749
  onContinue: async (args) => {
21820
22750
  const cascade = cascadeRef.current;
21821
22751
  if (!cascade) return "Not ready yet.";
21822
- if (!cascade.hasResumableRun()) return "Nothing to continue \u2014 no task was stopped by the budget cap.";
22752
+ if (state.isExecuting) {
22753
+ return "A task is already running \u2014 wait for it to finish (or press Esc to stop it) before using /continue.";
22754
+ }
21823
22755
  const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
21824
- const prompt = cascade.prepareResume(Number.isFinite(n) && n > 0 ? { maxTokens: n } : {});
21825
- if (!prompt) return "Nothing to continue.";
21826
- await handleSubmit(prompt);
22756
+ const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
22757
+ const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
22758
+ if (!prompt) {
22759
+ return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
22760
+ }
22761
+ try {
22762
+ await handleSubmit(prompt);
22763
+ } catch (err) {
22764
+ await cascade.releaseClaimedResume();
22765
+ throw err;
22766
+ }
21827
22767
  return "";
21828
22768
  }
21829
22769
  });
@@ -22475,7 +23415,7 @@ function stringifySlashOutput(val) {
22475
23415
  }
22476
23416
  async function searchSessionsAndMessages(query, workspacePath) {
22477
23417
  if (!query) return "Usage: /search <query>";
22478
- const dbPath = path33__default.default.join(workspacePath, CASCADE_DB_FILE);
23418
+ const dbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
22479
23419
  try {
22480
23420
  await fs11__default.default.access(dbPath);
22481
23421
  } catch {
@@ -22509,7 +23449,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
22509
23449
  async function diagnoseRuntime(config, workspacePath) {
22510
23450
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
22511
23451
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
22512
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
23452
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
22513
23453
  try {
22514
23454
  const sessions2 = store.listSessions(void 0, 3);
22515
23455
  return [
@@ -22528,7 +23468,7 @@ async function diagnoseRuntime(config, workspacePath) {
22528
23468
  }
22529
23469
  async function showRecentLogs(args, workspacePath) {
22530
23470
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
22531
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
23471
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
22532
23472
  try {
22533
23473
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
22534
23474
  if (!logs.length) return "No recent runtime logs.";
@@ -22540,7 +23480,7 @@ async function showRecentLogs(args, workspacePath) {
22540
23480
  async function loadSessionSnapshot(args, workspacePath) {
22541
23481
  const sessionId = args[0];
22542
23482
  if (!sessionId) return "Usage: /resume <sessionId>";
22543
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
23483
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
22544
23484
  try {
22545
23485
  const session = store.getSession(sessionId);
22546
23486
  if (!session) return `Session not found: ${sessionId}`;
@@ -22968,9 +23908,9 @@ function SetupWizard({ workspacePath, onComplete }) {
22968
23908
  ...anyAuto ? { cascadeAuto: true } : {}
22969
23909
  };
22970
23910
  const config = CascadeConfigSchema.parse(rawConfig);
22971
- const configDir = path33__default.default.join(workspacePath, ".cascade");
23911
+ const configDir = path34__default.default.join(workspacePath, ".cascade");
22972
23912
  await fs11__default.default.mkdir(configDir, { recursive: true });
22973
- const configPath = path33__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
23913
+ const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
22974
23914
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
22975
23915
  savedConfigRef.current = config;
22976
23916
  dispatchRef.current({ type: "GO_DONE" });
@@ -23312,14 +24252,14 @@ function printTelemetryBanner() {
23312
24252
  async function initCommand(workspacePath = process.cwd()) {
23313
24253
  const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
23314
24254
  try {
23315
- const configDir = path33__default.default.join(workspacePath, ".cascade");
24255
+ const configDir = path34__default.default.join(workspacePath, ".cascade");
23316
24256
  await fs11__default.default.mkdir(configDir, { recursive: true });
23317
- const mdPath = path33__default.default.join(workspacePath, "CASCADE.md");
24257
+ const mdPath = path34__default.default.join(workspacePath, "CASCADE.md");
23318
24258
  if (!await fileExists(mdPath)) {
23319
24259
  await createDefaultCascadeMd(workspacePath);
23320
24260
  spin.succeed(chalk7__default.default.green("Created CASCADE.md"));
23321
24261
  }
23322
- const ignorePath = path33__default.default.join(workspacePath, ".cascadeignore");
24262
+ const ignorePath = path34__default.default.join(workspacePath, ".cascadeignore");
23323
24263
  if (!await fileExists(ignorePath)) {
23324
24264
  await createDefaultIgnoreFile(workspacePath);
23325
24265
  spin.succeed(chalk7__default.default.green("Created .cascadeignore"));
@@ -23328,7 +24268,7 @@ async function initCommand(workspacePath = process.cwd()) {
23328
24268
  console.log();
23329
24269
  console.log(chalk7__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
23330
24270
  console.log();
23331
- const configPath = path33__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
24271
+ const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
23332
24272
  if (await fileExists(configPath)) {
23333
24273
  console.log(chalk7__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
23334
24274
  console.log();
@@ -23386,7 +24326,7 @@ function fromEnv(env) {
23386
24326
  return out;
23387
24327
  }
23388
24328
  async function fromClaudeCode(home) {
23389
- const file = path33__default.default.join(home, ".claude", ".credentials.json");
24329
+ const file = path34__default.default.join(home, ".claude", ".credentials.json");
23390
24330
  const data = await readJson(file);
23391
24331
  if (!data) return [];
23392
24332
  const oauth = data["claudeAiOauth"];
@@ -23409,7 +24349,7 @@ async function fromClaudeCode(home) {
23409
24349
  return [];
23410
24350
  }
23411
24351
  async function fromCodex(home) {
23412
- const file = path33__default.default.join(home, ".codex", "auth.json");
24352
+ const file = path34__default.default.join(home, ".codex", "auth.json");
23413
24353
  const data = await readJson(file);
23414
24354
  if (!data) return [];
23415
24355
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -23432,7 +24372,7 @@ async function fromCodex(home) {
23432
24372
  return [];
23433
24373
  }
23434
24374
  async function fromGemini(home) {
23435
- const file = path33__default.default.join(home, ".gemini", "oauth_creds.json");
24375
+ const file = path34__default.default.join(home, ".gemini", "oauth_creds.json");
23436
24376
  const data = await readJson(file);
23437
24377
  const accessToken = str(data?.["access_token"]);
23438
24378
  if (!accessToken) return [];
@@ -23448,7 +24388,7 @@ async function fromGemini(home) {
23448
24388
  }
23449
24389
  async function fromCopilot(home) {
23450
24390
  for (const name of ["apps.json", "hosts.json"]) {
23451
- const file = path33__default.default.join(home, ".config", "github-copilot", name);
24391
+ const file = path34__default.default.join(home, ".config", "github-copilot", name);
23452
24392
  const data = await readJson(file);
23453
24393
  if (!data) continue;
23454
24394
  for (const value of Object.values(data)) {
@@ -23502,7 +24442,7 @@ async function doctorCommand() {
23502
24442
  checks.push({
23503
24443
  label: "Cascade config",
23504
24444
  ok: true,
23505
- detail: `Loaded ${path33__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
24445
+ detail: `Loaded ${path34__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
23506
24446
  });
23507
24447
  const providers = [
23508
24448
  { type: "anthropic", name: "Anthropic" },
@@ -23585,17 +24525,17 @@ async function doctorCommand() {
23585
24525
  const detail = c.detail ? chalk7__default.default.gray(` \u2014 ${c.detail}`) : "";
23586
24526
  console.log(`${icon} ${label}${detail}`);
23587
24527
  }
23588
- const failures = checks.filter((c) => !c.ok);
24528
+ const failures2 = checks.filter((c) => !c.ok);
23589
24529
  console.log();
23590
- if (failures.length === 0) {
24530
+ if (failures2.length === 0) {
23591
24531
  console.log(chalk7__default.default.green(" All checks passed!\n"));
23592
24532
  } else {
23593
- const critical = failures.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
24533
+ const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
23594
24534
  if (critical.length) {
23595
24535
  console.log(chalk7__default.default.yellow(` ${critical.length} issue(s) need attention.
23596
24536
  `));
23597
24537
  } else {
23598
- console.log(chalk7__default.default.gray(` ${failures.length} optional item(s) not configured.
24538
+ console.log(chalk7__default.default.gray(` ${failures2.length} optional item(s) not configured.
23599
24539
  `));
23600
24540
  }
23601
24541
  }
@@ -23613,7 +24553,7 @@ async function checkEndpoint(url) {
23613
24553
  }
23614
24554
  }
23615
24555
  async function indexCommand(dirPath) {
23616
- const workspace = path33__default.default.resolve(dirPath || process.cwd());
24556
+ const workspace = path34__default.default.resolve(dirPath || process.cwd());
23617
24557
  const cm = new ConfigManager(workspace);
23618
24558
  await cm.load();
23619
24559
  const config = cm.getConfig();
@@ -23624,8 +24564,8 @@ async function indexCommand(dirPath) {
23624
24564
  process.exitCode = 1;
23625
24565
  return;
23626
24566
  }
23627
- const dbPath = config.codeIndex?.dbPath || path33__default.default.join(workspace, ".cascade", "code-index.db");
23628
- fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
24567
+ const dbPath = config.codeIndex?.dbPath || path34__default.default.join(workspace, ".cascade", "code-index.db");
24568
+ fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
23629
24569
  const db = new Database2__default.default(dbPath);
23630
24570
  db.pragma("journal_mode = WAL");
23631
24571
  const complete = chatCompleterFromProviders(config.providers);
@@ -24114,7 +25054,7 @@ function openBrowser2(url) {
24114
25054
  }
24115
25055
  function storePathFor(name) {
24116
25056
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
24117
- return path33__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
25057
+ return path34__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
24118
25058
  }
24119
25059
  function defaultName(url) {
24120
25060
  try {
@@ -24316,8 +25256,14 @@ var DashboardSocket = class {
24316
25256
  emitCascadeEvent(ev) {
24317
25257
  this.io.emit("cascade:event", ev);
24318
25258
  }
24319
- emitTierStatus(tierId, role, status, sessionId, action) {
24320
- const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId };
25259
+ /**
25260
+ * `extra` exists because this signature is positional: every field the tier
25261
+ * emits that is not named here is silently dropped on this path, while the
25262
+ * other two forwarders spread the whole event through. That asymmetry is how
25263
+ * a field can reach the web dashboard from one code path and not another.
25264
+ */
25265
+ emitTierStatus(tierId, role, status, sessionId, action, extra) {
25266
+ const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
24321
25267
  this.io.emit("tier:status", payload);
24322
25268
  this.io.to(`session:${sessionId}`).emit("tier:status", payload);
24323
25269
  }
@@ -24616,7 +25562,7 @@ function aggregateCostStats(sessions2, opts = {}) {
24616
25562
  }
24617
25563
 
24618
25564
  // src/dashboard/server.ts
24619
- var __dirname$1 = path33__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
25565
+ var __dirname$1 = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
24620
25566
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
24621
25567
  var DashboardServer = class {
24622
25568
  app;
@@ -24896,14 +25842,14 @@ var DashboardServer = class {
24896
25842
  */
24897
25843
  persistConfig() {
24898
25844
  try {
24899
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
24900
- fs30__default.default.mkdirSync(path33__default.default.dirname(configPath), { recursive: true });
24901
- fs30__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
25845
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
25846
+ fs31__default.default.mkdirSync(path34__default.default.dirname(configPath), { recursive: true });
25847
+ fs31__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
24902
25848
  } catch (err) {
24903
25849
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
24904
25850
  }
24905
25851
  try {
24906
- saveGlobalCredentials(path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
25852
+ saveGlobalCredentials(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
24907
25853
  } catch (err) {
24908
25854
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
24909
25855
  }
@@ -24918,15 +25864,15 @@ var DashboardServer = class {
24918
25864
  resolveDashboardSecret() {
24919
25865
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
24920
25866
  if (fromConfig) return fromConfig;
24921
- const secretPath = path33__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
25867
+ const secretPath = path34__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
24922
25868
  try {
24923
- if (fs30__default.default.existsSync(secretPath)) {
24924
- const existing = fs30__default.default.readFileSync(secretPath, "utf-8").trim();
25869
+ if (fs31__default.default.existsSync(secretPath)) {
25870
+ const existing = fs31__default.default.readFileSync(secretPath, "utf-8").trim();
24925
25871
  if (existing.length >= 16) return existing;
24926
25872
  }
24927
25873
  const generated = crypto.randomUUID();
24928
- fs30__default.default.mkdirSync(path33__default.default.dirname(secretPath), { recursive: true });
24929
- fs30__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
25874
+ fs31__default.default.mkdirSync(path34__default.default.dirname(secretPath), { recursive: true });
25875
+ fs31__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
24930
25876
  if (this.config.dashboard.auth) {
24931
25877
  console.warn(
24932
25878
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -24953,7 +25899,7 @@ var DashboardServer = class {
24953
25899
  // ── Setup ─────────────────────────────────────
24954
25900
  getGlobalStore() {
24955
25901
  if (!this.globalStore) {
24956
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25902
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
24957
25903
  this.globalStore = new MemoryStore(globalDbPath);
24958
25904
  }
24959
25905
  return this.globalStore;
@@ -25333,12 +26279,12 @@ ${prompt}`;
25333
26279
  }
25334
26280
  }
25335
26281
  watchRuntimeChanges() {
25336
- const workspaceDbPath = path33__default.default.join(this.workspacePath, CASCADE_DB_FILE);
25337
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26282
+ const workspaceDbPath = path34__default.default.join(this.workspacePath, CASCADE_DB_FILE);
26283
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25338
26284
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
25339
26285
  for (const watchPath of watchPaths) {
25340
- if (!fs30__default.default.existsSync(watchPath)) continue;
25341
- fs30__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
26286
+ if (!fs31__default.default.existsSync(watchPath)) continue;
26287
+ fs31__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
25342
26288
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
25343
26289
  });
25344
26290
  }
@@ -25460,7 +26406,7 @@ ${prompt}`;
25460
26406
  const sessionId = req.params.id;
25461
26407
  this.store.deleteSession(sessionId);
25462
26408
  this.store.deleteRuntimeSession(sessionId);
25463
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26409
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25464
26410
  const globalStore = new MemoryStore(globalDbPath);
25465
26411
  try {
25466
26412
  globalStore.deleteRuntimeSession(sessionId);
@@ -25553,7 +26499,7 @@ ${prompt}`;
25553
26499
  });
25554
26500
  this.app.delete("/api/sessions", auth2, (req, res) => {
25555
26501
  const body = req.body;
25556
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26502
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25557
26503
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
25558
26504
  const globalStore = new MemoryStore(globalDbPath);
25559
26505
  try {
@@ -25576,7 +26522,7 @@ ${prompt}`;
25576
26522
  });
25577
26523
  this.app.delete("/api/runtime", auth2, (_req, res) => {
25578
26524
  this.store.deleteAllRuntimeNodes();
25579
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26525
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25580
26526
  const globalStore = new MemoryStore(globalDbPath);
25581
26527
  try {
25582
26528
  globalStore.deleteAllRuntimeNodes();
@@ -25662,12 +26608,12 @@ ${prompt}`;
25662
26608
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
25663
26609
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
25664
26610
  try {
25665
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
25666
- const existing = fs30__default.default.existsSync(configPath) ? JSON.parse(fs30__default.default.readFileSync(configPath, "utf-8")) : {};
26611
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
26612
+ const existing = fs31__default.default.existsSync(configPath) ? JSON.parse(fs31__default.default.readFileSync(configPath, "utf-8")) : {};
25667
26613
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
25668
26614
  const tmp = configPath + ".tmp";
25669
- fs30__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
25670
- fs30__default.default.renameSync(tmp, configPath);
26615
+ fs31__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
26616
+ fs31__default.default.renameSync(tmp, configPath);
25671
26617
  res.json({ ok: true });
25672
26618
  } catch (err) {
25673
26619
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -25695,7 +26641,7 @@ ${prompt}`;
25695
26641
  this.app.get("/api/runtime", auth2, (req, res) => {
25696
26642
  const scope = req.query["scope"] ?? "workspace";
25697
26643
  if (scope === "global") {
25698
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26644
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25699
26645
  const globalStore = new MemoryStore(globalDbPath);
25700
26646
  try {
25701
26647
  res.json({
@@ -26036,13 +26982,13 @@ ${prompt}`;
26036
26982
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
26037
26983
  }
26038
26984
  });
26039
- const prodPath = path33__default.default.resolve(__dirname$1, "../web/dist");
26040
- const devPath = path33__default.default.resolve(__dirname$1, "../../web/dist");
26041
- const webDistPath = fs30__default.default.existsSync(prodPath) ? prodPath : devPath;
26042
- if (fs30__default.default.existsSync(webDistPath)) {
26985
+ const prodPath = path34__default.default.resolve(__dirname$1, "../web/dist");
26986
+ const devPath = path34__default.default.resolve(__dirname$1, "../../web/dist");
26987
+ const webDistPath = fs31__default.default.existsSync(prodPath) ? prodPath : devPath;
26988
+ if (fs31__default.default.existsSync(webDistPath)) {
26043
26989
  this.app.use(express__default.default.static(webDistPath));
26044
26990
  this.app.get("*", (_req, res) => {
26045
- res.sendFile(path33__default.default.join(webDistPath, "index.html"));
26991
+ res.sendFile(path34__default.default.join(webDistPath, "index.html"));
26046
26992
  });
26047
26993
  } else {
26048
26994
  this.app.get("/", (_req, res) => {
@@ -26063,7 +27009,7 @@ init_constants();
26063
27009
  async function dashboardCommand(config, workspacePath = process.cwd()) {
26064
27010
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
26065
27011
  const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
26066
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
27012
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26067
27013
  const server = new DashboardServer(config, store, workspacePath);
26068
27014
  server.watchRuntimeChanges();
26069
27015
  const gThis = globalThis;
@@ -26095,7 +27041,7 @@ init_constants();
26095
27041
  function makeIdentityCommand(workspacePath = process.cwd()) {
26096
27042
  const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
26097
27043
  identity.command("list").description("List all available identities").action(() => {
26098
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
27044
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26099
27045
  try {
26100
27046
  const identities = store.listIdentities();
26101
27047
  console.log(chalk7__default.default.bold("\n Identities:"));
@@ -26115,7 +27061,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
26115
27061
  }
26116
27062
  });
26117
27063
  identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
26118
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
27064
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26119
27065
  try {
26120
27066
  if (options.default) {
26121
27067
  const existingDefault = store.getDefaultIdentity();
@@ -26141,7 +27087,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
26141
27087
  }
26142
27088
  });
26143
27089
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
26144
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
27090
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26145
27091
  try {
26146
27092
  const identities = store.listIdentities();
26147
27093
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -26290,8 +27236,8 @@ async function exportCommand(options = {}) {
26290
27236
  let store;
26291
27237
  try {
26292
27238
  const workspacePath = options.workspacePath ?? process.cwd();
26293
- const workspaceDbPath = path33__default.default.join(workspacePath, CASCADE_DB_FILE);
26294
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
27239
+ const workspaceDbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
27240
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
26295
27241
  let dbPath = globalDbPath;
26296
27242
  try {
26297
27243
  await fs11__default.default.access(workspaceDbPath);
@@ -26333,7 +27279,7 @@ async function exportCommand(options = {}) {
26333
27279
  const ext = format === "json" ? ".json" : ".md";
26334
27280
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
26335
27281
  const defaultFile = `cascade-export-${safeName}${ext}`;
26336
- const outPath = options.output ? path33__default.default.resolve(options.output) : path33__default.default.join(process.cwd(), defaultFile);
27282
+ const outPath = options.output ? path34__default.default.resolve(options.output) : path34__default.default.join(process.cwd(), defaultFile);
26337
27283
  await fs11__default.default.writeFile(outPath, content, "utf-8");
26338
27284
  spin.succeed(chalk7__default.default.green(`Exported to ${chalk7__default.default.white(outPath)}`));
26339
27285
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -26561,11 +27507,11 @@ async function statsCommand() {
26561
27507
  dotenv__default.default.config();
26562
27508
  function warnIfBuildIsStale() {
26563
27509
  try {
26564
- const here = path33__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
27510
+ const here = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
26565
27511
  for (const rel of ["..", "../.."]) {
26566
- const pkgPath = path33__default.default.join(here, rel, "package.json");
26567
- if (!fs30__default.default.existsSync(pkgPath)) continue;
26568
- const pkg = JSON.parse(fs30__default.default.readFileSync(pkgPath, "utf-8"));
27512
+ const pkgPath = path34__default.default.join(here, rel, "package.json");
27513
+ if (!fs31__default.default.existsSync(pkgPath)) continue;
27514
+ const pkg = JSON.parse(fs31__default.default.readFileSync(pkgPath, "utf-8"));
26569
27515
  if (pkg.name !== "cascade-ai") continue;
26570
27516
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
26571
27517
  console.error(