@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.
@@ -7,7 +7,10 @@ function isRecord(value) {
7
7
  }
8
8
  function validateModelSwitchPayload(payload) {
9
9
  if (!isRecord(payload)) {
10
- return { ok: false, message: "model.switch payload must be an object with string provider and model" };
10
+ return {
11
+ ok: false,
12
+ message: "model.switch payload must be an object with string provider and model"
13
+ };
11
14
  }
12
15
  const provider = payload["provider"];
13
16
  const model = payload["model"];
@@ -29,13 +32,22 @@ function validateMailboxMessagesPayload(payload) {
29
32
  const agentId = payload["agentId"];
30
33
  const unreadOnly = payload["unreadOnly"];
31
34
  if (limit !== void 0 && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1)) {
32
- return { ok: false, message: "mailbox.messages payload.limit must be a positive number when provided" };
35
+ return {
36
+ ok: false,
37
+ message: "mailbox.messages payload.limit must be a positive number when provided"
38
+ };
33
39
  }
34
40
  if (agentId !== void 0 && typeof agentId !== "string") {
35
- return { ok: false, message: "mailbox.messages payload.agentId must be a string when provided" };
41
+ return {
42
+ ok: false,
43
+ message: "mailbox.messages payload.agentId must be a string when provided"
44
+ };
36
45
  }
37
46
  if (unreadOnly !== void 0 && typeof unreadOnly !== "boolean") {
38
- return { ok: false, message: "mailbox.messages payload.unreadOnly must be a boolean when provided" };
47
+ return {
48
+ ok: false,
49
+ message: "mailbox.messages payload.unreadOnly must be a boolean when provided"
50
+ };
39
51
  }
40
52
  return { ok: true, value: { limit, agentId, unreadOnly } };
41
53
  }
@@ -46,7 +58,10 @@ function validateMailboxAgentsPayload(payload) {
46
58
  }
47
59
  const onlineOnly = payload["onlineOnly"];
48
60
  if (onlineOnly !== void 0 && typeof onlineOnly !== "boolean") {
49
- return { ok: false, message: "mailbox.agents payload.onlineOnly must be a boolean when provided" };
61
+ return {
62
+ ok: false,
63
+ message: "mailbox.agents payload.onlineOnly must be a boolean when provided"
64
+ };
50
65
  }
51
66
  return { ok: true, value: { onlineOnly } };
52
67
  }
@@ -58,10 +73,16 @@ function validateMailboxPurgePayload(payload) {
58
73
  const completedMaxAgeMs = payload["completedMaxAgeMs"];
59
74
  const incompleteMaxAgeMs = payload["incompleteMaxAgeMs"];
60
75
  if (completedMaxAgeMs !== void 0 && (typeof completedMaxAgeMs !== "number" || !Number.isFinite(completedMaxAgeMs) || completedMaxAgeMs < 0)) {
61
- return { ok: false, message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided" };
76
+ return {
77
+ ok: false,
78
+ message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided"
79
+ };
62
80
  }
63
81
  if (incompleteMaxAgeMs !== void 0 && (typeof incompleteMaxAgeMs !== "number" || !Number.isFinite(incompleteMaxAgeMs) || incompleteMaxAgeMs < 0)) {
64
- return { ok: false, message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided" };
82
+ return {
83
+ ok: false,
84
+ message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided"
85
+ };
65
86
  }
66
87
  return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };
67
88
  }
@@ -72,7 +93,10 @@ function validateBrainRiskPayload(payload) {
72
93
  }
73
94
  const level = payload["level"];
74
95
  if (typeof level !== "string" || !BRAIN_RISK_VALUES.has(level)) {
75
- return { ok: false, message: "brain.risk payload.level must be one of off, low, medium, high, all" };
96
+ return {
97
+ ok: false,
98
+ message: "brain.risk payload.level must be one of off, low, medium, high, all"
99
+ };
76
100
  }
77
101
  return { ok: true, value: { level } };
78
102
  }
@@ -98,7 +122,10 @@ function validateAutonomySwitchPayload(payload) {
98
122
  }
99
123
  function validatePlanTemplateUsePayload(payload) {
100
124
  if (!isRecord(payload)) {
101
- return { ok: false, message: "plan.template_use payload must be an object with string template" };
125
+ return {
126
+ ok: false,
127
+ message: "plan.template_use payload must be an object with string template"
128
+ };
102
129
  }
103
130
  const template = payload["template"];
104
131
  if (typeof template !== "string" || template.trim().length === 0) {
@@ -113,7 +140,15 @@ var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
113
140
  var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
114
141
  var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
115
142
  var REASONING_MODE_VALUES = /* @__PURE__ */ new Set(["auto", "on", "off"]);
116
- var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
143
+ var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
144
+ "none",
145
+ "minimal",
146
+ "low",
147
+ "medium",
148
+ "high",
149
+ "xhigh",
150
+ "max"
151
+ ]);
117
152
  var CACHE_TTL_VALUES = /* @__PURE__ */ new Set(["default", "5m", "1h"]);
118
153
  var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
119
154
  "yolo",
@@ -134,8 +169,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
134
169
  "tgDelegate",
135
170
  "reasoningPreserve",
136
171
  "hqEnabled",
137
- "hqRawContent"
172
+ "hqRawContent",
173
+ "fallbackAuto"
138
174
  ]);
175
+ var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
139
176
  var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
140
177
  "autonomyDelayMs",
141
178
  "autoProceedMaxIterations",
@@ -167,6 +204,9 @@ function validatePreferenceValue(key, value) {
167
204
  if (STRING_PREF_KEYS.has(key)) {
168
205
  return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
169
206
  }
207
+ if (STRING_ARRAY_PREF_KEYS.has(key)) {
208
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
209
+ }
170
210
  const allowed = ENUM_PREF_KEYS[key];
171
211
  if (allowed) {
172
212
  return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
@@ -287,16 +327,25 @@ function validateContextModeCreatePayload(payload) {
287
327
  return { ok: false, message: "context.mode.create payload.description must be a string" };
288
328
  }
289
329
  if (!isRecord(thresholds)) {
290
- return { ok: false, message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers" };
330
+ return {
331
+ ok: false,
332
+ message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers"
333
+ };
291
334
  }
292
335
  if (!isFiniteNumber(thresholds["warn"]) || !isFiniteNumber(thresholds["soft"]) || !isFiniteNumber(thresholds["hard"])) {
293
- return { ok: false, message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers" };
336
+ return {
337
+ ok: false,
338
+ message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers"
339
+ };
294
340
  }
295
341
  if (!isFiniteNumber(preserveK)) {
296
342
  return { ok: false, message: "context.mode.create payload.preserveK must be a finite number" };
297
343
  }
298
344
  if (!isFiniteNumber(eliseThreshold)) {
299
- return { ok: false, message: "context.mode.create payload.eliseThreshold must be a finite number" };
345
+ return {
346
+ ok: false,
347
+ message: "context.mode.create payload.eliseThreshold must be a finite number"
348
+ };
300
349
  }
301
350
  return {
302
351
  ok: true,
@@ -320,22 +369,34 @@ function validateContextModeUpdatePayload(payload) {
320
369
  }
321
370
  const name2 = payload["name"];
322
371
  if (name2 !== void 0 && typeof name2 !== "string") {
323
- return { ok: false, message: "context.mode.update payload.name must be a string when provided" };
372
+ return {
373
+ ok: false,
374
+ message: "context.mode.update payload.name must be a string when provided"
375
+ };
324
376
  }
325
377
  const description = payload["description"];
326
378
  if (description !== void 0 && typeof description !== "string") {
327
- return { ok: false, message: "context.mode.update payload.description must be a string when provided" };
379
+ return {
380
+ ok: false,
381
+ message: "context.mode.update payload.description must be a string when provided"
382
+ };
328
383
  }
329
384
  const thresholds = payload["thresholds"];
330
385
  let validatedThresholds;
331
386
  if (thresholds !== void 0) {
332
387
  if (!isRecord(thresholds)) {
333
- return { ok: false, message: "context.mode.update payload.thresholds must be an object when provided" };
388
+ return {
389
+ ok: false,
390
+ message: "context.mode.update payload.thresholds must be an object when provided"
391
+ };
334
392
  }
335
393
  for (const key of ["warn", "soft", "hard"]) {
336
394
  const val = thresholds[key];
337
395
  if (val !== void 0 && !isFiniteNumber(val)) {
338
- return { ok: false, message: `context.mode.update payload.thresholds.${key} must be a finite number when provided` };
396
+ return {
397
+ ok: false,
398
+ message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`
399
+ };
339
400
  }
340
401
  }
341
402
  validatedThresholds = {
@@ -346,11 +407,17 @@ function validateContextModeUpdatePayload(payload) {
346
407
  }
347
408
  const preserveK = payload["preserveK"];
348
409
  if (preserveK !== void 0 && !isFiniteNumber(preserveK)) {
349
- return { ok: false, message: "context.mode.update payload.preserveK must be a finite number when provided" };
410
+ return {
411
+ ok: false,
412
+ message: "context.mode.update payload.preserveK must be a finite number when provided"
413
+ };
350
414
  }
351
415
  const eliseThreshold = payload["eliseThreshold"];
352
416
  if (eliseThreshold !== void 0 && !isFiniteNumber(eliseThreshold)) {
353
- return { ok: false, message: "context.mode.update payload.eliseThreshold must be a finite number when provided" };
417
+ return {
418
+ ok: false,
419
+ message: "context.mode.update payload.eliseThreshold must be a finite number when provided"
420
+ };
354
421
  }
355
422
  return {
356
423
  ok: true,
@@ -368,28 +435,31 @@ function validateShellOpenPayload(payload) {
368
435
  if (!isRecord(payload)) {
369
436
  return { ok: false, message: "shell.open payload must be an object with string path" };
370
437
  }
371
- const path16 = payload["path"];
372
- if (typeof path16 !== "string" || path16.trim().length === 0) {
438
+ const path17 = payload["path"];
439
+ if (typeof path17 !== "string" || path17.trim().length === 0) {
373
440
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
374
441
  }
375
442
  const target = payload["target"];
376
443
  if (target !== void 0 && target !== "file" && target !== "terminal") {
377
- return { ok: false, message: 'shell.open payload.target must be "file" or "terminal" when provided' };
444
+ return {
445
+ ok: false,
446
+ message: 'shell.open payload.target must be "file" or "terminal" when provided'
447
+ };
378
448
  }
379
- return { ok: true, value: { path: path16, target } };
449
+ return { ok: true, value: { path: path17, target } };
380
450
  }
381
451
  function validateGitDiffPayload(payload) {
382
452
  if (!isRecord(payload)) {
383
453
  return { ok: false, message: "git.diff payload must be an object" };
384
454
  }
385
- const path16 = payload["path"];
386
- if (path16 === void 0 || path16 === null) {
455
+ const path17 = payload["path"];
456
+ if (path17 === void 0 || path17 === null) {
387
457
  return { ok: true, value: { path: "" } };
388
458
  }
389
- if (typeof path16 !== "string") {
459
+ if (typeof path17 !== "string") {
390
460
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
391
461
  }
392
- return { ok: true, value: { path: path16 } };
462
+ return { ok: true, value: { path: path17 } };
393
463
  }
394
464
  function validateProjectsAddPayload(payload) {
395
465
  if (!isRecord(payload)) {
@@ -569,7 +639,7 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
569
639
  return;
570
640
  }
571
641
  try {
572
- const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
642
+ const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
573
643
  let changed = false;
574
644
  const plan = await mutatePlan(planPath, sessionId, async (p) => {
575
645
  const before = p.updatedAt;
@@ -649,7 +719,7 @@ import {
649
719
  createTieredBrainArbiter
650
720
  } from "@wrongstack/core";
651
721
  import * as fs13 from "fs/promises";
652
- import * as path15 from "path";
722
+ import * as path16 from "path";
653
723
 
654
724
  // src/server/http-server.ts
655
725
  import * as fs from "fs/promises";
@@ -826,7 +896,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
826
896
  return;
827
897
  }
828
898
  try {
829
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
899
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
830
900
  const registry = new SessionRegistry(globalRoot);
831
901
  const entry = await registry.get(sessionId);
832
902
  if (!entry) {
@@ -835,7 +905,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
835
905
  return;
836
906
  }
837
907
  const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
838
- const store = new DefaultSessionStore4({ dir: paths.projectSessions });
908
+ const store = new DefaultSessionStore3({ dir: paths.projectSessions });
839
909
  const reader = new DefaultSessionReader2({ store });
840
910
  const rawEntries = [];
841
911
  for await (const ev of reader.replay(sessionId)) {
@@ -1521,8 +1591,8 @@ function isInside(root, target) {
1521
1591
  }
1522
1592
 
1523
1593
  // src/server/file-handlers.ts
1524
- import * as fs3 from "fs/promises";
1525
- import * as path3 from "path";
1594
+ import * as fs4 from "fs/promises";
1595
+ import * as path4 from "path";
1526
1596
  import { atomicWrite } from "@wrongstack/core";
1527
1597
 
1528
1598
  // src/server/file-picker.ts
@@ -1573,6 +1643,34 @@ function rankFiles(paths, query, limit) {
1573
1643
  return scored.slice(0, limit).map((s) => s.path);
1574
1644
  }
1575
1645
 
1646
+ // src/server/path-containment.ts
1647
+ import * as fs3 from "fs/promises";
1648
+ import * as path3 from "path";
1649
+ function isPathInside(root, target) {
1650
+ const relative3 = path3.relative(root, target);
1651
+ return relative3 === "" || !relative3.startsWith("..") && !path3.isAbsolute(relative3);
1652
+ }
1653
+ async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
1654
+ const resolved = path3.resolve(projectRoot, inputPath);
1655
+ let stat3;
1656
+ try {
1657
+ stat3 = await fs3.stat(resolved);
1658
+ } catch {
1659
+ throw new Error(`Directory not found or not accessible: ${resolved}`);
1660
+ }
1661
+ if (!stat3.isDirectory()) {
1662
+ throw new Error(`Directory not found or not accessible: ${resolved}`);
1663
+ }
1664
+ const [realProjectRoot, realResolved] = await Promise.all([
1665
+ fs3.realpath(projectRoot),
1666
+ fs3.realpath(resolved)
1667
+ ]);
1668
+ if (!isPathInside(realProjectRoot, realResolved)) {
1669
+ throw new Error(`Path must stay inside the project root: ${projectRoot}`);
1670
+ }
1671
+ return resolved;
1672
+ }
1673
+
1576
1674
  // src/server/ws-utils.ts
1577
1675
  import { randomBytes } from "crypto";
1578
1676
  import { WebSocket } from "ws";
@@ -1603,23 +1701,73 @@ function generateAuthToken() {
1603
1701
  }
1604
1702
 
1605
1703
  // src/server/file-handlers.ts
1704
+ async function resolveFileInsideProject(projectRoot, filePath) {
1705
+ const resolved = path4.resolve(projectRoot, filePath);
1706
+ if (!isPathInside(projectRoot, resolved)) {
1707
+ throw new Error("Path outside project root");
1708
+ }
1709
+ const { parent, base } = splitParentAndBase(resolved);
1710
+ const realProjectRoot = await fs4.realpath(projectRoot);
1711
+ const realParent = await realpathAllowMissing(parent);
1712
+ const realFull = path4.join(realParent, base);
1713
+ if (!isPathInside(realProjectRoot, realFull)) {
1714
+ throw new Error("Path outside project root");
1715
+ }
1716
+ return realFull;
1717
+ }
1718
+ function splitParentAndBase(p) {
1719
+ const base = path4.basename(p);
1720
+ const parent = path4.dirname(p);
1721
+ return { parent, base };
1722
+ }
1723
+ async function realpathAllowMissing(p) {
1724
+ try {
1725
+ return await fs4.realpath(p);
1726
+ } catch (err) {
1727
+ if (err.code !== "ENOENT") throw err;
1728
+ }
1729
+ const segments = [];
1730
+ let cursor = p;
1731
+ while (true) {
1732
+ const parent = path4.dirname(cursor);
1733
+ if (parent === cursor) {
1734
+ throw new Error("Path outside project root");
1735
+ }
1736
+ segments.unshift(path4.basename(cursor));
1737
+ try {
1738
+ const realParent = await fs4.realpath(parent);
1739
+ return path4.join(realParent, ...segments);
1740
+ } catch (err) {
1741
+ if (err.code !== "ENOENT") throw err;
1742
+ cursor = parent;
1743
+ }
1744
+ }
1745
+ }
1606
1746
  async function handleFilesTree(ws, msg, projectRoot) {
1607
1747
  const payload = msg.payload;
1608
1748
  const rawPath = payload?.path?.trim();
1609
- const treeRoot = rawPath && rawPath !== "." ? path3.resolve(projectRoot, rawPath) : projectRoot;
1610
- if (!treeRoot.startsWith(projectRoot + path3.sep) && treeRoot !== projectRoot) {
1749
+ let treeRoot;
1750
+ let realProjectRoot;
1751
+ try {
1752
+ if (rawPath && rawPath !== ".") {
1753
+ treeRoot = await resolveWorkingDirInsideProject(projectRoot, rawPath);
1754
+ } else {
1755
+ treeRoot = projectRoot;
1756
+ }
1757
+ realProjectRoot = await fs4.realpath(projectRoot);
1758
+ } catch {
1611
1759
  send(ws, {
1612
1760
  type: "files.tree",
1613
1761
  payload: { root: projectRoot, tree: [], error: "Path outside project root" }
1614
1762
  });
1615
1763
  return;
1616
1764
  }
1617
- const pathPrefix = treeRoot === projectRoot ? "" : (path3.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1765
+ const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
1618
1766
  async function buildTree(dir, rel, depth) {
1619
1767
  if (depth > 10) return [];
1620
1768
  let entries = [];
1621
1769
  try {
1622
- entries = await fs3.readdir(dir, { withFileTypes: true });
1770
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1623
1771
  } catch {
1624
1772
  return [];
1625
1773
  }
@@ -1631,11 +1779,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
1631
1779
  for (const e of entries) {
1632
1780
  if (isHiddenEntry(e.name)) continue;
1633
1781
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1634
- const childAbs = path3.join(dir, e.name);
1782
+ const childAbs = path4.join(dir, e.name);
1635
1783
  const childPath = pathPrefix + childRel;
1636
1784
  if (e.isDirectory()) {
1637
1785
  if (SKIP_DIRS.has(e.name)) continue;
1638
- const children = await buildTree(childAbs, childRel, depth + 1);
1786
+ let realChild;
1787
+ try {
1788
+ realChild = await fs4.realpath(childAbs);
1789
+ } catch {
1790
+ continue;
1791
+ }
1792
+ if (!isPathInside(realProjectRoot, realChild)) {
1793
+ continue;
1794
+ }
1795
+ const children = await buildTree(realChild, childRel, depth + 1);
1639
1796
  nodes.push({ name: e.name, path: childPath, type: "directory", children });
1640
1797
  } else if (e.isFile()) {
1641
1798
  nodes.push({ name: e.name, path: childPath, type: "file" });
@@ -1645,10 +1802,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
1645
1802
  }
1646
1803
  try {
1647
1804
  const tree = await buildTree(treeRoot, "", 0);
1648
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1805
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1649
1806
  send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
1650
1807
  } catch (err) {
1651
- const rootLabel = treeRoot === projectRoot ? projectRoot : path3.relative(projectRoot, treeRoot) || ".";
1808
+ const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
1652
1809
  send(ws, {
1653
1810
  type: "files.tree",
1654
1811
  payload: { root: rootLabel, tree: [], error: errMessage(err) }
@@ -1657,13 +1814,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
1657
1814
  }
1658
1815
  async function handleFilesRead(ws, msg, projectRoot) {
1659
1816
  const { filePath } = msg.payload;
1660
- const resolved = path3.resolve(projectRoot, filePath);
1661
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1817
+ let realResolved;
1818
+ try {
1819
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1820
+ } catch {
1662
1821
  send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
1663
1822
  return;
1664
1823
  }
1665
1824
  try {
1666
- const content = await fs3.readFile(resolved, "utf8");
1825
+ const content = await fs4.readFile(realResolved, "utf8");
1667
1826
  send(ws, { type: "files.read", payload: { filePath, content } });
1668
1827
  } catch (err) {
1669
1828
  send(ws, {
@@ -1674,16 +1833,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
1674
1833
  }
1675
1834
  async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1676
1835
  const { filePath, content } = msg.payload;
1677
- const resolved = path3.resolve(projectRoot, filePath);
1678
- if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
1836
+ let realResolved;
1837
+ try {
1838
+ realResolved = await resolveFileInsideProject(projectRoot, filePath);
1839
+ } catch {
1679
1840
  send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
1680
1841
  return;
1681
1842
  }
1682
1843
  try {
1683
- await atomicWrite(resolved, content);
1844
+ await atomicWrite(realResolved, content);
1684
1845
  send(ws, { type: "files.written", payload: { filePath, success: true } });
1685
1846
  if (opts.onWritten) {
1686
- void Promise.resolve(opts.onWritten(resolved)).catch(() => void 0);
1847
+ void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
1687
1848
  }
1688
1849
  } catch (err) {
1689
1850
  send(ws, {
@@ -1695,8 +1856,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1695
1856
  async function handleFilesList(ws, msg, projectRoot) {
1696
1857
  const payload = msg.payload ?? {};
1697
1858
  const limit = payload.limit ?? 50;
1698
- const listRoot = payload.path ? path3.resolve(projectRoot, payload.path) : projectRoot;
1699
- if (!listRoot.startsWith(projectRoot + path3.sep) && listRoot !== projectRoot) {
1859
+ let listRoot;
1860
+ let realProjectRoot;
1861
+ try {
1862
+ if (payload.path) {
1863
+ listRoot = await resolveWorkingDirInsideProject(projectRoot, payload.path);
1864
+ } else {
1865
+ listRoot = projectRoot;
1866
+ }
1867
+ realProjectRoot = await fs4.realpath(projectRoot);
1868
+ } catch {
1700
1869
  send(ws, { type: "files.list", payload: { files: [] } });
1701
1870
  return;
1702
1871
  }
@@ -1705,7 +1874,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1705
1874
  if (depth > 8 || results.length >= 600) return;
1706
1875
  let entries = [];
1707
1876
  try {
1708
- entries = await fs3.readdir(dir, { withFileTypes: true });
1877
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1709
1878
  } catch {
1710
1879
  return;
1711
1880
  }
@@ -1715,7 +1884,16 @@ async function handleFilesList(ws, msg, projectRoot) {
1715
1884
  const childRel = rel ? `${rel}/${e.name}` : e.name;
1716
1885
  if (e.isDirectory()) {
1717
1886
  if (SKIP_DIRS.has(e.name)) continue;
1718
- await walk(path3.join(dir, e.name), childRel, depth + 1);
1887
+ let realChild;
1888
+ try {
1889
+ realChild = await fs4.realpath(path4.join(dir, e.name));
1890
+ } catch {
1891
+ continue;
1892
+ }
1893
+ if (!isPathInside(realProjectRoot, realChild)) {
1894
+ continue;
1895
+ }
1896
+ await walk(realChild, childRel, depth + 1);
1719
1897
  } else if (e.isFile()) {
1720
1898
  results.push(childRel);
1721
1899
  }
@@ -1729,7 +1907,7 @@ async function handleFilesList(ws, msg, projectRoot) {
1729
1907
  }
1730
1908
 
1731
1909
  // src/server/completion-handlers.ts
1732
- import * as path4 from "path";
1910
+ import * as path5 from "path";
1733
1911
  import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
1734
1912
  var MAX_PREFIX_CHARS = 12e3;
1735
1913
  var MAX_SUFFIX_CHARS = 4e3;
@@ -1804,8 +1982,8 @@ async function handleCompletionRequest(ws, msg, opts) {
1804
1982
  return;
1805
1983
  }
1806
1984
  const payload = parsed.payload;
1807
- const projectRoot = path4.resolve(opts.projectRoot);
1808
- const resolved = path4.resolve(projectRoot, payload.filePath);
1985
+ const projectRoot = path5.resolve(opts.projectRoot);
1986
+ const resolved = path5.resolve(projectRoot, payload.filePath);
1809
1987
  if (!isInside2(projectRoot, resolved)) {
1810
1988
  send(ws, {
1811
1989
  type: "completion.result",
@@ -2204,7 +2382,7 @@ function buildSearchQuery(linePrefix, filePath) {
2204
2382
  if (memberMatch?.[1]) return memberMatch[1];
2205
2383
  const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
2206
2384
  if (token && token.length >= 2) return token;
2207
- return path4.basename(filePath, path4.extname(filePath));
2385
+ return path5.basename(filePath, path5.extname(filePath));
2208
2386
  }
2209
2387
  function currentLinePrefix(prefix) {
2210
2388
  const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
@@ -2234,7 +2412,7 @@ function head(value, max) {
2234
2412
  return value.length <= max ? value : value.slice(0, max);
2235
2413
  }
2236
2414
  function isInside2(root, target) {
2237
- return target === root || target.startsWith(root + path4.sep);
2415
+ return target === root || target.startsWith(root + path5.sep);
2238
2416
  }
2239
2417
 
2240
2418
  // src/server/memory-handlers.ts
@@ -2488,8 +2666,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
2488
2666
  }
2489
2667
 
2490
2668
  // src/server/skills-handlers.ts
2491
- import { promises as fs4 } from "fs";
2492
- import path5 from "path";
2669
+ import { promises as fs5 } from "fs";
2670
+ import path6 from "path";
2493
2671
  import JSZip from "jszip";
2494
2672
  import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
2495
2673
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
@@ -2560,19 +2738,19 @@ async function handleSkillsContent(ws, ctx, msg) {
2560
2738
  send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
2561
2739
  return;
2562
2740
  }
2563
- const body = await fs4.readFile(entry.path, "utf8");
2564
- const skillDir = path5.dirname(entry.path);
2741
+ const body = await fs5.readFile(entry.path, "utf8");
2742
+ const skillDir = path6.dirname(entry.path);
2565
2743
  let relatedFiles = [];
2566
2744
  try {
2567
- const files = await fs4.readdir(skillDir);
2568
- relatedFiles = files.filter((f) => f !== path5.basename(entry.path)).map((f) => path5.join(skillDir, f));
2745
+ const files = await fs5.readdir(skillDir);
2746
+ relatedFiles = files.filter((f) => f !== path6.basename(entry.path)).map((f) => path6.join(skillDir, f));
2569
2747
  } catch {
2570
2748
  }
2571
2749
  const nameLower = name2.toLowerCase();
2572
2750
  const refResults = await Promise.all(
2573
2751
  entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
2574
2752
  try {
2575
- const content = await fs4.readFile(e.path, "utf8");
2753
+ const content = await fs5.readFile(e.path, "utf8");
2576
2754
  return [e.name, content.toLowerCase().includes(nameLower)];
2577
2755
  } catch {
2578
2756
  return [e.name, false];
@@ -2662,14 +2840,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
2662
2840
  }
2663
2841
  const createPayload = parsed.value;
2664
2842
  try {
2665
- const targetDir = createPayload.scope === "global" ? path5.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path5.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2843
+ const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2666
2844
  try {
2667
- await fs4.access(targetDir);
2845
+ await fs5.access(targetDir);
2668
2846
  send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
2669
2847
  return;
2670
2848
  } catch {
2671
2849
  }
2672
- await fs4.mkdir(targetDir, { recursive: true });
2850
+ await fs5.mkdir(targetDir, { recursive: true });
2673
2851
  const lines = createPayload.description.trim().split("\n");
2674
2852
  const firstLine = lines[0].trim();
2675
2853
  const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
@@ -2717,13 +2895,13 @@ ${trigger}
2717
2895
  "- `bug-hunter` \u2014 for systematic bug detection patterns",
2718
2896
  "- `output-standards` \u2014 for standardized `<next_steps>` formatting"
2719
2897
  ].join("\n");
2720
- await atomicWrite2(path5.join(targetDir, "SKILL.md"), skillContent);
2898
+ await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
2721
2899
  send(ws, {
2722
2900
  type: "skills.created",
2723
2901
  payload: {
2724
2902
  success: true,
2725
2903
  error: null,
2726
- skill: { name: createPayload.name.trim(), path: path5.join(targetDir, "SKILL.md"), scope: createPayload.scope }
2904
+ skill: { name: createPayload.name.trim(), path: path6.join(targetDir, "SKILL.md"), scope: createPayload.scope }
2727
2905
  }
2728
2906
  });
2729
2907
  } catch (err) {
@@ -2787,23 +2965,23 @@ import {
2787
2965
  Agent,
2788
2966
  AutoCompactionMiddleware,
2789
2967
  Context,
2790
- DefaultMemoryStore as DefaultMemoryStore2,
2791
- DefaultModeStore as DefaultModeStore2,
2968
+ DefaultMemoryStore,
2969
+ DefaultModeStore,
2792
2970
  DefaultModelsRegistry,
2793
2971
  DefaultSessionReader,
2794
- DefaultSessionStore as DefaultSessionStore3,
2795
- DefaultSkillLoader as DefaultSkillLoader2,
2796
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder4,
2797
- DefaultTokenCounter as DefaultTokenCounter2,
2972
+ DefaultSessionStore as DefaultSessionStore2,
2973
+ DefaultSkillLoader,
2974
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
2975
+ DefaultTokenCounter,
2798
2976
  AnnotationsStore,
2799
2977
  CollaborationBus,
2800
2978
  collabPauseMiddleware,
2801
2979
  collabInjectMiddleware,
2802
2980
  estimateRequestTokensCalibrated,
2803
2981
  EventBus,
2804
- createStrategyCompactor as createStrategyCompactor2,
2982
+ createStrategyCompactor,
2805
2983
  ProviderRegistry,
2806
- TOKENS as TOKENS2,
2984
+ TOKENS,
2807
2985
  ToolRegistry,
2808
2986
  atomicWrite as atomicWrite6,
2809
2987
  createDefaultPipelines,
@@ -2812,6 +2990,7 @@ import {
2812
2990
  DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
2813
2991
  DEFAULT_SESSION_PRUNE_DAYS,
2814
2992
  DEFAULT_TOOLS_CONFIG,
2993
+ applyToolDescriptionModes,
2815
2994
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
2816
2995
  enhanceUserPrompt,
2817
2996
  gatedEnhancerReasoning,
@@ -2821,109 +3000,10 @@ import {
2821
3000
  import { ToolExecutor } from "@wrongstack/core/execution";
2822
3001
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
2823
3002
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
2824
- import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
3003
+ import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
2825
3004
  import { MCPRegistry } from "@wrongstack/mcp";
2826
3005
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
2827
-
2828
- // ../runtime/src/container.ts
2829
- import {
2830
- Container,
2831
- DefaultConfigStore,
2832
- DefaultErrorHandler,
2833
- DefaultMemoryStore,
2834
- DefaultModeStore,
2835
- DefaultPermissionPolicy,
2836
- DefaultRetryPolicy,
2837
- DefaultSecretScrubber,
2838
- DefaultSessionStore,
2839
- DefaultSkillLoader,
2840
- DefaultSystemPromptBuilder,
2841
- DefaultTokenCounter,
2842
- createStrategyCompactor,
2843
- buildRecoveryStrategies,
2844
- TOKENS
2845
- } from "@wrongstack/core";
2846
- function createDefaultContainer(opts) {
2847
- const { config, wpaths, logger, modelsRegistry } = opts;
2848
- const container = new Container();
2849
- const configStore = new DefaultConfigStore(config);
2850
- container.bind(TOKENS.ConfigStore, () => configStore);
2851
- container.bind(TOKENS.Logger, () => logger);
2852
- container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
2853
- container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
2854
- container.bind(
2855
- TOKENS.ErrorHandler,
2856
- () => new DefaultErrorHandler(
2857
- buildRecoveryStrategies({
2858
- compactor: container.resolve(TOKENS.Compactor),
2859
- modelsRegistry
2860
- })
2861
- )
2862
- );
2863
- container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
2864
- container.bind(
2865
- TOKENS.TokenCounter,
2866
- () => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
2867
- );
2868
- const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
2869
- container.bind(TOKENS.ModeStore, () => modeStore);
2870
- container.bind(
2871
- TOKENS.SessionStore,
2872
- () => new DefaultSessionStore({
2873
- dir: wpaths.projectSessions,
2874
- // Scrub secrets out of persisted user/model turns (F-06). Tool output
2875
- // is already scrubbed by the executor.
2876
- secretScrubber: container.resolve(TOKENS.SecretScrubber)
2877
- })
2878
- );
2879
- const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
2880
- container.bind(TOKENS.MemoryStore, () => memoryStore);
2881
- const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
2882
- container.bind(TOKENS.SkillLoader, () => skillLoader);
2883
- if (opts.systemPrompt) {
2884
- container.bind(
2885
- TOKENS.SystemPromptBuilder,
2886
- () => new DefaultSystemPromptBuilder(opts.systemPrompt)
2887
- );
2888
- }
2889
- container.bind(
2890
- TOKENS.PermissionPolicy,
2891
- () => {
2892
- const policyOptions = {
2893
- trustFile: wpaths.projectTrust,
2894
- yolo: opts.permission?.yolo ?? false,
2895
- yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
2896
- confirmDestructive: opts.permission?.confirmDestructive ?? false
2897
- };
2898
- if (opts.permission?.promptDelegate !== void 0) {
2899
- policyOptions.promptDelegate = opts.permission.promptDelegate;
2900
- }
2901
- return new DefaultPermissionPolicy(policyOptions);
2902
- }
2903
- );
2904
- container.bind(
2905
- TOKENS.Compactor,
2906
- () => (
2907
- // Strategy comes from config.context.strategy: 'hybrid' (default, lossless
2908
- // rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
2909
- // (LLM-driven selection). The LLM strategies resolve their provider from
2910
- // ctx at compact()-time, so binding here (before context.provider exists)
2911
- // is safe. preserveK / eliseThreshold are class-level fallbacks; the active
2912
- // ContextWindowPolicy in ctx.meta normally overrides both at runtime.
2913
- // eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
2914
- // essentially every tool_result (anything > 1 token).
2915
- createStrategyCompactor({
2916
- strategy: config.context?.strategy,
2917
- preserveK: opts.compactor?.preserveK ?? 10,
2918
- eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
2919
- smart: true,
2920
- summarizerModel: config.context?.summarizerModel,
2921
- llmSelector: config.context?.llmSelector
2922
- })
2923
- )
2924
- );
2925
- return container;
2926
- }
3006
+ import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
2927
3007
 
2928
3008
  // src/server/boot.ts
2929
3009
  import {
@@ -2943,6 +3023,7 @@ function patchConfig(config, updates) {
2943
3023
  import { spawnSync } from "child_process";
2944
3024
  import { toErrorMessage } from "@wrongstack/core/utils";
2945
3025
  import {
3026
+ assignNickname,
2946
3027
  AutoPhasePlanner,
2947
3028
  PhaseGraphBuilder,
2948
3029
  PhaseOrchestrator,
@@ -2980,6 +3061,8 @@ var AutoPhaseWebSocketHandler = class {
2980
3061
  abort = null;
2981
3062
  /** Optional per-phase git-worktree isolation (lazily created at start). */
2982
3063
  worktrees = null;
3064
+ /** Per-run worker identities so the board can show "who is on what". */
3065
+ usedNicknames = /* @__PURE__ */ new Set();
2983
3066
  addClient(ws) {
2984
3067
  const client = { ws, id: crypto.randomUUID() };
2985
3068
  this.clients.add(client);
@@ -3022,6 +3105,29 @@ var AutoPhaseWebSocketHandler = class {
3022
3105
  await this.handleTaskStatusChange(taskId, status);
3023
3106
  break;
3024
3107
  }
3108
+ case "autophase.moveTask": {
3109
+ const { taskId, toPhaseId } = msg.payload;
3110
+ if (this.orchestrator?.moveTask(taskId, toPhaseId)) this.afterBoardMutation();
3111
+ break;
3112
+ }
3113
+ case "autophase.assignTask": {
3114
+ const { taskId, agentId, agentName } = msg.payload;
3115
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
3116
+ break;
3117
+ }
3118
+ case "autophase.addTask": {
3119
+ const { phaseId, title, description, type, priority } = msg.payload;
3120
+ if (title?.trim() && this.orchestrator?.addTask(phaseId, { title: title.trim(), description, type, priority })) {
3121
+ this.afterBoardMutation();
3122
+ }
3123
+ break;
3124
+ }
3125
+ case "autophase.retryTask":
3126
+ case "autophase.runTask": {
3127
+ const { taskId } = msg.payload;
3128
+ if (this.orchestrator?.requeueTask(taskId)) this.afterBoardMutation();
3129
+ break;
3130
+ }
3025
3131
  case "autophase.toggleAutonomous": {
3026
3132
  const autonomous = msg.payload?.autonomous ?? !this.graph?.autonomous;
3027
3133
  if (this.graph) {
@@ -3149,6 +3255,13 @@ var AutoPhaseWebSocketHandler = class {
3149
3255
  return this.defaultPhases();
3150
3256
  }
3151
3257
  async executeTaskWithAgent(task, phaseId, env) {
3258
+ if (!task.assignee) {
3259
+ const nick = assignNickname("executor", this.usedNicknames);
3260
+ this.usedNicknames.add(nick.key);
3261
+ task.assignee = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3262
+ task.updatedAt = Date.now();
3263
+ this.broadcastState();
3264
+ }
3152
3265
  const prompt = `Execute task: ${task.title}
3153
3266
 
3154
3267
  Description: ${task.description}
@@ -3164,6 +3277,11 @@ Type: ${task.type}`;
3164
3277
  this.context.cwd = prevCwd;
3165
3278
  }
3166
3279
  }
3280
+ /** Persist + broadcast after an interactive board mutation. */
3281
+ afterBoardMutation() {
3282
+ if (this.graph) void this.store.save(this.graph);
3283
+ this.broadcastState();
3284
+ }
3167
3285
  async handleTaskStatusChange(taskId, status) {
3168
3286
  if (!this.graph) return;
3169
3287
  for (const phase of this.graph.phases.values()) {
@@ -3207,23 +3325,7 @@ Type: ${task.type}`;
3207
3325
  (sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3208
3326
  0
3209
3327
  );
3210
- const phaseItems = phases.map((p) => ({
3211
- id: p.id,
3212
- name: p.name,
3213
- description: p.description,
3214
- status: p.status,
3215
- priority: p.priority,
3216
- estimateHours: p.estimateHours,
3217
- actualDurationMs: p.actualDurationMs,
3218
- startedAt: p.startedAt,
3219
- completedAt: p.completedAt,
3220
- 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,
3221
- taskCount: p.taskGraph.nodes.size,
3222
- completedTasks: Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3223
- assignedAgents: p.assignedAgents,
3224
- isActive: p.id === currentActiveId
3225
- }));
3226
- const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
3328
+ const mapTask = (t) => ({
3227
3329
  id: t.id,
3228
3330
  title: t.title,
3229
3331
  description: t.description,
@@ -3236,8 +3338,39 @@ Type: ${task.type}`;
3236
3338
  tags: t.tags || [],
3237
3339
  startedAt: t.startedAt,
3238
3340
  completedAt: t.completedAt
3239
- })) : [];
3341
+ });
3342
+ const phaseItems = phases.map((p) => {
3343
+ const nodes = Array.from(p.taskGraph.nodes.values());
3344
+ const done = nodes.filter((t) => t.status === "completed").length;
3345
+ return {
3346
+ id: p.id,
3347
+ name: p.name,
3348
+ description: p.description,
3349
+ status: p.status,
3350
+ priority: p.priority,
3351
+ estimateHours: p.estimateHours,
3352
+ actualDurationMs: p.actualDurationMs,
3353
+ startedAt: p.startedAt,
3354
+ completedAt: p.completedAt,
3355
+ progressPercent: nodes.length > 0 ? Math.round(done / nodes.length * 100) : 0,
3356
+ taskCount: nodes.length,
3357
+ completedTasks: done,
3358
+ assignedAgents: p.assignedAgents,
3359
+ isActive: p.id === currentActiveId,
3360
+ // Every phase carries its full task list so the board can render each
3361
+ // phase as a column (not just the selected one).
3362
+ tasks: nodes.map(mapTask)
3363
+ };
3364
+ });
3365
+ const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
3240
3366
  const completedPhases = phases.filter((p) => p.status === "completed").length;
3367
+ const failedPhases = phases.filter((p) => p.status === "failed").length;
3368
+ const failedTasks = phases.reduce(
3369
+ (sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "failed").length,
3370
+ 0
3371
+ );
3372
+ const lastFailed = phases.filter((p) => p.status === "failed").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
3373
+ const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
3241
3374
  return {
3242
3375
  title: this.graph.title,
3243
3376
  phases: phaseItems,
@@ -3246,7 +3379,18 @@ Type: ${task.type}`;
3246
3379
  overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
3247
3380
  autonomous: this.graph.autonomous,
3248
3381
  totalTasks,
3249
- completedTasks
3382
+ completedTasks,
3383
+ // Structured progress + lastError consumed by the autophase store (were
3384
+ // defined client-side but never sent, so they stayed null on the board).
3385
+ progress: {
3386
+ totalPhases: phases.length,
3387
+ completed: completedPhases,
3388
+ failed: failedPhases,
3389
+ totalTasks,
3390
+ completedTasks,
3391
+ failedTasks
3392
+ },
3393
+ lastError
3250
3394
  };
3251
3395
  }
3252
3396
  sendState(client) {
@@ -3269,6 +3413,510 @@ Type: ${task.type}`;
3269
3413
  }
3270
3414
  };
3271
3415
 
3416
+ // src/server/specs-ws-handler.ts
3417
+ import {
3418
+ computeTaskProgress,
3419
+ SpecStore,
3420
+ TaskGraphStore
3421
+ } from "@wrongstack/core";
3422
+ var SpecsWebSocketHandler = class {
3423
+ specStore;
3424
+ graphStore;
3425
+ clients = /* @__PURE__ */ new Set();
3426
+ constructor(specsDir, taskGraphsDir) {
3427
+ this.specStore = new SpecStore({ baseDir: specsDir });
3428
+ this.graphStore = new TaskGraphStore({ baseDir: taskGraphsDir });
3429
+ }
3430
+ addClient(ws) {
3431
+ const client = { ws, id: crypto.randomUUID() };
3432
+ this.clients.add(client);
3433
+ ws.on("close", () => this.clients.delete(client));
3434
+ ws.on("error", () => this.clients.delete(client));
3435
+ void this.sendList(client);
3436
+ }
3437
+ async handleMessage(msg) {
3438
+ switch (msg.type) {
3439
+ case "specs.list":
3440
+ await this.broadcastList();
3441
+ break;
3442
+ case "specs.get": {
3443
+ const specId = msg.payload?.specId;
3444
+ if (specId) await this.broadcastDetail(specId);
3445
+ break;
3446
+ }
3447
+ case "specs.taskStatus": {
3448
+ const { graphId, taskId, status } = msg.payload;
3449
+ await this.updateTaskStatus(graphId, taskId, status);
3450
+ break;
3451
+ }
3452
+ }
3453
+ }
3454
+ // ── List ──────────────────────────────────────────────────────────────────
3455
+ async buildList() {
3456
+ const [specs, graphs] = await Promise.all([this.specStore.list(), this.graphStore.list()]);
3457
+ return specs.map((s, i) => {
3458
+ const graph = graphs.find((g) => g.specId === s.id);
3459
+ return {
3460
+ id: s.id,
3461
+ // FORGE-style display id (spec-001…). The real UUID stays in `id`.
3462
+ displayId: `spec-${String(i + 1).padStart(3, "0")}`,
3463
+ title: s.title,
3464
+ status: s.status,
3465
+ graphId: graph?.id,
3466
+ total: graph?.nodeCount ?? 0,
3467
+ completed: graph?.completedCount ?? 0
3468
+ };
3469
+ });
3470
+ }
3471
+ async broadcastList() {
3472
+ this.broadcast({ type: "specs.list", payload: { specs: await this.buildList() } });
3473
+ }
3474
+ async sendList(client) {
3475
+ this.send(client, { type: "specs.list", payload: { specs: await this.buildList() } });
3476
+ }
3477
+ // ── Detail (dependency board) ───────────────────────────────────────────────
3478
+ async broadcastDetail(specId) {
3479
+ const spec = await this.specStore.load(specId);
3480
+ const graph = await this.findGraphForSpec(specId);
3481
+ if (!spec || !graph) {
3482
+ this.broadcast({ type: "specs.detail", payload: { specId, columns: [], notFound: true } });
3483
+ return;
3484
+ }
3485
+ this.broadcast({ type: "specs.detail", payload: this.buildDetail(spec, graph) });
3486
+ }
3487
+ async findGraphForSpec(specId) {
3488
+ const entry = (await this.graphStore.list()).find((g) => g.specId === specId);
3489
+ if (!entry) return null;
3490
+ return this.graphStore.load(entry.id);
3491
+ }
3492
+ buildDetail(spec, graph) {
3493
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
3494
+ const shortId = /* @__PURE__ */ new Map();
3495
+ nodes.forEach((n, i) => {
3496
+ shortId.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
3497
+ });
3498
+ const blockers = /* @__PURE__ */ new Map();
3499
+ for (const n of nodes) blockers.set(n.id, []);
3500
+ for (const e of graph.edges) {
3501
+ if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
3502
+ }
3503
+ const statusOf = (id) => graph.nodes.get(id)?.status;
3504
+ const depthCache = /* @__PURE__ */ new Map();
3505
+ const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
3506
+ const cached = depthCache.get(id);
3507
+ if (cached !== void 0) return cached;
3508
+ if (seen.has(id)) return 0;
3509
+ seen.add(id);
3510
+ const deps2 = blockers.get(id) ?? [];
3511
+ const d = deps2.length === 0 ? 0 : 1 + Math.max(...deps2.map((b) => depthOf(b, seen)));
3512
+ depthCache.set(id, d);
3513
+ return d;
3514
+ };
3515
+ const toBoardTask = (n) => {
3516
+ const deps2 = blockers.get(n.id) ?? [];
3517
+ const allDepsDone = deps2.every((b) => statusOf(b) === "completed");
3518
+ const displayStatus = n.status === "pending" && deps2.length > 0 && allDepsDone ? "queued" : n.status;
3519
+ return {
3520
+ id: n.id,
3521
+ shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
3522
+ title: n.title,
3523
+ description: n.description,
3524
+ priority: n.priority,
3525
+ type: n.type,
3526
+ status: n.status,
3527
+ displayStatus,
3528
+ deps: deps2.map((b) => shortId.get(b) ?? b.slice(0, 6))
3529
+ };
3530
+ };
3531
+ const byDepth = /* @__PURE__ */ new Map();
3532
+ for (const n of nodes) {
3533
+ const d = depthOf(n.id);
3534
+ if (!byDepth.has(d)) byDepth.set(d, []);
3535
+ byDepth.get(d)?.push(toBoardTask(n));
3536
+ }
3537
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, tasks: byDepth.get(d) ?? [] }));
3538
+ const progress = computeTaskProgress(graph);
3539
+ return {
3540
+ specId: spec.id,
3541
+ graphId: graph.id,
3542
+ title: spec.title,
3543
+ overview: spec.overview,
3544
+ status: spec.status,
3545
+ total: progress.total,
3546
+ completed: progress.completed,
3547
+ running: progress.inProgress,
3548
+ pending: progress.pending,
3549
+ columns
3550
+ };
3551
+ }
3552
+ async updateTaskStatus(graphId, taskId, status) {
3553
+ const graph = await this.graphStore.load(graphId);
3554
+ const node = graph?.nodes.get(taskId);
3555
+ if (!graph || !node) return;
3556
+ node.status = status;
3557
+ node.updatedAt = Date.now();
3558
+ graph.updatedAt = Date.now();
3559
+ await this.graphStore.save(graph);
3560
+ this.broadcastDetail(graph.specId).catch(() => {
3561
+ });
3562
+ await this.broadcastList();
3563
+ }
3564
+ // ── Transport ───────────────────────────────────────────────────────────────
3565
+ broadcast(msg) {
3566
+ const data = JSON.stringify(msg);
3567
+ for (const client of this.clients) {
3568
+ if (client.ws.readyState === 1) client.ws.send(data);
3569
+ }
3570
+ }
3571
+ send(client, msg) {
3572
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3573
+ }
3574
+ };
3575
+
3576
+ // src/server/sdd-board-ws-handler.ts
3577
+ import { SddBoardStore } from "@wrongstack/core";
3578
+ var CONTROL_TYPES = /* @__PURE__ */ new Set([
3579
+ "pause",
3580
+ "resume",
3581
+ "stop",
3582
+ "retry",
3583
+ "retry_all_failed",
3584
+ "reassign",
3585
+ // Per-task model / fallback / verification assignment + stop/delete (drained by start-sdd-run).
3586
+ "set_task_model",
3587
+ "set_task_fallbacks",
3588
+ "set_task_verification",
3589
+ "cancel_task",
3590
+ "delete_task",
3591
+ "split_task",
3592
+ // Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
3593
+ "cleanup_worktrees",
3594
+ "rollback"
3595
+ ]);
3596
+ var SddBoardWebSocketHandler = class {
3597
+ store;
3598
+ clients = /* @__PURE__ */ new Set();
3599
+ latest = null;
3600
+ poll = null;
3601
+ unsub = null;
3602
+ constructor(boardsDir, events) {
3603
+ this.store = new SddBoardStore({ baseDir: boardsDir });
3604
+ if (events) {
3605
+ const handler = (e) => {
3606
+ this.latest = e.snapshot;
3607
+ this.broadcast({ type: "sdd.board.snapshot", payload: e.snapshot });
3608
+ };
3609
+ this.unsub = events.on("sdd.board.snapshot", handler);
3610
+ } else {
3611
+ this.poll = setInterval(() => void this.pollLatest(), 1e3);
3612
+ }
3613
+ }
3614
+ addClient(ws) {
3615
+ const client = { ws, id: crypto.randomUUID() };
3616
+ this.clients.add(client);
3617
+ ws.on("close", () => this.clients.delete(client));
3618
+ ws.on("error", () => this.clients.delete(client));
3619
+ void this.sendCurrent(client);
3620
+ }
3621
+ async handleMessage(msg) {
3622
+ if (msg.type === "sdd.board.get") {
3623
+ await this.broadcastCurrent();
3624
+ return;
3625
+ }
3626
+ if (msg.type === "sdd.board.list") {
3627
+ const boards = await this.store.list();
3628
+ this.broadcast({ type: "sdd.board.list", payload: { boards } });
3629
+ return;
3630
+ }
3631
+ const action = msg.type.replace(/^sdd\.board\./, "");
3632
+ if (CONTROL_TYPES.has(action)) {
3633
+ const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
3634
+ if (runId) {
3635
+ await this.store.appendControl(runId, {
3636
+ ts: Date.now(),
3637
+ type: action,
3638
+ payload: msg.payload
3639
+ });
3640
+ }
3641
+ }
3642
+ }
3643
+ dispose() {
3644
+ if (this.poll) clearInterval(this.poll);
3645
+ this.unsub?.();
3646
+ this.poll = null;
3647
+ this.unsub = null;
3648
+ }
3649
+ // ── internal ────────────────────────────────────────────────────────────
3650
+ async pollLatest() {
3651
+ const entry = (await this.store.list())[0];
3652
+ if (!entry) return;
3653
+ if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
3654
+ return;
3655
+ }
3656
+ const snap = await this.store.load(entry.runId);
3657
+ if (snap) {
3658
+ this.latest = snap;
3659
+ this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3660
+ }
3661
+ }
3662
+ async sendCurrent(client) {
3663
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3664
+ if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
3665
+ }
3666
+ async broadcastCurrent() {
3667
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3668
+ if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3669
+ }
3670
+ async loadLatestFromDisk() {
3671
+ const entry = (await this.store.list())[0];
3672
+ return entry ? this.store.load(entry.runId) : null;
3673
+ }
3674
+ broadcast(msg) {
3675
+ const data = JSON.stringify(msg);
3676
+ for (const client of this.clients) {
3677
+ if (client.ws.readyState === 1) client.ws.send(data);
3678
+ }
3679
+ }
3680
+ send(client, msg) {
3681
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3682
+ }
3683
+ };
3684
+
3685
+ // src/server/sdd-wizard-ws-handler.ts
3686
+ var SddWizardWebSocketHandler = class {
3687
+ constructor(deps2) {
3688
+ this.deps = deps2;
3689
+ }
3690
+ deps;
3691
+ clients = /* @__PURE__ */ new Set();
3692
+ driver = null;
3693
+ /** The agent's most recent question — paired with the next user answer. */
3694
+ lastAgentText = "";
3695
+ /** Guards against overlapping interview turns (one in flight at a time). */
3696
+ busy = false;
3697
+ addClient(ws) {
3698
+ const client = { ws, id: crypto.randomUUID() };
3699
+ this.clients.add(client);
3700
+ ws.on("close", () => this.clients.delete(client));
3701
+ ws.on("error", () => this.clients.delete(client));
3702
+ if (this.driver) this.send(client, this.snapshotMsg());
3703
+ }
3704
+ async handleMessage(msg) {
3705
+ try {
3706
+ switch (msg.type) {
3707
+ case "sdd.spec.start":
3708
+ await this.onStart(String(msg.payload?.goal ?? "").trim());
3709
+ break;
3710
+ case "sdd.spec.message":
3711
+ await this.onMessage(String(msg.payload?.text ?? ""));
3712
+ break;
3713
+ case "sdd.spec.approve":
3714
+ await this.onApprove();
3715
+ break;
3716
+ case "sdd.spec.get":
3717
+ if (this.driver) this.broadcast(this.snapshotMsg());
3718
+ break;
3719
+ case "sdd.run.start":
3720
+ await this.onRunStart({
3721
+ parallelSlots: msg.payload?.parallelSlots,
3722
+ defaultModel: msg.payload?.model,
3723
+ defaultProvider: msg.payload?.provider,
3724
+ fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
3725
+ });
3726
+ break;
3727
+ }
3728
+ } catch (err) {
3729
+ this.busy = false;
3730
+ this.broadcast({
3731
+ type: "sdd.spec.error",
3732
+ payload: { message: err instanceof Error ? err.message : String(err) }
3733
+ });
3734
+ }
3735
+ }
3736
+ // ── message handlers ──────────────────────────────────────────────────────
3737
+ async onStart(goal) {
3738
+ if (!goal) {
3739
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "A goal is required." } });
3740
+ return;
3741
+ }
3742
+ if (this.busy) return;
3743
+ this.driver = this.deps.makeDriver();
3744
+ const prompt = this.driver.start(goal);
3745
+ await this.runTurn(prompt);
3746
+ }
3747
+ async onMessage(text) {
3748
+ if (!this.driver || this.busy) return;
3749
+ if (this.driver.phase() === "questioning" && this.lastAgentText) {
3750
+ this.driver.submitAnswer(this.lastAgentText, text);
3751
+ } else {
3752
+ this.driver.submitAnswer(this.lastAgentText || "(feedback)", text);
3753
+ }
3754
+ await this.runTurn(this.driver.currentPrompt());
3755
+ }
3756
+ async onApprove() {
3757
+ if (!this.driver || this.busy) return;
3758
+ const { phase, prompt } = await this.driver.approve();
3759
+ if (phase === "executing") {
3760
+ this.broadcast(this.snapshotMsg());
3761
+ return;
3762
+ }
3763
+ await this.runTurn(prompt);
3764
+ }
3765
+ async onRunStart(opts) {
3766
+ if (!this.driver) {
3767
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "No active spec session." } });
3768
+ return;
3769
+ }
3770
+ const graph = await this.driver.ensureTaskGraph();
3771
+ if (!graph) {
3772
+ this.broadcast({
3773
+ type: "sdd.spec.error",
3774
+ payload: { message: "No spec yet \u2014 finish the interview before starting a run." }
3775
+ });
3776
+ return;
3777
+ }
3778
+ const { runId } = await this.deps.startRun(this.driver, opts);
3779
+ this.broadcast({ type: "sdd.run.started", payload: { runId } });
3780
+ }
3781
+ // ── internals ───────────────────────────────────────────────────────────
3782
+ /** Run one interview turn against the isolated agent, then ingest + broadcast. */
3783
+ async runTurn(prompt) {
3784
+ this.busy = true;
3785
+ this.broadcast(this.snapshotMsg());
3786
+ try {
3787
+ const text = await this.deps.runInterviewTurn(prompt);
3788
+ this.lastAgentText = text;
3789
+ if (this.driver) await this.driver.ingestAgentOutput(text);
3790
+ this.broadcast({ type: "sdd.spec.agent_text", payload: { text } });
3791
+ } finally {
3792
+ this.busy = false;
3793
+ this.broadcast(this.snapshotMsg());
3794
+ }
3795
+ }
3796
+ snapshotMsg() {
3797
+ const snap = this.driver?.snapshot();
3798
+ return {
3799
+ type: "sdd.spec.snapshot",
3800
+ payload: { ...snap, busy: this.busy }
3801
+ };
3802
+ }
3803
+ broadcast(msg) {
3804
+ const data = JSON.stringify(msg);
3805
+ for (const client of this.clients) {
3806
+ if (client.ws.readyState === 1) client.ws.send(data);
3807
+ }
3808
+ }
3809
+ send(client, msg) {
3810
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3811
+ }
3812
+ };
3813
+
3814
+ // src/server/sdd-wizard-wiring.ts
3815
+ import * as path7 from "path";
3816
+ import { spawnSync as spawnSync2 } from "child_process";
3817
+ import {
3818
+ makeCommandVerifier,
3819
+ makeLlmSubtaskGenerator,
3820
+ SddBoardStore as SddBoardStore2,
3821
+ SddInterviewDriver,
3822
+ SddRunRegistry,
3823
+ SddSupervisor,
3824
+ SpecStore as SpecStore2,
3825
+ startSddRun,
3826
+ TaskGraphStore as TaskGraphStore2,
3827
+ WorktreeManager as WorktreeManager2
3828
+ } from "@wrongstack/core";
3829
+ function buildSddWizardDeps(opts) {
3830
+ const registry = new SddRunRegistry();
3831
+ let isolatedSeq = 0;
3832
+ const runIsolatedTurn = async (prompt, name2) => {
3833
+ const result = await opts.subagentFactory({
3834
+ id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
3835
+ role: "executor",
3836
+ name: name2,
3837
+ disabledTools: ["delegate"],
3838
+ allowedCapabilities: ["fs.read", "net.outbound"]
3839
+ });
3840
+ try {
3841
+ const res = await result.agent.run([{ type: "text", text: prompt }]);
3842
+ return res.finalText ?? "";
3843
+ } finally {
3844
+ await result.dispose?.();
3845
+ }
3846
+ };
3847
+ return {
3848
+ makeDriver: () => new SddInterviewDriver({
3849
+ specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
3850
+ graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
3851
+ sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
3852
+ }),
3853
+ runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
3854
+ startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
3855
+ const graph = driver.getGraph();
3856
+ const tracker = driver.getTracker();
3857
+ if (!graph || !tracker) {
3858
+ throw new Error("No task graph to run \u2014 finish the interview first.");
3859
+ }
3860
+ let worktrees;
3861
+ if (process.env["WRONGSTACK_SDD_WORKTREES"] !== "0") {
3862
+ const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
3863
+ cwd: opts.projectRoot,
3864
+ encoding: "utf8",
3865
+ windowsHide: true
3866
+ }).stdout?.trim() === "true";
3867
+ if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
3868
+ }
3869
+ const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
3870
+ const verifyTask = makeCommandVerifier();
3871
+ const superviseFailure = opts.brain ? new SddSupervisor({
3872
+ brain: opts.brain,
3873
+ // The run-level fallback chain (chosen in the wizard) doubles as the
3874
+ // supervisor's reassign options — a `reassign` verdict rotates the
3875
+ // worker model on retry. Empty/undefined → reassign option dropped.
3876
+ reassignModels: fallbackModels,
3877
+ // LLM auto-split: decompose a retry-exhausted task into smaller
3878
+ // sub-tasks on an isolated read-only turn. Heavily validated +
3879
+ // bounded; an empty result degrades the split into a retry.
3880
+ generateSubtasks: makeLlmSubtaskGenerator({
3881
+ run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
3882
+ }),
3883
+ // The standalone brain is a tiered policy→LLM arbiter with NO
3884
+ // human-escalation wrapper (see index.ts), so it never blocks on a
3885
+ // human prompt — an unresolved verdict degrades to a bounded retry.
3886
+ // Safe to let the LLM layer actually pick reassign/split.
3887
+ requestLlmVerdict: true
3888
+ }).superviseFailure : void 0;
3889
+ const handle = startSddRun({
3890
+ tracker,
3891
+ graph,
3892
+ agent: opts.agent,
3893
+ projectRoot: opts.projectRoot,
3894
+ events: opts.events,
3895
+ subagentFactory: opts.subagentFactory,
3896
+ worktrees,
3897
+ boardStore,
3898
+ registry,
3899
+ parallelSlots,
3900
+ defaultModel,
3901
+ defaultProvider,
3902
+ fallbackModels,
3903
+ verifyTask,
3904
+ superviseFailure
3905
+ });
3906
+ void handle.completion.catch(() => {
3907
+ });
3908
+ return { runId: handle.runId };
3909
+ }
3910
+ };
3911
+ }
3912
+
3913
+ // src/server/sdd-wizard-routes.ts
3914
+ async function handleSddWizardRoute(_ws, msg, handlers) {
3915
+ if (!(msg.type.startsWith("sdd.spec.") || msg.type.startsWith("sdd.run."))) return false;
3916
+ await handlers.handleMessage(msg);
3917
+ return true;
3918
+ }
3919
+
3272
3920
  // src/server/collaboration-ws-handler.ts
3273
3921
  import { randomUUID } from "crypto";
3274
3922
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -3995,16 +4643,16 @@ var CollaborationWebSocketHandler = class {
3995
4643
  };
3996
4644
 
3997
4645
  // src/server/projects-manifest.ts
3998
- import * as fs5 from "fs/promises";
3999
- import * as path6 from "path";
4646
+ import * as fs6 from "fs/promises";
4647
+ import * as path8 from "path";
4000
4648
  import { projectSlug } from "@wrongstack/core";
4001
4649
  function projectsJsonPath(globalConfigPath) {
4002
- const base = path6.dirname(globalConfigPath);
4003
- return path6.join(base, "projects.json");
4650
+ const base = path8.dirname(globalConfigPath);
4651
+ return path8.join(base, "projects.json");
4004
4652
  }
4005
4653
  async function loadManifest(globalConfigPath) {
4006
4654
  try {
4007
- const raw = await fs5.readFile(projectsJsonPath(globalConfigPath), "utf8");
4655
+ const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
4008
4656
  const parsed = JSON.parse(raw);
4009
4657
  return { projects: parsed.projects ?? [] };
4010
4658
  } catch {
@@ -4013,16 +4661,16 @@ async function loadManifest(globalConfigPath) {
4013
4661
  }
4014
4662
  async function saveManifest(manifest, globalConfigPath) {
4015
4663
  const file = projectsJsonPath(globalConfigPath);
4016
- await fs5.mkdir(path6.dirname(file), { recursive: true });
4017
- await fs5.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4664
+ await fs6.mkdir(path8.dirname(file), { recursive: true });
4665
+ await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4018
4666
  }
4019
4667
  function generateProjectSlug(rootPath) {
4020
4668
  return projectSlug(rootPath);
4021
4669
  }
4022
4670
  async function ensureProjectDataDir(slug, globalConfigPath) {
4023
- const base = path6.dirname(globalConfigPath);
4024
- const dir = path6.join(base, "projects", slug);
4025
- await fs5.mkdir(dir, { recursive: true });
4671
+ const base = path8.dirname(globalConfigPath);
4672
+ const dir = path8.join(base, "projects", slug);
4673
+ await fs6.mkdir(dir, { recursive: true });
4026
4674
  return dir;
4027
4675
  }
4028
4676
 
@@ -4448,14 +5096,14 @@ function registerShutdownHandlers(res) {
4448
5096
 
4449
5097
  // src/server/instance-registry.ts
4450
5098
  import * as os from "os";
4451
- import * as path7 from "path";
4452
- import * as fs6 from "fs/promises";
5099
+ import * as path9 from "path";
5100
+ import * as fs7 from "fs/promises";
4453
5101
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
4454
5102
  function defaultBaseDir() {
4455
- return path7.join(os.homedir(), ".wrongstack");
5103
+ return path9.join(os.homedir(), ".wrongstack");
4456
5104
  }
4457
5105
  function registryPath(baseDir = defaultBaseDir()) {
4458
- return path7.join(baseDir, "webui-instances.json");
5106
+ return path9.join(baseDir, "webui-instances.json");
4459
5107
  }
4460
5108
  function isPidAlive(pid) {
4461
5109
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -4468,7 +5116,7 @@ function isPidAlive(pid) {
4468
5116
  }
4469
5117
  async function load(file) {
4470
5118
  try {
4471
- const raw = await fs6.readFile(file, "utf8");
5119
+ const raw = await fs7.readFile(file, "utf8");
4472
5120
  const parsed = JSON.parse(raw);
4473
5121
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
4474
5122
  return parsed;
@@ -4577,9 +5225,10 @@ function openBrowser(url, platform = process.platform) {
4577
5225
  if (child.pid) {
4578
5226
  try {
4579
5227
  import("@wrongstack/tools").then(({ getProcessRegistry }) => {
5228
+ const pid = child.pid;
5229
+ if (pid === void 0) return;
4580
5230
  getProcessRegistry().register({
4581
- // biome-ignore lint/style/noNonNullAssertion: pid always present after spawn
4582
- pid: child.pid,
5231
+ pid,
4583
5232
  name: "browser",
4584
5233
  command: `${command} ${args.join(" ")}`,
4585
5234
  startedAt: Date.now(),
@@ -4587,7 +5236,7 @@ function openBrowser(url, platform = process.platform) {
4587
5236
  protected: true
4588
5237
  });
4589
5238
  child.on("exit", () => {
4590
- getProcessRegistry().unregister(child.pid);
5239
+ getProcessRegistry().unregister(pid);
4591
5240
  });
4592
5241
  }).catch(() => {
4593
5242
  });
@@ -4612,19 +5261,19 @@ function computeUsageCost(usage, rates) {
4612
5261
  }
4613
5262
 
4614
5263
  // src/server/provider-handlers.ts
4615
- import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core";
5264
+ import { DefaultSecretScrubber } from "@wrongstack/core";
4616
5265
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
4617
5266
 
4618
5267
  // src/server/provider-config-io.ts
4619
- import * as fs7 from "fs/promises";
4620
- import * as path8 from "path";
5268
+ import * as fs8 from "fs/promises";
5269
+ import * as path10 from "path";
4621
5270
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
4622
5271
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
4623
5272
  import { DefaultSecretVault } from "@wrongstack/core";
4624
5273
  async function loadSavedProviders(configPath, vault) {
4625
5274
  let raw;
4626
5275
  try {
4627
- raw = await fs7.readFile(configPath, "utf8");
5276
+ raw = await fs8.readFile(configPath, "utf8");
4628
5277
  } catch {
4629
5278
  return {};
4630
5279
  }
@@ -4641,7 +5290,7 @@ async function saveProviders(configPath, vault, providers) {
4641
5290
  let raw;
4642
5291
  let fileExists = true;
4643
5292
  try {
4644
- raw = await fs7.readFile(configPath, "utf8");
5293
+ raw = await fs8.readFile(configPath, "utf8");
4645
5294
  } catch (err) {
4646
5295
  if (err.code !== "ENOENT") {
4647
5296
  throw new Error(
@@ -4669,7 +5318,7 @@ async function saveProviders(configPath, vault, providers) {
4669
5318
  await atomicWrite4(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
4670
5319
  }
4671
5320
  function createProviderConfigIO(configPath) {
4672
- const keyFile = path8.join(path8.dirname(configPath), ".key");
5321
+ const keyFile = path10.join(path10.dirname(configPath), ".key");
4673
5322
  const vault = new DefaultSecretVault({ keyFile });
4674
5323
  return {
4675
5324
  load: () => loadSavedProviders(configPath, vault),
@@ -4798,7 +5447,7 @@ function projectSavedProviders(providers) {
4798
5447
  return view;
4799
5448
  });
4800
5449
  }
4801
- var probeScrubber = new DefaultSecretScrubber2();
5450
+ var probeScrubber = new DefaultSecretScrubber();
4802
5451
  function createProviderHandlers(deps2) {
4803
5452
  const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
4804
5453
  let configWriteLock = deps2.getConfigWriteLock();
@@ -4823,7 +5472,10 @@ function createProviderHandlers(deps2) {
4823
5472
  try {
4824
5473
  const providers = await loadConfigProviders();
4825
5474
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
4826
- if (result.ok) await saveConfigProviders(providers);
5475
+ if (result.ok) {
5476
+ await saveConfigProviders(providers);
5477
+ broadcastSaved(providers);
5478
+ }
4827
5479
  sendResult2(ws, result.ok, result.message);
4828
5480
  } catch (err) {
4829
5481
  sendResult2(ws, false, errMessage(err));
@@ -4833,7 +5485,10 @@ function createProviderHandlers(deps2) {
4833
5485
  try {
4834
5486
  const providers = await loadConfigProviders();
4835
5487
  const result = deleteKey(providers, providerId, label);
4836
- if (result.ok) await saveConfigProviders(providers);
5488
+ if (result.ok) {
5489
+ await saveConfigProviders(providers);
5490
+ broadcastSaved(providers);
5491
+ }
4837
5492
  sendResult2(ws, result.ok, result.message);
4838
5493
  } catch (err) {
4839
5494
  sendResult2(ws, false, errMessage(err));
@@ -4843,7 +5498,10 @@ function createProviderHandlers(deps2) {
4843
5498
  try {
4844
5499
  const providers = await loadConfigProviders();
4845
5500
  const result = setActiveKey(providers, providerId, label);
4846
- if (result.ok) await saveConfigProviders(providers);
5501
+ if (result.ok) {
5502
+ await saveConfigProviders(providers);
5503
+ broadcastSaved(providers);
5504
+ }
4847
5505
  sendResult2(ws, result.ok, result.message);
4848
5506
  } catch (err) {
4849
5507
  sendResult2(ws, false, errMessage(err));
@@ -4853,11 +5511,13 @@ function createProviderHandlers(deps2) {
4853
5511
  try {
4854
5512
  const providers = await loadConfigProviders();
4855
5513
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
4856
- if (result.ok) await saveConfigProviders(providers);
5514
+ if (result.ok) {
5515
+ await saveConfigProviders(providers);
5516
+ broadcastSaved(providers);
5517
+ }
4857
5518
  sendResult2(ws, result.ok, result.message);
4858
5519
  if (result.ok) {
4859
5520
  console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
4860
- broadcastSaved(providers);
4861
5521
  }
4862
5522
  } catch (err) {
4863
5523
  sendResult2(ws, false, errMessage(err));
@@ -4867,7 +5527,10 @@ function createProviderHandlers(deps2) {
4867
5527
  try {
4868
5528
  const providers = await loadConfigProviders();
4869
5529
  const result = removeProvider(providers, providerId);
4870
- if (result.ok) await saveConfigProviders(providers);
5530
+ if (result.ok) {
5531
+ await saveConfigProviders(providers);
5532
+ broadcastSaved(providers);
5533
+ }
4871
5534
  sendResult2(ws, result.ok, result.message);
4872
5535
  } catch (err) {
4873
5536
  sendResult2(ws, false, errMessage(err));
@@ -4973,7 +5636,7 @@ function createProviderHandlers(deps2) {
4973
5636
 
4974
5637
  // src/server/mode-handlers.ts
4975
5638
  import {
4976
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2
5639
+ DefaultSystemPromptBuilder
4977
5640
  } from "@wrongstack/core";
4978
5641
  function createModeHandlers(ctx) {
4979
5642
  return {
@@ -5021,7 +5684,7 @@ function createModeHandlers(ctx) {
5021
5684
  }
5022
5685
  ctx.setModeId(id);
5023
5686
  const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
5024
- const freshBuilder = new DefaultSystemPromptBuilder2({
5687
+ const freshBuilder = new DefaultSystemPromptBuilder({
5025
5688
  memoryStore: ctx.memoryStore,
5026
5689
  skillLoader: ctx.skillLoader,
5027
5690
  modeStore: ctx.modeStore,
@@ -5050,42 +5713,12 @@ function createModeHandlers(ctx) {
5050
5713
 
5051
5714
  // src/server/project-handlers.ts
5052
5715
  import * as fs9 from "fs/promises";
5053
- import * as path10 from "path";
5716
+ import * as path11 from "path";
5054
5717
  import {
5055
- DefaultSessionStore as DefaultSessionStore2,
5056
- DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
5718
+ DefaultSessionStore,
5719
+ DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
5057
5720
  getSessionRegistry
5058
5721
  } from "@wrongstack/core";
5059
-
5060
- // src/server/path-containment.ts
5061
- import * as fs8 from "fs/promises";
5062
- import * as path9 from "path";
5063
- function isPathInside(root, target) {
5064
- const relative3 = path9.relative(root, target);
5065
- return relative3 === "" || !relative3.startsWith("..") && !path9.isAbsolute(relative3);
5066
- }
5067
- async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
5068
- const resolved = path9.resolve(projectRoot, inputPath);
5069
- let stat3;
5070
- try {
5071
- stat3 = await fs8.stat(resolved);
5072
- } catch {
5073
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5074
- }
5075
- if (!stat3.isDirectory()) {
5076
- throw new Error(`Directory not found or not accessible: ${resolved}`);
5077
- }
5078
- const [realProjectRoot, realResolved] = await Promise.all([
5079
- fs8.realpath(projectRoot),
5080
- fs8.realpath(resolved)
5081
- ]);
5082
- if (!isPathInside(realProjectRoot, realResolved)) {
5083
- throw new Error(`Path must stay inside the project root: ${projectRoot}`);
5084
- }
5085
- return resolved;
5086
- }
5087
-
5088
- // src/server/project-handlers.ts
5089
5722
  function createProjectHandlers(ctx) {
5090
5723
  return {
5091
5724
  listProjects: async (ws) => {
@@ -5107,7 +5740,7 @@ function createProjectHandlers(ctx) {
5107
5740
  }
5108
5741
  const { root: addRoot, name: displayName } = parsed.value;
5109
5742
  try {
5110
- const resolved = path10.resolve(addRoot);
5743
+ const resolved = path11.resolve(addRoot);
5111
5744
  await fs9.access(resolved);
5112
5745
  const stat3 = await fs9.stat(resolved);
5113
5746
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
@@ -5125,7 +5758,7 @@ function createProjectHandlers(ctx) {
5125
5758
  });
5126
5759
  return;
5127
5760
  }
5128
- const name2 = displayName?.trim() || path10.basename(resolved);
5761
+ const name2 = displayName?.trim() || path11.basename(resolved);
5129
5762
  const slug = generateProjectSlug(resolved);
5130
5763
  await ensureProjectDataDir(slug, ctx.globalConfigPath);
5131
5764
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5138,7 +5771,7 @@ function createProjectHandlers(ctx) {
5138
5771
  } catch (err) {
5139
5772
  send(ws, {
5140
5773
  type: "projects.added",
5141
- payload: { name: path10.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5774
+ payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5142
5775
  });
5143
5776
  }
5144
5777
  },
@@ -5153,7 +5786,7 @@ function createProjectHandlers(ctx) {
5153
5786
  }
5154
5787
  const { root: selRoot, name: selName } = parsed.value;
5155
5788
  try {
5156
- const resolved = path10.resolve(selRoot);
5789
+ const resolved = path11.resolve(selRoot);
5157
5790
  try {
5158
5791
  await fs9.access(resolved);
5159
5792
  const stat3 = await fs9.stat(resolved);
@@ -5163,7 +5796,7 @@ function createProjectHandlers(ctx) {
5163
5796
  type: "projects.selected",
5164
5797
  payload: {
5165
5798
  root: selRoot,
5166
- name: selName || path10.basename(selRoot),
5799
+ name: selName || path11.basename(selRoot),
5167
5800
  message: `Cannot switch: ${errMessage(err)}`
5168
5801
  }
5169
5802
  });
@@ -5175,7 +5808,7 @@ function createProjectHandlers(ctx) {
5175
5808
  entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
5176
5809
  entry.lastWorkingDir = resolved;
5177
5810
  } else {
5178
- const name2 = selName?.trim() || path10.basename(resolved);
5811
+ const name2 = selName?.trim() || path11.basename(resolved);
5179
5812
  const slug = generateProjectSlug(resolved);
5180
5813
  manifest.projects.push({
5181
5814
  name: name2,
@@ -5197,7 +5830,7 @@ function createProjectHandlers(ctx) {
5197
5830
  try {
5198
5831
  const modeId = ctx.getModeId();
5199
5832
  const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
5200
- const switchBuilder = new DefaultSystemPromptBuilder3({
5833
+ const switchBuilder = new DefaultSystemPromptBuilder2({
5201
5834
  memoryStore: ctx.memoryStore,
5202
5835
  skillLoader: ctx.skillLoader,
5203
5836
  modeStore: ctx.modeStore,
@@ -5214,14 +5847,14 @@ function createProjectHandlers(ctx) {
5214
5847
  });
5215
5848
  } catch {
5216
5849
  }
5217
- const newSessionsDir = path10.join(
5218
- path10.dirname(ctx.globalConfigPath),
5850
+ const newSessionsDir = path11.join(
5851
+ path11.dirname(ctx.globalConfigPath),
5219
5852
  "projects",
5220
5853
  switchSlug,
5221
5854
  "sessions"
5222
5855
  );
5223
5856
  await fs9.mkdir(newSessionsDir, { recursive: true });
5224
- const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
5857
+ const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
5225
5858
  const oldSession = ctx.getSession();
5226
5859
  const oldSessionId = oldSession.id;
5227
5860
  try {
@@ -5254,7 +5887,7 @@ function createProjectHandlers(ctx) {
5254
5887
  sessionId: newSession.id,
5255
5888
  projectSlug: switchSlug,
5256
5889
  projectRoot: resolved,
5257
- projectName: path10.basename(resolved),
5890
+ projectName: path11.basename(resolved),
5258
5891
  workingDir: resolved,
5259
5892
  clientType: "webui",
5260
5893
  pid: process.pid,
@@ -5266,8 +5899,8 @@ function createProjectHandlers(ctx) {
5266
5899
  type: "projects.selected",
5267
5900
  payload: {
5268
5901
  root: resolved,
5269
- name: selName || path10.basename(resolved),
5270
- message: `Switched to ${selName || path10.basename(resolved)}`
5902
+ name: selName || path11.basename(resolved),
5903
+ message: `Switched to ${selName || path11.basename(resolved)}`
5271
5904
  }
5272
5905
  });
5273
5906
  broadcast(ctx.clients, {
@@ -5287,7 +5920,7 @@ function createProjectHandlers(ctx) {
5287
5920
  type: "projects.selected",
5288
5921
  payload: {
5289
5922
  root: selRoot,
5290
- name: selName || path10.basename(selRoot),
5923
+ name: selName || path11.basename(selRoot),
5291
5924
  message: errMessage(err)
5292
5925
  }
5293
5926
  });
@@ -5318,7 +5951,7 @@ function createProjectHandlers(ctx) {
5318
5951
  }
5319
5952
 
5320
5953
  // src/server/session-handlers.ts
5321
- import * as path11 from "path";
5954
+ import * as path12 from "path";
5322
5955
  import {
5323
5956
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
5324
5957
  repairToolUseAdjacency,
@@ -5660,7 +6293,7 @@ function createSessionHandlers(ctx) {
5660
6293
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5661
6294
  const projectRoot = ctx.getProjectRoot();
5662
6295
  const rewinder = new DefaultSessionRewinder(
5663
- path11.join(projectRoot, ".wrongstack", "sessions"),
6296
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5664
6297
  projectRoot
5665
6298
  );
5666
6299
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
@@ -5675,7 +6308,7 @@ function createSessionHandlers(ctx) {
5675
6308
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5676
6309
  const projectRoot = ctx.getProjectRoot();
5677
6310
  const rewinder = new DefaultSessionRewinder(
5678
- path11.join(projectRoot, ".wrongstack", "sessions"),
6311
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5679
6312
  projectRoot
5680
6313
  );
5681
6314
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
@@ -5918,6 +6551,22 @@ async function handleModeRoute(ws, msg, handlers) {
5918
6551
  }
5919
6552
  }
5920
6553
 
6554
+ // src/server/prefs-routes.ts
6555
+ async function handlePrefsRoute(ws, msg, handlers) {
6556
+ switch (msg.type) {
6557
+ case "prefs.get": {
6558
+ await handlers.getPrefs(ws);
6559
+ return true;
6560
+ }
6561
+ case "prefs.update": {
6562
+ await handlers.updatePrefs(ws, msg.payload ?? {});
6563
+ return true;
6564
+ }
6565
+ default:
6566
+ return false;
6567
+ }
6568
+ }
6569
+
5921
6570
  // src/server/shell-git-routes.ts
5922
6571
  async function handleShellGitRoute(ws, msg, handlers) {
5923
6572
  switch (msg.type) {
@@ -5958,6 +6607,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
5958
6607
  }
5959
6608
  }
5960
6609
 
6610
+ // src/server/mcp-routes.ts
6611
+ async function handleMcpRoute(ws, msg, handlers) {
6612
+ switch (msg.type) {
6613
+ case "mcp.list":
6614
+ await handlers.list(ws, msg);
6615
+ return true;
6616
+ case "mcp.add":
6617
+ await handlers.add(ws, msg);
6618
+ return true;
6619
+ case "mcp.update":
6620
+ await handlers.update(ws, msg);
6621
+ return true;
6622
+ case "mcp.remove":
6623
+ await handlers.remove(ws, msg);
6624
+ return true;
6625
+ case "mcp.enable":
6626
+ await handlers.enable(ws, msg);
6627
+ return true;
6628
+ case "mcp.disable":
6629
+ await handlers.disable(ws, msg);
6630
+ return true;
6631
+ case "mcp.sleep":
6632
+ await handlers.sleep(ws, msg);
6633
+ return true;
6634
+ case "mcp.wake":
6635
+ await handlers.wake(ws, msg);
6636
+ return true;
6637
+ case "mcp.restart":
6638
+ await handlers.restart(ws, msg);
6639
+ return true;
6640
+ case "mcp.discover":
6641
+ await handlers.discover(ws, msg);
6642
+ return true;
6643
+ default:
6644
+ return false;
6645
+ }
6646
+ }
6647
+
5961
6648
  // src/server/brain-routes.ts
5962
6649
  async function handleBrainRoute(ws, msg, handlers) {
5963
6650
  switch (msg.type) {
@@ -5982,10 +6669,24 @@ async function handleAutoPhaseRoute(_ws, msg, handlers) {
5982
6669
  return true;
5983
6670
  }
5984
6671
 
6672
+ // src/server/specs-routes.ts
6673
+ async function handleSpecsRoute(_ws, msg, handlers) {
6674
+ if (!msg.type.startsWith("specs.")) return false;
6675
+ await handlers.handleMessage(msg);
6676
+ return true;
6677
+ }
6678
+
6679
+ // src/server/sdd-board-routes.ts
6680
+ async function handleSddBoardRoute(_ws, msg, handlers) {
6681
+ if (!msg.type.startsWith("sdd.board.")) return false;
6682
+ await handlers.handleMessage(msg);
6683
+ return true;
6684
+ }
6685
+
5985
6686
  // src/server/setup-events.ts
5986
6687
  import * as fs10 from "fs/promises";
5987
6688
  import { watch as fsWatch } from "fs";
5988
- import * as path12 from "path";
6689
+ import * as path13 from "path";
5989
6690
  function setupEvents(deps2) {
5990
6691
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
5991
6692
  const disposers = [];
@@ -6415,11 +7116,13 @@ function setupEvents(deps2) {
6415
7116
  events.on("provider.response", (e) => {
6416
7117
  if (e.usage?.input != null) {
6417
7118
  const maxCtx = context.provider.capabilities.maxContext;
6418
- const pct = maxCtx > 0 ? e.usage.input / maxCtx : 0;
7119
+ const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
7120
+ const load2 = Math.max(0, Math.min(1, rawLoad));
6419
7121
  const costUsd = context.tokenCounter.estimateCost().total;
6420
7122
  forwardSubagent("ctx_pct", {
6421
7123
  subagentId: "leader",
6422
- load: pct,
7124
+ load: load2,
7125
+ rawLoad,
6423
7126
  tokens: e.usage.input,
6424
7127
  maxContext: maxCtx,
6425
7128
  costUsd
@@ -6450,7 +7153,7 @@ function setupEvents(deps2) {
6450
7153
  if (wpaths?.projectStatus) {
6451
7154
  try {
6452
7155
  const statusFile = wpaths.projectStatus(e.projectHash);
6453
- const dir = path12.dirname(statusFile);
7156
+ const dir = path13.dirname(statusFile);
6454
7157
  await fs10.mkdir(dir, { recursive: true });
6455
7158
  await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
6456
7159
  } catch (err) {
@@ -6459,7 +7162,7 @@ function setupEvents(deps2) {
6459
7162
  }
6460
7163
  });
6461
7164
  if (wpaths?.projectStatus && wpaths.configDir) {
6462
- const projectsDir = path12.join(wpaths.configDir, "projects");
7165
+ const projectsDir = path13.join(wpaths.configDir, "projects");
6463
7166
  const knownProjectHashes = /* @__PURE__ */ new Set();
6464
7167
  const debounceTimers = /* @__PURE__ */ new Map();
6465
7168
  const DEBOUNCE_MS = 150;
@@ -6486,7 +7189,7 @@ function setupEvents(deps2) {
6486
7189
  );
6487
7190
  };
6488
7191
  const metricsInterval = setInterval(logWatcherMetrics, 6e4);
6489
- const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
7192
+ const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
6490
7193
  broadcast2(clients, { type: "client.status_update", payload: statusData });
6491
7194
  if (watcherMetrics) {
6492
7195
  watcherMetrics.broadcastsSent++;
@@ -6527,9 +7230,9 @@ function setupEvents(deps2) {
6527
7230
  if (eventType === "change") {
6528
7231
  if (filename == null) return;
6529
7232
  if (watcherMetrics) watcherMetrics.fileChangesDetected++;
6530
- const targetFile = path12.join(projectsDir, String(filename));
7233
+ const targetFile = path13.join(projectsDir, String(filename));
6531
7234
  if (targetFile.endsWith("status.json")) {
6532
- const projectHash2 = path12.basename(path12.dirname(targetFile));
7235
+ const projectHash2 = path13.basename(path13.dirname(targetFile));
6533
7236
  if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
6534
7237
  return;
6535
7238
  }
@@ -6587,7 +7290,7 @@ function setupEvents(deps2) {
6587
7290
  }
6588
7291
  });
6589
7292
  }
6590
- const globalRoot = globalConfigPath ? path12.dirname(globalConfigPath) : void 0;
7293
+ const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
6591
7294
  if (globalRoot) {
6592
7295
  const broadcastSessions = async () => {
6593
7296
  try {
@@ -6661,10 +7364,10 @@ function setupEvents(deps2) {
6661
7364
  // src/server/custom-context-modes.ts
6662
7365
  import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
6663
7366
  import * as fs11 from "fs/promises";
6664
- import * as path13 from "path";
7367
+ import * as path14 from "path";
6665
7368
  var STORE_FILENAME = "custom-context-modes.json";
6666
7369
  function storePath(wrongstackDir) {
6667
- return path13.join(wrongstackDir, STORE_FILENAME);
7370
+ return path14.join(wrongstackDir, STORE_FILENAME);
6668
7371
  }
6669
7372
  var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
6670
7373
  function createCustomModeStore(wrongstackDir) {
@@ -6796,12 +7499,12 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
6796
7499
 
6797
7500
  // src/server/shell-open.ts
6798
7501
  import * as fs12 from "fs/promises";
6799
- import * as path14 from "path";
7502
+ import * as path15 from "path";
6800
7503
  import { spawn as spawn2 } from "child_process";
6801
7504
  var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
6802
7505
  async function handleShellOpen(req, logger) {
6803
7506
  try {
6804
- const resolved = path14.resolve(req.path);
7507
+ const resolved = path15.resolve(req.path);
6805
7508
  await fs12.access(resolved);
6806
7509
  if (METACHAR_REGEX.test(resolved)) {
6807
7510
  return { success: false, message: "Path contains unsupported characters." };
@@ -6911,15 +7614,15 @@ async function handleGitChanges(ws, projectRoot) {
6911
7614
  if (!m) continue;
6912
7615
  const added = m[1] === "-" ? 0 : Number(m[1]);
6913
7616
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
6914
- let path16 = m[3] ?? "";
6915
- if (path16 === "") {
7617
+ let path17 = m[3] ?? "";
7618
+ if (path17 === "") {
6916
7619
  i += 1;
6917
- path16 = parts[i + 1] ?? parts[i] ?? "";
7620
+ path17 = parts[i + 1] ?? parts[i] ?? "";
6918
7621
  i += 1;
6919
7622
  }
6920
- if (!path16) continue;
6921
- const prev = counts.get(path16) ?? { added: 0, deleted: 0 };
6922
- counts.set(path16, { added: prev.added + added, deleted: prev.deleted + deleted });
7623
+ if (!path17) continue;
7624
+ const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
7625
+ counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
6923
7626
  }
6924
7627
  };
6925
7628
  parseNumstat(unstagedNumstat);
@@ -6931,7 +7634,7 @@ async function handleGitChanges(ws, projectRoot) {
6931
7634
  if (!rec || rec.length < 3) continue;
6932
7635
  const x = rec[0] ?? " ";
6933
7636
  const y = rec[1] ?? " ";
6934
- const path16 = rec.slice(3);
7637
+ const path17 = rec.slice(3);
6935
7638
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
6936
7639
  if (isRename) i += 1;
6937
7640
  let status;
@@ -6943,13 +7646,13 @@ async function handleGitChanges(ws, projectRoot) {
6943
7646
  else if (x === "D" || y === "D") status = "D";
6944
7647
  else status = "M";
6945
7648
  const staged = x !== " " && x !== "?";
6946
- let added = counts.get(path16)?.added ?? 0;
6947
- let deleted = counts.get(path16)?.deleted ?? 0;
7649
+ let added = counts.get(path17)?.added ?? 0;
7650
+ let deleted = counts.get(path17)?.deleted ?? 0;
6948
7651
  if (status === "?") {
6949
7652
  added = 0;
6950
7653
  deleted = 0;
6951
7654
  }
6952
- files.push({ path: path16, status, added, deleted, staged });
7655
+ files.push({ path: path17, status, added, deleted, staged });
6953
7656
  }
6954
7657
  send(ws, { type: "git.changes", payload: { files } });
6955
7658
  } catch (err) {
@@ -6960,21 +7663,21 @@ async function handleGitChanges(ws, projectRoot) {
6960
7663
  }
6961
7664
  }
6962
7665
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
6963
- async function handleGitDiff(ws, projectRoot, path16) {
7666
+ async function handleGitDiff(ws, projectRoot, path17) {
6964
7667
  const cwd = projectRoot || void 0;
6965
- const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path16, ...extra } });
6966
- if (!path16 || path16.includes("\0") || path16.includes("..") || nodePath.isAbsolute(path16)) {
7668
+ const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
7669
+ if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
6967
7670
  reply({ oldText: "", newText: "", error: "invalid path" });
6968
7671
  return;
6969
7672
  }
6970
7673
  try {
6971
7674
  const git = makeGit(cwd);
6972
7675
  const { readFile: readFile9 } = await import("fs/promises");
6973
- const { join: join11 } = await import("path");
6974
- const oldText = await git(["show", `HEAD:${path16}`]);
7676
+ const { join: join12 } = await import("path");
7677
+ const oldText = await git(["show", `HEAD:${path17}`]);
6975
7678
  let newText = "";
6976
7679
  try {
6977
- const abs = cwd ? join11(cwd, path16) : path16;
7680
+ const abs = cwd ? join12(cwd, path17) : path17;
6978
7681
  const buf = await readFile9(abs);
6979
7682
  if (buf.includes(0)) {
6980
7683
  reply({ oldText: "", newText: "", binary: true });
@@ -7070,6 +7773,7 @@ async function handleGoalGet(projectRoot, broadcast2) {
7070
7773
 
7071
7774
  // src/server/index.ts
7072
7775
  async function startWebUI(opts = {}) {
7776
+ ensureSessionShell();
7073
7777
  const requestedWsPort = opts.wsPort ?? 3457;
7074
7778
  const wsHost = opts.wsHost ?? "127.0.0.1";
7075
7779
  const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
@@ -7151,7 +7855,7 @@ async function startWebUI(opts = {}) {
7151
7855
  ttlSeconds: 24 * 3600
7152
7856
  });
7153
7857
  const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
7154
- const configStore = opts.services?.configStore ?? container.resolve(TOKENS2.ConfigStore);
7858
+ const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
7155
7859
  const providerRegistry = new ProviderRegistry();
7156
7860
  try {
7157
7861
  const factories = await buildProviderFactoriesFromRegistry({
@@ -7173,7 +7877,7 @@ async function startWebUI(opts = {}) {
7173
7877
  r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
7174
7878
  return r;
7175
7879
  })();
7176
- const memoryStore = new DefaultMemoryStore2({ paths: wpaths });
7880
+ const memoryStore = new DefaultMemoryStore({ paths: wpaths });
7177
7881
  if (config.features.memory) {
7178
7882
  toolRegistry.register(rememberTool(memoryStore));
7179
7883
  toolRegistry.register(forgetTool(memoryStore));
@@ -7185,6 +7889,8 @@ async function startWebUI(opts = {}) {
7185
7889
  toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
7186
7890
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
7187
7891
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
7892
+ applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
7893
+ configureExecPolicy(config.tools?.exec ?? {});
7188
7894
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7189
7895
  const mcpRegistry = new MCPRegistry({
7190
7896
  toolRegistry,
@@ -7201,7 +7907,7 @@ async function startWebUI(opts = {}) {
7201
7907
  });
7202
7908
  }
7203
7909
  }
7204
- let sessionStore = opts.services?.session ?? new DefaultSessionStore3({ dir: wpaths.projectSessions });
7910
+ let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
7205
7911
  if (!opts.services?.session) {
7206
7912
  sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
7207
7913
  if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
@@ -7228,7 +7934,7 @@ async function startWebUI(opts = {}) {
7228
7934
  sessionId: session.id,
7229
7935
  projectSlug: wpaths.projectSlug,
7230
7936
  projectRoot,
7231
- projectName: path15.basename(projectRoot),
7937
+ projectName: path16.basename(projectRoot),
7232
7938
  workingDir,
7233
7939
  clientType: "webui",
7234
7940
  pid: process.pid,
@@ -7248,7 +7954,7 @@ async function startWebUI(opts = {}) {
7248
7954
  const hqTelemetry = createHqPublisherFromEnv({
7249
7955
  clientKind: "webui",
7250
7956
  projectRoot,
7251
- projectName: path15.basename(projectRoot),
7957
+ projectName: path16.basename(projectRoot),
7252
7958
  appConfig: config,
7253
7959
  socketFactory: (url) => new WebSocket2(url)
7254
7960
  });
@@ -7260,7 +7966,7 @@ async function startWebUI(opts = {}) {
7260
7966
  events,
7261
7967
  sessionId: session.id,
7262
7968
  projectRoot,
7263
- projectName: path15.basename(projectRoot),
7969
+ projectName: path16.basename(projectRoot),
7264
7970
  globalRoot: wpaths.globalRoot,
7265
7971
  initialAgents: statusTracker?.getAgents(),
7266
7972
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -7289,11 +7995,11 @@ async function startWebUI(opts = {}) {
7289
7995
  });
7290
7996
  } catch {
7291
7997
  }
7292
- const tokenCounter = new DefaultTokenCounter2({
7998
+ const tokenCounter = new DefaultTokenCounter({
7293
7999
  registry: modelsRegistry,
7294
8000
  providerId: config.provider
7295
8001
  });
7296
- const modeStore = new DefaultModeStore2({ directory: wpaths.configDir });
8002
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7297
8003
  const activeMode = await modeStore.getActiveMode();
7298
8004
  let modeId = activeMode?.id ?? "default";
7299
8005
  const modePrompt = activeMode?.prompt ?? "";
@@ -7314,15 +8020,15 @@ async function startWebUI(opts = {}) {
7314
8020
  const modelCapabilitiesRef = {
7315
8021
  current: modelCapabilities
7316
8022
  };
7317
- const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
8023
+ const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7318
8024
  const skillInstaller = config.features.skills ? new SkillInstaller({
7319
- manifestPath: path15.join(wstackGlobalRoot2(), "installed-skills.json"),
7320
- projectSkillsDir: path15.join(projectRoot, ".wrongstack", "skills"),
7321
- globalSkillsDir: path15.join(wstackGlobalRoot2(), "skills"),
8025
+ manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
8026
+ projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
8027
+ globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
7322
8028
  projectHash: projectHash(projectRoot),
7323
8029
  skillLoader
7324
8030
  }) : void 0;
7325
- const systemPromptBuilder = new DefaultSystemPromptBuilder4({
8031
+ const systemPromptBuilder = new DefaultSystemPromptBuilder3({
7326
8032
  memoryStore,
7327
8033
  skillLoader,
7328
8034
  modeStore,
@@ -7422,6 +8128,8 @@ async function startWebUI(opts = {}) {
7422
8128
  context.meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
7423
8129
  context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
7424
8130
  context.meta["nextPrediction"] = config.nextPrediction ?? false;
8131
+ context.meta["fallbackModels"] = config.fallbackModels ?? [];
8132
+ context.meta["fallbackAuto"] = config.fallbackAuto !== false;
7425
8133
  context.meta["featureMcp"] = config.features.mcp !== false;
7426
8134
  context.meta["featurePlugins"] = config.features.plugins !== false;
7427
8135
  context.meta["featureMemory"] = config.features.memory !== false;
@@ -7479,7 +8187,9 @@ async function startWebUI(opts = {}) {
7479
8187
  "reasoningMode",
7480
8188
  "reasoningEffort",
7481
8189
  "reasoningPreserve",
7482
- "cacheTtl"
8190
+ "cacheTtl",
8191
+ "fallbackModels",
8192
+ "fallbackAuto"
7483
8193
  ];
7484
8194
  const prefSnapshot = () => {
7485
8195
  const snapshot = {};
@@ -7510,6 +8220,8 @@ async function startWebUI(opts = {}) {
7510
8220
  if (typeof payload["enhanceLanguage"] === "string") setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
7511
8221
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
7512
8222
  if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
8223
+ if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
8224
+ if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
7513
8225
  const FEATURE_MAP = {
7514
8226
  featureMcp: "mcp",
7515
8227
  featurePlugins: "plugins",
@@ -7606,7 +8318,7 @@ async function startWebUI(opts = {}) {
7606
8318
  projectRoot,
7607
8319
  logger
7608
8320
  });
7609
- const compactor = createStrategyCompactor2({
8321
+ const compactor = createStrategyCompactor({
7610
8322
  strategy: config.context?.strategy,
7611
8323
  preserveK: config.context?.preserveK ?? 10,
7612
8324
  eliseThreshold: config.context?.eliseThreshold ?? 2e3,
@@ -7682,9 +8394,9 @@ async function startWebUI(opts = {}) {
7682
8394
  maxContext: newMaxContext
7683
8395
  });
7684
8396
  }
7685
- const secretScrubber = container.resolve(TOKENS2.SecretScrubber);
7686
- const renderer = container.has(TOKENS2.Renderer) ? container.resolve(TOKENS2.Renderer) : void 0;
7687
- const permissionPolicy = container.resolve(TOKENS2.PermissionPolicy);
8397
+ const secretScrubber = container.resolve(TOKENS.SecretScrubber);
8398
+ const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
8399
+ const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
7688
8400
  const toolExecutor = new ToolExecutor(toolRegistry, {
7689
8401
  permissionPolicy,
7690
8402
  secretScrubber,
@@ -7727,7 +8439,7 @@ async function startWebUI(opts = {}) {
7727
8439
  }),
7728
8440
  events
7729
8441
  );
7730
- container.bind(TOKENS2.BrainArbiter, () => brain);
8442
+ container.bind(TOKENS.BrainArbiter, () => brain);
7731
8443
  const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
7732
8444
  const brainMonitor = new BrainMonitor({
7733
8445
  events,
@@ -7790,6 +8502,29 @@ async function startWebUI(opts = {}) {
7790
8502
  events,
7791
8503
  projectRoot
7792
8504
  );
8505
+ const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
8506
+ const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
8507
+ const sddWizardHandler = new SddWizardWebSocketHandler(
8508
+ buildSddWizardDeps({
8509
+ agent,
8510
+ events,
8511
+ projectRoot,
8512
+ brain,
8513
+ subagentFactory: makeLightSubagentFactory({
8514
+ container,
8515
+ providerRegistry,
8516
+ toolRegistry,
8517
+ session,
8518
+ projectRoot
8519
+ }),
8520
+ paths: {
8521
+ projectSpecs: wpaths.projectSpecs,
8522
+ projectTaskGraphs: wpaths.projectTaskGraphs,
8523
+ projectSddBoards: wpaths.projectSddBoards,
8524
+ projectDir: wpaths.projectDir
8525
+ }
8526
+ })
8527
+ );
7793
8528
  const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
7794
8529
  const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
7795
8530
  const collabHandler = new CollaborationWebSocketHandler(
@@ -7829,7 +8564,7 @@ async function startWebUI(opts = {}) {
7829
8564
  inputCost,
7830
8565
  outputCost,
7831
8566
  cacheReadCost,
7832
- projectName: path15.basename(projectRoot) || projectRoot,
8567
+ projectName: path16.basename(projectRoot) || projectRoot,
7833
8568
  projectRoot,
7834
8569
  cwd: workingDir,
7835
8570
  mode: modeId,
@@ -7921,6 +8656,9 @@ async function startWebUI(opts = {}) {
7921
8656
  }));
7922
8657
  });
7923
8658
  autoPhaseHandler.addClient(ws);
8659
+ specsHandler.addClient(ws);
8660
+ sddBoardHandler.addClient(ws);
8661
+ sddWizardHandler.addClient(ws);
7924
8662
  worktreeHandler.addClient(ws);
7925
8663
  collabHandler.addClient(ws);
7926
8664
  terminalHandler.addClient(ws);
@@ -8045,21 +8783,21 @@ async function startWebUI(opts = {}) {
8045
8783
  });
8046
8784
  }
8047
8785
  async function touchProjectEntry(root, workDir) {
8048
- const resolved = path15.resolve(root);
8786
+ const resolved = path16.resolve(root);
8049
8787
  const manifest = await loadManifest(globalConfigPath);
8050
8788
  const now = (/* @__PURE__ */ new Date()).toISOString();
8051
- const existing = manifest.projects.find((p) => path15.resolve(p.root) === resolved);
8789
+ const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
8052
8790
  if (existing) {
8053
8791
  existing.lastSeen = now;
8054
- if (workDir) existing.lastWorkingDir = path15.resolve(workDir);
8792
+ if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
8055
8793
  } else {
8056
8794
  manifest.projects.push({
8057
- name: path15.basename(resolved),
8795
+ name: path16.basename(resolved),
8058
8796
  root: resolved,
8059
8797
  slug: generateProjectSlug(resolved),
8060
8798
  createdAt: now,
8061
8799
  lastSeen: now,
8062
- lastWorkingDir: workDir ? path15.resolve(workDir) : void 0
8800
+ lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
8063
8801
  });
8064
8802
  }
8065
8803
  await saveManifest(manifest, globalConfigPath);
@@ -8081,19 +8819,29 @@ async function startWebUI(opts = {}) {
8081
8819
  let sessionRoutes;
8082
8820
  let projectRoutes;
8083
8821
  let modeRoutes;
8822
+ let prefsRoutes;
8084
8823
  let shellGitRoutes;
8085
8824
  let mailboxRoutes;
8825
+ let mcpRoutes;
8086
8826
  let brainRoutes;
8087
8827
  let autoPhaseRoutes;
8828
+ let specsRoutes;
8829
+ let sddBoardRoutes;
8830
+ let sddWizardRoutes;
8088
8831
  async function handleMessage(ws, _client, msg) {
8089
8832
  if (await handleProviderRoute(ws, msg, providerRoutes)) return;
8090
8833
  if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
8091
8834
  if (await handleProjectRoute(ws, msg, projectRoutes)) return;
8092
8835
  if (await handleModeRoute(ws, msg, modeRoutes)) return;
8836
+ if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
8093
8837
  if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
8094
8838
  if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
8839
+ if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
8095
8840
  if (await handleBrainRoute(ws, msg, brainRoutes)) return;
8096
8841
  if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
8842
+ if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
8843
+ if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
8844
+ if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
8097
8845
  switch (msg.type) {
8098
8846
  // Collaboration messages short-circuit the user/agent flow.
8099
8847
  // They don't touch runLock, the agent loop, or the message queue —
@@ -8199,27 +8947,31 @@ async function startWebUI(opts = {}) {
8199
8947
  case "memory.forget":
8200
8948
  return handleMemoryForget(ws, msg, memoryStore);
8201
8949
  // ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
8202
- // backed by the live MCPRegistry constructed above. ──
8950
+ // backed by the live MCPRegistry constructed above. Routed via
8951
+ // handleMcpRoute (see mcpRoutes = { ... } below). These case arms
8952
+ // are unreachable but left as tripwires for any future regression
8953
+ // where the route chain stops claiming 'mcp.*'. If you see one
8954
+ // fire, fix the dispatch order in the handleMessage chain above.
8203
8955
  case "mcp.list":
8204
- return handleMcpList(ws, msg, globalConfigPath, mcpRegistry);
8956
+ throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
8205
8957
  case "mcp.add":
8206
- return handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry);
8207
- case "mcp.remove":
8208
- return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
8958
+ throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
8209
8959
  case "mcp.update":
8210
- return handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry);
8211
- case "mcp.wake":
8212
- return handleMcpWake(ws, msg, globalConfigPath, mcpRegistry);
8213
- case "mcp.sleep":
8214
- return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
8215
- case "mcp.discover":
8216
- return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
8960
+ throw new Error("handleMcpRoute did not claim mcp.update \u2014 check chain order");
8961
+ case "mcp.remove":
8962
+ throw new Error("handleMcpRoute did not claim mcp.remove \u2014 check chain order");
8217
8963
  case "mcp.enable":
8218
- return handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry);
8964
+ throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
8219
8965
  case "mcp.disable":
8220
- return handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry);
8966
+ throw new Error("handleMcpRoute did not claim mcp.disable \u2014 check chain order");
8967
+ case "mcp.sleep":
8968
+ throw new Error("handleMcpRoute did not claim mcp.sleep \u2014 check chain order");
8969
+ case "mcp.wake":
8970
+ throw new Error("handleMcpRoute did not claim mcp.wake \u2014 check chain order");
8221
8971
  case "mcp.restart":
8222
- return handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry);
8972
+ throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
8973
+ case "mcp.discover":
8974
+ throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
8223
8975
  // Skills — full request→response cycle lives in skills-handlers.ts
8224
8976
  // (shared with the CLI's embedded server). skillsCtx is the closed-over
8225
8977
  // loader/installer/projectRoot the handlers need.
@@ -8367,49 +9119,11 @@ async function startWebUI(opts = {}) {
8367
9119
  break;
8368
9120
  }
8369
9121
  case "prefs.update": {
8370
- const parsed = validatePrefsUpdatePayload(msg.payload);
8371
- if (!parsed.ok) {
8372
- sendResult2(ws, false, parsed.message);
8373
- break;
8374
- }
8375
- const payload = parsed.value.prefs;
8376
- for (const [key, val] of Object.entries(payload)) {
8377
- context.meta[key] = val;
8378
- }
8379
- void persistPrefsToConfig(payload);
8380
- if (typeof payload["yolo"] === "boolean") {
8381
- permissionPolicy.setYolo?.(payload["yolo"]);
8382
- }
8383
- if (typeof payload["featureMcp"] === "boolean")
8384
- config.features.mcp = payload["featureMcp"];
8385
- if (typeof payload["featurePlugins"] === "boolean")
8386
- config.features.plugins = payload["featurePlugins"];
8387
- if (typeof payload["featureMemory"] === "boolean")
8388
- config.features.memory = payload["featureMemory"];
8389
- if (typeof payload["featureSkills"] === "boolean")
8390
- config.features.skills = payload["featureSkills"];
8391
- if (typeof payload["featureModelsRegistry"] === "boolean")
8392
- config.features.modelsRegistry = payload["featureModelsRegistry"];
8393
- if (typeof payload["contextAutoCompact"] === "boolean") {
8394
- if (payload["contextAutoCompact"] && autoCompactor) {
8395
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
8396
- pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
8397
- } else {
8398
- pipelines.contextWindow.remove("AutoCompaction", { optional: true });
8399
- }
8400
- }
8401
- if (typeof payload["logLevel"] === "string") {
8402
- const valid = ["debug", "info", "warn", "error"];
8403
- if (valid.includes(payload["logLevel"])) {
8404
- logger.level = payload["logLevel"];
8405
- }
8406
- }
8407
- broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
8408
- break;
9122
+ void ws;
9123
+ throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
8409
9124
  }
8410
9125
  case "prefs.get": {
8411
- send(ws, { type: "prefs.updated", payload: prefSnapshot() });
8412
- break;
9126
+ throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
8413
9127
  }
8414
9128
  default:
8415
9129
  send(ws, {
@@ -8452,22 +9166,7 @@ async function startWebUI(opts = {}) {
8452
9166
  const saved = await providerHandlers.loadConfigProviders();
8453
9167
  send(ws, {
8454
9168
  type: "providers.saved",
8455
- payload: {
8456
- providers: Object.entries(saved).map(([id, cfg]) => {
8457
- const keys = normalizeKeys(cfg);
8458
- return {
8459
- id,
8460
- family: cfg.family ?? id,
8461
- baseUrl: cfg.baseUrl,
8462
- apiKeys: keys.map((k) => ({
8463
- label: k.label,
8464
- maskedKey: maskedKey(k.apiKey),
8465
- isActive: k.label === cfg.activeKey,
8466
- createdAt: k.createdAt
8467
- }))
8468
- };
8469
- })
8470
- }
9169
+ payload: { providers: projectSavedProviders(saved) }
8471
9170
  });
8472
9171
  },
8473
9172
  listProviderModels: async (ws, msg) => {
@@ -8645,6 +9344,55 @@ async function startWebUI(opts = {}) {
8645
9344
  },
8646
9345
  sessionStartPayload
8647
9346
  });
9347
+ prefsRoutes = {
9348
+ getPrefs: async (ws) => {
9349
+ send(ws, { type: "prefs.updated", payload: prefSnapshot() });
9350
+ },
9351
+ updatePrefs: async (ws, msgPayload) => {
9352
+ const parsed = validatePrefsUpdatePayload(msgPayload);
9353
+ if (!parsed.ok) {
9354
+ sendResult2(ws, false, parsed.message);
9355
+ return;
9356
+ }
9357
+ const payload = parsed.value.prefs;
9358
+ for (const [key, val] of Object.entries(payload)) {
9359
+ context.meta[key] = val;
9360
+ }
9361
+ void persistPrefsToConfig(payload);
9362
+ if (typeof payload["yolo"] === "boolean") {
9363
+ permissionPolicy.setYolo?.(payload["yolo"]);
9364
+ }
9365
+ if (typeof payload["featureMcp"] === "boolean")
9366
+ config.features.mcp = payload["featureMcp"];
9367
+ if (typeof payload["featurePlugins"] === "boolean")
9368
+ config.features.plugins = payload["featurePlugins"];
9369
+ if (typeof payload["featureMemory"] === "boolean")
9370
+ config.features.memory = payload["featureMemory"];
9371
+ if (typeof payload["featureSkills"] === "boolean")
9372
+ config.features.skills = payload["featureSkills"];
9373
+ if (typeof payload["featureModelsRegistry"] === "boolean")
9374
+ config.features.modelsRegistry = payload["featureModelsRegistry"];
9375
+ if (Array.isArray(payload["fallbackModels"]))
9376
+ config.fallbackModels = payload["fallbackModels"];
9377
+ if (typeof payload["fallbackAuto"] === "boolean")
9378
+ config.fallbackAuto = payload["fallbackAuto"];
9379
+ if (typeof payload["contextAutoCompact"] === "boolean") {
9380
+ if (payload["contextAutoCompact"] && autoCompactor) {
9381
+ pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9382
+ pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
9383
+ } else {
9384
+ pipelines.contextWindow.remove("AutoCompaction", { optional: true });
9385
+ }
9386
+ }
9387
+ if (typeof payload["logLevel"] === "string") {
9388
+ const valid = ["debug", "info", "warn", "error"];
9389
+ if (valid.includes(payload["logLevel"])) {
9390
+ logger.level = payload["logLevel"];
9391
+ }
9392
+ }
9393
+ broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
9394
+ }
9395
+ };
8648
9396
  shellGitRoutes = {
8649
9397
  gitInfo: async (ws) => {
8650
9398
  await handleGitInfo(ws, projectRoot);
@@ -8677,7 +9425,7 @@ async function startWebUI(opts = {}) {
8677
9425
  sendResult2(ws, false, parsed.message);
8678
9426
  return;
8679
9427
  }
8680
- return handleMailboxMessages(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9428
+ return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8681
9429
  },
8682
9430
  agents: (ws, msg) => {
8683
9431
  const parsed = validateMailboxAgentsPayload(msg.payload);
@@ -8685,18 +9433,30 @@ async function startWebUI(opts = {}) {
8685
9433
  sendResult2(ws, false, parsed.message);
8686
9434
  return;
8687
9435
  }
8688
- return handleMailboxAgents(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9436
+ return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8689
9437
  },
8690
- clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }),
9438
+ clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
8691
9439
  purge: (ws, msg) => {
8692
9440
  const parsed = validateMailboxPurgePayload(msg.payload);
8693
9441
  if (!parsed.ok) {
8694
9442
  sendResult2(ws, false, parsed.message);
8695
9443
  return;
8696
9444
  }
8697
- return handleMailboxPurge(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9445
+ return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8698
9446
  }
8699
9447
  };
9448
+ mcpRoutes = {
9449
+ list: (ws, msg) => handleMcpList(ws, msg, globalConfigPath, mcpRegistry),
9450
+ add: (ws, msg) => handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry),
9451
+ update: (ws, msg) => handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry),
9452
+ remove: (ws, msg) => handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry),
9453
+ enable: (ws, msg) => handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry),
9454
+ disable: (ws, msg) => handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry),
9455
+ sleep: (ws, msg) => handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry),
9456
+ wake: (ws, msg) => handleMcpWake(ws, msg, globalConfigPath, mcpRegistry),
9457
+ restart: (ws, msg) => handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry),
9458
+ discover: (ws, msg) => handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry)
9459
+ };
8700
9460
  brainRoutes = {
8701
9461
  status: (ws) => {
8702
9462
  send(ws, {
@@ -8741,6 +9501,15 @@ async function startWebUI(opts = {}) {
8741
9501
  autoPhaseRoutes = {
8742
9502
  handleMessage: (msg) => autoPhaseHandler.handleMessage(msg)
8743
9503
  };
9504
+ specsRoutes = {
9505
+ handleMessage: (msg) => specsHandler.handleMessage(msg)
9506
+ };
9507
+ sddBoardRoutes = {
9508
+ handleMessage: (msg) => sddBoardHandler.handleMessage(msg)
9509
+ };
9510
+ sddWizardRoutes = {
9511
+ handleMessage: (msg) => sddWizardHandler.handleMessage(msg)
9512
+ };
8744
9513
  const watcherMetrics = {
8745
9514
  fileChangesDetected: 0,
8746
9515
  filesProcessed: 0,
@@ -8753,7 +9522,7 @@ async function startWebUI(opts = {}) {
8753
9522
  };
8754
9523
  const httpServer = createHttpServer({
8755
9524
  host: wsHost,
8756
- distDir: path15.resolve(import.meta.dirname, "../../dist"),
9525
+ distDir: path16.resolve(import.meta.dirname, "../../dist"),
8757
9526
  wsPort,
8758
9527
  globalRoot: wpaths.globalRoot,
8759
9528
  apiToken: wsToken,
@@ -8762,7 +9531,7 @@ async function startWebUI(opts = {}) {
8762
9531
  void fleetBroadcast?.();
8763
9532
  }
8764
9533
  });
8765
- const registryBaseDir = path15.dirname(globalConfigPath);
9534
+ const registryBaseDir = path16.dirname(globalConfigPath);
8766
9535
  httpServer.listen(httpPort, wsHost, () => {
8767
9536
  const openUrl = `http://${wsHost}:${httpPort}`;
8768
9537
  console.log(`[WebUI] HTTP server running on ${openUrl}`);
@@ -8774,7 +9543,7 @@ async function startWebUI(opts = {}) {
8774
9543
  wsPort,
8775
9544
  host: wsHost,
8776
9545
  projectRoot,
8777
- projectName: path15.basename(projectRoot) || projectRoot,
9546
+ projectName: path16.basename(projectRoot) || projectRoot,
8778
9547
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8779
9548
  url: `http://${wsHost}:${httpPort}`
8780
9549
  },
@@ -8817,11 +9586,15 @@ async function startWebUI(opts = {}) {
8817
9586
  }
8818
9587
  export {
8819
9588
  AutoPhaseWebSocketHandler,
9589
+ SddBoardWebSocketHandler,
9590
+ SddWizardWebSocketHandler,
9591
+ SpecsWebSocketHandler,
8820
9592
  WorktreeWebSocketHandler,
8821
9593
  addProvider,
8822
9594
  broadcast,
8823
9595
  browserOpenCommand,
8824
9596
  buildCspHeader,
9597
+ buildSddWizardDeps,
8825
9598
  createCustomModeStore,
8826
9599
  createEternalSubscription,
8827
9600
  createHttpServer,