@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.
@@ -897,7 +897,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
897
897
  return;
898
898
  }
899
899
  try {
900
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
900
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
901
901
  const registry = new SessionRegistry(globalRoot);
902
902
  const entry = await registry.get(sessionId);
903
903
  if (!entry) {
@@ -906,7 +906,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
906
906
  return;
907
907
  }
908
908
  const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
909
- const store = new DefaultSessionStore4({ dir: paths.projectSessions });
909
+ const store = new DefaultSessionStore3({ dir: paths.projectSessions });
910
910
  const reader = new DefaultSessionReader2({ store });
911
911
  const rawEntries = [];
912
912
  for await (const ev of reader.replay(sessionId)) {
@@ -1592,8 +1592,8 @@ function isInside(root, target) {
1592
1592
  }
1593
1593
 
1594
1594
  // src/server/file-handlers.ts
1595
- import * as fs3 from "fs/promises";
1596
- import * as path3 from "path";
1595
+ import * as fs4 from "fs/promises";
1596
+ import * as path4 from "path";
1597
1597
  import { atomicWrite } from "@wrongstack/core";
1598
1598
 
1599
1599
  // src/server/file-picker.ts
@@ -1644,6 +1644,34 @@ function rankFiles(paths, query, limit) {
1644
1644
  return scored.slice(0, limit).map((s) => s.path);
1645
1645
  }
1646
1646
 
1647
+ // src/server/path-containment.ts
1648
+ import * as fs3 from "fs/promises";
1649
+ import * as path3 from "path";
1650
+ function isPathInside(root, target) {
1651
+ const relative3 = path3.relative(root, target);
1652
+ return relative3 === "" || !relative3.startsWith("..") && !path3.isAbsolute(relative3);
1653
+ }
1654
+ async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
1655
+ const resolved = path3.resolve(projectRoot, inputPath);
1656
+ let stat3;
1657
+ try {
1658
+ stat3 = await fs3.stat(resolved);
1659
+ } catch {
1660
+ throw new Error(`Directory not found or not accessible: ${resolved}`);
1661
+ }
1662
+ if (!stat3.isDirectory()) {
1663
+ throw new Error(`Directory not found or not accessible: ${resolved}`);
1664
+ }
1665
+ const [realProjectRoot, realResolved] = await Promise.all([
1666
+ fs3.realpath(projectRoot),
1667
+ fs3.realpath(resolved)
1668
+ ]);
1669
+ if (!isPathInside(realProjectRoot, realResolved)) {
1670
+ throw new Error(`Path must stay inside the project root: ${projectRoot}`);
1671
+ }
1672
+ return resolved;
1673
+ }
1674
+
1647
1675
  // src/server/ws-utils.ts
1648
1676
  import { randomBytes } from "crypto";
1649
1677
  import { WebSocket } from "ws";
@@ -1674,23 +1702,73 @@ function generateAuthToken() {
1674
1702
  }
1675
1703
 
1676
1704
  // src/server/file-handlers.ts
1705
+ async function resolveFileInsideProject(projectRoot, filePath) {
1706
+ const resolved = path4.resolve(projectRoot, filePath);
1707
+ if (!isPathInside(projectRoot, resolved)) {
1708
+ throw new Error("Path outside project root");
1709
+ }
1710
+ const { parent, base } = splitParentAndBase(resolved);
1711
+ const realProjectRoot = await fs4.realpath(projectRoot);
1712
+ const realParent = await realpathAllowMissing(parent);
1713
+ const realFull = path4.join(realParent, base);
1714
+ if (!isPathInside(realProjectRoot, realFull)) {
1715
+ throw new Error("Path outside project root");
1716
+ }
1717
+ return realFull;
1718
+ }
1719
+ function splitParentAndBase(p) {
1720
+ const base = path4.basename(p);
1721
+ const parent = path4.dirname(p);
1722
+ return { parent, base };
1723
+ }
1724
+ async function realpathAllowMissing(p) {
1725
+ try {
1726
+ return await fs4.realpath(p);
1727
+ } catch (err) {
1728
+ if (err.code !== "ENOENT") throw err;
1729
+ }
1730
+ const segments = [];
1731
+ let cursor = p;
1732
+ while (true) {
1733
+ const parent = path4.dirname(cursor);
1734
+ if (parent === cursor) {
1735
+ throw new Error("Path outside project root");
1736
+ }
1737
+ segments.unshift(path4.basename(cursor));
1738
+ try {
1739
+ const realParent = await fs4.realpath(parent);
1740
+ return path4.join(realParent, ...segments);
1741
+ } catch (err) {
1742
+ if (err.code !== "ENOENT") throw err;
1743
+ cursor = parent;
1744
+ }
1745
+ }
1746
+ }
1677
1747
  async function handleFilesTree(ws, msg, projectRoot) {
1678
1748
  const payload = msg.payload;
1679
1749
  const rawPath = payload?.path?.trim();
1680
- const treeRoot = rawPath && rawPath !== "." ? path3.resolve(projectRoot, rawPath) : projectRoot;
1681
- if (!treeRoot.startsWith(projectRoot + path3.sep) && treeRoot !== projectRoot) {
1750
+ let treeRoot;
1751
+ let realProjectRoot;
1752
+ try {
1753
+ if (rawPath && rawPath !== ".") {
1754
+ treeRoot = await resolveWorkingDirInsideProject(projectRoot, rawPath);
1755
+ } else {
1756
+ treeRoot = projectRoot;
1757
+ }
1758
+ realProjectRoot = await fs4.realpath(projectRoot);
1759
+ } catch {
1682
1760
  send(ws, {
1683
1761
  type: "files.tree",
1684
1762
  payload: { root: projectRoot, tree: [], error: "Path outside project root" }
1685
1763
  });
1686
1764
  return;
1687
1765
  }
1688
- const pathPrefix = treeRoot === projectRoot ? "" : (path3.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1766
+ const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1689
1767
  async function buildTree(dir, rel, depth) {
1690
1768
  if (depth > 10) return [];
1691
1769
  let entries = [];
1692
1770
  try {
1693
- entries = await fs3.readdir(dir, { withFileTypes: true });
1771
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1694
1772
  } catch {
1695
1773
  return [];
1696
1774
  }
@@ -1702,11 +1780,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
1702
1780
  for (const e of entries) {
1703
1781
  if (isHiddenEntry(e.name)) continue;
1704
1782
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1705
- const childAbs = path3.join(dir, e.name);
1783
+ const childAbs = path4.join(dir, e.name);
1706
1784
  const childPath = pathPrefix + childRel;
1707
1785
  if (e.isDirectory()) {
1708
1786
  if (SKIP_DIRS.has(e.name)) continue;
1709
- const children = await buildTree(childAbs, childRel, depth + 1);
1787
+ let realChild;
1788
+ try {
1789
+ realChild = await fs4.realpath(childAbs);
1790
+ } catch {
1791
+ continue;
1792
+ }
1793
+ if (!isPathInside(realProjectRoot, realChild)) {
1794
+ continue;
1795
+ }
1796
+ const children = await buildTree(realChild, childRel, depth + 1);
1710
1797
  nodes.push({ name: e.name, path: childPath, type: "directory", children });
1711
1798
  } else if (e.isFile()) {
1712
1799
  nodes.push({ name: e.name, path: childPath, type: "file" });
@@ -1716,10 +1803,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
1716
1803
  }
1717
1804
  try {
1718
1805
  const tree = await buildTree(treeRoot, "", 0);
1719
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1806
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1720
1807
  send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
1721
1808
  } catch (err) {
1722
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1809
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1723
1810
  send(ws, {
1724
1811
  type: "files.tree",
1725
1812
  payload: { root: rootLabel, tree: [], error: errMessage(err) }
@@ -1728,13 +1815,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
1728
1815
  }
1729
1816
  async function handleFilesRead(ws, msg, projectRoot) {
1730
1817
  const { filePath } = msg.payload;
1731
- const resolved = path3.resolve(projectRoot, filePath);
1732
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1818
+ let realResolved;
1819
+ try {
1820
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1821
+ } catch {
1733
1822
  send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
1734
1823
  return;
1735
1824
  }
1736
1825
  try {
1737
- const content = await fs3.readFile(resolved, "utf8");
1826
+ const content = await fs4.readFile(realResolved, "utf8");
1738
1827
  send(ws, { type: "files.read", payload: { filePath, content } });
1739
1828
  } catch (err) {
1740
1829
  send(ws, {
@@ -1745,16 +1834,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
1745
1834
  }
1746
1835
  async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1747
1836
  const { filePath, content } = msg.payload;
1748
- const resolved = path3.resolve(projectRoot, filePath);
1749
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1837
+ let realResolved;
1838
+ try {
1839
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1840
+ } catch {
1750
1841
  send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
1751
1842
  return;
1752
1843
  }
1753
1844
  try {
1754
- await atomicWrite(resolved, content);
1845
+ await atomicWrite(realResolved, content);
1755
1846
  send(ws, { type: "files.written", payload: { filePath, success: true } });
1756
1847
  if (opts.onWritten) {
1757
- void Promise.resolve(opts.onWritten(resolved)).catch(() => void 0);
1848
+ void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
1758
1849
  }
1759
1850
  } catch (err) {
1760
1851
  send(ws, {
@@ -1766,8 +1857,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1766
1857
  async function handleFilesList(ws, msg, projectRoot) {
1767
1858
  const payload = msg.payload ?? {};
1768
1859
  const limit = payload.limit ?? 50;
1769
- const listRoot = payload.path ? path3.resolve(projectRoot, payload.path) : projectRoot;
1770
- if (!listRoot.startsWith(projectRoot + path3.sep) && listRoot !== projectRoot) {
1860
+ let listRoot;
1861
+ let realProjectRoot;
1862
+ try {
1863
+ if (payload.path) {
1864
+ listRoot = await resolveWorkingDirInsideProject(projectRoot, payload.path);
1865
+ } else {
1866
+ listRoot = projectRoot;
1867
+ }
1868
+ realProjectRoot = await fs4.realpath(projectRoot);
1869
+ } catch {
1771
1870
  send(ws, { type: "files.list", payload: { files: [] } });
1772
1871
  return;
1773
1872
  }
@@ -1776,7 +1875,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1776
1875
  if (depth > 8 || results.length >= 600) return;
1777
1876
  let entries = [];
1778
1877
  try {
1779
- entries = await fs3.readdir(dir, { withFileTypes: true });
1878
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1780
1879
  } catch {
1781
1880
  return;
1782
1881
  }
@@ -1786,7 +1885,16 @@ async function handleFilesList(ws, msg, projectRoot) {
1786
1885
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1787
1886
  if (e.isDirectory()) {
1788
1887
  if (SKIP_DIRS.has(e.name)) continue;
1789
- await walk(path3.join(dir, e.name), childRel, depth + 1);
1888
+ let realChild;
1889
+ try {
1890
+ realChild = await fs4.realpath(path4.join(dir, e.name));
1891
+ } catch {
1892
+ continue;
1893
+ }
1894
+ if (!isPathInside(realProjectRoot, realChild)) {
1895
+ continue;
1896
+ }
1897
+ await walk(realChild, childRel, depth + 1);
1790
1898
  } else if (e.isFile()) {
1791
1899
  results.push(childRel);
1792
1900
  }
@@ -1800,7 +1908,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1800
1908
  }
1801
1909
 
1802
1910
  // src/server/completion-handlers.ts
1803
- import * as path4 from "path";
1911
+ import * as path5 from "path";
1804
1912
  import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
1805
1913
  var MAX_PREFIX_CHARS = 12e3;
1806
1914
  var MAX_SUFFIX_CHARS = 4e3;
@@ -1875,8 +1983,8 @@ async function handleCompletionRequest(ws, msg, opts) {
1875
1983
  return;
1876
1984
  }
1877
1985
  const payload = parsed.payload;
1878
- const projectRoot = path4.resolve(opts.projectRoot);
1879
- const resolved = path4.resolve(projectRoot, payload.filePath);
1986
+ const projectRoot = path5.resolve(opts.projectRoot);
1987
+ const resolved = path5.resolve(projectRoot, payload.filePath);
1880
1988
  if (!isInside2(projectRoot, resolved)) {
1881
1989
  send(ws, {
1882
1990
  type: "completion.result",
@@ -2275,7 +2383,7 @@ function buildSearchQuery(linePrefix, filePath) {
2275
2383
  if (memberMatch?.[1]) return memberMatch[1];
2276
2384
  const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
2277
2385
  if (token && token.length >= 2) return token;
2278
- return path4.basename(filePath, path4.extname(filePath));
2386
+ return path5.basename(filePath, path5.extname(filePath));
2279
2387
  }
2280
2388
  function currentLinePrefix(prefix) {
2281
2389
  const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
@@ -2305,7 +2413,7 @@ function head(value, max) {
2305
2413
  return value.length <= max ? value : value.slice(0, max);
2306
2414
  }
2307
2415
  function isInside2(root, target) {
2308
- return target === root || target.startsWith(root + path4.sep);
2416
+ return target === root || target.startsWith(root + path5.sep);
2309
2417
  }
2310
2418
 
2311
2419
  // src/server/memory-handlers.ts
@@ -2559,8 +2667,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
2559
2667
  }
2560
2668
 
2561
2669
  // src/server/skills-handlers.ts
2562
- import { promises as fs4 } from "fs";
2563
- import path5 from "path";
2670
+ import { promises as fs5 } from "fs";
2671
+ import path6 from "path";
2564
2672
  import JSZip from "jszip";
2565
2673
  import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
2566
2674
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
@@ -2631,19 +2739,19 @@ async function handleSkillsContent(ws, ctx, msg) {
2631
2739
  send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
2632
2740
  return;
2633
2741
  }
2634
- const body = await fs4.readFile(entry.path, "utf8");
2635
- const skillDir = path5.dirname(entry.path);
2742
+ const body = await fs5.readFile(entry.path, "utf8");
2743
+ const skillDir = path6.dirname(entry.path);
2636
2744
  let relatedFiles = [];
2637
2745
  try {
2638
- const files = await fs4.readdir(skillDir);
2639
- relatedFiles = files.filter((f) => f !== path5.basename(entry.path)).map((f) => path5.join(skillDir, f));
2746
+ const files = await fs5.readdir(skillDir);
2747
+ relatedFiles = files.filter((f) => f !== path6.basename(entry.path)).map((f) => path6.join(skillDir, f));
2640
2748
  } catch {
2641
2749
  }
2642
2750
  const nameLower = name2.toLowerCase();
2643
2751
  const refResults = await Promise.all(
2644
2752
  entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
2645
2753
  try {
2646
- const content = await fs4.readFile(e.path, "utf8");
2754
+ const content = await fs5.readFile(e.path, "utf8");
2647
2755
  return [e.name, content.toLowerCase().includes(nameLower)];
2648
2756
  } catch {
2649
2757
  return [e.name, false];
@@ -2733,14 +2841,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
2733
2841
  }
2734
2842
  const createPayload = parsed.value;
2735
2843
  try {
2736
- const targetDir = createPayload.scope === "global" ? path5.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path5.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2844
+ const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2737
2845
  try {
2738
- await fs4.access(targetDir);
2846
+ await fs5.access(targetDir);
2739
2847
  send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
2740
2848
  return;
2741
2849
  } catch {
2742
2850
  }
2743
- await fs4.mkdir(targetDir, { recursive: true });
2851
+ await fs5.mkdir(targetDir, { recursive: true });
2744
2852
  const lines = createPayload.description.trim().split("\n");
2745
2853
  const firstLine = lines[0].trim();
2746
2854
  const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
@@ -2788,13 +2896,13 @@ ${trigger}
2788
2896
  "- `bug-hunter` \u2014 for systematic bug detection patterns",
2789
2897
  "- `output-standards` \u2014 for standardized `<next_steps>` formatting"
2790
2898
  ].join("\n");
2791
- await atomicWrite2(path5.join(targetDir, "SKILL.md"), skillContent);
2899
+ await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
2792
2900
  send(ws, {
2793
2901
  type: "skills.created",
2794
2902
  payload: {
2795
2903
  success: true,
2796
2904
  error: null,
2797
- skill: { name: createPayload.name.trim(), path: path5.join(targetDir, "SKILL.md"), scope: createPayload.scope }
2905
+ skill: { name: createPayload.name.trim(), path: path6.join(targetDir, "SKILL.md"), scope: createPayload.scope }
2798
2906
  }
2799
2907
  });
2800
2908
  } catch (err) {
@@ -2858,23 +2966,23 @@ import {
2858
2966
  Agent,
2859
2967
  AutoCompactionMiddleware,
2860
2968
  Context,
2861
- DefaultMemoryStore as DefaultMemoryStore2,
2862
- DefaultModeStore as DefaultModeStore2,
2969
+ DefaultMemoryStore,
2970
+ DefaultModeStore,
2863
2971
  DefaultModelsRegistry,
2864
2972
  DefaultSessionReader,
2865
- DefaultSessionStore as DefaultSessionStore3,
2866
- DefaultSkillLoader as DefaultSkillLoader2,
2867
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder4,
2868
- DefaultTokenCounter as DefaultTokenCounter2,
2973
+ DefaultSessionStore as DefaultSessionStore2,
2974
+ DefaultSkillLoader,
2975
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
2976
+ DefaultTokenCounter,
2869
2977
  AnnotationsStore,
2870
2978
  CollaborationBus,
2871
2979
  collabPauseMiddleware,
2872
2980
  collabInjectMiddleware,
2873
2981
  estimateRequestTokensCalibrated,
2874
2982
  EventBus,
2875
- createStrategyCompactor as createStrategyCompactor2,
2983
+ createStrategyCompactor,
2876
2984
  ProviderRegistry,
2877
- TOKENS as TOKENS2,
2985
+ TOKENS,
2878
2986
  ToolRegistry,
2879
2987
  atomicWrite as atomicWrite6,
2880
2988
  createDefaultPipelines,
@@ -2893,110 +3001,10 @@ import {
2893
3001
  import { ToolExecutor } from "@wrongstack/core/execution";
2894
3002
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
2895
3003
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
2896
- import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
3004
+ import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
2897
3005
  import { MCPRegistry } from "@wrongstack/mcp";
2898
3006
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
2899
-
2900
- // ../runtime/src/container.ts
2901
- import {
2902
- Container,
2903
- DefaultConfigStore,
2904
- DefaultErrorHandler,
2905
- DefaultMemoryStore,
2906
- DefaultModeStore,
2907
- DefaultPermissionPolicy,
2908
- DefaultRetryPolicy,
2909
- DefaultSecretScrubber,
2910
- DefaultSessionStore,
2911
- DefaultSkillLoader,
2912
- DefaultSystemPromptBuilder,
2913
- DefaultTokenCounter,
2914
- createStrategyCompactor,
2915
- buildRecoveryStrategies,
2916
- TOKENS
2917
- } from "@wrongstack/core";
2918
- function createDefaultContainer(opts) {
2919
- const { config, wpaths, logger, modelsRegistry } = opts;
2920
- const container = new Container();
2921
- const configStore = new DefaultConfigStore(config);
2922
- container.bind(TOKENS.ConfigStore, () => configStore);
2923
- container.bind(TOKENS.Logger, () => logger);
2924
- container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
2925
- container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
2926
- container.bind(
2927
- TOKENS.ErrorHandler,
2928
- () => new DefaultErrorHandler(
2929
- buildRecoveryStrategies({
2930
- compactor: container.resolve(TOKENS.Compactor),
2931
- modelsRegistry,
2932
- getConfig: () => configStore.get()
2933
- })
2934
- )
2935
- );
2936
- container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
2937
- container.bind(
2938
- TOKENS.TokenCounter,
2939
- () => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
2940
- );
2941
- const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
2942
- container.bind(TOKENS.ModeStore, () => modeStore);
2943
- container.bind(
2944
- TOKENS.SessionStore,
2945
- () => new DefaultSessionStore({
2946
- dir: wpaths.projectSessions,
2947
- // Scrub secrets out of persisted user/model turns (F-06). Tool output
2948
- // is already scrubbed by the executor.
2949
- secretScrubber: container.resolve(TOKENS.SecretScrubber)
2950
- })
2951
- );
2952
- const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
2953
- container.bind(TOKENS.MemoryStore, () => memoryStore);
2954
- const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
2955
- container.bind(TOKENS.SkillLoader, () => skillLoader);
2956
- if (opts.systemPrompt) {
2957
- container.bind(
2958
- TOKENS.SystemPromptBuilder,
2959
- () => new DefaultSystemPromptBuilder(opts.systemPrompt)
2960
- );
2961
- }
2962
- container.bind(
2963
- TOKENS.PermissionPolicy,
2964
- () => {
2965
- const policyOptions = {
2966
- trustFile: wpaths.projectTrust,
2967
- yolo: opts.permission?.yolo ?? false,
2968
- yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
2969
- confirmDestructive: opts.permission?.confirmDestructive ?? false
2970
- };
2971
- if (opts.permission?.promptDelegate !== void 0) {
2972
- policyOptions.promptDelegate = opts.permission.promptDelegate;
2973
- }
2974
- return new DefaultPermissionPolicy(policyOptions);
2975
- }
2976
- );
2977
- container.bind(
2978
- TOKENS.Compactor,
2979
- () => (
2980
- // Strategy comes from config.context.strategy: 'hybrid' (default, lossless
2981
- // rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
2982
- // (LLM-driven selection). The LLM strategies resolve their provider from
2983
- // ctx at compact()-time, so binding here (before context.provider exists)
2984
- // is safe. preserveK / eliseThreshold are class-level fallbacks; the active
2985
- // ContextWindowPolicy in ctx.meta normally overrides both at runtime.
2986
- // eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
2987
- // essentially every tool_result (anything > 1 token).
2988
- createStrategyCompactor({
2989
- strategy: config.context?.strategy,
2990
- preserveK: opts.compactor?.preserveK ?? 10,
2991
- eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
2992
- smart: true,
2993
- summarizerModel: config.context?.summarizerModel,
2994
- llmSelector: config.context?.llmSelector
2995
- })
2996
- )
2997
- );
2998
- return container;
2999
- }
3007
+ import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
3000
3008
 
3001
3009
  // src/server/boot.ts
3002
3010
  import {
@@ -3357,6 +3365,13 @@ Type: ${task.type}`;
3357
3365
  });
3358
3366
  const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
3359
3367
  const completedPhases = phases.filter((p) => p.status === "completed").length;
3368
+ const failedPhases = phases.filter((p) => p.status === "failed").length;
3369
+ const failedTasks = phases.reduce(
3370
+ (sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "failed").length,
3371
+ 0
3372
+ );
3373
+ const lastFailed = phases.filter((p) => p.status === "failed").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
3374
+ const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
3360
3375
  return {
3361
3376
  title: this.graph.title,
3362
3377
  phases: phaseItems,
@@ -3365,7 +3380,18 @@ Type: ${task.type}`;
3365
3380
  overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
3366
3381
  autonomous: this.graph.autonomous,
3367
3382
  totalTasks,
3368
- completedTasks
3383
+ completedTasks,
3384
+ // Structured progress + lastError consumed by the autophase store (were
3385
+ // defined client-side but never sent, so they stayed null on the board).
3386
+ progress: {
3387
+ totalPhases: phases.length,
3388
+ completed: completedPhases,
3389
+ failed: failedPhases,
3390
+ totalTasks,
3391
+ completedTasks,
3392
+ failedTasks
3393
+ },
3394
+ lastError
3369
3395
  };
3370
3396
  }
3371
3397
  sendState(client) {
@@ -3787,7 +3813,7 @@ var SddWizardWebSocketHandler = class {
3787
3813
  };
3788
3814
 
3789
3815
  // src/server/sdd-wizard-wiring.ts
3790
- import * as path6 from "path";
3816
+ import * as path7 from "path";
3791
3817
  import { spawnSync as spawnSync2 } from "child_process";
3792
3818
  import {
3793
3819
  makeCommandVerifier,
@@ -3823,7 +3849,7 @@ function buildSddWizardDeps(opts) {
3823
3849
  makeDriver: () => new SddInterviewDriver({
3824
3850
  specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
3825
3851
  graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
3826
- sessionPath: path6.join(opts.paths.projectDir, "sdd-wizard-session.json")
3852
+ sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
3827
3853
  }),
3828
3854
  runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
3829
3855
  startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
@@ -3892,9 +3918,6 @@ async function handleSddWizardRoute(_ws, msg, handlers) {
3892
3918
  return true;
3893
3919
  }
3894
3920
 
3895
- // src/server/index.ts
3896
- import { makeLightSubagentFactory } from "@wrongstack/runtime";
3897
-
3898
3921
  // src/server/collaboration-ws-handler.ts
3899
3922
  import { randomUUID } from "crypto";
3900
3923
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -4621,16 +4644,16 @@ var CollaborationWebSocketHandler = class {
4621
4644
  };
4622
4645
 
4623
4646
  // src/server/projects-manifest.ts
4624
- import * as fs5 from "fs/promises";
4625
- import * as path7 from "path";
4647
+ import * as fs6 from "fs/promises";
4648
+ import * as path8 from "path";
4626
4649
  import { projectSlug } from "@wrongstack/core";
4627
4650
  function projectsJsonPath(globalConfigPath) {
4628
- const base = path7.dirname(globalConfigPath);
4629
- return path7.join(base, "projects.json");
4651
+ const base = path8.dirname(globalConfigPath);
4652
+ return path8.join(base, "projects.json");
4630
4653
  }
4631
4654
  async function loadManifest(globalConfigPath) {
4632
4655
  try {
4633
- const raw = await fs5.readFile(projectsJsonPath(globalConfigPath), "utf8");
4656
+ const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
4634
4657
  const parsed = JSON.parse(raw);
4635
4658
  return { projects: parsed.projects ?? [] };
4636
4659
  } catch {
@@ -4639,16 +4662,16 @@ async function loadManifest(globalConfigPath) {
4639
4662
  }
4640
4663
  async function saveManifest(manifest, globalConfigPath) {
4641
4664
  const file = projectsJsonPath(globalConfigPath);
4642
- await fs5.mkdir(path7.dirname(file), { recursive: true });
4643
- await fs5.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4665
+ await fs6.mkdir(path8.dirname(file), { recursive: true });
4666
+ await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4644
4667
  }
4645
4668
  function generateProjectSlug(rootPath) {
4646
4669
  return projectSlug(rootPath);
4647
4670
  }
4648
4671
  async function ensureProjectDataDir(slug, globalConfigPath) {
4649
- const base = path7.dirname(globalConfigPath);
4650
- const dir = path7.join(base, "projects", slug);
4651
- await fs5.mkdir(dir, { recursive: true });
4672
+ const base = path8.dirname(globalConfigPath);
4673
+ const dir = path8.join(base, "projects", slug);
4674
+ await fs6.mkdir(dir, { recursive: true });
4652
4675
  return dir;
4653
4676
  }
4654
4677
 
@@ -5074,14 +5097,14 @@ function registerShutdownHandlers(res) {
5074
5097
 
5075
5098
  // src/server/instance-registry.ts
5076
5099
  import * as os from "os";
5077
- import * as path8 from "path";
5078
- import * as fs6 from "fs/promises";
5100
+ import * as path9 from "path";
5101
+ import * as fs7 from "fs/promises";
5079
5102
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
5080
5103
  function defaultBaseDir() {
5081
- return path8.join(os.homedir(), ".wrongstack");
5104
+ return path9.join(os.homedir(), ".wrongstack");
5082
5105
  }
5083
5106
  function registryPath(baseDir = defaultBaseDir()) {
5084
- return path8.join(baseDir, "webui-instances.json");
5107
+ return path9.join(baseDir, "webui-instances.json");
5085
5108
  }
5086
5109
  function isPidAlive(pid) {
5087
5110
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -5094,7 +5117,7 @@ function isPidAlive(pid) {
5094
5117
  }
5095
5118
  async function load(file) {
5096
5119
  try {
5097
- const raw = await fs6.readFile(file, "utf8");
5120
+ const raw = await fs7.readFile(file, "utf8");
5098
5121
  const parsed = JSON.parse(raw);
5099
5122
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
5100
5123
  return parsed;
@@ -5239,19 +5262,19 @@ function computeUsageCost(usage, rates) {
5239
5262
  }
5240
5263
 
5241
5264
  // src/server/provider-handlers.ts
5242
- import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core";
5265
+ import { DefaultSecretScrubber } from "@wrongstack/core";
5243
5266
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
5244
5267
 
5245
5268
  // src/server/provider-config-io.ts
5246
- import * as fs7 from "fs/promises";
5247
- import * as path9 from "path";
5269
+ import * as fs8 from "fs/promises";
5270
+ import * as path10 from "path";
5248
5271
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
5249
5272
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
5250
5273
  import { DefaultSecretVault } from "@wrongstack/core";
5251
5274
  async function loadSavedProviders(configPath, vault) {
5252
5275
  let raw;
5253
5276
  try {
5254
- raw = await fs7.readFile(configPath, "utf8");
5277
+ raw = await fs8.readFile(configPath, "utf8");
5255
5278
  } catch {
5256
5279
  return {};
5257
5280
  }
@@ -5268,7 +5291,7 @@ async function saveProviders(configPath, vault, providers) {
5268
5291
  let raw;
5269
5292
  let fileExists = true;
5270
5293
  try {
5271
- raw = await fs7.readFile(configPath, "utf8");
5294
+ raw = await fs8.readFile(configPath, "utf8");
5272
5295
  } catch (err) {
5273
5296
  if (err.code !== "ENOENT") {
5274
5297
  throw new Error(
@@ -5417,7 +5440,7 @@ function projectSavedProviders(providers) {
5417
5440
  return view;
5418
5441
  });
5419
5442
  }
5420
- var probeScrubber = new DefaultSecretScrubber2();
5443
+ var probeScrubber = new DefaultSecretScrubber();
5421
5444
  function createProviderHandlers(deps2) {
5422
5445
  const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
5423
5446
  let configWriteLock = deps2.getConfigWriteLock();
@@ -5606,7 +5629,7 @@ function createProviderHandlers(deps2) {
5606
5629
 
5607
5630
  // src/server/mode-handlers.ts
5608
5631
  import {
5609
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2
5632
+ DefaultSystemPromptBuilder
5610
5633
  } from "@wrongstack/core";
5611
5634
  function createModeHandlers(ctx) {
5612
5635
  return {
@@ -5654,7 +5677,7 @@ function createModeHandlers(ctx) {
5654
5677
  }
5655
5678
  ctx.setModeId(id);
5656
5679
  const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
5657
- const freshBuilder = new DefaultSystemPromptBuilder2({
5680
+ const freshBuilder = new DefaultSystemPromptBuilder({
5658
5681
  memoryStore: ctx.memoryStore,
5659
5682
  skillLoader: ctx.skillLoader,
5660
5683
  modeStore: ctx.modeStore,
@@ -5685,40 +5708,10 @@ function createModeHandlers(ctx) {
5685
5708
  import * as fs9 from "fs/promises";
5686
5709
  import * as path11 from "path";
5687
5710
  import {
5688
- DefaultSessionStore as DefaultSessionStore2,
5689
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
5711
+ DefaultSessionStore,
5712
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
5690
5713
  getSessionRegistry
5691
5714
  } from "@wrongstack/core";
5692
-
5693
- // src/server/path-containment.ts
5694
- import * as fs8 from "fs/promises";
5695
- import * as path10 from "path";
5696
- function isPathInside(root, target) {
5697
- const relative3 = path10.relative(root, target);
5698
- return relative3 === "" || !relative3.startsWith("..") && !path10.isAbsolute(relative3);
5699
- }
5700
- async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
5701
- const resolved = path10.resolve(projectRoot, inputPath);
5702
- let stat3;
5703
- try {
5704
- stat3 = await fs8.stat(resolved);
5705
- } catch {
5706
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5707
- }
5708
- if (!stat3.isDirectory()) {
5709
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5710
- }
5711
- const [realProjectRoot, realResolved] = await Promise.all([
5712
- fs8.realpath(projectRoot),
5713
- fs8.realpath(resolved)
5714
- ]);
5715
- if (!isPathInside(realProjectRoot, realResolved)) {
5716
- throw new Error(`Path must stay inside the project root: ${projectRoot}`);
5717
- }
5718
- return resolved;
5719
- }
5720
-
5721
- // src/server/project-handlers.ts
5722
5715
  function createProjectHandlers(ctx) {
5723
5716
  return {
5724
5717
  listProjects: async (ws) => {
@@ -5830,7 +5823,7 @@ function createProjectHandlers(ctx) {
5830
5823
  try {
5831
5824
  const modeId = ctx.getModeId();
5832
5825
  const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
5833
- const switchBuilder = new DefaultSystemPromptBuilder3({
5826
+ const switchBuilder = new DefaultSystemPromptBuilder2({
5834
5827
  memoryStore: ctx.memoryStore,
5835
5828
  skillLoader: ctx.skillLoader,
5836
5829
  modeStore: ctx.modeStore,
@@ -5854,7 +5847,7 @@ function createProjectHandlers(ctx) {
5854
5847
  "sessions"
5855
5848
  );
5856
5849
  await fs9.mkdir(newSessionsDir, { recursive: true });
5857
- const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
5850
+ const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
5858
5851
  const oldSession = ctx.getSession();
5859
5852
  const oldSessionId = oldSession.id;
5860
5853
  try {
@@ -6551,6 +6544,22 @@ async function handleModeRoute(ws, msg, handlers) {
6551
6544
  }
6552
6545
  }
6553
6546
 
6547
+ // src/server/prefs-routes.ts
6548
+ async function handlePrefsRoute(ws, msg, handlers) {
6549
+ switch (msg.type) {
6550
+ case "prefs.get": {
6551
+ await handlers.getPrefs(ws);
6552
+ return true;
6553
+ }
6554
+ case "prefs.update": {
6555
+ await handlers.updatePrefs(ws, msg.payload ?? {});
6556
+ return true;
6557
+ }
6558
+ default:
6559
+ return false;
6560
+ }
6561
+ }
6562
+
6554
6563
  // src/server/shell-git-routes.ts
6555
6564
  async function handleShellGitRoute(ws, msg, handlers) {
6556
6565
  switch (msg.type) {
@@ -6591,6 +6600,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
6591
6600
  }
6592
6601
  }
6593
6602
 
6603
+ // src/server/mcp-routes.ts
6604
+ async function handleMcpRoute(ws, msg, handlers) {
6605
+ switch (msg.type) {
6606
+ case "mcp.list":
6607
+ await handlers.list(ws, msg);
6608
+ return true;
6609
+ case "mcp.add":
6610
+ await handlers.add(ws, msg);
6611
+ return true;
6612
+ case "mcp.update":
6613
+ await handlers.update(ws, msg);
6614
+ return true;
6615
+ case "mcp.remove":
6616
+ await handlers.remove(ws, msg);
6617
+ return true;
6618
+ case "mcp.enable":
6619
+ await handlers.enable(ws, msg);
6620
+ return true;
6621
+ case "mcp.disable":
6622
+ await handlers.disable(ws, msg);
6623
+ return true;
6624
+ case "mcp.sleep":
6625
+ await handlers.sleep(ws, msg);
6626
+ return true;
6627
+ case "mcp.wake":
6628
+ await handlers.wake(ws, msg);
6629
+ return true;
6630
+ case "mcp.restart":
6631
+ await handlers.restart(ws, msg);
6632
+ return true;
6633
+ case "mcp.discover":
6634
+ await handlers.discover(ws, msg);
6635
+ return true;
6636
+ default:
6637
+ return false;
6638
+ }
6639
+ }
6640
+
6594
6641
  // src/server/brain-routes.ts
6595
6642
  async function handleBrainRoute(ws, msg, handlers) {
6596
6643
  switch (msg.type) {
@@ -7062,11 +7109,13 @@ function setupEvents(deps2) {
7062
7109
  events.on("provider.response", (e) => {
7063
7110
  if (e.usage?.input != null) {
7064
7111
  const maxCtx = context.provider.capabilities.maxContext;
7065
- const pct = maxCtx > 0 ? e.usage.input / maxCtx : 0;
7112
+ const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
7113
+ const load2 = Math.max(0, Math.min(1, rawLoad));
7066
7114
  const costUsd = context.tokenCounter.estimateCost().total;
7067
7115
  forwardSubagent("ctx_pct", {
7068
7116
  subagentId: "leader",
7069
- load: pct,
7117
+ load: load2,
7118
+ rawLoad,
7070
7119
  tokens: e.usage.input,
7071
7120
  maxContext: maxCtx,
7072
7121
  costUsd
@@ -7717,6 +7766,7 @@ async function handleGoalGet(projectRoot, broadcast2) {
7717
7766
 
7718
7767
  // src/server/index.ts
7719
7768
  async function startWebUI(opts = {}) {
7769
+ ensureSessionShell();
7720
7770
  const requestedWsPort = opts.wsPort ?? 3457;
7721
7771
  const wsHost = opts.wsHost ?? "127.0.0.1";
7722
7772
  const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
@@ -7798,7 +7848,7 @@ async function startWebUI(opts = {}) {
7798
7848
  ttlSeconds: 24 * 3600
7799
7849
  });
7800
7850
  const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
7801
- const configStore = opts.services?.configStore ?? container.resolve(TOKENS2.ConfigStore);
7851
+ const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
7802
7852
  const providerRegistry = new ProviderRegistry();
7803
7853
  try {
7804
7854
  const factories = await buildProviderFactoriesFromRegistry({
@@ -7820,7 +7870,7 @@ async function startWebUI(opts = {}) {
7820
7870
  r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
7821
7871
  return r;
7822
7872
  })();
7823
- const memoryStore = new DefaultMemoryStore2({ paths: wpaths });
7873
+ const memoryStore = new DefaultMemoryStore({ paths: wpaths });
7824
7874
  if (config.features.memory) {
7825
7875
  toolRegistry.register(rememberTool(memoryStore));
7826
7876
  toolRegistry.register(forgetTool(memoryStore));
@@ -7833,6 +7883,7 @@ async function startWebUI(opts = {}) {
7833
7883
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
7834
7884
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
7835
7885
  applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
7886
+ configureExecPolicy(config.tools?.exec ?? {});
7836
7887
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7837
7888
  const mcpRegistry = new MCPRegistry({
7838
7889
  toolRegistry,
@@ -7849,7 +7900,7 @@ async function startWebUI(opts = {}) {
7849
7900
  });
7850
7901
  }
7851
7902
  }
7852
- let sessionStore = opts.services?.session ?? new DefaultSessionStore3({ dir: wpaths.projectSessions });
7903
+ let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
7853
7904
  if (!opts.services?.session) {
7854
7905
  sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
7855
7906
  if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
@@ -7937,11 +7988,11 @@ async function startWebUI(opts = {}) {
7937
7988
  });
7938
7989
  } catch {
7939
7990
  }
7940
- const tokenCounter = new DefaultTokenCounter2({
7991
+ const tokenCounter = new DefaultTokenCounter({
7941
7992
  registry: modelsRegistry,
7942
7993
  providerId: config.provider
7943
7994
  });
7944
- const modeStore = new DefaultModeStore2({ directory: wpaths.configDir });
7995
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7945
7996
  const activeMode = await modeStore.getActiveMode();
7946
7997
  let modeId = activeMode?.id ?? "default";
7947
7998
  const modePrompt = activeMode?.prompt ?? "";
@@ -7962,7 +8013,7 @@ async function startWebUI(opts = {}) {
7962
8013
  const modelCapabilitiesRef = {
7963
8014
  current: modelCapabilities
7964
8015
  };
7965
- const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
8016
+ const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7966
8017
  const skillInstaller = config.features.skills ? new SkillInstaller({
7967
8018
  manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
7968
8019
  projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
@@ -7970,7 +8021,7 @@ async function startWebUI(opts = {}) {
7970
8021
  projectHash: projectHash(projectRoot),
7971
8022
  skillLoader
7972
8023
  }) : void 0;
7973
- const systemPromptBuilder = new DefaultSystemPromptBuilder4({
8024
+ const systemPromptBuilder = new DefaultSystemPromptBuilder3({
7974
8025
  memoryStore,
7975
8026
  skillLoader,
7976
8027
  modeStore,
@@ -8260,7 +8311,7 @@ async function startWebUI(opts = {}) {
8260
8311
  projectRoot,
8261
8312
  logger
8262
8313
  });
8263
- const compactor = createStrategyCompactor2({
8314
+ const compactor = createStrategyCompactor({
8264
8315
  strategy: config.context?.strategy,
8265
8316
  preserveK: config.context?.preserveK ?? 10,
8266
8317
  eliseThreshold: config.context?.eliseThreshold ?? 2e3,
@@ -8336,9 +8387,9 @@ async function startWebUI(opts = {}) {
8336
8387
  maxContext: newMaxContext
8337
8388
  });
8338
8389
  }
8339
- const secretScrubber = container.resolve(TOKENS2.SecretScrubber);
8340
- const renderer = container.has(TOKENS2.Renderer) ? container.resolve(TOKENS2.Renderer) : void 0;
8341
- const permissionPolicy = container.resolve(TOKENS2.PermissionPolicy);
8390
+ const secretScrubber = container.resolve(TOKENS.SecretScrubber);
8391
+ const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
8392
+ const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
8342
8393
  const toolExecutor = new ToolExecutor(toolRegistry, {
8343
8394
  permissionPolicy,
8344
8395
  secretScrubber,
@@ -8381,7 +8432,7 @@ async function startWebUI(opts = {}) {
8381
8432
  }),
8382
8433
  events
8383
8434
  );
8384
- container.bind(TOKENS2.BrainArbiter, () => brain);
8435
+ container.bind(TOKENS.BrainArbiter, () => brain);
8385
8436
  const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
8386
8437
  const brainMonitor = new BrainMonitor({
8387
8438
  events,
@@ -8761,8 +8812,10 @@ async function startWebUI(opts = {}) {
8761
8812
  let sessionRoutes;
8762
8813
  let projectRoutes;
8763
8814
  let modeRoutes;
8815
+ let prefsRoutes;
8764
8816
  let shellGitRoutes;
8765
8817
  let mailboxRoutes;
8818
+ let mcpRoutes;
8766
8819
  let brainRoutes;
8767
8820
  let autoPhaseRoutes;
8768
8821
  let specsRoutes;
@@ -8773,8 +8826,10 @@ async function startWebUI(opts = {}) {
8773
8826
  if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
8774
8827
  if (await handleProjectRoute(ws, msg, projectRoutes)) return;
8775
8828
  if (await handleModeRoute(ws, msg, modeRoutes)) return;
8829
+ if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
8776
8830
  if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
8777
8831
  if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
8832
+ if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
8778
8833
  if (await handleBrainRoute(ws, msg, brainRoutes)) return;
8779
8834
  if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
8780
8835
  if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
@@ -8885,27 +8940,31 @@ async function startWebUI(opts = {}) {
8885
8940
  case "memory.forget":
8886
8941
  return handleMemoryForget(ws, msg, memoryStore);
8887
8942
  // ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
8888
- // backed by the live MCPRegistry constructed above. ──
8943
+ // backed by the live MCPRegistry constructed above. Routed via
8944
+ // handleMcpRoute (see mcpRoutes = { ... } below). These case arms
8945
+ // are unreachable but left as tripwires for any future regression
8946
+ // where the route chain stops claiming 'mcp.*'. If you see one
8947
+ // fire, fix the dispatch order in the handleMessage chain above.
8889
8948
  case "mcp.list":
8890
- return handleMcpList(ws, msg, globalConfigPath, mcpRegistry);
8949
+ throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
8891
8950
  case "mcp.add":
8892
- return handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry);
8893
- case "mcp.remove":
8894
- return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
8951
+ throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
8895
8952
  case "mcp.update":
8896
- return handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry);
8897
- case "mcp.wake":
8898
- return handleMcpWake(ws, msg, globalConfigPath, mcpRegistry);
8899
- case "mcp.sleep":
8900
- return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
8901
- case "mcp.discover":
8902
- return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
8953
+ throw new Error("handleMcpRoute did not claim mcp.update \u2014 check chain order");
8954
+ case "mcp.remove":
8955
+ throw new Error("handleMcpRoute did not claim mcp.remove \u2014 check chain order");
8903
8956
  case "mcp.enable":
8904
- return handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry);
8957
+ throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
8905
8958
  case "mcp.disable":
8906
- return handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry);
8959
+ throw new Error("handleMcpRoute did not claim mcp.disable \u2014 check chain order");
8960
+ case "mcp.sleep":
8961
+ throw new Error("handleMcpRoute did not claim mcp.sleep \u2014 check chain order");
8962
+ case "mcp.wake":
8963
+ throw new Error("handleMcpRoute did not claim mcp.wake \u2014 check chain order");
8907
8964
  case "mcp.restart":
8908
- return handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry);
8965
+ throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
8966
+ case "mcp.discover":
8967
+ throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
8909
8968
  // Skills — full request→response cycle lives in skills-handlers.ts
8910
8969
  // (shared with the CLI's embedded server). skillsCtx is the closed-over
8911
8970
  // loader/installer/projectRoot the handlers need.
@@ -9053,53 +9112,11 @@ async function startWebUI(opts = {}) {
9053
9112
  break;
9054
9113
  }
9055
9114
  case "prefs.update": {
9056
- const parsed = validatePrefsUpdatePayload(msg.payload);
9057
- if (!parsed.ok) {
9058
- sendResult2(ws, false, parsed.message);
9059
- break;
9060
- }
9061
- const payload = parsed.value.prefs;
9062
- for (const [key, val] of Object.entries(payload)) {
9063
- context.meta[key] = val;
9064
- }
9065
- void persistPrefsToConfig(payload);
9066
- if (typeof payload["yolo"] === "boolean") {
9067
- permissionPolicy.setYolo?.(payload["yolo"]);
9068
- }
9069
- if (typeof payload["featureMcp"] === "boolean")
9070
- config.features.mcp = payload["featureMcp"];
9071
- if (typeof payload["featurePlugins"] === "boolean")
9072
- config.features.plugins = payload["featurePlugins"];
9073
- if (typeof payload["featureMemory"] === "boolean")
9074
- config.features.memory = payload["featureMemory"];
9075
- if (typeof payload["featureSkills"] === "boolean")
9076
- config.features.skills = payload["featureSkills"];
9077
- if (typeof payload["featureModelsRegistry"] === "boolean")
9078
- config.features.modelsRegistry = payload["featureModelsRegistry"];
9079
- if (Array.isArray(payload["fallbackModels"]))
9080
- config.fallbackModels = payload["fallbackModels"];
9081
- if (typeof payload["fallbackAuto"] === "boolean")
9082
- config.fallbackAuto = payload["fallbackAuto"];
9083
- if (typeof payload["contextAutoCompact"] === "boolean") {
9084
- if (payload["contextAutoCompact"] && autoCompactor) {
9085
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9086
- pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
9087
- } else {
9088
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9089
- }
9090
- }
9091
- if (typeof payload["logLevel"] === "string") {
9092
- const valid = ["debug", "info", "warn", "error"];
9093
- if (valid.includes(payload["logLevel"])) {
9094
- logger.level = payload["logLevel"];
9095
- }
9096
- }
9097
- broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
9098
- break;
9115
+ void ws;
9116
+ throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
9099
9117
  }
9100
9118
  case "prefs.get": {
9101
- send(ws, { type: "prefs.updated", payload: prefSnapshot() });
9102
- break;
9119
+ throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
9103
9120
  }
9104
9121
  default:
9105
9122
  send(ws, {
@@ -9320,6 +9337,55 @@ async function startWebUI(opts = {}) {
9320
9337
  },
9321
9338
  sessionStartPayload
9322
9339
  });
9340
+ prefsRoutes = {
9341
+ getPrefs: async (ws) => {
9342
+ send(ws, { type: "prefs.updated", payload: prefSnapshot() });
9343
+ },
9344
+ updatePrefs: async (ws, msgPayload) => {
9345
+ const parsed = validatePrefsUpdatePayload(msgPayload);
9346
+ if (!parsed.ok) {
9347
+ sendResult2(ws, false, parsed.message);
9348
+ return;
9349
+ }
9350
+ const payload = parsed.value.prefs;
9351
+ for (const [key, val] of Object.entries(payload)) {
9352
+ context.meta[key] = val;
9353
+ }
9354
+ void persistPrefsToConfig(payload);
9355
+ if (typeof payload["yolo"] === "boolean") {
9356
+ permissionPolicy.setYolo?.(payload["yolo"]);
9357
+ }
9358
+ if (typeof payload["featureMcp"] === "boolean")
9359
+ config.features.mcp = payload["featureMcp"];
9360
+ if (typeof payload["featurePlugins"] === "boolean")
9361
+ config.features.plugins = payload["featurePlugins"];
9362
+ if (typeof payload["featureMemory"] === "boolean")
9363
+ config.features.memory = payload["featureMemory"];
9364
+ if (typeof payload["featureSkills"] === "boolean")
9365
+ config.features.skills = payload["featureSkills"];
9366
+ if (typeof payload["featureModelsRegistry"] === "boolean")
9367
+ config.features.modelsRegistry = payload["featureModelsRegistry"];
9368
+ if (Array.isArray(payload["fallbackModels"]))
9369
+ config.fallbackModels = payload["fallbackModels"];
9370
+ if (typeof payload["fallbackAuto"] === "boolean")
9371
+ config.fallbackAuto = payload["fallbackAuto"];
9372
+ if (typeof payload["contextAutoCompact"] === "boolean") {
9373
+ if (payload["contextAutoCompact"] && autoCompactor) {
9374
+ pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9375
+ pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
9376
+ } else {
9377
+ pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9378
+ }
9379
+ }
9380
+ if (typeof payload["logLevel"] === "string") {
9381
+ const valid = ["debug", "info", "warn", "error"];
9382
+ if (valid.includes(payload["logLevel"])) {
9383
+ logger.level = payload["logLevel"];
9384
+ }
9385
+ }
9386
+ broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
9387
+ }
9388
+ };
9323
9389
  shellGitRoutes = {
9324
9390
  gitInfo: async (ws) => {
9325
9391
  await handleGitInfo(ws, projectRoot);
@@ -9372,6 +9438,18 @@ async function startWebUI(opts = {}) {
9372
9438
  return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
9373
9439
  }
9374
9440
  };
9441
+ mcpRoutes = {
9442
+ list: (ws, msg) => handleMcpList(ws, msg, globalConfigPath, mcpRegistry),
9443
+ add: (ws, msg) => handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry),
9444
+ update: (ws, msg) => handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry),
9445
+ remove: (ws, msg) => handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry),
9446
+ enable: (ws, msg) => handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry),
9447
+ disable: (ws, msg) => handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry),
9448
+ sleep: (ws, msg) => handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry),
9449
+ wake: (ws, msg) => handleMcpWake(ws, msg, globalConfigPath, mcpRegistry),
9450
+ restart: (ws, msg) => handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry),
9451
+ discover: (ws, msg) => handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry)
9452
+ };
9375
9453
  brainRoutes = {
9376
9454
  status: (ws) => {
9377
9455
  send(ws, {