cascade-ai 0.12.23 → 0.13.1

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.23";
61
+ CASCADE_VERSION = "0.13.1";
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,14 @@ 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(),
2840
2992
  /**
2841
2993
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
2842
2994
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -2886,15 +3038,15 @@ var ConfigManager = class {
2886
3038
  globalDir;
2887
3039
  constructor(workspacePath = process.cwd()) {
2888
3040
  this.workspacePath = workspacePath;
2889
- this.globalDir = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR);
3041
+ this.globalDir = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR);
2890
3042
  }
2891
3043
  async load() {
2892
3044
  this.config = await this.loadConfig();
2893
3045
  this.ignore = new CascadeIgnore();
2894
3046
  await this.ignore.load(this.workspacePath);
2895
3047
  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));
3048
+ this.keystore = new Keystore(path25.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
3049
+ this.store = new MemoryStore(path25.join(this.workspacePath, CASCADE_DB_FILE));
2898
3050
  await this.injectEnvKeys();
2899
3051
  await this.ensureDefaultIdentity();
2900
3052
  }
@@ -2917,8 +3069,8 @@ var ConfigManager = class {
2917
3069
  return this.workspacePath;
2918
3070
  }
2919
3071
  async save() {
2920
- const configPath = path23.join(this.workspacePath, CASCADE_CONFIG_FILE);
2921
- await fs9.mkdir(path23.dirname(configPath), { recursive: true });
3072
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
3073
+ await fs9.mkdir(path25.dirname(configPath), { recursive: true });
2922
3074
  await fs9.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
2923
3075
  }
2924
3076
  async updateConfig(updates) {
@@ -2942,7 +3094,7 @@ var ConfigManager = class {
2942
3094
  return configProvider?.apiKey;
2943
3095
  }
2944
3096
  async loadConfig() {
2945
- const configPath = path23.join(this.workspacePath, CASCADE_CONFIG_FILE);
3097
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
2946
3098
  try {
2947
3099
  const raw = await fs9.readFile(configPath, "utf-8");
2948
3100
  return validateConfig(JSON.parse(raw));
@@ -3821,7 +3973,7 @@ init_constants();
3821
3973
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
3822
3974
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
3823
3975
  var FETCH_TIMEOUT_MS = 8e3;
3824
- var DEFAULT_CACHE_FILE = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
3976
+ var DEFAULT_CACHE_FILE = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
3825
3977
  function normalizeModelId(id) {
3826
3978
  let s = id.toLowerCase();
3827
3979
  const slash = s.lastIndexOf("/");
@@ -3936,7 +4088,7 @@ var LiveDataProvider = class {
3936
4088
  }
3937
4089
  async saveCache() {
3938
4090
  try {
3939
- await fs9.mkdir(path23.dirname(this.opts.cacheFile), { recursive: true });
4091
+ await fs9.mkdir(path25.dirname(this.opts.cacheFile), { recursive: true });
3940
4092
  const cache = {
3941
4093
  fetchedAt: this.fetchedAt,
3942
4094
  snapshot: this.snapshot ?? void 0,
@@ -4066,7 +4218,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4066
4218
  costByTier: {},
4067
4219
  tokensByTier: {},
4068
4220
  inputTokensByTier: {},
4069
- outputTokensByTier: {}
4221
+ outputTokensByTier: {},
4222
+ costByFeature: {}
4070
4223
  };
4071
4224
  tierModels = /* @__PURE__ */ new Map();
4072
4225
  config;
@@ -4088,6 +4241,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4088
4241
  tpmLimiter;
4089
4242
  localQueue;
4090
4243
  taskAnalyzer;
4244
+ worldStateDB;
4245
+ privacyPaths;
4246
+ guidanceQueue;
4091
4247
  liveData;
4092
4248
  /** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
4093
4249
  originalTierModels;
@@ -4245,7 +4401,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4245
4401
  if (options.model && !requireVision) {
4246
4402
  this.ensureProvider(options.model, this.config.providers);
4247
4403
  }
4248
- const model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
4404
+ let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
4405
+ if (options.forceLocal && model && !this.isPrivateModel(model)) {
4406
+ const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
4407
+ if (!localModel) {
4408
+ throw new Error(
4409
+ "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."
4410
+ );
4411
+ }
4412
+ this.ensureProvider(localModel, this.config.providers);
4413
+ model = localModel;
4414
+ }
4249
4415
  if (!model) throw new Error(`No model available for tier ${tier}`);
4250
4416
  const provider = this.getProvider(model);
4251
4417
  if (!provider) throw new Error(`No provider for model ${model.id}`);
@@ -4317,7 +4483,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4317
4483
  if (!result || typeof result.content !== "string" || !result.usage) {
4318
4484
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
4319
4485
  }
4320
- this.recordStats(tier, model, result.usage);
4486
+ this.recordStats(tier, model, result.usage, options.featureTag);
4321
4487
  this.failover.recordSuccess(model.provider);
4322
4488
  return result;
4323
4489
  } catch (err) {
@@ -4422,6 +4588,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4422
4588
  setTaskAnalyzer(analyzer) {
4423
4589
  this.taskAnalyzer = analyzer;
4424
4590
  }
4591
+ setWorldStateDB(db) {
4592
+ this.worldStateDB = db;
4593
+ }
4594
+ getWorldStateDB() {
4595
+ return this.worldStateDB;
4596
+ }
4597
+ setPrivacyPaths(paths) {
4598
+ this.privacyPaths = paths;
4599
+ }
4600
+ getPrivacyPaths() {
4601
+ return this.privacyPaths;
4602
+ }
4603
+ setGuidanceQueue(queue) {
4604
+ this.guidanceQueue = queue;
4605
+ }
4606
+ getGuidanceQueue() {
4607
+ return this.guidanceQueue;
4608
+ }
4609
+ /**
4610
+ * "Private" = inference never leaves the user's machine/network: Ollama
4611
+ * models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
4612
+ * LM Studio) whose configured host is loopback or a private range. A cloud
4613
+ * OpenAI-compatible endpoint (public host) does NOT qualify.
4614
+ */
4615
+ isPrivateModel(model) {
4616
+ if (model.isLocal) return true;
4617
+ if (model.provider !== "openai-compatible") return false;
4618
+ const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
4619
+ if (!baseUrl) return false;
4620
+ try {
4621
+ const host = new URL(baseUrl).hostname.toLowerCase();
4622
+ 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");
4623
+ } catch {
4624
+ return false;
4625
+ }
4626
+ }
4425
4627
  /**
4426
4628
  * Cascade Auto per-subtask routing: pick the benchmark-best model for a
4427
4629
  * specific subtask's text, scoped to the tier's eligible candidates. Returns
@@ -4445,7 +4647,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4445
4647
  costByTier: { ...this.stats.costByTier },
4446
4648
  tokensByTier: { ...this.stats.tokensByTier },
4447
4649
  inputTokensByTier: { ...this.stats.inputTokensByTier },
4448
- outputTokensByTier: { ...this.stats.outputTokensByTier }
4650
+ outputTokensByTier: { ...this.stats.outputTokensByTier },
4651
+ costByFeature: { ...this.stats.costByFeature }
4449
4652
  };
4450
4653
  }
4451
4654
  /**
@@ -4495,7 +4698,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4495
4698
  costByTier: {},
4496
4699
  tokensByTier: {},
4497
4700
  inputTokensByTier: {},
4498
- outputTokensByTier: {}
4701
+ outputTokensByTier: {},
4702
+ costByFeature: {}
4499
4703
  };
4500
4704
  this.sessionCostUsd = 0;
4501
4705
  this.budgetState = "ok";
@@ -4661,7 +4865,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4661
4865
  }
4662
4866
  return void 0;
4663
4867
  }
4664
- recordStats(tier, model, usage) {
4868
+ recordStats(tier, model, usage, featureTag) {
4665
4869
  this.stats.totalTokens += usage.totalTokens;
4666
4870
  this.stats.totalCostUsd += usage.estimatedCostUsd;
4667
4871
  this.sessionCostUsd += usage.estimatedCostUsd;
@@ -4671,6 +4875,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4671
4875
  this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
4672
4876
  this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
4673
4877
  this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
4878
+ if (featureTag) {
4879
+ this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
4880
+ }
4674
4881
  this.runTokens += usage.totalTokens;
4675
4882
  this.runCostUsd += usage.estimatedCostUsd;
4676
4883
  this.updateBudgetState();
@@ -5208,6 +5415,8 @@ var T3Worker = class extends BaseTier {
5208
5415
  toolRegistry;
5209
5416
  context;
5210
5417
  assignment;
5418
+ /** True when this subtask matched a privacy.paths local-only pattern. */
5419
+ localOnlyMatch = false;
5211
5420
  peerSyncBuffer = [];
5212
5421
  store;
5213
5422
  audit;
@@ -5263,6 +5472,11 @@ var T3Worker = class extends BaseTier {
5263
5472
  this.taskId = taskId;
5264
5473
  this.setLabel(assignment.subtaskTitle);
5265
5474
  this.setStatus("ACTIVE");
5475
+ const privacy = this.router.getPrivacyPaths?.();
5476
+ this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
5477
+ if (this.localOnlyMatch) {
5478
+ this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
5479
+ }
5266
5480
  this.tools = this.toolRegistry.getToolDefinitions();
5267
5481
  if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
5268
5482
  this.tools = [...this.tools, {
@@ -5393,9 +5607,17 @@ Now execute your subtask using this context where relevant.`
5393
5607
  }
5394
5608
  const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
5395
5609
  if (reflectCfg.enabled) {
5396
- this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
5610
+ this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
5397
5611
  output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
5398
5612
  }
5613
+ const db = this.router.getWorldStateDB?.();
5614
+ if (db) {
5615
+ try {
5616
+ db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
5617
+ } catch (e) {
5618
+ this.log("Failed to write to World State DB");
5619
+ }
5620
+ }
5399
5621
  this.setStatus("COMPLETED", output);
5400
5622
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
5401
5623
  this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
@@ -5468,6 +5690,16 @@ Now execute your subtask using this context where relevant.`
5468
5690
  while (iterations < MAX_ITERATIONS) {
5469
5691
  iterations++;
5470
5692
  this.throwIfCancelled();
5693
+ const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
5694
+ for (const g of guidance) {
5695
+ this.log(`User intervention received: ${g.text}`);
5696
+ this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
5697
+ await this.context.addMessage({
5698
+ role: "user",
5699
+ content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
5700
+ ${g.text}`
5701
+ });
5702
+ }
5471
5703
  const options = {
5472
5704
  messages: this.context.getMessages(),
5473
5705
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -5476,7 +5708,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
5476
5708
  // Don't pass tools array when model can't use them natively
5477
5709
  tools: useTextTools ? void 0 : tools.length ? tools : void 0,
5478
5710
  maxTokens: 4096,
5479
- ...subtaskModel ? { model: subtaskModel } : {}
5711
+ ...subtaskModel ? { model: subtaskModel } : {},
5712
+ featureTag: this.assignment?.sectionTitle,
5713
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5480
5714
  };
5481
5715
  const result = await this.router.generate(
5482
5716
  "T3",
@@ -5640,8 +5874,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
5640
5874
  tierId: this.id,
5641
5875
  sessionId: this.taskId,
5642
5876
  requireApproval: false,
5643
- saveSnapshot: async (path28, content) => {
5644
- this.store?.addFileSnapshot(this.taskId, path28, content);
5877
+ saveSnapshot: async (path30, content) => {
5878
+ this.store?.addFileSnapshot(this.taskId, path30, content);
5645
5879
  },
5646
5880
  sendPeerSync: (to, syncType, content) => {
5647
5881
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -5811,7 +6045,7 @@ ${assignment.expectedOutput}`;
5811
6045
  const { promisify: promisify4 } = await import('util');
5812
6046
  const execAsync3 = promisify4(exec3);
5813
6047
  for (const artifactPath of artifactPaths) {
5814
- const absolutePath = path23.resolve(process.cwd(), artifactPath);
6048
+ const absolutePath = path25.resolve(process.cwd(), artifactPath);
5815
6049
  try {
5816
6050
  const stat = await fs9.stat(absolutePath);
5817
6051
  if (!stat.isFile()) {
@@ -5832,7 +6066,7 @@ ${assignment.expectedOutput}`;
5832
6066
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
5833
6067
  continue;
5834
6068
  }
5835
- const ext = path23.extname(absolutePath).toLowerCase();
6069
+ const ext = path25.extname(absolutePath).toLowerCase();
5836
6070
  try {
5837
6071
  if (ext === ".ts" || ext === ".tsx") {
5838
6072
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -5861,30 +6095,35 @@ ${stdout}`);
5861
6095
  * needed. Best-effort: any parse/error just keeps the current output.
5862
6096
  */
5863
6097
  async reflectAndImprove(assignment, output, maxRounds) {
5864
- const sys = this.systemPromptOverride + (this.hierarchyContext ? `
5865
-
5866
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
5867
6098
  let current = output;
5868
- for (let round = 0; round < Math.max(1, maxRounds); round++) {
5869
- try {
5870
- const verdict = await this.router.generate("T3", {
6099
+ try {
6100
+ for (let round = 0; round < Math.max(1, maxRounds); round++) {
6101
+ const verdictResult = await this.router.generate("T2", {
5871
6102
  messages: [{
5872
6103
  role: "user",
5873
- content: `Does this output FULLY achieve the goal \u2014 not just the literal task, but the intent behind it?
6104
+ content: `You are an independent critic reviewing another worker's output against its assignment.
5874
6105
 
5875
- Goal / expected: ${assignment.expectedOutput}
6106
+ Goal: ${assignment.expectedOutput}
5876
6107
  Subtask: ${assignment.description}
5877
-
5878
- Output:
6108
+ Current Output:
5879
6109
  ${current}
5880
6110
 
5881
- Reply with ONLY JSON: {"sufficient": true|false, "notes": "what is weak or missing if not sufficient"}`
6111
+ Is this output sufficient and correct? Respond with ONLY a JSON object:
6112
+ {"sufficient": true|false, "notes": "what is wrong or missing if false"}`
5882
6113
  }],
5883
- systemPrompt: sys,
5884
- maxTokens: 400
6114
+ systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
6115
+ maxTokens: 400,
6116
+ signal: this.signal,
6117
+ featureTag: assignment.sectionTitle,
6118
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5885
6119
  });
5886
- const parsed = JSON.parse(/\{[\s\S]*\}/.exec(verdict.content)?.[0] ?? "{}");
5887
- if (parsed.sufficient !== false) break;
6120
+ const match = /\{[\s\S]*\}/.exec(verdictResult.content);
6121
+ const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
6122
+ if (parsed.sufficient !== false) {
6123
+ this.log("T2-Critic approved output.");
6124
+ break;
6125
+ }
6126
+ this.log(`T2-Critic rejected output: ${parsed.notes}`);
5888
6127
  const improved = await this.router.generate("T3", {
5889
6128
  messages: [{
5890
6129
  role: "user",
@@ -5896,16 +6135,20 @@ Goal / expected: ${assignment.expectedOutput}
5896
6135
  Current output:
5897
6136
  ${current}`
5898
6137
  }],
5899
- systemPrompt: sys,
5900
- maxTokens: 4096
6138
+ systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
6139
+
6140
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6141
+ maxTokens: 4096,
6142
+ featureTag: assignment.sectionTitle,
6143
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5901
6144
  });
5902
6145
  const next = (improved.content ?? "").trim();
5903
6146
  if (!next) break;
5904
6147
  current = next;
5905
6148
  this.log("Reflection: revised output for better goal alignment.");
5906
- } catch {
5907
- break;
5908
6149
  }
6150
+ } catch (e) {
6151
+ this.log(`T2-Critic reflection failed: ${e}`);
5909
6152
  }
5910
6153
  return current;
5911
6154
  }
@@ -5926,7 +6169,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
5926
6169
  maxTokens: 500,
5927
6170
  systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
5928
6171
 
5929
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
6172
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6173
+ featureTag: assignment.sectionTitle,
6174
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
5930
6175
  });
5931
6176
  try {
5932
6177
  const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
@@ -6021,6 +6266,7 @@ Begin execution now.`;
6021
6266
  issues,
6022
6267
  peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
6023
6268
  correctionAttempts,
6269
+ localOnly: this.localOnlyMatch || void 0,
6024
6270
  reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
6025
6271
  };
6026
6272
  }
@@ -6312,6 +6558,42 @@ var PeerBus = class extends EventEmitter {
6312
6558
  }
6313
6559
  };
6314
6560
 
6561
+ // src/core/audit/redaction.ts
6562
+ var RedactionLayer = class {
6563
+ // Regexes for common secrets/PII
6564
+ static RULES = [
6565
+ // IPv4 addresses (basic approximation)
6566
+ { pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
6567
+ // Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
6568
+ { 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]" },
6569
+ // Email addresses
6570
+ { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
6571
+ // Phone numbers (simplistic)
6572
+ { pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
6573
+ // AWS Access Key ID
6574
+ { pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
6575
+ ];
6576
+ /**
6577
+ * Applies all redaction rules to the input string.
6578
+ */
6579
+ static redact(text) {
6580
+ if (!text) return text;
6581
+ let redacted = text;
6582
+ for (const rule of this.RULES) {
6583
+ if (rule.pattern.test(redacted)) {
6584
+ rule.pattern.lastIndex = 0;
6585
+ redacted = redacted.replace(rule.pattern, (match, p1, offset, str2) => {
6586
+ if (p1 && match.includes(p1) && p1 !== match) {
6587
+ return match.replace(p1, rule.replacement.replace("$1", ""));
6588
+ }
6589
+ return rule.replacement;
6590
+ });
6591
+ }
6592
+ }
6593
+ return redacted;
6594
+ }
6595
+ };
6596
+
6315
6597
  // src/core/tiers/t2-manager.ts
6316
6598
  var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
6317
6599
  Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
@@ -6540,7 +6822,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6540
6822
  try {
6541
6823
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
6542
6824
  if (!jsonMatch) throw new Error("No JSON array found");
6543
- return JSON.parse(jsonMatch[0]);
6825
+ const parsed = JSON.parse(jsonMatch[0]);
6826
+ return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
6544
6827
  } catch {
6545
6828
  return [{
6546
6829
  subtaskId: randomUUID(),
@@ -6550,6 +6833,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6550
6833
  constraints: assignment.constraints,
6551
6834
  peerT3Ids: [],
6552
6835
  parentT2: this.id,
6836
+ sectionTitle: assignment.sectionTitle,
6837
+ dependsOn: [],
6553
6838
  executionMode: "parallel"
6554
6839
  }];
6555
6840
  }
@@ -6669,6 +6954,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6669
6954
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
6670
6955
  const worker = workerMap.get(id);
6671
6956
  const result = await worker.execute(assignment, taskId, waveSignal);
6957
+ if (result.localOnly) {
6958
+ 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}]`;
6959
+ } else {
6960
+ if (typeof result.output === "string" && result.output) {
6961
+ result.output = RedactionLayer.redact(result.output);
6962
+ }
6963
+ }
6964
+ if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
6672
6965
  resultMap.set(id, result);
6673
6966
  return result;
6674
6967
  };
@@ -7301,10 +7594,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
7301
7594
  }
7302
7595
  }
7303
7596
  async decomposeTask(prompt, systemContext) {
7597
+ const db = this.router.getWorldStateDB?.();
7598
+ const worldStateContext = db ? `
7599
+
7600
+ PROJECT WORLD STATE (Current architecture and recent changes):
7601
+ ${db.getFormattedState()}` : "";
7304
7602
  const contextSection = systemContext ? `
7305
7603
  Project context:
7306
7604
  ${systemContext}` : "";
7307
- const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
7605
+ const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
7308
7606
 
7309
7607
  Task: ${prompt}
7310
7608
 
@@ -7792,16 +8090,16 @@ function resolveInWorkspace(workspaceRoot, input) {
7792
8090
  if (typeof input !== "string" || input.length === 0) {
7793
8091
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
7794
8092
  }
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)) {
8093
+ const root = path25.resolve(workspaceRoot);
8094
+ const abs = path25.isAbsolute(input) ? path25.resolve(input) : path25.resolve(root, input);
8095
+ const rel = path25.relative(root, abs);
8096
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25.isAbsolute(rel)) {
7799
8097
  throw new WorkspaceSandboxError(input, root);
7800
8098
  }
7801
8099
  try {
7802
- const real = fs21.realpathSync(abs);
7803
- const realRel = path23.relative(root, real);
7804
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path23.isAbsolute(realRel))) {
8100
+ const real = fs23.realpathSync(abs);
8101
+ const realRel = path25.relative(root, real);
8102
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25.isAbsolute(realRel))) {
7805
8103
  throw new WorkspaceSandboxError(input, root);
7806
8104
  }
7807
8105
  } catch (e) {
@@ -7862,7 +8160,7 @@ var FileWriteTool = class extends BaseTool {
7862
8160
  } catch {
7863
8161
  }
7864
8162
  }
7865
- await fs9.mkdir(path23.dirname(absPath), { recursive: true });
8163
+ await fs9.mkdir(path25.dirname(absPath), { recursive: true });
7866
8164
  await fs9.writeFile(absPath, content, "utf-8");
7867
8165
  return `Written ${content.length} characters to ${filePath}`;
7868
8166
  }
@@ -8360,7 +8658,7 @@ var ImageAnalyzeTool = class extends BaseTool {
8360
8658
  };
8361
8659
  async function fileToImageAttachment(filePath) {
8362
8660
  const data = await fs9.readFile(filePath);
8363
- const ext = path23.extname(filePath).toLowerCase();
8661
+ const ext = path25.extname(filePath).toLowerCase();
8364
8662
  const mimeMap = {
8365
8663
  ".jpg": "image/jpeg",
8366
8664
  ".jpeg": "image/jpeg",
@@ -8394,14 +8692,14 @@ var PDFCreateTool = class extends BaseTool {
8394
8692
  const filePath = input["path"];
8395
8693
  const content = input["content"];
8396
8694
  const title = input["title"];
8397
- const dir = path23.dirname(filePath);
8398
- if (!fs21.existsSync(dir)) {
8399
- fs21.mkdirSync(dir, { recursive: true });
8695
+ const dir = path25.dirname(filePath);
8696
+ if (!fs23.existsSync(dir)) {
8697
+ fs23.mkdirSync(dir, { recursive: true });
8400
8698
  }
8401
8699
  return new Promise((resolve, reject) => {
8402
8700
  try {
8403
8701
  const doc = new PDFDocument({ margin: 50 });
8404
- const stream = fs21.createWriteStream(filePath);
8702
+ const stream = fs23.createWriteStream(filePath);
8405
8703
  doc.pipe(stream);
8406
8704
  if (title) {
8407
8705
  doc.info["Title"] = title;
@@ -8479,22 +8777,22 @@ var CodeInterpreterTool = class extends BaseTool {
8479
8777
  }
8480
8778
  cmdPrefix = NODE_CMD;
8481
8779
  }
8482
- const tmpDir = path23.join(this.workspaceRoot, ".cascade", "tmp");
8483
- if (!fs21.existsSync(tmpDir)) {
8484
- fs21.mkdirSync(tmpDir, { recursive: true });
8780
+ const tmpDir = path25.join(this.workspaceRoot, ".cascade", "tmp");
8781
+ if (!fs23.existsSync(tmpDir)) {
8782
+ fs23.mkdirSync(tmpDir, { recursive: true });
8485
8783
  }
8486
8784
  const extension = language === "python" ? "py" : "js";
8487
8785
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
8488
- const filePath = path23.join(tmpDir, fileName);
8489
- fs21.writeFileSync(filePath, code, "utf-8");
8786
+ const filePath = path25.join(tmpDir, fileName);
8787
+ fs23.writeFileSync(filePath, code, "utf-8");
8490
8788
  const execArgs = [filePath, ...args];
8491
8789
  return new Promise((resolve) => {
8492
8790
  const startMs = Date.now();
8493
8791
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
8494
8792
  const duration = Date.now() - startMs;
8495
8793
  try {
8496
- if (fs21.existsSync(filePath)) {
8497
- fs21.unlinkSync(filePath);
8794
+ if (fs23.existsSync(filePath)) {
8795
+ fs23.unlinkSync(filePath);
8498
8796
  }
8499
8797
  } catch (cleanupErr) {
8500
8798
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -8773,7 +9071,7 @@ var GlobTool = class extends BaseTool {
8773
9071
  };
8774
9072
  async execute(input, _options) {
8775
9073
  const pattern = input["pattern"];
8776
- const searchPath = input["path"] ? path23.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9074
+ const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
8777
9075
  const matches = await glob(pattern, {
8778
9076
  cwd: searchPath,
8779
9077
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -8786,7 +9084,7 @@ var GlobTool = class extends BaseTool {
8786
9084
  const withMtime = await Promise.all(
8787
9085
  matches.map(async (rel) => {
8788
9086
  try {
8789
- const stat = await fs9.stat(path23.join(searchPath, rel));
9087
+ const stat = await fs9.stat(path25.join(searchPath, rel));
8790
9088
  return { rel, mtime: stat.mtimeMs };
8791
9089
  } catch {
8792
9090
  return { rel, mtime: 0 };
@@ -8835,7 +9133,7 @@ var GrepTool = class extends BaseTool {
8835
9133
  };
8836
9134
  async execute(input, _options) {
8837
9135
  const pattern = input["pattern"];
8838
- const searchPath = input["path"] ? path23.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9136
+ const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
8839
9137
  const globPattern = input["glob"];
8840
9138
  const outputMode = input["output_mode"] ?? "content";
8841
9139
  const context = input["context"] ?? 0;
@@ -8889,12 +9187,12 @@ var GrepTool = class extends BaseTool {
8889
9187
  nodir: true
8890
9188
  });
8891
9189
  } catch {
8892
- files = [path23.relative(searchPath, searchPath) || "."];
9190
+ files = [path25.relative(searchPath, searchPath) || "."];
8893
9191
  }
8894
9192
  const results = [];
8895
9193
  let totalCount = 0;
8896
9194
  for (const rel of files) {
8897
- const abs = path23.join(searchPath, rel);
9195
+ const abs = path25.join(searchPath, rel);
8898
9196
  let content;
8899
9197
  try {
8900
9198
  content = await fs9.readFile(abs, "utf-8");
@@ -9256,10 +9554,10 @@ var ToolRegistry = class extends EventEmitter {
9256
9554
  }
9257
9555
  isIgnored(filePath) {
9258
9556
  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("/");
9557
+ const abs = path25.resolve(this.workspaceRoot, filePath);
9558
+ const rel = path25.relative(this.workspaceRoot, abs);
9559
+ if (!rel || rel.startsWith("..") || path25.isAbsolute(rel)) return true;
9560
+ const posixRel = rel.split(path25.sep).join("/");
9263
9561
  return this.ignoreMatcher.ignores(posixRel);
9264
9562
  }
9265
9563
  };
@@ -9376,6 +9674,9 @@ var McpClient = class _McpClient {
9376
9674
  return this.clients.has(serverName);
9377
9675
  }
9378
9676
  };
9677
+
9678
+ // src/core/cascade.ts
9679
+ init_audit_logger();
9379
9680
  var SAFE_TOOLS = /* @__PURE__ */ new Set([
9380
9681
  "file_read",
9381
9682
  "file_list",
@@ -9759,9 +10060,10 @@ var TaskAnalyzer = class {
9759
10060
  analysisCache.clear();
9760
10061
  }
9761
10062
  };
9762
- var DEFAULT_STATS_FILE = path23.join(os6.homedir(), ".cascade", "model-perf.json");
10063
+ var DEFAULT_STATS_FILE = path25.join(os8.homedir(), ".cascade", "model-perf.json");
9763
10064
  var ModelPerformanceTracker = class {
9764
10065
  stats = /* @__PURE__ */ new Map();
10066
+ featureStats = /* @__PURE__ */ new Map();
9765
10067
  statsFile;
9766
10068
  loaded = false;
9767
10069
  constructor(statsFile = DEFAULT_STATS_FILE) {
@@ -9773,18 +10075,29 @@ var ModelPerformanceTracker = class {
9773
10075
  try {
9774
10076
  const raw = await fs9.readFile(this.statsFile, "utf-8");
9775
10077
  const parsed = JSON.parse(raw);
9776
- for (const [key, stat] of Object.entries(parsed)) {
9777
- this.stats.set(key, stat);
10078
+ if (parsed.models) {
10079
+ for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
10080
+ } else {
10081
+ for (const [key, stat] of Object.entries(parsed)) {
10082
+ if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
10083
+ this.stats.set(key, stat);
10084
+ }
10085
+ }
10086
+ }
10087
+ if (parsed.features) {
10088
+ for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
9778
10089
  }
9779
10090
  } catch {
9780
10091
  }
9781
10092
  }
9782
10093
  async save() {
9783
10094
  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");
10095
+ await fs9.mkdir(path25.dirname(this.statsFile), { recursive: true });
10096
+ const modelsObj = {};
10097
+ const featuresObj = {};
10098
+ for (const [key, stat] of this.stats) modelsObj[key] = stat;
10099
+ for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
10100
+ await fs9.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
9788
10101
  } catch {
9789
10102
  }
9790
10103
  }
@@ -9805,6 +10118,13 @@ var ModelPerformanceTracker = class {
9805
10118
  sampleCount: s.sampleCount + 1
9806
10119
  });
9807
10120
  }
10121
+ recordFeatureCost(featureTag, costUsd) {
10122
+ const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
10123
+ this.featureStats.set(featureTag, {
10124
+ totalCostUsd: s.totalCostUsd + costUsd,
10125
+ runCount: s.runCount + 1
10126
+ });
10127
+ }
9808
10128
  /**
9809
10129
  * Record an explicit user rating (good/bad). Counts as 3 automatic samples
9810
10130
  * so user feedback carries significantly more weight than auto-detected outcomes.
@@ -9819,6 +10139,9 @@ var ModelPerformanceTracker = class {
9819
10139
  getAll() {
9820
10140
  return new Map(this.stats);
9821
10141
  }
10142
+ getAllFeatures() {
10143
+ return new Map(this.featureStats);
10144
+ }
9822
10145
  /**
9823
10146
  * Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
9824
10147
  * High retry counts penalise the score.
@@ -10183,7 +10506,7 @@ Required capability: ${description.slice(0, 300)}`;
10183
10506
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
10184
10507
  async loadPersistedTools() {
10185
10508
  if (!this.workspacePath || !this.persistEnabled) return;
10186
- const file = path23.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
10509
+ const file = path25.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
10187
10510
  try {
10188
10511
  const raw = await fs9.readFile(file, "utf-8");
10189
10512
  const specs = JSON.parse(raw);
@@ -10207,8 +10530,8 @@ Required capability: ${description.slice(0, 300)}`;
10207
10530
  }
10208
10531
  async persist() {
10209
10532
  if (!this.workspacePath || !this.persistEnabled) return;
10210
- const dir = path23.join(this.workspacePath, ".cascade");
10211
- const file = path23.join(dir, DYNAMIC_TOOLS_FILE);
10533
+ const dir = path25.join(this.workspacePath, ".cascade");
10534
+ const file = path25.join(dir, DYNAMIC_TOOLS_FILE);
10212
10535
  try {
10213
10536
  await fs9.mkdir(dir, { recursive: true });
10214
10537
  await fs9.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -10221,6 +10544,166 @@ Required capability: ${description.slice(0, 300)}`;
10221
10544
  return Array.from(this.specs.keys());
10222
10545
  }
10223
10546
  };
10547
+ var WorldStateDB = class {
10548
+ constructor(workspacePath, debugMode = false) {
10549
+ this.workspacePath = workspacePath;
10550
+ this.debugMode = debugMode;
10551
+ const cascadeDir = path25.join(workspacePath, ".cascade");
10552
+ if (!fs23.existsSync(cascadeDir)) {
10553
+ fs23.mkdirSync(cascadeDir, { recursive: true });
10554
+ }
10555
+ this.keyPath = path25.join(cascadeDir, "world_state.key");
10556
+ this.dbPath = path25.join(cascadeDir, "world_state.db");
10557
+ this.initEncryptionKey();
10558
+ this.db = new Database2(this.dbPath);
10559
+ this.db.pragma("journal_mode = WAL");
10560
+ this.db.exec(`
10561
+ CREATE TABLE IF NOT EXISTS world_state (
10562
+ id TEXT PRIMARY KEY,
10563
+ timestamp TEXT NOT NULL,
10564
+ worker_id TEXT NOT NULL,
10565
+ encrypted_payload TEXT NOT NULL
10566
+ )
10567
+ `);
10568
+ }
10569
+ workspacePath;
10570
+ debugMode;
10571
+ db;
10572
+ keyPath;
10573
+ dbPath;
10574
+ encryptionKey;
10575
+ initEncryptionKey() {
10576
+ if (fs23.existsSync(this.keyPath)) {
10577
+ this.encryptionKey = fs23.readFileSync(this.keyPath);
10578
+ } else {
10579
+ this.encryptionKey = crypto.randomBytes(32);
10580
+ fs23.writeFileSync(this.keyPath, this.encryptionKey);
10581
+ }
10582
+ }
10583
+ encrypt(text) {
10584
+ const iv = crypto.randomBytes(16);
10585
+ const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
10586
+ let encrypted = cipher.update(text, "utf8", "hex");
10587
+ encrypted += cipher.final("hex");
10588
+ const authTag = cipher.getAuthTag().toString("hex");
10589
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
10590
+ }
10591
+ decrypt(text) {
10592
+ const parts = text.split(":");
10593
+ if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
10594
+ const [ivHex, authTagHex, encryptedHex] = parts;
10595
+ const iv = Buffer.from(ivHex, "hex");
10596
+ const authTag = Buffer.from(authTagHex, "hex");
10597
+ const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
10598
+ decipher.setAuthTag(authTag);
10599
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
10600
+ decrypted += decipher.final("utf8");
10601
+ return decrypted;
10602
+ }
10603
+ addEntry(workerId, summary) {
10604
+ const id = crypto.randomUUID();
10605
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10606
+ const payload = JSON.stringify({ summary });
10607
+ const encryptedPayload = this.encrypt(payload);
10608
+ const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
10609
+ stmt.run(id, timestamp, workerId, encryptedPayload);
10610
+ this.dumpDebugIfNeeded();
10611
+ return id;
10612
+ }
10613
+ getAllEntries() {
10614
+ const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
10615
+ const rows = stmt.all();
10616
+ return rows.map((row) => {
10617
+ try {
10618
+ const decrypted = this.decrypt(row.encrypted_payload);
10619
+ const parsed = JSON.parse(decrypted);
10620
+ return {
10621
+ id: row.id,
10622
+ timestamp: row.timestamp,
10623
+ workerId: row.worker_id,
10624
+ summary: parsed.summary
10625
+ };
10626
+ } catch (err) {
10627
+ return {
10628
+ id: row.id,
10629
+ timestamp: row.timestamp,
10630
+ workerId: row.worker_id,
10631
+ summary: "[Decryption Failed - Payload Corrupted]"
10632
+ };
10633
+ }
10634
+ });
10635
+ }
10636
+ getFormattedState() {
10637
+ const entries = this.getAllEntries();
10638
+ if (entries.length === 0) return "World State is currently empty.";
10639
+ return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
10640
+ }
10641
+ dumpDebugIfNeeded() {
10642
+ if (!this.debugMode) return;
10643
+ try {
10644
+ const dumpPath = path25.join(os8.tmpdir(), "cascade_world_state_debug.json");
10645
+ const entries = this.getAllEntries();
10646
+ fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
10647
+ } catch (err) {
10648
+ console.error("Failed to dump debug world state", err);
10649
+ }
10650
+ }
10651
+ close() {
10652
+ this.db.close();
10653
+ }
10654
+ };
10655
+ var ignore3 = _ignoreModule.default ?? _ignoreModule;
10656
+ var PrivacyPaths = class {
10657
+ localOnly;
10658
+ hasRules;
10659
+ constructor(policies = []) {
10660
+ this.localOnly = ignore3();
10661
+ const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
10662
+ if (patterns.length) this.localOnly.add(patterns);
10663
+ this.hasRules = patterns.length > 0;
10664
+ }
10665
+ /** True when any privacy rules are configured at all (cheap short-circuit). */
10666
+ hasPolicies() {
10667
+ return this.hasRules;
10668
+ }
10669
+ /** True when the given workspace-relative path falls under a local-only policy. */
10670
+ isLocalOnly(relativePath) {
10671
+ if (!this.hasRules || !relativePath) return false;
10672
+ try {
10673
+ return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
10674
+ } catch {
10675
+ return false;
10676
+ }
10677
+ }
10678
+ /** True when ANY of the given paths falls under a local-only policy. */
10679
+ anyLocalOnly(relativePaths) {
10680
+ return relativePaths.some((p) => this.isLocalOnly(p));
10681
+ }
10682
+ };
10683
+
10684
+ // src/core/steering/guidance.ts
10685
+ var GuidanceQueue = class {
10686
+ entries = [];
10687
+ /** Per-consumer read cursor so a broadcast entry reaches each worker once. */
10688
+ cursors = /* @__PURE__ */ new Map();
10689
+ push(text, nodeId) {
10690
+ const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
10691
+ this.entries.push(entry);
10692
+ return entry;
10693
+ }
10694
+ /**
10695
+ * New entries for this consumer since its last drain, filtered to entries
10696
+ * that target it (or target everyone). Advances the consumer's cursor.
10697
+ */
10698
+ drain(consumerId) {
10699
+ const from = this.cursors.get(consumerId) ?? 0;
10700
+ this.cursors.set(consumerId, this.entries.length);
10701
+ return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
10702
+ }
10703
+ get size() {
10704
+ return this.entries.length;
10705
+ }
10706
+ };
10224
10707
 
10225
10708
  // src/core/cascade.ts
10226
10709
  var Cascade = class _Cascade extends EventEmitter {
@@ -10240,6 +10723,9 @@ var Cascade = class _Cascade extends EventEmitter {
10240
10723
  taskAnalyzer;
10241
10724
  perfTracker;
10242
10725
  toolCreator;
10726
+ worldStateDB;
10727
+ encryptedAuditLogger;
10728
+ guidanceQueue;
10243
10729
  workspacePath;
10244
10730
  constructor(config, workspacePath, store) {
10245
10731
  super();
@@ -10261,6 +10747,23 @@ var Cascade = class _Cascade extends EventEmitter {
10261
10747
  });
10262
10748
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
10263
10749
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
10750
+ this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
10751
+ this.router.setWorldStateDB(this.worldStateDB);
10752
+ this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
10753
+ const privacyPolicies = this.config.privacy?.paths ?? [];
10754
+ if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
10755
+ this.guidanceQueue = new GuidanceQueue();
10756
+ this.router.setGuidanceQueue(this.guidanceQueue);
10757
+ }
10758
+ /**
10759
+ * Live intervention: inject a user correction into the running hierarchy.
10760
+ * Every active T3 worker (or only the one matching `nodeId`) picks it up at
10761
+ * the top of its next agent-loop iteration as a USER INTERVENTION message.
10762
+ */
10763
+ injectGuidance(text, nodeId) {
10764
+ const entry = this.guidanceQueue.push(text, nodeId);
10765
+ this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
10766
+ this.emit("guidance:injected", entry);
10264
10767
  }
10265
10768
  initOptionalFeatures() {
10266
10769
  if (this.config.cascadeAuto === true) {
@@ -10423,6 +10926,12 @@ ${last.partialOutput}` : "");
10423
10926
  if (!prompt) return null;
10424
10927
  return this.run({ prompt });
10425
10928
  }
10929
+ getWorkspacePath() {
10930
+ return this.workspacePath;
10931
+ }
10932
+ getWorldStateDB() {
10933
+ return this.worldStateDB;
10934
+ }
10426
10935
  /**
10427
10936
  * Record an explicit user rating for the last completed run.
10428
10937
  * Explicit ratings carry 3× the weight of auto-detected outcomes so user
@@ -10509,6 +11018,11 @@ ${last.partialOutput}` : "");
10509
11018
  }
10510
11019
  this.initOptionalFeatures();
10511
11020
  if (this.toolCreator) await this.toolCreator.loadPersistedTools();
11021
+ if (this.encryptedAuditLogger) {
11022
+ this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
11023
+ this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
11024
+ this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
11025
+ }
10512
11026
  this.initialized = true;
10513
11027
  })();
10514
11028
  try {
@@ -10934,6 +11448,7 @@ ${prompt}` : prompt;
10934
11448
  durationMs,
10935
11449
  costByTier: stats.costByTier,
10936
11450
  tokensByTier: stats.tokensByTier,
11451
+ costByFeature: stats.costByFeature,
10937
11452
  costPercentByTier: this.router.getTierCostPercentages()
10938
11453
  };
10939
11454
  }
@@ -11183,6 +11698,30 @@ var SlashCommandRegistry = class {
11183
11698
  return { output: out || "File changes rolled back.", handled: true };
11184
11699
  }
11185
11700
  });
11701
+ this.register({
11702
+ command: "/steer",
11703
+ description: "Steer a running task: /steer <correction for the active workers>",
11704
+ args: ["<guidance>"],
11705
+ handler: async (args, ctx) => {
11706
+ const output = await ctx.onSteer(args);
11707
+ return { output, handled: true };
11708
+ }
11709
+ });
11710
+ this.register({
11711
+ command: "/audit",
11712
+ description: "Verify the tamper-evident audit log (hash chain integrity)",
11713
+ handler: async (_args, ctx) => {
11714
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
11715
+ const logger = new AuditLogger3(ctx.workspacePath);
11716
+ try {
11717
+ const v = logger.verifyChain();
11718
+ 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.`;
11719
+ return { output, handled: true };
11720
+ } finally {
11721
+ logger.close();
11722
+ }
11723
+ }
11724
+ });
11186
11725
  this.register({
11187
11726
  command: "/branch",
11188
11727
  description: "Fork current session into parallel branches",
@@ -11830,7 +12369,8 @@ function CostTracker({
11830
12369
  tokensByTier,
11831
12370
  compact = false,
11832
12371
  savedUsd = 0,
11833
- savedPct = 0
12372
+ savedPct = 0,
12373
+ costByFeature
11834
12374
  }) {
11835
12375
  const hasTierCost = costByTier && Object.keys(costByTier).length > 0;
11836
12376
  const savingsLine = savedUsd > 0 ? `Saved $${savedUsd.toFixed(4)} (${savedPct}%) vs. running every call on T1` : null;
@@ -11929,6 +12469,16 @@ function CostTracker({
11929
12469
  ] }, tier);
11930
12470
  })
11931
12471
  ] })
12472
+ ] }),
12473
+ costByFeature && Object.keys(costByFeature).length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
12474
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: "By feature/section:" }),
12475
+ Object.entries(costByFeature).map(([feature, cost]) => /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
12476
+ /* @__PURE__ */ jsx(Box, { width: 30, children: /* @__PURE__ */ jsx(Text, { color: theme.colors.secondary, wrap: "truncate-end", children: feature }) }),
12477
+ /* @__PURE__ */ jsxs(Text, { color: theme.colors.success, children: [
12478
+ "$",
12479
+ cost.toFixed(6)
12480
+ ] })
12481
+ ] }, feature))
11932
12482
  ] })
11933
12483
  ] });
11934
12484
  }
@@ -12342,7 +12892,7 @@ function replReducer(state, action) {
12342
12892
  case "SET_TREE":
12343
12893
  return { ...state, agentTree: action.tree };
12344
12894
  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 };
12895
+ 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
12896
  case "SET_APPROVAL":
12347
12897
  return { ...state, approvalRequest: action.request };
12348
12898
  case "SET_EXECUTING":
@@ -12356,7 +12906,7 @@ function replReducer(state, action) {
12356
12906
  case "TOGGLE_COMMS":
12357
12907
  return { ...state, showComms: !state.showComms };
12358
12908
  case "CLEAR":
12359
- return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
12909
+ return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
12360
12910
  case "CLEAR_TREE":
12361
12911
  return { ...state, agentTree: null };
12362
12912
  case "TOGGLE_COST":
@@ -12405,7 +12955,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12405
12955
  const [slashIndex, setSlashIndex] = useState(0);
12406
12956
  const [identities, setIdentities] = useState([]);
12407
12957
  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 });
12958
+ 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
12959
  const [isShowingModels, setIsShowingModels] = useState(false);
12410
12960
  const [planApprovalRequest, setPlanApprovalRequest] = useState(null);
12411
12961
  const [historyOffset, setHistoryOffset] = useState(0);
@@ -12474,9 +13024,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12474
13024
  }
12475
13025
  } catch {
12476
13026
  }
12477
- const configPath = path23.join(workspacePath, ".cascade", "config.json");
13027
+ const configPath = path25.join(workspacePath, ".cascade", "config.json");
12478
13028
  try {
12479
- await fs9.mkdir(path23.dirname(configPath), { recursive: true });
13029
+ await fs9.mkdir(path25.dirname(configPath), { recursive: true });
12480
13030
  await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
12481
13031
  } catch (err) {
12482
13032
  const msg = err instanceof Error ? err.message : String(err);
@@ -12528,9 +13078,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12528
13078
  if (msg.includes("non-text parts")) return;
12529
13079
  originalLog(...args);
12530
13080
  };
12531
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
13081
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
12532
13082
  storeRef.current = store;
12533
- globalStoreRef.current = new MemoryStore(path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
13083
+ globalStoreRef.current = new MemoryStore(path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
12534
13084
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
12535
13085
  setIdentities(identityRows);
12536
13086
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -12593,7 +13143,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
12593
13143
  onThemeChange: (name) => setTheme(getTheme(name)),
12594
13144
  onExport: async (fmt) => {
12595
13145
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12596
- const exportPath = path23.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
13146
+ const exportPath = path25.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
12597
13147
  if (fmt === "json") {
12598
13148
  await fs9.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
12599
13149
  } else {
@@ -12617,6 +13167,14 @@ ${msg.content}`).join("\n\n");
12617
13167
  }
12618
13168
  return `Restored ${snapshots.length} files to their initial session state.`;
12619
13169
  },
13170
+ onSteer: (args) => {
13171
+ const text = args.join(" ").trim();
13172
+ if (!text) return "Usage: /steer <correction for the active workers>";
13173
+ const cascade = cascadeRef.current;
13174
+ if (!cascade) return "No active Cascade instance.";
13175
+ cascade.injectGuidance(text);
13176
+ return "Guidance queued \u2014 active workers apply it on their next step. (No task running? It applies to the next run's workers.)";
13177
+ },
12620
13178
  onBranch: async () => {
12621
13179
  const store = storeRef.current;
12622
13180
  if (!store) return;
@@ -12935,7 +13493,7 @@ ${lastUser.content}`;
12935
13493
  if (lastTool !== null) dispatch({ type: "SET_ACTIVE_TOOL", toolName: lastTool });
12936
13494
  const stats = cascade.getRouter().getStats();
12937
13495
  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 });
13496
+ 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
13497
  statusThrottleTimeout = null;
12940
13498
  };
12941
13499
  cascade.on("tier:root", onRoot);
@@ -13027,7 +13585,7 @@ ${lastUser.content}`;
13027
13585
  flushPeerEvents();
13028
13586
  const stats = cascade.getRouter().getStats();
13029
13587
  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 });
13588
+ dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, costByFeature: stats.costByFeature, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
13031
13589
  dispatch({ type: "COMMIT_STREAM", finalText: result.output, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
13032
13590
  persistMessage("assistant", result.output, (/* @__PURE__ */ new Date()).toISOString());
13033
13591
  const receipt = formatRunReceipt(result, stats.totalCostUsd, savings);
@@ -13312,7 +13870,7 @@ ${lastUser.content}`;
13312
13870
  ),
13313
13871
  adaptiveMode === "wide" && state.showComms && liveBudget.commsMaxEvents > 0 && /* @__PURE__ */ jsx(PeerFeed, { events: state.peerEvents, theme, maxRows: liveBudget.commsMaxEvents }),
13314
13872
  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 }),
13873
+ 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
13874
  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
13875
  state.approvalRequest && /* @__PURE__ */ jsx(ApprovalPrompt, { request: state.approvalRequest, theme, onDecision: (decision) => {
13318
13876
  dispatch({ type: "SET_APPROVAL", request: null });
@@ -13481,7 +14039,7 @@ function stringifySlashOutput(val) {
13481
14039
  }
13482
14040
  async function searchSessionsAndMessages(query, workspacePath) {
13483
14041
  if (!query) return "Usage: /search <query>";
13484
- const dbPath = path23.join(workspacePath, CASCADE_DB_FILE);
14042
+ const dbPath = path25.join(workspacePath, CASCADE_DB_FILE);
13485
14043
  try {
13486
14044
  await fs9.access(dbPath);
13487
14045
  } catch {
@@ -13515,7 +14073,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
13515
14073
  async function diagnoseRuntime(config, workspacePath) {
13516
14074
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
13517
14075
  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));
14076
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
13519
14077
  try {
13520
14078
  const sessions = store.listSessions(void 0, 3);
13521
14079
  return [
@@ -13534,7 +14092,7 @@ async function diagnoseRuntime(config, workspacePath) {
13534
14092
  }
13535
14093
  async function showRecentLogs(args, workspacePath) {
13536
14094
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
13537
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
14095
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
13538
14096
  try {
13539
14097
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
13540
14098
  if (!logs.length) return "No recent runtime logs.";
@@ -13546,7 +14104,7 @@ async function showRecentLogs(args, workspacePath) {
13546
14104
  async function loadSessionSnapshot(args, workspacePath) {
13547
14105
  const sessionId = args[0];
13548
14106
  if (!sessionId) return "Usage: /resume <sessionId>";
13549
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
14107
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
13550
14108
  try {
13551
14109
  const session = store.getSession(sessionId);
13552
14110
  if (!session) return `Session not found: ${sessionId}`;
@@ -13966,9 +14524,9 @@ function SetupWizard({ workspacePath, onComplete }) {
13966
14524
  ...anyAuto ? { cascadeAuto: true } : {}
13967
14525
  };
13968
14526
  const config = CascadeConfigSchema.parse(rawConfig);
13969
- const configDir = path23.join(workspacePath, ".cascade");
14527
+ const configDir = path25.join(workspacePath, ".cascade");
13970
14528
  await fs9.mkdir(configDir, { recursive: true });
13971
- const configPath = path23.join(workspacePath, CASCADE_CONFIG_FILE);
14529
+ const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
13972
14530
  await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
13973
14531
  savedConfigRef.current = config;
13974
14532
  dispatchRef.current({ type: "GO_DONE" });
@@ -14290,14 +14848,14 @@ function printTelemetryBanner() {
14290
14848
  async function initCommand(workspacePath = process.cwd()) {
14291
14849
  const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
14292
14850
  try {
14293
- const configDir = path23.join(workspacePath, ".cascade");
14851
+ const configDir = path25.join(workspacePath, ".cascade");
14294
14852
  await fs9.mkdir(configDir, { recursive: true });
14295
- const mdPath = path23.join(workspacePath, "CASCADE.md");
14853
+ const mdPath = path25.join(workspacePath, "CASCADE.md");
14296
14854
  if (!await fileExists(mdPath)) {
14297
14855
  await createDefaultCascadeMd(workspacePath);
14298
14856
  spin.succeed(chalk9.green("Created CASCADE.md"));
14299
14857
  }
14300
- const ignorePath = path23.join(workspacePath, ".cascadeignore");
14858
+ const ignorePath = path25.join(workspacePath, ".cascadeignore");
14301
14859
  if (!await fileExists(ignorePath)) {
14302
14860
  await createDefaultIgnoreFile(workspacePath);
14303
14861
  spin.succeed(chalk9.green("Created .cascadeignore"));
@@ -14306,7 +14864,7 @@ async function initCommand(workspacePath = process.cwd()) {
14306
14864
  console.log();
14307
14865
  console.log(chalk9.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
14308
14866
  console.log();
14309
- const configPath = path23.join(workspacePath, CASCADE_CONFIG_FILE);
14867
+ const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
14310
14868
  if (await fileExists(configPath)) {
14311
14869
  console.log(chalk9.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
14312
14870
  console.log();
@@ -14364,7 +14922,7 @@ function fromEnv(env) {
14364
14922
  return out;
14365
14923
  }
14366
14924
  async function fromClaudeCode(home) {
14367
- const file = path23.join(home, ".claude", ".credentials.json");
14925
+ const file = path25.join(home, ".claude", ".credentials.json");
14368
14926
  const data = await readJson(file);
14369
14927
  if (!data) return [];
14370
14928
  const oauth = data["claudeAiOauth"];
@@ -14387,7 +14945,7 @@ async function fromClaudeCode(home) {
14387
14945
  return [];
14388
14946
  }
14389
14947
  async function fromCodex(home) {
14390
- const file = path23.join(home, ".codex", "auth.json");
14948
+ const file = path25.join(home, ".codex", "auth.json");
14391
14949
  const data = await readJson(file);
14392
14950
  if (!data) return [];
14393
14951
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -14410,7 +14968,7 @@ async function fromCodex(home) {
14410
14968
  return [];
14411
14969
  }
14412
14970
  async function fromGemini(home) {
14413
- const file = path23.join(home, ".gemini", "oauth_creds.json");
14971
+ const file = path25.join(home, ".gemini", "oauth_creds.json");
14414
14972
  const data = await readJson(file);
14415
14973
  const accessToken = str(data?.["access_token"]);
14416
14974
  if (!accessToken) return [];
@@ -14426,7 +14984,7 @@ async function fromGemini(home) {
14426
14984
  }
14427
14985
  async function fromCopilot(home) {
14428
14986
  for (const name of ["apps.json", "hosts.json"]) {
14429
- const file = path23.join(home, ".config", "github-copilot", name);
14987
+ const file = path25.join(home, ".config", "github-copilot", name);
14430
14988
  const data = await readJson(file);
14431
14989
  if (!data) continue;
14432
14990
  for (const value of Object.values(data)) {
@@ -14447,7 +15005,7 @@ async function fromCopilot(home) {
14447
15005
  return [];
14448
15006
  }
14449
15007
  async function discoverCredentials(opts = {}) {
14450
- const home = opts.homeDir ?? os6.homedir();
15008
+ const home = opts.homeDir ?? os8.homedir();
14451
15009
  const env = opts.env ?? process.env;
14452
15010
  const groups = await Promise.all([
14453
15011
  Promise.resolve(fromEnv(env)),
@@ -14480,7 +15038,7 @@ async function doctorCommand() {
14480
15038
  checks.push({
14481
15039
  label: "Cascade config",
14482
15040
  ok: true,
14483
- detail: `Loaded ${path23.join(process.cwd(), CASCADE_CONFIG_FILE)}`
15041
+ detail: `Loaded ${path25.join(process.cwd(), CASCADE_CONFIG_FILE)}`
14484
15042
  });
14485
15043
  const providers = [
14486
15044
  { type: "anthropic", name: "Anthropic" },
@@ -14759,6 +15317,24 @@ var DashboardSocket = class {
14759
15317
  });
14760
15318
  });
14761
15319
  }
15320
+ onSessionHalt(callback) {
15321
+ this.io.on("connection", (socket) => {
15322
+ socket.on("session:halt", (payload) => {
15323
+ if (typeof payload?.sessionId === "string") {
15324
+ callback(payload.sessionId);
15325
+ }
15326
+ });
15327
+ });
15328
+ }
15329
+ onSessionSteer(callback) {
15330
+ this.io.on("connection", (socket) => {
15331
+ socket.on("session:steer", (payload) => {
15332
+ if (typeof payload?.message === "string" && payload.message.trim()) {
15333
+ callback(payload.message.trim(), payload.sessionId, payload.nodeId);
15334
+ }
15335
+ });
15336
+ });
15337
+ }
14762
15338
  onConfigUpdate(callback) {
14763
15339
  this.io.on("connection", (socket) => {
14764
15340
  socket.on("config:update", (payload) => {
@@ -14793,7 +15369,7 @@ var DashboardSocket = class {
14793
15369
 
14794
15370
  // src/dashboard/server.ts
14795
15371
  init_constants();
14796
- var __dirname$1 = path23.dirname(fileURLToPath(import.meta.url));
15372
+ var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
14797
15373
  var DashboardServer = class {
14798
15374
  app;
14799
15375
  httpServer;
@@ -14804,6 +15380,13 @@ var DashboardServer = class {
14804
15380
  globalStore = null;
14805
15381
  broadcastTimer = null;
14806
15382
  activeSessions = /* @__PURE__ */ new Map();
15383
+ activeControllers = /* @__PURE__ */ new Map();
15384
+ /**
15385
+ * Run taskIds per chat session — file snapshots are keyed by the run's
15386
+ * taskId (see t3-worker saveSnapshot), so session rollback needs the list
15387
+ * of runs the session performed in this server's lifetime.
15388
+ */
15389
+ sessionTaskIds = /* @__PURE__ */ new Map();
14807
15390
  port;
14808
15391
  host;
14809
15392
  workspacePath;
@@ -14864,6 +15447,8 @@ var DashboardServer = class {
14864
15447
  });
14865
15448
  this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
14866
15449
  const sessionId = requestedSessionId ?? randomUUID();
15450
+ const abortController = new AbortController();
15451
+ this.activeControllers.set(sessionId, abortController);
14867
15452
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
14868
15453
  const title = this.persistRunStart(sessionId, prompt);
14869
15454
  const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
@@ -14882,7 +15467,8 @@ var DashboardServer = class {
14882
15467
  this.socket.emitPeerMessage(e);
14883
15468
  });
14884
15469
  try {
14885
- const result = await cascade.run({ prompt: runPrompt });
15470
+ const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
15471
+ this.recordSessionTask(sessionId, result.taskId);
14886
15472
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
14887
15473
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
14888
15474
  this.socket.broadcast("cost:update", {
@@ -14899,6 +15485,16 @@ var DashboardServer = class {
14899
15485
  });
14900
15486
  } finally {
14901
15487
  this.activeSessions.delete(sessionId);
15488
+ this.activeControllers.delete(sessionId);
15489
+ }
15490
+ });
15491
+ this.socket.onSessionHalt((sessionId) => {
15492
+ this.activeControllers.get(sessionId)?.abort();
15493
+ });
15494
+ this.socket.onSessionSteer((message, sessionId, nodeId) => {
15495
+ const steered = this.steerSessions(message, sessionId, nodeId);
15496
+ if (steered > 0) {
15497
+ this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
14902
15498
  }
14903
15499
  });
14904
15500
  }
@@ -14946,9 +15542,9 @@ var DashboardServer = class {
14946
15542
  */
14947
15543
  persistConfig() {
14948
15544
  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");
15545
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
15546
+ fs23.mkdirSync(path25.dirname(configPath), { recursive: true });
15547
+ fs23.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
14952
15548
  } catch (err) {
14953
15549
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
14954
15550
  }
@@ -14963,15 +15559,15 @@ var DashboardServer = class {
14963
15559
  resolveDashboardSecret() {
14964
15560
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
14965
15561
  if (fromConfig) return fromConfig;
14966
- const secretPath = path23.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
15562
+ const secretPath = path25.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
14967
15563
  try {
14968
- if (fs21.existsSync(secretPath)) {
14969
- const existing = fs21.readFileSync(secretPath, "utf-8").trim();
15564
+ if (fs23.existsSync(secretPath)) {
15565
+ const existing = fs23.readFileSync(secretPath, "utf-8").trim();
14970
15566
  if (existing.length >= 16) return existing;
14971
15567
  }
14972
15568
  const generated = randomUUID();
14973
- fs21.mkdirSync(path23.dirname(secretPath), { recursive: true });
14974
- fs21.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
15569
+ fs23.mkdirSync(path25.dirname(secretPath), { recursive: true });
15570
+ fs23.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
14975
15571
  if (this.config.dashboard.auth) {
14976
15572
  console.warn(
14977
15573
  `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 +15594,7 @@ var DashboardServer = class {
14998
15594
  // ── Setup ─────────────────────────────────────
14999
15595
  getGlobalStore() {
15000
15596
  if (!this.globalStore) {
15001
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15597
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15002
15598
  this.globalStore = new MemoryStore(globalDbPath);
15003
15599
  }
15004
15600
  return this.globalStore;
@@ -15046,6 +15642,21 @@ var DashboardServer = class {
15046
15642
  }
15047
15643
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
15048
15644
  }
15645
+ /**
15646
+ * Route steering text into running Cascade instances. Targets the given
15647
+ * session, or every active session when none is specified (the desktop
15648
+ * usually has exactly one run in flight). Returns how many were reached.
15649
+ */
15650
+ steerSessions(message, sessionId, nodeId) {
15651
+ const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
15652
+ for (const cascade of targets) cascade.injectGuidance(message, nodeId);
15653
+ return targets.length;
15654
+ }
15655
+ recordSessionTask(sessionId, taskId) {
15656
+ const list = this.sessionTaskIds.get(sessionId) ?? [];
15657
+ list.push(taskId);
15658
+ this.sessionTaskIds.set(sessionId, list);
15659
+ }
15049
15660
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
15050
15661
  const now = (/* @__PURE__ */ new Date()).toISOString();
15051
15662
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -15139,12 +15750,12 @@ ${prompt}`;
15139
15750
  }
15140
15751
  }
15141
15752
  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);
15753
+ const workspaceDbPath = path25.join(this.workspacePath, CASCADE_DB_FILE);
15754
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15144
15755
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
15145
15756
  for (const watchPath of watchPaths) {
15146
- if (!fs21.existsSync(watchPath)) continue;
15147
- fs21.watchFile(watchPath, { interval: 3e3 }, () => {
15757
+ if (!fs23.existsSync(watchPath)) continue;
15758
+ fs23.watchFile(watchPath, { interval: 3e3 }, () => {
15148
15759
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
15149
15760
  });
15150
15761
  }
@@ -15252,7 +15863,8 @@ ${prompt}`;
15252
15863
  res.status(400).json({ error: "message is required and must be a string" });
15253
15864
  return;
15254
15865
  }
15255
- const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15866
+ const steered = this.steerSessions(message, sessionId, nodeId);
15867
+ const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15256
15868
  this.socket.broadcast("session:message-injected", payload);
15257
15869
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
15258
15870
  res.json({ success: true, ...payload });
@@ -15274,7 +15886,7 @@ ${prompt}`;
15274
15886
  const sessionId = req.params.id;
15275
15887
  this.store.deleteSession(sessionId);
15276
15888
  this.store.deleteRuntimeSession(sessionId);
15277
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15889
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15278
15890
  const globalStore = new MemoryStore(globalDbPath);
15279
15891
  try {
15280
15892
  globalStore.deleteRuntimeSession(sessionId);
@@ -15286,9 +15898,34 @@ ${prompt}`;
15286
15898
  this.socket.broadcast("runtime:refresh", { scope: "global" });
15287
15899
  res.json({ ok: true });
15288
15900
  });
15901
+ this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
15902
+ const sessionId = req.params.id;
15903
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
15904
+ if (!taskIds.length) {
15905
+ res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
15906
+ return;
15907
+ }
15908
+ const toRestore = /* @__PURE__ */ new Map();
15909
+ for (const taskId of taskIds) {
15910
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
15911
+ if (!toRestore.has(filePath)) toRestore.set(filePath, content);
15912
+ }
15913
+ }
15914
+ const { writeFile } = await import('fs/promises');
15915
+ let restored = 0;
15916
+ for (const [filePath, content] of toRestore) {
15917
+ try {
15918
+ await writeFile(filePath, content, "utf-8");
15919
+ restored++;
15920
+ } catch (err) {
15921
+ console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
15922
+ }
15923
+ }
15924
+ res.json({ ok: true, restored });
15925
+ });
15289
15926
  this.app.delete("/api/sessions", auth, (req, res) => {
15290
15927
  const body = req.body;
15291
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15928
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15292
15929
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
15293
15930
  const globalStore = new MemoryStore(globalDbPath);
15294
15931
  try {
@@ -15311,7 +15948,7 @@ ${prompt}`;
15311
15948
  });
15312
15949
  this.app.delete("/api/runtime", auth, (_req, res) => {
15313
15950
  this.store.deleteAllRuntimeNodes();
15314
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15951
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15315
15952
  const globalStore = new MemoryStore(globalDbPath);
15316
15953
  try {
15317
15954
  globalStore.deleteAllRuntimeNodes();
@@ -15362,6 +15999,19 @@ ${prompt}`;
15362
15999
  this.store.deleteIdentity(req.params.id);
15363
16000
  res.json({ ok: true });
15364
16001
  });
16002
+ this.app.get("/api/audit/verify", auth, async (_req, res) => {
16003
+ try {
16004
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
16005
+ const logger = new AuditLogger3(this.workspacePath);
16006
+ try {
16007
+ res.json(logger.verifyChain());
16008
+ } finally {
16009
+ logger.close();
16010
+ }
16011
+ } catch (err) {
16012
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
16013
+ }
16014
+ });
15365
16015
  this.app.get("/api/audit/:sessionId", auth, (req, res) => {
15366
16016
  const log = this.store.getAuditLog(req.params.sessionId);
15367
16017
  res.json(log);
@@ -15384,12 +16034,12 @@ ${prompt}`;
15384
16034
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
15385
16035
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
15386
16036
  try {
15387
- const configPath = path23.join(this.workspacePath, CASCADE_CONFIG_FILE);
15388
- const existing = fs21.existsSync(configPath) ? JSON.parse(fs21.readFileSync(configPath, "utf-8")) : {};
16037
+ const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
16038
+ const existing = fs23.existsSync(configPath) ? JSON.parse(fs23.readFileSync(configPath, "utf-8")) : {};
15389
16039
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
15390
16040
  const tmp = configPath + ".tmp";
15391
- fs21.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
15392
- fs21.renameSync(tmp, configPath);
16041
+ fs23.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
16042
+ fs23.renameSync(tmp, configPath);
15393
16043
  res.json({ ok: true });
15394
16044
  } catch (err) {
15395
16045
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -15417,7 +16067,7 @@ ${prompt}`;
15417
16067
  this.app.get("/api/runtime", auth, (req, res) => {
15418
16068
  const scope = req.query["scope"] ?? "workspace";
15419
16069
  if (scope === "global") {
15420
- const globalDbPath = path23.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16070
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
15421
16071
  const globalStore = new MemoryStore(globalDbPath);
15422
16072
  try {
15423
16073
  res.json({
@@ -15467,6 +16117,7 @@ ${prompt}`;
15467
16117
  });
15468
16118
  try {
15469
16119
  const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
16120
+ this.recordSessionTask(sessionId, result.taskId);
15470
16121
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
15471
16122
  this.socket.broadcast("cost:update", {
15472
16123
  sessionId,
@@ -15497,13 +16148,13 @@ ${prompt}`;
15497
16148
  }))
15498
16149
  });
15499
16150
  });
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)) {
16151
+ const prodPath = path25.resolve(__dirname$1, "../web/dist");
16152
+ const devPath = path25.resolve(__dirname$1, "../../web/dist");
16153
+ const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;
16154
+ if (fs23.existsSync(webDistPath)) {
15504
16155
  this.app.use(express.static(webDistPath));
15505
16156
  this.app.get("*", (_req, res) => {
15506
- res.sendFile(path23.join(webDistPath, "index.html"));
16157
+ res.sendFile(path25.join(webDistPath, "index.html"));
15507
16158
  });
15508
16159
  } else {
15509
16160
  this.app.get("/", (_req, res) => {
@@ -15524,7 +16175,7 @@ init_constants();
15524
16175
  async function dashboardCommand(config, workspacePath = process.cwd()) {
15525
16176
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
15526
16177
  const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
15527
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
16178
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15528
16179
  const server = new DashboardServer(config, store, workspacePath);
15529
16180
  server.watchRuntimeChanges();
15530
16181
  const gThis = globalThis;
@@ -15556,7 +16207,7 @@ init_constants();
15556
16207
  function makeIdentityCommand(workspacePath = process.cwd()) {
15557
16208
  const identity = new Command("identity").alias("id").description("Manage Cascade identities");
15558
16209
  identity.command("list").description("List all available identities").action(() => {
15559
- const store = new MemoryStore(path23.join(workspacePath, CASCADE_DB_FILE));
16210
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15560
16211
  try {
15561
16212
  const identities = store.listIdentities();
15562
16213
  console.log(chalk9.bold("\n Identities:"));
@@ -15576,7 +16227,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
15576
16227
  }
15577
16228
  });
15578
16229
  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));
16230
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15580
16231
  try {
15581
16232
  if (options.default) {
15582
16233
  const existingDefault = store.getDefaultIdentity();
@@ -15602,7 +16253,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
15602
16253
  }
15603
16254
  });
15604
16255
  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));
16256
+ const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15606
16257
  try {
15607
16258
  const identities = store.listIdentities();
15608
16259
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -15730,8 +16381,8 @@ async function exportCommand(options = {}) {
15730
16381
  let store;
15731
16382
  try {
15732
16383
  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);
16384
+ const workspaceDbPath = path25.join(workspacePath, CASCADE_DB_FILE);
16385
+ const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
15735
16386
  let dbPath = globalDbPath;
15736
16387
  try {
15737
16388
  await fs9.access(workspaceDbPath);
@@ -15773,7 +16424,7 @@ async function exportCommand(options = {}) {
15773
16424
  const ext = format === "json" ? ".json" : ".md";
15774
16425
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
15775
16426
  const defaultFile = `cascade-export-${safeName}${ext}`;
15776
- const outPath = options.output ? path23.resolve(options.output) : path23.join(process.cwd(), defaultFile);
16427
+ const outPath = options.output ? path25.resolve(options.output) : path25.join(process.cwd(), defaultFile);
15777
16428
  await fs9.writeFile(outPath, content, "utf-8");
15778
16429
  spin.succeed(chalk9.green(`Exported to ${chalk9.white(outPath)}`));
15779
16430
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -16001,11 +16652,11 @@ async function statsCommand() {
16001
16652
  dotenv.config();
16002
16653
  function warnIfBuildIsStale() {
16003
16654
  try {
16004
- const here = path23.dirname(fileURLToPath(import.meta.url));
16655
+ const here = path25.dirname(fileURLToPath(import.meta.url));
16005
16656
  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"));
16657
+ const pkgPath = path25.join(here, rel, "package.json");
16658
+ if (!fs23.existsSync(pkgPath)) continue;
16659
+ const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf-8"));
16009
16660
  if (pkg.name !== "cascade-ai") continue;
16010
16661
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
16011
16662
  console.error(