@wrongstack/webui 0.272.2 → 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.
@@ -8,7 +8,10 @@ function isRecord(value) {
8
8
  }
9
9
  function validateModelSwitchPayload(payload) {
10
10
  if (!isRecord(payload)) {
11
- return { ok: false, message: "model.switch payload must be an object with string provider and model" };
11
+ return {
12
+ ok: false,
13
+ message: "model.switch payload must be an object with string provider and model"
14
+ };
12
15
  }
13
16
  const provider = payload["provider"];
14
17
  const model = payload["model"];
@@ -30,13 +33,22 @@ function validateMailboxMessagesPayload(payload) {
30
33
  const agentId = payload["agentId"];
31
34
  const unreadOnly = payload["unreadOnly"];
32
35
  if (limit !== void 0 && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1)) {
33
- return { ok: false, message: "mailbox.messages payload.limit must be a positive number when provided" };
36
+ return {
37
+ ok: false,
38
+ message: "mailbox.messages payload.limit must be a positive number when provided"
39
+ };
34
40
  }
35
41
  if (agentId !== void 0 && typeof agentId !== "string") {
36
- return { ok: false, message: "mailbox.messages payload.agentId must be a string when provided" };
42
+ return {
43
+ ok: false,
44
+ message: "mailbox.messages payload.agentId must be a string when provided"
45
+ };
37
46
  }
38
47
  if (unreadOnly !== void 0 && typeof unreadOnly !== "boolean") {
39
- return { ok: false, message: "mailbox.messages payload.unreadOnly must be a boolean when provided" };
48
+ return {
49
+ ok: false,
50
+ message: "mailbox.messages payload.unreadOnly must be a boolean when provided"
51
+ };
40
52
  }
41
53
  return { ok: true, value: { limit, agentId, unreadOnly } };
42
54
  }
@@ -47,7 +59,10 @@ function validateMailboxAgentsPayload(payload) {
47
59
  }
48
60
  const onlineOnly = payload["onlineOnly"];
49
61
  if (onlineOnly !== void 0 && typeof onlineOnly !== "boolean") {
50
- return { ok: false, message: "mailbox.agents payload.onlineOnly must be a boolean when provided" };
62
+ return {
63
+ ok: false,
64
+ message: "mailbox.agents payload.onlineOnly must be a boolean when provided"
65
+ };
51
66
  }
52
67
  return { ok: true, value: { onlineOnly } };
53
68
  }
@@ -59,10 +74,16 @@ function validateMailboxPurgePayload(payload) {
59
74
  const completedMaxAgeMs = payload["completedMaxAgeMs"];
60
75
  const incompleteMaxAgeMs = payload["incompleteMaxAgeMs"];
61
76
  if (completedMaxAgeMs !== void 0 && (typeof completedMaxAgeMs !== "number" || !Number.isFinite(completedMaxAgeMs) || completedMaxAgeMs < 0)) {
62
- return { ok: false, message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided" };
77
+ return {
78
+ ok: false,
79
+ message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided"
80
+ };
63
81
  }
64
82
  if (incompleteMaxAgeMs !== void 0 && (typeof incompleteMaxAgeMs !== "number" || !Number.isFinite(incompleteMaxAgeMs) || incompleteMaxAgeMs < 0)) {
65
- return { ok: false, message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided" };
83
+ return {
84
+ ok: false,
85
+ message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided"
86
+ };
66
87
  }
67
88
  return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };
68
89
  }
@@ -73,7 +94,10 @@ function validateBrainRiskPayload(payload) {
73
94
  }
74
95
  const level = payload["level"];
75
96
  if (typeof level !== "string" || !BRAIN_RISK_VALUES.has(level)) {
76
- return { ok: false, message: "brain.risk payload.level must be one of off, low, medium, high, all" };
97
+ return {
98
+ ok: false,
99
+ message: "brain.risk payload.level must be one of off, low, medium, high, all"
100
+ };
77
101
  }
78
102
  return { ok: true, value: { level } };
79
103
  }
@@ -99,7 +123,10 @@ function validateAutonomySwitchPayload(payload) {
99
123
  }
100
124
  function validatePlanTemplateUsePayload(payload) {
101
125
  if (!isRecord(payload)) {
102
- return { ok: false, message: "plan.template_use payload must be an object with string template" };
126
+ return {
127
+ ok: false,
128
+ message: "plan.template_use payload must be an object with string template"
129
+ };
103
130
  }
104
131
  const template = payload["template"];
105
132
  if (typeof template !== "string" || template.trim().length === 0) {
@@ -114,7 +141,15 @@ var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
114
141
  var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
115
142
  var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
116
143
  var REASONING_MODE_VALUES = /* @__PURE__ */ new Set(["auto", "on", "off"]);
117
- var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
144
+ var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
145
+ "none",
146
+ "minimal",
147
+ "low",
148
+ "medium",
149
+ "high",
150
+ "xhigh",
151
+ "max"
152
+ ]);
118
153
  var CACHE_TTL_VALUES = /* @__PURE__ */ new Set(["default", "5m", "1h"]);
119
154
  var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
120
155
  "yolo",
@@ -135,8 +170,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
135
170
  "tgDelegate",
136
171
  "reasoningPreserve",
137
172
  "hqEnabled",
138
- "hqRawContent"
173
+ "hqRawContent",
174
+ "fallbackAuto"
139
175
  ]);
176
+ var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
140
177
  var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
141
178
  "autonomyDelayMs",
142
179
  "autoProceedMaxIterations",
@@ -168,6 +205,9 @@ function validatePreferenceValue(key, value) {
168
205
  if (STRING_PREF_KEYS.has(key)) {
169
206
  return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
170
207
  }
208
+ if (STRING_ARRAY_PREF_KEYS.has(key)) {
209
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
210
+ }
171
211
  const allowed = ENUM_PREF_KEYS[key];
172
212
  if (allowed) {
173
213
  return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
@@ -288,16 +328,25 @@ function validateContextModeCreatePayload(payload) {
288
328
  return { ok: false, message: "context.mode.create payload.description must be a string" };
289
329
  }
290
330
  if (!isRecord(thresholds)) {
291
- return { ok: false, message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers" };
331
+ return {
332
+ ok: false,
333
+ message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers"
334
+ };
292
335
  }
293
336
  if (!isFiniteNumber(thresholds["warn"]) || !isFiniteNumber(thresholds["soft"]) || !isFiniteNumber(thresholds["hard"])) {
294
- return { ok: false, message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers" };
337
+ return {
338
+ ok: false,
339
+ message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers"
340
+ };
295
341
  }
296
342
  if (!isFiniteNumber(preserveK)) {
297
343
  return { ok: false, message: "context.mode.create payload.preserveK must be a finite number" };
298
344
  }
299
345
  if (!isFiniteNumber(eliseThreshold)) {
300
- return { ok: false, message: "context.mode.create payload.eliseThreshold must be a finite number" };
346
+ return {
347
+ ok: false,
348
+ message: "context.mode.create payload.eliseThreshold must be a finite number"
349
+ };
301
350
  }
302
351
  return {
303
352
  ok: true,
@@ -321,22 +370,34 @@ function validateContextModeUpdatePayload(payload) {
321
370
  }
322
371
  const name2 = payload["name"];
323
372
  if (name2 !== void 0 && typeof name2 !== "string") {
324
- return { ok: false, message: "context.mode.update payload.name must be a string when provided" };
373
+ return {
374
+ ok: false,
375
+ message: "context.mode.update payload.name must be a string when provided"
376
+ };
325
377
  }
326
378
  const description = payload["description"];
327
379
  if (description !== void 0 && typeof description !== "string") {
328
- return { ok: false, message: "context.mode.update payload.description must be a string when provided" };
380
+ return {
381
+ ok: false,
382
+ message: "context.mode.update payload.description must be a string when provided"
383
+ };
329
384
  }
330
385
  const thresholds = payload["thresholds"];
331
386
  let validatedThresholds;
332
387
  if (thresholds !== void 0) {
333
388
  if (!isRecord(thresholds)) {
334
- return { ok: false, message: "context.mode.update payload.thresholds must be an object when provided" };
389
+ return {
390
+ ok: false,
391
+ message: "context.mode.update payload.thresholds must be an object when provided"
392
+ };
335
393
  }
336
394
  for (const key of ["warn", "soft", "hard"]) {
337
395
  const val = thresholds[key];
338
396
  if (val !== void 0 && !isFiniteNumber(val)) {
339
- return { ok: false, message: `context.mode.update payload.thresholds.${key} must be a finite number when provided` };
397
+ return {
398
+ ok: false,
399
+ message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`
400
+ };
340
401
  }
341
402
  }
342
403
  validatedThresholds = {
@@ -347,11 +408,17 @@ function validateContextModeUpdatePayload(payload) {
347
408
  }
348
409
  const preserveK = payload["preserveK"];
349
410
  if (preserveK !== void 0 && !isFiniteNumber(preserveK)) {
350
- return { ok: false, message: "context.mode.update payload.preserveK must be a finite number when provided" };
411
+ return {
412
+ ok: false,
413
+ message: "context.mode.update payload.preserveK must be a finite number when provided"
414
+ };
351
415
  }
352
416
  const eliseThreshold = payload["eliseThreshold"];
353
417
  if (eliseThreshold !== void 0 && !isFiniteNumber(eliseThreshold)) {
354
- return { ok: false, message: "context.mode.update payload.eliseThreshold must be a finite number when provided" };
418
+ return {
419
+ ok: false,
420
+ message: "context.mode.update payload.eliseThreshold must be a finite number when provided"
421
+ };
355
422
  }
356
423
  return {
357
424
  ok: true,
@@ -369,28 +436,31 @@ function validateShellOpenPayload(payload) {
369
436
  if (!isRecord(payload)) {
370
437
  return { ok: false, message: "shell.open payload must be an object with string path" };
371
438
  }
372
- const path16 = payload["path"];
373
- if (typeof path16 !== "string" || path16.trim().length === 0) {
439
+ const path17 = payload["path"];
440
+ if (typeof path17 !== "string" || path17.trim().length === 0) {
374
441
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
375
442
  }
376
443
  const target = payload["target"];
377
444
  if (target !== void 0 && target !== "file" && target !== "terminal") {
378
- return { ok: false, message: 'shell.open payload.target must be "file" or "terminal" when provided' };
445
+ return {
446
+ ok: false,
447
+ message: 'shell.open payload.target must be "file" or "terminal" when provided'
448
+ };
379
449
  }
380
- return { ok: true, value: { path: path16, target } };
450
+ return { ok: true, value: { path: path17, target } };
381
451
  }
382
452
  function validateGitDiffPayload(payload) {
383
453
  if (!isRecord(payload)) {
384
454
  return { ok: false, message: "git.diff payload must be an object" };
385
455
  }
386
- const path16 = payload["path"];
387
- if (path16 === void 0 || path16 === null) {
456
+ const path17 = payload["path"];
457
+ if (path17 === void 0 || path17 === null) {
388
458
  return { ok: true, value: { path: "" } };
389
459
  }
390
- if (typeof path16 !== "string") {
460
+ if (typeof path17 !== "string") {
391
461
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
392
462
  }
393
- return { ok: true, value: { path: path16 } };
463
+ return { ok: true, value: { path: path17 } };
394
464
  }
395
465
  function validateProjectsAddPayload(payload) {
396
466
  if (!isRecord(payload)) {
@@ -570,7 +640,7 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
570
640
  return;
571
641
  }
572
642
  try {
573
- const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
643
+ const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
574
644
  let changed = false;
575
645
  const plan = await mutatePlan(planPath, sessionId, async (p) => {
576
646
  const before = p.updatedAt;
@@ -650,7 +720,7 @@ import {
650
720
  createTieredBrainArbiter
651
721
  } from "@wrongstack/core";
652
722
  import * as fs13 from "fs/promises";
653
- import * as path15 from "path";
723
+ import * as path16 from "path";
654
724
 
655
725
  // src/server/http-server.ts
656
726
  import * as fs from "fs/promises";
@@ -827,7 +897,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
827
897
  return;
828
898
  }
829
899
  try {
830
- 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");
831
901
  const registry = new SessionRegistry(globalRoot);
832
902
  const entry = await registry.get(sessionId);
833
903
  if (!entry) {
@@ -836,7 +906,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
836
906
  return;
837
907
  }
838
908
  const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
839
- const store = new DefaultSessionStore4({ dir: paths.projectSessions });
909
+ const store = new DefaultSessionStore3({ dir: paths.projectSessions });
840
910
  const reader = new DefaultSessionReader2({ store });
841
911
  const rawEntries = [];
842
912
  for await (const ev of reader.replay(sessionId)) {
@@ -1522,8 +1592,8 @@ function isInside(root, target) {
1522
1592
  }
1523
1593
 
1524
1594
  // src/server/file-handlers.ts
1525
- import * as fs3 from "fs/promises";
1526
- import * as path3 from "path";
1595
+ import * as fs4 from "fs/promises";
1596
+ import * as path4 from "path";
1527
1597
  import { atomicWrite } from "@wrongstack/core";
1528
1598
 
1529
1599
  // src/server/file-picker.ts
@@ -1574,6 +1644,34 @@ function rankFiles(paths, query, limit) {
1574
1644
  return scored.slice(0, limit).map((s) => s.path);
1575
1645
  }
1576
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
+
1577
1675
  // src/server/ws-utils.ts
1578
1676
  import { randomBytes } from "crypto";
1579
1677
  import { WebSocket } from "ws";
@@ -1604,23 +1702,73 @@ function generateAuthToken() {
1604
1702
  }
1605
1703
 
1606
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
+ }
1607
1747
  async function handleFilesTree(ws, msg, projectRoot) {
1608
1748
  const payload = msg.payload;
1609
1749
  const rawPath = payload?.path?.trim();
1610
- const treeRoot = rawPath && rawPath !== "." ? path3.resolve(projectRoot, rawPath) : projectRoot;
1611
- 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 {
1612
1760
  send(ws, {
1613
1761
  type: "files.tree",
1614
1762
  payload: { root: projectRoot, tree: [], error: "Path outside project root" }
1615
1763
  });
1616
1764
  return;
1617
1765
  }
1618
- const pathPrefix = treeRoot === projectRoot ? "" : (path3.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1766
+ const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1619
1767
  async function buildTree(dir, rel, depth) {
1620
1768
  if (depth > 10) return [];
1621
1769
  let entries = [];
1622
1770
  try {
1623
- entries = await fs3.readdir(dir, { withFileTypes: true });
1771
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1624
1772
  } catch {
1625
1773
  return [];
1626
1774
  }
@@ -1632,11 +1780,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
1632
1780
  for (const e of entries) {
1633
1781
  if (isHiddenEntry(e.name)) continue;
1634
1782
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1635
- const childAbs = path3.join(dir, e.name);
1783
+ const childAbs = path4.join(dir, e.name);
1636
1784
  const childPath = pathPrefix + childRel;
1637
1785
  if (e.isDirectory()) {
1638
1786
  if (SKIP_DIRS.has(e.name)) continue;
1639
- 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);
1640
1797
  nodes.push({ name: e.name, path: childPath, type: "directory", children });
1641
1798
  } else if (e.isFile()) {
1642
1799
  nodes.push({ name: e.name, path: childPath, type: "file" });
@@ -1646,10 +1803,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
1646
1803
  }
1647
1804
  try {
1648
1805
  const tree = await buildTree(treeRoot, "", 0);
1649
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1806
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1650
1807
  send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
1651
1808
  } catch (err) {
1652
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1809
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1653
1810
  send(ws, {
1654
1811
  type: "files.tree",
1655
1812
  payload: { root: rootLabel, tree: [], error: errMessage(err) }
@@ -1658,13 +1815,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
1658
1815
  }
1659
1816
  async function handleFilesRead(ws, msg, projectRoot) {
1660
1817
  const { filePath } = msg.payload;
1661
- const resolved = path3.resolve(projectRoot, filePath);
1662
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1818
+ let realResolved;
1819
+ try {
1820
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1821
+ } catch {
1663
1822
  send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
1664
1823
  return;
1665
1824
  }
1666
1825
  try {
1667
- const content = await fs3.readFile(resolved, "utf8");
1826
+ const content = await fs4.readFile(realResolved, "utf8");
1668
1827
  send(ws, { type: "files.read", payload: { filePath, content } });
1669
1828
  } catch (err) {
1670
1829
  send(ws, {
@@ -1675,16 +1834,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
1675
1834
  }
1676
1835
  async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1677
1836
  const { filePath, content } = msg.payload;
1678
- const resolved = path3.resolve(projectRoot, filePath);
1679
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1837
+ let realResolved;
1838
+ try {
1839
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1840
+ } catch {
1680
1841
  send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
1681
1842
  return;
1682
1843
  }
1683
1844
  try {
1684
- await atomicWrite(resolved, content);
1845
+ await atomicWrite(realResolved, content);
1685
1846
  send(ws, { type: "files.written", payload: { filePath, success: true } });
1686
1847
  if (opts.onWritten) {
1687
- void Promise.resolve(opts.onWritten(resolved)).catch(() => void 0);
1848
+ void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
1688
1849
  }
1689
1850
  } catch (err) {
1690
1851
  send(ws, {
@@ -1696,8 +1857,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1696
1857
  async function handleFilesList(ws, msg, projectRoot) {
1697
1858
  const payload = msg.payload ?? {};
1698
1859
  const limit = payload.limit ?? 50;
1699
- const listRoot = payload.path ? path3.resolve(projectRoot, payload.path) : projectRoot;
1700
- 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 {
1701
1870
  send(ws, { type: "files.list", payload: { files: [] } });
1702
1871
  return;
1703
1872
  }
@@ -1706,7 +1875,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1706
1875
  if (depth > 8 || results.length >= 600) return;
1707
1876
  let entries = [];
1708
1877
  try {
1709
- entries = await fs3.readdir(dir, { withFileTypes: true });
1878
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1710
1879
  } catch {
1711
1880
  return;
1712
1881
  }
@@ -1716,7 +1885,16 @@ async function handleFilesList(ws, msg, projectRoot) {
1716
1885
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1717
1886
  if (e.isDirectory()) {
1718
1887
  if (SKIP_DIRS.has(e.name)) continue;
1719
- 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);
1720
1898
  } else if (e.isFile()) {
1721
1899
  results.push(childRel);
1722
1900
  }
@@ -1730,7 +1908,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1730
1908
  }
1731
1909
 
1732
1910
  // src/server/completion-handlers.ts
1733
- import * as path4 from "path";
1911
+ import * as path5 from "path";
1734
1912
  import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
1735
1913
  var MAX_PREFIX_CHARS = 12e3;
1736
1914
  var MAX_SUFFIX_CHARS = 4e3;
@@ -1805,8 +1983,8 @@ async function handleCompletionRequest(ws, msg, opts) {
1805
1983
  return;
1806
1984
  }
1807
1985
  const payload = parsed.payload;
1808
- const projectRoot = path4.resolve(opts.projectRoot);
1809
- const resolved = path4.resolve(projectRoot, payload.filePath);
1986
+ const projectRoot = path5.resolve(opts.projectRoot);
1987
+ const resolved = path5.resolve(projectRoot, payload.filePath);
1810
1988
  if (!isInside2(projectRoot, resolved)) {
1811
1989
  send(ws, {
1812
1990
  type: "completion.result",
@@ -2205,7 +2383,7 @@ function buildSearchQuery(linePrefix, filePath) {
2205
2383
  if (memberMatch?.[1]) return memberMatch[1];
2206
2384
  const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
2207
2385
  if (token && token.length >= 2) return token;
2208
- return path4.basename(filePath, path4.extname(filePath));
2386
+ return path5.basename(filePath, path5.extname(filePath));
2209
2387
  }
2210
2388
  function currentLinePrefix(prefix) {
2211
2389
  const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
@@ -2235,7 +2413,7 @@ function head(value, max) {
2235
2413
  return value.length <= max ? value : value.slice(0, max);
2236
2414
  }
2237
2415
  function isInside2(root, target) {
2238
- return target === root || target.startsWith(root + path4.sep);
2416
+ return target === root || target.startsWith(root + path5.sep);
2239
2417
  }
2240
2418
 
2241
2419
  // src/server/memory-handlers.ts
@@ -2489,8 +2667,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
2489
2667
  }
2490
2668
 
2491
2669
  // src/server/skills-handlers.ts
2492
- import { promises as fs4 } from "fs";
2493
- import path5 from "path";
2670
+ import { promises as fs5 } from "fs";
2671
+ import path6 from "path";
2494
2672
  import JSZip from "jszip";
2495
2673
  import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
2496
2674
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
@@ -2561,19 +2739,19 @@ async function handleSkillsContent(ws, ctx, msg) {
2561
2739
  send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
2562
2740
  return;
2563
2741
  }
2564
- const body = await fs4.readFile(entry.path, "utf8");
2565
- const skillDir = path5.dirname(entry.path);
2742
+ const body = await fs5.readFile(entry.path, "utf8");
2743
+ const skillDir = path6.dirname(entry.path);
2566
2744
  let relatedFiles = [];
2567
2745
  try {
2568
- const files = await fs4.readdir(skillDir);
2569
- 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));
2570
2748
  } catch {
2571
2749
  }
2572
2750
  const nameLower = name2.toLowerCase();
2573
2751
  const refResults = await Promise.all(
2574
2752
  entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
2575
2753
  try {
2576
- const content = await fs4.readFile(e.path, "utf8");
2754
+ const content = await fs5.readFile(e.path, "utf8");
2577
2755
  return [e.name, content.toLowerCase().includes(nameLower)];
2578
2756
  } catch {
2579
2757
  return [e.name, false];
@@ -2663,14 +2841,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
2663
2841
  }
2664
2842
  const createPayload = parsed.value;
2665
2843
  try {
2666
- 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());
2667
2845
  try {
2668
- await fs4.access(targetDir);
2846
+ await fs5.access(targetDir);
2669
2847
  send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
2670
2848
  return;
2671
2849
  } catch {
2672
2850
  }
2673
- await fs4.mkdir(targetDir, { recursive: true });
2851
+ await fs5.mkdir(targetDir, { recursive: true });
2674
2852
  const lines = createPayload.description.trim().split("\n");
2675
2853
  const firstLine = lines[0].trim();
2676
2854
  const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
@@ -2718,13 +2896,13 @@ ${trigger}
2718
2896
  "- `bug-hunter` \u2014 for systematic bug detection patterns",
2719
2897
  "- `output-standards` \u2014 for standardized `<next_steps>` formatting"
2720
2898
  ].join("\n");
2721
- await atomicWrite2(path5.join(targetDir, "SKILL.md"), skillContent);
2899
+ await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
2722
2900
  send(ws, {
2723
2901
  type: "skills.created",
2724
2902
  payload: {
2725
2903
  success: true,
2726
2904
  error: null,
2727
- 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 }
2728
2906
  }
2729
2907
  });
2730
2908
  } catch (err) {
@@ -2788,23 +2966,23 @@ import {
2788
2966
  Agent,
2789
2967
  AutoCompactionMiddleware,
2790
2968
  Context,
2791
- DefaultMemoryStore as DefaultMemoryStore2,
2792
- DefaultModeStore as DefaultModeStore2,
2969
+ DefaultMemoryStore,
2970
+ DefaultModeStore,
2793
2971
  DefaultModelsRegistry,
2794
2972
  DefaultSessionReader,
2795
- DefaultSessionStore as DefaultSessionStore3,
2796
- DefaultSkillLoader as DefaultSkillLoader2,
2797
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder4,
2798
- DefaultTokenCounter as DefaultTokenCounter2,
2973
+ DefaultSessionStore as DefaultSessionStore2,
2974
+ DefaultSkillLoader,
2975
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
2976
+ DefaultTokenCounter,
2799
2977
  AnnotationsStore,
2800
2978
  CollaborationBus,
2801
2979
  collabPauseMiddleware,
2802
2980
  collabInjectMiddleware,
2803
2981
  estimateRequestTokensCalibrated,
2804
2982
  EventBus,
2805
- createStrategyCompactor as createStrategyCompactor2,
2983
+ createStrategyCompactor,
2806
2984
  ProviderRegistry,
2807
- TOKENS as TOKENS2,
2985
+ TOKENS,
2808
2986
  ToolRegistry,
2809
2987
  atomicWrite as atomicWrite6,
2810
2988
  createDefaultPipelines,
@@ -2813,6 +2991,7 @@ import {
2813
2991
  DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
2814
2992
  DEFAULT_SESSION_PRUNE_DAYS,
2815
2993
  DEFAULT_TOOLS_CONFIG,
2994
+ applyToolDescriptionModes,
2816
2995
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
2817
2996
  enhanceUserPrompt,
2818
2997
  gatedEnhancerReasoning,
@@ -2822,109 +3001,10 @@ import {
2822
3001
  import { ToolExecutor } from "@wrongstack/core/execution";
2823
3002
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
2824
3003
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
2825
- import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
3004
+ import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
2826
3005
  import { MCPRegistry } from "@wrongstack/mcp";
2827
3006
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
2828
-
2829
- // ../runtime/src/container.ts
2830
- import {
2831
- Container,
2832
- DefaultConfigStore,
2833
- DefaultErrorHandler,
2834
- DefaultMemoryStore,
2835
- DefaultModeStore,
2836
- DefaultPermissionPolicy,
2837
- DefaultRetryPolicy,
2838
- DefaultSecretScrubber,
2839
- DefaultSessionStore,
2840
- DefaultSkillLoader,
2841
- DefaultSystemPromptBuilder,
2842
- DefaultTokenCounter,
2843
- createStrategyCompactor,
2844
- buildRecoveryStrategies,
2845
- TOKENS
2846
- } from "@wrongstack/core";
2847
- function createDefaultContainer(opts) {
2848
- const { config, wpaths, logger, modelsRegistry } = opts;
2849
- const container = new Container();
2850
- const configStore = new DefaultConfigStore(config);
2851
- container.bind(TOKENS.ConfigStore, () => configStore);
2852
- container.bind(TOKENS.Logger, () => logger);
2853
- container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
2854
- container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
2855
- container.bind(
2856
- TOKENS.ErrorHandler,
2857
- () => new DefaultErrorHandler(
2858
- buildRecoveryStrategies({
2859
- compactor: container.resolve(TOKENS.Compactor),
2860
- modelsRegistry
2861
- })
2862
- )
2863
- );
2864
- container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
2865
- container.bind(
2866
- TOKENS.TokenCounter,
2867
- () => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
2868
- );
2869
- const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
2870
- container.bind(TOKENS.ModeStore, () => modeStore);
2871
- container.bind(
2872
- TOKENS.SessionStore,
2873
- () => new DefaultSessionStore({
2874
- dir: wpaths.projectSessions,
2875
- // Scrub secrets out of persisted user/model turns (F-06). Tool output
2876
- // is already scrubbed by the executor.
2877
- secretScrubber: container.resolve(TOKENS.SecretScrubber)
2878
- })
2879
- );
2880
- const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
2881
- container.bind(TOKENS.MemoryStore, () => memoryStore);
2882
- const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
2883
- container.bind(TOKENS.SkillLoader, () => skillLoader);
2884
- if (opts.systemPrompt) {
2885
- container.bind(
2886
- TOKENS.SystemPromptBuilder,
2887
- () => new DefaultSystemPromptBuilder(opts.systemPrompt)
2888
- );
2889
- }
2890
- container.bind(
2891
- TOKENS.PermissionPolicy,
2892
- () => {
2893
- const policyOptions = {
2894
- trustFile: wpaths.projectTrust,
2895
- yolo: opts.permission?.yolo ?? false,
2896
- yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
2897
- confirmDestructive: opts.permission?.confirmDestructive ?? false
2898
- };
2899
- if (opts.permission?.promptDelegate !== void 0) {
2900
- policyOptions.promptDelegate = opts.permission.promptDelegate;
2901
- }
2902
- return new DefaultPermissionPolicy(policyOptions);
2903
- }
2904
- );
2905
- container.bind(
2906
- TOKENS.Compactor,
2907
- () => (
2908
- // Strategy comes from config.context.strategy: 'hybrid' (default, lossless
2909
- // rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
2910
- // (LLM-driven selection). The LLM strategies resolve their provider from
2911
- // ctx at compact()-time, so binding here (before context.provider exists)
2912
- // is safe. preserveK / eliseThreshold are class-level fallbacks; the active
2913
- // ContextWindowPolicy in ctx.meta normally overrides both at runtime.
2914
- // eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
2915
- // essentially every tool_result (anything > 1 token).
2916
- createStrategyCompactor({
2917
- strategy: config.context?.strategy,
2918
- preserveK: opts.compactor?.preserveK ?? 10,
2919
- eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
2920
- smart: true,
2921
- summarizerModel: config.context?.summarizerModel,
2922
- llmSelector: config.context?.llmSelector
2923
- })
2924
- )
2925
- );
2926
- return container;
2927
- }
3007
+ import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
2928
3008
 
2929
3009
  // src/server/boot.ts
2930
3010
  import {
@@ -2944,6 +3024,7 @@ function patchConfig(config, updates) {
2944
3024
  import { spawnSync } from "child_process";
2945
3025
  import { toErrorMessage } from "@wrongstack/core/utils";
2946
3026
  import {
3027
+ assignNickname,
2947
3028
  AutoPhasePlanner,
2948
3029
  PhaseGraphBuilder,
2949
3030
  PhaseOrchestrator,
@@ -2981,6 +3062,8 @@ var AutoPhaseWebSocketHandler = class {
2981
3062
  abort = null;
2982
3063
  /** Optional per-phase git-worktree isolation (lazily created at start). */
2983
3064
  worktrees = null;
3065
+ /** Per-run worker identities so the board can show "who is on what". */
3066
+ usedNicknames = /* @__PURE__ */ new Set();
2984
3067
  addClient(ws) {
2985
3068
  const client = { ws, id: crypto.randomUUID() };
2986
3069
  this.clients.add(client);
@@ -3023,6 +3106,29 @@ var AutoPhaseWebSocketHandler = class {
3023
3106
  await this.handleTaskStatusChange(taskId, status);
3024
3107
  break;
3025
3108
  }
3109
+ case "autophase.moveTask": {
3110
+ const { taskId, toPhaseId } = msg.payload;
3111
+ if (this.orchestrator?.moveTask(taskId, toPhaseId)) this.afterBoardMutation();
3112
+ break;
3113
+ }
3114
+ case "autophase.assignTask": {
3115
+ const { taskId, agentId, agentName } = msg.payload;
3116
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
3117
+ break;
3118
+ }
3119
+ case "autophase.addTask": {
3120
+ const { phaseId, title, description, type, priority } = msg.payload;
3121
+ if (title?.trim() && this.orchestrator?.addTask(phaseId, { title: title.trim(), description, type, priority })) {
3122
+ this.afterBoardMutation();
3123
+ }
3124
+ break;
3125
+ }
3126
+ case "autophase.retryTask":
3127
+ case "autophase.runTask": {
3128
+ const { taskId } = msg.payload;
3129
+ if (this.orchestrator?.requeueTask(taskId)) this.afterBoardMutation();
3130
+ break;
3131
+ }
3026
3132
  case "autophase.toggleAutonomous": {
3027
3133
  const autonomous = msg.payload?.autonomous ?? !this.graph?.autonomous;
3028
3134
  if (this.graph) {
@@ -3150,6 +3256,13 @@ var AutoPhaseWebSocketHandler = class {
3150
3256
  return this.defaultPhases();
3151
3257
  }
3152
3258
  async executeTaskWithAgent(task, phaseId, env) {
3259
+ if (!task.assignee) {
3260
+ const nick = assignNickname("executor", this.usedNicknames);
3261
+ this.usedNicknames.add(nick.key);
3262
+ task.assignee = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3263
+ task.updatedAt = Date.now();
3264
+ this.broadcastState();
3265
+ }
3153
3266
  const prompt = `Execute task: ${task.title}
3154
3267
 
3155
3268
  Description: ${task.description}
@@ -3165,6 +3278,11 @@ Type: ${task.type}`;
3165
3278
  this.context.cwd = prevCwd;
3166
3279
  }
3167
3280
  }
3281
+ /** Persist + broadcast after an interactive board mutation. */
3282
+ afterBoardMutation() {
3283
+ if (this.graph) void this.store.save(this.graph);
3284
+ this.broadcastState();
3285
+ }
3168
3286
  async handleTaskStatusChange(taskId, status) {
3169
3287
  if (!this.graph) return;
3170
3288
  for (const phase of this.graph.phases.values()) {
@@ -3208,23 +3326,7 @@ Type: ${task.type}`;
3208
3326
  (sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3209
3327
  0
3210
3328
  );
3211
- const phaseItems = phases.map((p) => ({
3212
- id: p.id,
3213
- name: p.name,
3214
- description: p.description,
3215
- status: p.status,
3216
- priority: p.priority,
3217
- estimateHours: p.estimateHours,
3218
- actualDurationMs: p.actualDurationMs,
3219
- startedAt: p.startedAt,
3220
- completedAt: p.completedAt,
3221
- progressPercent: p.taskGraph.nodes.size > 0 ? Math.round(Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length / p.taskGraph.nodes.size * 100) : 0,
3222
- taskCount: p.taskGraph.nodes.size,
3223
- completedTasks: Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3224
- assignedAgents: p.assignedAgents,
3225
- isActive: p.id === currentActiveId
3226
- }));
3227
- const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
3329
+ const mapTask = (t) => ({
3228
3330
  id: t.id,
3229
3331
  title: t.title,
3230
3332
  description: t.description,
@@ -3237,8 +3339,39 @@ Type: ${task.type}`;
3237
3339
  tags: t.tags || [],
3238
3340
  startedAt: t.startedAt,
3239
3341
  completedAt: t.completedAt
3240
- })) : [];
3342
+ });
3343
+ const phaseItems = phases.map((p) => {
3344
+ const nodes = Array.from(p.taskGraph.nodes.values());
3345
+ const done = nodes.filter((t) => t.status === "completed").length;
3346
+ return {
3347
+ id: p.id,
3348
+ name: p.name,
3349
+ description: p.description,
3350
+ status: p.status,
3351
+ priority: p.priority,
3352
+ estimateHours: p.estimateHours,
3353
+ actualDurationMs: p.actualDurationMs,
3354
+ startedAt: p.startedAt,
3355
+ completedAt: p.completedAt,
3356
+ progressPercent: nodes.length > 0 ? Math.round(done / nodes.length * 100) : 0,
3357
+ taskCount: nodes.length,
3358
+ completedTasks: done,
3359
+ assignedAgents: p.assignedAgents,
3360
+ isActive: p.id === currentActiveId,
3361
+ // Every phase carries its full task list so the board can render each
3362
+ // phase as a column (not just the selected one).
3363
+ tasks: nodes.map(mapTask)
3364
+ };
3365
+ });
3366
+ const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
3241
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;
3242
3375
  return {
3243
3376
  title: this.graph.title,
3244
3377
  phases: phaseItems,
@@ -3247,7 +3380,18 @@ Type: ${task.type}`;
3247
3380
  overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
3248
3381
  autonomous: this.graph.autonomous,
3249
3382
  totalTasks,
3250
- 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
3251
3395
  };
3252
3396
  }
3253
3397
  sendState(client) {
@@ -3270,6 +3414,510 @@ Type: ${task.type}`;
3270
3414
  }
3271
3415
  };
3272
3416
 
3417
+ // src/server/specs-ws-handler.ts
3418
+ import {
3419
+ computeTaskProgress,
3420
+ SpecStore,
3421
+ TaskGraphStore
3422
+ } from "@wrongstack/core";
3423
+ var SpecsWebSocketHandler = class {
3424
+ specStore;
3425
+ graphStore;
3426
+ clients = /* @__PURE__ */ new Set();
3427
+ constructor(specsDir, taskGraphsDir) {
3428
+ this.specStore = new SpecStore({ baseDir: specsDir });
3429
+ this.graphStore = new TaskGraphStore({ baseDir: taskGraphsDir });
3430
+ }
3431
+ addClient(ws) {
3432
+ const client = { ws, id: crypto.randomUUID() };
3433
+ this.clients.add(client);
3434
+ ws.on("close", () => this.clients.delete(client));
3435
+ ws.on("error", () => this.clients.delete(client));
3436
+ void this.sendList(client);
3437
+ }
3438
+ async handleMessage(msg) {
3439
+ switch (msg.type) {
3440
+ case "specs.list":
3441
+ await this.broadcastList();
3442
+ break;
3443
+ case "specs.get": {
3444
+ const specId = msg.payload?.specId;
3445
+ if (specId) await this.broadcastDetail(specId);
3446
+ break;
3447
+ }
3448
+ case "specs.taskStatus": {
3449
+ const { graphId, taskId, status } = msg.payload;
3450
+ await this.updateTaskStatus(graphId, taskId, status);
3451
+ break;
3452
+ }
3453
+ }
3454
+ }
3455
+ // ── List ──────────────────────────────────────────────────────────────────
3456
+ async buildList() {
3457
+ const [specs, graphs] = await Promise.all([this.specStore.list(), this.graphStore.list()]);
3458
+ return specs.map((s, i) => {
3459
+ const graph = graphs.find((g) => g.specId === s.id);
3460
+ return {
3461
+ id: s.id,
3462
+ // FORGE-style display id (spec-001…). The real UUID stays in `id`.
3463
+ displayId: `spec-${String(i + 1).padStart(3, "0")}`,
3464
+ title: s.title,
3465
+ status: s.status,
3466
+ graphId: graph?.id,
3467
+ total: graph?.nodeCount ?? 0,
3468
+ completed: graph?.completedCount ?? 0
3469
+ };
3470
+ });
3471
+ }
3472
+ async broadcastList() {
3473
+ this.broadcast({ type: "specs.list", payload: { specs: await this.buildList() } });
3474
+ }
3475
+ async sendList(client) {
3476
+ this.send(client, { type: "specs.list", payload: { specs: await this.buildList() } });
3477
+ }
3478
+ // ── Detail (dependency board) ───────────────────────────────────────────────
3479
+ async broadcastDetail(specId) {
3480
+ const spec = await this.specStore.load(specId);
3481
+ const graph = await this.findGraphForSpec(specId);
3482
+ if (!spec || !graph) {
3483
+ this.broadcast({ type: "specs.detail", payload: { specId, columns: [], notFound: true } });
3484
+ return;
3485
+ }
3486
+ this.broadcast({ type: "specs.detail", payload: this.buildDetail(spec, graph) });
3487
+ }
3488
+ async findGraphForSpec(specId) {
3489
+ const entry = (await this.graphStore.list()).find((g) => g.specId === specId);
3490
+ if (!entry) return null;
3491
+ return this.graphStore.load(entry.id);
3492
+ }
3493
+ buildDetail(spec, graph) {
3494
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
3495
+ const shortId = /* @__PURE__ */ new Map();
3496
+ nodes.forEach((n, i) => {
3497
+ shortId.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
3498
+ });
3499
+ const blockers = /* @__PURE__ */ new Map();
3500
+ for (const n of nodes) blockers.set(n.id, []);
3501
+ for (const e of graph.edges) {
3502
+ if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
3503
+ }
3504
+ const statusOf = (id) => graph.nodes.get(id)?.status;
3505
+ const depthCache = /* @__PURE__ */ new Map();
3506
+ const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
3507
+ const cached = depthCache.get(id);
3508
+ if (cached !== void 0) return cached;
3509
+ if (seen.has(id)) return 0;
3510
+ seen.add(id);
3511
+ const deps2 = blockers.get(id) ?? [];
3512
+ const d = deps2.length === 0 ? 0 : 1 + Math.max(...deps2.map((b) => depthOf(b, seen)));
3513
+ depthCache.set(id, d);
3514
+ return d;
3515
+ };
3516
+ const toBoardTask = (n) => {
3517
+ const deps2 = blockers.get(n.id) ?? [];
3518
+ const allDepsDone = deps2.every((b) => statusOf(b) === "completed");
3519
+ const displayStatus = n.status === "pending" && deps2.length > 0 && allDepsDone ? "queued" : n.status;
3520
+ return {
3521
+ id: n.id,
3522
+ shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
3523
+ title: n.title,
3524
+ description: n.description,
3525
+ priority: n.priority,
3526
+ type: n.type,
3527
+ status: n.status,
3528
+ displayStatus,
3529
+ deps: deps2.map((b) => shortId.get(b) ?? b.slice(0, 6))
3530
+ };
3531
+ };
3532
+ const byDepth = /* @__PURE__ */ new Map();
3533
+ for (const n of nodes) {
3534
+ const d = depthOf(n.id);
3535
+ if (!byDepth.has(d)) byDepth.set(d, []);
3536
+ byDepth.get(d)?.push(toBoardTask(n));
3537
+ }
3538
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, tasks: byDepth.get(d) ?? [] }));
3539
+ const progress = computeTaskProgress(graph);
3540
+ return {
3541
+ specId: spec.id,
3542
+ graphId: graph.id,
3543
+ title: spec.title,
3544
+ overview: spec.overview,
3545
+ status: spec.status,
3546
+ total: progress.total,
3547
+ completed: progress.completed,
3548
+ running: progress.inProgress,
3549
+ pending: progress.pending,
3550
+ columns
3551
+ };
3552
+ }
3553
+ async updateTaskStatus(graphId, taskId, status) {
3554
+ const graph = await this.graphStore.load(graphId);
3555
+ const node = graph?.nodes.get(taskId);
3556
+ if (!graph || !node) return;
3557
+ node.status = status;
3558
+ node.updatedAt = Date.now();
3559
+ graph.updatedAt = Date.now();
3560
+ await this.graphStore.save(graph);
3561
+ this.broadcastDetail(graph.specId).catch(() => {
3562
+ });
3563
+ await this.broadcastList();
3564
+ }
3565
+ // ── Transport ───────────────────────────────────────────────────────────────
3566
+ broadcast(msg) {
3567
+ const data = JSON.stringify(msg);
3568
+ for (const client of this.clients) {
3569
+ if (client.ws.readyState === 1) client.ws.send(data);
3570
+ }
3571
+ }
3572
+ send(client, msg) {
3573
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3574
+ }
3575
+ };
3576
+
3577
+ // src/server/sdd-board-ws-handler.ts
3578
+ import { SddBoardStore } from "@wrongstack/core";
3579
+ var CONTROL_TYPES = /* @__PURE__ */ new Set([
3580
+ "pause",
3581
+ "resume",
3582
+ "stop",
3583
+ "retry",
3584
+ "retry_all_failed",
3585
+ "reassign",
3586
+ // Per-task model / fallback / verification assignment + stop/delete (drained by start-sdd-run).
3587
+ "set_task_model",
3588
+ "set_task_fallbacks",
3589
+ "set_task_verification",
3590
+ "cancel_task",
3591
+ "delete_task",
3592
+ "split_task",
3593
+ // Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
3594
+ "cleanup_worktrees",
3595
+ "rollback"
3596
+ ]);
3597
+ var SddBoardWebSocketHandler = class {
3598
+ store;
3599
+ clients = /* @__PURE__ */ new Set();
3600
+ latest = null;
3601
+ poll = null;
3602
+ unsub = null;
3603
+ constructor(boardsDir, events) {
3604
+ this.store = new SddBoardStore({ baseDir: boardsDir });
3605
+ if (events) {
3606
+ const handler = (e) => {
3607
+ this.latest = e.snapshot;
3608
+ this.broadcast({ type: "sdd.board.snapshot", payload: e.snapshot });
3609
+ };
3610
+ this.unsub = events.on("sdd.board.snapshot", handler);
3611
+ } else {
3612
+ this.poll = setInterval(() => void this.pollLatest(), 1e3);
3613
+ }
3614
+ }
3615
+ addClient(ws) {
3616
+ const client = { ws, id: crypto.randomUUID() };
3617
+ this.clients.add(client);
3618
+ ws.on("close", () => this.clients.delete(client));
3619
+ ws.on("error", () => this.clients.delete(client));
3620
+ void this.sendCurrent(client);
3621
+ }
3622
+ async handleMessage(msg) {
3623
+ if (msg.type === "sdd.board.get") {
3624
+ await this.broadcastCurrent();
3625
+ return;
3626
+ }
3627
+ if (msg.type === "sdd.board.list") {
3628
+ const boards = await this.store.list();
3629
+ this.broadcast({ type: "sdd.board.list", payload: { boards } });
3630
+ return;
3631
+ }
3632
+ const action = msg.type.replace(/^sdd\.board\./, "");
3633
+ if (CONTROL_TYPES.has(action)) {
3634
+ const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
3635
+ if (runId) {
3636
+ await this.store.appendControl(runId, {
3637
+ ts: Date.now(),
3638
+ type: action,
3639
+ payload: msg.payload
3640
+ });
3641
+ }
3642
+ }
3643
+ }
3644
+ dispose() {
3645
+ if (this.poll) clearInterval(this.poll);
3646
+ this.unsub?.();
3647
+ this.poll = null;
3648
+ this.unsub = null;
3649
+ }
3650
+ // ── internal ────────────────────────────────────────────────────────────
3651
+ async pollLatest() {
3652
+ const entry = (await this.store.list())[0];
3653
+ if (!entry) return;
3654
+ if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
3655
+ return;
3656
+ }
3657
+ const snap = await this.store.load(entry.runId);
3658
+ if (snap) {
3659
+ this.latest = snap;
3660
+ this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3661
+ }
3662
+ }
3663
+ async sendCurrent(client) {
3664
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3665
+ if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
3666
+ }
3667
+ async broadcastCurrent() {
3668
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3669
+ if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3670
+ }
3671
+ async loadLatestFromDisk() {
3672
+ const entry = (await this.store.list())[0];
3673
+ return entry ? this.store.load(entry.runId) : null;
3674
+ }
3675
+ broadcast(msg) {
3676
+ const data = JSON.stringify(msg);
3677
+ for (const client of this.clients) {
3678
+ if (client.ws.readyState === 1) client.ws.send(data);
3679
+ }
3680
+ }
3681
+ send(client, msg) {
3682
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3683
+ }
3684
+ };
3685
+
3686
+ // src/server/sdd-wizard-ws-handler.ts
3687
+ var SddWizardWebSocketHandler = class {
3688
+ constructor(deps2) {
3689
+ this.deps = deps2;
3690
+ }
3691
+ deps;
3692
+ clients = /* @__PURE__ */ new Set();
3693
+ driver = null;
3694
+ /** The agent's most recent question — paired with the next user answer. */
3695
+ lastAgentText = "";
3696
+ /** Guards against overlapping interview turns (one in flight at a time). */
3697
+ busy = false;
3698
+ addClient(ws) {
3699
+ const client = { ws, id: crypto.randomUUID() };
3700
+ this.clients.add(client);
3701
+ ws.on("close", () => this.clients.delete(client));
3702
+ ws.on("error", () => this.clients.delete(client));
3703
+ if (this.driver) this.send(client, this.snapshotMsg());
3704
+ }
3705
+ async handleMessage(msg) {
3706
+ try {
3707
+ switch (msg.type) {
3708
+ case "sdd.spec.start":
3709
+ await this.onStart(String(msg.payload?.goal ?? "").trim());
3710
+ break;
3711
+ case "sdd.spec.message":
3712
+ await this.onMessage(String(msg.payload?.text ?? ""));
3713
+ break;
3714
+ case "sdd.spec.approve":
3715
+ await this.onApprove();
3716
+ break;
3717
+ case "sdd.spec.get":
3718
+ if (this.driver) this.broadcast(this.snapshotMsg());
3719
+ break;
3720
+ case "sdd.run.start":
3721
+ await this.onRunStart({
3722
+ parallelSlots: msg.payload?.parallelSlots,
3723
+ defaultModel: msg.payload?.model,
3724
+ defaultProvider: msg.payload?.provider,
3725
+ fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
3726
+ });
3727
+ break;
3728
+ }
3729
+ } catch (err) {
3730
+ this.busy = false;
3731
+ this.broadcast({
3732
+ type: "sdd.spec.error",
3733
+ payload: { message: err instanceof Error ? err.message : String(err) }
3734
+ });
3735
+ }
3736
+ }
3737
+ // ── message handlers ──────────────────────────────────────────────────────
3738
+ async onStart(goal) {
3739
+ if (!goal) {
3740
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "A goal is required." } });
3741
+ return;
3742
+ }
3743
+ if (this.busy) return;
3744
+ this.driver = this.deps.makeDriver();
3745
+ const prompt = this.driver.start(goal);
3746
+ await this.runTurn(prompt);
3747
+ }
3748
+ async onMessage(text) {
3749
+ if (!this.driver || this.busy) return;
3750
+ if (this.driver.phase() === "questioning" && this.lastAgentText) {
3751
+ this.driver.submitAnswer(this.lastAgentText, text);
3752
+ } else {
3753
+ this.driver.submitAnswer(this.lastAgentText || "(feedback)", text);
3754
+ }
3755
+ await this.runTurn(this.driver.currentPrompt());
3756
+ }
3757
+ async onApprove() {
3758
+ if (!this.driver || this.busy) return;
3759
+ const { phase, prompt } = await this.driver.approve();
3760
+ if (phase === "executing") {
3761
+ this.broadcast(this.snapshotMsg());
3762
+ return;
3763
+ }
3764
+ await this.runTurn(prompt);
3765
+ }
3766
+ async onRunStart(opts) {
3767
+ if (!this.driver) {
3768
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "No active spec session." } });
3769
+ return;
3770
+ }
3771
+ const graph = await this.driver.ensureTaskGraph();
3772
+ if (!graph) {
3773
+ this.broadcast({
3774
+ type: "sdd.spec.error",
3775
+ payload: { message: "No spec yet \u2014 finish the interview before starting a run." }
3776
+ });
3777
+ return;
3778
+ }
3779
+ const { runId } = await this.deps.startRun(this.driver, opts);
3780
+ this.broadcast({ type: "sdd.run.started", payload: { runId } });
3781
+ }
3782
+ // ── internals ───────────────────────────────────────────────────────────
3783
+ /** Run one interview turn against the isolated agent, then ingest + broadcast. */
3784
+ async runTurn(prompt) {
3785
+ this.busy = true;
3786
+ this.broadcast(this.snapshotMsg());
3787
+ try {
3788
+ const text = await this.deps.runInterviewTurn(prompt);
3789
+ this.lastAgentText = text;
3790
+ if (this.driver) await this.driver.ingestAgentOutput(text);
3791
+ this.broadcast({ type: "sdd.spec.agent_text", payload: { text } });
3792
+ } finally {
3793
+ this.busy = false;
3794
+ this.broadcast(this.snapshotMsg());
3795
+ }
3796
+ }
3797
+ snapshotMsg() {
3798
+ const snap = this.driver?.snapshot();
3799
+ return {
3800
+ type: "sdd.spec.snapshot",
3801
+ payload: { ...snap, busy: this.busy }
3802
+ };
3803
+ }
3804
+ broadcast(msg) {
3805
+ const data = JSON.stringify(msg);
3806
+ for (const client of this.clients) {
3807
+ if (client.ws.readyState === 1) client.ws.send(data);
3808
+ }
3809
+ }
3810
+ send(client, msg) {
3811
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3812
+ }
3813
+ };
3814
+
3815
+ // src/server/sdd-wizard-wiring.ts
3816
+ import * as path7 from "path";
3817
+ import { spawnSync as spawnSync2 } from "child_process";
3818
+ import {
3819
+ makeCommandVerifier,
3820
+ makeLlmSubtaskGenerator,
3821
+ SddBoardStore as SddBoardStore2,
3822
+ SddInterviewDriver,
3823
+ SddRunRegistry,
3824
+ SddSupervisor,
3825
+ SpecStore as SpecStore2,
3826
+ startSddRun,
3827
+ TaskGraphStore as TaskGraphStore2,
3828
+ WorktreeManager as WorktreeManager2
3829
+ } from "@wrongstack/core";
3830
+ function buildSddWizardDeps(opts) {
3831
+ const registry = new SddRunRegistry();
3832
+ let isolatedSeq = 0;
3833
+ const runIsolatedTurn = async (prompt, name2) => {
3834
+ const result = await opts.subagentFactory({
3835
+ id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
3836
+ role: "executor",
3837
+ name: name2,
3838
+ disabledTools: ["delegate"],
3839
+ allowedCapabilities: ["fs.read", "net.outbound"]
3840
+ });
3841
+ try {
3842
+ const res = await result.agent.run([{ type: "text", text: prompt }]);
3843
+ return res.finalText ?? "";
3844
+ } finally {
3845
+ await result.dispose?.();
3846
+ }
3847
+ };
3848
+ return {
3849
+ makeDriver: () => new SddInterviewDriver({
3850
+ specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
3851
+ graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
3852
+ sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
3853
+ }),
3854
+ runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
3855
+ startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
3856
+ const graph = driver.getGraph();
3857
+ const tracker = driver.getTracker();
3858
+ if (!graph || !tracker) {
3859
+ throw new Error("No task graph to run \u2014 finish the interview first.");
3860
+ }
3861
+ let worktrees;
3862
+ if (process.env["WRONGSTACK_SDD_WORKTREES"] !== "0") {
3863
+ const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
3864
+ cwd: opts.projectRoot,
3865
+ encoding: "utf8",
3866
+ windowsHide: true
3867
+ }).stdout?.trim() === "true";
3868
+ if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
3869
+ }
3870
+ const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
3871
+ const verifyTask = makeCommandVerifier();
3872
+ const superviseFailure = opts.brain ? new SddSupervisor({
3873
+ brain: opts.brain,
3874
+ // The run-level fallback chain (chosen in the wizard) doubles as the
3875
+ // supervisor's reassign options — a `reassign` verdict rotates the
3876
+ // worker model on retry. Empty/undefined → reassign option dropped.
3877
+ reassignModels: fallbackModels,
3878
+ // LLM auto-split: decompose a retry-exhausted task into smaller
3879
+ // sub-tasks on an isolated read-only turn. Heavily validated +
3880
+ // bounded; an empty result degrades the split into a retry.
3881
+ generateSubtasks: makeLlmSubtaskGenerator({
3882
+ run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
3883
+ }),
3884
+ // The standalone brain is a tiered policy→LLM arbiter with NO
3885
+ // human-escalation wrapper (see index.ts), so it never blocks on a
3886
+ // human prompt — an unresolved verdict degrades to a bounded retry.
3887
+ // Safe to let the LLM layer actually pick reassign/split.
3888
+ requestLlmVerdict: true
3889
+ }).superviseFailure : void 0;
3890
+ const handle = startSddRun({
3891
+ tracker,
3892
+ graph,
3893
+ agent: opts.agent,
3894
+ projectRoot: opts.projectRoot,
3895
+ events: opts.events,
3896
+ subagentFactory: opts.subagentFactory,
3897
+ worktrees,
3898
+ boardStore,
3899
+ registry,
3900
+ parallelSlots,
3901
+ defaultModel,
3902
+ defaultProvider,
3903
+ fallbackModels,
3904
+ verifyTask,
3905
+ superviseFailure
3906
+ });
3907
+ void handle.completion.catch(() => {
3908
+ });
3909
+ return { runId: handle.runId };
3910
+ }
3911
+ };
3912
+ }
3913
+
3914
+ // src/server/sdd-wizard-routes.ts
3915
+ async function handleSddWizardRoute(_ws, msg, handlers) {
3916
+ if (!(msg.type.startsWith("sdd.spec.") || msg.type.startsWith("sdd.run."))) return false;
3917
+ await handlers.handleMessage(msg);
3918
+ return true;
3919
+ }
3920
+
3273
3921
  // src/server/collaboration-ws-handler.ts
3274
3922
  import { randomUUID } from "crypto";
3275
3923
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -3996,16 +4644,16 @@ var CollaborationWebSocketHandler = class {
3996
4644
  };
3997
4645
 
3998
4646
  // src/server/projects-manifest.ts
3999
- import * as fs5 from "fs/promises";
4000
- import * as path6 from "path";
4647
+ import * as fs6 from "fs/promises";
4648
+ import * as path8 from "path";
4001
4649
  import { projectSlug } from "@wrongstack/core";
4002
4650
  function projectsJsonPath(globalConfigPath) {
4003
- const base = path6.dirname(globalConfigPath);
4004
- return path6.join(base, "projects.json");
4651
+ const base = path8.dirname(globalConfigPath);
4652
+ return path8.join(base, "projects.json");
4005
4653
  }
4006
4654
  async function loadManifest(globalConfigPath) {
4007
4655
  try {
4008
- const raw = await fs5.readFile(projectsJsonPath(globalConfigPath), "utf8");
4656
+ const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
4009
4657
  const parsed = JSON.parse(raw);
4010
4658
  return { projects: parsed.projects ?? [] };
4011
4659
  } catch {
@@ -4014,16 +4662,16 @@ async function loadManifest(globalConfigPath) {
4014
4662
  }
4015
4663
  async function saveManifest(manifest, globalConfigPath) {
4016
4664
  const file = projectsJsonPath(globalConfigPath);
4017
- await fs5.mkdir(path6.dirname(file), { recursive: true });
4018
- 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");
4019
4667
  }
4020
4668
  function generateProjectSlug(rootPath) {
4021
4669
  return projectSlug(rootPath);
4022
4670
  }
4023
4671
  async function ensureProjectDataDir(slug, globalConfigPath) {
4024
- const base = path6.dirname(globalConfigPath);
4025
- const dir = path6.join(base, "projects", slug);
4026
- 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 });
4027
4675
  return dir;
4028
4676
  }
4029
4677
 
@@ -4449,14 +5097,14 @@ function registerShutdownHandlers(res) {
4449
5097
 
4450
5098
  // src/server/instance-registry.ts
4451
5099
  import * as os from "os";
4452
- import * as path7 from "path";
4453
- import * as fs6 from "fs/promises";
5100
+ import * as path9 from "path";
5101
+ import * as fs7 from "fs/promises";
4454
5102
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
4455
5103
  function defaultBaseDir() {
4456
- return path7.join(os.homedir(), ".wrongstack");
5104
+ return path9.join(os.homedir(), ".wrongstack");
4457
5105
  }
4458
5106
  function registryPath(baseDir = defaultBaseDir()) {
4459
- return path7.join(baseDir, "webui-instances.json");
5107
+ return path9.join(baseDir, "webui-instances.json");
4460
5108
  }
4461
5109
  function isPidAlive(pid) {
4462
5110
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -4469,7 +5117,7 @@ function isPidAlive(pid) {
4469
5117
  }
4470
5118
  async function load(file) {
4471
5119
  try {
4472
- const raw = await fs6.readFile(file, "utf8");
5120
+ const raw = await fs7.readFile(file, "utf8");
4473
5121
  const parsed = JSON.parse(raw);
4474
5122
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
4475
5123
  return parsed;
@@ -4578,9 +5226,10 @@ function openBrowser(url, platform = process.platform) {
4578
5226
  if (child.pid) {
4579
5227
  try {
4580
5228
  import("@wrongstack/tools").then(({ getProcessRegistry }) => {
5229
+ const pid = child.pid;
5230
+ if (pid === void 0) return;
4581
5231
  getProcessRegistry().register({
4582
- // biome-ignore lint/style/noNonNullAssertion: pid always present after spawn
4583
- pid: child.pid,
5232
+ pid,
4584
5233
  name: "browser",
4585
5234
  command: `${command} ${args.join(" ")}`,
4586
5235
  startedAt: Date.now(),
@@ -4588,7 +5237,7 @@ function openBrowser(url, platform = process.platform) {
4588
5237
  protected: true
4589
5238
  });
4590
5239
  child.on("exit", () => {
4591
- getProcessRegistry().unregister(child.pid);
5240
+ getProcessRegistry().unregister(pid);
4592
5241
  });
4593
5242
  }).catch(() => {
4594
5243
  });
@@ -4613,19 +5262,19 @@ function computeUsageCost(usage, rates) {
4613
5262
  }
4614
5263
 
4615
5264
  // src/server/provider-handlers.ts
4616
- import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core";
5265
+ import { DefaultSecretScrubber } from "@wrongstack/core";
4617
5266
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
4618
5267
 
4619
5268
  // src/server/provider-config-io.ts
4620
- import * as fs7 from "fs/promises";
4621
- import * as path8 from "path";
5269
+ import * as fs8 from "fs/promises";
5270
+ import * as path10 from "path";
4622
5271
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
4623
5272
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
4624
5273
  import { DefaultSecretVault } from "@wrongstack/core";
4625
5274
  async function loadSavedProviders(configPath, vault) {
4626
5275
  let raw;
4627
5276
  try {
4628
- raw = await fs7.readFile(configPath, "utf8");
5277
+ raw = await fs8.readFile(configPath, "utf8");
4629
5278
  } catch {
4630
5279
  return {};
4631
5280
  }
@@ -4642,7 +5291,7 @@ async function saveProviders(configPath, vault, providers) {
4642
5291
  let raw;
4643
5292
  let fileExists = true;
4644
5293
  try {
4645
- raw = await fs7.readFile(configPath, "utf8");
5294
+ raw = await fs8.readFile(configPath, "utf8");
4646
5295
  } catch (err) {
4647
5296
  if (err.code !== "ENOENT") {
4648
5297
  throw new Error(
@@ -4791,7 +5440,7 @@ function projectSavedProviders(providers) {
4791
5440
  return view;
4792
5441
  });
4793
5442
  }
4794
- var probeScrubber = new DefaultSecretScrubber2();
5443
+ var probeScrubber = new DefaultSecretScrubber();
4795
5444
  function createProviderHandlers(deps2) {
4796
5445
  const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
4797
5446
  let configWriteLock = deps2.getConfigWriteLock();
@@ -4816,7 +5465,10 @@ function createProviderHandlers(deps2) {
4816
5465
  try {
4817
5466
  const providers = await loadConfigProviders();
4818
5467
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
4819
- if (result.ok) await saveConfigProviders(providers);
5468
+ if (result.ok) {
5469
+ await saveConfigProviders(providers);
5470
+ broadcastSaved(providers);
5471
+ }
4820
5472
  sendResult2(ws, result.ok, result.message);
4821
5473
  } catch (err) {
4822
5474
  sendResult2(ws, false, errMessage(err));
@@ -4826,7 +5478,10 @@ function createProviderHandlers(deps2) {
4826
5478
  try {
4827
5479
  const providers = await loadConfigProviders();
4828
5480
  const result = deleteKey(providers, providerId, label);
4829
- if (result.ok) await saveConfigProviders(providers);
5481
+ if (result.ok) {
5482
+ await saveConfigProviders(providers);
5483
+ broadcastSaved(providers);
5484
+ }
4830
5485
  sendResult2(ws, result.ok, result.message);
4831
5486
  } catch (err) {
4832
5487
  sendResult2(ws, false, errMessage(err));
@@ -4836,7 +5491,10 @@ function createProviderHandlers(deps2) {
4836
5491
  try {
4837
5492
  const providers = await loadConfigProviders();
4838
5493
  const result = setActiveKey(providers, providerId, label);
4839
- if (result.ok) await saveConfigProviders(providers);
5494
+ if (result.ok) {
5495
+ await saveConfigProviders(providers);
5496
+ broadcastSaved(providers);
5497
+ }
4840
5498
  sendResult2(ws, result.ok, result.message);
4841
5499
  } catch (err) {
4842
5500
  sendResult2(ws, false, errMessage(err));
@@ -4846,11 +5504,13 @@ function createProviderHandlers(deps2) {
4846
5504
  try {
4847
5505
  const providers = await loadConfigProviders();
4848
5506
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
4849
- if (result.ok) await saveConfigProviders(providers);
5507
+ if (result.ok) {
5508
+ await saveConfigProviders(providers);
5509
+ broadcastSaved(providers);
5510
+ }
4850
5511
  sendResult2(ws, result.ok, result.message);
4851
5512
  if (result.ok) {
4852
5513
  console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
4853
- broadcastSaved(providers);
4854
5514
  }
4855
5515
  } catch (err) {
4856
5516
  sendResult2(ws, false, errMessage(err));
@@ -4860,7 +5520,10 @@ function createProviderHandlers(deps2) {
4860
5520
  try {
4861
5521
  const providers = await loadConfigProviders();
4862
5522
  const result = removeProvider(providers, providerId);
4863
- if (result.ok) await saveConfigProviders(providers);
5523
+ if (result.ok) {
5524
+ await saveConfigProviders(providers);
5525
+ broadcastSaved(providers);
5526
+ }
4864
5527
  sendResult2(ws, result.ok, result.message);
4865
5528
  } catch (err) {
4866
5529
  sendResult2(ws, false, errMessage(err));
@@ -4966,7 +5629,7 @@ function createProviderHandlers(deps2) {
4966
5629
 
4967
5630
  // src/server/mode-handlers.ts
4968
5631
  import {
4969
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2
5632
+ DefaultSystemPromptBuilder
4970
5633
  } from "@wrongstack/core";
4971
5634
  function createModeHandlers(ctx) {
4972
5635
  return {
@@ -5014,7 +5677,7 @@ function createModeHandlers(ctx) {
5014
5677
  }
5015
5678
  ctx.setModeId(id);
5016
5679
  const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
5017
- const freshBuilder = new DefaultSystemPromptBuilder2({
5680
+ const freshBuilder = new DefaultSystemPromptBuilder({
5018
5681
  memoryStore: ctx.memoryStore,
5019
5682
  skillLoader: ctx.skillLoader,
5020
5683
  modeStore: ctx.modeStore,
@@ -5043,42 +5706,12 @@ function createModeHandlers(ctx) {
5043
5706
 
5044
5707
  // src/server/project-handlers.ts
5045
5708
  import * as fs9 from "fs/promises";
5046
- import * as path10 from "path";
5709
+ import * as path11 from "path";
5047
5710
  import {
5048
- DefaultSessionStore as DefaultSessionStore2,
5049
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
5711
+ DefaultSessionStore,
5712
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
5050
5713
  getSessionRegistry
5051
5714
  } from "@wrongstack/core";
5052
-
5053
- // src/server/path-containment.ts
5054
- import * as fs8 from "fs/promises";
5055
- import * as path9 from "path";
5056
- function isPathInside(root, target) {
5057
- const relative3 = path9.relative(root, target);
5058
- return relative3 === "" || !relative3.startsWith("..") && !path9.isAbsolute(relative3);
5059
- }
5060
- async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
5061
- const resolved = path9.resolve(projectRoot, inputPath);
5062
- let stat3;
5063
- try {
5064
- stat3 = await fs8.stat(resolved);
5065
- } catch {
5066
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5067
- }
5068
- if (!stat3.isDirectory()) {
5069
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5070
- }
5071
- const [realProjectRoot, realResolved] = await Promise.all([
5072
- fs8.realpath(projectRoot),
5073
- fs8.realpath(resolved)
5074
- ]);
5075
- if (!isPathInside(realProjectRoot, realResolved)) {
5076
- throw new Error(`Path must stay inside the project root: ${projectRoot}`);
5077
- }
5078
- return resolved;
5079
- }
5080
-
5081
- // src/server/project-handlers.ts
5082
5715
  function createProjectHandlers(ctx) {
5083
5716
  return {
5084
5717
  listProjects: async (ws) => {
@@ -5100,7 +5733,7 @@ function createProjectHandlers(ctx) {
5100
5733
  }
5101
5734
  const { root: addRoot, name: displayName } = parsed.value;
5102
5735
  try {
5103
- const resolved = path10.resolve(addRoot);
5736
+ const resolved = path11.resolve(addRoot);
5104
5737
  await fs9.access(resolved);
5105
5738
  const stat3 = await fs9.stat(resolved);
5106
5739
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
@@ -5118,7 +5751,7 @@ function createProjectHandlers(ctx) {
5118
5751
  });
5119
5752
  return;
5120
5753
  }
5121
- const name2 = displayName?.trim() || path10.basename(resolved);
5754
+ const name2 = displayName?.trim() || path11.basename(resolved);
5122
5755
  const slug = generateProjectSlug(resolved);
5123
5756
  await ensureProjectDataDir(slug, ctx.globalConfigPath);
5124
5757
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5131,7 +5764,7 @@ function createProjectHandlers(ctx) {
5131
5764
  } catch (err) {
5132
5765
  send(ws, {
5133
5766
  type: "projects.added",
5134
- payload: { name: path10.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5767
+ payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5135
5768
  });
5136
5769
  }
5137
5770
  },
@@ -5146,7 +5779,7 @@ function createProjectHandlers(ctx) {
5146
5779
  }
5147
5780
  const { root: selRoot, name: selName } = parsed.value;
5148
5781
  try {
5149
- const resolved = path10.resolve(selRoot);
5782
+ const resolved = path11.resolve(selRoot);
5150
5783
  try {
5151
5784
  await fs9.access(resolved);
5152
5785
  const stat3 = await fs9.stat(resolved);
@@ -5156,7 +5789,7 @@ function createProjectHandlers(ctx) {
5156
5789
  type: "projects.selected",
5157
5790
  payload: {
5158
5791
  root: selRoot,
5159
- name: selName || path10.basename(selRoot),
5792
+ name: selName || path11.basename(selRoot),
5160
5793
  message: `Cannot switch: ${errMessage(err)}`
5161
5794
  }
5162
5795
  });
@@ -5168,7 +5801,7 @@ function createProjectHandlers(ctx) {
5168
5801
  entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
5169
5802
  entry.lastWorkingDir = resolved;
5170
5803
  } else {
5171
- const name2 = selName?.trim() || path10.basename(resolved);
5804
+ const name2 = selName?.trim() || path11.basename(resolved);
5172
5805
  const slug = generateProjectSlug(resolved);
5173
5806
  manifest.projects.push({
5174
5807
  name: name2,
@@ -5190,7 +5823,7 @@ function createProjectHandlers(ctx) {
5190
5823
  try {
5191
5824
  const modeId = ctx.getModeId();
5192
5825
  const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
5193
- const switchBuilder = new DefaultSystemPromptBuilder3({
5826
+ const switchBuilder = new DefaultSystemPromptBuilder2({
5194
5827
  memoryStore: ctx.memoryStore,
5195
5828
  skillLoader: ctx.skillLoader,
5196
5829
  modeStore: ctx.modeStore,
@@ -5207,14 +5840,14 @@ function createProjectHandlers(ctx) {
5207
5840
  });
5208
5841
  } catch {
5209
5842
  }
5210
- const newSessionsDir = path10.join(
5211
- path10.dirname(ctx.globalConfigPath),
5843
+ const newSessionsDir = path11.join(
5844
+ path11.dirname(ctx.globalConfigPath),
5212
5845
  "projects",
5213
5846
  switchSlug,
5214
5847
  "sessions"
5215
5848
  );
5216
5849
  await fs9.mkdir(newSessionsDir, { recursive: true });
5217
- const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
5850
+ const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
5218
5851
  const oldSession = ctx.getSession();
5219
5852
  const oldSessionId = oldSession.id;
5220
5853
  try {
@@ -5247,7 +5880,7 @@ function createProjectHandlers(ctx) {
5247
5880
  sessionId: newSession.id,
5248
5881
  projectSlug: switchSlug,
5249
5882
  projectRoot: resolved,
5250
- projectName: path10.basename(resolved),
5883
+ projectName: path11.basename(resolved),
5251
5884
  workingDir: resolved,
5252
5885
  clientType: "webui",
5253
5886
  pid: process.pid,
@@ -5259,8 +5892,8 @@ function createProjectHandlers(ctx) {
5259
5892
  type: "projects.selected",
5260
5893
  payload: {
5261
5894
  root: resolved,
5262
- name: selName || path10.basename(resolved),
5263
- message: `Switched to ${selName || path10.basename(resolved)}`
5895
+ name: selName || path11.basename(resolved),
5896
+ message: `Switched to ${selName || path11.basename(resolved)}`
5264
5897
  }
5265
5898
  });
5266
5899
  broadcast(ctx.clients, {
@@ -5280,7 +5913,7 @@ function createProjectHandlers(ctx) {
5280
5913
  type: "projects.selected",
5281
5914
  payload: {
5282
5915
  root: selRoot,
5283
- name: selName || path10.basename(selRoot),
5916
+ name: selName || path11.basename(selRoot),
5284
5917
  message: errMessage(err)
5285
5918
  }
5286
5919
  });
@@ -5311,7 +5944,7 @@ function createProjectHandlers(ctx) {
5311
5944
  }
5312
5945
 
5313
5946
  // src/server/session-handlers.ts
5314
- import * as path11 from "path";
5947
+ import * as path12 from "path";
5315
5948
  import {
5316
5949
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
5317
5950
  repairToolUseAdjacency,
@@ -5653,7 +6286,7 @@ function createSessionHandlers(ctx) {
5653
6286
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5654
6287
  const projectRoot = ctx.getProjectRoot();
5655
6288
  const rewinder = new DefaultSessionRewinder(
5656
- path11.join(projectRoot, ".wrongstack", "sessions"),
6289
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5657
6290
  projectRoot
5658
6291
  );
5659
6292
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
@@ -5668,7 +6301,7 @@ function createSessionHandlers(ctx) {
5668
6301
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5669
6302
  const projectRoot = ctx.getProjectRoot();
5670
6303
  const rewinder = new DefaultSessionRewinder(
5671
- path11.join(projectRoot, ".wrongstack", "sessions"),
6304
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5672
6305
  projectRoot
5673
6306
  );
5674
6307
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
@@ -5911,6 +6544,22 @@ async function handleModeRoute(ws, msg, handlers) {
5911
6544
  }
5912
6545
  }
5913
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
+
5914
6563
  // src/server/shell-git-routes.ts
5915
6564
  async function handleShellGitRoute(ws, msg, handlers) {
5916
6565
  switch (msg.type) {
@@ -5951,6 +6600,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
5951
6600
  }
5952
6601
  }
5953
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
+
5954
6641
  // src/server/brain-routes.ts
5955
6642
  async function handleBrainRoute(ws, msg, handlers) {
5956
6643
  switch (msg.type) {
@@ -5975,10 +6662,24 @@ async function handleAutoPhaseRoute(_ws, msg, handlers) {
5975
6662
  return true;
5976
6663
  }
5977
6664
 
6665
+ // src/server/specs-routes.ts
6666
+ async function handleSpecsRoute(_ws, msg, handlers) {
6667
+ if (!msg.type.startsWith("specs.")) return false;
6668
+ await handlers.handleMessage(msg);
6669
+ return true;
6670
+ }
6671
+
6672
+ // src/server/sdd-board-routes.ts
6673
+ async function handleSddBoardRoute(_ws, msg, handlers) {
6674
+ if (!msg.type.startsWith("sdd.board.")) return false;
6675
+ await handlers.handleMessage(msg);
6676
+ return true;
6677
+ }
6678
+
5978
6679
  // src/server/setup-events.ts
5979
6680
  import * as fs10 from "fs/promises";
5980
6681
  import { watch as fsWatch } from "fs";
5981
- import * as path12 from "path";
6682
+ import * as path13 from "path";
5982
6683
  function setupEvents(deps2) {
5983
6684
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
5984
6685
  const disposers = [];
@@ -6408,11 +7109,13 @@ function setupEvents(deps2) {
6408
7109
  events.on("provider.response", (e) => {
6409
7110
  if (e.usage?.input != null) {
6410
7111
  const maxCtx = context.provider.capabilities.maxContext;
6411
- 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));
6412
7114
  const costUsd = context.tokenCounter.estimateCost().total;
6413
7115
  forwardSubagent("ctx_pct", {
6414
7116
  subagentId: "leader",
6415
- load: pct,
7117
+ load: load2,
7118
+ rawLoad,
6416
7119
  tokens: e.usage.input,
6417
7120
  maxContext: maxCtx,
6418
7121
  costUsd
@@ -6443,7 +7146,7 @@ function setupEvents(deps2) {
6443
7146
  if (wpaths?.projectStatus) {
6444
7147
  try {
6445
7148
  const statusFile = wpaths.projectStatus(e.projectHash);
6446
- const dir = path12.dirname(statusFile);
7149
+ const dir = path13.dirname(statusFile);
6447
7150
  await fs10.mkdir(dir, { recursive: true });
6448
7151
  await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
6449
7152
  } catch (err) {
@@ -6452,7 +7155,7 @@ function setupEvents(deps2) {
6452
7155
  }
6453
7156
  });
6454
7157
  if (wpaths?.projectStatus && wpaths.configDir) {
6455
- const projectsDir = path12.join(wpaths.configDir, "projects");
7158
+ const projectsDir = path13.join(wpaths.configDir, "projects");
6456
7159
  const knownProjectHashes = /* @__PURE__ */ new Set();
6457
7160
  const debounceTimers = /* @__PURE__ */ new Map();
6458
7161
  const DEBOUNCE_MS = 150;
@@ -6479,7 +7182,7 @@ function setupEvents(deps2) {
6479
7182
  );
6480
7183
  };
6481
7184
  const metricsInterval = setInterval(logWatcherMetrics, 6e4);
6482
- const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
7185
+ const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
6483
7186
  broadcast2(clients, { type: "client.status_update", payload: statusData });
6484
7187
  if (watcherMetrics) {
6485
7188
  watcherMetrics.broadcastsSent++;
@@ -6520,9 +7223,9 @@ function setupEvents(deps2) {
6520
7223
  if (eventType === "change") {
6521
7224
  if (filename == null) return;
6522
7225
  if (watcherMetrics) watcherMetrics.fileChangesDetected++;
6523
- const targetFile = path12.join(projectsDir, String(filename));
7226
+ const targetFile = path13.join(projectsDir, String(filename));
6524
7227
  if (targetFile.endsWith("status.json")) {
6525
- const projectHash2 = path12.basename(path12.dirname(targetFile));
7228
+ const projectHash2 = path13.basename(path13.dirname(targetFile));
6526
7229
  if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
6527
7230
  return;
6528
7231
  }
@@ -6580,7 +7283,7 @@ function setupEvents(deps2) {
6580
7283
  }
6581
7284
  });
6582
7285
  }
6583
- const globalRoot = globalConfigPath ? path12.dirname(globalConfigPath) : void 0;
7286
+ const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
6584
7287
  if (globalRoot) {
6585
7288
  const broadcastSessions = async () => {
6586
7289
  try {
@@ -6654,10 +7357,10 @@ function setupEvents(deps2) {
6654
7357
  // src/server/custom-context-modes.ts
6655
7358
  import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
6656
7359
  import * as fs11 from "fs/promises";
6657
- import * as path13 from "path";
7360
+ import * as path14 from "path";
6658
7361
  var STORE_FILENAME = "custom-context-modes.json";
6659
7362
  function storePath(wrongstackDir) {
6660
- return path13.join(wrongstackDir, STORE_FILENAME);
7363
+ return path14.join(wrongstackDir, STORE_FILENAME);
6661
7364
  }
6662
7365
  var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
6663
7366
  function createCustomModeStore(wrongstackDir) {
@@ -6789,12 +7492,12 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
6789
7492
 
6790
7493
  // src/server/shell-open.ts
6791
7494
  import * as fs12 from "fs/promises";
6792
- import * as path14 from "path";
7495
+ import * as path15 from "path";
6793
7496
  import { spawn as spawn2 } from "child_process";
6794
7497
  var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
6795
7498
  async function handleShellOpen(req, logger) {
6796
7499
  try {
6797
- const resolved = path14.resolve(req.path);
7500
+ const resolved = path15.resolve(req.path);
6798
7501
  await fs12.access(resolved);
6799
7502
  if (METACHAR_REGEX.test(resolved)) {
6800
7503
  return { success: false, message: "Path contains unsupported characters." };
@@ -6904,15 +7607,15 @@ async function handleGitChanges(ws, projectRoot) {
6904
7607
  if (!m) continue;
6905
7608
  const added = m[1] === "-" ? 0 : Number(m[1]);
6906
7609
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
6907
- let path16 = m[3] ?? "";
6908
- if (path16 === "") {
7610
+ let path17 = m[3] ?? "";
7611
+ if (path17 === "") {
6909
7612
  i += 1;
6910
- path16 = parts[i + 1] ?? parts[i] ?? "";
7613
+ path17 = parts[i + 1] ?? parts[i] ?? "";
6911
7614
  i += 1;
6912
7615
  }
6913
- if (!path16) continue;
6914
- const prev = counts.get(path16) ?? { added: 0, deleted: 0 };
6915
- counts.set(path16, { added: prev.added + added, deleted: prev.deleted + deleted });
7616
+ if (!path17) continue;
7617
+ const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
7618
+ counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
6916
7619
  }
6917
7620
  };
6918
7621
  parseNumstat(unstagedNumstat);
@@ -6924,7 +7627,7 @@ async function handleGitChanges(ws, projectRoot) {
6924
7627
  if (!rec || rec.length < 3) continue;
6925
7628
  const x = rec[0] ?? " ";
6926
7629
  const y = rec[1] ?? " ";
6927
- const path16 = rec.slice(3);
7630
+ const path17 = rec.slice(3);
6928
7631
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
6929
7632
  if (isRename) i += 1;
6930
7633
  let status;
@@ -6936,13 +7639,13 @@ async function handleGitChanges(ws, projectRoot) {
6936
7639
  else if (x === "D" || y === "D") status = "D";
6937
7640
  else status = "M";
6938
7641
  const staged = x !== " " && x !== "?";
6939
- let added = counts.get(path16)?.added ?? 0;
6940
- let deleted = counts.get(path16)?.deleted ?? 0;
7642
+ let added = counts.get(path17)?.added ?? 0;
7643
+ let deleted = counts.get(path17)?.deleted ?? 0;
6941
7644
  if (status === "?") {
6942
7645
  added = 0;
6943
7646
  deleted = 0;
6944
7647
  }
6945
- files.push({ path: path16, status, added, deleted, staged });
7648
+ files.push({ path: path17, status, added, deleted, staged });
6946
7649
  }
6947
7650
  send(ws, { type: "git.changes", payload: { files } });
6948
7651
  } catch (err) {
@@ -6953,21 +7656,21 @@ async function handleGitChanges(ws, projectRoot) {
6953
7656
  }
6954
7657
  }
6955
7658
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
6956
- async function handleGitDiff(ws, projectRoot, path16) {
7659
+ async function handleGitDiff(ws, projectRoot, path17) {
6957
7660
  const cwd = projectRoot || void 0;
6958
- const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path16, ...extra } });
6959
- if (!path16 || path16.includes("\0") || path16.includes("..") || nodePath.isAbsolute(path16)) {
7661
+ const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
7662
+ if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
6960
7663
  reply({ oldText: "", newText: "", error: "invalid path" });
6961
7664
  return;
6962
7665
  }
6963
7666
  try {
6964
7667
  const git = makeGit(cwd);
6965
7668
  const { readFile: readFile9 } = await import("fs/promises");
6966
- const { join: join11 } = await import("path");
6967
- const oldText = await git(["show", `HEAD:${path16}`]);
7669
+ const { join: join12 } = await import("path");
7670
+ const oldText = await git(["show", `HEAD:${path17}`]);
6968
7671
  let newText = "";
6969
7672
  try {
6970
- const abs = cwd ? join11(cwd, path16) : path16;
7673
+ const abs = cwd ? join12(cwd, path17) : path17;
6971
7674
  const buf = await readFile9(abs);
6972
7675
  if (buf.includes(0)) {
6973
7676
  reply({ oldText: "", newText: "", binary: true });
@@ -7063,6 +7766,7 @@ async function handleGoalGet(projectRoot, broadcast2) {
7063
7766
 
7064
7767
  // src/server/index.ts
7065
7768
  async function startWebUI(opts = {}) {
7769
+ ensureSessionShell();
7066
7770
  const requestedWsPort = opts.wsPort ?? 3457;
7067
7771
  const wsHost = opts.wsHost ?? "127.0.0.1";
7068
7772
  const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
@@ -7144,7 +7848,7 @@ async function startWebUI(opts = {}) {
7144
7848
  ttlSeconds: 24 * 3600
7145
7849
  });
7146
7850
  const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
7147
- const configStore = opts.services?.configStore ?? container.resolve(TOKENS2.ConfigStore);
7851
+ const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
7148
7852
  const providerRegistry = new ProviderRegistry();
7149
7853
  try {
7150
7854
  const factories = await buildProviderFactoriesFromRegistry({
@@ -7166,7 +7870,7 @@ async function startWebUI(opts = {}) {
7166
7870
  r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
7167
7871
  return r;
7168
7872
  })();
7169
- const memoryStore = new DefaultMemoryStore2({ paths: wpaths });
7873
+ const memoryStore = new DefaultMemoryStore({ paths: wpaths });
7170
7874
  if (config.features.memory) {
7171
7875
  toolRegistry.register(rememberTool(memoryStore));
7172
7876
  toolRegistry.register(forgetTool(memoryStore));
@@ -7178,6 +7882,8 @@ async function startWebUI(opts = {}) {
7178
7882
  toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
7179
7883
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
7180
7884
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
7885
+ applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
7886
+ configureExecPolicy(config.tools?.exec ?? {});
7181
7887
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7182
7888
  const mcpRegistry = new MCPRegistry({
7183
7889
  toolRegistry,
@@ -7194,7 +7900,7 @@ async function startWebUI(opts = {}) {
7194
7900
  });
7195
7901
  }
7196
7902
  }
7197
- let sessionStore = opts.services?.session ?? new DefaultSessionStore3({ dir: wpaths.projectSessions });
7903
+ let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
7198
7904
  if (!opts.services?.session) {
7199
7905
  sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
7200
7906
  if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
@@ -7221,7 +7927,7 @@ async function startWebUI(opts = {}) {
7221
7927
  sessionId: session.id,
7222
7928
  projectSlug: wpaths.projectSlug,
7223
7929
  projectRoot,
7224
- projectName: path15.basename(projectRoot),
7930
+ projectName: path16.basename(projectRoot),
7225
7931
  workingDir,
7226
7932
  clientType: "webui",
7227
7933
  pid: process.pid,
@@ -7241,7 +7947,7 @@ async function startWebUI(opts = {}) {
7241
7947
  const hqTelemetry = createHqPublisherFromEnv({
7242
7948
  clientKind: "webui",
7243
7949
  projectRoot,
7244
- projectName: path15.basename(projectRoot),
7950
+ projectName: path16.basename(projectRoot),
7245
7951
  appConfig: config,
7246
7952
  socketFactory: (url) => new WebSocket2(url)
7247
7953
  });
@@ -7253,7 +7959,7 @@ async function startWebUI(opts = {}) {
7253
7959
  events,
7254
7960
  sessionId: session.id,
7255
7961
  projectRoot,
7256
- projectName: path15.basename(projectRoot),
7962
+ projectName: path16.basename(projectRoot),
7257
7963
  globalRoot: wpaths.globalRoot,
7258
7964
  initialAgents: statusTracker?.getAgents(),
7259
7965
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -7282,11 +7988,11 @@ async function startWebUI(opts = {}) {
7282
7988
  });
7283
7989
  } catch {
7284
7990
  }
7285
- const tokenCounter = new DefaultTokenCounter2({
7991
+ const tokenCounter = new DefaultTokenCounter({
7286
7992
  registry: modelsRegistry,
7287
7993
  providerId: config.provider
7288
7994
  });
7289
- const modeStore = new DefaultModeStore2({ directory: wpaths.configDir });
7995
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7290
7996
  const activeMode = await modeStore.getActiveMode();
7291
7997
  let modeId = activeMode?.id ?? "default";
7292
7998
  const modePrompt = activeMode?.prompt ?? "";
@@ -7307,15 +8013,15 @@ async function startWebUI(opts = {}) {
7307
8013
  const modelCapabilitiesRef = {
7308
8014
  current: modelCapabilities
7309
8015
  };
7310
- const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
8016
+ const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7311
8017
  const skillInstaller = config.features.skills ? new SkillInstaller({
7312
- manifestPath: path15.join(wstackGlobalRoot2(), "installed-skills.json"),
7313
- projectSkillsDir: path15.join(projectRoot, ".wrongstack", "skills"),
7314
- globalSkillsDir: path15.join(wstackGlobalRoot2(), "skills"),
8018
+ manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
8019
+ projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
8020
+ globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
7315
8021
  projectHash: projectHash(projectRoot),
7316
8022
  skillLoader
7317
8023
  }) : void 0;
7318
- const systemPromptBuilder = new DefaultSystemPromptBuilder4({
8024
+ const systemPromptBuilder = new DefaultSystemPromptBuilder3({
7319
8025
  memoryStore,
7320
8026
  skillLoader,
7321
8027
  modeStore,
@@ -7415,6 +8121,8 @@ async function startWebUI(opts = {}) {
7415
8121
  context.meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
7416
8122
  context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
7417
8123
  context.meta["nextPrediction"] = config.nextPrediction ?? false;
8124
+ context.meta["fallbackModels"] = config.fallbackModels ?? [];
8125
+ context.meta["fallbackAuto"] = config.fallbackAuto !== false;
7418
8126
  context.meta["featureMcp"] = config.features.mcp !== false;
7419
8127
  context.meta["featurePlugins"] = config.features.plugins !== false;
7420
8128
  context.meta["featureMemory"] = config.features.memory !== false;
@@ -7472,7 +8180,9 @@ async function startWebUI(opts = {}) {
7472
8180
  "reasoningMode",
7473
8181
  "reasoningEffort",
7474
8182
  "reasoningPreserve",
7475
- "cacheTtl"
8183
+ "cacheTtl",
8184
+ "fallbackModels",
8185
+ "fallbackAuto"
7476
8186
  ];
7477
8187
  const prefSnapshot = () => {
7478
8188
  const snapshot = {};
@@ -7503,6 +8213,8 @@ async function startWebUI(opts = {}) {
7503
8213
  if (typeof payload["enhanceLanguage"] === "string") setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
7504
8214
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
7505
8215
  if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
8216
+ if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
8217
+ if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
7506
8218
  const FEATURE_MAP = {
7507
8219
  featureMcp: "mcp",
7508
8220
  featurePlugins: "plugins",
@@ -7599,7 +8311,7 @@ async function startWebUI(opts = {}) {
7599
8311
  projectRoot,
7600
8312
  logger
7601
8313
  });
7602
- const compactor = createStrategyCompactor2({
8314
+ const compactor = createStrategyCompactor({
7603
8315
  strategy: config.context?.strategy,
7604
8316
  preserveK: config.context?.preserveK ?? 10,
7605
8317
  eliseThreshold: config.context?.eliseThreshold ?? 2e3,
@@ -7675,9 +8387,9 @@ async function startWebUI(opts = {}) {
7675
8387
  maxContext: newMaxContext
7676
8388
  });
7677
8389
  }
7678
- const secretScrubber = container.resolve(TOKENS2.SecretScrubber);
7679
- const renderer = container.has(TOKENS2.Renderer) ? container.resolve(TOKENS2.Renderer) : void 0;
7680
- 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);
7681
8393
  const toolExecutor = new ToolExecutor(toolRegistry, {
7682
8394
  permissionPolicy,
7683
8395
  secretScrubber,
@@ -7720,7 +8432,7 @@ async function startWebUI(opts = {}) {
7720
8432
  }),
7721
8433
  events
7722
8434
  );
7723
- container.bind(TOKENS2.BrainArbiter, () => brain);
8435
+ container.bind(TOKENS.BrainArbiter, () => brain);
7724
8436
  const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
7725
8437
  const brainMonitor = new BrainMonitor({
7726
8438
  events,
@@ -7783,6 +8495,29 @@ async function startWebUI(opts = {}) {
7783
8495
  events,
7784
8496
  projectRoot
7785
8497
  );
8498
+ const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
8499
+ const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
8500
+ const sddWizardHandler = new SddWizardWebSocketHandler(
8501
+ buildSddWizardDeps({
8502
+ agent,
8503
+ events,
8504
+ projectRoot,
8505
+ brain,
8506
+ subagentFactory: makeLightSubagentFactory({
8507
+ container,
8508
+ providerRegistry,
8509
+ toolRegistry,
8510
+ session,
8511
+ projectRoot
8512
+ }),
8513
+ paths: {
8514
+ projectSpecs: wpaths.projectSpecs,
8515
+ projectTaskGraphs: wpaths.projectTaskGraphs,
8516
+ projectSddBoards: wpaths.projectSddBoards,
8517
+ projectDir: wpaths.projectDir
8518
+ }
8519
+ })
8520
+ );
7786
8521
  const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
7787
8522
  const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
7788
8523
  const collabHandler = new CollaborationWebSocketHandler(
@@ -7822,7 +8557,7 @@ async function startWebUI(opts = {}) {
7822
8557
  inputCost,
7823
8558
  outputCost,
7824
8559
  cacheReadCost,
7825
- projectName: path15.basename(projectRoot) || projectRoot,
8560
+ projectName: path16.basename(projectRoot) || projectRoot,
7826
8561
  projectRoot,
7827
8562
  cwd: workingDir,
7828
8563
  mode: modeId,
@@ -7914,6 +8649,9 @@ async function startWebUI(opts = {}) {
7914
8649
  }));
7915
8650
  });
7916
8651
  autoPhaseHandler.addClient(ws);
8652
+ specsHandler.addClient(ws);
8653
+ sddBoardHandler.addClient(ws);
8654
+ sddWizardHandler.addClient(ws);
7917
8655
  worktreeHandler.addClient(ws);
7918
8656
  collabHandler.addClient(ws);
7919
8657
  terminalHandler.addClient(ws);
@@ -8038,21 +8776,21 @@ async function startWebUI(opts = {}) {
8038
8776
  });
8039
8777
  }
8040
8778
  async function touchProjectEntry(root, workDir) {
8041
- const resolved = path15.resolve(root);
8779
+ const resolved = path16.resolve(root);
8042
8780
  const manifest = await loadManifest(globalConfigPath);
8043
8781
  const now = (/* @__PURE__ */ new Date()).toISOString();
8044
- const existing = manifest.projects.find((p) => path15.resolve(p.root) === resolved);
8782
+ const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
8045
8783
  if (existing) {
8046
8784
  existing.lastSeen = now;
8047
- if (workDir) existing.lastWorkingDir = path15.resolve(workDir);
8785
+ if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
8048
8786
  } else {
8049
8787
  manifest.projects.push({
8050
- name: path15.basename(resolved),
8788
+ name: path16.basename(resolved),
8051
8789
  root: resolved,
8052
8790
  slug: generateProjectSlug(resolved),
8053
8791
  createdAt: now,
8054
8792
  lastSeen: now,
8055
- lastWorkingDir: workDir ? path15.resolve(workDir) : void 0
8793
+ lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
8056
8794
  });
8057
8795
  }
8058
8796
  await saveManifest(manifest, globalConfigPath);
@@ -8074,19 +8812,29 @@ async function startWebUI(opts = {}) {
8074
8812
  let sessionRoutes;
8075
8813
  let projectRoutes;
8076
8814
  let modeRoutes;
8815
+ let prefsRoutes;
8077
8816
  let shellGitRoutes;
8078
8817
  let mailboxRoutes;
8818
+ let mcpRoutes;
8079
8819
  let brainRoutes;
8080
8820
  let autoPhaseRoutes;
8821
+ let specsRoutes;
8822
+ let sddBoardRoutes;
8823
+ let sddWizardRoutes;
8081
8824
  async function handleMessage(ws, _client, msg) {
8082
8825
  if (await handleProviderRoute(ws, msg, providerRoutes)) return;
8083
8826
  if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
8084
8827
  if (await handleProjectRoute(ws, msg, projectRoutes)) return;
8085
8828
  if (await handleModeRoute(ws, msg, modeRoutes)) return;
8829
+ if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
8086
8830
  if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
8087
8831
  if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
8832
+ if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
8088
8833
  if (await handleBrainRoute(ws, msg, brainRoutes)) return;
8089
8834
  if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
8835
+ if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
8836
+ if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
8837
+ if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
8090
8838
  switch (msg.type) {
8091
8839
  // Collaboration messages short-circuit the user/agent flow.
8092
8840
  // They don't touch runLock, the agent loop, or the message queue —
@@ -8192,27 +8940,31 @@ async function startWebUI(opts = {}) {
8192
8940
  case "memory.forget":
8193
8941
  return handleMemoryForget(ws, msg, memoryStore);
8194
8942
  // ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
8195
- // 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.
8196
8948
  case "mcp.list":
8197
- return handleMcpList(ws, msg, globalConfigPath, mcpRegistry);
8949
+ throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
8198
8950
  case "mcp.add":
8199
- return handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry);
8200
- case "mcp.remove":
8201
- return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
8951
+ throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
8202
8952
  case "mcp.update":
8203
- return handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry);
8204
- case "mcp.wake":
8205
- return handleMcpWake(ws, msg, globalConfigPath, mcpRegistry);
8206
- case "mcp.sleep":
8207
- return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
8208
- case "mcp.discover":
8209
- 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");
8210
8956
  case "mcp.enable":
8211
- return handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry);
8957
+ throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
8212
8958
  case "mcp.disable":
8213
- 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");
8214
8964
  case "mcp.restart":
8215
- 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");
8216
8968
  // Skills — full request→response cycle lives in skills-handlers.ts
8217
8969
  // (shared with the CLI's embedded server). skillsCtx is the closed-over
8218
8970
  // loader/installer/projectRoot the handlers need.
@@ -8360,49 +9112,11 @@ async function startWebUI(opts = {}) {
8360
9112
  break;
8361
9113
  }
8362
9114
  case "prefs.update": {
8363
- const parsed = validatePrefsUpdatePayload(msg.payload);
8364
- if (!parsed.ok) {
8365
- sendResult2(ws, false, parsed.message);
8366
- break;
8367
- }
8368
- const payload = parsed.value.prefs;
8369
- for (const [key, val] of Object.entries(payload)) {
8370
- context.meta[key] = val;
8371
- }
8372
- void persistPrefsToConfig(payload);
8373
- if (typeof payload["yolo"] === "boolean") {
8374
- permissionPolicy.setYolo?.(payload["yolo"]);
8375
- }
8376
- if (typeof payload["featureMcp"] === "boolean")
8377
- config.features.mcp = payload["featureMcp"];
8378
- if (typeof payload["featurePlugins"] === "boolean")
8379
- config.features.plugins = payload["featurePlugins"];
8380
- if (typeof payload["featureMemory"] === "boolean")
8381
- config.features.memory = payload["featureMemory"];
8382
- if (typeof payload["featureSkills"] === "boolean")
8383
- config.features.skills = payload["featureSkills"];
8384
- if (typeof payload["featureModelsRegistry"] === "boolean")
8385
- config.features.modelsRegistry = payload["featureModelsRegistry"];
8386
- if (typeof payload["contextAutoCompact"] === "boolean") {
8387
- if (payload["contextAutoCompact"] && autoCompactor) {
8388
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
8389
- pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
8390
- } else {
8391
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
8392
- }
8393
- }
8394
- if (typeof payload["logLevel"] === "string") {
8395
- const valid = ["debug", "info", "warn", "error"];
8396
- if (valid.includes(payload["logLevel"])) {
8397
- logger.level = payload["logLevel"];
8398
- }
8399
- }
8400
- broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
8401
- break;
9115
+ void ws;
9116
+ throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
8402
9117
  }
8403
9118
  case "prefs.get": {
8404
- send(ws, { type: "prefs.updated", payload: prefSnapshot() });
8405
- break;
9119
+ throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
8406
9120
  }
8407
9121
  default:
8408
9122
  send(ws, {
@@ -8445,22 +9159,7 @@ async function startWebUI(opts = {}) {
8445
9159
  const saved = await providerHandlers.loadConfigProviders();
8446
9160
  send(ws, {
8447
9161
  type: "providers.saved",
8448
- payload: {
8449
- providers: Object.entries(saved).map(([id, cfg]) => {
8450
- const keys = normalizeKeys(cfg);
8451
- return {
8452
- id,
8453
- family: cfg.family ?? id,
8454
- baseUrl: cfg.baseUrl,
8455
- apiKeys: keys.map((k) => ({
8456
- label: k.label,
8457
- maskedKey: maskedKey(k.apiKey),
8458
- isActive: k.label === cfg.activeKey,
8459
- createdAt: k.createdAt
8460
- }))
8461
- };
8462
- })
8463
- }
9162
+ payload: { providers: projectSavedProviders(saved) }
8464
9163
  });
8465
9164
  },
8466
9165
  listProviderModels: async (ws, msg) => {
@@ -8638,6 +9337,55 @@ async function startWebUI(opts = {}) {
8638
9337
  },
8639
9338
  sessionStartPayload
8640
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
+ };
8641
9389
  shellGitRoutes = {
8642
9390
  gitInfo: async (ws) => {
8643
9391
  await handleGitInfo(ws, projectRoot);
@@ -8670,7 +9418,7 @@ async function startWebUI(opts = {}) {
8670
9418
  sendResult2(ws, false, parsed.message);
8671
9419
  return;
8672
9420
  }
8673
- return handleMailboxMessages(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9421
+ return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8674
9422
  },
8675
9423
  agents: (ws, msg) => {
8676
9424
  const parsed = validateMailboxAgentsPayload(msg.payload);
@@ -8678,18 +9426,30 @@ async function startWebUI(opts = {}) {
8678
9426
  sendResult2(ws, false, parsed.message);
8679
9427
  return;
8680
9428
  }
8681
- return handleMailboxAgents(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9429
+ return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8682
9430
  },
8683
- clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }),
9431
+ clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
8684
9432
  purge: (ws, msg) => {
8685
9433
  const parsed = validateMailboxPurgePayload(msg.payload);
8686
9434
  if (!parsed.ok) {
8687
9435
  sendResult2(ws, false, parsed.message);
8688
9436
  return;
8689
9437
  }
8690
- return handleMailboxPurge(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9438
+ return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8691
9439
  }
8692
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
+ };
8693
9453
  brainRoutes = {
8694
9454
  status: (ws) => {
8695
9455
  send(ws, {
@@ -8734,6 +9494,15 @@ async function startWebUI(opts = {}) {
8734
9494
  autoPhaseRoutes = {
8735
9495
  handleMessage: (msg) => autoPhaseHandler.handleMessage(msg)
8736
9496
  };
9497
+ specsRoutes = {
9498
+ handleMessage: (msg) => specsHandler.handleMessage(msg)
9499
+ };
9500
+ sddBoardRoutes = {
9501
+ handleMessage: (msg) => sddBoardHandler.handleMessage(msg)
9502
+ };
9503
+ sddWizardRoutes = {
9504
+ handleMessage: (msg) => sddWizardHandler.handleMessage(msg)
9505
+ };
8737
9506
  const watcherMetrics = {
8738
9507
  fileChangesDetected: 0,
8739
9508
  filesProcessed: 0,
@@ -8746,7 +9515,7 @@ async function startWebUI(opts = {}) {
8746
9515
  };
8747
9516
  const httpServer = createHttpServer({
8748
9517
  host: wsHost,
8749
- distDir: path15.resolve(import.meta.dirname, "../../dist"),
9518
+ distDir: path16.resolve(import.meta.dirname, "../../dist"),
8750
9519
  wsPort,
8751
9520
  globalRoot: wpaths.globalRoot,
8752
9521
  apiToken: wsToken,
@@ -8755,7 +9524,7 @@ async function startWebUI(opts = {}) {
8755
9524
  void fleetBroadcast?.();
8756
9525
  }
8757
9526
  });
8758
- const registryBaseDir = path15.dirname(globalConfigPath);
9527
+ const registryBaseDir = path16.dirname(globalConfigPath);
8759
9528
  httpServer.listen(httpPort, wsHost, () => {
8760
9529
  const openUrl = `http://${wsHost}:${httpPort}`;
8761
9530
  console.log(`[WebUI] HTTP server running on ${openUrl}`);
@@ -8767,7 +9536,7 @@ async function startWebUI(opts = {}) {
8767
9536
  wsPort,
8768
9537
  host: wsHost,
8769
9538
  projectRoot,
8770
- projectName: path15.basename(projectRoot) || projectRoot,
9539
+ projectName: path16.basename(projectRoot) || projectRoot,
8771
9540
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8772
9541
  url: `http://${wsHost}:${httpPort}`
8773
9542
  },