@wrongstack/webui 0.273.0 → 0.273.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.
@@ -896,7 +896,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
896
896
  return;
897
897
  }
898
898
  try {
899
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
899
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
900
900
  const registry = new SessionRegistry(globalRoot);
901
901
  const entry = await registry.get(sessionId);
902
902
  if (!entry) {
@@ -905,7 +905,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
905
905
  return;
906
906
  }
907
907
  const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
908
- const store = new DefaultSessionStore4({ dir: paths.projectSessions });
908
+ const store = new DefaultSessionStore3({ dir: paths.projectSessions });
909
909
  const reader = new DefaultSessionReader2({ store });
910
910
  const rawEntries = [];
911
911
  for await (const ev of reader.replay(sessionId)) {
@@ -1591,8 +1591,8 @@ function isInside(root, target) {
1591
1591
  }
1592
1592
 
1593
1593
  // src/server/file-handlers.ts
1594
- import * as fs3 from "fs/promises";
1595
- import * as path3 from "path";
1594
+ import * as fs4 from "fs/promises";
1595
+ import * as path4 from "path";
1596
1596
  import { atomicWrite } from "@wrongstack/core";
1597
1597
 
1598
1598
  // src/server/file-picker.ts
@@ -1643,6 +1643,34 @@ function rankFiles(paths, query, limit) {
1643
1643
  return scored.slice(0, limit).map((s) => s.path);
1644
1644
  }
1645
1645
 
1646
+ // src/server/path-containment.ts
1647
+ import * as fs3 from "fs/promises";
1648
+ import * as path3 from "path";
1649
+ function isPathInside(root, target) {
1650
+ const relative3 = path3.relative(root, target);
1651
+ return relative3 === "" || !relative3.startsWith("..") && !path3.isAbsolute(relative3);
1652
+ }
1653
+ async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
1654
+ const resolved = path3.resolve(projectRoot, inputPath);
1655
+ let stat3;
1656
+ try {
1657
+ stat3 = await fs3.stat(resolved);
1658
+ } catch {
1659
+ throw new Error(`Directory not found or not accessible: ${resolved}`);
1660
+ }
1661
+ if (!stat3.isDirectory()) {
1662
+ throw new Error(`Directory not found or not accessible: ${resolved}`);
1663
+ }
1664
+ const [realProjectRoot, realResolved] = await Promise.all([
1665
+ fs3.realpath(projectRoot),
1666
+ fs3.realpath(resolved)
1667
+ ]);
1668
+ if (!isPathInside(realProjectRoot, realResolved)) {
1669
+ throw new Error(`Path must stay inside the project root: ${projectRoot}`);
1670
+ }
1671
+ return resolved;
1672
+ }
1673
+
1646
1674
  // src/server/ws-utils.ts
1647
1675
  import { randomBytes } from "crypto";
1648
1676
  import { WebSocket } from "ws";
@@ -1673,23 +1701,73 @@ function generateAuthToken() {
1673
1701
  }
1674
1702
 
1675
1703
  // src/server/file-handlers.ts
1704
+ async function resolveFileInsideProject(projectRoot, filePath) {
1705
+ const resolved = path4.resolve(projectRoot, filePath);
1706
+ if (!isPathInside(projectRoot, resolved)) {
1707
+ throw new Error("Path outside project root");
1708
+ }
1709
+ const { parent, base } = splitParentAndBase(resolved);
1710
+ const realProjectRoot = await fs4.realpath(projectRoot);
1711
+ const realParent = await realpathAllowMissing(parent);
1712
+ const realFull = path4.join(realParent, base);
1713
+ if (!isPathInside(realProjectRoot, realFull)) {
1714
+ throw new Error("Path outside project root");
1715
+ }
1716
+ return realFull;
1717
+ }
1718
+ function splitParentAndBase(p) {
1719
+ const base = path4.basename(p);
1720
+ const parent = path4.dirname(p);
1721
+ return { parent, base };
1722
+ }
1723
+ async function realpathAllowMissing(p) {
1724
+ try {
1725
+ return await fs4.realpath(p);
1726
+ } catch (err) {
1727
+ if (err.code !== "ENOENT") throw err;
1728
+ }
1729
+ const segments = [];
1730
+ let cursor = p;
1731
+ while (true) {
1732
+ const parent = path4.dirname(cursor);
1733
+ if (parent === cursor) {
1734
+ throw new Error("Path outside project root");
1735
+ }
1736
+ segments.unshift(path4.basename(cursor));
1737
+ try {
1738
+ const realParent = await fs4.realpath(parent);
1739
+ return path4.join(realParent, ...segments);
1740
+ } catch (err) {
1741
+ if (err.code !== "ENOENT") throw err;
1742
+ cursor = parent;
1743
+ }
1744
+ }
1745
+ }
1676
1746
  async function handleFilesTree(ws, msg, projectRoot) {
1677
1747
  const payload = msg.payload;
1678
1748
  const rawPath = payload?.path?.trim();
1679
- const treeRoot = rawPath && rawPath !== "." ? path3.resolve(projectRoot, rawPath) : projectRoot;
1680
- if (!treeRoot.startsWith(projectRoot + path3.sep) && treeRoot !== projectRoot) {
1749
+ let treeRoot;
1750
+ let realProjectRoot;
1751
+ try {
1752
+ if (rawPath && rawPath !== ".") {
1753
+ treeRoot = await resolveWorkingDirInsideProject(projectRoot, rawPath);
1754
+ } else {
1755
+ treeRoot = projectRoot;
1756
+ }
1757
+ realProjectRoot = await fs4.realpath(projectRoot);
1758
+ } catch {
1681
1759
  send(ws, {
1682
1760
  type: "files.tree",
1683
1761
  payload: { root: projectRoot, tree: [], error: "Path outside project root" }
1684
1762
  });
1685
1763
  return;
1686
1764
  }
1687
- const pathPrefix = treeRoot === projectRoot ? "" : (path3.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1765
+ const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1688
1766
  async function buildTree(dir, rel, depth) {
1689
1767
  if (depth > 10) return [];
1690
1768
  let entries = [];
1691
1769
  try {
1692
- entries = await fs3.readdir(dir, { withFileTypes: true });
1770
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1693
1771
  } catch {
1694
1772
  return [];
1695
1773
  }
@@ -1701,11 +1779,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
1701
1779
  for (const e of entries) {
1702
1780
  if (isHiddenEntry(e.name)) continue;
1703
1781
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1704
- const childAbs = path3.join(dir, e.name);
1782
+ const childAbs = path4.join(dir, e.name);
1705
1783
  const childPath = pathPrefix + childRel;
1706
1784
  if (e.isDirectory()) {
1707
1785
  if (SKIP_DIRS.has(e.name)) continue;
1708
- const children = await buildTree(childAbs, childRel, depth + 1);
1786
+ let realChild;
1787
+ try {
1788
+ realChild = await fs4.realpath(childAbs);
1789
+ } catch {
1790
+ continue;
1791
+ }
1792
+ if (!isPathInside(realProjectRoot, realChild)) {
1793
+ continue;
1794
+ }
1795
+ const children = await buildTree(realChild, childRel, depth + 1);
1709
1796
  nodes.push({ name: e.name, path: childPath, type: "directory", children });
1710
1797
  } else if (e.isFile()) {
1711
1798
  nodes.push({ name: e.name, path: childPath, type: "file" });
@@ -1715,10 +1802,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
1715
1802
  }
1716
1803
  try {
1717
1804
  const tree = await buildTree(treeRoot, "", 0);
1718
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1805
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1719
1806
  send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
1720
1807
  } catch (err) {
1721
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1808
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1722
1809
  send(ws, {
1723
1810
  type: "files.tree",
1724
1811
  payload: { root: rootLabel, tree: [], error: errMessage(err) }
@@ -1727,13 +1814,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
1727
1814
  }
1728
1815
  async function handleFilesRead(ws, msg, projectRoot) {
1729
1816
  const { filePath } = msg.payload;
1730
- const resolved = path3.resolve(projectRoot, filePath);
1731
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1817
+ let realResolved;
1818
+ try {
1819
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1820
+ } catch {
1732
1821
  send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
1733
1822
  return;
1734
1823
  }
1735
1824
  try {
1736
- const content = await fs3.readFile(resolved, "utf8");
1825
+ const content = await fs4.readFile(realResolved, "utf8");
1737
1826
  send(ws, { type: "files.read", payload: { filePath, content } });
1738
1827
  } catch (err) {
1739
1828
  send(ws, {
@@ -1744,16 +1833,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
1744
1833
  }
1745
1834
  async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1746
1835
  const { filePath, content } = msg.payload;
1747
- const resolved = path3.resolve(projectRoot, filePath);
1748
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1836
+ let realResolved;
1837
+ try {
1838
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1839
+ } catch {
1749
1840
  send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
1750
1841
  return;
1751
1842
  }
1752
1843
  try {
1753
- await atomicWrite(resolved, content);
1844
+ await atomicWrite(realResolved, content);
1754
1845
  send(ws, { type: "files.written", payload: { filePath, success: true } });
1755
1846
  if (opts.onWritten) {
1756
- void Promise.resolve(opts.onWritten(resolved)).catch(() => void 0);
1847
+ void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
1757
1848
  }
1758
1849
  } catch (err) {
1759
1850
  send(ws, {
@@ -1765,8 +1856,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1765
1856
  async function handleFilesList(ws, msg, projectRoot) {
1766
1857
  const payload = msg.payload ?? {};
1767
1858
  const limit = payload.limit ?? 50;
1768
- const listRoot = payload.path ? path3.resolve(projectRoot, payload.path) : projectRoot;
1769
- if (!listRoot.startsWith(projectRoot + path3.sep) && listRoot !== projectRoot) {
1859
+ let listRoot;
1860
+ let realProjectRoot;
1861
+ try {
1862
+ if (payload.path) {
1863
+ listRoot = await resolveWorkingDirInsideProject(projectRoot, payload.path);
1864
+ } else {
1865
+ listRoot = projectRoot;
1866
+ }
1867
+ realProjectRoot = await fs4.realpath(projectRoot);
1868
+ } catch {
1770
1869
  send(ws, { type: "files.list", payload: { files: [] } });
1771
1870
  return;
1772
1871
  }
@@ -1775,7 +1874,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1775
1874
  if (depth > 8 || results.length >= 600) return;
1776
1875
  let entries = [];
1777
1876
  try {
1778
- entries = await fs3.readdir(dir, { withFileTypes: true });
1877
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1779
1878
  } catch {
1780
1879
  return;
1781
1880
  }
@@ -1785,7 +1884,16 @@ async function handleFilesList(ws, msg, projectRoot) {
1785
1884
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1786
1885
  if (e.isDirectory()) {
1787
1886
  if (SKIP_DIRS.has(e.name)) continue;
1788
- await walk(path3.join(dir, e.name), childRel, depth + 1);
1887
+ let realChild;
1888
+ try {
1889
+ realChild = await fs4.realpath(path4.join(dir, e.name));
1890
+ } catch {
1891
+ continue;
1892
+ }
1893
+ if (!isPathInside(realProjectRoot, realChild)) {
1894
+ continue;
1895
+ }
1896
+ await walk(realChild, childRel, depth + 1);
1789
1897
  } else if (e.isFile()) {
1790
1898
  results.push(childRel);
1791
1899
  }
@@ -1799,7 +1907,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1799
1907
  }
1800
1908
 
1801
1909
  // src/server/completion-handlers.ts
1802
- import * as path4 from "path";
1910
+ import * as path5 from "path";
1803
1911
  import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
1804
1912
  var MAX_PREFIX_CHARS = 12e3;
1805
1913
  var MAX_SUFFIX_CHARS = 4e3;
@@ -1874,8 +1982,8 @@ async function handleCompletionRequest(ws, msg, opts) {
1874
1982
  return;
1875
1983
  }
1876
1984
  const payload = parsed.payload;
1877
- const projectRoot = path4.resolve(opts.projectRoot);
1878
- const resolved = path4.resolve(projectRoot, payload.filePath);
1985
+ const projectRoot = path5.resolve(opts.projectRoot);
1986
+ const resolved = path5.resolve(projectRoot, payload.filePath);
1879
1987
  if (!isInside2(projectRoot, resolved)) {
1880
1988
  send(ws, {
1881
1989
  type: "completion.result",
@@ -2274,7 +2382,7 @@ function buildSearchQuery(linePrefix, filePath) {
2274
2382
  if (memberMatch?.[1]) return memberMatch[1];
2275
2383
  const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
2276
2384
  if (token && token.length >= 2) return token;
2277
- return path4.basename(filePath, path4.extname(filePath));
2385
+ return path5.basename(filePath, path5.extname(filePath));
2278
2386
  }
2279
2387
  function currentLinePrefix(prefix) {
2280
2388
  const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
@@ -2304,7 +2412,7 @@ function head(value, max) {
2304
2412
  return value.length <= max ? value : value.slice(0, max);
2305
2413
  }
2306
2414
  function isInside2(root, target) {
2307
- return target === root || target.startsWith(root + path4.sep);
2415
+ return target === root || target.startsWith(root + path5.sep);
2308
2416
  }
2309
2417
 
2310
2418
  // src/server/memory-handlers.ts
@@ -2558,8 +2666,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
2558
2666
  }
2559
2667
 
2560
2668
  // src/server/skills-handlers.ts
2561
- import { promises as fs4 } from "fs";
2562
- import path5 from "path";
2669
+ import { promises as fs5 } from "fs";
2670
+ import path6 from "path";
2563
2671
  import JSZip from "jszip";
2564
2672
  import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
2565
2673
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
@@ -2630,19 +2738,19 @@ async function handleSkillsContent(ws, ctx, msg) {
2630
2738
  send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
2631
2739
  return;
2632
2740
  }
2633
- const body = await fs4.readFile(entry.path, "utf8");
2634
- const skillDir = path5.dirname(entry.path);
2741
+ const body = await fs5.readFile(entry.path, "utf8");
2742
+ const skillDir = path6.dirname(entry.path);
2635
2743
  let relatedFiles = [];
2636
2744
  try {
2637
- const files = await fs4.readdir(skillDir);
2638
- relatedFiles = files.filter((f) => f !== path5.basename(entry.path)).map((f) => path5.join(skillDir, f));
2745
+ const files = await fs5.readdir(skillDir);
2746
+ relatedFiles = files.filter((f) => f !== path6.basename(entry.path)).map((f) => path6.join(skillDir, f));
2639
2747
  } catch {
2640
2748
  }
2641
2749
  const nameLower = name2.toLowerCase();
2642
2750
  const refResults = await Promise.all(
2643
2751
  entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
2644
2752
  try {
2645
- const content = await fs4.readFile(e.path, "utf8");
2753
+ const content = await fs5.readFile(e.path, "utf8");
2646
2754
  return [e.name, content.toLowerCase().includes(nameLower)];
2647
2755
  } catch {
2648
2756
  return [e.name, false];
@@ -2732,14 +2840,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
2732
2840
  }
2733
2841
  const createPayload = parsed.value;
2734
2842
  try {
2735
- const targetDir = createPayload.scope === "global" ? path5.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path5.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2843
+ const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2736
2844
  try {
2737
- await fs4.access(targetDir);
2845
+ await fs5.access(targetDir);
2738
2846
  send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
2739
2847
  return;
2740
2848
  } catch {
2741
2849
  }
2742
- await fs4.mkdir(targetDir, { recursive: true });
2850
+ await fs5.mkdir(targetDir, { recursive: true });
2743
2851
  const lines = createPayload.description.trim().split("\n");
2744
2852
  const firstLine = lines[0].trim();
2745
2853
  const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
@@ -2787,13 +2895,13 @@ ${trigger}
2787
2895
  "- `bug-hunter` \u2014 for systematic bug detection patterns",
2788
2896
  "- `output-standards` \u2014 for standardized `<next_steps>` formatting"
2789
2897
  ].join("\n");
2790
- await atomicWrite2(path5.join(targetDir, "SKILL.md"), skillContent);
2898
+ await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
2791
2899
  send(ws, {
2792
2900
  type: "skills.created",
2793
2901
  payload: {
2794
2902
  success: true,
2795
2903
  error: null,
2796
- skill: { name: createPayload.name.trim(), path: path5.join(targetDir, "SKILL.md"), scope: createPayload.scope }
2904
+ skill: { name: createPayload.name.trim(), path: path6.join(targetDir, "SKILL.md"), scope: createPayload.scope }
2797
2905
  }
2798
2906
  });
2799
2907
  } catch (err) {
@@ -2857,23 +2965,23 @@ import {
2857
2965
  Agent,
2858
2966
  AutoCompactionMiddleware,
2859
2967
  Context,
2860
- DefaultMemoryStore as DefaultMemoryStore2,
2861
- DefaultModeStore as DefaultModeStore2,
2968
+ DefaultMemoryStore,
2969
+ DefaultModeStore,
2862
2970
  DefaultModelsRegistry,
2863
2971
  DefaultSessionReader,
2864
- DefaultSessionStore as DefaultSessionStore3,
2865
- DefaultSkillLoader as DefaultSkillLoader2,
2866
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder4,
2867
- DefaultTokenCounter as DefaultTokenCounter2,
2972
+ DefaultSessionStore as DefaultSessionStore2,
2973
+ DefaultSkillLoader,
2974
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
2975
+ DefaultTokenCounter,
2868
2976
  AnnotationsStore,
2869
2977
  CollaborationBus,
2870
2978
  collabPauseMiddleware,
2871
2979
  collabInjectMiddleware,
2872
2980
  estimateRequestTokensCalibrated,
2873
2981
  EventBus,
2874
- createStrategyCompactor as createStrategyCompactor2,
2982
+ createStrategyCompactor,
2875
2983
  ProviderRegistry,
2876
- TOKENS as TOKENS2,
2984
+ TOKENS,
2877
2985
  ToolRegistry,
2878
2986
  atomicWrite as atomicWrite6,
2879
2987
  createDefaultPipelines,
@@ -2892,110 +3000,10 @@ import {
2892
3000
  import { ToolExecutor } from "@wrongstack/core/execution";
2893
3001
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
2894
3002
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
2895
- import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
3003
+ import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
2896
3004
  import { MCPRegistry } from "@wrongstack/mcp";
2897
3005
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
2898
-
2899
- // ../runtime/src/container.ts
2900
- import {
2901
- Container,
2902
- DefaultConfigStore,
2903
- DefaultErrorHandler,
2904
- DefaultMemoryStore,
2905
- DefaultModeStore,
2906
- DefaultPermissionPolicy,
2907
- DefaultRetryPolicy,
2908
- DefaultSecretScrubber,
2909
- DefaultSessionStore,
2910
- DefaultSkillLoader,
2911
- DefaultSystemPromptBuilder,
2912
- DefaultTokenCounter,
2913
- createStrategyCompactor,
2914
- buildRecoveryStrategies,
2915
- TOKENS
2916
- } from "@wrongstack/core";
2917
- function createDefaultContainer(opts) {
2918
- const { config, wpaths, logger, modelsRegistry } = opts;
2919
- const container = new Container();
2920
- const configStore = new DefaultConfigStore(config);
2921
- container.bind(TOKENS.ConfigStore, () => configStore);
2922
- container.bind(TOKENS.Logger, () => logger);
2923
- container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
2924
- container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
2925
- container.bind(
2926
- TOKENS.ErrorHandler,
2927
- () => new DefaultErrorHandler(
2928
- buildRecoveryStrategies({
2929
- compactor: container.resolve(TOKENS.Compactor),
2930
- modelsRegistry,
2931
- getConfig: () => configStore.get()
2932
- })
2933
- )
2934
- );
2935
- container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
2936
- container.bind(
2937
- TOKENS.TokenCounter,
2938
- () => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
2939
- );
2940
- const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
2941
- container.bind(TOKENS.ModeStore, () => modeStore);
2942
- container.bind(
2943
- TOKENS.SessionStore,
2944
- () => new DefaultSessionStore({
2945
- dir: wpaths.projectSessions,
2946
- // Scrub secrets out of persisted user/model turns (F-06). Tool output
2947
- // is already scrubbed by the executor.
2948
- secretScrubber: container.resolve(TOKENS.SecretScrubber)
2949
- })
2950
- );
2951
- const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
2952
- container.bind(TOKENS.MemoryStore, () => memoryStore);
2953
- const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
2954
- container.bind(TOKENS.SkillLoader, () => skillLoader);
2955
- if (opts.systemPrompt) {
2956
- container.bind(
2957
- TOKENS.SystemPromptBuilder,
2958
- () => new DefaultSystemPromptBuilder(opts.systemPrompt)
2959
- );
2960
- }
2961
- container.bind(
2962
- TOKENS.PermissionPolicy,
2963
- () => {
2964
- const policyOptions = {
2965
- trustFile: wpaths.projectTrust,
2966
- yolo: opts.permission?.yolo ?? false,
2967
- yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
2968
- confirmDestructive: opts.permission?.confirmDestructive ?? false
2969
- };
2970
- if (opts.permission?.promptDelegate !== void 0) {
2971
- policyOptions.promptDelegate = opts.permission.promptDelegate;
2972
- }
2973
- return new DefaultPermissionPolicy(policyOptions);
2974
- }
2975
- );
2976
- container.bind(
2977
- TOKENS.Compactor,
2978
- () => (
2979
- // Strategy comes from config.context.strategy: 'hybrid' (default, lossless
2980
- // rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
2981
- // (LLM-driven selection). The LLM strategies resolve their provider from
2982
- // ctx at compact()-time, so binding here (before context.provider exists)
2983
- // is safe. preserveK / eliseThreshold are class-level fallbacks; the active
2984
- // ContextWindowPolicy in ctx.meta normally overrides both at runtime.
2985
- // eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
2986
- // essentially every tool_result (anything > 1 token).
2987
- createStrategyCompactor({
2988
- strategy: config.context?.strategy,
2989
- preserveK: opts.compactor?.preserveK ?? 10,
2990
- eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
2991
- smart: true,
2992
- summarizerModel: config.context?.summarizerModel,
2993
- llmSelector: config.context?.llmSelector
2994
- })
2995
- )
2996
- );
2997
- return container;
2998
- }
3006
+ import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
2999
3007
 
3000
3008
  // src/server/boot.ts
3001
3009
  import {
@@ -3356,6 +3364,13 @@ Type: ${task.type}`;
3356
3364
  });
3357
3365
  const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
3358
3366
  const completedPhases = phases.filter((p) => p.status === "completed").length;
3367
+ const failedPhases = phases.filter((p) => p.status === "failed").length;
3368
+ const failedTasks = phases.reduce(
3369
+ (sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "failed").length,
3370
+ 0
3371
+ );
3372
+ const lastFailed = phases.filter((p) => p.status === "failed").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
3373
+ const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
3359
3374
  return {
3360
3375
  title: this.graph.title,
3361
3376
  phases: phaseItems,
@@ -3364,7 +3379,18 @@ Type: ${task.type}`;
3364
3379
  overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
3365
3380
  autonomous: this.graph.autonomous,
3366
3381
  totalTasks,
3367
- completedTasks
3382
+ completedTasks,
3383
+ // Structured progress + lastError consumed by the autophase store (were
3384
+ // defined client-side but never sent, so they stayed null on the board).
3385
+ progress: {
3386
+ totalPhases: phases.length,
3387
+ completed: completedPhases,
3388
+ failed: failedPhases,
3389
+ totalTasks,
3390
+ completedTasks,
3391
+ failedTasks
3392
+ },
3393
+ lastError
3368
3394
  };
3369
3395
  }
3370
3396
  sendState(client) {
@@ -3786,7 +3812,7 @@ var SddWizardWebSocketHandler = class {
3786
3812
  };
3787
3813
 
3788
3814
  // src/server/sdd-wizard-wiring.ts
3789
- import * as path6 from "path";
3815
+ import * as path7 from "path";
3790
3816
  import { spawnSync as spawnSync2 } from "child_process";
3791
3817
  import {
3792
3818
  makeCommandVerifier,
@@ -3822,7 +3848,7 @@ function buildSddWizardDeps(opts) {
3822
3848
  makeDriver: () => new SddInterviewDriver({
3823
3849
  specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
3824
3850
  graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
3825
- sessionPath: path6.join(opts.paths.projectDir, "sdd-wizard-session.json")
3851
+ sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
3826
3852
  }),
3827
3853
  runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
3828
3854
  startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
@@ -3891,9 +3917,6 @@ async function handleSddWizardRoute(_ws, msg, handlers) {
3891
3917
  return true;
3892
3918
  }
3893
3919
 
3894
- // src/server/index.ts
3895
- import { makeLightSubagentFactory } from "@wrongstack/runtime";
3896
-
3897
3920
  // src/server/collaboration-ws-handler.ts
3898
3921
  import { randomUUID } from "crypto";
3899
3922
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -4620,16 +4643,16 @@ var CollaborationWebSocketHandler = class {
4620
4643
  };
4621
4644
 
4622
4645
  // src/server/projects-manifest.ts
4623
- import * as fs5 from "fs/promises";
4624
- import * as path7 from "path";
4646
+ import * as fs6 from "fs/promises";
4647
+ import * as path8 from "path";
4625
4648
  import { projectSlug } from "@wrongstack/core";
4626
4649
  function projectsJsonPath(globalConfigPath) {
4627
- const base = path7.dirname(globalConfigPath);
4628
- return path7.join(base, "projects.json");
4650
+ const base = path8.dirname(globalConfigPath);
4651
+ return path8.join(base, "projects.json");
4629
4652
  }
4630
4653
  async function loadManifest(globalConfigPath) {
4631
4654
  try {
4632
- const raw = await fs5.readFile(projectsJsonPath(globalConfigPath), "utf8");
4655
+ const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
4633
4656
  const parsed = JSON.parse(raw);
4634
4657
  return { projects: parsed.projects ?? [] };
4635
4658
  } catch {
@@ -4638,16 +4661,16 @@ async function loadManifest(globalConfigPath) {
4638
4661
  }
4639
4662
  async function saveManifest(manifest, globalConfigPath) {
4640
4663
  const file = projectsJsonPath(globalConfigPath);
4641
- await fs5.mkdir(path7.dirname(file), { recursive: true });
4642
- await fs5.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4664
+ await fs6.mkdir(path8.dirname(file), { recursive: true });
4665
+ await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4643
4666
  }
4644
4667
  function generateProjectSlug(rootPath) {
4645
4668
  return projectSlug(rootPath);
4646
4669
  }
4647
4670
  async function ensureProjectDataDir(slug, globalConfigPath) {
4648
- const base = path7.dirname(globalConfigPath);
4649
- const dir = path7.join(base, "projects", slug);
4650
- await fs5.mkdir(dir, { recursive: true });
4671
+ const base = path8.dirname(globalConfigPath);
4672
+ const dir = path8.join(base, "projects", slug);
4673
+ await fs6.mkdir(dir, { recursive: true });
4651
4674
  return dir;
4652
4675
  }
4653
4676
 
@@ -5073,14 +5096,14 @@ function registerShutdownHandlers(res) {
5073
5096
 
5074
5097
  // src/server/instance-registry.ts
5075
5098
  import * as os from "os";
5076
- import * as path8 from "path";
5077
- import * as fs6 from "fs/promises";
5099
+ import * as path9 from "path";
5100
+ import * as fs7 from "fs/promises";
5078
5101
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
5079
5102
  function defaultBaseDir() {
5080
- return path8.join(os.homedir(), ".wrongstack");
5103
+ return path9.join(os.homedir(), ".wrongstack");
5081
5104
  }
5082
5105
  function registryPath(baseDir = defaultBaseDir()) {
5083
- return path8.join(baseDir, "webui-instances.json");
5106
+ return path9.join(baseDir, "webui-instances.json");
5084
5107
  }
5085
5108
  function isPidAlive(pid) {
5086
5109
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -5093,7 +5116,7 @@ function isPidAlive(pid) {
5093
5116
  }
5094
5117
  async function load(file) {
5095
5118
  try {
5096
- const raw = await fs6.readFile(file, "utf8");
5119
+ const raw = await fs7.readFile(file, "utf8");
5097
5120
  const parsed = JSON.parse(raw);
5098
5121
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
5099
5122
  return parsed;
@@ -5238,19 +5261,19 @@ function computeUsageCost(usage, rates) {
5238
5261
  }
5239
5262
 
5240
5263
  // src/server/provider-handlers.ts
5241
- import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core";
5264
+ import { DefaultSecretScrubber } from "@wrongstack/core";
5242
5265
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
5243
5266
 
5244
5267
  // src/server/provider-config-io.ts
5245
- import * as fs7 from "fs/promises";
5246
- import * as path9 from "path";
5268
+ import * as fs8 from "fs/promises";
5269
+ import * as path10 from "path";
5247
5270
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
5248
5271
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
5249
5272
  import { DefaultSecretVault } from "@wrongstack/core";
5250
5273
  async function loadSavedProviders(configPath, vault) {
5251
5274
  let raw;
5252
5275
  try {
5253
- raw = await fs7.readFile(configPath, "utf8");
5276
+ raw = await fs8.readFile(configPath, "utf8");
5254
5277
  } catch {
5255
5278
  return {};
5256
5279
  }
@@ -5267,7 +5290,7 @@ async function saveProviders(configPath, vault, providers) {
5267
5290
  let raw;
5268
5291
  let fileExists = true;
5269
5292
  try {
5270
- raw = await fs7.readFile(configPath, "utf8");
5293
+ raw = await fs8.readFile(configPath, "utf8");
5271
5294
  } catch (err) {
5272
5295
  if (err.code !== "ENOENT") {
5273
5296
  throw new Error(
@@ -5295,7 +5318,7 @@ async function saveProviders(configPath, vault, providers) {
5295
5318
  await atomicWrite4(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
5296
5319
  }
5297
5320
  function createProviderConfigIO(configPath) {
5298
- const keyFile = path9.join(path9.dirname(configPath), ".key");
5321
+ const keyFile = path10.join(path10.dirname(configPath), ".key");
5299
5322
  const vault = new DefaultSecretVault({ keyFile });
5300
5323
  return {
5301
5324
  load: () => loadSavedProviders(configPath, vault),
@@ -5424,7 +5447,7 @@ function projectSavedProviders(providers) {
5424
5447
  return view;
5425
5448
  });
5426
5449
  }
5427
- var probeScrubber = new DefaultSecretScrubber2();
5450
+ var probeScrubber = new DefaultSecretScrubber();
5428
5451
  function createProviderHandlers(deps2) {
5429
5452
  const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
5430
5453
  let configWriteLock = deps2.getConfigWriteLock();
@@ -5613,7 +5636,7 @@ function createProviderHandlers(deps2) {
5613
5636
 
5614
5637
  // src/server/mode-handlers.ts
5615
5638
  import {
5616
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2
5639
+ DefaultSystemPromptBuilder
5617
5640
  } from "@wrongstack/core";
5618
5641
  function createModeHandlers(ctx) {
5619
5642
  return {
@@ -5661,7 +5684,7 @@ function createModeHandlers(ctx) {
5661
5684
  }
5662
5685
  ctx.setModeId(id);
5663
5686
  const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
5664
- const freshBuilder = new DefaultSystemPromptBuilder2({
5687
+ const freshBuilder = new DefaultSystemPromptBuilder({
5665
5688
  memoryStore: ctx.memoryStore,
5666
5689
  skillLoader: ctx.skillLoader,
5667
5690
  modeStore: ctx.modeStore,
@@ -5692,40 +5715,10 @@ function createModeHandlers(ctx) {
5692
5715
  import * as fs9 from "fs/promises";
5693
5716
  import * as path11 from "path";
5694
5717
  import {
5695
- DefaultSessionStore as DefaultSessionStore2,
5696
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
5718
+ DefaultSessionStore,
5719
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
5697
5720
  getSessionRegistry
5698
5721
  } from "@wrongstack/core";
5699
-
5700
- // src/server/path-containment.ts
5701
- import * as fs8 from "fs/promises";
5702
- import * as path10 from "path";
5703
- function isPathInside(root, target) {
5704
- const relative3 = path10.relative(root, target);
5705
- return relative3 === "" || !relative3.startsWith("..") && !path10.isAbsolute(relative3);
5706
- }
5707
- async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
5708
- const resolved = path10.resolve(projectRoot, inputPath);
5709
- let stat3;
5710
- try {
5711
- stat3 = await fs8.stat(resolved);
5712
- } catch {
5713
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5714
- }
5715
- if (!stat3.isDirectory()) {
5716
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5717
- }
5718
- const [realProjectRoot, realResolved] = await Promise.all([
5719
- fs8.realpath(projectRoot),
5720
- fs8.realpath(resolved)
5721
- ]);
5722
- if (!isPathInside(realProjectRoot, realResolved)) {
5723
- throw new Error(`Path must stay inside the project root: ${projectRoot}`);
5724
- }
5725
- return resolved;
5726
- }
5727
-
5728
- // src/server/project-handlers.ts
5729
5722
  function createProjectHandlers(ctx) {
5730
5723
  return {
5731
5724
  listProjects: async (ws) => {
@@ -5837,7 +5830,7 @@ function createProjectHandlers(ctx) {
5837
5830
  try {
5838
5831
  const modeId = ctx.getModeId();
5839
5832
  const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
5840
- const switchBuilder = new DefaultSystemPromptBuilder3({
5833
+ const switchBuilder = new DefaultSystemPromptBuilder2({
5841
5834
  memoryStore: ctx.memoryStore,
5842
5835
  skillLoader: ctx.skillLoader,
5843
5836
  modeStore: ctx.modeStore,
@@ -5861,7 +5854,7 @@ function createProjectHandlers(ctx) {
5861
5854
  "sessions"
5862
5855
  );
5863
5856
  await fs9.mkdir(newSessionsDir, { recursive: true });
5864
- const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
5857
+ const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
5865
5858
  const oldSession = ctx.getSession();
5866
5859
  const oldSessionId = oldSession.id;
5867
5860
  try {
@@ -6558,6 +6551,22 @@ async function handleModeRoute(ws, msg, handlers) {
6558
6551
  }
6559
6552
  }
6560
6553
 
6554
+ // src/server/prefs-routes.ts
6555
+ async function handlePrefsRoute(ws, msg, handlers) {
6556
+ switch (msg.type) {
6557
+ case "prefs.get": {
6558
+ await handlers.getPrefs(ws);
6559
+ return true;
6560
+ }
6561
+ case "prefs.update": {
6562
+ await handlers.updatePrefs(ws, msg.payload ?? {});
6563
+ return true;
6564
+ }
6565
+ default:
6566
+ return false;
6567
+ }
6568
+ }
6569
+
6561
6570
  // src/server/shell-git-routes.ts
6562
6571
  async function handleShellGitRoute(ws, msg, handlers) {
6563
6572
  switch (msg.type) {
@@ -6598,6 +6607,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
6598
6607
  }
6599
6608
  }
6600
6609
 
6610
+ // src/server/mcp-routes.ts
6611
+ async function handleMcpRoute(ws, msg, handlers) {
6612
+ switch (msg.type) {
6613
+ case "mcp.list":
6614
+ await handlers.list(ws, msg);
6615
+ return true;
6616
+ case "mcp.add":
6617
+ await handlers.add(ws, msg);
6618
+ return true;
6619
+ case "mcp.update":
6620
+ await handlers.update(ws, msg);
6621
+ return true;
6622
+ case "mcp.remove":
6623
+ await handlers.remove(ws, msg);
6624
+ return true;
6625
+ case "mcp.enable":
6626
+ await handlers.enable(ws, msg);
6627
+ return true;
6628
+ case "mcp.disable":
6629
+ await handlers.disable(ws, msg);
6630
+ return true;
6631
+ case "mcp.sleep":
6632
+ await handlers.sleep(ws, msg);
6633
+ return true;
6634
+ case "mcp.wake":
6635
+ await handlers.wake(ws, msg);
6636
+ return true;
6637
+ case "mcp.restart":
6638
+ await handlers.restart(ws, msg);
6639
+ return true;
6640
+ case "mcp.discover":
6641
+ await handlers.discover(ws, msg);
6642
+ return true;
6643
+ default:
6644
+ return false;
6645
+ }
6646
+ }
6647
+
6601
6648
  // src/server/brain-routes.ts
6602
6649
  async function handleBrainRoute(ws, msg, handlers) {
6603
6650
  switch (msg.type) {
@@ -7069,11 +7116,13 @@ function setupEvents(deps2) {
7069
7116
  events.on("provider.response", (e) => {
7070
7117
  if (e.usage?.input != null) {
7071
7118
  const maxCtx = context.provider.capabilities.maxContext;
7072
- const pct = maxCtx > 0 ? e.usage.input / maxCtx : 0;
7119
+ const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
7120
+ const load2 = Math.max(0, Math.min(1, rawLoad));
7073
7121
  const costUsd = context.tokenCounter.estimateCost().total;
7074
7122
  forwardSubagent("ctx_pct", {
7075
7123
  subagentId: "leader",
7076
- load: pct,
7124
+ load: load2,
7125
+ rawLoad,
7077
7126
  tokens: e.usage.input,
7078
7127
  maxContext: maxCtx,
7079
7128
  costUsd
@@ -7724,6 +7773,7 @@ async function handleGoalGet(projectRoot, broadcast2) {
7724
7773
 
7725
7774
  // src/server/index.ts
7726
7775
  async function startWebUI(opts = {}) {
7776
+ ensureSessionShell();
7727
7777
  const requestedWsPort = opts.wsPort ?? 3457;
7728
7778
  const wsHost = opts.wsHost ?? "127.0.0.1";
7729
7779
  const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
@@ -7805,7 +7855,7 @@ async function startWebUI(opts = {}) {
7805
7855
  ttlSeconds: 24 * 3600
7806
7856
  });
7807
7857
  const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
7808
- const configStore = opts.services?.configStore ?? container.resolve(TOKENS2.ConfigStore);
7858
+ const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
7809
7859
  const providerRegistry = new ProviderRegistry();
7810
7860
  try {
7811
7861
  const factories = await buildProviderFactoriesFromRegistry({
@@ -7827,7 +7877,7 @@ async function startWebUI(opts = {}) {
7827
7877
  r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
7828
7878
  return r;
7829
7879
  })();
7830
- const memoryStore = new DefaultMemoryStore2({ paths: wpaths });
7880
+ const memoryStore = new DefaultMemoryStore({ paths: wpaths });
7831
7881
  if (config.features.memory) {
7832
7882
  toolRegistry.register(rememberTool(memoryStore));
7833
7883
  toolRegistry.register(forgetTool(memoryStore));
@@ -7840,6 +7890,7 @@ async function startWebUI(opts = {}) {
7840
7890
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
7841
7891
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
7842
7892
  applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
7893
+ configureExecPolicy(config.tools?.exec ?? {});
7843
7894
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7844
7895
  const mcpRegistry = new MCPRegistry({
7845
7896
  toolRegistry,
@@ -7856,7 +7907,7 @@ async function startWebUI(opts = {}) {
7856
7907
  });
7857
7908
  }
7858
7909
  }
7859
- let sessionStore = opts.services?.session ?? new DefaultSessionStore3({ dir: wpaths.projectSessions });
7910
+ let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
7860
7911
  if (!opts.services?.session) {
7861
7912
  sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
7862
7913
  if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
@@ -7944,11 +7995,11 @@ async function startWebUI(opts = {}) {
7944
7995
  });
7945
7996
  } catch {
7946
7997
  }
7947
- const tokenCounter = new DefaultTokenCounter2({
7998
+ const tokenCounter = new DefaultTokenCounter({
7948
7999
  registry: modelsRegistry,
7949
8000
  providerId: config.provider
7950
8001
  });
7951
- const modeStore = new DefaultModeStore2({ directory: wpaths.configDir });
8002
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7952
8003
  const activeMode = await modeStore.getActiveMode();
7953
8004
  let modeId = activeMode?.id ?? "default";
7954
8005
  const modePrompt = activeMode?.prompt ?? "";
@@ -7969,7 +8020,7 @@ async function startWebUI(opts = {}) {
7969
8020
  const modelCapabilitiesRef = {
7970
8021
  current: modelCapabilities
7971
8022
  };
7972
- const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
8023
+ const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7973
8024
  const skillInstaller = config.features.skills ? new SkillInstaller({
7974
8025
  manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
7975
8026
  projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
@@ -7977,7 +8028,7 @@ async function startWebUI(opts = {}) {
7977
8028
  projectHash: projectHash(projectRoot),
7978
8029
  skillLoader
7979
8030
  }) : void 0;
7980
- const systemPromptBuilder = new DefaultSystemPromptBuilder4({
8031
+ const systemPromptBuilder = new DefaultSystemPromptBuilder3({
7981
8032
  memoryStore,
7982
8033
  skillLoader,
7983
8034
  modeStore,
@@ -8267,7 +8318,7 @@ async function startWebUI(opts = {}) {
8267
8318
  projectRoot,
8268
8319
  logger
8269
8320
  });
8270
- const compactor = createStrategyCompactor2({
8321
+ const compactor = createStrategyCompactor({
8271
8322
  strategy: config.context?.strategy,
8272
8323
  preserveK: config.context?.preserveK ?? 10,
8273
8324
  eliseThreshold: config.context?.eliseThreshold ?? 2e3,
@@ -8343,9 +8394,9 @@ async function startWebUI(opts = {}) {
8343
8394
  maxContext: newMaxContext
8344
8395
  });
8345
8396
  }
8346
- const secretScrubber = container.resolve(TOKENS2.SecretScrubber);
8347
- const renderer = container.has(TOKENS2.Renderer) ? container.resolve(TOKENS2.Renderer) : void 0;
8348
- const permissionPolicy = container.resolve(TOKENS2.PermissionPolicy);
8397
+ const secretScrubber = container.resolve(TOKENS.SecretScrubber);
8398
+ const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
8399
+ const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
8349
8400
  const toolExecutor = new ToolExecutor(toolRegistry, {
8350
8401
  permissionPolicy,
8351
8402
  secretScrubber,
@@ -8388,7 +8439,7 @@ async function startWebUI(opts = {}) {
8388
8439
  }),
8389
8440
  events
8390
8441
  );
8391
- container.bind(TOKENS2.BrainArbiter, () => brain);
8442
+ container.bind(TOKENS.BrainArbiter, () => brain);
8392
8443
  const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
8393
8444
  const brainMonitor = new BrainMonitor({
8394
8445
  events,
@@ -8768,8 +8819,10 @@ async function startWebUI(opts = {}) {
8768
8819
  let sessionRoutes;
8769
8820
  let projectRoutes;
8770
8821
  let modeRoutes;
8822
+ let prefsRoutes;
8771
8823
  let shellGitRoutes;
8772
8824
  let mailboxRoutes;
8825
+ let mcpRoutes;
8773
8826
  let brainRoutes;
8774
8827
  let autoPhaseRoutes;
8775
8828
  let specsRoutes;
@@ -8780,8 +8833,10 @@ async function startWebUI(opts = {}) {
8780
8833
  if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
8781
8834
  if (await handleProjectRoute(ws, msg, projectRoutes)) return;
8782
8835
  if (await handleModeRoute(ws, msg, modeRoutes)) return;
8836
+ if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
8783
8837
  if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
8784
8838
  if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
8839
+ if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
8785
8840
  if (await handleBrainRoute(ws, msg, brainRoutes)) return;
8786
8841
  if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
8787
8842
  if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
@@ -8892,27 +8947,31 @@ async function startWebUI(opts = {}) {
8892
8947
  case "memory.forget":
8893
8948
  return handleMemoryForget(ws, msg, memoryStore);
8894
8949
  // ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
8895
- // backed by the live MCPRegistry constructed above. ──
8950
+ // backed by the live MCPRegistry constructed above. Routed via
8951
+ // handleMcpRoute (see mcpRoutes = { ... } below). These case arms
8952
+ // are unreachable but left as tripwires for any future regression
8953
+ // where the route chain stops claiming 'mcp.*'. If you see one
8954
+ // fire, fix the dispatch order in the handleMessage chain above.
8896
8955
  case "mcp.list":
8897
- return handleMcpList(ws, msg, globalConfigPath, mcpRegistry);
8956
+ throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
8898
8957
  case "mcp.add":
8899
- return handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry);
8900
- case "mcp.remove":
8901
- return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
8958
+ throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
8902
8959
  case "mcp.update":
8903
- return handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry);
8904
- case "mcp.wake":
8905
- return handleMcpWake(ws, msg, globalConfigPath, mcpRegistry);
8906
- case "mcp.sleep":
8907
- return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
8908
- case "mcp.discover":
8909
- return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
8960
+ throw new Error("handleMcpRoute did not claim mcp.update \u2014 check chain order");
8961
+ case "mcp.remove":
8962
+ throw new Error("handleMcpRoute did not claim mcp.remove \u2014 check chain order");
8910
8963
  case "mcp.enable":
8911
- return handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry);
8964
+ throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
8912
8965
  case "mcp.disable":
8913
- return handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry);
8966
+ throw new Error("handleMcpRoute did not claim mcp.disable \u2014 check chain order");
8967
+ case "mcp.sleep":
8968
+ throw new Error("handleMcpRoute did not claim mcp.sleep \u2014 check chain order");
8969
+ case "mcp.wake":
8970
+ throw new Error("handleMcpRoute did not claim mcp.wake \u2014 check chain order");
8914
8971
  case "mcp.restart":
8915
- return handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry);
8972
+ throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
8973
+ case "mcp.discover":
8974
+ throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
8916
8975
  // Skills — full request→response cycle lives in skills-handlers.ts
8917
8976
  // (shared with the CLI's embedded server). skillsCtx is the closed-over
8918
8977
  // loader/installer/projectRoot the handlers need.
@@ -9060,53 +9119,11 @@ async function startWebUI(opts = {}) {
9060
9119
  break;
9061
9120
  }
9062
9121
  case "prefs.update": {
9063
- const parsed = validatePrefsUpdatePayload(msg.payload);
9064
- if (!parsed.ok) {
9065
- sendResult2(ws, false, parsed.message);
9066
- break;
9067
- }
9068
- const payload = parsed.value.prefs;
9069
- for (const [key, val] of Object.entries(payload)) {
9070
- context.meta[key] = val;
9071
- }
9072
- void persistPrefsToConfig(payload);
9073
- if (typeof payload["yolo"] === "boolean") {
9074
- permissionPolicy.setYolo?.(payload["yolo"]);
9075
- }
9076
- if (typeof payload["featureMcp"] === "boolean")
9077
- config.features.mcp = payload["featureMcp"];
9078
- if (typeof payload["featurePlugins"] === "boolean")
9079
- config.features.plugins = payload["featurePlugins"];
9080
- if (typeof payload["featureMemory"] === "boolean")
9081
- config.features.memory = payload["featureMemory"];
9082
- if (typeof payload["featureSkills"] === "boolean")
9083
- config.features.skills = payload["featureSkills"];
9084
- if (typeof payload["featureModelsRegistry"] === "boolean")
9085
- config.features.modelsRegistry = payload["featureModelsRegistry"];
9086
- if (Array.isArray(payload["fallbackModels"]))
9087
- config.fallbackModels = payload["fallbackModels"];
9088
- if (typeof payload["fallbackAuto"] === "boolean")
9089
- config.fallbackAuto = payload["fallbackAuto"];
9090
- if (typeof payload["contextAutoCompact"] === "boolean") {
9091
- if (payload["contextAutoCompact"] && autoCompactor) {
9092
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9093
- pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
9094
- } else {
9095
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9096
- }
9097
- }
9098
- if (typeof payload["logLevel"] === "string") {
9099
- const valid = ["debug", "info", "warn", "error"];
9100
- if (valid.includes(payload["logLevel"])) {
9101
- logger.level = payload["logLevel"];
9102
- }
9103
- }
9104
- broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
9105
- break;
9122
+ void ws;
9123
+ throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
9106
9124
  }
9107
9125
  case "prefs.get": {
9108
- send(ws, { type: "prefs.updated", payload: prefSnapshot() });
9109
- break;
9126
+ throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
9110
9127
  }
9111
9128
  default:
9112
9129
  send(ws, {
@@ -9327,6 +9344,55 @@ async function startWebUI(opts = {}) {
9327
9344
  },
9328
9345
  sessionStartPayload
9329
9346
  });
9347
+ prefsRoutes = {
9348
+ getPrefs: async (ws) => {
9349
+ send(ws, { type: "prefs.updated", payload: prefSnapshot() });
9350
+ },
9351
+ updatePrefs: async (ws, msgPayload) => {
9352
+ const parsed = validatePrefsUpdatePayload(msgPayload);
9353
+ if (!parsed.ok) {
9354
+ sendResult2(ws, false, parsed.message);
9355
+ return;
9356
+ }
9357
+ const payload = parsed.value.prefs;
9358
+ for (const [key, val] of Object.entries(payload)) {
9359
+ context.meta[key] = val;
9360
+ }
9361
+ void persistPrefsToConfig(payload);
9362
+ if (typeof payload["yolo"] === "boolean") {
9363
+ permissionPolicy.setYolo?.(payload["yolo"]);
9364
+ }
9365
+ if (typeof payload["featureMcp"] === "boolean")
9366
+ config.features.mcp = payload["featureMcp"];
9367
+ if (typeof payload["featurePlugins"] === "boolean")
9368
+ config.features.plugins = payload["featurePlugins"];
9369
+ if (typeof payload["featureMemory"] === "boolean")
9370
+ config.features.memory = payload["featureMemory"];
9371
+ if (typeof payload["featureSkills"] === "boolean")
9372
+ config.features.skills = payload["featureSkills"];
9373
+ if (typeof payload["featureModelsRegistry"] === "boolean")
9374
+ config.features.modelsRegistry = payload["featureModelsRegistry"];
9375
+ if (Array.isArray(payload["fallbackModels"]))
9376
+ config.fallbackModels = payload["fallbackModels"];
9377
+ if (typeof payload["fallbackAuto"] === "boolean")
9378
+ config.fallbackAuto = payload["fallbackAuto"];
9379
+ if (typeof payload["contextAutoCompact"] === "boolean") {
9380
+ if (payload["contextAutoCompact"] && autoCompactor) {
9381
+ pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9382
+ pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
9383
+ } else {
9384
+ pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9385
+ }
9386
+ }
9387
+ if (typeof payload["logLevel"] === "string") {
9388
+ const valid = ["debug", "info", "warn", "error"];
9389
+ if (valid.includes(payload["logLevel"])) {
9390
+ logger.level = payload["logLevel"];
9391
+ }
9392
+ }
9393
+ broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
9394
+ }
9395
+ };
9330
9396
  shellGitRoutes = {
9331
9397
  gitInfo: async (ws) => {
9332
9398
  await handleGitInfo(ws, projectRoot);
@@ -9379,6 +9445,18 @@ async function startWebUI(opts = {}) {
9379
9445
  return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
9380
9446
  }
9381
9447
  };
9448
+ mcpRoutes = {
9449
+ list: (ws, msg) => handleMcpList(ws, msg, globalConfigPath, mcpRegistry),
9450
+ add: (ws, msg) => handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry),
9451
+ update: (ws, msg) => handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry),
9452
+ remove: (ws, msg) => handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry),
9453
+ enable: (ws, msg) => handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry),
9454
+ disable: (ws, msg) => handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry),
9455
+ sleep: (ws, msg) => handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry),
9456
+ wake: (ws, msg) => handleMcpWake(ws, msg, globalConfigPath, mcpRegistry),
9457
+ restart: (ws, msg) => handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry),
9458
+ discover: (ws, msg) => handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry)
9459
+ };
9382
9460
  brainRoutes = {
9383
9461
  status: (ws) => {
9384
9462
  send(ws, {