cascade-ai 0.12.24 → 0.13.2

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
@@ -8,19 +8,19 @@ var http = require('http');
8
8
  var https = require('https');
9
9
  var zlib = require('zlib');
10
10
  var stream = require('stream');
11
+ var Database2 = require('better-sqlite3');
12
+ var path25 = require('path');
13
+ var fs23 = require('fs');
14
+ var os8 = require('os');
15
+ var crypto = require('crypto');
11
16
  var ink = require('ink');
12
17
  var commander = require('commander');
13
18
  var React2 = require('react');
14
19
  var chalk9 = require('chalk');
15
20
  var dotenv = require('dotenv');
16
- var fs21 = require('fs');
17
- var path23 = require('path');
18
21
  var url = require('url');
19
22
  var fs9 = require('fs/promises');
20
- var os6 = require('os');
21
- var crypto = require('crypto');
22
23
  var _ignoreModule = require('ignore');
23
- var Database = require('better-sqlite3');
24
24
  var zod = require('zod');
25
25
  var child_process = require('child_process');
26
26
  var jsxRuntime = require('react/jsx-runtime');
@@ -71,16 +71,16 @@ var dns__default = /*#__PURE__*/_interopDefault(dns);
71
71
  var http__default = /*#__PURE__*/_interopDefault(http);
72
72
  var https__default = /*#__PURE__*/_interopDefault(https);
73
73
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
74
+ var Database2__default = /*#__PURE__*/_interopDefault(Database2);
75
+ var path25__default = /*#__PURE__*/_interopDefault(path25);
76
+ var fs23__default = /*#__PURE__*/_interopDefault(fs23);
77
+ var os8__default = /*#__PURE__*/_interopDefault(os8);
78
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
74
79
  var React2__default = /*#__PURE__*/_interopDefault(React2);
75
80
  var chalk9__default = /*#__PURE__*/_interopDefault(chalk9);
76
81
  var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
77
- var fs21__default = /*#__PURE__*/_interopDefault(fs21);
78
- var path23__default = /*#__PURE__*/_interopDefault(path23);
79
82
  var fs9__default = /*#__PURE__*/_interopDefault(fs9);
80
- var os6__default = /*#__PURE__*/_interopDefault(os6);
81
- var crypto__default = /*#__PURE__*/_interopDefault(crypto);
82
83
  var _ignoreModule__namespace = /*#__PURE__*/_interopNamespace(_ignoreModule);
83
- var Database__default = /*#__PURE__*/_interopDefault(Database);
84
84
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
85
85
  var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
86
86
  var dns2__default = /*#__PURE__*/_interopDefault(dns2);
@@ -109,7 +109,7 @@ var __export = (target, all) => {
109
109
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
110
110
  var init_constants = __esm({
111
111
  "src/constants.ts"() {
112
- CASCADE_VERSION = "0.12.24";
112
+ CASCADE_VERSION = "0.13.2";
113
113
  CASCADE_CONFIG_FILE = ".cascade/config.json";
114
114
  CASCADE_DB_FILE = ".cascade/memory.db";
115
115
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -1576,6 +1576,149 @@ var init_openai_compatible = __esm({
1576
1576
  }
1577
1577
  });
1578
1578
 
1579
+ // src/core/audit/audit-logger.ts
1580
+ var audit_logger_exports = {};
1581
+ __export(audit_logger_exports, {
1582
+ AuditLogger: () => AuditLogger2
1583
+ });
1584
+ var AuditLogger2;
1585
+ var init_audit_logger = __esm({
1586
+ "src/core/audit/audit-logger.ts"() {
1587
+ AuditLogger2 = class {
1588
+ constructor(workspacePath, debugMode = false) {
1589
+ this.workspacePath = workspacePath;
1590
+ this.debugMode = debugMode;
1591
+ const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
1592
+ if (!fs23__default.default.existsSync(cascadeDir)) {
1593
+ fs23__default.default.mkdirSync(cascadeDir, { recursive: true });
1594
+ }
1595
+ this.keyPath = path25__default.default.join(cascadeDir, "audit_log.key");
1596
+ this.dbPath = path25__default.default.join(cascadeDir, "audit_log.db");
1597
+ this.initEncryptionKey();
1598
+ this.db = new Database2__default.default(this.dbPath);
1599
+ this.db.pragma("journal_mode = WAL");
1600
+ this.db.exec(`
1601
+ CREATE TABLE IF NOT EXISTS audit_logs (
1602
+ id TEXT PRIMARY KEY,
1603
+ timestamp TEXT NOT NULL,
1604
+ event_type TEXT NOT NULL,
1605
+ tier_id TEXT NOT NULL,
1606
+ encrypted_payload TEXT NOT NULL,
1607
+ prev_hash TEXT NOT NULL DEFAULT '',
1608
+ hash TEXT NOT NULL DEFAULT ''
1609
+ )
1610
+ `);
1611
+ const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
1612
+ if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
1613
+ if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
1614
+ }
1615
+ workspacePath;
1616
+ debugMode;
1617
+ db;
1618
+ keyPath;
1619
+ dbPath;
1620
+ encryptionKey;
1621
+ /** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
1622
+ chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
1623
+ return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
1624
+ }
1625
+ initEncryptionKey() {
1626
+ if (fs23__default.default.existsSync(this.keyPath)) {
1627
+ this.encryptionKey = fs23__default.default.readFileSync(this.keyPath);
1628
+ } else {
1629
+ this.encryptionKey = crypto__default.default.randomBytes(32);
1630
+ fs23__default.default.writeFileSync(this.keyPath, this.encryptionKey);
1631
+ }
1632
+ }
1633
+ encrypt(text) {
1634
+ const iv = crypto__default.default.randomBytes(16);
1635
+ const cipher = crypto__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
1636
+ let encrypted = cipher.update(text, "utf8", "hex");
1637
+ encrypted += cipher.final("hex");
1638
+ const authTag = cipher.getAuthTag().toString("hex");
1639
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
1640
+ }
1641
+ decrypt(text) {
1642
+ const parts = text.split(":");
1643
+ if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
1644
+ const [ivHex, authTagHex, encryptedHex] = parts;
1645
+ const iv = Buffer.from(ivHex, "hex");
1646
+ const authTag = Buffer.from(authTagHex, "hex");
1647
+ const decipher = crypto__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
1648
+ decipher.setAuthTag(authTag);
1649
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
1650
+ decrypted += decipher.final("utf8");
1651
+ return decrypted;
1652
+ }
1653
+ logEvent(eventType, tierId, payloadObj) {
1654
+ const id = crypto__default.default.randomUUID();
1655
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1656
+ const payload = JSON.stringify(payloadObj);
1657
+ const encryptedPayload = this.encrypt(payload);
1658
+ const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
1659
+ const prevHash = last?.hash ?? "";
1660
+ const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
1661
+ const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
1662
+ stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
1663
+ this.dumpDebugIfNeeded();
1664
+ return id;
1665
+ }
1666
+ /**
1667
+ * Recompute the whole hash chain in insertion (rowid) order. Any edited,
1668
+ * removed, or reordered row makes every later hash mismatch. Rows written
1669
+ * before the chain existed (empty hash) fail verification too — integrity
1670
+ * can only be claimed for chained history.
1671
+ */
1672
+ verifyChain() {
1673
+ const rows = this.db.prepare(
1674
+ "SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
1675
+ ).all();
1676
+ let prevHash = "";
1677
+ for (const row of rows) {
1678
+ const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
1679
+ if (row.prev_hash !== prevHash || row.hash !== expected) {
1680
+ return { ok: false, entries: rows.length, firstBadRow: row.rowid };
1681
+ }
1682
+ prevHash = row.hash;
1683
+ }
1684
+ return { ok: true, entries: rows.length };
1685
+ }
1686
+ getAllLogs() {
1687
+ const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
1688
+ const rows = stmt.all();
1689
+ return rows.map((row) => {
1690
+ let payload = "";
1691
+ try {
1692
+ payload = this.decrypt(row.encrypted_payload);
1693
+ } catch (err) {
1694
+ payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
1695
+ }
1696
+ return {
1697
+ id: row.id,
1698
+ timestamp: row.timestamp,
1699
+ eventType: row.event_type,
1700
+ tierId: row.tier_id,
1701
+ payload
1702
+ };
1703
+ });
1704
+ }
1705
+ dumpDebugIfNeeded() {
1706
+ if (!this.debugMode) return;
1707
+ try {
1708
+ const dumpPath = path25__default.default.join(os8__default.default.tmpdir(), "cascade_audit_logs_debug.json");
1709
+ const entries = this.getAllLogs();
1710
+ fs23__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
1711
+ } catch (err) {
1712
+ console.error("Failed to dump debug audit logs", err);
1713
+ }
1714
+ }
1715
+ close() {
1716
+ this.db.close();
1717
+ }
1718
+ };
1719
+ }
1720
+ });
1721
+
1579
1722
  // src/cli/index.ts
1580
1723
  init_constants();
1581
1724
  var ALGORITHM = "aes-256-gcm";
@@ -1619,7 +1762,7 @@ var Keystore = class {
1619
1762
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
1620
1763
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
1621
1764
  this.backend = "keytar";
1622
- if (password && fs21__default.default.existsSync(this.storePath)) {
1765
+ if (password && fs23__default.default.existsSync(this.storePath)) {
1623
1766
  try {
1624
1767
  const fileEntries = this.decryptFile(password);
1625
1768
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -1638,7 +1781,7 @@ var Keystore = class {
1638
1781
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
1639
1782
  );
1640
1783
  }
1641
- if (!fs21__default.default.existsSync(this.storePath)) {
1784
+ if (!fs23__default.default.existsSync(this.storePath)) {
1642
1785
  const salt = crypto__default.default.randomBytes(SALT_LEN);
1643
1786
  this.masterKey = this.deriveKey(password, salt);
1644
1787
  this.writeWithSalt({}, salt);
@@ -1652,7 +1795,7 @@ var Keystore = class {
1652
1795
  }
1653
1796
  /** Synchronous legacy unlock kept for AES-only environments. */
1654
1797
  unlockSync(password) {
1655
- if (!fs21__default.default.existsSync(this.storePath)) {
1798
+ if (!fs23__default.default.existsSync(this.storePath)) {
1656
1799
  const salt = crypto__default.default.randomBytes(SALT_LEN);
1657
1800
  this.masterKey = this.deriveKey(password, salt);
1658
1801
  this.writeWithSalt({}, salt);
@@ -1710,7 +1853,7 @@ var Keystore = class {
1710
1853
  }
1711
1854
  }
1712
1855
  decryptFile(password, knownSalt) {
1713
- if (!fs21__default.default.existsSync(this.storePath)) return {};
1856
+ if (!fs23__default.default.existsSync(this.storePath)) return {};
1714
1857
  try {
1715
1858
  const { salt, ciphertext, iv, tag } = this.readRaw();
1716
1859
  const useSalt = knownSalt ?? salt;
@@ -1732,8 +1875,8 @@ var Keystore = class {
1732
1875
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1733
1876
  const tag = cipher.getAuthTag();
1734
1877
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
1735
- fs21__default.default.mkdirSync(path23__default.default.dirname(this.storePath), { recursive: true });
1736
- fs21__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1878
+ fs23__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
1879
+ fs23__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1737
1880
  }
1738
1881
  writeWithSalt(data, salt) {
1739
1882
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -1743,11 +1886,11 @@ var Keystore = class {
1743
1886
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1744
1887
  const tag = cipher.getAuthTag();
1745
1888
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
1746
- fs21__default.default.mkdirSync(path23__default.default.dirname(this.storePath), { recursive: true });
1747
- fs21__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1889
+ fs23__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
1890
+ fs23__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1748
1891
  }
1749
1892
  readRaw() {
1750
- const buf = fs21__default.default.readFileSync(this.storePath);
1893
+ const buf = fs23__default.default.readFileSync(this.storePath);
1751
1894
  let offset = 0;
1752
1895
  const salt = buf.subarray(offset, offset + SALT_LEN);
1753
1896
  offset += SALT_LEN;
@@ -1780,7 +1923,7 @@ var CascadeIgnore = class {
1780
1923
  ]);
1781
1924
  }
1782
1925
  async load(workspacePath) {
1783
- const filePath = path23__default.default.join(workspacePath, ".cascadeignore");
1926
+ const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
1784
1927
  try {
1785
1928
  const content = await fs9__default.default.readFile(filePath, "utf-8");
1786
1929
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -1791,7 +1934,7 @@ var CascadeIgnore = class {
1791
1934
  }
1792
1935
  isIgnored(filePath, workspacePath) {
1793
1936
  try {
1794
- const relative = workspacePath ? path23__default.default.relative(workspacePath, filePath) : filePath;
1937
+ const relative = workspacePath ? path25__default.default.relative(workspacePath, filePath) : filePath;
1795
1938
  return this.ig.ignores(relative);
1796
1939
  } catch {
1797
1940
  return false;
@@ -1802,7 +1945,7 @@ var CascadeIgnore = class {
1802
1945
  }
1803
1946
  };
1804
1947
  async function createDefaultIgnoreFile(workspacePath) {
1805
- const filePath = path23__default.default.join(workspacePath, ".cascadeignore");
1948
+ const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
1806
1949
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
1807
1950
  # Syntax identical to .gitignore
1808
1951
 
@@ -1832,7 +1975,7 @@ Thumbs.db
1832
1975
  await fs9__default.default.writeFile(filePath, content, "utf-8");
1833
1976
  }
1834
1977
  async function loadCascadeMd(workspacePath) {
1835
- const filePath = path23__default.default.join(workspacePath, "CASCADE.md");
1978
+ const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
1836
1979
  try {
1837
1980
  const raw = await fs9__default.default.readFile(filePath, "utf-8");
1838
1981
  return parseCascadeMd(raw);
@@ -1861,7 +2004,7 @@ ${raw.trim()}`;
1861
2004
  return { raw, sections, systemPrompt };
1862
2005
  }
1863
2006
  async function createDefaultCascadeMd(workspacePath) {
1864
- const filePath = path23__default.default.join(workspacePath, "CASCADE.md");
2007
+ const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
1865
2008
  const content = `# Cascade Project Instructions
1866
2009
 
1867
2010
  This file contains project-specific instructions for the Cascade AI agent.
@@ -1896,9 +2039,9 @@ List any areas the agent should not touch.
1896
2039
  var MemoryStore = class _MemoryStore {
1897
2040
  db;
1898
2041
  constructor(dbPath) {
1899
- fs21__default.default.mkdirSync(path23__default.default.dirname(dbPath), { recursive: true });
2042
+ fs23__default.default.mkdirSync(path25__default.default.dirname(dbPath), { recursive: true });
1900
2043
  try {
1901
- this.db = new Database__default.default(dbPath, { timeout: 5e3 });
2044
+ this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
1902
2045
  this.db.pragma("journal_mode = WAL");
1903
2046
  this.db.pragma("foreign_keys = ON");
1904
2047
  this.db.pragma("synchronous = NORMAL");
@@ -2743,7 +2886,8 @@ var WorkspaceConfigSchema = zod.z.object({
2743
2886
  cascadeMdPath: zod.z.string().default("CASCADE.md"),
2744
2887
  configPath: zod.z.string().default(".cascade/config.json"),
2745
2888
  keystorePath: zod.z.string().default(".cascade/keystore.enc"),
2746
- auditLogPath: zod.z.string().default(".cascade/audit.log")
2889
+ auditLogPath: zod.z.string().default(".cascade/audit.log"),
2890
+ debugWorldState: zod.z.boolean().default(false)
2747
2891
  });
2748
2892
  var CascadeConfigSchema = zod.z.object({
2749
2893
  version: zod.z.literal("1.0").default("1.0"),
@@ -2888,6 +3032,16 @@ var CascadeConfigSchema = zod.z.object({
2888
3032
  * 'parallel' / 'sequential' — force it.
2889
3033
  */
2890
3034
  t3Execution: zod.z.enum(["auto", "parallel", "sequential"]).default("auto"),
3035
+ /**
3036
+ * Per-path privacy tiers. A subtask touching a `local-only` path is forced
3037
+ * onto LOCAL models (never cloud) and its raw output is withheld from the
3038
+ * tiers above. Patterns use .gitignore syntax, like .cascadeignore.
3039
+ */
3040
+ privacy: zod.z.object({
3041
+ paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
3042
+ }).optional(),
3043
+ /** Routing controls — forceTier pins the root tier, bypassing the classifier. */
3044
+ routing: zod.z.object({ forceTier: zod.z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
2891
3045
  /**
2892
3046
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
2893
3047
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -2937,15 +3091,15 @@ var ConfigManager = class {
2937
3091
  globalDir;
2938
3092
  constructor(workspacePath = process.cwd()) {
2939
3093
  this.workspacePath = workspacePath;
2940
- this.globalDir = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
3094
+ this.globalDir = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR);
2941
3095
  }
2942
3096
  async load() {
2943
3097
  this.config = await this.loadConfig();
2944
3098
  this.ignore = new CascadeIgnore();
2945
3099
  await this.ignore.load(this.workspacePath);
2946
3100
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
2947
- this.keystore = new Keystore(path23__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
2948
- this.store = new MemoryStore(path23__default.default.join(this.workspacePath, CASCADE_DB_FILE));
3101
+ this.keystore = new Keystore(path25__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
3102
+ this.store = new MemoryStore(path25__default.default.join(this.workspacePath, CASCADE_DB_FILE));
2949
3103
  await this.injectEnvKeys();
2950
3104
  await this.ensureDefaultIdentity();
2951
3105
  }
@@ -2968,8 +3122,8 @@ var ConfigManager = class {
2968
3122
  return this.workspacePath;
2969
3123
  }
2970
3124
  async save() {
2971
- const configPath = path23__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
2972
- await fs9__default.default.mkdir(path23__default.default.dirname(configPath), { recursive: true });
3125
+ const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
3126
+ await fs9__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
2973
3127
  await fs9__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
2974
3128
  }
2975
3129
  async updateConfig(updates) {
@@ -2993,7 +3147,7 @@ var ConfigManager = class {
2993
3147
  return configProvider?.apiKey;
2994
3148
  }
2995
3149
  async loadConfig() {
2996
- const configPath = path23__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
3150
+ const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
2997
3151
  try {
2998
3152
  const raw = await fs9__default.default.readFile(configPath, "utf-8");
2999
3153
  return validateConfig(JSON.parse(raw));
@@ -3872,7 +4026,7 @@ init_constants();
3872
4026
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
3873
4027
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
3874
4028
  var FETCH_TIMEOUT_MS = 8e3;
3875
- var DEFAULT_CACHE_FILE = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4029
+ var DEFAULT_CACHE_FILE = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
3876
4030
  function normalizeModelId(id) {
3877
4031
  let s = id.toLowerCase();
3878
4032
  const slash = s.lastIndexOf("/");
@@ -3987,7 +4141,7 @@ var LiveDataProvider = class {
3987
4141
  }
3988
4142
  async saveCache() {
3989
4143
  try {
3990
- await fs9__default.default.mkdir(path23__default.default.dirname(this.opts.cacheFile), { recursive: true });
4144
+ await fs9__default.default.mkdir(path25__default.default.dirname(this.opts.cacheFile), { recursive: true });
3991
4145
  const cache = {
3992
4146
  fetchedAt: this.fetchedAt,
3993
4147
  snapshot: this.snapshot ?? void 0,
@@ -4117,7 +4271,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4117
4271
  costByTier: {},
4118
4272
  tokensByTier: {},
4119
4273
  inputTokensByTier: {},
4120
- outputTokensByTier: {}
4274
+ outputTokensByTier: {},
4275
+ costByFeature: {}
4121
4276
  };
4122
4277
  tierModels = /* @__PURE__ */ new Map();
4123
4278
  config;
@@ -4139,6 +4294,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4139
4294
  tpmLimiter;
4140
4295
  localQueue;
4141
4296
  taskAnalyzer;
4297
+ worldStateDB;
4298
+ privacyPaths;
4299
+ guidanceQueue;
4142
4300
  liveData;
4143
4301
  /** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
4144
4302
  originalTierModels;
@@ -4296,7 +4454,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4296
4454
  if (options.model && !requireVision) {
4297
4455
  this.ensureProvider(options.model, this.config.providers);
4298
4456
  }
4299
- const model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
4457
+ let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
4458
+ if (options.forceLocal && model && !this.isPrivateModel(model)) {
4459
+ const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
4460
+ if (!localModel) {
4461
+ throw new Error(
4462
+ "privacy.paths: this subtask touches a local-only path but no LOCAL model is available. Configure Ollama or an OpenAI-compatible endpoint on a loopback/private host, or remove the privacy policy."
4463
+ );
4464
+ }
4465
+ this.ensureProvider(localModel, this.config.providers);
4466
+ model = localModel;
4467
+ }
4300
4468
  if (!model) throw new Error(`No model available for tier ${tier}`);
4301
4469
  const provider = this.getProvider(model);
4302
4470
  if (!provider) throw new Error(`No provider for model ${model.id}`);
@@ -4368,7 +4536,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4368
4536
  if (!result || typeof result.content !== "string" || !result.usage) {
4369
4537
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
4370
4538
  }
4371
- this.recordStats(tier, model, result.usage);
4539
+ this.recordStats(tier, model, result.usage, options.featureTag);
4372
4540
  this.failover.recordSuccess(model.provider);
4373
4541
  return result;
4374
4542
  } catch (err) {
@@ -4473,6 +4641,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4473
4641
  setTaskAnalyzer(analyzer) {
4474
4642
  this.taskAnalyzer = analyzer;
4475
4643
  }
4644
+ setWorldStateDB(db) {
4645
+ this.worldStateDB = db;
4646
+ }
4647
+ getWorldStateDB() {
4648
+ return this.worldStateDB;
4649
+ }
4650
+ setPrivacyPaths(paths) {
4651
+ this.privacyPaths = paths;
4652
+ }
4653
+ getPrivacyPaths() {
4654
+ return this.privacyPaths;
4655
+ }
4656
+ setGuidanceQueue(queue) {
4657
+ this.guidanceQueue = queue;
4658
+ }
4659
+ getGuidanceQueue() {
4660
+ return this.guidanceQueue;
4661
+ }
4662
+ /**
4663
+ * "Private" = inference never leaves the user's machine/network: Ollama
4664
+ * models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
4665
+ * LM Studio) whose configured host is loopback or a private range. A cloud
4666
+ * OpenAI-compatible endpoint (public host) does NOT qualify.
4667
+ */
4668
+ isPrivateModel(model) {
4669
+ if (model.isLocal) return true;
4670
+ if (model.provider !== "openai-compatible") return false;
4671
+ const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
4672
+ if (!baseUrl) return false;
4673
+ try {
4674
+ const host = new URL(baseUrl).hostname.toLowerCase();
4675
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith(".local");
4676
+ } catch {
4677
+ return false;
4678
+ }
4679
+ }
4476
4680
  /**
4477
4681
  * Cascade Auto per-subtask routing: pick the benchmark-best model for a
4478
4682
  * specific subtask's text, scoped to the tier's eligible candidates. Returns
@@ -4496,7 +4700,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4496
4700
  costByTier: { ...this.stats.costByTier },
4497
4701
  tokensByTier: { ...this.stats.tokensByTier },
4498
4702
  inputTokensByTier: { ...this.stats.inputTokensByTier },
4499
- outputTokensByTier: { ...this.stats.outputTokensByTier }
4703
+ outputTokensByTier: { ...this.stats.outputTokensByTier },
4704
+ costByFeature: { ...this.stats.costByFeature }
4500
4705
  };
4501
4706
  }
4502
4707
  /**
@@ -4546,7 +4751,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4546
4751
  costByTier: {},
4547
4752
  tokensByTier: {},
4548
4753
  inputTokensByTier: {},
4549
- outputTokensByTier: {}
4754
+ outputTokensByTier: {},
4755
+ costByFeature: {}
4550
4756
  };
4551
4757
  this.sessionCostUsd = 0;
4552
4758
  this.budgetState = "ok";
@@ -4712,7 +4918,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4712
4918
  }
4713
4919
  return void 0;
4714
4920
  }
4715
- recordStats(tier, model, usage) {
4921
+ recordStats(tier, model, usage, featureTag) {
4716
4922
  this.stats.totalTokens += usage.totalTokens;
4717
4923
  this.stats.totalCostUsd += usage.estimatedCostUsd;
4718
4924
  this.sessionCostUsd += usage.estimatedCostUsd;
@@ -4722,6 +4928,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4722
4928
  this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
4723
4929
  this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
4724
4930
  this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
4931
+ if (featureTag) {
4932
+ this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
4933
+ }
4725
4934
  this.runTokens += usage.totalTokens;
4726
4935
  this.runCostUsd += usage.estimatedCostUsd;
4727
4936
  this.updateBudgetState();
@@ -4818,6 +5027,13 @@ var BaseTier = class extends EventEmitter__default.default {
4818
5027
  hierarchyContext = "";
4819
5028
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
4820
5029
  signal;
5030
+ /**
5031
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
5032
+ * Complex). Its own synthesis stream is the user-facing answer and is
5033
+ * tagged `primary` so the desktop renders it live — background workers,
5034
+ * which would interleave, are not tagged.
5035
+ */
5036
+ isPresenter = false;
4821
5037
  constructor(role, id, parentId) {
4822
5038
  super();
4823
5039
  this.role = role;
@@ -4825,6 +5041,10 @@ var BaseTier = class extends EventEmitter__default.default {
4825
5041
  this.parentId = parentId;
4826
5042
  this.label = this.id;
4827
5043
  }
5044
+ /** Mark this tier as the run's presenter (root tier). */
5045
+ setPresenter(on = true) {
5046
+ this.isPresenter = on;
5047
+ }
4828
5048
  getStatus() {
4829
5049
  return this.status;
4830
5050
  }
@@ -5259,6 +5479,8 @@ var T3Worker = class extends BaseTier {
5259
5479
  toolRegistry;
5260
5480
  context;
5261
5481
  assignment;
5482
+ /** True when this subtask matched a privacy.paths local-only pattern. */
5483
+ localOnlyMatch = false;
5262
5484
  peerSyncBuffer = [];
5263
5485
  store;
5264
5486
  audit;
@@ -5314,6 +5536,11 @@ var T3Worker = class extends BaseTier {
5314
5536
  this.taskId = taskId;
5315
5537
  this.setLabel(assignment.subtaskTitle);
5316
5538
  this.setStatus("ACTIVE");
5539
+ const privacy = this.router.getPrivacyPaths?.();
5540
+ this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
5541
+ if (this.localOnlyMatch) {
5542
+ this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
5543
+ }
5317
5544
  this.tools = this.toolRegistry.getToolDefinitions();
5318
5545
  if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
5319
5546
  this.tools = [...this.tools, {
@@ -5444,9 +5671,17 @@ Now execute your subtask using this context where relevant.`
5444
5671
  }
5445
5672
  const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
5446
5673
  if (reflectCfg.enabled) {
5447
- this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
5674
+ this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
5448
5675
  output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
5449
5676
  }
5677
+ const db = this.router.getWorldStateDB?.();
5678
+ if (db) {
5679
+ try {
5680
+ db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
5681
+ } catch (e) {
5682
+ this.log("Failed to write to World State DB");
5683
+ }
5684
+ }
5450
5685
  this.setStatus("COMPLETED", output);
5451
5686
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
5452
5687
  this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
@@ -5519,6 +5754,16 @@ Now execute your subtask using this context where relevant.`
5519
5754
  while (iterations < MAX_ITERATIONS) {
5520
5755
  iterations++;
5521
5756
  this.throwIfCancelled();
5757
+ const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
5758
+ for (const g of guidance) {
5759
+ this.log(`User intervention received: ${g.text}`);
5760
+ this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
5761
+ await this.context.addMessage({
5762
+ role: "user",
5763
+ content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
5764
+ ${g.text}`
5765
+ });
5766
+ }
5522
5767
  const options = {
5523
5768
  messages: this.context.getMessages(),
5524
5769
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -5527,13 +5772,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
5527
5772
  // Don't pass tools array when model can't use them natively
5528
5773
  tools: useTextTools ? void 0 : tools.length ? tools : void 0,
5529
5774
  maxTokens: 4096,
5530
- ...subtaskModel ? { model: subtaskModel } : {}
5775
+ ...subtaskModel ? { model: subtaskModel } : {},
5776
+ featureTag: this.assignment?.sectionTitle,
5777
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5531
5778
  };
5532
5779
  const result = await this.router.generate(
5533
5780
  "T3",
5534
5781
  options,
5535
5782
  (chunk) => {
5536
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
5783
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
5537
5784
  }
5538
5785
  );
5539
5786
  let effectiveToolCalls = result.toolCalls ?? [];
@@ -5691,8 +5938,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
5691
5938
  tierId: this.id,
5692
5939
  sessionId: this.taskId,
5693
5940
  requireApproval: false,
5694
- saveSnapshot: async (path28, content) => {
5695
- this.store?.addFileSnapshot(this.taskId, path28, content);
5941
+ saveSnapshot: async (path30, content) => {
5942
+ this.store?.addFileSnapshot(this.taskId, path30, content);
5696
5943
  },
5697
5944
  sendPeerSync: (to, syncType, content) => {
5698
5945
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -5862,7 +6109,7 @@ ${assignment.expectedOutput}`;
5862
6109
  const { promisify: promisify4 } = await import('util');
5863
6110
  const execAsync3 = promisify4(exec3);
5864
6111
  for (const artifactPath of artifactPaths) {
5865
- const absolutePath = path23__default.default.resolve(process.cwd(), artifactPath);
6112
+ const absolutePath = path25__default.default.resolve(process.cwd(), artifactPath);
5866
6113
  try {
5867
6114
  const stat = await fs9__default.default.stat(absolutePath);
5868
6115
  if (!stat.isFile()) {
@@ -5883,7 +6130,7 @@ ${assignment.expectedOutput}`;
5883
6130
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
5884
6131
  continue;
5885
6132
  }
5886
- const ext = path23__default.default.extname(absolutePath).toLowerCase();
6133
+ const ext = path25__default.default.extname(absolutePath).toLowerCase();
5887
6134
  try {
5888
6135
  if (ext === ".ts" || ext === ".tsx") {
5889
6136
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -5912,30 +6159,35 @@ ${stdout}`);
5912
6159
  * needed. Best-effort: any parse/error just keeps the current output.
5913
6160
  */
5914
6161
  async reflectAndImprove(assignment, output, maxRounds) {
5915
- const sys = this.systemPromptOverride + (this.hierarchyContext ? `
5916
-
5917
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
5918
6162
  let current = output;
5919
- for (let round = 0; round < Math.max(1, maxRounds); round++) {
5920
- try {
5921
- const verdict = await this.router.generate("T3", {
6163
+ try {
6164
+ for (let round = 0; round < Math.max(1, maxRounds); round++) {
6165
+ const verdictResult = await this.router.generate("T2", {
5922
6166
  messages: [{
5923
6167
  role: "user",
5924
- content: `Does this output FULLY achieve the goal \u2014 not just the literal task, but the intent behind it?
6168
+ content: `You are an independent critic reviewing another worker's output against its assignment.
5925
6169
 
5926
- Goal / expected: ${assignment.expectedOutput}
6170
+ Goal: ${assignment.expectedOutput}
5927
6171
  Subtask: ${assignment.description}
5928
-
5929
- Output:
6172
+ Current Output:
5930
6173
  ${current}
5931
6174
 
5932
- Reply with ONLY JSON: {"sufficient": true|false, "notes": "what is weak or missing if not sufficient"}`
6175
+ Is this output sufficient and correct? Respond with ONLY a JSON object:
6176
+ {"sufficient": true|false, "notes": "what is wrong or missing if false"}`
5933
6177
  }],
5934
- systemPrompt: sys,
5935
- maxTokens: 400
6178
+ systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
6179
+ maxTokens: 400,
6180
+ signal: this.signal,
6181
+ featureTag: assignment.sectionTitle,
6182
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5936
6183
  });
5937
- const parsed = JSON.parse(/\{[\s\S]*\}/.exec(verdict.content)?.[0] ?? "{}");
5938
- if (parsed.sufficient !== false) break;
6184
+ const match = /\{[\s\S]*\}/.exec(verdictResult.content);
6185
+ const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
6186
+ if (parsed.sufficient !== false) {
6187
+ this.log("T2-Critic approved output.");
6188
+ break;
6189
+ }
6190
+ this.log(`T2-Critic rejected output: ${parsed.notes}`);
5939
6191
  const improved = await this.router.generate("T3", {
5940
6192
  messages: [{
5941
6193
  role: "user",
@@ -5947,16 +6199,20 @@ Goal / expected: ${assignment.expectedOutput}
5947
6199
  Current output:
5948
6200
  ${current}`
5949
6201
  }],
5950
- systemPrompt: sys,
5951
- maxTokens: 4096
6202
+ systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
6203
+
6204
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6205
+ maxTokens: 4096,
6206
+ featureTag: assignment.sectionTitle,
6207
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5952
6208
  });
5953
6209
  const next = (improved.content ?? "").trim();
5954
6210
  if (!next) break;
5955
6211
  current = next;
5956
6212
  this.log("Reflection: revised output for better goal alignment.");
5957
- } catch {
5958
- break;
5959
6213
  }
6214
+ } catch (e) {
6215
+ this.log(`T2-Critic reflection failed: ${e}`);
5960
6216
  }
5961
6217
  return current;
5962
6218
  }
@@ -5977,7 +6233,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
5977
6233
  maxTokens: 500,
5978
6234
  systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
5979
6235
 
5980
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
6236
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6237
+ featureTag: assignment.sectionTitle,
6238
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5981
6239
  });
5982
6240
  try {
5983
6241
  const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
@@ -6072,6 +6330,7 @@ Begin execution now.`;
6072
6330
  issues,
6073
6331
  peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
6074
6332
  correctionAttempts,
6333
+ localOnly: this.localOnlyMatch || void 0,
6075
6334
  reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
6076
6335
  };
6077
6336
  }
@@ -6363,6 +6622,42 @@ var PeerBus = class extends EventEmitter__default.default {
6363
6622
  }
6364
6623
  };
6365
6624
 
6625
+ // src/core/audit/redaction.ts
6626
+ var RedactionLayer = class {
6627
+ // Regexes for common secrets/PII
6628
+ static RULES = [
6629
+ // IPv4 addresses (basic approximation)
6630
+ { pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
6631
+ // Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
6632
+ { pattern: /(?:\b(?:api_key|apikey|secret|token|password|bearer|auth|authorization)\b[^a-zA-Z0-9_]{1,4})([a-zA-Z0-9_\-\.]{16,})/gi, replacement: "$1[REDACTED_SECRET]" },
6633
+ // Email addresses
6634
+ { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
6635
+ // Phone numbers (simplistic)
6636
+ { pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
6637
+ // AWS Access Key ID
6638
+ { pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
6639
+ ];
6640
+ /**
6641
+ * Applies all redaction rules to the input string.
6642
+ */
6643
+ static redact(text) {
6644
+ if (!text) return text;
6645
+ let redacted = text;
6646
+ for (const rule of this.RULES) {
6647
+ if (rule.pattern.test(redacted)) {
6648
+ rule.pattern.lastIndex = 0;
6649
+ redacted = redacted.replace(rule.pattern, (match, p1, offset, str2) => {
6650
+ if (p1 && match.includes(p1) && p1 !== match) {
6651
+ return match.replace(p1, rule.replacement.replace("$1", ""));
6652
+ }
6653
+ return rule.replacement;
6654
+ });
6655
+ }
6656
+ }
6657
+ return redacted;
6658
+ }
6659
+ };
6660
+
6366
6661
  // src/core/tiers/t2-manager.ts
6367
6662
  var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
6368
6663
  Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
@@ -6591,7 +6886,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6591
6886
  try {
6592
6887
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
6593
6888
  if (!jsonMatch) throw new Error("No JSON array found");
6594
- return JSON.parse(jsonMatch[0]);
6889
+ const parsed = JSON.parse(jsonMatch[0]);
6890
+ return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
6595
6891
  } catch {
6596
6892
  return [{
6597
6893
  subtaskId: crypto.randomUUID(),
@@ -6601,6 +6897,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6601
6897
  constraints: assignment.constraints,
6602
6898
  peerT3Ids: [],
6603
6899
  parentT2: this.id,
6900
+ sectionTitle: assignment.sectionTitle,
6901
+ dependsOn: [],
6604
6902
  executionMode: "parallel"
6605
6903
  }];
6606
6904
  }
@@ -6720,6 +7018,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6720
7018
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
6721
7019
  const worker = workerMap.get(id);
6722
7020
  const result = await worker.execute(assignment, taskId, waveSignal);
7021
+ if (result.localOnly) {
7022
+ result.output = `[local-only path \u2014 output withheld by privacy policy; status: ${result.status}; checks passed: ${result.testResults.passed.length}/${result.testResults.checksRun.length || 0}]`;
7023
+ } else {
7024
+ if (typeof result.output === "string" && result.output) {
7025
+ result.output = RedactionLayer.redact(result.output);
7026
+ }
7027
+ }
7028
+ if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
6723
7029
  resultMap.set(id, result);
6724
7030
  return result;
6725
7031
  };
@@ -6935,6 +7241,7 @@ ${peerOutputs}` : "";
6935
7241
  chunkEnd++;
6936
7242
  }
6937
7243
  i = chunkEnd;
7244
+ const isLastChunk = chunkEnd >= completed.length;
6938
7245
  const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
6939
7246
  ${currentSummary ? `
6940
7247
  PREVIOUS SUMMARY SO FAR:
@@ -6944,6 +7251,7 @@ NEW OUTPUTS TO INTEGRATE:
6944
7251
  ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
6945
7252
  const messages = [{ role: "user", content: prompt }];
6946
7253
  try {
7254
+ const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
6947
7255
  const result = await this.router.generate("T2", {
6948
7256
  messages,
6949
7257
  systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
@@ -6951,7 +7259,7 @@ NEW OUTPUTS TO INTEGRATE:
6951
7259
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6952
7260
  maxTokens: 500,
6953
7261
  ...this.sectionModel ? { model: this.sectionModel } : {}
6954
- });
7262
+ }, streamFinal);
6955
7263
  currentSummary = result.content;
6956
7264
  } catch (err) {
6957
7265
  this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -7003,14 +7311,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7003
7311
  ...this.sectionModel ? { model: this.sectionModel } : {}
7004
7312
  });
7005
7313
  const answer = result.content.trim().toUpperCase();
7006
- if (answer.includes("YES")) {
7007
- return { requestId: req.id, approved: true, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: consistent with section goal" };
7008
- }
7009
- if (answer.includes("NO")) {
7010
- return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
7011
- }
7314
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
7315
+ (req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
7012
7316
  return null;
7013
7317
  } catch {
7318
+ (req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
7014
7319
  return null;
7015
7320
  }
7016
7321
  }
@@ -7352,10 +7657,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
7352
7657
  }
7353
7658
  }
7354
7659
  async decomposeTask(prompt, systemContext) {
7660
+ const db = this.router.getWorldStateDB?.();
7661
+ const worldStateContext = db ? `
7662
+
7663
+ PROJECT WORLD STATE (Current architecture and recent changes):
7664
+ ${db.getFormattedState()}` : "";
7355
7665
  const contextSection = systemContext ? `
7356
7666
  Project context:
7357
7667
  ${systemContext}` : "";
7358
- const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
7668
+ const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
7359
7669
 
7360
7670
  Task: ${prompt}
7361
7671
 
@@ -7687,7 +7997,7 @@ Instructions:
7687
7997
  systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
7688
7998
  maxTokens: 8e3
7689
7999
  }, (chunk) => {
7690
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
8000
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
7691
8001
  });
7692
8002
  return result.content;
7693
8003
  }
@@ -7716,14 +8026,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
7716
8026
  temperature: 0
7717
8027
  });
7718
8028
  const answer = result.content.trim().toUpperCase();
7719
- if (answer.includes("YES")) {
7720
- return { requestId: req.id, approved: true, always: true, decidedBy: "T1", reasoning: "T1 evaluated: consistent with overall task goal" };
7721
- }
7722
- if (answer.includes("NO")) {
7723
- return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
7724
- }
8029
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
8030
+ (req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
7725
8031
  return null;
7726
8032
  } catch {
8033
+ (req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
7727
8034
  return null;
7728
8035
  }
7729
8036
  }
@@ -7843,16 +8150,16 @@ function resolveInWorkspace(workspaceRoot, input) {
7843
8150
  if (typeof input !== "string" || input.length === 0) {
7844
8151
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
7845
8152
  }
7846
- const root = path23__default.default.resolve(workspaceRoot);
7847
- const abs = path23__default.default.isAbsolute(input) ? path23__default.default.resolve(input) : path23__default.default.resolve(root, input);
7848
- const rel = path23__default.default.relative(root, abs);
7849
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path23__default.default.isAbsolute(rel)) {
8153
+ const root = path25__default.default.resolve(workspaceRoot);
8154
+ const abs = path25__default.default.isAbsolute(input) ? path25__default.default.resolve(input) : path25__default.default.resolve(root, input);
8155
+ const rel = path25__default.default.relative(root, abs);
8156
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25__default.default.isAbsolute(rel)) {
7850
8157
  throw new WorkspaceSandboxError(input, root);
7851
8158
  }
7852
8159
  try {
7853
- const real = fs21__default.default.realpathSync(abs);
7854
- const realRel = path23__default.default.relative(root, real);
7855
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path23__default.default.isAbsolute(realRel))) {
8160
+ const real = fs23__default.default.realpathSync(abs);
8161
+ const realRel = path25__default.default.relative(root, real);
8162
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25__default.default.isAbsolute(realRel))) {
7856
8163
  throw new WorkspaceSandboxError(input, root);
7857
8164
  }
7858
8165
  } catch (e) {
@@ -7913,7 +8220,7 @@ var FileWriteTool = class extends BaseTool {
7913
8220
  } catch {
7914
8221
  }
7915
8222
  }
7916
- await fs9__default.default.mkdir(path23__default.default.dirname(absPath), { recursive: true });
8223
+ await fs9__default.default.mkdir(path25__default.default.dirname(absPath), { recursive: true });
7917
8224
  await fs9__default.default.writeFile(absPath, content, "utf-8");
7918
8225
  return `Written ${content.length} characters to ${filePath}`;
7919
8226
  }
@@ -8411,7 +8718,7 @@ var ImageAnalyzeTool = class extends BaseTool {
8411
8718
  };
8412
8719
  async function fileToImageAttachment(filePath) {
8413
8720
  const data = await fs9__default.default.readFile(filePath);
8414
- const ext = path23__default.default.extname(filePath).toLowerCase();
8721
+ const ext = path25__default.default.extname(filePath).toLowerCase();
8415
8722
  const mimeMap = {
8416
8723
  ".jpg": "image/jpeg",
8417
8724
  ".jpeg": "image/jpeg",
@@ -8445,14 +8752,14 @@ var PDFCreateTool = class extends BaseTool {
8445
8752
  const filePath = input["path"];
8446
8753
  const content = input["content"];
8447
8754
  const title = input["title"];
8448
- const dir = path23__default.default.dirname(filePath);
8449
- if (!fs21__default.default.existsSync(dir)) {
8450
- fs21__default.default.mkdirSync(dir, { recursive: true });
8755
+ const dir = path25__default.default.dirname(filePath);
8756
+ if (!fs23__default.default.existsSync(dir)) {
8757
+ fs23__default.default.mkdirSync(dir, { recursive: true });
8451
8758
  }
8452
8759
  return new Promise((resolve, reject) => {
8453
8760
  try {
8454
8761
  const doc = new PDFDocument__default.default({ margin: 50 });
8455
- const stream = fs21__default.default.createWriteStream(filePath);
8762
+ const stream = fs23__default.default.createWriteStream(filePath);
8456
8763
  doc.pipe(stream);
8457
8764
  if (title) {
8458
8765
  doc.info["Title"] = title;
@@ -8530,22 +8837,22 @@ var CodeInterpreterTool = class extends BaseTool {
8530
8837
  }
8531
8838
  cmdPrefix = NODE_CMD;
8532
8839
  }
8533
- const tmpDir = path23__default.default.join(this.workspaceRoot, ".cascade", "tmp");
8534
- if (!fs21__default.default.existsSync(tmpDir)) {
8535
- fs21__default.default.mkdirSync(tmpDir, { recursive: true });
8840
+ const tmpDir = path25__default.default.join(this.workspaceRoot, ".cascade", "tmp");
8841
+ if (!fs23__default.default.existsSync(tmpDir)) {
8842
+ fs23__default.default.mkdirSync(tmpDir, { recursive: true });
8536
8843
  }
8537
8844
  const extension = language === "python" ? "py" : "js";
8538
8845
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
8539
- const filePath = path23__default.default.join(tmpDir, fileName);
8540
- fs21__default.default.writeFileSync(filePath, code, "utf-8");
8846
+ const filePath = path25__default.default.join(tmpDir, fileName);
8847
+ fs23__default.default.writeFileSync(filePath, code, "utf-8");
8541
8848
  const execArgs = [filePath, ...args];
8542
8849
  return new Promise((resolve) => {
8543
8850
  const startMs = Date.now();
8544
8851
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
8545
8852
  const duration = Date.now() - startMs;
8546
8853
  try {
8547
- if (fs21__default.default.existsSync(filePath)) {
8548
- fs21__default.default.unlinkSync(filePath);
8854
+ if (fs23__default.default.existsSync(filePath)) {
8855
+ fs23__default.default.unlinkSync(filePath);
8549
8856
  }
8550
8857
  } catch (cleanupErr) {
8551
8858
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -8824,7 +9131,7 @@ var GlobTool = class extends BaseTool {
8824
9131
  };
8825
9132
  async execute(input, _options) {
8826
9133
  const pattern = input["pattern"];
8827
- const searchPath = input["path"] ? path23__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9134
+ const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
8828
9135
  const matches = await glob.glob(pattern, {
8829
9136
  cwd: searchPath,
8830
9137
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -8837,7 +9144,7 @@ var GlobTool = class extends BaseTool {
8837
9144
  const withMtime = await Promise.all(
8838
9145
  matches.map(async (rel) => {
8839
9146
  try {
8840
- const stat = await fs9__default.default.stat(path23__default.default.join(searchPath, rel));
9147
+ const stat = await fs9__default.default.stat(path25__default.default.join(searchPath, rel));
8841
9148
  return { rel, mtime: stat.mtimeMs };
8842
9149
  } catch {
8843
9150
  return { rel, mtime: 0 };
@@ -8886,7 +9193,7 @@ var GrepTool = class extends BaseTool {
8886
9193
  };
8887
9194
  async execute(input, _options) {
8888
9195
  const pattern = input["pattern"];
8889
- const searchPath = input["path"] ? path23__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9196
+ const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
8890
9197
  const globPattern = input["glob"];
8891
9198
  const outputMode = input["output_mode"] ?? "content";
8892
9199
  const context = input["context"] ?? 0;
@@ -8940,12 +9247,12 @@ var GrepTool = class extends BaseTool {
8940
9247
  nodir: true
8941
9248
  });
8942
9249
  } catch {
8943
- files = [path23__default.default.relative(searchPath, searchPath) || "."];
9250
+ files = [path25__default.default.relative(searchPath, searchPath) || "."];
8944
9251
  }
8945
9252
  const results = [];
8946
9253
  let totalCount = 0;
8947
9254
  for (const rel of files) {
8948
- const abs = path23__default.default.join(searchPath, rel);
9255
+ const abs = path25__default.default.join(searchPath, rel);
8949
9256
  let content;
8950
9257
  try {
8951
9258
  content = await fs9__default.default.readFile(abs, "utf-8");
@@ -9307,10 +9614,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
9307
9614
  }
9308
9615
  isIgnored(filePath) {
9309
9616
  if (!filePath) return false;
9310
- const abs = path23__default.default.resolve(this.workspaceRoot, filePath);
9311
- const rel = path23__default.default.relative(this.workspaceRoot, abs);
9312
- if (!rel || rel.startsWith("..") || path23__default.default.isAbsolute(rel)) return true;
9313
- const posixRel = rel.split(path23__default.default.sep).join("/");
9617
+ const abs = path25__default.default.resolve(this.workspaceRoot, filePath);
9618
+ const rel = path25__default.default.relative(this.workspaceRoot, abs);
9619
+ if (!rel || rel.startsWith("..") || path25__default.default.isAbsolute(rel)) return true;
9620
+ const posixRel = rel.split(path25__default.default.sep).join("/");
9314
9621
  return this.ignoreMatcher.ignores(posixRel);
9315
9622
  }
9316
9623
  };
@@ -9427,6 +9734,9 @@ var McpClient = class _McpClient {
9427
9734
  return this.clients.has(serverName);
9428
9735
  }
9429
9736
  };
9737
+
9738
+ // src/core/cascade.ts
9739
+ init_audit_logger();
9430
9740
  var SAFE_TOOLS = /* @__PURE__ */ new Set([
9431
9741
  "file_read",
9432
9742
  "file_list",
@@ -9810,9 +10120,10 @@ var TaskAnalyzer = class {
9810
10120
  analysisCache.clear();
9811
10121
  }
9812
10122
  };
9813
- var DEFAULT_STATS_FILE = path23__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
10123
+ var DEFAULT_STATS_FILE = path25__default.default.join(os8__default.default.homedir(), ".cascade", "model-perf.json");
9814
10124
  var ModelPerformanceTracker = class {
9815
10125
  stats = /* @__PURE__ */ new Map();
10126
+ featureStats = /* @__PURE__ */ new Map();
9816
10127
  statsFile;
9817
10128
  loaded = false;
9818
10129
  constructor(statsFile = DEFAULT_STATS_FILE) {
@@ -9824,18 +10135,29 @@ var ModelPerformanceTracker = class {
9824
10135
  try {
9825
10136
  const raw = await fs9__default.default.readFile(this.statsFile, "utf-8");
9826
10137
  const parsed = JSON.parse(raw);
9827
- for (const [key, stat] of Object.entries(parsed)) {
9828
- this.stats.set(key, stat);
10138
+ if (parsed.models) {
10139
+ for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
10140
+ } else {
10141
+ for (const [key, stat] of Object.entries(parsed)) {
10142
+ if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
10143
+ this.stats.set(key, stat);
10144
+ }
10145
+ }
10146
+ }
10147
+ if (parsed.features) {
10148
+ for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
9829
10149
  }
9830
10150
  } catch {
9831
10151
  }
9832
10152
  }
9833
10153
  async save() {
9834
10154
  try {
9835
- await fs9__default.default.mkdir(path23__default.default.dirname(this.statsFile), { recursive: true });
9836
- const obj = {};
9837
- for (const [key, stat] of this.stats) obj[key] = stat;
9838
- await fs9__default.default.writeFile(this.statsFile, JSON.stringify(obj, null, 2), "utf-8");
10155
+ await fs9__default.default.mkdir(path25__default.default.dirname(this.statsFile), { recursive: true });
10156
+ const modelsObj = {};
10157
+ const featuresObj = {};
10158
+ for (const [key, stat] of this.stats) modelsObj[key] = stat;
10159
+ for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
10160
+ await fs9__default.default.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
9839
10161
  } catch {
9840
10162
  }
9841
10163
  }
@@ -9856,6 +10178,13 @@ var ModelPerformanceTracker = class {
9856
10178
  sampleCount: s.sampleCount + 1
9857
10179
  });
9858
10180
  }
10181
+ recordFeatureCost(featureTag, costUsd) {
10182
+ const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
10183
+ this.featureStats.set(featureTag, {
10184
+ totalCostUsd: s.totalCostUsd + costUsd,
10185
+ runCount: s.runCount + 1
10186
+ });
10187
+ }
9859
10188
  /**
9860
10189
  * Record an explicit user rating (good/bad). Counts as 3 automatic samples
9861
10190
  * so user feedback carries significantly more weight than auto-detected outcomes.
@@ -9870,6 +10199,9 @@ var ModelPerformanceTracker = class {
9870
10199
  getAll() {
9871
10200
  return new Map(this.stats);
9872
10201
  }
10202
+ getAllFeatures() {
10203
+ return new Map(this.featureStats);
10204
+ }
9873
10205
  /**
9874
10206
  * Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
9875
10207
  * High retry counts penalise the score.
@@ -10234,7 +10566,7 @@ Required capability: ${description.slice(0, 300)}`;
10234
10566
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
10235
10567
  async loadPersistedTools() {
10236
10568
  if (!this.workspacePath || !this.persistEnabled) return;
10237
- const file = path23__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
10569
+ const file = path25__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
10238
10570
  try {
10239
10571
  const raw = await fs9__default.default.readFile(file, "utf-8");
10240
10572
  const specs = JSON.parse(raw);
@@ -10258,8 +10590,8 @@ Required capability: ${description.slice(0, 300)}`;
10258
10590
  }
10259
10591
  async persist() {
10260
10592
  if (!this.workspacePath || !this.persistEnabled) return;
10261
- const dir = path23__default.default.join(this.workspacePath, ".cascade");
10262
- const file = path23__default.default.join(dir, DYNAMIC_TOOLS_FILE);
10593
+ const dir = path25__default.default.join(this.workspacePath, ".cascade");
10594
+ const file = path25__default.default.join(dir, DYNAMIC_TOOLS_FILE);
10263
10595
  try {
10264
10596
  await fs9__default.default.mkdir(dir, { recursive: true });
10265
10597
  await fs9__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -10272,6 +10604,166 @@ Required capability: ${description.slice(0, 300)}`;
10272
10604
  return Array.from(this.specs.keys());
10273
10605
  }
10274
10606
  };
10607
+ var WorldStateDB = class {
10608
+ constructor(workspacePath, debugMode = false) {
10609
+ this.workspacePath = workspacePath;
10610
+ this.debugMode = debugMode;
10611
+ const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
10612
+ if (!fs23__default.default.existsSync(cascadeDir)) {
10613
+ fs23__default.default.mkdirSync(cascadeDir, { recursive: true });
10614
+ }
10615
+ this.keyPath = path25__default.default.join(cascadeDir, "world_state.key");
10616
+ this.dbPath = path25__default.default.join(cascadeDir, "world_state.db");
10617
+ this.initEncryptionKey();
10618
+ this.db = new Database2__default.default(this.dbPath);
10619
+ this.db.pragma("journal_mode = WAL");
10620
+ this.db.exec(`
10621
+ CREATE TABLE IF NOT EXISTS world_state (
10622
+ id TEXT PRIMARY KEY,
10623
+ timestamp TEXT NOT NULL,
10624
+ worker_id TEXT NOT NULL,
10625
+ encrypted_payload TEXT NOT NULL
10626
+ )
10627
+ `);
10628
+ }
10629
+ workspacePath;
10630
+ debugMode;
10631
+ db;
10632
+ keyPath;
10633
+ dbPath;
10634
+ encryptionKey;
10635
+ initEncryptionKey() {
10636
+ if (fs23__default.default.existsSync(this.keyPath)) {
10637
+ this.encryptionKey = fs23__default.default.readFileSync(this.keyPath);
10638
+ } else {
10639
+ this.encryptionKey = crypto__default.default.randomBytes(32);
10640
+ fs23__default.default.writeFileSync(this.keyPath, this.encryptionKey);
10641
+ }
10642
+ }
10643
+ encrypt(text) {
10644
+ const iv = crypto__default.default.randomBytes(16);
10645
+ const cipher = crypto__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
10646
+ let encrypted = cipher.update(text, "utf8", "hex");
10647
+ encrypted += cipher.final("hex");
10648
+ const authTag = cipher.getAuthTag().toString("hex");
10649
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
10650
+ }
10651
+ decrypt(text) {
10652
+ const parts = text.split(":");
10653
+ if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
10654
+ const [ivHex, authTagHex, encryptedHex] = parts;
10655
+ const iv = Buffer.from(ivHex, "hex");
10656
+ const authTag = Buffer.from(authTagHex, "hex");
10657
+ const decipher = crypto__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
10658
+ decipher.setAuthTag(authTag);
10659
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
10660
+ decrypted += decipher.final("utf8");
10661
+ return decrypted;
10662
+ }
10663
+ addEntry(workerId, summary) {
10664
+ const id = crypto__default.default.randomUUID();
10665
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10666
+ const payload = JSON.stringify({ summary });
10667
+ const encryptedPayload = this.encrypt(payload);
10668
+ const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
10669
+ stmt.run(id, timestamp, workerId, encryptedPayload);
10670
+ this.dumpDebugIfNeeded();
10671
+ return id;
10672
+ }
10673
+ getAllEntries() {
10674
+ const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
10675
+ const rows = stmt.all();
10676
+ return rows.map((row) => {
10677
+ try {
10678
+ const decrypted = this.decrypt(row.encrypted_payload);
10679
+ const parsed = JSON.parse(decrypted);
10680
+ return {
10681
+ id: row.id,
10682
+ timestamp: row.timestamp,
10683
+ workerId: row.worker_id,
10684
+ summary: parsed.summary
10685
+ };
10686
+ } catch (err) {
10687
+ return {
10688
+ id: row.id,
10689
+ timestamp: row.timestamp,
10690
+ workerId: row.worker_id,
10691
+ summary: "[Decryption Failed - Payload Corrupted]"
10692
+ };
10693
+ }
10694
+ });
10695
+ }
10696
+ getFormattedState() {
10697
+ const entries = this.getAllEntries();
10698
+ if (entries.length === 0) return "World State is currently empty.";
10699
+ return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
10700
+ }
10701
+ dumpDebugIfNeeded() {
10702
+ if (!this.debugMode) return;
10703
+ try {
10704
+ const dumpPath = path25__default.default.join(os8__default.default.tmpdir(), "cascade_world_state_debug.json");
10705
+ const entries = this.getAllEntries();
10706
+ fs23__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
10707
+ } catch (err) {
10708
+ console.error("Failed to dump debug world state", err);
10709
+ }
10710
+ }
10711
+ close() {
10712
+ this.db.close();
10713
+ }
10714
+ };
10715
+ var ignore3 = _ignoreModule__namespace.default ?? _ignoreModule__namespace;
10716
+ var PrivacyPaths = class {
10717
+ localOnly;
10718
+ hasRules;
10719
+ constructor(policies = []) {
10720
+ this.localOnly = ignore3();
10721
+ const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
10722
+ if (patterns.length) this.localOnly.add(patterns);
10723
+ this.hasRules = patterns.length > 0;
10724
+ }
10725
+ /** True when any privacy rules are configured at all (cheap short-circuit). */
10726
+ hasPolicies() {
10727
+ return this.hasRules;
10728
+ }
10729
+ /** True when the given workspace-relative path falls under a local-only policy. */
10730
+ isLocalOnly(relativePath) {
10731
+ if (!this.hasRules || !relativePath) return false;
10732
+ try {
10733
+ return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
10734
+ } catch {
10735
+ return false;
10736
+ }
10737
+ }
10738
+ /** True when ANY of the given paths falls under a local-only policy. */
10739
+ anyLocalOnly(relativePaths) {
10740
+ return relativePaths.some((p) => this.isLocalOnly(p));
10741
+ }
10742
+ };
10743
+
10744
+ // src/core/steering/guidance.ts
10745
+ var GuidanceQueue = class {
10746
+ entries = [];
10747
+ /** Per-consumer read cursor so a broadcast entry reaches each worker once. */
10748
+ cursors = /* @__PURE__ */ new Map();
10749
+ push(text, nodeId) {
10750
+ const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
10751
+ this.entries.push(entry);
10752
+ return entry;
10753
+ }
10754
+ /**
10755
+ * New entries for this consumer since its last drain, filtered to entries
10756
+ * that target it (or target everyone). Advances the consumer's cursor.
10757
+ */
10758
+ drain(consumerId) {
10759
+ const from = this.cursors.get(consumerId) ?? 0;
10760
+ this.cursors.set(consumerId, this.entries.length);
10761
+ return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
10762
+ }
10763
+ get size() {
10764
+ return this.entries.length;
10765
+ }
10766
+ };
10275
10767
 
10276
10768
  // src/core/cascade.ts
10277
10769
  var Cascade = class _Cascade extends EventEmitter__default.default {
@@ -10291,6 +10783,9 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
10291
10783
  taskAnalyzer;
10292
10784
  perfTracker;
10293
10785
  toolCreator;
10786
+ worldStateDB;
10787
+ encryptedAuditLogger;
10788
+ guidanceQueue;
10294
10789
  workspacePath;
10295
10790
  constructor(config, workspacePath, store) {
10296
10791
  super();
@@ -10312,6 +10807,23 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
10312
10807
  });
10313
10808
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
10314
10809
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
10810
+ this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
10811
+ this.router.setWorldStateDB(this.worldStateDB);
10812
+ this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
10813
+ const privacyPolicies = this.config.privacy?.paths ?? [];
10814
+ if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
10815
+ this.guidanceQueue = new GuidanceQueue();
10816
+ this.router.setGuidanceQueue(this.guidanceQueue);
10817
+ }
10818
+ /**
10819
+ * Live intervention: inject a user correction into the running hierarchy.
10820
+ * Every active T3 worker (or only the one matching `nodeId`) picks it up at
10821
+ * the top of its next agent-loop iteration as a USER INTERVENTION message.
10822
+ */
10823
+ injectGuidance(text, nodeId) {
10824
+ const entry = this.guidanceQueue.push(text, nodeId);
10825
+ this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
10826
+ this.emit("guidance:injected", entry);
10315
10827
  }
10316
10828
  initOptionalFeatures() {
10317
10829
  if (this.config.cascadeAuto === true) {
@@ -10474,6 +10986,12 @@ ${last.partialOutput}` : "");
10474
10986
  if (!prompt) return null;
10475
10987
  return this.run({ prompt });
10476
10988
  }
10989
+ getWorkspacePath() {
10990
+ return this.workspacePath;
10991
+ }
10992
+ getWorldStateDB() {
10993
+ return this.worldStateDB;
10994
+ }
10477
10995
  /**
10478
10996
  * Record an explicit user rating for the last completed run.
10479
10997
  * Explicit ratings carry 3× the weight of auto-detected outcomes so user
@@ -10560,6 +11078,11 @@ ${last.partialOutput}` : "");
10560
11078
  }
10561
11079
  this.initOptionalFeatures();
10562
11080
  if (this.toolCreator) await this.toolCreator.loadPersistedTools();
11081
+ if (this.encryptedAuditLogger) {
11082
+ this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
11083
+ this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
11084
+ this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
11085
+ }
10563
11086
  this.initialized = true;
10564
11087
  })();
10565
11088
  try {
@@ -10603,6 +11126,21 @@ ${last.partialOutput}` : "");
10603
11126
  const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p);
10604
11127
  return inquiry && !producesArtifact;
10605
11128
  }
11129
+ /**
11130
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
11131
+ * multiple sections/workers). Deliberately conservative: requires a
11132
+ * build/implementation verb AND either an app/system-scale noun or an
11133
+ * explicit multi-part structure, so ordinary single-file asks (handled as
11134
+ * Simple/Moderate) don't get over-escalated.
11135
+ */
11136
+ looksClearlyComplex(prompt) {
11137
+ const p = prompt.trim();
11138
+ if (p.length < 24) return false;
11139
+ const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11140
+ const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
11141
+ const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11142
+ return buildVerb && (scaleNoun || multiPart);
11143
+ }
10606
11144
  // Cache glob scan results per workspace path to avoid repeated I/O.
10607
11145
  static globCache = /* @__PURE__ */ new Map();
10608
11146
  async countWorkspaceFiles(workspacePath) {
@@ -10686,10 +11224,16 @@ ${prompt}` : prompt;
10686
11224
  let verdict;
10687
11225
  if (match) {
10688
11226
  verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
10689
- this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11227
+ if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11228
+ this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11229
+ verdict = "Complex";
11230
+ } else {
11231
+ this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11232
+ }
10690
11233
  } else {
10691
- verdict = prompt.trim().split(/\s+/).length <= 12 ? "Simple" : "Moderate";
10692
- this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length`);
11234
+ const words = prompt.trim().split(/\s+/).length;
11235
+ verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
11236
+ this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
10693
11237
  }
10694
11238
  return verdict;
10695
11239
  } catch {
@@ -10741,7 +11285,15 @@ ${prompt}` : prompt;
10741
11285
  }
10742
11286
  escalator.resolveUserDecision(req.id, approved, always);
10743
11287
  });
10744
- const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
11288
+ const forceTier = this.config.routing?.forceTier;
11289
+ const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
11290
+ let complexity;
11291
+ if (forced) {
11292
+ complexity = forced;
11293
+ this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
11294
+ } else {
11295
+ complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
11296
+ }
10745
11297
  this.telemetry.capture("cascade:session_start", {
10746
11298
  complexity,
10747
11299
  providerCount: this.config.providers.length,
@@ -10821,6 +11373,7 @@ ${prompt}` : prompt;
10821
11373
  try {
10822
11374
  if (complexity === "Simple") {
10823
11375
  const t3 = new T3Worker(this.router, this.toolRegistry, "root");
11376
+ t3.setPresenter(true);
10824
11377
  t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
10825
11378
  if (identityPrompt) {
10826
11379
  t3.setSystemPromptOverride(identityPrompt);
@@ -10845,6 +11398,7 @@ ${prompt}` : prompt;
10845
11398
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
10846
11399
  } else if (complexity === "Moderate") {
10847
11400
  const t2 = new T2Manager(this.router, this.toolRegistry, "root");
11401
+ t2.setPresenter(true);
10848
11402
  t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
10849
11403
  if (identityPrompt) {
10850
11404
  t2.setSystemPromptOverride(identityPrompt);
@@ -10894,6 +11448,7 @@ ${prompt}` : prompt;
10894
11448
  }
10895
11449
  } else {
10896
11450
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
11451
+ t1.setPresenter(true);
10897
11452
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
10898
11453
  if (identityPrompt) {
10899
11454
  t1.setSystemPromptOverride(identityPrompt);
@@ -10985,6 +11540,7 @@ ${prompt}` : prompt;
10985
11540
  durationMs,
10986
11541
  costByTier: stats.costByTier,
10987
11542
  tokensByTier: stats.tokensByTier,
11543
+ costByFeature: stats.costByFeature,
10988
11544
  costPercentByTier: this.router.getTierCostPercentages()
10989
11545
  };
10990
11546
  }
@@ -11234,6 +11790,30 @@ var SlashCommandRegistry = class {
11234
11790
  return { output: out || "File changes rolled back.", handled: true };
11235
11791
  }
11236
11792
  });
11793
+ this.register({
11794
+ command: "/steer",
11795
+ description: "Steer a running task: /steer <correction for the active workers>",
11796
+ args: ["<guidance>"],
11797
+ handler: async (args, ctx) => {
11798
+ const output = await ctx.onSteer(args);
11799
+ return { output, handled: true };
11800
+ }
11801
+ });
11802
+ this.register({
11803
+ command: "/audit",
11804
+ description: "Verify the tamper-evident audit log (hash chain integrity)",
11805
+ handler: async (_args, ctx) => {
11806
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
11807
+ const logger = new AuditLogger3(ctx.workspacePath);
11808
+ try {
11809
+ const v = logger.verifyChain();
11810
+ const output = v.ok ? `\u2714 Audit log intact \u2014 ${v.entries} entr${v.entries === 1 ? "y" : "ies"}, hash chain verified.` : `\u2718 Audit log FAILED verification at row ${v.firstBadRow} of ${v.entries} \u2014 entries at or after that row were modified, removed, or predate the hash chain.`;
11811
+ return { output, handled: true };
11812
+ } finally {
11813
+ logger.close();
11814
+ }
11815
+ }
11816
+ });
11237
11817
  this.register({
11238
11818
  command: "/branch",
11239
11819
  description: "Fork current session into parallel branches",
@@ -11881,7 +12461,8 @@ function CostTracker({
11881
12461
  tokensByTier,
11882
12462
  compact = false,
11883
12463
  savedUsd = 0,
11884
- savedPct = 0
12464
+ savedPct = 0,
12465
+ costByFeature
11885
12466
  }) {
11886
12467
  const hasTierCost = costByTier && Object.keys(costByTier).length > 0;
11887
12468
  const savingsLine = savedUsd > 0 ? `Saved $${savedUsd.toFixed(4)} (${savedPct}%) vs. running every call on T1` : null;
@@ -11980,6 +12561,16 @@ function CostTracker({
11980
12561
  ] }, tier);
11981
12562
  })
11982
12563
  ] })
12564
+ ] }),
12565
+ costByFeature && Object.keys(costByFeature).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 1, children: [
12566
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: "By feature/section:" }),
12567
+ Object.entries(costByFeature).map(([feature, cost]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginLeft: 2, children: [
12568
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { width: 30, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.secondary, wrap: "truncate-end", children: feature }) }),
12569
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: theme.colors.success, children: [
12570
+ "$",
12571
+ cost.toFixed(6)
12572
+ ] })
12573
+ ] }, feature))
11983
12574
  ] })
11984
12575
  ] });
11985
12576
  }
@@ -12393,7 +12984,7 @@ function replReducer(state, action) {
12393
12984
  case "SET_TREE":
12394
12985
  return { ...state, agentTree: action.tree };
12395
12986
  case "UPDATE_COST":
12396
- return { ...state, totalTokens: action.tokens, totalCostUsd: action.costUsd, callsByProvider: action.byProvider, callsByTier: action.byTier, costByTier: action.costByTier, tokensByTier: action.tokensByTier, savedUsd: action.savedUsd, savedPct: action.savedPct };
12987
+ return { ...state, totalTokens: action.tokens, totalCostUsd: action.costUsd, callsByProvider: action.byProvider, callsByTier: action.byTier, costByTier: action.costByTier, tokensByTier: action.tokensByTier, costByFeature: action.costByFeature, savedUsd: action.savedUsd, savedPct: action.savedPct };
12397
12988
  case "SET_APPROVAL":
12398
12989
  return { ...state, approvalRequest: action.request };
12399
12990
  case "SET_EXECUTING":
@@ -12407,7 +12998,7 @@ function replReducer(state, action) {
12407
12998
  case "TOGGLE_COMMS":
12408
12999
  return { ...state, showComms: !state.showComms };
12409
13000
  case "CLEAR":
12410
- return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
13001
+ return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
12411
13002
  case "CLEAR_TREE":
12412
13003
  return { ...state, agentTree: null };
12413
13004
  case "TOGGLE_COST":
@@ -12456,7 +13047,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12456
13047
  const [slashIndex, setSlashIndex] = React2.useState(0);
12457
13048
  const [identities, setIdentities] = React2.useState([]);
12458
13049
  const [currentIdentityId, setCurrentIdentityId] = React2.useState(config.defaultIdentityId);
12459
- const [state, dispatch] = React2.useReducer(replReducer, { messages: [], agentTree: null, isStreaming: false, isExecuting: false, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], showComms: true, approvalRequest: null, showCost: false, showDetails: false, error: null, activeTool: null });
13050
+ const [state, dispatch] = React2.useReducer(replReducer, { messages: [], agentTree: null, isStreaming: false, isExecuting: false, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], showComms: true, approvalRequest: null, showCost: false, showDetails: false, error: null, activeTool: null });
12460
13051
  const [isShowingModels, setIsShowingModels] = React2.useState(false);
12461
13052
  const [planApprovalRequest, setPlanApprovalRequest] = React2.useState(null);
12462
13053
  const [historyOffset, setHistoryOffset] = React2.useState(0);
@@ -12525,9 +13116,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12525
13116
  }
12526
13117
  } catch {
12527
13118
  }
12528
- const configPath = path23__default.default.join(workspacePath, ".cascade", "config.json");
13119
+ const configPath = path25__default.default.join(workspacePath, ".cascade", "config.json");
12529
13120
  try {
12530
- await fs9__default.default.mkdir(path23__default.default.dirname(configPath), { recursive: true });
13121
+ await fs9__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
12531
13122
  await fs9__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
12532
13123
  } catch (err) {
12533
13124
  const msg = err instanceof Error ? err.message : String(err);
@@ -12579,9 +13170,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12579
13170
  if (msg.includes("non-text parts")) return;
12580
13171
  originalLog(...args);
12581
13172
  };
12582
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
13173
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
12583
13174
  storeRef.current = store;
12584
- globalStoreRef.current = new MemoryStore(path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
13175
+ globalStoreRef.current = new MemoryStore(path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
12585
13176
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
12586
13177
  setIdentities(identityRows);
12587
13178
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -12644,7 +13235,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12644
13235
  onThemeChange: (name) => setTheme(getTheme(name)),
12645
13236
  onExport: async (fmt) => {
12646
13237
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12647
- const exportPath = path23__default.default.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
13238
+ const exportPath = path25__default.default.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
12648
13239
  if (fmt === "json") {
12649
13240
  await fs9__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
12650
13241
  } else {
@@ -12668,6 +13259,14 @@ ${msg.content}`).join("\n\n");
12668
13259
  }
12669
13260
  return `Restored ${snapshots.length} files to their initial session state.`;
12670
13261
  },
13262
+ onSteer: (args) => {
13263
+ const text = args.join(" ").trim();
13264
+ if (!text) return "Usage: /steer <correction for the active workers>";
13265
+ const cascade = cascadeRef.current;
13266
+ if (!cascade) return "No active Cascade instance.";
13267
+ cascade.injectGuidance(text);
13268
+ return "Guidance queued \u2014 active workers apply it on their next step. (No task running? It applies to the next run's workers.)";
13269
+ },
12671
13270
  onBranch: async () => {
12672
13271
  const store = storeRef.current;
12673
13272
  if (!store) return;
@@ -12986,7 +13585,7 @@ ${lastUser.content}`;
12986
13585
  if (lastTool !== null) dispatch({ type: "SET_ACTIVE_TOOL", toolName: lastTool });
12987
13586
  const stats = cascade.getRouter().getStats();
12988
13587
  const savings = cascade.getRouter().getDelegationSavings();
12989
- dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
13588
+ dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, costByFeature: stats.costByFeature, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
12990
13589
  statusThrottleTimeout = null;
12991
13590
  };
12992
13591
  cascade.on("tier:root", onRoot);
@@ -13078,7 +13677,7 @@ ${lastUser.content}`;
13078
13677
  flushPeerEvents();
13079
13678
  const stats = cascade.getRouter().getStats();
13080
13679
  const savings = cascade.getRouter().getDelegationSavings();
13081
- dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
13680
+ dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, costByFeature: stats.costByFeature, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
13082
13681
  dispatch({ type: "COMMIT_STREAM", finalText: result.output, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
13083
13682
  persistMessage("assistant", result.output, (/* @__PURE__ */ new Date()).toISOString());
13084
13683
  const receipt = formatRunReceipt(result, stats.totalCostUsd, savings);
@@ -13363,7 +13962,7 @@ ${lastUser.content}`;
13363
13962
  ),
13364
13963
  adaptiveMode === "wide" && state.showComms && liveBudget.commsMaxEvents > 0 && /* @__PURE__ */ jsxRuntime.jsx(PeerFeed, { events: state.peerEvents, theme, maxRows: liveBudget.commsMaxEvents }),
13365
13964
  adaptiveMode === "wide" && state.showDetails && liveBudget.showTimeline && /* @__PURE__ */ jsxRuntime.jsx(TimelinePanel, { nodes: [...treeNodesRef.current.values()], theme, currentIndex: timelineIndex, onChangeIndex: setTimelineIndex }),
13366
- adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsxRuntime.jsx(CostTracker, { theme, totalTokens: state.totalTokens, totalCostUsd: state.totalCostUsd, callsByProvider: state.callsByProvider, callsByTier: state.callsByTier, costByTier: state.costByTier, tokensByTier: state.tokensByTier, compact: liveBudget.costCompact, savedUsd: state.savedUsd, savedPct: state.savedPct }),
13965
+ adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsxRuntime.jsx(CostTracker, { theme, totalTokens: state.totalTokens, totalCostUsd: state.totalCostUsd, callsByProvider: state.callsByProvider, callsByTier: state.callsByTier, costByTier: state.costByTier, tokensByTier: state.tokensByTier, costByFeature: state.costByFeature, compact: liveBudget.costCompact, savedUsd: state.savedUsd, savedPct: state.savedPct }),
13367
13966
  liveBudget.collapsed && (state.showCost || state.showDetails || state.agentTree != null) && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: " \u25B8 panels collapsed (small terminal)" }),
13368
13967
  state.approvalRequest && /* @__PURE__ */ jsxRuntime.jsx(ApprovalPrompt, { request: state.approvalRequest, theme, onDecision: (decision) => {
13369
13968
  dispatch({ type: "SET_APPROVAL", request: null });
@@ -13532,7 +14131,7 @@ function stringifySlashOutput(val) {
13532
14131
  }
13533
14132
  async function searchSessionsAndMessages(query, workspacePath) {
13534
14133
  if (!query) return "Usage: /search <query>";
13535
- const dbPath = path23__default.default.join(workspacePath, CASCADE_DB_FILE);
14134
+ const dbPath = path25__default.default.join(workspacePath, CASCADE_DB_FILE);
13536
14135
  try {
13537
14136
  await fs9__default.default.access(dbPath);
13538
14137
  } catch {
@@ -13566,7 +14165,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
13566
14165
  async function diagnoseRuntime(config, workspacePath) {
13567
14166
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
13568
14167
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
13569
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
14168
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
13570
14169
  try {
13571
14170
  const sessions = store.listSessions(void 0, 3);
13572
14171
  return [
@@ -13585,7 +14184,7 @@ async function diagnoseRuntime(config, workspacePath) {
13585
14184
  }
13586
14185
  async function showRecentLogs(args, workspacePath) {
13587
14186
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
13588
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
14187
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
13589
14188
  try {
13590
14189
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
13591
14190
  if (!logs.length) return "No recent runtime logs.";
@@ -13597,7 +14196,7 @@ async function showRecentLogs(args, workspacePath) {
13597
14196
  async function loadSessionSnapshot(args, workspacePath) {
13598
14197
  const sessionId = args[0];
13599
14198
  if (!sessionId) return "Usage: /resume <sessionId>";
13600
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
14199
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
13601
14200
  try {
13602
14201
  const session = store.getSession(sessionId);
13603
14202
  if (!session) return `Session not found: ${sessionId}`;
@@ -14017,9 +14616,9 @@ function SetupWizard({ workspacePath, onComplete }) {
14017
14616
  ...anyAuto ? { cascadeAuto: true } : {}
14018
14617
  };
14019
14618
  const config = CascadeConfigSchema.parse(rawConfig);
14020
- const configDir = path23__default.default.join(workspacePath, ".cascade");
14619
+ const configDir = path25__default.default.join(workspacePath, ".cascade");
14021
14620
  await fs9__default.default.mkdir(configDir, { recursive: true });
14022
- const configPath = path23__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
14621
+ const configPath = path25__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
14023
14622
  await fs9__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
14024
14623
  savedConfigRef.current = config;
14025
14624
  dispatchRef.current({ type: "GO_DONE" });
@@ -14341,14 +14940,14 @@ function printTelemetryBanner() {
14341
14940
  async function initCommand(workspacePath = process.cwd()) {
14342
14941
  const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
14343
14942
  try {
14344
- const configDir = path23__default.default.join(workspacePath, ".cascade");
14943
+ const configDir = path25__default.default.join(workspacePath, ".cascade");
14345
14944
  await fs9__default.default.mkdir(configDir, { recursive: true });
14346
- const mdPath = path23__default.default.join(workspacePath, "CASCADE.md");
14945
+ const mdPath = path25__default.default.join(workspacePath, "CASCADE.md");
14347
14946
  if (!await fileExists(mdPath)) {
14348
14947
  await createDefaultCascadeMd(workspacePath);
14349
14948
  spin.succeed(chalk9__default.default.green("Created CASCADE.md"));
14350
14949
  }
14351
- const ignorePath = path23__default.default.join(workspacePath, ".cascadeignore");
14950
+ const ignorePath = path25__default.default.join(workspacePath, ".cascadeignore");
14352
14951
  if (!await fileExists(ignorePath)) {
14353
14952
  await createDefaultIgnoreFile(workspacePath);
14354
14953
  spin.succeed(chalk9__default.default.green("Created .cascadeignore"));
@@ -14357,7 +14956,7 @@ async function initCommand(workspacePath = process.cwd()) {
14357
14956
  console.log();
14358
14957
  console.log(chalk9__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
14359
14958
  console.log();
14360
- const configPath = path23__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
14959
+ const configPath = path25__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
14361
14960
  if (await fileExists(configPath)) {
14362
14961
  console.log(chalk9__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
14363
14962
  console.log();
@@ -14415,7 +15014,7 @@ function fromEnv(env) {
14415
15014
  return out;
14416
15015
  }
14417
15016
  async function fromClaudeCode(home) {
14418
- const file = path23__default.default.join(home, ".claude", ".credentials.json");
15017
+ const file = path25__default.default.join(home, ".claude", ".credentials.json");
14419
15018
  const data = await readJson(file);
14420
15019
  if (!data) return [];
14421
15020
  const oauth = data["claudeAiOauth"];
@@ -14438,7 +15037,7 @@ async function fromClaudeCode(home) {
14438
15037
  return [];
14439
15038
  }
14440
15039
  async function fromCodex(home) {
14441
- const file = path23__default.default.join(home, ".codex", "auth.json");
15040
+ const file = path25__default.default.join(home, ".codex", "auth.json");
14442
15041
  const data = await readJson(file);
14443
15042
  if (!data) return [];
14444
15043
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -14461,7 +15060,7 @@ async function fromCodex(home) {
14461
15060
  return [];
14462
15061
  }
14463
15062
  async function fromGemini(home) {
14464
- const file = path23__default.default.join(home, ".gemini", "oauth_creds.json");
15063
+ const file = path25__default.default.join(home, ".gemini", "oauth_creds.json");
14465
15064
  const data = await readJson(file);
14466
15065
  const accessToken = str(data?.["access_token"]);
14467
15066
  if (!accessToken) return [];
@@ -14477,7 +15076,7 @@ async function fromGemini(home) {
14477
15076
  }
14478
15077
  async function fromCopilot(home) {
14479
15078
  for (const name of ["apps.json", "hosts.json"]) {
14480
- const file = path23__default.default.join(home, ".config", "github-copilot", name);
15079
+ const file = path25__default.default.join(home, ".config", "github-copilot", name);
14481
15080
  const data = await readJson(file);
14482
15081
  if (!data) continue;
14483
15082
  for (const value of Object.values(data)) {
@@ -14498,7 +15097,7 @@ async function fromCopilot(home) {
14498
15097
  return [];
14499
15098
  }
14500
15099
  async function discoverCredentials(opts = {}) {
14501
- const home = opts.homeDir ?? os6__default.default.homedir();
15100
+ const home = opts.homeDir ?? os8__default.default.homedir();
14502
15101
  const env = opts.env ?? process.env;
14503
15102
  const groups = await Promise.all([
14504
15103
  Promise.resolve(fromEnv(env)),
@@ -14531,7 +15130,7 @@ async function doctorCommand() {
14531
15130
  checks.push({
14532
15131
  label: "Cascade config",
14533
15132
  ok: true,
14534
- detail: `Loaded ${path23__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
15133
+ detail: `Loaded ${path25__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
14535
15134
  });
14536
15135
  const providers = [
14537
15136
  { type: "anthropic", name: "Anthropic" },
@@ -14810,6 +15409,24 @@ var DashboardSocket = class {
14810
15409
  });
14811
15410
  });
14812
15411
  }
15412
+ onSessionHalt(callback) {
15413
+ this.io.on("connection", (socket) => {
15414
+ socket.on("session:halt", (payload) => {
15415
+ if (typeof payload?.sessionId === "string") {
15416
+ callback(payload.sessionId);
15417
+ }
15418
+ });
15419
+ });
15420
+ }
15421
+ onSessionSteer(callback) {
15422
+ this.io.on("connection", (socket) => {
15423
+ socket.on("session:steer", (payload) => {
15424
+ if (typeof payload?.message === "string" && payload.message.trim()) {
15425
+ callback(payload.message.trim(), payload.sessionId, payload.nodeId);
15426
+ }
15427
+ });
15428
+ });
15429
+ }
14813
15430
  onConfigUpdate(callback) {
14814
15431
  this.io.on("connection", (socket) => {
14815
15432
  socket.on("config:update", (payload) => {
@@ -14832,7 +15449,8 @@ var DashboardSocket = class {
14832
15449
  socket.on("cascade:run", (payload) => {
14833
15450
  if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
14834
15451
  const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
14835
- callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId);
15452
+ const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
15453
+ callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
14836
15454
  }
14837
15455
  });
14838
15456
  });
@@ -14844,7 +15462,7 @@ var DashboardSocket = class {
14844
15462
 
14845
15463
  // src/dashboard/server.ts
14846
15464
  init_constants();
14847
- var __dirname$1 = path23__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))));
15465
+ var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
14848
15466
  var DashboardServer = class {
14849
15467
  app;
14850
15468
  httpServer;
@@ -14855,6 +15473,20 @@ var DashboardServer = class {
14855
15473
  globalStore = null;
14856
15474
  broadcastTimer = null;
14857
15475
  activeSessions = /* @__PURE__ */ new Map();
15476
+ activeControllers = /* @__PURE__ */ new Map();
15477
+ /**
15478
+ * Run taskIds per chat session — file snapshots are keyed by the run's
15479
+ * taskId (see t3-worker saveSnapshot), so session rollback needs the list
15480
+ * of runs the session performed in this server's lifetime.
15481
+ */
15482
+ sessionTaskIds = /* @__PURE__ */ new Map();
15483
+ /**
15484
+ * Tool-approval requests awaiting a user decision from a connected client,
15485
+ * keyed by the request's uuid. The desktop shows a modal on
15486
+ * `permission:user-required` and answers with `permission:decision`; this
15487
+ * map is how that answer reaches the run that's blocked on it.
15488
+ */
15489
+ pendingApprovals = /* @__PURE__ */ new Map();
14858
15490
  port;
14859
15491
  host;
14860
15492
  workspacePath;
@@ -14913,15 +15545,18 @@ var DashboardServer = class {
14913
15545
  }
14914
15546
  this.persistConfig();
14915
15547
  });
14916
- this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
15548
+ this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
14917
15549
  const sessionId = requestedSessionId ?? crypto.randomUUID();
15550
+ const abortController = new AbortController();
15551
+ this.activeControllers.set(sessionId, abortController);
14918
15552
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
14919
15553
  const title = this.persistRunStart(sessionId, prompt);
14920
- const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
15554
+ let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
15555
+ if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
14921
15556
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
14922
15557
  this.activeSessions.set(sessionId, cascade);
14923
15558
  cascade.on("stream:token", (e) => {
14924
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
15559
+ this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
14925
15560
  });
14926
15561
  cascade.on("tier:status", (e) => {
14927
15562
  this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
@@ -14933,7 +15568,12 @@ var DashboardServer = class {
14933
15568
  this.socket.emitPeerMessage(e);
14934
15569
  });
14935
15570
  try {
14936
- const result = await cascade.run({ prompt: runPrompt });
15571
+ const result = await cascade.run({
15572
+ prompt: runPrompt,
15573
+ signal: abortController.signal,
15574
+ approvalCallback: this.makeApprovalCallback(sessionId)
15575
+ });
15576
+ this.recordSessionTask(sessionId, result.taskId);
14937
15577
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
14938
15578
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
14939
15579
  this.socket.broadcast("cost:update", {
@@ -14950,6 +15590,24 @@ var DashboardServer = class {
14950
15590
  });
14951
15591
  } finally {
14952
15592
  this.activeSessions.delete(sessionId);
15593
+ this.activeControllers.delete(sessionId);
15594
+ this.denyPendingApprovals(sessionId);
15595
+ }
15596
+ });
15597
+ this.socket.onSessionHalt((sessionId) => {
15598
+ this.activeControllers.get(sessionId)?.abort();
15599
+ this.denyPendingApprovals(sessionId);
15600
+ });
15601
+ this.socket.onApprovalResponse(({ requestId, approved, always }) => {
15602
+ const pending = this.pendingApprovals.get(requestId);
15603
+ if (!pending) return;
15604
+ this.pendingApprovals.delete(requestId);
15605
+ pending.resolve({ approved: !!approved, always: !!always });
15606
+ });
15607
+ this.socket.onSessionSteer((message, sessionId, nodeId) => {
15608
+ const steered = this.steerSessions(message, sessionId, nodeId);
15609
+ if (steered > 0) {
15610
+ this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
14953
15611
  }
14954
15612
  });
14955
15613
  }
@@ -14997,9 +15655,9 @@ var DashboardServer = class {
14997
15655
  */
14998
15656
  persistConfig() {
14999
15657
  try {
15000
- const configPath = path23__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
15001
- fs21__default.default.mkdirSync(path23__default.default.dirname(configPath), { recursive: true });
15002
- fs21__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
15658
+ const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
15659
+ fs23__default.default.mkdirSync(path25__default.default.dirname(configPath), { recursive: true });
15660
+ fs23__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
15003
15661
  } catch (err) {
15004
15662
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
15005
15663
  }
@@ -15014,15 +15672,15 @@ var DashboardServer = class {
15014
15672
  resolveDashboardSecret() {
15015
15673
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
15016
15674
  if (fromConfig) return fromConfig;
15017
- const secretPath = path23__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
15675
+ const secretPath = path25__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
15018
15676
  try {
15019
- if (fs21__default.default.existsSync(secretPath)) {
15020
- const existing = fs21__default.default.readFileSync(secretPath, "utf-8").trim();
15677
+ if (fs23__default.default.existsSync(secretPath)) {
15678
+ const existing = fs23__default.default.readFileSync(secretPath, "utf-8").trim();
15021
15679
  if (existing.length >= 16) return existing;
15022
15680
  }
15023
15681
  const generated = crypto.randomUUID();
15024
- fs21__default.default.mkdirSync(path23__default.default.dirname(secretPath), { recursive: true });
15025
- fs21__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
15682
+ fs23__default.default.mkdirSync(path25__default.default.dirname(secretPath), { recursive: true });
15683
+ fs23__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
15026
15684
  if (this.config.dashboard.auth) {
15027
15685
  console.warn(
15028
15686
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -15049,7 +15707,7 @@ var DashboardServer = class {
15049
15707
  // ── Setup ─────────────────────────────────────
15050
15708
  getGlobalStore() {
15051
15709
  if (!this.globalStore) {
15052
- const globalDbPath = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15710
+ const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15053
15711
  this.globalStore = new MemoryStore(globalDbPath);
15054
15712
  }
15055
15713
  return this.globalStore;
@@ -15097,6 +15755,44 @@ var DashboardServer = class {
15097
15755
  }
15098
15756
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
15099
15757
  }
15758
+ /**
15759
+ * Route steering text into running Cascade instances. Targets the given
15760
+ * session, or every active session when none is specified (the desktop
15761
+ * usually has exactly one run in flight). Returns how many were reached.
15762
+ */
15763
+ steerSessions(message, sessionId, nodeId) {
15764
+ const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
15765
+ for (const cascade of targets) cascade.injectGuidance(message, nodeId);
15766
+ return targets.length;
15767
+ }
15768
+ recordSessionTask(sessionId, taskId) {
15769
+ const list = this.sessionTaskIds.get(sessionId) ?? [];
15770
+ list.push(taskId);
15771
+ this.sessionTaskIds.set(sessionId, list);
15772
+ }
15773
+ /**
15774
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
15775
+ * user. The request itself was already pushed to the client via the
15776
+ * `permission:user-required` forward; here we just park a resolver keyed by
15777
+ * the request id and wait for the client's `permission:decision` (handled in
15778
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
15779
+ * pending until the client answers, the run ends, or the escalator's own
15780
+ * timeout denies it.
15781
+ */
15782
+ makeApprovalCallback(sessionId) {
15783
+ return (request) => new Promise((resolve) => {
15784
+ this.pendingApprovals.set(request.id, { resolve, sessionId });
15785
+ });
15786
+ }
15787
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
15788
+ denyPendingApprovals(sessionId) {
15789
+ for (const [id, pending] of this.pendingApprovals) {
15790
+ if (pending.sessionId === sessionId) {
15791
+ this.pendingApprovals.delete(id);
15792
+ pending.resolve({ approved: false, always: false });
15793
+ }
15794
+ }
15795
+ }
15100
15796
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
15101
15797
  const now = (/* @__PURE__ */ new Date()).toISOString();
15102
15798
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -15190,12 +15886,12 @@ ${prompt}`;
15190
15886
  }
15191
15887
  }
15192
15888
  watchRuntimeChanges() {
15193
- const workspaceDbPath = path23__default.default.join(this.workspacePath, CASCADE_DB_FILE);
15194
- const globalDbPath = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15889
+ const workspaceDbPath = path25__default.default.join(this.workspacePath, CASCADE_DB_FILE);
15890
+ const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15195
15891
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
15196
15892
  for (const watchPath of watchPaths) {
15197
- if (!fs21__default.default.existsSync(watchPath)) continue;
15198
- fs21__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
15893
+ if (!fs23__default.default.existsSync(watchPath)) continue;
15894
+ fs23__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
15199
15895
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
15200
15896
  });
15201
15897
  }
@@ -15285,15 +15981,6 @@ ${prompt}`;
15285
15981
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
15286
15982
  res.json({ success: true, ...payload });
15287
15983
  });
15288
- this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
15289
- const body = req.body;
15290
- const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
15291
- const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
15292
- const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15293
- this.socket.broadcast("session:approve", payload);
15294
- if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
15295
- res.json({ success: true, ...payload });
15296
- });
15297
15984
  this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
15298
15985
  const body = req.body;
15299
15986
  const message = typeof body["message"] === "string" ? body["message"] : void 0;
@@ -15303,7 +15990,8 @@ ${prompt}`;
15303
15990
  res.status(400).json({ error: "message is required and must be a string" });
15304
15991
  return;
15305
15992
  }
15306
- const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15993
+ const steered = this.steerSessions(message, sessionId, nodeId);
15994
+ const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15307
15995
  this.socket.broadcast("session:message-injected", payload);
15308
15996
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
15309
15997
  res.json({ success: true, ...payload });
@@ -15325,7 +16013,7 @@ ${prompt}`;
15325
16013
  const sessionId = req.params.id;
15326
16014
  this.store.deleteSession(sessionId);
15327
16015
  this.store.deleteRuntimeSession(sessionId);
15328
- const globalDbPath = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16016
+ const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15329
16017
  const globalStore = new MemoryStore(globalDbPath);
15330
16018
  try {
15331
16019
  globalStore.deleteRuntimeSession(sessionId);
@@ -15337,9 +16025,34 @@ ${prompt}`;
15337
16025
  this.socket.broadcast("runtime:refresh", { scope: "global" });
15338
16026
  res.json({ ok: true });
15339
16027
  });
16028
+ this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
16029
+ const sessionId = req.params.id;
16030
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
16031
+ if (!taskIds.length) {
16032
+ res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
16033
+ return;
16034
+ }
16035
+ const toRestore = /* @__PURE__ */ new Map();
16036
+ for (const taskId of taskIds) {
16037
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
16038
+ if (!toRestore.has(filePath)) toRestore.set(filePath, content);
16039
+ }
16040
+ }
16041
+ const { writeFile } = await import('fs/promises');
16042
+ let restored = 0;
16043
+ for (const [filePath, content] of toRestore) {
16044
+ try {
16045
+ await writeFile(filePath, content, "utf-8");
16046
+ restored++;
16047
+ } catch (err) {
16048
+ console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
16049
+ }
16050
+ }
16051
+ res.json({ ok: true, restored });
16052
+ });
15340
16053
  this.app.delete("/api/sessions", auth, (req, res) => {
15341
16054
  const body = req.body;
15342
- const globalDbPath = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16055
+ const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15343
16056
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
15344
16057
  const globalStore = new MemoryStore(globalDbPath);
15345
16058
  try {
@@ -15362,7 +16075,7 @@ ${prompt}`;
15362
16075
  });
15363
16076
  this.app.delete("/api/runtime", auth, (_req, res) => {
15364
16077
  this.store.deleteAllRuntimeNodes();
15365
- const globalDbPath = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16078
+ const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15366
16079
  const globalStore = new MemoryStore(globalDbPath);
15367
16080
  try {
15368
16081
  globalStore.deleteAllRuntimeNodes();
@@ -15413,6 +16126,19 @@ ${prompt}`;
15413
16126
  this.store.deleteIdentity(req.params.id);
15414
16127
  res.json({ ok: true });
15415
16128
  });
16129
+ this.app.get("/api/audit/verify", auth, async (_req, res) => {
16130
+ try {
16131
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
16132
+ const logger = new AuditLogger3(this.workspacePath);
16133
+ try {
16134
+ res.json(logger.verifyChain());
16135
+ } finally {
16136
+ logger.close();
16137
+ }
16138
+ } catch (err) {
16139
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
16140
+ }
16141
+ });
15416
16142
  this.app.get("/api/audit/:sessionId", auth, (req, res) => {
15417
16143
  const log = this.store.getAuditLog(req.params.sessionId);
15418
16144
  res.json(log);
@@ -15435,12 +16161,12 @@ ${prompt}`;
15435
16161
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
15436
16162
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
15437
16163
  try {
15438
- const configPath = path23__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
15439
- const existing = fs21__default.default.existsSync(configPath) ? JSON.parse(fs21__default.default.readFileSync(configPath, "utf-8")) : {};
16164
+ const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16165
+ const existing = fs23__default.default.existsSync(configPath) ? JSON.parse(fs23__default.default.readFileSync(configPath, "utf-8")) : {};
15440
16166
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
15441
16167
  const tmp = configPath + ".tmp";
15442
- fs21__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
15443
- fs21__default.default.renameSync(tmp, configPath);
16168
+ fs23__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
16169
+ fs23__default.default.renameSync(tmp, configPath);
15444
16170
  res.json({ ok: true });
15445
16171
  } catch (err) {
15446
16172
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -15468,7 +16194,7 @@ ${prompt}`;
15468
16194
  this.app.get("/api/runtime", auth, (req, res) => {
15469
16195
  const scope = req.query["scope"] ?? "workspace";
15470
16196
  if (scope === "global") {
15471
- const globalDbPath = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16197
+ const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15472
16198
  const globalStore = new MemoryStore(globalDbPath);
15473
16199
  try {
15474
16200
  res.json({
@@ -15505,7 +16231,7 @@ ${prompt}`;
15505
16231
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
15506
16232
  this.activeSessions.set(sessionId, cascade);
15507
16233
  cascade.on("stream:token", (e) => {
15508
- this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
16234
+ this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
15509
16235
  });
15510
16236
  cascade.on("tier:status", (e) => {
15511
16237
  this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
@@ -15517,7 +16243,12 @@ ${prompt}`;
15517
16243
  this.socket.emitPeerMessage(e);
15518
16244
  });
15519
16245
  try {
15520
- const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
16246
+ const result = await cascade.run({
16247
+ prompt: runPrompt,
16248
+ identityId: body.identityId,
16249
+ approvalCallback: this.makeApprovalCallback(sessionId)
16250
+ });
16251
+ this.recordSessionTask(sessionId, result.taskId);
15521
16252
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
15522
16253
  this.socket.broadcast("cost:update", {
15523
16254
  sessionId,
@@ -15534,6 +16265,7 @@ ${prompt}`;
15534
16265
  });
15535
16266
  } finally {
15536
16267
  this.activeSessions.delete(sessionId);
16268
+ this.denyPendingApprovals(sessionId);
15537
16269
  }
15538
16270
  })();
15539
16271
  });
@@ -15548,13 +16280,13 @@ ${prompt}`;
15548
16280
  }))
15549
16281
  });
15550
16282
  });
15551
- const prodPath = path23__default.default.resolve(__dirname$1, "../web/dist");
15552
- const devPath = path23__default.default.resolve(__dirname$1, "../../web/dist");
15553
- const webDistPath = fs21__default.default.existsSync(prodPath) ? prodPath : devPath;
15554
- if (fs21__default.default.existsSync(webDistPath)) {
16283
+ const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
16284
+ const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
16285
+ const webDistPath = fs23__default.default.existsSync(prodPath) ? prodPath : devPath;
16286
+ if (fs23__default.default.existsSync(webDistPath)) {
15555
16287
  this.app.use(express__default.default.static(webDistPath));
15556
16288
  this.app.get("*", (_req, res) => {
15557
- res.sendFile(path23__default.default.join(webDistPath, "index.html"));
16289
+ res.sendFile(path25__default.default.join(webDistPath, "index.html"));
15558
16290
  });
15559
16291
  } else {
15560
16292
  this.app.get("/", (_req, res) => {
@@ -15575,7 +16307,7 @@ init_constants();
15575
16307
  async function dashboardCommand(config, workspacePath = process.cwd()) {
15576
16308
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
15577
16309
  const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
15578
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
16310
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
15579
16311
  const server = new DashboardServer(config, store, workspacePath);
15580
16312
  server.watchRuntimeChanges();
15581
16313
  const gThis = globalThis;
@@ -15607,7 +16339,7 @@ init_constants();
15607
16339
  function makeIdentityCommand(workspacePath = process.cwd()) {
15608
16340
  const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
15609
16341
  identity.command("list").description("List all available identities").action(() => {
15610
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
16342
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
15611
16343
  try {
15612
16344
  const identities = store.listIdentities();
15613
16345
  console.log(chalk9__default.default.bold("\n Identities:"));
@@ -15627,7 +16359,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
15627
16359
  }
15628
16360
  });
15629
16361
  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) => {
15630
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
16362
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
15631
16363
  try {
15632
16364
  if (options.default) {
15633
16365
  const existingDefault = store.getDefaultIdentity();
@@ -15653,7 +16385,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
15653
16385
  }
15654
16386
  });
15655
16387
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
15656
- const store = new MemoryStore(path23__default.default.join(workspacePath, CASCADE_DB_FILE));
16388
+ const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
15657
16389
  try {
15658
16390
  const identities = store.listIdentities();
15659
16391
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -15781,8 +16513,8 @@ async function exportCommand(options = {}) {
15781
16513
  let store;
15782
16514
  try {
15783
16515
  const workspacePath = options.workspacePath ?? process.cwd();
15784
- const workspaceDbPath = path23__default.default.join(workspacePath, CASCADE_DB_FILE);
15785
- const globalDbPath = path23__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
16516
+ const workspaceDbPath = path25__default.default.join(workspacePath, CASCADE_DB_FILE);
16517
+ const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
15786
16518
  let dbPath = globalDbPath;
15787
16519
  try {
15788
16520
  await fs9__default.default.access(workspaceDbPath);
@@ -15824,7 +16556,7 @@ async function exportCommand(options = {}) {
15824
16556
  const ext = format === "json" ? ".json" : ".md";
15825
16557
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
15826
16558
  const defaultFile = `cascade-export-${safeName}${ext}`;
15827
- const outPath = options.output ? path23__default.default.resolve(options.output) : path23__default.default.join(process.cwd(), defaultFile);
16559
+ const outPath = options.output ? path25__default.default.resolve(options.output) : path25__default.default.join(process.cwd(), defaultFile);
15828
16560
  await fs9__default.default.writeFile(outPath, content, "utf-8");
15829
16561
  spin.succeed(chalk9__default.default.green(`Exported to ${chalk9__default.default.white(outPath)}`));
15830
16562
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -16052,11 +16784,11 @@ async function statsCommand() {
16052
16784
  dotenv__default.default.config();
16053
16785
  function warnIfBuildIsStale() {
16054
16786
  try {
16055
- const here = path23__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))));
16787
+ const here = path25__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))));
16056
16788
  for (const rel of ["..", "../.."]) {
16057
- const pkgPath = path23__default.default.join(here, rel, "package.json");
16058
- if (!fs21__default.default.existsSync(pkgPath)) continue;
16059
- const pkg = JSON.parse(fs21__default.default.readFileSync(pkgPath, "utf-8"));
16789
+ const pkgPath = path25__default.default.join(here, rel, "package.json");
16790
+ if (!fs23__default.default.existsSync(pkgPath)) continue;
16791
+ const pkg = JSON.parse(fs23__default.default.readFileSync(pkgPath, "utf-8"));
16060
16792
  if (pkg.name !== "cascade-ai") continue;
16061
16793
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
16062
16794
  console.error(