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.js CHANGED
@@ -6,20 +6,20 @@ import http, { createServer } from 'http';
6
6
  import https from 'https';
7
7
  import zlib from 'zlib';
8
8
  import { Readable } from 'stream';
9
+ import Database2 from 'better-sqlite3';
10
+ import path25 from 'path';
11
+ import fs23 from 'fs';
12
+ import os8 from 'os';
13
+ import crypto, { randomUUID, timingSafeEqual } from 'crypto';
9
14
  import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
10
15
  import { Command } from 'commander';
11
16
  import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo } from 'react';
12
17
  import chalk9 from 'chalk';
13
18
  import dotenv from 'dotenv';
14
- import fs21 from 'fs';
15
- import path23 from 'path';
16
19
  import { fileURLToPath } from 'url';
17
20
  import fs9 from 'fs/promises';
18
- import os6 from 'os';
19
- import crypto, { randomUUID, timingSafeEqual } from 'crypto';
20
21
  import * as _ignoreModule from 'ignore';
21
22
  import _ignoreModule__default from 'ignore';
22
- import Database from 'better-sqlite3';
23
23
  import { z } from 'zod';
24
24
  import { exec, execFile, execSync, spawnSync } from 'child_process';
25
25
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -58,7 +58,7 @@ var __export = (target, all) => {
58
58
  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;
59
59
  var init_constants = __esm({
60
60
  "src/constants.ts"() {
61
- CASCADE_VERSION = "0.12.24";
61
+ CASCADE_VERSION = "0.13.2";
62
62
  CASCADE_CONFIG_FILE = ".cascade/config.json";
63
63
  CASCADE_DB_FILE = ".cascade/memory.db";
64
64
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -1525,6 +1525,149 @@ var init_openai_compatible = __esm({
1525
1525
  }
1526
1526
  });
1527
1527
 
1528
+ // src/core/audit/audit-logger.ts
1529
+ var audit_logger_exports = {};
1530
+ __export(audit_logger_exports, {
1531
+ AuditLogger: () => AuditLogger2
1532
+ });
1533
+ var AuditLogger2;
1534
+ var init_audit_logger = __esm({
1535
+ "src/core/audit/audit-logger.ts"() {
1536
+ AuditLogger2 = class {
1537
+ constructor(workspacePath, debugMode = false) {
1538
+ this.workspacePath = workspacePath;
1539
+ this.debugMode = debugMode;
1540
+ const cascadeDir = path25.join(workspacePath, ".cascade");
1541
+ if (!fs23.existsSync(cascadeDir)) {
1542
+ fs23.mkdirSync(cascadeDir, { recursive: true });
1543
+ }
1544
+ this.keyPath = path25.join(cascadeDir, "audit_log.key");
1545
+ this.dbPath = path25.join(cascadeDir, "audit_log.db");
1546
+ this.initEncryptionKey();
1547
+ this.db = new Database2(this.dbPath);
1548
+ this.db.pragma("journal_mode = WAL");
1549
+ this.db.exec(`
1550
+ CREATE TABLE IF NOT EXISTS audit_logs (
1551
+ id TEXT PRIMARY KEY,
1552
+ timestamp TEXT NOT NULL,
1553
+ event_type TEXT NOT NULL,
1554
+ tier_id TEXT NOT NULL,
1555
+ encrypted_payload TEXT NOT NULL,
1556
+ prev_hash TEXT NOT NULL DEFAULT '',
1557
+ hash TEXT NOT NULL DEFAULT ''
1558
+ )
1559
+ `);
1560
+ const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
1561
+ if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
1562
+ if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
1563
+ }
1564
+ workspacePath;
1565
+ debugMode;
1566
+ db;
1567
+ keyPath;
1568
+ dbPath;
1569
+ encryptionKey;
1570
+ /** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
1571
+ chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
1572
+ return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
1573
+ }
1574
+ initEncryptionKey() {
1575
+ if (fs23.existsSync(this.keyPath)) {
1576
+ this.encryptionKey = fs23.readFileSync(this.keyPath);
1577
+ } else {
1578
+ this.encryptionKey = crypto.randomBytes(32);
1579
+ fs23.writeFileSync(this.keyPath, this.encryptionKey);
1580
+ }
1581
+ }
1582
+ encrypt(text) {
1583
+ const iv = crypto.randomBytes(16);
1584
+ const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
1585
+ let encrypted = cipher.update(text, "utf8", "hex");
1586
+ encrypted += cipher.final("hex");
1587
+ const authTag = cipher.getAuthTag().toString("hex");
1588
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
1589
+ }
1590
+ decrypt(text) {
1591
+ const parts = text.split(":");
1592
+ if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
1593
+ const [ivHex, authTagHex, encryptedHex] = parts;
1594
+ const iv = Buffer.from(ivHex, "hex");
1595
+ const authTag = Buffer.from(authTagHex, "hex");
1596
+ const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
1597
+ decipher.setAuthTag(authTag);
1598
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
1599
+ decrypted += decipher.final("utf8");
1600
+ return decrypted;
1601
+ }
1602
+ logEvent(eventType, tierId, payloadObj) {
1603
+ const id = crypto.randomUUID();
1604
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1605
+ const payload = JSON.stringify(payloadObj);
1606
+ const encryptedPayload = this.encrypt(payload);
1607
+ const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
1608
+ const prevHash = last?.hash ?? "";
1609
+ const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
1610
+ const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
1611
+ stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
1612
+ this.dumpDebugIfNeeded();
1613
+ return id;
1614
+ }
1615
+ /**
1616
+ * Recompute the whole hash chain in insertion (rowid) order. Any edited,
1617
+ * removed, or reordered row makes every later hash mismatch. Rows written
1618
+ * before the chain existed (empty hash) fail verification too — integrity
1619
+ * can only be claimed for chained history.
1620
+ */
1621
+ verifyChain() {
1622
+ const rows = this.db.prepare(
1623
+ "SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
1624
+ ).all();
1625
+ let prevHash = "";
1626
+ for (const row of rows) {
1627
+ const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
1628
+ if (row.prev_hash !== prevHash || row.hash !== expected) {
1629
+ return { ok: false, entries: rows.length, firstBadRow: row.rowid };
1630
+ }
1631
+ prevHash = row.hash;
1632
+ }
1633
+ return { ok: true, entries: rows.length };
1634
+ }
1635
+ getAllLogs() {
1636
+ const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
1637
+ const rows = stmt.all();
1638
+ return rows.map((row) => {
1639
+ let payload = "";
1640
+ try {
1641
+ payload = this.decrypt(row.encrypted_payload);
1642
+ } catch (err) {
1643
+ payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
1644
+ }
1645
+ return {
1646
+ id: row.id,
1647
+ timestamp: row.timestamp,
1648
+ eventType: row.event_type,
1649
+ tierId: row.tier_id,
1650
+ payload
1651
+ };
1652
+ });
1653
+ }
1654
+ dumpDebugIfNeeded() {
1655
+ if (!this.debugMode) return;
1656
+ try {
1657
+ const dumpPath = path25.join(os8.tmpdir(), "cascade_audit_logs_debug.json");
1658
+ const entries = this.getAllLogs();
1659
+ fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
1660
+ } catch (err) {
1661
+ console.error("Failed to dump debug audit logs", err);
1662
+ }
1663
+ }
1664
+ close() {
1665
+ this.db.close();
1666
+ }
1667
+ };
1668
+ }
1669
+ });
1670
+
1528
1671
  // src/cli/index.ts
1529
1672
  init_constants();
1530
1673
  var ALGORITHM = "aes-256-gcm";
@@ -1568,7 +1711,7 @@ var Keystore = class {
1568
1711
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
1569
1712
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
1570
1713
  this.backend = "keytar";
1571
- if (password && fs21.existsSync(this.storePath)) {
1714
+ if (password && fs23.existsSync(this.storePath)) {
1572
1715
  try {
1573
1716
  const fileEntries = this.decryptFile(password);
1574
1717
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -1587,7 +1730,7 @@ var Keystore = class {
1587
1730
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
1588
1731
  );
1589
1732
  }
1590
- if (!fs21.existsSync(this.storePath)) {
1733
+ if (!fs23.existsSync(this.storePath)) {
1591
1734
  const salt = crypto.randomBytes(SALT_LEN);
1592
1735
  this.masterKey = this.deriveKey(password, salt);
1593
1736
  this.writeWithSalt({}, salt);
@@ -1601,7 +1744,7 @@ var Keystore = class {
1601
1744
  }
1602
1745
  /** Synchronous legacy unlock kept for AES-only environments. */
1603
1746
  unlockSync(password) {
1604
- if (!fs21.existsSync(this.storePath)) {
1747
+ if (!fs23.existsSync(this.storePath)) {
1605
1748
  const salt = crypto.randomBytes(SALT_LEN);
1606
1749
  this.masterKey = this.deriveKey(password, salt);
1607
1750
  this.writeWithSalt({}, salt);
@@ -1659,7 +1802,7 @@ var Keystore = class {
1659
1802
  }
1660
1803
  }
1661
1804
  decryptFile(password, knownSalt) {
1662
- if (!fs21.existsSync(this.storePath)) return {};
1805
+ if (!fs23.existsSync(this.storePath)) return {};
1663
1806
  try {
1664
1807
  const { salt, ciphertext, iv, tag } = this.readRaw();
1665
1808
  const useSalt = knownSalt ?? salt;
@@ -1681,8 +1824,8 @@ var Keystore = class {
1681
1824
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1682
1825
  const tag = cipher.getAuthTag();
1683
1826
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
1684
- fs21.mkdirSync(path23.dirname(this.storePath), { recursive: true });
1685
- fs21.writeFileSync(this.storePath, out, { mode: 384 });
1827
+ fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
1828
+ fs23.writeFileSync(this.storePath, out, { mode: 384 });
1686
1829
  }
1687
1830
  writeWithSalt(data, salt) {
1688
1831
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -1692,11 +1835,11 @@ var Keystore = class {
1692
1835
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1693
1836
  const tag = cipher.getAuthTag();
1694
1837
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
1695
- fs21.mkdirSync(path23.dirname(this.storePath), { recursive: true });
1696
- fs21.writeFileSync(this.storePath, out, { mode: 384 });
1838
+ fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
1839
+ fs23.writeFileSync(this.storePath, out, { mode: 384 });
1697
1840
  }
1698
1841
  readRaw() {
1699
- const buf = fs21.readFileSync(this.storePath);
1842
+ const buf = fs23.readFileSync(this.storePath);
1700
1843
  let offset = 0;
1701
1844
  const salt = buf.subarray(offset, offset + SALT_LEN);
1702
1845
  offset += SALT_LEN;
@@ -1729,7 +1872,7 @@ var CascadeIgnore = class {
1729
1872
  ]);
1730
1873
  }
1731
1874
  async load(workspacePath) {
1732
- const filePath = path23.join(workspacePath, ".cascadeignore");
1875
+ const filePath = path25.join(workspacePath, ".cascadeignore");
1733
1876
  try {
1734
1877
  const content = await fs9.readFile(filePath, "utf-8");
1735
1878
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -1740,7 +1883,7 @@ var CascadeIgnore = class {
1740
1883
  }
1741
1884
  isIgnored(filePath, workspacePath) {
1742
1885
  try {
1743
- const relative = workspacePath ? path23.relative(workspacePath, filePath) : filePath;
1886
+ const relative = workspacePath ? path25.relative(workspacePath, filePath) : filePath;
1744
1887
  return this.ig.ignores(relative);
1745
1888
  } catch {
1746
1889
  return false;
@@ -1751,7 +1894,7 @@ var CascadeIgnore = class {
1751
1894
  }
1752
1895
  };
1753
1896
  async function createDefaultIgnoreFile(workspacePath) {
1754
- const filePath = path23.join(workspacePath, ".cascadeignore");
1897
+ const filePath = path25.join(workspacePath, ".cascadeignore");
1755
1898
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
1756
1899
  # Syntax identical to .gitignore
1757
1900
 
@@ -1781,7 +1924,7 @@ Thumbs.db
1781
1924
  await fs9.writeFile(filePath, content, "utf-8");
1782
1925
  }
1783
1926
  async function loadCascadeMd(workspacePath) {
1784
- const filePath = path23.join(workspacePath, "CASCADE.md");
1927
+ const filePath = path25.join(workspacePath, "CASCADE.md");
1785
1928
  try {
1786
1929
  const raw = await fs9.readFile(filePath, "utf-8");
1787
1930
  return parseCascadeMd(raw);
@@ -1810,7 +1953,7 @@ ${raw.trim()}`;
1810
1953
  return { raw, sections, systemPrompt };
1811
1954
  }
1812
1955
  async function createDefaultCascadeMd(workspacePath) {
1813
- const filePath = path23.join(workspacePath, "CASCADE.md");
1956
+ const filePath = path25.join(workspacePath, "CASCADE.md");
1814
1957
  const content = `# Cascade Project Instructions
1815
1958
 
1816
1959
  This file contains project-specific instructions for the Cascade AI agent.
@@ -1845,9 +1988,9 @@ List any areas the agent should not touch.
1845
1988
  var MemoryStore = class _MemoryStore {
1846
1989
  db;
1847
1990
  constructor(dbPath) {
1848
- fs21.mkdirSync(path23.dirname(dbPath), { recursive: true });
1991
+ fs23.mkdirSync(path25.dirname(dbPath), { recursive: true });
1849
1992
  try {
1850
- this.db = new Database(dbPath, { timeout: 5e3 });
1993
+ this.db = new Database2(dbPath, { timeout: 5e3 });
1851
1994
  this.db.pragma("journal_mode = WAL");
1852
1995
  this.db.pragma("foreign_keys = ON");
1853
1996
  this.db.pragma("synchronous = NORMAL");
@@ -2692,7 +2835,8 @@ var WorkspaceConfigSchema = z.object({
2692
2835
  cascadeMdPath: z.string().default("CASCADE.md"),
2693
2836
  configPath: z.string().default(".cascade/config.json"),
2694
2837
  keystorePath: z.string().default(".cascade/keystore.enc"),
2695
- auditLogPath: z.string().default(".cascade/audit.log")
2838
+ auditLogPath: z.string().default(".cascade/audit.log"),
2839
+ debugWorldState: z.boolean().default(false)
2696
2840
  });
2697
2841
  var CascadeConfigSchema = z.object({
2698
2842
  version: z.literal("1.0").default("1.0"),
@@ -2837,6 +2981,16 @@ var CascadeConfigSchema = z.object({
2837
2981
  * 'parallel' / 'sequential' — force it.
2838
2982
  */
2839
2983
  t3Execution: z.enum(["auto", "parallel", "sequential"]).default("auto"),
2984
+ /**
2985
+ * Per-path privacy tiers. A subtask touching a `local-only` path is forced
2986
+ * onto LOCAL models (never cloud) and its raw output is withheld from the
2987
+ * tiers above. Patterns use .gitignore syntax, like .cascadeignore.
2988
+ */
2989
+ privacy: z.object({
2990
+ paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
2991
+ }).optional(),
2992
+ /** Routing controls — forceTier pins the root tier, bypassing the classifier. */
2993
+ routing: z.object({ forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
2840
2994
  /**
2841
2995
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
2842
2996
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -2886,15 +3040,15 @@ var ConfigManager = class {
2886
3040
  globalDir;
2887
3041
  constructor(workspacePath = process.cwd()) {
2888
3042
  this.workspacePath = workspacePath;
2889
- this.globalDir = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR);
3043
+ this.globalDir = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR);
2890
3044
  }
2891
3045
  async load() {
2892
3046
  this.config = await this.loadConfig();
2893
3047
  this.ignore = new CascadeIgnore();
2894
3048
  await this.ignore.load(this.workspacePath);
2895
3049
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
2896
- this.keystore = new Keystore(path23.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
2897
- this.store = new MemoryStore(path23.join(this.workspacePath, CASCADE_DB_FILE));
3050
+ this.keystore = new Keystore(path25.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
3051
+ this.store = new MemoryStore(path25.join(this.workspacePath, CASCADE_DB_FILE));
2898
3052
  await this.injectEnvKeys();
2899
3053
  await this.ensureDefaultIdentity();
2900
3054
  }
@@ -2917,8 +3071,8 @@ var ConfigManager = class {
2917
3071
  return this.workspacePath;
2918
3072
  }
2919
3073
  async save() {
2920
- const configPath = path23.join(this.workspacePath, CASCADE_CONFIG_FILE);
2921
- await fs9.mkdir(path23.dirname(configPath), { recursive: true });
3074
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
3075
+ await fs9.mkdir(path25.dirname(configPath), { recursive: true });
2922
3076
  await fs9.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
2923
3077
  }
2924
3078
  async updateConfig(updates) {
@@ -2942,7 +3096,7 @@ var ConfigManager = class {
2942
3096
  return configProvider?.apiKey;
2943
3097
  }
2944
3098
  async loadConfig() {
2945
- const configPath = path23.join(this.workspacePath, CASCADE_CONFIG_FILE);
3099
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
2946
3100
  try {
2947
3101
  const raw = await fs9.readFile(configPath, "utf-8");
2948
3102
  return validateConfig(JSON.parse(raw));
@@ -3821,7 +3975,7 @@ init_constants();
3821
3975
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
3822
3976
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
3823
3977
  var FETCH_TIMEOUT_MS = 8e3;
3824
- var DEFAULT_CACHE_FILE = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
3978
+ var DEFAULT_CACHE_FILE = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
3825
3979
  function normalizeModelId(id) {
3826
3980
  let s = id.toLowerCase();
3827
3981
  const slash = s.lastIndexOf("/");
@@ -3936,7 +4090,7 @@ var LiveDataProvider = class {
3936
4090
  }
3937
4091
  async saveCache() {
3938
4092
  try {
3939
- await fs9.mkdir(path23.dirname(this.opts.cacheFile), { recursive: true });
4093
+ await fs9.mkdir(path25.dirname(this.opts.cacheFile), { recursive: true });
3940
4094
  const cache = {
3941
4095
  fetchedAt: this.fetchedAt,
3942
4096
  snapshot: this.snapshot ?? void 0,
@@ -4066,7 +4220,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4066
4220
  costByTier: {},
4067
4221
  tokensByTier: {},
4068
4222
  inputTokensByTier: {},
4069
- outputTokensByTier: {}
4223
+ outputTokensByTier: {},
4224
+ costByFeature: {}
4070
4225
  };
4071
4226
  tierModels = /* @__PURE__ */ new Map();
4072
4227
  config;
@@ -4088,6 +4243,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4088
4243
  tpmLimiter;
4089
4244
  localQueue;
4090
4245
  taskAnalyzer;
4246
+ worldStateDB;
4247
+ privacyPaths;
4248
+ guidanceQueue;
4091
4249
  liveData;
4092
4250
  /** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
4093
4251
  originalTierModels;
@@ -4245,7 +4403,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4245
4403
  if (options.model && !requireVision) {
4246
4404
  this.ensureProvider(options.model, this.config.providers);
4247
4405
  }
4248
- const model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
4406
+ let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
4407
+ if (options.forceLocal && model && !this.isPrivateModel(model)) {
4408
+ const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
4409
+ if (!localModel) {
4410
+ throw new Error(
4411
+ "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."
4412
+ );
4413
+ }
4414
+ this.ensureProvider(localModel, this.config.providers);
4415
+ model = localModel;
4416
+ }
4249
4417
  if (!model) throw new Error(`No model available for tier ${tier}`);
4250
4418
  const provider = this.getProvider(model);
4251
4419
  if (!provider) throw new Error(`No provider for model ${model.id}`);
@@ -4317,7 +4485,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4317
4485
  if (!result || typeof result.content !== "string" || !result.usage) {
4318
4486
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
4319
4487
  }
4320
- this.recordStats(tier, model, result.usage);
4488
+ this.recordStats(tier, model, result.usage, options.featureTag);
4321
4489
  this.failover.recordSuccess(model.provider);
4322
4490
  return result;
4323
4491
  } catch (err) {
@@ -4422,6 +4590,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4422
4590
  setTaskAnalyzer(analyzer) {
4423
4591
  this.taskAnalyzer = analyzer;
4424
4592
  }
4593
+ setWorldStateDB(db) {
4594
+ this.worldStateDB = db;
4595
+ }
4596
+ getWorldStateDB() {
4597
+ return this.worldStateDB;
4598
+ }
4599
+ setPrivacyPaths(paths) {
4600
+ this.privacyPaths = paths;
4601
+ }
4602
+ getPrivacyPaths() {
4603
+ return this.privacyPaths;
4604
+ }
4605
+ setGuidanceQueue(queue) {
4606
+ this.guidanceQueue = queue;
4607
+ }
4608
+ getGuidanceQueue() {
4609
+ return this.guidanceQueue;
4610
+ }
4611
+ /**
4612
+ * "Private" = inference never leaves the user's machine/network: Ollama
4613
+ * models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
4614
+ * LM Studio) whose configured host is loopback or a private range. A cloud
4615
+ * OpenAI-compatible endpoint (public host) does NOT qualify.
4616
+ */
4617
+ isPrivateModel(model) {
4618
+ if (model.isLocal) return true;
4619
+ if (model.provider !== "openai-compatible") return false;
4620
+ const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
4621
+ if (!baseUrl) return false;
4622
+ try {
4623
+ const host = new URL(baseUrl).hostname.toLowerCase();
4624
+ 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");
4625
+ } catch {
4626
+ return false;
4627
+ }
4628
+ }
4425
4629
  /**
4426
4630
  * Cascade Auto per-subtask routing: pick the benchmark-best model for a
4427
4631
  * specific subtask's text, scoped to the tier's eligible candidates. Returns
@@ -4445,7 +4649,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4445
4649
  costByTier: { ...this.stats.costByTier },
4446
4650
  tokensByTier: { ...this.stats.tokensByTier },
4447
4651
  inputTokensByTier: { ...this.stats.inputTokensByTier },
4448
- outputTokensByTier: { ...this.stats.outputTokensByTier }
4652
+ outputTokensByTier: { ...this.stats.outputTokensByTier },
4653
+ costByFeature: { ...this.stats.costByFeature }
4449
4654
  };
4450
4655
  }
4451
4656
  /**
@@ -4495,7 +4700,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4495
4700
  costByTier: {},
4496
4701
  tokensByTier: {},
4497
4702
  inputTokensByTier: {},
4498
- outputTokensByTier: {}
4703
+ outputTokensByTier: {},
4704
+ costByFeature: {}
4499
4705
  };
4500
4706
  this.sessionCostUsd = 0;
4501
4707
  this.budgetState = "ok";
@@ -4661,7 +4867,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4661
4867
  }
4662
4868
  return void 0;
4663
4869
  }
4664
- recordStats(tier, model, usage) {
4870
+ recordStats(tier, model, usage, featureTag) {
4665
4871
  this.stats.totalTokens += usage.totalTokens;
4666
4872
  this.stats.totalCostUsd += usage.estimatedCostUsd;
4667
4873
  this.sessionCostUsd += usage.estimatedCostUsd;
@@ -4671,6 +4877,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4671
4877
  this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
4672
4878
  this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
4673
4879
  this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
4880
+ if (featureTag) {
4881
+ this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
4882
+ }
4674
4883
  this.runTokens += usage.totalTokens;
4675
4884
  this.runCostUsd += usage.estimatedCostUsd;
4676
4885
  this.updateBudgetState();
@@ -4767,6 +4976,13 @@ var BaseTier = class extends EventEmitter {
4767
4976
  hierarchyContext = "";
4768
4977
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
4769
4978
  signal;
4979
+ /**
4980
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
4981
+ * Complex). Its own synthesis stream is the user-facing answer and is
4982
+ * tagged `primary` so the desktop renders it live — background workers,
4983
+ * which would interleave, are not tagged.
4984
+ */
4985
+ isPresenter = false;
4770
4986
  constructor(role, id, parentId) {
4771
4987
  super();
4772
4988
  this.role = role;
@@ -4774,6 +4990,10 @@ var BaseTier = class extends EventEmitter {
4774
4990
  this.parentId = parentId;
4775
4991
  this.label = this.id;
4776
4992
  }
4993
+ /** Mark this tier as the run's presenter (root tier). */
4994
+ setPresenter(on = true) {
4995
+ this.isPresenter = on;
4996
+ }
4777
4997
  getStatus() {
4778
4998
  return this.status;
4779
4999
  }
@@ -5208,6 +5428,8 @@ var T3Worker = class extends BaseTier {
5208
5428
  toolRegistry;
5209
5429
  context;
5210
5430
  assignment;
5431
+ /** True when this subtask matched a privacy.paths local-only pattern. */
5432
+ localOnlyMatch = false;
5211
5433
  peerSyncBuffer = [];
5212
5434
  store;
5213
5435
  audit;
@@ -5263,6 +5485,11 @@ var T3Worker = class extends BaseTier {
5263
5485
  this.taskId = taskId;
5264
5486
  this.setLabel(assignment.subtaskTitle);
5265
5487
  this.setStatus("ACTIVE");
5488
+ const privacy = this.router.getPrivacyPaths?.();
5489
+ this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
5490
+ if (this.localOnlyMatch) {
5491
+ this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
5492
+ }
5266
5493
  this.tools = this.toolRegistry.getToolDefinitions();
5267
5494
  if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
5268
5495
  this.tools = [...this.tools, {
@@ -5393,9 +5620,17 @@ Now execute your subtask using this context where relevant.`
5393
5620
  }
5394
5621
  const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
5395
5622
  if (reflectCfg.enabled) {
5396
- this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
5623
+ this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
5397
5624
  output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
5398
5625
  }
5626
+ const db = this.router.getWorldStateDB?.();
5627
+ if (db) {
5628
+ try {
5629
+ db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
5630
+ } catch (e) {
5631
+ this.log("Failed to write to World State DB");
5632
+ }
5633
+ }
5399
5634
  this.setStatus("COMPLETED", output);
5400
5635
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
5401
5636
  this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
@@ -5468,6 +5703,16 @@ Now execute your subtask using this context where relevant.`
5468
5703
  while (iterations < MAX_ITERATIONS) {
5469
5704
  iterations++;
5470
5705
  this.throwIfCancelled();
5706
+ const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
5707
+ for (const g of guidance) {
5708
+ this.log(`User intervention received: ${g.text}`);
5709
+ this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
5710
+ await this.context.addMessage({
5711
+ role: "user",
5712
+ content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
5713
+ ${g.text}`
5714
+ });
5715
+ }
5471
5716
  const options = {
5472
5717
  messages: this.context.getMessages(),
5473
5718
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -5476,13 +5721,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
5476
5721
  // Don't pass tools array when model can't use them natively
5477
5722
  tools: useTextTools ? void 0 : tools.length ? tools : void 0,
5478
5723
  maxTokens: 4096,
5479
- ...subtaskModel ? { model: subtaskModel } : {}
5724
+ ...subtaskModel ? { model: subtaskModel } : {},
5725
+ featureTag: this.assignment?.sectionTitle,
5726
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5480
5727
  };
5481
5728
  const result = await this.router.generate(
5482
5729
  "T3",
5483
5730
  options,
5484
5731
  (chunk) => {
5485
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
5732
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
5486
5733
  }
5487
5734
  );
5488
5735
  let effectiveToolCalls = result.toolCalls ?? [];
@@ -5640,8 +5887,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
5640
5887
  tierId: this.id,
5641
5888
  sessionId: this.taskId,
5642
5889
  requireApproval: false,
5643
- saveSnapshot: async (path28, content) => {
5644
- this.store?.addFileSnapshot(this.taskId, path28, content);
5890
+ saveSnapshot: async (path30, content) => {
5891
+ this.store?.addFileSnapshot(this.taskId, path30, content);
5645
5892
  },
5646
5893
  sendPeerSync: (to, syncType, content) => {
5647
5894
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -5811,7 +6058,7 @@ ${assignment.expectedOutput}`;
5811
6058
  const { promisify: promisify4 } = await import('util');
5812
6059
  const execAsync3 = promisify4(exec3);
5813
6060
  for (const artifactPath of artifactPaths) {
5814
- const absolutePath = path23.resolve(process.cwd(), artifactPath);
6061
+ const absolutePath = path25.resolve(process.cwd(), artifactPath);
5815
6062
  try {
5816
6063
  const stat = await fs9.stat(absolutePath);
5817
6064
  if (!stat.isFile()) {
@@ -5832,7 +6079,7 @@ ${assignment.expectedOutput}`;
5832
6079
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
5833
6080
  continue;
5834
6081
  }
5835
- const ext = path23.extname(absolutePath).toLowerCase();
6082
+ const ext = path25.extname(absolutePath).toLowerCase();
5836
6083
  try {
5837
6084
  if (ext === ".ts" || ext === ".tsx") {
5838
6085
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -5861,30 +6108,35 @@ ${stdout}`);
5861
6108
  * needed. Best-effort: any parse/error just keeps the current output.
5862
6109
  */
5863
6110
  async reflectAndImprove(assignment, output, maxRounds) {
5864
- const sys = this.systemPromptOverride + (this.hierarchyContext ? `
5865
-
5866
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
5867
6111
  let current = output;
5868
- for (let round = 0; round < Math.max(1, maxRounds); round++) {
5869
- try {
5870
- const verdict = await this.router.generate("T3", {
6112
+ try {
6113
+ for (let round = 0; round < Math.max(1, maxRounds); round++) {
6114
+ const verdictResult = await this.router.generate("T2", {
5871
6115
  messages: [{
5872
6116
  role: "user",
5873
- content: `Does this output FULLY achieve the goal \u2014 not just the literal task, but the intent behind it?
6117
+ content: `You are an independent critic reviewing another worker's output against its assignment.
5874
6118
 
5875
- Goal / expected: ${assignment.expectedOutput}
6119
+ Goal: ${assignment.expectedOutput}
5876
6120
  Subtask: ${assignment.description}
5877
-
5878
- Output:
6121
+ Current Output:
5879
6122
  ${current}
5880
6123
 
5881
- Reply with ONLY JSON: {"sufficient": true|false, "notes": "what is weak or missing if not sufficient"}`
6124
+ Is this output sufficient and correct? Respond with ONLY a JSON object:
6125
+ {"sufficient": true|false, "notes": "what is wrong or missing if false"}`
5882
6126
  }],
5883
- systemPrompt: sys,
5884
- maxTokens: 400
6127
+ systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
6128
+ maxTokens: 400,
6129
+ signal: this.signal,
6130
+ featureTag: assignment.sectionTitle,
6131
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5885
6132
  });
5886
- const parsed = JSON.parse(/\{[\s\S]*\}/.exec(verdict.content)?.[0] ?? "{}");
5887
- if (parsed.sufficient !== false) break;
6133
+ const match = /\{[\s\S]*\}/.exec(verdictResult.content);
6134
+ const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
6135
+ if (parsed.sufficient !== false) {
6136
+ this.log("T2-Critic approved output.");
6137
+ break;
6138
+ }
6139
+ this.log(`T2-Critic rejected output: ${parsed.notes}`);
5888
6140
  const improved = await this.router.generate("T3", {
5889
6141
  messages: [{
5890
6142
  role: "user",
@@ -5896,16 +6148,20 @@ Goal / expected: ${assignment.expectedOutput}
5896
6148
  Current output:
5897
6149
  ${current}`
5898
6150
  }],
5899
- systemPrompt: sys,
5900
- maxTokens: 4096
6151
+ systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
6152
+
6153
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6154
+ maxTokens: 4096,
6155
+ featureTag: assignment.sectionTitle,
6156
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5901
6157
  });
5902
6158
  const next = (improved.content ?? "").trim();
5903
6159
  if (!next) break;
5904
6160
  current = next;
5905
6161
  this.log("Reflection: revised output for better goal alignment.");
5906
- } catch {
5907
- break;
5908
6162
  }
6163
+ } catch (e) {
6164
+ this.log(`T2-Critic reflection failed: ${e}`);
5909
6165
  }
5910
6166
  return current;
5911
6167
  }
@@ -5926,7 +6182,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
5926
6182
  maxTokens: 500,
5927
6183
  systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
5928
6184
 
5929
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
6185
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6186
+ featureTag: assignment.sectionTitle,
6187
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5930
6188
  });
5931
6189
  try {
5932
6190
  const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
@@ -6021,6 +6279,7 @@ Begin execution now.`;
6021
6279
  issues,
6022
6280
  peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
6023
6281
  correctionAttempts,
6282
+ localOnly: this.localOnlyMatch || void 0,
6024
6283
  reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
6025
6284
  };
6026
6285
  }
@@ -6312,6 +6571,42 @@ var PeerBus = class extends EventEmitter {
6312
6571
  }
6313
6572
  };
6314
6573
 
6574
+ // src/core/audit/redaction.ts
6575
+ var RedactionLayer = class {
6576
+ // Regexes for common secrets/PII
6577
+ static RULES = [
6578
+ // IPv4 addresses (basic approximation)
6579
+ { pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
6580
+ // Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
6581
+ { 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]" },
6582
+ // Email addresses
6583
+ { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
6584
+ // Phone numbers (simplistic)
6585
+ { pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
6586
+ // AWS Access Key ID
6587
+ { pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
6588
+ ];
6589
+ /**
6590
+ * Applies all redaction rules to the input string.
6591
+ */
6592
+ static redact(text) {
6593
+ if (!text) return text;
6594
+ let redacted = text;
6595
+ for (const rule of this.RULES) {
6596
+ if (rule.pattern.test(redacted)) {
6597
+ rule.pattern.lastIndex = 0;
6598
+ redacted = redacted.replace(rule.pattern, (match, p1, offset, str2) => {
6599
+ if (p1 && match.includes(p1) && p1 !== match) {
6600
+ return match.replace(p1, rule.replacement.replace("$1", ""));
6601
+ }
6602
+ return rule.replacement;
6603
+ });
6604
+ }
6605
+ }
6606
+ return redacted;
6607
+ }
6608
+ };
6609
+
6315
6610
  // src/core/tiers/t2-manager.ts
6316
6611
  var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
6317
6612
  Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
@@ -6540,7 +6835,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6540
6835
  try {
6541
6836
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
6542
6837
  if (!jsonMatch) throw new Error("No JSON array found");
6543
- return JSON.parse(jsonMatch[0]);
6838
+ const parsed = JSON.parse(jsonMatch[0]);
6839
+ return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
6544
6840
  } catch {
6545
6841
  return [{
6546
6842
  subtaskId: randomUUID(),
@@ -6550,6 +6846,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6550
6846
  constraints: assignment.constraints,
6551
6847
  peerT3Ids: [],
6552
6848
  parentT2: this.id,
6849
+ sectionTitle: assignment.sectionTitle,
6850
+ dependsOn: [],
6553
6851
  executionMode: "parallel"
6554
6852
  }];
6555
6853
  }
@@ -6669,6 +6967,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6669
6967
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
6670
6968
  const worker = workerMap.get(id);
6671
6969
  const result = await worker.execute(assignment, taskId, waveSignal);
6970
+ if (result.localOnly) {
6971
+ 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}]`;
6972
+ } else {
6973
+ if (typeof result.output === "string" && result.output) {
6974
+ result.output = RedactionLayer.redact(result.output);
6975
+ }
6976
+ }
6977
+ if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
6672
6978
  resultMap.set(id, result);
6673
6979
  return result;
6674
6980
  };
@@ -6884,6 +7190,7 @@ ${peerOutputs}` : "";
6884
7190
  chunkEnd++;
6885
7191
  }
6886
7192
  i = chunkEnd;
7193
+ const isLastChunk = chunkEnd >= completed.length;
6887
7194
  const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
6888
7195
  ${currentSummary ? `
6889
7196
  PREVIOUS SUMMARY SO FAR:
@@ -6893,6 +7200,7 @@ NEW OUTPUTS TO INTEGRATE:
6893
7200
  ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
6894
7201
  const messages = [{ role: "user", content: prompt }];
6895
7202
  try {
7203
+ const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
6896
7204
  const result = await this.router.generate("T2", {
6897
7205
  messages,
6898
7206
  systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
@@ -6900,7 +7208,7 @@ NEW OUTPUTS TO INTEGRATE:
6900
7208
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6901
7209
  maxTokens: 500,
6902
7210
  ...this.sectionModel ? { model: this.sectionModel } : {}
6903
- });
7211
+ }, streamFinal);
6904
7212
  currentSummary = result.content;
6905
7213
  } catch (err) {
6906
7214
  this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -6952,14 +7260,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6952
7260
  ...this.sectionModel ? { model: this.sectionModel } : {}
6953
7261
  });
6954
7262
  const answer = result.content.trim().toUpperCase();
6955
- if (answer.includes("YES")) {
6956
- return { requestId: req.id, approved: true, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: consistent with section goal" };
6957
- }
6958
- if (answer.includes("NO")) {
6959
- return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
6960
- }
7263
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
7264
+ (req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
6961
7265
  return null;
6962
7266
  } catch {
7267
+ (req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
6963
7268
  return null;
6964
7269
  }
6965
7270
  }
@@ -7301,10 +7606,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
7301
7606
  }
7302
7607
  }
7303
7608
  async decomposeTask(prompt, systemContext) {
7609
+ const db = this.router.getWorldStateDB?.();
7610
+ const worldStateContext = db ? `
7611
+
7612
+ PROJECT WORLD STATE (Current architecture and recent changes):
7613
+ ${db.getFormattedState()}` : "";
7304
7614
  const contextSection = systemContext ? `
7305
7615
  Project context:
7306
7616
  ${systemContext}` : "";
7307
- const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
7617
+ const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
7308
7618
 
7309
7619
  Task: ${prompt}
7310
7620
 
@@ -7636,7 +7946,7 @@ Instructions:
7636
7946
  systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
7637
7947
  maxTokens: 8e3
7638
7948
  }, (chunk) => {
7639
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
7949
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
7640
7950
  });
7641
7951
  return result.content;
7642
7952
  }
@@ -7665,14 +7975,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
7665
7975
  temperature: 0
7666
7976
  });
7667
7977
  const answer = result.content.trim().toUpperCase();
7668
- if (answer.includes("YES")) {
7669
- return { requestId: req.id, approved: true, always: true, decidedBy: "T1", reasoning: "T1 evaluated: consistent with overall task goal" };
7670
- }
7671
- if (answer.includes("NO")) {
7672
- return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
7673
- }
7978
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
7979
+ (req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
7674
7980
  return null;
7675
7981
  } catch {
7982
+ (req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
7676
7983
  return null;
7677
7984
  }
7678
7985
  }
@@ -7792,16 +8099,16 @@ function resolveInWorkspace(workspaceRoot, input) {
7792
8099
  if (typeof input !== "string" || input.length === 0) {
7793
8100
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
7794
8101
  }
7795
- const root = path23.resolve(workspaceRoot);
7796
- const abs = path23.isAbsolute(input) ? path23.resolve(input) : path23.resolve(root, input);
7797
- const rel = path23.relative(root, abs);
7798
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path23.isAbsolute(rel)) {
8102
+ const root = path25.resolve(workspaceRoot);
8103
+ const abs = path25.isAbsolute(input) ? path25.resolve(input) : path25.resolve(root, input);
8104
+ const rel = path25.relative(root, abs);
8105
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25.isAbsolute(rel)) {
7799
8106
  throw new WorkspaceSandboxError(input, root);
7800
8107
  }
7801
8108
  try {
7802
- const real = fs21.realpathSync(abs);
7803
- const realRel = path23.relative(root, real);
7804
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path23.isAbsolute(realRel))) {
8109
+ const real = fs23.realpathSync(abs);
8110
+ const realRel = path25.relative(root, real);
8111
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25.isAbsolute(realRel))) {
7805
8112
  throw new WorkspaceSandboxError(input, root);
7806
8113
  }
7807
8114
  } catch (e) {
@@ -7862,7 +8169,7 @@ var FileWriteTool = class extends BaseTool {
7862
8169
  } catch {
7863
8170
  }
7864
8171
  }
7865
- await fs9.mkdir(path23.dirname(absPath), { recursive: true });
8172
+ await fs9.mkdir(path25.dirname(absPath), { recursive: true });
7866
8173
  await fs9.writeFile(absPath, content, "utf-8");
7867
8174
  return `Written ${content.length} characters to ${filePath}`;
7868
8175
  }
@@ -8360,7 +8667,7 @@ var ImageAnalyzeTool = class extends BaseTool {
8360
8667
  };
8361
8668
  async function fileToImageAttachment(filePath) {
8362
8669
  const data = await fs9.readFile(filePath);
8363
- const ext = path23.extname(filePath).toLowerCase();
8670
+ const ext = path25.extname(filePath).toLowerCase();
8364
8671
  const mimeMap = {
8365
8672
  ".jpg": "image/jpeg",
8366
8673
  ".jpeg": "image/jpeg",
@@ -8394,14 +8701,14 @@ var PDFCreateTool = class extends BaseTool {
8394
8701
  const filePath = input["path"];
8395
8702
  const content = input["content"];
8396
8703
  const title = input["title"];
8397
- const dir = path23.dirname(filePath);
8398
- if (!fs21.existsSync(dir)) {
8399
- fs21.mkdirSync(dir, { recursive: true });
8704
+ const dir = path25.dirname(filePath);
8705
+ if (!fs23.existsSync(dir)) {
8706
+ fs23.mkdirSync(dir, { recursive: true });
8400
8707
  }
8401
8708
  return new Promise((resolve, reject) => {
8402
8709
  try {
8403
8710
  const doc = new PDFDocument({ margin: 50 });
8404
- const stream = fs21.createWriteStream(filePath);
8711
+ const stream = fs23.createWriteStream(filePath);
8405
8712
  doc.pipe(stream);
8406
8713
  if (title) {
8407
8714
  doc.info["Title"] = title;
@@ -8479,22 +8786,22 @@ var CodeInterpreterTool = class extends BaseTool {
8479
8786
  }
8480
8787
  cmdPrefix = NODE_CMD;
8481
8788
  }
8482
- const tmpDir = path23.join(this.workspaceRoot, ".cascade", "tmp");
8483
- if (!fs21.existsSync(tmpDir)) {
8484
- fs21.mkdirSync(tmpDir, { recursive: true });
8789
+ const tmpDir = path25.join(this.workspaceRoot, ".cascade", "tmp");
8790
+ if (!fs23.existsSync(tmpDir)) {
8791
+ fs23.mkdirSync(tmpDir, { recursive: true });
8485
8792
  }
8486
8793
  const extension = language === "python" ? "py" : "js";
8487
8794
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
8488
- const filePath = path23.join(tmpDir, fileName);
8489
- fs21.writeFileSync(filePath, code, "utf-8");
8795
+ const filePath = path25.join(tmpDir, fileName);
8796
+ fs23.writeFileSync(filePath, code, "utf-8");
8490
8797
  const execArgs = [filePath, ...args];
8491
8798
  return new Promise((resolve) => {
8492
8799
  const startMs = Date.now();
8493
8800
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
8494
8801
  const duration = Date.now() - startMs;
8495
8802
  try {
8496
- if (fs21.existsSync(filePath)) {
8497
- fs21.unlinkSync(filePath);
8803
+ if (fs23.existsSync(filePath)) {
8804
+ fs23.unlinkSync(filePath);
8498
8805
  }
8499
8806
  } catch (cleanupErr) {
8500
8807
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -8773,7 +9080,7 @@ var GlobTool = class extends BaseTool {
8773
9080
  };
8774
9081
  async execute(input, _options) {
8775
9082
  const pattern = input["pattern"];
8776
- const searchPath = input["path"] ? path23.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9083
+ const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
8777
9084
  const matches = await glob(pattern, {
8778
9085
  cwd: searchPath,
8779
9086
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -8786,7 +9093,7 @@ var GlobTool = class extends BaseTool {
8786
9093
  const withMtime = await Promise.all(
8787
9094
  matches.map(async (rel) => {
8788
9095
  try {
8789
- const stat = await fs9.stat(path23.join(searchPath, rel));
9096
+ const stat = await fs9.stat(path25.join(searchPath, rel));
8790
9097
  return { rel, mtime: stat.mtimeMs };
8791
9098
  } catch {
8792
9099
  return { rel, mtime: 0 };
@@ -8835,7 +9142,7 @@ var GrepTool = class extends BaseTool {
8835
9142
  };
8836
9143
  async execute(input, _options) {
8837
9144
  const pattern = input["pattern"];
8838
- const searchPath = input["path"] ? path23.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9145
+ const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
8839
9146
  const globPattern = input["glob"];
8840
9147
  const outputMode = input["output_mode"] ?? "content";
8841
9148
  const context = input["context"] ?? 0;
@@ -8889,12 +9196,12 @@ var GrepTool = class extends BaseTool {
8889
9196
  nodir: true
8890
9197
  });
8891
9198
  } catch {
8892
- files = [path23.relative(searchPath, searchPath) || "."];
9199
+ files = [path25.relative(searchPath, searchPath) || "."];
8893
9200
  }
8894
9201
  const results = [];
8895
9202
  let totalCount = 0;
8896
9203
  for (const rel of files) {
8897
- const abs = path23.join(searchPath, rel);
9204
+ const abs = path25.join(searchPath, rel);
8898
9205
  let content;
8899
9206
  try {
8900
9207
  content = await fs9.readFile(abs, "utf-8");
@@ -9256,10 +9563,10 @@ var ToolRegistry = class extends EventEmitter {
9256
9563
  }
9257
9564
  isIgnored(filePath) {
9258
9565
  if (!filePath) return false;
9259
- const abs = path23.resolve(this.workspaceRoot, filePath);
9260
- const rel = path23.relative(this.workspaceRoot, abs);
9261
- if (!rel || rel.startsWith("..") || path23.isAbsolute(rel)) return true;
9262
- const posixRel = rel.split(path23.sep).join("/");
9566
+ const abs = path25.resolve(this.workspaceRoot, filePath);
9567
+ const rel = path25.relative(this.workspaceRoot, abs);
9568
+ if (!rel || rel.startsWith("..") || path25.isAbsolute(rel)) return true;
9569
+ const posixRel = rel.split(path25.sep).join("/");
9263
9570
  return this.ignoreMatcher.ignores(posixRel);
9264
9571
  }
9265
9572
  };
@@ -9376,6 +9683,9 @@ var McpClient = class _McpClient {
9376
9683
  return this.clients.has(serverName);
9377
9684
  }
9378
9685
  };
9686
+
9687
+ // src/core/cascade.ts
9688
+ init_audit_logger();
9379
9689
  var SAFE_TOOLS = /* @__PURE__ */ new Set([
9380
9690
  "file_read",
9381
9691
  "file_list",
@@ -9759,9 +10069,10 @@ var TaskAnalyzer = class {
9759
10069
  analysisCache.clear();
9760
10070
  }
9761
10071
  };
9762
- var DEFAULT_STATS_FILE = path23.join(os6.homedir(), ".cascade", "model-perf.json");
10072
+ var DEFAULT_STATS_FILE = path25.join(os8.homedir(), ".cascade", "model-perf.json");
9763
10073
  var ModelPerformanceTracker = class {
9764
10074
  stats = /* @__PURE__ */ new Map();
10075
+ featureStats = /* @__PURE__ */ new Map();
9765
10076
  statsFile;
9766
10077
  loaded = false;
9767
10078
  constructor(statsFile = DEFAULT_STATS_FILE) {
@@ -9773,18 +10084,29 @@ var ModelPerformanceTracker = class {
9773
10084
  try {
9774
10085
  const raw = await fs9.readFile(this.statsFile, "utf-8");
9775
10086
  const parsed = JSON.parse(raw);
9776
- for (const [key, stat] of Object.entries(parsed)) {
9777
- this.stats.set(key, stat);
10087
+ if (parsed.models) {
10088
+ for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
10089
+ } else {
10090
+ for (const [key, stat] of Object.entries(parsed)) {
10091
+ if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
10092
+ this.stats.set(key, stat);
10093
+ }
10094
+ }
10095
+ }
10096
+ if (parsed.features) {
10097
+ for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
9778
10098
  }
9779
10099
  } catch {
9780
10100
  }
9781
10101
  }
9782
10102
  async save() {
9783
10103
  try {
9784
- await fs9.mkdir(path23.dirname(this.statsFile), { recursive: true });
9785
- const obj = {};
9786
- for (const [key, stat] of this.stats) obj[key] = stat;
9787
- await fs9.writeFile(this.statsFile, JSON.stringify(obj, null, 2), "utf-8");
10104
+ await fs9.mkdir(path25.dirname(this.statsFile), { recursive: true });
10105
+ const modelsObj = {};
10106
+ const featuresObj = {};
10107
+ for (const [key, stat] of this.stats) modelsObj[key] = stat;
10108
+ for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
10109
+ await fs9.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
9788
10110
  } catch {
9789
10111
  }
9790
10112
  }
@@ -9805,6 +10127,13 @@ var ModelPerformanceTracker = class {
9805
10127
  sampleCount: s.sampleCount + 1
9806
10128
  });
9807
10129
  }
10130
+ recordFeatureCost(featureTag, costUsd) {
10131
+ const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
10132
+ this.featureStats.set(featureTag, {
10133
+ totalCostUsd: s.totalCostUsd + costUsd,
10134
+ runCount: s.runCount + 1
10135
+ });
10136
+ }
9808
10137
  /**
9809
10138
  * Record an explicit user rating (good/bad). Counts as 3 automatic samples
9810
10139
  * so user feedback carries significantly more weight than auto-detected outcomes.
@@ -9819,6 +10148,9 @@ var ModelPerformanceTracker = class {
9819
10148
  getAll() {
9820
10149
  return new Map(this.stats);
9821
10150
  }
10151
+ getAllFeatures() {
10152
+ return new Map(this.featureStats);
10153
+ }
9822
10154
  /**
9823
10155
  * Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
9824
10156
  * High retry counts penalise the score.
@@ -10183,7 +10515,7 @@ Required capability: ${description.slice(0, 300)}`;
10183
10515
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
10184
10516
  async loadPersistedTools() {
10185
10517
  if (!this.workspacePath || !this.persistEnabled) return;
10186
- const file = path23.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
10518
+ const file = path25.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
10187
10519
  try {
10188
10520
  const raw = await fs9.readFile(file, "utf-8");
10189
10521
  const specs = JSON.parse(raw);
@@ -10207,8 +10539,8 @@ Required capability: ${description.slice(0, 300)}`;
10207
10539
  }
10208
10540
  async persist() {
10209
10541
  if (!this.workspacePath || !this.persistEnabled) return;
10210
- const dir = path23.join(this.workspacePath, ".cascade");
10211
- const file = path23.join(dir, DYNAMIC_TOOLS_FILE);
10542
+ const dir = path25.join(this.workspacePath, ".cascade");
10543
+ const file = path25.join(dir, DYNAMIC_TOOLS_FILE);
10212
10544
  try {
10213
10545
  await fs9.mkdir(dir, { recursive: true });
10214
10546
  await fs9.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -10221,6 +10553,166 @@ Required capability: ${description.slice(0, 300)}`;
10221
10553
  return Array.from(this.specs.keys());
10222
10554
  }
10223
10555
  };
10556
+ var WorldStateDB = class {
10557
+ constructor(workspacePath, debugMode = false) {
10558
+ this.workspacePath = workspacePath;
10559
+ this.debugMode = debugMode;
10560
+ const cascadeDir = path25.join(workspacePath, ".cascade");
10561
+ if (!fs23.existsSync(cascadeDir)) {
10562
+ fs23.mkdirSync(cascadeDir, { recursive: true });
10563
+ }
10564
+ this.keyPath = path25.join(cascadeDir, "world_state.key");
10565
+ this.dbPath = path25.join(cascadeDir, "world_state.db");
10566
+ this.initEncryptionKey();
10567
+ this.db = new Database2(this.dbPath);
10568
+ this.db.pragma("journal_mode = WAL");
10569
+ this.db.exec(`
10570
+ CREATE TABLE IF NOT EXISTS world_state (
10571
+ id TEXT PRIMARY KEY,
10572
+ timestamp TEXT NOT NULL,
10573
+ worker_id TEXT NOT NULL,
10574
+ encrypted_payload TEXT NOT NULL
10575
+ )
10576
+ `);
10577
+ }
10578
+ workspacePath;
10579
+ debugMode;
10580
+ db;
10581
+ keyPath;
10582
+ dbPath;
10583
+ encryptionKey;
10584
+ initEncryptionKey() {
10585
+ if (fs23.existsSync(this.keyPath)) {
10586
+ this.encryptionKey = fs23.readFileSync(this.keyPath);
10587
+ } else {
10588
+ this.encryptionKey = crypto.randomBytes(32);
10589
+ fs23.writeFileSync(this.keyPath, this.encryptionKey);
10590
+ }
10591
+ }
10592
+ encrypt(text) {
10593
+ const iv = crypto.randomBytes(16);
10594
+ const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
10595
+ let encrypted = cipher.update(text, "utf8", "hex");
10596
+ encrypted += cipher.final("hex");
10597
+ const authTag = cipher.getAuthTag().toString("hex");
10598
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
10599
+ }
10600
+ decrypt(text) {
10601
+ const parts = text.split(":");
10602
+ if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
10603
+ const [ivHex, authTagHex, encryptedHex] = parts;
10604
+ const iv = Buffer.from(ivHex, "hex");
10605
+ const authTag = Buffer.from(authTagHex, "hex");
10606
+ const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
10607
+ decipher.setAuthTag(authTag);
10608
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
10609
+ decrypted += decipher.final("utf8");
10610
+ return decrypted;
10611
+ }
10612
+ addEntry(workerId, summary) {
10613
+ const id = crypto.randomUUID();
10614
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10615
+ const payload = JSON.stringify({ summary });
10616
+ const encryptedPayload = this.encrypt(payload);
10617
+ const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
10618
+ stmt.run(id, timestamp, workerId, encryptedPayload);
10619
+ this.dumpDebugIfNeeded();
10620
+ return id;
10621
+ }
10622
+ getAllEntries() {
10623
+ const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
10624
+ const rows = stmt.all();
10625
+ return rows.map((row) => {
10626
+ try {
10627
+ const decrypted = this.decrypt(row.encrypted_payload);
10628
+ const parsed = JSON.parse(decrypted);
10629
+ return {
10630
+ id: row.id,
10631
+ timestamp: row.timestamp,
10632
+ workerId: row.worker_id,
10633
+ summary: parsed.summary
10634
+ };
10635
+ } catch (err) {
10636
+ return {
10637
+ id: row.id,
10638
+ timestamp: row.timestamp,
10639
+ workerId: row.worker_id,
10640
+ summary: "[Decryption Failed - Payload Corrupted]"
10641
+ };
10642
+ }
10643
+ });
10644
+ }
10645
+ getFormattedState() {
10646
+ const entries = this.getAllEntries();
10647
+ if (entries.length === 0) return "World State is currently empty.";
10648
+ return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
10649
+ }
10650
+ dumpDebugIfNeeded() {
10651
+ if (!this.debugMode) return;
10652
+ try {
10653
+ const dumpPath = path25.join(os8.tmpdir(), "cascade_world_state_debug.json");
10654
+ const entries = this.getAllEntries();
10655
+ fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
10656
+ } catch (err) {
10657
+ console.error("Failed to dump debug world state", err);
10658
+ }
10659
+ }
10660
+ close() {
10661
+ this.db.close();
10662
+ }
10663
+ };
10664
+ var ignore3 = _ignoreModule.default ?? _ignoreModule;
10665
+ var PrivacyPaths = class {
10666
+ localOnly;
10667
+ hasRules;
10668
+ constructor(policies = []) {
10669
+ this.localOnly = ignore3();
10670
+ const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
10671
+ if (patterns.length) this.localOnly.add(patterns);
10672
+ this.hasRules = patterns.length > 0;
10673
+ }
10674
+ /** True when any privacy rules are configured at all (cheap short-circuit). */
10675
+ hasPolicies() {
10676
+ return this.hasRules;
10677
+ }
10678
+ /** True when the given workspace-relative path falls under a local-only policy. */
10679
+ isLocalOnly(relativePath) {
10680
+ if (!this.hasRules || !relativePath) return false;
10681
+ try {
10682
+ return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
10683
+ } catch {
10684
+ return false;
10685
+ }
10686
+ }
10687
+ /** True when ANY of the given paths falls under a local-only policy. */
10688
+ anyLocalOnly(relativePaths) {
10689
+ return relativePaths.some((p) => this.isLocalOnly(p));
10690
+ }
10691
+ };
10692
+
10693
+ // src/core/steering/guidance.ts
10694
+ var GuidanceQueue = class {
10695
+ entries = [];
10696
+ /** Per-consumer read cursor so a broadcast entry reaches each worker once. */
10697
+ cursors = /* @__PURE__ */ new Map();
10698
+ push(text, nodeId) {
10699
+ const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
10700
+ this.entries.push(entry);
10701
+ return entry;
10702
+ }
10703
+ /**
10704
+ * New entries for this consumer since its last drain, filtered to entries
10705
+ * that target it (or target everyone). Advances the consumer's cursor.
10706
+ */
10707
+ drain(consumerId) {
10708
+ const from = this.cursors.get(consumerId) ?? 0;
10709
+ this.cursors.set(consumerId, this.entries.length);
10710
+ return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
10711
+ }
10712
+ get size() {
10713
+ return this.entries.length;
10714
+ }
10715
+ };
10224
10716
 
10225
10717
  // src/core/cascade.ts
10226
10718
  var Cascade = class _Cascade extends EventEmitter {
@@ -10240,6 +10732,9 @@ var Cascade = class _Cascade extends EventEmitter {
10240
10732
  taskAnalyzer;
10241
10733
  perfTracker;
10242
10734
  toolCreator;
10735
+ worldStateDB;
10736
+ encryptedAuditLogger;
10737
+ guidanceQueue;
10243
10738
  workspacePath;
10244
10739
  constructor(config, workspacePath, store) {
10245
10740
  super();
@@ -10261,6 +10756,23 @@ var Cascade = class _Cascade extends EventEmitter {
10261
10756
  });
10262
10757
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
10263
10758
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
10759
+ this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
10760
+ this.router.setWorldStateDB(this.worldStateDB);
10761
+ this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
10762
+ const privacyPolicies = this.config.privacy?.paths ?? [];
10763
+ if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
10764
+ this.guidanceQueue = new GuidanceQueue();
10765
+ this.router.setGuidanceQueue(this.guidanceQueue);
10766
+ }
10767
+ /**
10768
+ * Live intervention: inject a user correction into the running hierarchy.
10769
+ * Every active T3 worker (or only the one matching `nodeId`) picks it up at
10770
+ * the top of its next agent-loop iteration as a USER INTERVENTION message.
10771
+ */
10772
+ injectGuidance(text, nodeId) {
10773
+ const entry = this.guidanceQueue.push(text, nodeId);
10774
+ this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
10775
+ this.emit("guidance:injected", entry);
10264
10776
  }
10265
10777
  initOptionalFeatures() {
10266
10778
  if (this.config.cascadeAuto === true) {
@@ -10423,6 +10935,12 @@ ${last.partialOutput}` : "");
10423
10935
  if (!prompt) return null;
10424
10936
  return this.run({ prompt });
10425
10937
  }
10938
+ getWorkspacePath() {
10939
+ return this.workspacePath;
10940
+ }
10941
+ getWorldStateDB() {
10942
+ return this.worldStateDB;
10943
+ }
10426
10944
  /**
10427
10945
  * Record an explicit user rating for the last completed run.
10428
10946
  * Explicit ratings carry 3× the weight of auto-detected outcomes so user
@@ -10509,6 +11027,11 @@ ${last.partialOutput}` : "");
10509
11027
  }
10510
11028
  this.initOptionalFeatures();
10511
11029
  if (this.toolCreator) await this.toolCreator.loadPersistedTools();
11030
+ if (this.encryptedAuditLogger) {
11031
+ this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
11032
+ this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
11033
+ this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
11034
+ }
10512
11035
  this.initialized = true;
10513
11036
  })();
10514
11037
  try {
@@ -10552,6 +11075,21 @@ ${last.partialOutput}` : "");
10552
11075
  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);
10553
11076
  return inquiry && !producesArtifact;
10554
11077
  }
11078
+ /**
11079
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
11080
+ * multiple sections/workers). Deliberately conservative: requires a
11081
+ * build/implementation verb AND either an app/system-scale noun or an
11082
+ * explicit multi-part structure, so ordinary single-file asks (handled as
11083
+ * Simple/Moderate) don't get over-escalated.
11084
+ */
11085
+ looksClearlyComplex(prompt) {
11086
+ const p = prompt.trim();
11087
+ if (p.length < 24) return false;
11088
+ const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11089
+ 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);
11090
+ const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11091
+ return buildVerb && (scaleNoun || multiPart);
11092
+ }
10555
11093
  // Cache glob scan results per workspace path to avoid repeated I/O.
10556
11094
  static globCache = /* @__PURE__ */ new Map();
10557
11095
  async countWorkspaceFiles(workspacePath) {
@@ -10635,10 +11173,16 @@ ${prompt}` : prompt;
10635
11173
  let verdict;
10636
11174
  if (match) {
10637
11175
  verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
10638
- this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11176
+ if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11177
+ this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11178
+ verdict = "Complex";
11179
+ } else {
11180
+ this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11181
+ }
10639
11182
  } else {
10640
- verdict = prompt.trim().split(/\s+/).length <= 12 ? "Simple" : "Moderate";
10641
- this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length`);
11183
+ const words = prompt.trim().split(/\s+/).length;
11184
+ verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
11185
+ this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
10642
11186
  }
10643
11187
  return verdict;
10644
11188
  } catch {
@@ -10690,7 +11234,15 @@ ${prompt}` : prompt;
10690
11234
  }
10691
11235
  escalator.resolveUserDecision(req.id, approved, always);
10692
11236
  });
10693
- const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
11237
+ const forceTier = this.config.routing?.forceTier;
11238
+ const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
11239
+ let complexity;
11240
+ if (forced) {
11241
+ complexity = forced;
11242
+ this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
11243
+ } else {
11244
+ complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
11245
+ }
10694
11246
  this.telemetry.capture("cascade:session_start", {
10695
11247
  complexity,
10696
11248
  providerCount: this.config.providers.length,
@@ -10770,6 +11322,7 @@ ${prompt}` : prompt;
10770
11322
  try {
10771
11323
  if (complexity === "Simple") {
10772
11324
  const t3 = new T3Worker(this.router, this.toolRegistry, "root");
11325
+ t3.setPresenter(true);
10773
11326
  t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
10774
11327
  if (identityPrompt) {
10775
11328
  t3.setSystemPromptOverride(identityPrompt);
@@ -10794,6 +11347,7 @@ ${prompt}` : prompt;
10794
11347
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
10795
11348
  } else if (complexity === "Moderate") {
10796
11349
  const t2 = new T2Manager(this.router, this.toolRegistry, "root");
11350
+ t2.setPresenter(true);
10797
11351
  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.");
10798
11352
  if (identityPrompt) {
10799
11353
  t2.setSystemPromptOverride(identityPrompt);
@@ -10843,6 +11397,7 @@ ${prompt}` : prompt;
10843
11397
  }
10844
11398
  } else {
10845
11399
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
11400
+ t1.setPresenter(true);
10846
11401
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
10847
11402
  if (identityPrompt) {
10848
11403
  t1.setSystemPromptOverride(identityPrompt);
@@ -10934,6 +11489,7 @@ ${prompt}` : prompt;
10934
11489
  durationMs,
10935
11490
  costByTier: stats.costByTier,
10936
11491
  tokensByTier: stats.tokensByTier,
11492
+ costByFeature: stats.costByFeature,
10937
11493
  costPercentByTier: this.router.getTierCostPercentages()
10938
11494
  };
10939
11495
  }
@@ -11183,6 +11739,30 @@ var SlashCommandRegistry = class {
11183
11739
  return { output: out || "File changes rolled back.", handled: true };
11184
11740
  }
11185
11741
  });
11742
+ this.register({
11743
+ command: "/steer",
11744
+ description: "Steer a running task: /steer <correction for the active workers>",
11745
+ args: ["<guidance>"],
11746
+ handler: async (args, ctx) => {
11747
+ const output = await ctx.onSteer(args);
11748
+ return { output, handled: true };
11749
+ }
11750
+ });
11751
+ this.register({
11752
+ command: "/audit",
11753
+ description: "Verify the tamper-evident audit log (hash chain integrity)",
11754
+ handler: async (_args, ctx) => {
11755
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
11756
+ const logger = new AuditLogger3(ctx.workspacePath);
11757
+ try {
11758
+ const v = logger.verifyChain();
11759
+ 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.`;
11760
+ return { output, handled: true };
11761
+ } finally {
11762
+ logger.close();
11763
+ }
11764
+ }
11765
+ });
11186
11766
  this.register({
11187
11767
  command: "/branch",
11188
11768
  description: "Fork current session into parallel branches",
@@ -11830,7 +12410,8 @@ function CostTracker({
11830
12410
  tokensByTier,
11831
12411
  compact = false,
11832
12412
  savedUsd = 0,
11833
- savedPct = 0
12413
+ savedPct = 0,
12414
+ costByFeature
11834
12415
  }) {
11835
12416
  const hasTierCost = costByTier && Object.keys(costByTier).length > 0;
11836
12417
  const savingsLine = savedUsd > 0 ? `Saved $${savedUsd.toFixed(4)} (${savedPct}%) vs. running every call on T1` : null;
@@ -11929,6 +12510,16 @@ function CostTracker({
11929
12510
  ] }, tier);
11930
12511
  })
11931
12512
  ] })
12513
+ ] }),
12514
+ costByFeature && Object.keys(costByFeature).length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
12515
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: "By feature/section:" }),
12516
+ Object.entries(costByFeature).map(([feature, cost]) => /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
12517
+ /* @__PURE__ */ jsx(Box, { width: 30, children: /* @__PURE__ */ jsx(Text, { color: theme.colors.secondary, wrap: "truncate-end", children: feature }) }),
12518
+ /* @__PURE__ */ jsxs(Text, { color: theme.colors.success, children: [
12519
+ "$",
12520
+ cost.toFixed(6)
12521
+ ] })
12522
+ ] }, feature))
11932
12523
  ] })
11933
12524
  ] });
11934
12525
  }
@@ -12342,7 +12933,7 @@ function replReducer(state, action) {
12342
12933
  case "SET_TREE":
12343
12934
  return { ...state, agentTree: action.tree };
12344
12935
  case "UPDATE_COST":
12345
- 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 };
12936
+ 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 };
12346
12937
  case "SET_APPROVAL":
12347
12938
  return { ...state, approvalRequest: action.request };
12348
12939
  case "SET_EXECUTING":
@@ -12356,7 +12947,7 @@ function replReducer(state, action) {
12356
12947
  case "TOGGLE_COMMS":
12357
12948
  return { ...state, showComms: !state.showComms };
12358
12949
  case "CLEAR":
12359
- return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
12950
+ return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
12360
12951
  case "CLEAR_TREE":
12361
12952
  return { ...state, agentTree: null };
12362
12953
  case "TOGGLE_COST":
@@ -12405,7 +12996,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12405
12996
  const [slashIndex, setSlashIndex] = useState(0);
12406
12997
  const [identities, setIdentities] = useState([]);
12407
12998
  const [currentIdentityId, setCurrentIdentityId] = useState(config.defaultIdentityId);
12408
- const [state, dispatch] = 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 });
12999
+ const [state, dispatch] = 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 });
12409
13000
  const [isShowingModels, setIsShowingModels] = useState(false);
12410
13001
  const [planApprovalRequest, setPlanApprovalRequest] = useState(null);
12411
13002
  const [historyOffset, setHistoryOffset] = useState(0);
@@ -12474,9 +13065,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12474
13065
  }
12475
13066
  } catch {
12476
13067
  }
12477
- const configPath = path23.join(workspacePath, ".cascade", "config.json");
13068
+ const configPath = path25.join(workspacePath, ".cascade", "config.json");
12478
13069
  try {
12479
- await fs9.mkdir(path23.dirname(configPath), { recursive: true });
13070
+ await fs9.mkdir(path25.dirname(configPath), { recursive: true });
12480
13071
  await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
12481
13072
  } catch (err) {
12482
13073
  const msg = err instanceof Error ? err.message : String(err);
@@ -12528,9 +13119,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12528
13119
  if (msg.includes("non-text parts")) return;
12529
13120
  originalLog(...args);
12530
13121
  };
12531
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
13122
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
12532
13123
  storeRef.current = store;
12533
- globalStoreRef.current = new MemoryStore(path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
13124
+ globalStoreRef.current = new MemoryStore(path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
12534
13125
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
12535
13126
  setIdentities(identityRows);
12536
13127
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -12593,7 +13184,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12593
13184
  onThemeChange: (name) => setTheme(getTheme(name)),
12594
13185
  onExport: async (fmt) => {
12595
13186
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12596
- const exportPath = path23.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
13187
+ const exportPath = path25.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
12597
13188
  if (fmt === "json") {
12598
13189
  await fs9.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
12599
13190
  } else {
@@ -12617,6 +13208,14 @@ ${msg.content}`).join("\n\n");
12617
13208
  }
12618
13209
  return `Restored ${snapshots.length} files to their initial session state.`;
12619
13210
  },
13211
+ onSteer: (args) => {
13212
+ const text = args.join(" ").trim();
13213
+ if (!text) return "Usage: /steer <correction for the active workers>";
13214
+ const cascade = cascadeRef.current;
13215
+ if (!cascade) return "No active Cascade instance.";
13216
+ cascade.injectGuidance(text);
13217
+ return "Guidance queued \u2014 active workers apply it on their next step. (No task running? It applies to the next run's workers.)";
13218
+ },
12620
13219
  onBranch: async () => {
12621
13220
  const store = storeRef.current;
12622
13221
  if (!store) return;
@@ -12935,7 +13534,7 @@ ${lastUser.content}`;
12935
13534
  if (lastTool !== null) dispatch({ type: "SET_ACTIVE_TOOL", toolName: lastTool });
12936
13535
  const stats = cascade.getRouter().getStats();
12937
13536
  const savings = cascade.getRouter().getDelegationSavings();
12938
- 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 });
13537
+ 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 });
12939
13538
  statusThrottleTimeout = null;
12940
13539
  };
12941
13540
  cascade.on("tier:root", onRoot);
@@ -13027,7 +13626,7 @@ ${lastUser.content}`;
13027
13626
  flushPeerEvents();
13028
13627
  const stats = cascade.getRouter().getStats();
13029
13628
  const savings = cascade.getRouter().getDelegationSavings();
13030
- 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 });
13629
+ 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 });
13031
13630
  dispatch({ type: "COMMIT_STREAM", finalText: result.output, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
13032
13631
  persistMessage("assistant", result.output, (/* @__PURE__ */ new Date()).toISOString());
13033
13632
  const receipt = formatRunReceipt(result, stats.totalCostUsd, savings);
@@ -13312,7 +13911,7 @@ ${lastUser.content}`;
13312
13911
  ),
13313
13912
  adaptiveMode === "wide" && state.showComms && liveBudget.commsMaxEvents > 0 && /* @__PURE__ */ jsx(PeerFeed, { events: state.peerEvents, theme, maxRows: liveBudget.commsMaxEvents }),
13314
13913
  adaptiveMode === "wide" && state.showDetails && liveBudget.showTimeline && /* @__PURE__ */ jsx(TimelinePanel, { nodes: [...treeNodesRef.current.values()], theme, currentIndex: timelineIndex, onChangeIndex: setTimelineIndex }),
13315
- adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ 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 }),
13914
+ adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ 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 }),
13316
13915
  liveBudget.collapsed && (state.showCost || state.showDetails || state.agentTree != null) && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u25B8 panels collapsed (small terminal)" }),
13317
13916
  state.approvalRequest && /* @__PURE__ */ jsx(ApprovalPrompt, { request: state.approvalRequest, theme, onDecision: (decision) => {
13318
13917
  dispatch({ type: "SET_APPROVAL", request: null });
@@ -13481,7 +14080,7 @@ function stringifySlashOutput(val) {
13481
14080
  }
13482
14081
  async function searchSessionsAndMessages(query, workspacePath) {
13483
14082
  if (!query) return "Usage: /search <query>";
13484
- const dbPath = path23.join(workspacePath, CASCADE_DB_FILE);
14083
+ const dbPath = path25.join(workspacePath, CASCADE_DB_FILE);
13485
14084
  try {
13486
14085
  await fs9.access(dbPath);
13487
14086
  } catch {
@@ -13515,7 +14114,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
13515
14114
  async function diagnoseRuntime(config, workspacePath) {
13516
14115
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
13517
14116
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
13518
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
14117
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
13519
14118
  try {
13520
14119
  const sessions = store.listSessions(void 0, 3);
13521
14120
  return [
@@ -13534,7 +14133,7 @@ async function diagnoseRuntime(config, workspacePath) {
13534
14133
  }
13535
14134
  async function showRecentLogs(args, workspacePath) {
13536
14135
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
13537
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
14136
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
13538
14137
  try {
13539
14138
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
13540
14139
  if (!logs.length) return "No recent runtime logs.";
@@ -13546,7 +14145,7 @@ async function showRecentLogs(args, workspacePath) {
13546
14145
  async function loadSessionSnapshot(args, workspacePath) {
13547
14146
  const sessionId = args[0];
13548
14147
  if (!sessionId) return "Usage: /resume <sessionId>";
13549
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
14148
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
13550
14149
  try {
13551
14150
  const session = store.getSession(sessionId);
13552
14151
  if (!session) return `Session not found: ${sessionId}`;
@@ -13966,9 +14565,9 @@ function SetupWizard({ workspacePath, onComplete }) {
13966
14565
  ...anyAuto ? { cascadeAuto: true } : {}
13967
14566
  };
13968
14567
  const config = CascadeConfigSchema.parse(rawConfig);
13969
- const configDir = path23.join(workspacePath, ".cascade");
14568
+ const configDir = path25.join(workspacePath, ".cascade");
13970
14569
  await fs9.mkdir(configDir, { recursive: true });
13971
- const configPath = path23.join(workspacePath, CASCADE_CONFIG_FILE);
14570
+ const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
13972
14571
  await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
13973
14572
  savedConfigRef.current = config;
13974
14573
  dispatchRef.current({ type: "GO_DONE" });
@@ -14290,14 +14889,14 @@ function printTelemetryBanner() {
14290
14889
  async function initCommand(workspacePath = process.cwd()) {
14291
14890
  const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
14292
14891
  try {
14293
- const configDir = path23.join(workspacePath, ".cascade");
14892
+ const configDir = path25.join(workspacePath, ".cascade");
14294
14893
  await fs9.mkdir(configDir, { recursive: true });
14295
- const mdPath = path23.join(workspacePath, "CASCADE.md");
14894
+ const mdPath = path25.join(workspacePath, "CASCADE.md");
14296
14895
  if (!await fileExists(mdPath)) {
14297
14896
  await createDefaultCascadeMd(workspacePath);
14298
14897
  spin.succeed(chalk9.green("Created CASCADE.md"));
14299
14898
  }
14300
- const ignorePath = path23.join(workspacePath, ".cascadeignore");
14899
+ const ignorePath = path25.join(workspacePath, ".cascadeignore");
14301
14900
  if (!await fileExists(ignorePath)) {
14302
14901
  await createDefaultIgnoreFile(workspacePath);
14303
14902
  spin.succeed(chalk9.green("Created .cascadeignore"));
@@ -14306,7 +14905,7 @@ async function initCommand(workspacePath = process.cwd()) {
14306
14905
  console.log();
14307
14906
  console.log(chalk9.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
14308
14907
  console.log();
14309
- const configPath = path23.join(workspacePath, CASCADE_CONFIG_FILE);
14908
+ const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
14310
14909
  if (await fileExists(configPath)) {
14311
14910
  console.log(chalk9.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
14312
14911
  console.log();
@@ -14364,7 +14963,7 @@ function fromEnv(env) {
14364
14963
  return out;
14365
14964
  }
14366
14965
  async function fromClaudeCode(home) {
14367
- const file = path23.join(home, ".claude", ".credentials.json");
14966
+ const file = path25.join(home, ".claude", ".credentials.json");
14368
14967
  const data = await readJson(file);
14369
14968
  if (!data) return [];
14370
14969
  const oauth = data["claudeAiOauth"];
@@ -14387,7 +14986,7 @@ async function fromClaudeCode(home) {
14387
14986
  return [];
14388
14987
  }
14389
14988
  async function fromCodex(home) {
14390
- const file = path23.join(home, ".codex", "auth.json");
14989
+ const file = path25.join(home, ".codex", "auth.json");
14391
14990
  const data = await readJson(file);
14392
14991
  if (!data) return [];
14393
14992
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -14410,7 +15009,7 @@ async function fromCodex(home) {
14410
15009
  return [];
14411
15010
  }
14412
15011
  async function fromGemini(home) {
14413
- const file = path23.join(home, ".gemini", "oauth_creds.json");
15012
+ const file = path25.join(home, ".gemini", "oauth_creds.json");
14414
15013
  const data = await readJson(file);
14415
15014
  const accessToken = str(data?.["access_token"]);
14416
15015
  if (!accessToken) return [];
@@ -14426,7 +15025,7 @@ async function fromGemini(home) {
14426
15025
  }
14427
15026
  async function fromCopilot(home) {
14428
15027
  for (const name of ["apps.json", "hosts.json"]) {
14429
- const file = path23.join(home, ".config", "github-copilot", name);
15028
+ const file = path25.join(home, ".config", "github-copilot", name);
14430
15029
  const data = await readJson(file);
14431
15030
  if (!data) continue;
14432
15031
  for (const value of Object.values(data)) {
@@ -14447,7 +15046,7 @@ async function fromCopilot(home) {
14447
15046
  return [];
14448
15047
  }
14449
15048
  async function discoverCredentials(opts = {}) {
14450
- const home = opts.homeDir ?? os6.homedir();
15049
+ const home = opts.homeDir ?? os8.homedir();
14451
15050
  const env = opts.env ?? process.env;
14452
15051
  const groups = await Promise.all([
14453
15052
  Promise.resolve(fromEnv(env)),
@@ -14480,7 +15079,7 @@ async function doctorCommand() {
14480
15079
  checks.push({
14481
15080
  label: "Cascade config",
14482
15081
  ok: true,
14483
- detail: `Loaded ${path23.join(process.cwd(), CASCADE_CONFIG_FILE)}`
15082
+ detail: `Loaded ${path25.join(process.cwd(), CASCADE_CONFIG_FILE)}`
14484
15083
  });
14485
15084
  const providers = [
14486
15085
  { type: "anthropic", name: "Anthropic" },
@@ -14759,6 +15358,24 @@ var DashboardSocket = class {
14759
15358
  });
14760
15359
  });
14761
15360
  }
15361
+ onSessionHalt(callback) {
15362
+ this.io.on("connection", (socket) => {
15363
+ socket.on("session:halt", (payload) => {
15364
+ if (typeof payload?.sessionId === "string") {
15365
+ callback(payload.sessionId);
15366
+ }
15367
+ });
15368
+ });
15369
+ }
15370
+ onSessionSteer(callback) {
15371
+ this.io.on("connection", (socket) => {
15372
+ socket.on("session:steer", (payload) => {
15373
+ if (typeof payload?.message === "string" && payload.message.trim()) {
15374
+ callback(payload.message.trim(), payload.sessionId, payload.nodeId);
15375
+ }
15376
+ });
15377
+ });
15378
+ }
14762
15379
  onConfigUpdate(callback) {
14763
15380
  this.io.on("connection", (socket) => {
14764
15381
  socket.on("config:update", (payload) => {
@@ -14781,7 +15398,8 @@ var DashboardSocket = class {
14781
15398
  socket.on("cascade:run", (payload) => {
14782
15399
  if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
14783
15400
  const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
14784
- callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId);
15401
+ const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
15402
+ callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
14785
15403
  }
14786
15404
  });
14787
15405
  });
@@ -14793,7 +15411,7 @@ var DashboardSocket = class {
14793
15411
 
14794
15412
  // src/dashboard/server.ts
14795
15413
  init_constants();
14796
- var __dirname$1 = path23.dirname(fileURLToPath(import.meta.url));
15414
+ var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
14797
15415
  var DashboardServer = class {
14798
15416
  app;
14799
15417
  httpServer;
@@ -14804,6 +15422,20 @@ var DashboardServer = class {
14804
15422
  globalStore = null;
14805
15423
  broadcastTimer = null;
14806
15424
  activeSessions = /* @__PURE__ */ new Map();
15425
+ activeControllers = /* @__PURE__ */ new Map();
15426
+ /**
15427
+ * Run taskIds per chat session — file snapshots are keyed by the run's
15428
+ * taskId (see t3-worker saveSnapshot), so session rollback needs the list
15429
+ * of runs the session performed in this server's lifetime.
15430
+ */
15431
+ sessionTaskIds = /* @__PURE__ */ new Map();
15432
+ /**
15433
+ * Tool-approval requests awaiting a user decision from a connected client,
15434
+ * keyed by the request's uuid. The desktop shows a modal on
15435
+ * `permission:user-required` and answers with `permission:decision`; this
15436
+ * map is how that answer reaches the run that's blocked on it.
15437
+ */
15438
+ pendingApprovals = /* @__PURE__ */ new Map();
14807
15439
  port;
14808
15440
  host;
14809
15441
  workspacePath;
@@ -14862,15 +15494,18 @@ var DashboardServer = class {
14862
15494
  }
14863
15495
  this.persistConfig();
14864
15496
  });
14865
- this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
15497
+ this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
14866
15498
  const sessionId = requestedSessionId ?? randomUUID();
15499
+ const abortController = new AbortController();
15500
+ this.activeControllers.set(sessionId, abortController);
14867
15501
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
14868
15502
  const title = this.persistRunStart(sessionId, prompt);
14869
- const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
15503
+ let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
15504
+ if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
14870
15505
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
14871
15506
  this.activeSessions.set(sessionId, cascade);
14872
15507
  cascade.on("stream:token", (e) => {
14873
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
15508
+ this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
14874
15509
  });
14875
15510
  cascade.on("tier:status", (e) => {
14876
15511
  this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
@@ -14882,7 +15517,12 @@ var DashboardServer = class {
14882
15517
  this.socket.emitPeerMessage(e);
14883
15518
  });
14884
15519
  try {
14885
- const result = await cascade.run({ prompt: runPrompt });
15520
+ const result = await cascade.run({
15521
+ prompt: runPrompt,
15522
+ signal: abortController.signal,
15523
+ approvalCallback: this.makeApprovalCallback(sessionId)
15524
+ });
15525
+ this.recordSessionTask(sessionId, result.taskId);
14886
15526
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
14887
15527
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
14888
15528
  this.socket.broadcast("cost:update", {
@@ -14899,6 +15539,24 @@ var DashboardServer = class {
14899
15539
  });
14900
15540
  } finally {
14901
15541
  this.activeSessions.delete(sessionId);
15542
+ this.activeControllers.delete(sessionId);
15543
+ this.denyPendingApprovals(sessionId);
15544
+ }
15545
+ });
15546
+ this.socket.onSessionHalt((sessionId) => {
15547
+ this.activeControllers.get(sessionId)?.abort();
15548
+ this.denyPendingApprovals(sessionId);
15549
+ });
15550
+ this.socket.onApprovalResponse(({ requestId, approved, always }) => {
15551
+ const pending = this.pendingApprovals.get(requestId);
15552
+ if (!pending) return;
15553
+ this.pendingApprovals.delete(requestId);
15554
+ pending.resolve({ approved: !!approved, always: !!always });
15555
+ });
15556
+ this.socket.onSessionSteer((message, sessionId, nodeId) => {
15557
+ const steered = this.steerSessions(message, sessionId, nodeId);
15558
+ if (steered > 0) {
15559
+ this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
14902
15560
  }
14903
15561
  });
14904
15562
  }
@@ -14946,9 +15604,9 @@ var DashboardServer = class {
14946
15604
  */
14947
15605
  persistConfig() {
14948
15606
  try {
14949
- const configPath = path23.join(this.workspacePath, CASCADE_CONFIG_FILE);
14950
- fs21.mkdirSync(path23.dirname(configPath), { recursive: true });
14951
- fs21.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
15607
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
15608
+ fs23.mkdirSync(path25.dirname(configPath), { recursive: true });
15609
+ fs23.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
14952
15610
  } catch (err) {
14953
15611
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
14954
15612
  }
@@ -14963,15 +15621,15 @@ var DashboardServer = class {
14963
15621
  resolveDashboardSecret() {
14964
15622
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
14965
15623
  if (fromConfig) return fromConfig;
14966
- const secretPath = path23.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
15624
+ const secretPath = path25.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
14967
15625
  try {
14968
- if (fs21.existsSync(secretPath)) {
14969
- const existing = fs21.readFileSync(secretPath, "utf-8").trim();
15626
+ if (fs23.existsSync(secretPath)) {
15627
+ const existing = fs23.readFileSync(secretPath, "utf-8").trim();
14970
15628
  if (existing.length >= 16) return existing;
14971
15629
  }
14972
15630
  const generated = randomUUID();
14973
- fs21.mkdirSync(path23.dirname(secretPath), { recursive: true });
14974
- fs21.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
15631
+ fs23.mkdirSync(path25.dirname(secretPath), { recursive: true });
15632
+ fs23.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
14975
15633
  if (this.config.dashboard.auth) {
14976
15634
  console.warn(
14977
15635
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -14998,7 +15656,7 @@ var DashboardServer = class {
14998
15656
  // ── Setup ─────────────────────────────────────
14999
15657
  getGlobalStore() {
15000
15658
  if (!this.globalStore) {
15001
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15659
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15002
15660
  this.globalStore = new MemoryStore(globalDbPath);
15003
15661
  }
15004
15662
  return this.globalStore;
@@ -15046,6 +15704,44 @@ var DashboardServer = class {
15046
15704
  }
15047
15705
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
15048
15706
  }
15707
+ /**
15708
+ * Route steering text into running Cascade instances. Targets the given
15709
+ * session, or every active session when none is specified (the desktop
15710
+ * usually has exactly one run in flight). Returns how many were reached.
15711
+ */
15712
+ steerSessions(message, sessionId, nodeId) {
15713
+ const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
15714
+ for (const cascade of targets) cascade.injectGuidance(message, nodeId);
15715
+ return targets.length;
15716
+ }
15717
+ recordSessionTask(sessionId, taskId) {
15718
+ const list = this.sessionTaskIds.get(sessionId) ?? [];
15719
+ list.push(taskId);
15720
+ this.sessionTaskIds.set(sessionId, list);
15721
+ }
15722
+ /**
15723
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
15724
+ * user. The request itself was already pushed to the client via the
15725
+ * `permission:user-required` forward; here we just park a resolver keyed by
15726
+ * the request id and wait for the client's `permission:decision` (handled in
15727
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
15728
+ * pending until the client answers, the run ends, or the escalator's own
15729
+ * timeout denies it.
15730
+ */
15731
+ makeApprovalCallback(sessionId) {
15732
+ return (request) => new Promise((resolve) => {
15733
+ this.pendingApprovals.set(request.id, { resolve, sessionId });
15734
+ });
15735
+ }
15736
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
15737
+ denyPendingApprovals(sessionId) {
15738
+ for (const [id, pending] of this.pendingApprovals) {
15739
+ if (pending.sessionId === sessionId) {
15740
+ this.pendingApprovals.delete(id);
15741
+ pending.resolve({ approved: false, always: false });
15742
+ }
15743
+ }
15744
+ }
15049
15745
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
15050
15746
  const now = (/* @__PURE__ */ new Date()).toISOString();
15051
15747
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -15139,12 +15835,12 @@ ${prompt}`;
15139
15835
  }
15140
15836
  }
15141
15837
  watchRuntimeChanges() {
15142
- const workspaceDbPath = path23.join(this.workspacePath, CASCADE_DB_FILE);
15143
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15838
+ const workspaceDbPath = path25.join(this.workspacePath, CASCADE_DB_FILE);
15839
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15144
15840
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
15145
15841
  for (const watchPath of watchPaths) {
15146
- if (!fs21.existsSync(watchPath)) continue;
15147
- fs21.watchFile(watchPath, { interval: 3e3 }, () => {
15842
+ if (!fs23.existsSync(watchPath)) continue;
15843
+ fs23.watchFile(watchPath, { interval: 3e3 }, () => {
15148
15844
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
15149
15845
  });
15150
15846
  }
@@ -15234,15 +15930,6 @@ ${prompt}`;
15234
15930
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
15235
15931
  res.json({ success: true, ...payload });
15236
15932
  });
15237
- this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
15238
- const body = req.body;
15239
- const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
15240
- const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
15241
- const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15242
- this.socket.broadcast("session:approve", payload);
15243
- if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
15244
- res.json({ success: true, ...payload });
15245
- });
15246
15933
  this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
15247
15934
  const body = req.body;
15248
15935
  const message = typeof body["message"] === "string" ? body["message"] : void 0;
@@ -15252,7 +15939,8 @@ ${prompt}`;
15252
15939
  res.status(400).json({ error: "message is required and must be a string" });
15253
15940
  return;
15254
15941
  }
15255
- const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15942
+ const steered = this.steerSessions(message, sessionId, nodeId);
15943
+ const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15256
15944
  this.socket.broadcast("session:message-injected", payload);
15257
15945
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
15258
15946
  res.json({ success: true, ...payload });
@@ -15274,7 +15962,7 @@ ${prompt}`;
15274
15962
  const sessionId = req.params.id;
15275
15963
  this.store.deleteSession(sessionId);
15276
15964
  this.store.deleteRuntimeSession(sessionId);
15277
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15965
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15278
15966
  const globalStore = new MemoryStore(globalDbPath);
15279
15967
  try {
15280
15968
  globalStore.deleteRuntimeSession(sessionId);
@@ -15286,9 +15974,34 @@ ${prompt}`;
15286
15974
  this.socket.broadcast("runtime:refresh", { scope: "global" });
15287
15975
  res.json({ ok: true });
15288
15976
  });
15977
+ this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
15978
+ const sessionId = req.params.id;
15979
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
15980
+ if (!taskIds.length) {
15981
+ res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
15982
+ return;
15983
+ }
15984
+ const toRestore = /* @__PURE__ */ new Map();
15985
+ for (const taskId of taskIds) {
15986
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
15987
+ if (!toRestore.has(filePath)) toRestore.set(filePath, content);
15988
+ }
15989
+ }
15990
+ const { writeFile } = await import('fs/promises');
15991
+ let restored = 0;
15992
+ for (const [filePath, content] of toRestore) {
15993
+ try {
15994
+ await writeFile(filePath, content, "utf-8");
15995
+ restored++;
15996
+ } catch (err) {
15997
+ console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
15998
+ }
15999
+ }
16000
+ res.json({ ok: true, restored });
16001
+ });
15289
16002
  this.app.delete("/api/sessions", auth, (req, res) => {
15290
16003
  const body = req.body;
15291
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16004
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15292
16005
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
15293
16006
  const globalStore = new MemoryStore(globalDbPath);
15294
16007
  try {
@@ -15311,7 +16024,7 @@ ${prompt}`;
15311
16024
  });
15312
16025
  this.app.delete("/api/runtime", auth, (_req, res) => {
15313
16026
  this.store.deleteAllRuntimeNodes();
15314
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16027
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15315
16028
  const globalStore = new MemoryStore(globalDbPath);
15316
16029
  try {
15317
16030
  globalStore.deleteAllRuntimeNodes();
@@ -15362,6 +16075,19 @@ ${prompt}`;
15362
16075
  this.store.deleteIdentity(req.params.id);
15363
16076
  res.json({ ok: true });
15364
16077
  });
16078
+ this.app.get("/api/audit/verify", auth, async (_req, res) => {
16079
+ try {
16080
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
16081
+ const logger = new AuditLogger3(this.workspacePath);
16082
+ try {
16083
+ res.json(logger.verifyChain());
16084
+ } finally {
16085
+ logger.close();
16086
+ }
16087
+ } catch (err) {
16088
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
16089
+ }
16090
+ });
15365
16091
  this.app.get("/api/audit/:sessionId", auth, (req, res) => {
15366
16092
  const log = this.store.getAuditLog(req.params.sessionId);
15367
16093
  res.json(log);
@@ -15384,12 +16110,12 @@ ${prompt}`;
15384
16110
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
15385
16111
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
15386
16112
  try {
15387
- const configPath = path23.join(this.workspacePath, CASCADE_CONFIG_FILE);
15388
- const existing = fs21.existsSync(configPath) ? JSON.parse(fs21.readFileSync(configPath, "utf-8")) : {};
16113
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
16114
+ const existing = fs23.existsSync(configPath) ? JSON.parse(fs23.readFileSync(configPath, "utf-8")) : {};
15389
16115
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
15390
16116
  const tmp = configPath + ".tmp";
15391
- fs21.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
15392
- fs21.renameSync(tmp, configPath);
16117
+ fs23.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
16118
+ fs23.renameSync(tmp, configPath);
15393
16119
  res.json({ ok: true });
15394
16120
  } catch (err) {
15395
16121
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -15417,7 +16143,7 @@ ${prompt}`;
15417
16143
  this.app.get("/api/runtime", auth, (req, res) => {
15418
16144
  const scope = req.query["scope"] ?? "workspace";
15419
16145
  if (scope === "global") {
15420
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16146
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15421
16147
  const globalStore = new MemoryStore(globalDbPath);
15422
16148
  try {
15423
16149
  res.json({
@@ -15454,7 +16180,7 @@ ${prompt}`;
15454
16180
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
15455
16181
  this.activeSessions.set(sessionId, cascade);
15456
16182
  cascade.on("stream:token", (e) => {
15457
- this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
16183
+ this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
15458
16184
  });
15459
16185
  cascade.on("tier:status", (e) => {
15460
16186
  this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
@@ -15466,7 +16192,12 @@ ${prompt}`;
15466
16192
  this.socket.emitPeerMessage(e);
15467
16193
  });
15468
16194
  try {
15469
- const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
16195
+ const result = await cascade.run({
16196
+ prompt: runPrompt,
16197
+ identityId: body.identityId,
16198
+ approvalCallback: this.makeApprovalCallback(sessionId)
16199
+ });
16200
+ this.recordSessionTask(sessionId, result.taskId);
15470
16201
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
15471
16202
  this.socket.broadcast("cost:update", {
15472
16203
  sessionId,
@@ -15483,6 +16214,7 @@ ${prompt}`;
15483
16214
  });
15484
16215
  } finally {
15485
16216
  this.activeSessions.delete(sessionId);
16217
+ this.denyPendingApprovals(sessionId);
15486
16218
  }
15487
16219
  })();
15488
16220
  });
@@ -15497,13 +16229,13 @@ ${prompt}`;
15497
16229
  }))
15498
16230
  });
15499
16231
  });
15500
- const prodPath = path23.resolve(__dirname$1, "../web/dist");
15501
- const devPath = path23.resolve(__dirname$1, "../../web/dist");
15502
- const webDistPath = fs21.existsSync(prodPath) ? prodPath : devPath;
15503
- if (fs21.existsSync(webDistPath)) {
16232
+ const prodPath = path25.resolve(__dirname$1, "../web/dist");
16233
+ const devPath = path25.resolve(__dirname$1, "../../web/dist");
16234
+ const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;
16235
+ if (fs23.existsSync(webDistPath)) {
15504
16236
  this.app.use(express.static(webDistPath));
15505
16237
  this.app.get("*", (_req, res) => {
15506
- res.sendFile(path23.join(webDistPath, "index.html"));
16238
+ res.sendFile(path25.join(webDistPath, "index.html"));
15507
16239
  });
15508
16240
  } else {
15509
16241
  this.app.get("/", (_req, res) => {
@@ -15524,7 +16256,7 @@ init_constants();
15524
16256
  async function dashboardCommand(config, workspacePath = process.cwd()) {
15525
16257
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
15526
16258
  const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
15527
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
16259
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15528
16260
  const server = new DashboardServer(config, store, workspacePath);
15529
16261
  server.watchRuntimeChanges();
15530
16262
  const gThis = globalThis;
@@ -15556,7 +16288,7 @@ init_constants();
15556
16288
  function makeIdentityCommand(workspacePath = process.cwd()) {
15557
16289
  const identity = new Command("identity").alias("id").description("Manage Cascade identities");
15558
16290
  identity.command("list").description("List all available identities").action(() => {
15559
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
16291
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15560
16292
  try {
15561
16293
  const identities = store.listIdentities();
15562
16294
  console.log(chalk9.bold("\n Identities:"));
@@ -15576,7 +16308,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
15576
16308
  }
15577
16309
  });
15578
16310
  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) => {
15579
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
16311
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15580
16312
  try {
15581
16313
  if (options.default) {
15582
16314
  const existingDefault = store.getDefaultIdentity();
@@ -15602,7 +16334,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
15602
16334
  }
15603
16335
  });
15604
16336
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
15605
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
16337
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15606
16338
  try {
15607
16339
  const identities = store.listIdentities();
15608
16340
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -15730,8 +16462,8 @@ async function exportCommand(options = {}) {
15730
16462
  let store;
15731
16463
  try {
15732
16464
  const workspacePath = options.workspacePath ?? process.cwd();
15733
- const workspaceDbPath = path23.join(workspacePath, CASCADE_DB_FILE);
15734
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
16465
+ const workspaceDbPath = path25.join(workspacePath, CASCADE_DB_FILE);
16466
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
15735
16467
  let dbPath = globalDbPath;
15736
16468
  try {
15737
16469
  await fs9.access(workspaceDbPath);
@@ -15773,7 +16505,7 @@ async function exportCommand(options = {}) {
15773
16505
  const ext = format === "json" ? ".json" : ".md";
15774
16506
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
15775
16507
  const defaultFile = `cascade-export-${safeName}${ext}`;
15776
- const outPath = options.output ? path23.resolve(options.output) : path23.join(process.cwd(), defaultFile);
16508
+ const outPath = options.output ? path25.resolve(options.output) : path25.join(process.cwd(), defaultFile);
15777
16509
  await fs9.writeFile(outPath, content, "utf-8");
15778
16510
  spin.succeed(chalk9.green(`Exported to ${chalk9.white(outPath)}`));
15779
16511
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -16001,11 +16733,11 @@ async function statsCommand() {
16001
16733
  dotenv.config();
16002
16734
  function warnIfBuildIsStale() {
16003
16735
  try {
16004
- const here = path23.dirname(fileURLToPath(import.meta.url));
16736
+ const here = path25.dirname(fileURLToPath(import.meta.url));
16005
16737
  for (const rel of ["..", "../.."]) {
16006
- const pkgPath = path23.join(here, rel, "package.json");
16007
- if (!fs21.existsSync(pkgPath)) continue;
16008
- const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
16738
+ const pkgPath = path25.join(here, rel, "package.json");
16739
+ if (!fs23.existsSync(pkgPath)) continue;
16740
+ const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf-8"));
16009
16741
  if (pkg.name !== "cascade-ai") continue;
16010
16742
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
16011
16743
  console.error(