@wrongstack/webui 0.274.0 → 0.275.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.
@@ -170,9 +170,12 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
170
170
  "reasoningPreserve",
171
171
  "hqEnabled",
172
172
  "hqRawContent",
173
- "fallbackAuto"
173
+ "fallbackAuto",
174
+ "favoriteModelsOnly"
174
175
  ]);
175
- var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
176
+ var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels", "favoriteModels"]);
177
+ var STRING_ARRAY_RECORD_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackProfiles"]);
178
+ var MODEL_MATRIX_PREF_KEYS = /* @__PURE__ */ new Set(["modelMatrix"]);
176
179
  var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
177
180
  "autonomyDelayMs",
178
181
  "autoProceedMaxIterations",
@@ -207,6 +210,33 @@ function validatePreferenceValue(key, value) {
207
210
  if (STRING_ARRAY_PREF_KEYS.has(key)) {
208
211
  return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
209
212
  }
213
+ if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
214
+ return isRecord(value) && Object.values(value).every(
215
+ (v) => Array.isArray(v) && v.every((item) => typeof item === "string")
216
+ ) ? null : `prefs.update payload.${key} must be an object of string arrays`;
217
+ }
218
+ if (MODEL_MATRIX_PREF_KEYS.has(key)) {
219
+ if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
220
+ for (const entry of Object.values(value)) {
221
+ if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
222
+ const provider = entry["provider"];
223
+ const model = entry["model"];
224
+ const fallbackProfile = entry["fallbackProfile"];
225
+ if (provider !== void 0 && typeof provider !== "string") {
226
+ return `prefs.update payload.${key}.provider must be a string when provided`;
227
+ }
228
+ if (model !== void 0 && typeof model !== "string") {
229
+ return `prefs.update payload.${key}.model must be a string when provided`;
230
+ }
231
+ if (fallbackProfile !== void 0 && typeof fallbackProfile !== "string") {
232
+ return `prefs.update payload.${key}.fallbackProfile must be a string when provided`;
233
+ }
234
+ if (model === void 0 && fallbackProfile === void 0) {
235
+ return `prefs.update payload.${key} entries require model or fallbackProfile`;
236
+ }
237
+ }
238
+ return null;
239
+ }
210
240
  const allowed = ENUM_PREF_KEYS[key];
211
241
  if (allowed) {
212
242
  return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
@@ -435,8 +465,8 @@ function validateShellOpenPayload(payload) {
435
465
  if (!isRecord(payload)) {
436
466
  return { ok: false, message: "shell.open payload must be an object with string path" };
437
467
  }
438
- const path17 = payload["path"];
439
- if (typeof path17 !== "string" || path17.trim().length === 0) {
468
+ const path18 = payload["path"];
469
+ if (typeof path18 !== "string" || path18.trim().length === 0) {
440
470
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
441
471
  }
442
472
  const target = payload["target"];
@@ -446,20 +476,20 @@ function validateShellOpenPayload(payload) {
446
476
  message: 'shell.open payload.target must be "file" or "terminal" when provided'
447
477
  };
448
478
  }
449
- return { ok: true, value: { path: path17, target } };
479
+ return { ok: true, value: { path: path18, target } };
450
480
  }
451
481
  function validateGitDiffPayload(payload) {
452
482
  if (!isRecord(payload)) {
453
483
  return { ok: false, message: "git.diff payload must be an object" };
454
484
  }
455
- const path17 = payload["path"];
456
- if (path17 === void 0 || path17 === null) {
485
+ const path18 = payload["path"];
486
+ if (path18 === void 0 || path18 === null) {
457
487
  return { ok: true, value: { path: "" } };
458
488
  }
459
- if (typeof path17 !== "string") {
489
+ if (typeof path18 !== "string") {
460
490
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
461
491
  }
462
- return { ok: true, value: { path: path17 } };
492
+ return { ok: true, value: { path: path18 } };
463
493
  }
464
494
  function validateProjectsAddPayload(payload) {
465
495
  if (!isRecord(payload)) {
@@ -709,8 +739,57 @@ async function handleWorklistMessage(ctx, ws, msg) {
709
739
 
710
740
  // src/server/index.ts
711
741
  import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
712
- import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as wstackGlobalRoot2, projectHash } from "@wrongstack/core/utils";
742
+ import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as wstackGlobalRoot3, projectHash } from "@wrongstack/core/utils";
713
743
  import { SkillInstaller } from "@wrongstack/core/skills";
744
+
745
+ // src/server/discover-mailbox-bridge.ts
746
+ import { resolveProjectDir, wstackGlobalRoot } from "@wrongstack/core";
747
+ import { readLiveLock } from "@wrongstack/core/coordination";
748
+ async function discoverMailboxBridgeForWebui(params) {
749
+ const mode = params.config?.features?.mailboxBridge ?? "auto";
750
+ if (mode === "off") return;
751
+ const projectDir = resolveProjectDir(params.projectRoot, wstackGlobalRoot());
752
+ const result = await readLiveLock(projectDir);
753
+ switch (result.kind) {
754
+ case "live": {
755
+ params.logger.debug("webui joined existing mailbox bridge", {
756
+ url: result.lock.url,
757
+ lockPath: projectDir
758
+ });
759
+ params.ctx.meta["mailboxBridge"] = {
760
+ url: result.lock.url,
761
+ token: result.lock.token,
762
+ lockPath: projectDir,
763
+ childPid: null,
764
+ source: "joined"
765
+ };
766
+ break;
767
+ }
768
+ case "probe-failed": {
769
+ params.logger.warn(
770
+ "mailbox bridge present but /healthz unreachable; webui will start without external-agent connectivity",
771
+ { url: result.lock.url, lockPath: projectDir }
772
+ );
773
+ params.ctx.meta["mailboxBridge"] = {
774
+ url: result.lock.url,
775
+ token: result.lock.token,
776
+ lockPath: projectDir,
777
+ childPid: null,
778
+ source: "unhealthy"
779
+ };
780
+ break;
781
+ }
782
+ case "absent": {
783
+ params.logger.info(
784
+ "no mailbox bridge running; webui will start without external-agent connectivity. Run `wstack mailbox serve` or a CLI surface to bring one up.",
785
+ { projectDir }
786
+ );
787
+ break;
788
+ }
789
+ }
790
+ }
791
+
792
+ // src/server/index.ts
714
793
  import {
715
794
  BrainMonitor,
716
795
  DefaultBrainArbiter,
@@ -718,8 +797,9 @@ import {
718
797
  createAutonomyBrain,
719
798
  createTieredBrainArbiter
720
799
  } from "@wrongstack/core";
721
- import * as fs13 from "fs/promises";
722
- import * as path16 from "path";
800
+ import * as fs14 from "fs/promises";
801
+ import * as path17 from "path";
802
+ import { createRequire as createRequire2 } from "module";
723
803
 
724
804
  // src/server/http-server.ts
725
805
  import * as fs from "fs/promises";
@@ -896,7 +976,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
896
976
  return;
897
977
  }
898
978
  try {
899
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
979
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
900
980
  const registry = new SessionRegistry(globalRoot);
901
981
  const entry = await registry.get(sessionId);
902
982
  if (!entry) {
@@ -904,7 +984,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
904
984
  res.end(JSON.stringify({ error: "Session not found" }));
905
985
  return;
906
986
  }
907
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
987
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
908
988
  const store = new DefaultSessionStore3({ dir: paths.projectSessions });
909
989
  const reader = new DefaultSessionReader2({ store });
910
990
  const rawEntries = [];
@@ -931,7 +1011,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
931
1011
  }
932
1012
  }
933
1013
  function readJsonBody(req) {
934
- return new Promise((resolve9, reject) => {
1014
+ return new Promise((resolve10, reject) => {
935
1015
  let data = "";
936
1016
  req.on("data", (chunk) => {
937
1017
  data += chunk;
@@ -942,7 +1022,7 @@ function readJsonBody(req) {
942
1022
  });
943
1023
  req.on("end", () => {
944
1024
  try {
945
- resolve9(data ? JSON.parse(data) : {});
1025
+ resolve10(data ? JSON.parse(data) : {});
946
1026
  } catch (err) {
947
1027
  reject(err instanceof Error ? err : new Error(String(err)));
948
1028
  }
@@ -978,7 +1058,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
978
1058
  const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
979
1059
  const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
980
1060
  try {
981
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1061
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
982
1062
  const registry = new SessionRegistry(globalRoot);
983
1063
  const entry = await registry.get(sessionId);
984
1064
  if (!entry) {
@@ -986,7 +1066,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
986
1066
  res.end(JSON.stringify({ error: "Session not found" }));
987
1067
  return;
988
1068
  }
989
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
1069
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
990
1070
  const mailbox = new GlobalMailbox3(paths.projectDir);
991
1071
  const to = `leader@${mailboxSessionTag2(sessionId)}`;
992
1072
  const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
@@ -1004,7 +1084,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
1004
1084
  return;
1005
1085
  }
1006
1086
  try {
1007
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1087
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1008
1088
  const registry = new SessionRegistry(globalRoot);
1009
1089
  const entry = await registry.get(sessionId);
1010
1090
  if (!entry) {
@@ -1012,7 +1092,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
1012
1092
  res.end(JSON.stringify({ error: "Session not found" }));
1013
1093
  return;
1014
1094
  }
1015
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
1095
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
1016
1096
  const mailbox = new GlobalMailbox3(paths.projectDir);
1017
1097
  const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
1018
1098
  const [inbound, outbound] = await Promise.all([
@@ -1062,7 +1142,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
1062
1142
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
1063
1143
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
1064
1144
  try {
1065
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1145
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1066
1146
  const registry = new SessionRegistry(globalRoot);
1067
1147
  const entry = await registry.get(sessionId);
1068
1148
  if (!entry) {
@@ -1070,7 +1150,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
1070
1150
  res.end(JSON.stringify({ error: "Session not found" }));
1071
1151
  return;
1072
1152
  }
1073
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
1153
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
1074
1154
  const mailbox = new GlobalMailbox3(paths.projectDir);
1075
1155
  const to = `leader@${mailboxSessionTag2(sessionId)}`;
1076
1156
  const sent = await mailbox.send({
@@ -1110,7 +1190,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
1110
1190
  }
1111
1191
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
1112
1192
  try {
1113
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1193
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1114
1194
  const registry = new SessionRegistry(globalRoot);
1115
1195
  const all = await registry.list();
1116
1196
  const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
@@ -1122,7 +1202,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
1122
1202
  }
1123
1203
  const mbByDir = /* @__PURE__ */ new Map();
1124
1204
  const mailboxFor = (projectRoot) => {
1125
- const dir = resolveWstackPaths2({ projectRoot, globalRoot }).projectDir;
1205
+ const dir = resolveWstackPaths4({ projectRoot, globalRoot }).projectDir;
1126
1206
  let mb = mbByDir.get(dir);
1127
1207
  if (!mb) {
1128
1208
  mb = new GlobalMailbox3(dir);
@@ -1333,7 +1413,7 @@ function buildCspHeader(wsPort, requestHost, publicWsUrl) {
1333
1413
  `ws://127.0.0.1:${wsPort}`,
1334
1414
  `wss://127.0.0.1:${wsPort}`
1335
1415
  ]);
1336
- if (requestHost && requestHost !== "127.0.0.1") {
1416
+ if (requestHost && requestHost !== "127.0.0.1" && requestHost !== "::1" && requestHost !== "[::1]") {
1337
1417
  const host = formatCspHostname(requestHost);
1338
1418
  connect.add(`ws://${host}:${wsPort}`);
1339
1419
  connect.add(`wss://${host}:${wsPort}`);
@@ -1858,6 +1938,16 @@ async function realpathAllowMissing(p) {
1858
1938
  }
1859
1939
  }
1860
1940
  }
1941
+ function validatedPayload(msg, label) {
1942
+ if (msg == null || typeof msg !== "object") {
1943
+ throw new TypeError(`Expected object for ${label}, got ${msg}`);
1944
+ }
1945
+ const payload = msg.payload;
1946
+ if (payload == null || typeof payload !== "object") {
1947
+ throw new TypeError(`Expected payload object for ${label}, got ${payload}`);
1948
+ }
1949
+ return payload;
1950
+ }
1861
1951
  async function handleFilesTree(ws, msg, projectRoot) {
1862
1952
  const payload = msg.payload;
1863
1953
  const rawPath = payload?.path?.trim();
@@ -1928,7 +2018,13 @@ async function handleFilesTree(ws, msg, projectRoot) {
1928
2018
  }
1929
2019
  }
1930
2020
  async function handleFilesRead(ws, msg, projectRoot) {
1931
- const { filePath } = msg.payload;
2021
+ let filePath;
2022
+ try {
2023
+ ({ filePath } = validatedPayload(msg, "files.read"));
2024
+ } catch {
2025
+ send(ws, { type: "files.read", payload: { filePath: "", content: "", error: "Malformed request" } });
2026
+ return;
2027
+ }
1932
2028
  let realResolved;
1933
2029
  try {
1934
2030
  realResolved = await resolveFileInsideProject(projectRoot, filePath);
@@ -1947,7 +2043,14 @@ async function handleFilesRead(ws, msg, projectRoot) {
1947
2043
  }
1948
2044
  }
1949
2045
  async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1950
- const { filePath, content } = msg.payload;
2046
+ let filePath;
2047
+ let content;
2048
+ try {
2049
+ ({ filePath, content } = validatedPayload(msg, "files.write"));
2050
+ } catch {
2051
+ send(ws, { type: "files.written", payload: { filePath: "", success: false, error: "Malformed request" } });
2052
+ return;
2053
+ }
1951
2054
  let realResolved;
1952
2055
  try {
1953
2056
  realResolved = await resolveFileInsideProject(projectRoot, filePath);
@@ -2785,7 +2888,7 @@ import { promises as fs5 } from "fs";
2785
2888
  import path6 from "path";
2786
2889
  import JSZip from "jszip";
2787
2890
  import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
2788
- import { wstackGlobalRoot } from "@wrongstack/core/utils";
2891
+ import { wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
2789
2892
  async function handleSkillsList(ws, ctx) {
2790
2893
  if (!ctx.skillLoader) {
2791
2894
  send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
@@ -2955,7 +3058,7 @@ async function handleSkillsCreate(ws, ctx, msg) {
2955
3058
  }
2956
3059
  const createPayload = parsed.value;
2957
3060
  try {
2958
- const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
3061
+ const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2959
3062
  try {
2960
3063
  await fs5.access(targetDir);
2961
3064
  send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
@@ -3075,6 +3178,416 @@ async function handleSkillsExport(ws, ctx) {
3075
3178
  }
3076
3179
  }
3077
3180
 
3181
+ // src/server/prompts-handlers.ts
3182
+ function parseVariablesPayload(raw) {
3183
+ if (!Array.isArray(raw)) return void 0;
3184
+ const out = [];
3185
+ for (const item of raw) {
3186
+ if (!item || typeof item !== "object") continue;
3187
+ const o = item;
3188
+ if (typeof o["name"] !== "string" || !o["name"].trim()) continue;
3189
+ const enumVals = Array.isArray(o["enum"]) && o["enum"].every((x) => typeof x === "string") ? o["enum"].map((s) => s.trim()).filter(Boolean) : void 0;
3190
+ const v = { name: o["name"].trim() };
3191
+ if (typeof o["description"] === "string" && o["description"].trim()) {
3192
+ v.description = o["description"].trim();
3193
+ }
3194
+ if (o["required"] === true) v.required = true;
3195
+ if (o["multiline"] === true) v.multiline = true;
3196
+ if (enumVals && enumVals.length > 0) v.enum = enumVals;
3197
+ out.push(v);
3198
+ }
3199
+ return out.length > 0 ? out : void 0;
3200
+ }
3201
+ function toMeta(e) {
3202
+ return {
3203
+ id: e.id,
3204
+ slug: e.slug,
3205
+ title: e.title,
3206
+ description: e.description,
3207
+ category: e.category,
3208
+ tags: e.tags,
3209
+ source: e.source,
3210
+ favorite: e.favorite,
3211
+ variables: e.variables ?? []
3212
+ };
3213
+ }
3214
+ async function handlePromptsList(ws, ctx) {
3215
+ if (!ctx.promptLoader) {
3216
+ send(ws, { type: "prompts.list", payload: { enabled: false, prompts: [], categories: [] } });
3217
+ return;
3218
+ }
3219
+ try {
3220
+ const [all, categories] = await Promise.all([
3221
+ ctx.promptLoader.list(),
3222
+ ctx.promptLoader.categories()
3223
+ ]);
3224
+ send(ws, {
3225
+ type: "prompts.list",
3226
+ payload: { enabled: true, prompts: all.map(toMeta), categories }
3227
+ });
3228
+ } catch (err) {
3229
+ send(ws, {
3230
+ type: "prompts.list",
3231
+ payload: { enabled: true, prompts: [], categories: [], error: errMessage(err) }
3232
+ });
3233
+ }
3234
+ }
3235
+ async function handlePromptsSearch(ws, ctx, msg) {
3236
+ if (!ctx.promptLoader) {
3237
+ send(ws, { type: "prompts.search", payload: { enabled: false, prompts: [] } });
3238
+ return;
3239
+ }
3240
+ const payload = msg.payload ?? {};
3241
+ try {
3242
+ const results = await ctx.promptLoader.search(payload.query ?? "", {
3243
+ ...payload.category ? { category: payload.category } : {},
3244
+ limit: 50
3245
+ });
3246
+ send(ws, { type: "prompts.search", payload: { enabled: true, prompts: results.map(toMeta) } });
3247
+ } catch (err) {
3248
+ send(ws, {
3249
+ type: "prompts.search",
3250
+ payload: { enabled: true, prompts: [], error: errMessage(err) }
3251
+ });
3252
+ }
3253
+ }
3254
+ async function handlePromptsContent(ws, ctx, msg) {
3255
+ const slug = msg.payload?.slug;
3256
+ if (!ctx.promptLoader || !slug) {
3257
+ send(ws, {
3258
+ type: "prompts.content",
3259
+ payload: { slug: slug ?? "", found: false, content: "", variables: [] }
3260
+ });
3261
+ return;
3262
+ }
3263
+ try {
3264
+ const entry = await ctx.promptLoader.find(slug);
3265
+ if (!entry) {
3266
+ send(ws, {
3267
+ type: "prompts.content",
3268
+ payload: { slug, found: false, content: "", variables: [] }
3269
+ });
3270
+ return;
3271
+ }
3272
+ send(ws, {
3273
+ type: "prompts.content",
3274
+ payload: {
3275
+ slug: entry.slug,
3276
+ found: true,
3277
+ title: entry.title,
3278
+ content: entry.content,
3279
+ variables: entry.variables ?? [],
3280
+ category: entry.category,
3281
+ source: entry.source
3282
+ }
3283
+ });
3284
+ } catch (err) {
3285
+ send(ws, {
3286
+ type: "prompts.content",
3287
+ payload: { slug, found: false, content: "", variables: [], error: errMessage(err) }
3288
+ });
3289
+ }
3290
+ }
3291
+ async function handlePromptsFavorite(ws, ctx, msg) {
3292
+ const payload = msg.payload;
3293
+ if (!ctx.promptLoader || !payload?.slug) {
3294
+ send(ws, {
3295
+ type: "prompts.favorite",
3296
+ payload: { success: false, error: "Prompt library unavailable" }
3297
+ });
3298
+ return;
3299
+ }
3300
+ try {
3301
+ const updated = await ctx.promptLoader.setFavorite(payload.slug, payload.favorite !== false);
3302
+ if (!updated) {
3303
+ send(ws, {
3304
+ type: "prompts.favorite",
3305
+ payload: { success: false, error: "Prompt not found" }
3306
+ });
3307
+ return;
3308
+ }
3309
+ send(ws, {
3310
+ type: "prompts.favorite",
3311
+ payload: { success: true, slug: updated.slug, favorite: updated.favorite }
3312
+ });
3313
+ } catch (err) {
3314
+ send(ws, { type: "prompts.favorite", payload: { success: false, error: errMessage(err) } });
3315
+ }
3316
+ }
3317
+ async function handlePromptsCreate(ws, ctx, msg) {
3318
+ const p = msg.payload;
3319
+ if (!ctx.promptLoader || !p) {
3320
+ send(ws, {
3321
+ type: "prompts.created",
3322
+ payload: { success: false, error: "Prompt library unavailable" }
3323
+ });
3324
+ return;
3325
+ }
3326
+ const title = typeof p["title"] === "string" ? p["title"].trim() : "";
3327
+ const content = typeof p["content"] === "string" ? p["content"] : "";
3328
+ if (!title || !content) {
3329
+ send(ws, {
3330
+ type: "prompts.created",
3331
+ payload: { success: false, error: "Title and content are required" }
3332
+ });
3333
+ return;
3334
+ }
3335
+ try {
3336
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3337
+ const tags = Array.isArray(p["tags"]) ? p["tags"].filter((t) => typeof t === "string") : [];
3338
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "prompt";
3339
+ const variables = parseVariablesPayload(p["variables"]);
3340
+ const entry = {
3341
+ id: slug,
3342
+ slug,
3343
+ title,
3344
+ description: typeof p["description"] === "string" ? p["description"] : "",
3345
+ content,
3346
+ category: typeof p["category"] === "string" && p["category"] ? p["category"] : "uncategorized",
3347
+ tags,
3348
+ source: "user",
3349
+ favorite: false,
3350
+ ...variables ? { variables } : {},
3351
+ createdAt: now,
3352
+ updatedAt: now
3353
+ };
3354
+ await ctx.promptLoader.save(entry);
3355
+ send(ws, { type: "prompts.created", payload: { success: true, slug } });
3356
+ } catch (err) {
3357
+ send(ws, { type: "prompts.created", payload: { success: false, error: errMessage(err) } });
3358
+ }
3359
+ }
3360
+ async function handlePromptsUsed(ws, ctx, msg) {
3361
+ const slug = msg.payload?.slug;
3362
+ if (!ctx.promptUsage || !slug) {
3363
+ send(ws, { type: "prompts.used", payload: { success: false } });
3364
+ return;
3365
+ }
3366
+ try {
3367
+ await ctx.promptUsage.record(slug);
3368
+ send(ws, { type: "prompts.used", payload: { success: true, slug } });
3369
+ } catch {
3370
+ send(ws, { type: "prompts.used", payload: { success: false } });
3371
+ }
3372
+ }
3373
+ async function handlePromptsRecent(ws, ctx) {
3374
+ if (!ctx.promptUsage) {
3375
+ send(ws, { type: "prompts.recent", payload: { slugs: [] } });
3376
+ return;
3377
+ }
3378
+ try {
3379
+ const recent = await ctx.promptUsage.recent(50);
3380
+ send(ws, { type: "prompts.recent", payload: { slugs: recent.map((r) => r.slug) } });
3381
+ } catch (err) {
3382
+ send(ws, { type: "prompts.recent", payload: { slugs: [], error: errMessage(err) } });
3383
+ }
3384
+ }
3385
+
3386
+ // src/server/design-handlers.ts
3387
+ import * as fs6 from "fs/promises";
3388
+ import * as path7 from "path";
3389
+ import {
3390
+ applyTokenOverrides,
3391
+ getDesignKitLoader,
3392
+ getDesignState,
3393
+ isDesignStack,
3394
+ loadActiveKit,
3395
+ materializeTokens,
3396
+ recordKitChoice,
3397
+ recordOverrides,
3398
+ runDesignVerify,
3399
+ setActiveKit,
3400
+ setDesignOverrides
3401
+ } from "@wrongstack/core";
3402
+ function readOverrides(value) {
3403
+ const out = {};
3404
+ if (value && typeof value === "object") {
3405
+ for (const [k, v] of Object.entries(value)) {
3406
+ if (typeof v === "string") out[k] = v;
3407
+ }
3408
+ }
3409
+ return out;
3410
+ }
3411
+ var FOUNDATIONS_ID = "_foundations";
3412
+ async function buildListPayload(ctx) {
3413
+ const loader = getDesignKitLoader(ctx.projectRoot);
3414
+ const manifests = (await loader.list()).filter((k) => k.id !== FOUNDATIONS_ID);
3415
+ const kits = [];
3416
+ for (const m of manifests) {
3417
+ const tokens = await loader.readTokens(m.id);
3418
+ kits.push({
3419
+ id: m.id,
3420
+ name: m.name,
3421
+ aesthetic: m.aesthetic,
3422
+ bestFor: m.bestFor,
3423
+ stacks: m.stacks,
3424
+ tags: m.tags,
3425
+ light: tokens?.light ?? {},
3426
+ dark: tokens?.dark ?? {}
3427
+ });
3428
+ }
3429
+ const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
3430
+ const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
3431
+ return {
3432
+ kits,
3433
+ activeKit: state?.activeKit ?? persisted?.kit ?? null,
3434
+ stack: state?.stack ?? persisted?.stack ?? null,
3435
+ overrides: state?.overrides ?? persisted?.overrides ?? {}
3436
+ };
3437
+ }
3438
+ async function handleDesignList(ws, ctx) {
3439
+ try {
3440
+ send(ws, { type: "design.list", payload: await buildListPayload(ctx) });
3441
+ } catch (err) {
3442
+ send(ws, {
3443
+ type: "design.list",
3444
+ payload: { kits: [], activeKit: null, stack: null, error: String(err) }
3445
+ });
3446
+ }
3447
+ }
3448
+ async function handleDesignState(ws, ctx) {
3449
+ const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
3450
+ send(ws, {
3451
+ type: "design.state",
3452
+ payload: {
3453
+ activeKit: state?.activeKit ?? null,
3454
+ stack: state?.stack ?? null,
3455
+ overrides: state?.overrides ?? {}
3456
+ }
3457
+ });
3458
+ }
3459
+ async function handleDesignUse(ws, ctx, msg) {
3460
+ const payload = msg.payload ?? {};
3461
+ const kitId = typeof payload.kit === "string" ? payload.kit.trim() : "";
3462
+ if (!kitId) {
3463
+ send(ws, { type: "design.use", payload: { ok: false, error: "No kit id provided" } });
3464
+ return;
3465
+ }
3466
+ try {
3467
+ const loader = getDesignKitLoader(ctx.projectRoot);
3468
+ const kit = await loader.find(kitId);
3469
+ if (!kit) {
3470
+ send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: "Kit not found" } });
3471
+ return;
3472
+ }
3473
+ const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
3474
+ const stack = stackArg && isDesignStack(stackArg) ? stackArg : kit.stacks[0] ?? "web";
3475
+ const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
3476
+ const keep = persisted?.kit === kit.id ? persisted.overrides ?? {} : {};
3477
+ const overrides = { ...keep, ...readOverrides(payload.overrides) };
3478
+ if (ctx.agentMeta) setActiveKit(ctx.agentMeta, kit.id, stack, overrides);
3479
+ await recordKitChoice(
3480
+ ctx.projectRoot,
3481
+ kit.id,
3482
+ stack,
3483
+ "webui",
3484
+ (/* @__PURE__ */ new Date()).toISOString(),
3485
+ Object.keys(overrides).length ? overrides : void 0
3486
+ );
3487
+ const body = await loader.readBody(kit.id, stack);
3488
+ const rawTokens = await loader.readTokens(kit.id);
3489
+ const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
3490
+ send(ws, {
3491
+ type: "design.use",
3492
+ payload: {
3493
+ ok: true,
3494
+ kit: kit.id,
3495
+ name: kit.name,
3496
+ aesthetic: kit.aesthetic,
3497
+ stack,
3498
+ body,
3499
+ overrides,
3500
+ light: tokens?.light ?? {},
3501
+ dark: tokens?.dark ?? {}
3502
+ }
3503
+ });
3504
+ } catch (err) {
3505
+ send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: String(err) } });
3506
+ }
3507
+ }
3508
+ async function handleDesignSet(ws, ctx, msg) {
3509
+ const patch = readOverrides(msg.payload?.overrides);
3510
+ if (Object.keys(patch).length === 0) {
3511
+ send(ws, { type: "design.set", payload: { ok: false, error: "No overrides provided" } });
3512
+ return;
3513
+ }
3514
+ try {
3515
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
3516
+ if (!merged) {
3517
+ send(ws, { type: "design.set", payload: { ok: false, error: "No active kit" } });
3518
+ return;
3519
+ }
3520
+ if (ctx.agentMeta) setDesignOverrides(ctx.agentMeta, merged);
3521
+ send(ws, { type: "design.set", payload: { ok: true, overrides: merged } });
3522
+ } catch (err) {
3523
+ send(ws, { type: "design.set", payload: { ok: false, error: String(err) } });
3524
+ }
3525
+ }
3526
+ async function handleDesignMaterialize(ws, ctx, msg) {
3527
+ const payload = msg.payload ?? {};
3528
+ try {
3529
+ const active = await loadActiveKit(ctx.projectRoot);
3530
+ if (!active) {
3531
+ send(ws, { type: "design.materialize", payload: { ok: false, error: "No active kit" } });
3532
+ return;
3533
+ }
3534
+ const loader = getDesignKitLoader(ctx.projectRoot);
3535
+ const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
3536
+ const stack = stackArg && isDesignStack(stackArg) ? stackArg : active.stack && isDesignStack(active.stack) ? active.stack : "web";
3537
+ const raw = await loader.readTokens(active.kit);
3538
+ if (!raw) {
3539
+ send(ws, { type: "design.materialize", payload: { ok: false, error: "Kit has no tokens" } });
3540
+ return;
3541
+ }
3542
+ const tokens = applyTokenOverrides(raw, active.overrides);
3543
+ const result = materializeTokens({
3544
+ tokens,
3545
+ stack,
3546
+ kitId: active.kit,
3547
+ outPath: typeof payload.out === "string" ? payload.out : void 0
3548
+ });
3549
+ const abs = path7.join(ctx.projectRoot, result.path);
3550
+ await fs6.mkdir(path7.dirname(abs), { recursive: true });
3551
+ await fs6.writeFile(abs, result.content);
3552
+ send(ws, {
3553
+ type: "design.materialize",
3554
+ payload: { ok: true, path: result.path, format: result.format, stack }
3555
+ });
3556
+ } catch (err) {
3557
+ send(ws, { type: "design.materialize", payload: { ok: false, error: String(err) } });
3558
+ }
3559
+ }
3560
+ async function handleDesignVerify(ws, ctx) {
3561
+ try {
3562
+ const active = await loadActiveKit(ctx.projectRoot);
3563
+ if (!active) {
3564
+ send(ws, { type: "design.verify", payload: { ok: false, error: "No active kit" } });
3565
+ return;
3566
+ }
3567
+ const loader = getDesignKitLoader(ctx.projectRoot);
3568
+ const raw = await loader.readTokens(active.kit);
3569
+ if (!raw) {
3570
+ send(ws, { type: "design.verify", payload: { ok: false, error: "Kit has no tokens" } });
3571
+ return;
3572
+ }
3573
+ const tokens = applyTokenOverrides(raw, active.overrides);
3574
+ const report = await runDesignVerify(ctx.projectRoot, tokens);
3575
+ send(ws, {
3576
+ type: "design.verify",
3577
+ payload: {
3578
+ ok: true,
3579
+ kit: active.kit,
3580
+ filesScanned: report.filesScanned,
3581
+ score: report.score,
3582
+ violations: report.violations.slice(0, 50),
3583
+ violationCount: report.violations.length
3584
+ }
3585
+ });
3586
+ } catch (err) {
3587
+ send(ws, { type: "design.verify", payload: { ok: false, error: String(err) } });
3588
+ }
3589
+ }
3590
+
3078
3591
  // src/server/index.ts
3079
3592
  import {
3080
3593
  Agent,
@@ -3086,6 +3599,8 @@ import {
3086
3599
  DefaultSessionReader,
3087
3600
  DefaultSessionStore as DefaultSessionStore2,
3088
3601
  DefaultSkillLoader,
3602
+ DefaultPromptLoader,
3603
+ PromptUsageStore,
3089
3604
  DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
3090
3605
  DefaultTokenCounter,
3091
3606
  AnnotationsStore,
@@ -3100,17 +3615,20 @@ import {
3100
3615
  ToolRegistry,
3101
3616
  atomicWrite as atomicWrite6,
3102
3617
  createDefaultPipelines,
3618
+ installDesignStudioMiddleware,
3103
3619
  createSessionEventBridge,
3104
3620
  resolveSessionLoggingConfig,
3105
3621
  DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
3106
3622
  DEFAULT_SESSION_PRUNE_DAYS,
3107
3623
  DEFAULT_TOOLS_CONFIG,
3108
3624
  applyToolDescriptionModes,
3625
+ applyToolResultRenderModes,
3109
3626
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
3110
3627
  enhanceUserPrompt,
3111
3628
  gatedEnhancerReasoning,
3112
3629
  recentTextTurns,
3113
- resolveProviderModelList
3630
+ resolveProviderModelList,
3631
+ cleanupStaleSddWorktrees as cleanupStaleSddWorktrees3
3114
3632
  } from "@wrongstack/core";
3115
3633
  import { ToolExecutor } from "@wrongstack/core/execution";
3116
3634
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
@@ -3793,7 +4311,7 @@ var SpecsWebSocketHandler = class {
3793
4311
  };
3794
4312
 
3795
4313
  // src/server/sdd-board-ws-handler.ts
3796
- import { SddBoardStore } from "@wrongstack/core";
4314
+ import { applySddLifecycle, SddBoardStore } from "@wrongstack/core";
3797
4315
  var CONTROL_TYPES = /* @__PURE__ */ new Set([
3798
4316
  "pause",
3799
4317
  "resume",
@@ -3807,19 +4325,19 @@ var CONTROL_TYPES = /* @__PURE__ */ new Set([
3807
4325
  "set_task_verification",
3808
4326
  "cancel_task",
3809
4327
  "delete_task",
3810
- "split_task",
3811
- // Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
3812
- "cleanup_worktrees",
3813
- "rollback"
4328
+ "split_task"
3814
4329
  ]);
4330
+ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set(["cleanup_worktrees", "rollback", "destroy"]);
3815
4331
  var SddBoardWebSocketHandler = class {
3816
4332
  store;
3817
4333
  clients = /* @__PURE__ */ new Set();
4334
+ lifecycle;
3818
4335
  latest = null;
3819
4336
  poll = null;
3820
4337
  unsub = null;
3821
- constructor(boardsDir, events) {
4338
+ constructor(boardsDir, events, lifecycle) {
3822
4339
  this.store = new SddBoardStore({ baseDir: boardsDir });
4340
+ this.lifecycle = lifecycle;
3823
4341
  if (events) {
3824
4342
  const handler = (e) => {
3825
4343
  this.latest = e.snapshot;
@@ -3848,6 +4366,10 @@ var SddBoardWebSocketHandler = class {
3848
4366
  return;
3849
4367
  }
3850
4368
  const action = msg.type.replace(/^sdd\.board\./, "");
4369
+ if (LIFECYCLE_TYPES.has(action)) {
4370
+ await this.applyLifecycle(action, msg.payload);
4371
+ return;
4372
+ }
3851
4373
  if (CONTROL_TYPES.has(action)) {
3852
4374
  const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
3853
4375
  if (runId) {
@@ -3859,6 +4381,40 @@ var SddBoardWebSocketHandler = class {
3859
4381
  }
3860
4382
  }
3861
4383
  }
4384
+ /**
4385
+ * Apply a cleanup/rollback/destroy from disk and broadcast a structured
4386
+ * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
4387
+ * the user must stop it first; the UI gates the buttons on `!active` and the
4388
+ * Destroy flow auto-stops then waits before sending `destroy`.
4389
+ */
4390
+ async applyLifecycle(op, payload) {
4391
+ if (!this.lifecycle) {
4392
+ this.broadcast({
4393
+ type: "sdd.board.lifecycle_result",
4394
+ payload: { op, ok: false, reason: "Lifecycle operations are not available in this session." }
4395
+ });
4396
+ return;
4397
+ }
4398
+ if (this.latest && (this.latest.status === "running" || this.latest.status === "paused")) {
4399
+ this.broadcast({
4400
+ type: "sdd.board.lifecycle_result",
4401
+ payload: { op, ok: false, reason: "Stop the run first, then retry." }
4402
+ });
4403
+ return;
4404
+ }
4405
+ const runId = payload?.runId ?? this.latest?.runId;
4406
+ const result = await applySddLifecycle(op, {
4407
+ projectRoot: this.lifecycle.projectRoot,
4408
+ paths: this.lifecycle.paths,
4409
+ runId,
4410
+ revertMerged: payload?.revertMerged === true
4411
+ });
4412
+ this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
4413
+ if (op === "destroy" && result.ok) {
4414
+ this.latest = null;
4415
+ this.broadcast({ type: "sdd.board.snapshot", payload: null });
4416
+ }
4417
+ }
3862
4418
  dispose() {
3863
4419
  if (this.poll) clearInterval(this.poll);
3864
4420
  this.unsub?.();
@@ -4038,9 +4594,10 @@ var SddWizardWebSocketHandler = class {
4038
4594
  };
4039
4595
 
4040
4596
  // src/server/sdd-wizard-wiring.ts
4041
- import * as path7 from "path";
4597
+ import * as path8 from "path";
4042
4598
  import { spawnSync as spawnSync2 } from "child_process";
4043
4599
  import {
4600
+ cleanupStaleSddWorktrees,
4044
4601
  makeCommandVerifier,
4045
4602
  makeLlmSubtaskGenerator,
4046
4603
  SddBoardStore as SddBoardStore2,
@@ -4075,7 +4632,7 @@ function buildSddWizardDeps(opts) {
4075
4632
  makeDriver: () => new SddInterviewDriver({
4076
4633
  specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
4077
4634
  graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
4078
- sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
4635
+ sessionPath: path8.join(opts.paths.projectDir, "sdd-wizard-session.json")
4079
4636
  }),
4080
4637
  runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
4081
4638
  startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels, worktrees: useWorktrees }) => {
@@ -4092,7 +4649,13 @@ function buildSddWizardDeps(opts) {
4092
4649
  encoding: "utf8",
4093
4650
  windowsHide: true
4094
4651
  }).stdout?.trim() === "true";
4095
- if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
4652
+ if (inGit) {
4653
+ await cleanupStaleSddWorktrees({
4654
+ projectRoot: opts.projectRoot,
4655
+ boardsDir: opts.paths.projectSddBoards
4656
+ }).catch(() => void 0);
4657
+ worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
4658
+ }
4096
4659
  }
4097
4660
  const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
4098
4661
  const verifyTask = makeCommandVerifier();
@@ -4871,16 +5434,16 @@ var CollaborationWebSocketHandler = class {
4871
5434
  };
4872
5435
 
4873
5436
  // src/server/projects-manifest.ts
4874
- import * as fs6 from "fs/promises";
4875
- import * as path8 from "path";
5437
+ import * as fs7 from "fs/promises";
5438
+ import * as path9 from "path";
4876
5439
  import { projectSlug } from "@wrongstack/core";
4877
5440
  function projectsJsonPath(globalConfigPath) {
4878
- const base = path8.dirname(globalConfigPath);
4879
- return path8.join(base, "projects.json");
5441
+ const base = path9.dirname(globalConfigPath);
5442
+ return path9.join(base, "projects.json");
4880
5443
  }
4881
5444
  async function loadManifest(globalConfigPath) {
4882
5445
  try {
4883
- const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
5446
+ const raw = await fs7.readFile(projectsJsonPath(globalConfigPath), "utf8");
4884
5447
  const parsed = JSON.parse(raw);
4885
5448
  return { projects: parsed.projects ?? [] };
4886
5449
  } catch {
@@ -4889,16 +5452,16 @@ async function loadManifest(globalConfigPath) {
4889
5452
  }
4890
5453
  async function saveManifest(manifest, globalConfigPath) {
4891
5454
  const file = projectsJsonPath(globalConfigPath);
4892
- await fs6.mkdir(path8.dirname(file), { recursive: true });
4893
- await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
5455
+ await fs7.mkdir(path9.dirname(file), { recursive: true });
5456
+ await fs7.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4894
5457
  }
4895
5458
  function generateProjectSlug(rootPath) {
4896
5459
  return projectSlug(rootPath);
4897
5460
  }
4898
5461
  async function ensureProjectDataDir(slug, globalConfigPath) {
4899
- const base = path8.dirname(globalConfigPath);
4900
- const dir = path8.join(base, "projects", slug);
4901
- await fs6.mkdir(dir, { recursive: true });
5462
+ const base = path9.dirname(globalConfigPath);
5463
+ const dir = path9.join(base, "projects", slug);
5464
+ await fs7.mkdir(dir, { recursive: true });
4902
5465
  return dir;
4903
5466
  }
4904
5467
 
@@ -5060,16 +5623,22 @@ function clampDim(value, fallback) {
5060
5623
  }
5061
5624
 
5062
5625
  // src/server/worktree-ws-handler.ts
5626
+ import { join as join6, resolve as resolve6, sep as sep4 } from "path";
5627
+ import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2, WorktreeManager as WorktreeManager3 } from "@wrongstack/core";
5063
5628
  import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
5064
5629
  var MAX_ACTIVITY = 6;
5630
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
5631
+ var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
5065
5632
  var WorktreeWebSocketHandler = class {
5066
- constructor(events, logger) {
5633
+ constructor(events, logger, management) {
5067
5634
  this.events = events;
5068
5635
  this.logger = logger;
5636
+ this.management = management;
5069
5637
  this.subscribe();
5070
5638
  }
5071
5639
  events;
5072
5640
  logger;
5641
+ management;
5073
5642
  clients = /* @__PURE__ */ new Set();
5074
5643
  handles = /* @__PURE__ */ new Map();
5075
5644
  baseBranch = "";
@@ -5080,12 +5649,197 @@ var WorktreeWebSocketHandler = class {
5080
5649
  ws.on("close", () => this.clients.delete(ws));
5081
5650
  ws.on("error", () => this.clients.delete(ws));
5082
5651
  this.send(ws, this.stateMessage());
5652
+ void this.scanAndBroadcast();
5653
+ }
5654
+ /** Handle worktree-panel control messages (scan / clean / per-row ops). */
5655
+ async handleMessage(msg) {
5656
+ if (msg.type === "worktree.scan") {
5657
+ await this.scanAndBroadcast();
5658
+ return true;
5659
+ }
5660
+ if (msg.type === "worktree.cleanup") {
5661
+ await this.cleanupOrphans();
5662
+ return true;
5663
+ }
5664
+ if (msg.type === "worktree.remove") {
5665
+ await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
5666
+ return true;
5667
+ }
5668
+ if (msg.type === "worktree.merge") {
5669
+ await this.mergeBranch(msg.payload?.["branch"]);
5670
+ return true;
5671
+ }
5672
+ if (msg.type === "worktree.diff") {
5673
+ await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
5674
+ return true;
5675
+ }
5676
+ return false;
5083
5677
  }
5084
5678
  dispose() {
5085
5679
  for (const off of this.offs) off();
5086
5680
  this.offs.length = 0;
5087
5681
  this.stopBroadcast();
5088
5682
  }
5683
+ // ── orphan management ─────────────────────────────────────────────────────
5684
+ /** Absolute managed-worktrees root for this project. */
5685
+ worktreesRoot() {
5686
+ return resolve6(join6(this.management.projectRoot, ".wrongstack", "worktrees"));
5687
+ }
5688
+ /** True iff `dir` resolves strictly inside the managed worktrees root. */
5689
+ underRoot(dir) {
5690
+ const abs = resolve6(dir);
5691
+ const root = this.worktreesRoot();
5692
+ return abs !== root && abs.startsWith(root + sep4);
5693
+ }
5694
+ /** Branches of worktrees a live in-session run currently owns. */
5695
+ liveActiveBranches() {
5696
+ const live = /* @__PURE__ */ new Set();
5697
+ for (const h of this.handles.values()) {
5698
+ if (ACTIVE_STATUSES.has(h.status) && h.branch) live.add(h.branch);
5699
+ }
5700
+ return live;
5701
+ }
5702
+ /**
5703
+ * Scan the disk for managed worktrees/branches NOT owned by a live in-session
5704
+ * run and broadcast them as orphans, with whether it is safe to clean now.
5705
+ * No-op (empty inventory) when management deps were not wired.
5706
+ */
5707
+ async scanAndBroadcast() {
5708
+ if (!this.management) {
5709
+ this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
5710
+ return;
5711
+ }
5712
+ try {
5713
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5714
+ const { worktrees, branches } = await wt.listManaged();
5715
+ const live = this.liveActiveBranches();
5716
+ const orphans = [];
5717
+ const seenBranches = /* @__PURE__ */ new Set();
5718
+ for (const w of worktrees) {
5719
+ if (w.branch && live.has(w.branch)) continue;
5720
+ if (w.branch) seenBranches.add(w.branch);
5721
+ orphans.push({ kind: "worktree", dir: w.dir, branch: w.branch });
5722
+ }
5723
+ for (const b of branches) {
5724
+ if (live.has(b) || seenBranches.has(b)) continue;
5725
+ orphans.push({ kind: "branch", branch: b });
5726
+ }
5727
+ const canClean = this.liveActiveBranches().size === 0;
5728
+ this.broadcast({
5729
+ type: "worktree.orphans",
5730
+ payload: {
5731
+ orphans,
5732
+ canClean,
5733
+ reason: canClean ? void 0 : "a run is live in this session"
5734
+ }
5735
+ });
5736
+ } catch (err) {
5737
+ this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage4(err)}`);
5738
+ this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
5739
+ }
5740
+ }
5741
+ /**
5742
+ * Force-remove every orphaned worktree + branch. Refused while a run is live —
5743
+ * in this session (active handles) OR another process (the SDD board liveness
5744
+ * guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
5745
+ */
5746
+ async cleanupOrphans() {
5747
+ if (!this.management) {
5748
+ this.broadcast({
5749
+ type: "worktree.cleanup_result",
5750
+ payload: { ok: false, removed: 0, reason: "cleanup is not available in this session" }
5751
+ });
5752
+ return;
5753
+ }
5754
+ if (this.liveActiveBranches().size > 0) {
5755
+ this.broadcast({
5756
+ type: "worktree.cleanup_result",
5757
+ payload: { ok: false, removed: 0, reason: "a run is live in this session \u2014 stop it first" }
5758
+ });
5759
+ return;
5760
+ }
5761
+ const res = await cleanupStaleSddWorktrees2({
5762
+ projectRoot: this.management.projectRoot,
5763
+ boardsDir: this.management.boardsDir
5764
+ });
5765
+ if (res.skippedReason) {
5766
+ this.broadcast({
5767
+ type: "worktree.cleanup_result",
5768
+ payload: { ok: false, removed: 0, reason: res.skippedReason }
5769
+ });
5770
+ await this.scanAndBroadcast();
5771
+ return;
5772
+ }
5773
+ for (const [id, h] of [...this.handles]) {
5774
+ if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
5775
+ }
5776
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: true, removed: res.removed } });
5777
+ this.broadcastState();
5778
+ await this.scanAndBroadcast();
5779
+ }
5780
+ /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
5781
+ async removeOne(dir, branch) {
5782
+ if (!this.management || !dir && !branch) {
5783
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "nothing to remove" } });
5784
+ return;
5785
+ }
5786
+ if (branch && !MANAGED_BRANCH_RE.test(branch)) {
5787
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "not a managed worktree branch" } });
5788
+ return;
5789
+ }
5790
+ if (dir && !this.underRoot(dir)) {
5791
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" } });
5792
+ return;
5793
+ }
5794
+ if (branch && this.liveActiveBranches().has(branch)) {
5795
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "a run is live on this worktree \u2014 stop it first" } });
5796
+ return;
5797
+ }
5798
+ let removed = false;
5799
+ if (dir) {
5800
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5801
+ ({ removed } = await wt.removeOne(dir, branch));
5802
+ }
5803
+ for (const [id, h] of [...this.handles]) {
5804
+ if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId)) this.handles.delete(id);
5805
+ }
5806
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: removed, removed: removed ? 1 : 0, reason: removed ? void 0 : "remove failed (not a managed worktree?)" } });
5807
+ this.broadcastState();
5808
+ await this.scanAndBroadcast();
5809
+ }
5810
+ /** Squash-merge ONE branch into base. Refused while a live run owns it. */
5811
+ async mergeBranch(branch) {
5812
+ if (!this.management || !branch) {
5813
+ this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch: branch ?? "", reason: "no branch" } });
5814
+ return;
5815
+ }
5816
+ if (!MANAGED_BRANCH_RE.test(branch)) {
5817
+ this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "not a managed worktree branch" } });
5818
+ return;
5819
+ }
5820
+ if (this.liveActiveBranches().has(branch)) {
5821
+ this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" } });
5822
+ return;
5823
+ }
5824
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5825
+ const res = await wt.mergeBranch(branch);
5826
+ this.broadcast({
5827
+ type: "worktree.merge_result",
5828
+ payload: { ok: res.ok, branch, conflict: res.conflict, conflictFiles: res.conflictFiles, reason: res.reason }
5829
+ });
5830
+ await this.scanAndBroadcast();
5831
+ }
5832
+ /** Compact change summary for one worktree checkout. */
5833
+ async diffOne(dir, baseBranch) {
5834
+ if (!this.management || !dir || !this.underRoot(dir)) {
5835
+ this.broadcast({ type: "worktree.diff_result", payload: { dir: dir ?? "", summary: null } });
5836
+ return;
5837
+ }
5838
+ const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
5839
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5840
+ const summary = await wt.diffSummary(resolve6(dir), base);
5841
+ this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
5842
+ }
5089
5843
  // ── internals ───────────────────────────────────────────────────────────
5090
5844
  subscribe() {
5091
5845
  const on = this.events.on.bind(this.events);
@@ -5097,6 +5851,7 @@ var WorktreeWebSocketHandler = class {
5097
5851
  handleId: e.handleId,
5098
5852
  ownerId: e.ownerId,
5099
5853
  ownerLabel: e.ownerLabel,
5854
+ dir: e.dir,
5100
5855
  branch: e.branch,
5101
5856
  baseBranch: e.baseBranch,
5102
5857
  status: "active",
@@ -5199,10 +5954,10 @@ var WorktreeWebSocketHandler = class {
5199
5954
  };
5200
5955
 
5201
5956
  // src/server/mailbox-handlers.ts
5202
- import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
5957
+ import { GlobalMailbox, resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core";
5203
5958
  async function handleMailboxMessages(ws, deps2, payload) {
5204
5959
  try {
5205
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
5960
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5206
5961
  const mb = new GlobalMailbox(dir);
5207
5962
  const messages = await mb.query({
5208
5963
  limit: payload?.limit ?? 30,
@@ -5238,7 +5993,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
5238
5993
  }
5239
5994
  async function handleMailboxAgents(ws, deps2, payload) {
5240
5995
  try {
5241
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
5996
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5242
5997
  const mb = new GlobalMailbox(dir);
5243
5998
  const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
5244
5999
  send(ws, {
@@ -5267,7 +6022,7 @@ async function handleMailboxAgents(ws, deps2, payload) {
5267
6022
  }
5268
6023
  async function handleMailboxClear(ws, deps2) {
5269
6024
  try {
5270
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
6025
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5271
6026
  const mb = new GlobalMailbox(dir);
5272
6027
  await mb.clearAll();
5273
6028
  send(ws, { type: "mailbox.cleared", payload: {} });
@@ -5277,7 +6032,7 @@ async function handleMailboxClear(ws, deps2) {
5277
6032
  }
5278
6033
  async function handleMailboxPurge(ws, deps2, opts) {
5279
6034
  try {
5280
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
6035
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5281
6036
  const mb = new GlobalMailbox(dir);
5282
6037
  const result = await mb.purgeStale(opts);
5283
6038
  send(ws, { type: "mailbox.purged", payload: result });
@@ -5324,14 +6079,14 @@ function registerShutdownHandlers(res) {
5324
6079
 
5325
6080
  // src/server/instance-registry.ts
5326
6081
  import * as os from "os";
5327
- import * as path9 from "path";
5328
- import * as fs7 from "fs/promises";
6082
+ import * as path10 from "path";
6083
+ import * as fs8 from "fs/promises";
5329
6084
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
5330
6085
  function defaultBaseDir() {
5331
- return path9.join(os.homedir(), ".wrongstack");
6086
+ return path10.join(os.homedir(), ".wrongstack");
5332
6087
  }
5333
6088
  function registryPath(baseDir = defaultBaseDir()) {
5334
- return path9.join(baseDir, "webui-instances.json");
6089
+ return path10.join(baseDir, "webui-instances.json");
5335
6090
  }
5336
6091
  function isPidAlive(pid) {
5337
6092
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -5344,7 +6099,7 @@ function isPidAlive(pid) {
5344
6099
  }
5345
6100
  async function load(file) {
5346
6101
  try {
5347
- const raw = await fs7.readFile(file, "utf8");
6102
+ const raw = await fs8.readFile(file, "utf8");
5348
6103
  const parsed = JSON.parse(raw);
5349
6104
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
5350
6105
  return parsed;
@@ -5403,16 +6158,16 @@ function formatInstances(instances) {
5403
6158
  // src/server/port-utils.ts
5404
6159
  import * as net from "net";
5405
6160
  function isPortFree(host, port) {
5406
- return new Promise((resolve9) => {
6161
+ return new Promise((resolve10) => {
5407
6162
  const srv = net.createServer();
5408
- srv.once("error", () => resolve9(false));
6163
+ srv.once("error", () => resolve10(false));
5409
6164
  srv.once("listening", () => {
5410
- srv.close(() => resolve9(true));
6165
+ srv.close(() => resolve10(true));
5411
6166
  });
5412
6167
  try {
5413
6168
  srv.listen(port, host);
5414
6169
  } catch {
5415
- resolve9(false);
6170
+ resolve10(false);
5416
6171
  }
5417
6172
  });
5418
6173
  }
@@ -5493,15 +6248,15 @@ import { DefaultSecretScrubber } from "@wrongstack/core";
5493
6248
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
5494
6249
 
5495
6250
  // src/server/provider-config-io.ts
5496
- import * as fs8 from "fs/promises";
5497
- import * as path10 from "path";
6251
+ import * as fs9 from "fs/promises";
6252
+ import * as path11 from "path";
5498
6253
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
5499
6254
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
5500
6255
  import { DefaultSecretVault } from "@wrongstack/core";
5501
6256
  async function loadSavedProviders(configPath, vault) {
5502
6257
  let raw;
5503
6258
  try {
5504
- raw = await fs8.readFile(configPath, "utf8");
6259
+ raw = await fs9.readFile(configPath, "utf8");
5505
6260
  } catch {
5506
6261
  return {};
5507
6262
  }
@@ -5518,7 +6273,7 @@ async function saveProviders(configPath, vault, providers) {
5518
6273
  let raw;
5519
6274
  let fileExists = true;
5520
6275
  try {
5521
- raw = await fs8.readFile(configPath, "utf8");
6276
+ raw = await fs9.readFile(configPath, "utf8");
5522
6277
  } catch (err) {
5523
6278
  if (err.code !== "ENOENT") {
5524
6279
  throw new Error(
@@ -5546,7 +6301,7 @@ async function saveProviders(configPath, vault, providers) {
5546
6301
  await atomicWrite4(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
5547
6302
  }
5548
6303
  function createProviderConfigIO(configPath) {
5549
- const keyFile = path10.join(path10.dirname(configPath), ".key");
6304
+ const keyFile = path11.join(path11.dirname(configPath), ".key");
5550
6305
  const vault = new DefaultSecretVault({ keyFile });
5551
6306
  return {
5552
6307
  load: () => loadSavedProviders(configPath, vault),
@@ -5864,7 +6619,8 @@ function createProviderHandlers(deps2) {
5864
6619
 
5865
6620
  // src/server/mode-handlers.ts
5866
6621
  import {
5867
- DefaultSystemPromptBuilder
6622
+ DefaultSystemPromptBuilder,
6623
+ resolveWstackPaths
5868
6624
  } from "@wrongstack/core";
5869
6625
  function createModeHandlers(ctx) {
5870
6626
  return {
@@ -5912,13 +6668,18 @@ function createModeHandlers(ctx) {
5912
6668
  }
5913
6669
  ctx.setModeId(id);
5914
6670
  const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
6671
+ const paths = resolveWstackPaths({ projectRoot: ctx.projectRoot, globalRoot: ctx.globalRoot });
5915
6672
  const freshBuilder = new DefaultSystemPromptBuilder({
5916
6673
  memoryStore: ctx.memoryStore,
5917
6674
  skillLoader: ctx.skillLoader,
5918
6675
  modeStore: ctx.modeStore,
5919
6676
  modeId: id,
5920
6677
  modePrompt,
5921
- modelCapabilities: ctx.modelCapabilities
6678
+ modelCapabilities: ctx.modelCapabilities,
6679
+ instructionPaths: {
6680
+ globalDir: paths.globalInstructions,
6681
+ projectDir: paths.inProjectInstructions
6682
+ }
5922
6683
  });
5923
6684
  ctx.context.systemPrompt = await freshBuilder.build({
5924
6685
  cwd: ctx.projectRoot,
@@ -5940,12 +6701,13 @@ function createModeHandlers(ctx) {
5940
6701
  }
5941
6702
 
5942
6703
  // src/server/project-handlers.ts
5943
- import * as fs9 from "fs/promises";
5944
- import * as path11 from "path";
6704
+ import * as fs10 from "fs/promises";
6705
+ import * as path12 from "path";
5945
6706
  import {
5946
6707
  DefaultSessionStore,
5947
6708
  DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
5948
- getSessionRegistry
6709
+ getSessionRegistry,
6710
+ resolveWstackPaths as resolveWstackPaths2
5949
6711
  } from "@wrongstack/core";
5950
6712
  function createProjectHandlers(ctx) {
5951
6713
  return {
@@ -5968,9 +6730,9 @@ function createProjectHandlers(ctx) {
5968
6730
  }
5969
6731
  const { root: addRoot, name: displayName } = parsed.value;
5970
6732
  try {
5971
- const resolved = path11.resolve(addRoot);
5972
- await fs9.access(resolved);
5973
- const stat3 = await fs9.stat(resolved);
6733
+ const resolved = path12.resolve(addRoot);
6734
+ await fs10.access(resolved);
6735
+ const stat3 = await fs10.stat(resolved);
5974
6736
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
5975
6737
  const manifest = await loadManifest(ctx.globalConfigPath);
5976
6738
  const existing = manifest.projects.find((p) => p.root === resolved);
@@ -5986,7 +6748,7 @@ function createProjectHandlers(ctx) {
5986
6748
  });
5987
6749
  return;
5988
6750
  }
5989
- const name2 = displayName?.trim() || path11.basename(resolved);
6751
+ const name2 = displayName?.trim() || path12.basename(resolved);
5990
6752
  const slug = generateProjectSlug(resolved);
5991
6753
  await ensureProjectDataDir(slug, ctx.globalConfigPath);
5992
6754
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5999,7 +6761,7 @@ function createProjectHandlers(ctx) {
5999
6761
  } catch (err) {
6000
6762
  send(ws, {
6001
6763
  type: "projects.added",
6002
- payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
6764
+ payload: { name: path12.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
6003
6765
  });
6004
6766
  }
6005
6767
  },
@@ -6014,17 +6776,17 @@ function createProjectHandlers(ctx) {
6014
6776
  }
6015
6777
  const { root: selRoot, name: selName } = parsed.value;
6016
6778
  try {
6017
- const resolved = path11.resolve(selRoot);
6779
+ const resolved = path12.resolve(selRoot);
6018
6780
  try {
6019
- await fs9.access(resolved);
6020
- const stat3 = await fs9.stat(resolved);
6781
+ await fs10.access(resolved);
6782
+ const stat3 = await fs10.stat(resolved);
6021
6783
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
6022
6784
  } catch (err) {
6023
6785
  send(ws, {
6024
6786
  type: "projects.selected",
6025
6787
  payload: {
6026
6788
  root: selRoot,
6027
- name: selName || path11.basename(selRoot),
6789
+ name: selName || path12.basename(selRoot),
6028
6790
  message: `Cannot switch: ${errMessage(err)}`
6029
6791
  }
6030
6792
  });
@@ -6036,7 +6798,7 @@ function createProjectHandlers(ctx) {
6036
6798
  entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
6037
6799
  entry.lastWorkingDir = resolved;
6038
6800
  } else {
6039
- const name2 = selName?.trim() || path11.basename(resolved);
6801
+ const name2 = selName?.trim() || path12.basename(resolved);
6040
6802
  const slug = generateProjectSlug(resolved);
6041
6803
  manifest.projects.push({
6042
6804
  name: name2,
@@ -6058,13 +6820,21 @@ function createProjectHandlers(ctx) {
6058
6820
  try {
6059
6821
  const modeId = ctx.getModeId();
6060
6822
  const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
6823
+ const switchPaths = resolveWstackPaths2({
6824
+ projectRoot: resolved,
6825
+ globalRoot: ctx.wpaths.globalRoot
6826
+ });
6061
6827
  const switchBuilder = new DefaultSystemPromptBuilder2({
6062
6828
  memoryStore: ctx.memoryStore,
6063
6829
  skillLoader: ctx.skillLoader,
6064
6830
  modeStore: ctx.modeStore,
6065
6831
  modeId,
6066
6832
  modePrompt: switchMode?.prompt ?? "",
6067
- modelCapabilities: ctx.modelCapabilities
6833
+ modelCapabilities: ctx.modelCapabilities,
6834
+ instructionPaths: {
6835
+ globalDir: switchPaths.globalInstructions,
6836
+ projectDir: switchPaths.inProjectInstructions
6837
+ }
6068
6838
  });
6069
6839
  ctx.context.systemPrompt = await switchBuilder.build({
6070
6840
  cwd: resolved,
@@ -6075,13 +6845,13 @@ function createProjectHandlers(ctx) {
6075
6845
  });
6076
6846
  } catch {
6077
6847
  }
6078
- const newSessionsDir = path11.join(
6079
- path11.dirname(ctx.globalConfigPath),
6848
+ const newSessionsDir = path12.join(
6849
+ path12.dirname(ctx.globalConfigPath),
6080
6850
  "projects",
6081
6851
  switchSlug,
6082
6852
  "sessions"
6083
6853
  );
6084
- await fs9.mkdir(newSessionsDir, { recursive: true });
6854
+ await fs10.mkdir(newSessionsDir, { recursive: true });
6085
6855
  const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
6086
6856
  const oldSession = ctx.getSession();
6087
6857
  const oldSessionId = oldSession.id;
@@ -6115,7 +6885,7 @@ function createProjectHandlers(ctx) {
6115
6885
  sessionId: newSession.id,
6116
6886
  projectSlug: switchSlug,
6117
6887
  projectRoot: resolved,
6118
- projectName: path11.basename(resolved),
6888
+ projectName: path12.basename(resolved),
6119
6889
  workingDir: resolved,
6120
6890
  clientType: "webui",
6121
6891
  pid: process.pid,
@@ -6127,8 +6897,8 @@ function createProjectHandlers(ctx) {
6127
6897
  type: "projects.selected",
6128
6898
  payload: {
6129
6899
  root: resolved,
6130
- name: selName || path11.basename(resolved),
6131
- message: `Switched to ${selName || path11.basename(resolved)}`
6900
+ name: selName || path12.basename(resolved),
6901
+ message: `Switched to ${selName || path12.basename(resolved)}`
6132
6902
  }
6133
6903
  });
6134
6904
  broadcast(ctx.clients, {
@@ -6148,7 +6918,7 @@ function createProjectHandlers(ctx) {
6148
6918
  type: "projects.selected",
6149
6919
  payload: {
6150
6920
  root: selRoot,
6151
- name: selName || path11.basename(selRoot),
6921
+ name: selName || path12.basename(selRoot),
6152
6922
  message: errMessage(err)
6153
6923
  }
6154
6924
  });
@@ -6179,7 +6949,7 @@ function createProjectHandlers(ctx) {
6179
6949
  }
6180
6950
 
6181
6951
  // src/server/session-handlers.ts
6182
- import * as path12 from "path";
6952
+ import * as path13 from "path";
6183
6953
  import {
6184
6954
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
6185
6955
  repairToolUseAdjacency,
@@ -6521,7 +7291,7 @@ function createSessionHandlers(ctx) {
6521
7291
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
6522
7292
  const projectRoot = ctx.getProjectRoot();
6523
7293
  const rewinder = new DefaultSessionRewinder(
6524
- path12.join(projectRoot, ".wrongstack", "sessions"),
7294
+ path13.join(projectRoot, ".wrongstack", "sessions"),
6525
7295
  projectRoot
6526
7296
  );
6527
7297
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
@@ -6536,7 +7306,7 @@ function createSessionHandlers(ctx) {
6536
7306
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
6537
7307
  const projectRoot = ctx.getProjectRoot();
6538
7308
  const rewinder = new DefaultSessionRewinder(
6539
- path12.join(projectRoot, ".wrongstack", "sessions"),
7309
+ path13.join(projectRoot, ".wrongstack", "sessions"),
6540
7310
  projectRoot
6541
7311
  );
6542
7312
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
@@ -6912,9 +7682,9 @@ async function handleSddBoardRoute(_ws, msg, handlers) {
6912
7682
  }
6913
7683
 
6914
7684
  // src/server/setup-events.ts
6915
- import * as fs10 from "fs/promises";
7685
+ import * as fs11 from "fs/promises";
6916
7686
  import { watch as fsWatch } from "fs";
6917
- import * as path13 from "path";
7687
+ import * as path14 from "path";
6918
7688
  function setupEvents(deps2) {
6919
7689
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
6920
7690
  const disposers = [];
@@ -7003,6 +7773,22 @@ function setupEvents(deps2) {
7003
7773
  }).catch(() => {
7004
7774
  });
7005
7775
  broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
7776
+ const sideEffects = context.sideEffects ?? [];
7777
+ if (sideEffects.length > 0) {
7778
+ broadcast2(clients, {
7779
+ type: "side_effects",
7780
+ payload: {
7781
+ sideEffects: sideEffects.slice(-50).map((se) => ({
7782
+ toolUseId: se.toolUseId,
7783
+ toolName: se.toolName,
7784
+ ts: se.ts,
7785
+ input: se.input,
7786
+ outcome: se.outcome,
7787
+ risk: se.risk
7788
+ }))
7789
+ }
7790
+ });
7791
+ }
7006
7792
  if (e.name === "task" || e.name === "plan" || e.name === "todo") {
7007
7793
  void (async () => {
7008
7794
  try {
@@ -7381,16 +8167,16 @@ function setupEvents(deps2) {
7381
8167
  if (wpaths?.projectStatus) {
7382
8168
  try {
7383
8169
  const statusFile = wpaths.projectStatus(e.projectHash);
7384
- const dir = path13.dirname(statusFile);
7385
- await fs10.mkdir(dir, { recursive: true });
7386
- await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
8170
+ const dir = path14.dirname(statusFile);
8171
+ await fs11.mkdir(dir, { recursive: true });
8172
+ await fs11.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
7387
8173
  } catch (err) {
7388
8174
  console.error("[setup-events] Failed to write status.json:", err);
7389
8175
  }
7390
8176
  }
7391
8177
  });
7392
8178
  if (wpaths?.projectStatus && wpaths.configDir) {
7393
- const projectsDir = path13.join(wpaths.configDir, "projects");
8179
+ const projectsDir = path14.join(wpaths.configDir, "projects");
7394
8180
  const knownProjectHashes = /* @__PURE__ */ new Set();
7395
8181
  const debounceTimers = /* @__PURE__ */ new Map();
7396
8182
  const DEBOUNCE_MS = 150;
@@ -7453,20 +8239,20 @@ function setupEvents(deps2) {
7453
8239
  let watcher;
7454
8240
  const startWatcher = async () => {
7455
8241
  try {
7456
- await fs10.mkdir(projectsDir, { recursive: true });
8242
+ await fs11.mkdir(projectsDir, { recursive: true });
7457
8243
  watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
7458
8244
  if (eventType === "change") {
7459
8245
  if (filename == null) return;
7460
8246
  if (watcherMetrics) watcherMetrics.fileChangesDetected++;
7461
- const targetFile = path13.join(projectsDir, String(filename));
8247
+ const targetFile = path14.join(projectsDir, String(filename));
7462
8248
  if (targetFile.endsWith("status.json")) {
7463
- const projectHash2 = path13.basename(path13.dirname(targetFile));
8249
+ const projectHash2 = path14.basename(path14.dirname(targetFile));
7464
8250
  if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
7465
8251
  return;
7466
8252
  }
7467
8253
  if (watcherMetrics) watcherMetrics.filesProcessed++;
7468
8254
  try {
7469
- const content = await fs10.readFile(targetFile, "utf-8");
8255
+ const content = await fs11.readFile(targetFile, "utf-8");
7470
8256
  const statusData = JSON.parse(content);
7471
8257
  if (statusData.projectHash) {
7472
8258
  const hash = String(statusData.projectHash);
@@ -7518,7 +8304,7 @@ function setupEvents(deps2) {
7518
8304
  }
7519
8305
  });
7520
8306
  }
7521
- const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
8307
+ const globalRoot = globalConfigPath ? path14.dirname(globalConfigPath) : void 0;
7522
8308
  if (globalRoot) {
7523
8309
  const broadcastSessions = async () => {
7524
8310
  try {
@@ -7591,11 +8377,11 @@ function setupEvents(deps2) {
7591
8377
 
7592
8378
  // src/server/custom-context-modes.ts
7593
8379
  import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
7594
- import * as fs11 from "fs/promises";
7595
- import * as path14 from "path";
8380
+ import * as fs12 from "fs/promises";
8381
+ import * as path15 from "path";
7596
8382
  var STORE_FILENAME = "custom-context-modes.json";
7597
8383
  function storePath(wrongstackDir) {
7598
- return path14.join(wrongstackDir, STORE_FILENAME);
8384
+ return path15.join(wrongstackDir, STORE_FILENAME);
7599
8385
  }
7600
8386
  var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
7601
8387
  function createCustomModeStore(wrongstackDir) {
@@ -7603,7 +8389,7 @@ function createCustomModeStore(wrongstackDir) {
7603
8389
  const load2 = async () => {
7604
8390
  modes.clear();
7605
8391
  try {
7606
- const raw = await fs11.readFile(storePath(wrongstackDir), "utf8");
8392
+ const raw = await fs12.readFile(storePath(wrongstackDir), "utf8");
7607
8393
  const parsed = JSON.parse(raw);
7608
8394
  if (Array.isArray(parsed.modes)) {
7609
8395
  for (const m of parsed.modes) {
@@ -7649,7 +8435,8 @@ function createCustomModeStore(wrongstackDir) {
7649
8435
  custom: true
7650
8436
  };
7651
8437
  modes.set(mode.id, entry);
7652
- void save2();
8438
+ void save2().catch(() => {
8439
+ });
7653
8440
  return { ok: true };
7654
8441
  };
7655
8442
  const update = (id, patch) => {
@@ -7675,7 +8462,8 @@ function createCustomModeStore(wrongstackDir) {
7675
8462
  if (patch.targetLoad !== void 0) next.targetLoad = patch.targetLoad;
7676
8463
  if (patch.aggressiveOn !== void 0) next.aggressiveOn = patch.aggressiveOn;
7677
8464
  modes.set(id, next);
7678
- void save2();
8465
+ void save2().catch(() => {
8466
+ });
7679
8467
  return { ok: true };
7680
8468
  };
7681
8469
  const remove = (id) => {
@@ -7685,7 +8473,8 @@ function createCustomModeStore(wrongstackDir) {
7685
8473
  if (!modes.delete(id)) {
7686
8474
  return { ok: false, error: `Mode "${id}" not found` };
7687
8475
  }
7688
- void save2();
8476
+ void save2().catch(() => {
8477
+ });
7689
8478
  return { ok: true };
7690
8479
  };
7691
8480
  const list = () => {
@@ -7726,14 +8515,17 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
7726
8515
  }
7727
8516
 
7728
8517
  // src/server/shell-open.ts
7729
- import * as fs12 from "fs/promises";
7730
- import * as path15 from "path";
8518
+ import * as fs13 from "fs/promises";
8519
+ import * as path16 from "path";
7731
8520
  import { spawn as spawn2 } from "child_process";
7732
- var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
8521
+ var METACHAR_REGEX = /[&|<>^"'`'\n\r]/;
8522
+ function shellQuote(s) {
8523
+ return s.replaceAll("'", `'"'"'`);
8524
+ }
7733
8525
  async function handleShellOpen(req, logger) {
7734
8526
  try {
7735
- const resolved = path15.resolve(req.path);
7736
- await fs12.access(resolved);
8527
+ const resolved = path16.resolve(req.path);
8528
+ await fs13.access(resolved);
7737
8529
  if (METACHAR_REGEX.test(resolved)) {
7738
8530
  return { success: false, message: "Path contains unsupported characters." };
7739
8531
  }
@@ -7766,7 +8558,11 @@ async function handleShellOpen(req, logger) {
7766
8558
  () => launch(
7767
8559
  "gnome-terminal",
7768
8560
  [`--working-directory=${resolved}`],
7769
- () => launch("xterm", ["-e", `cd '${resolved}' && ${process.env["SHELL"] ?? "sh"}`])
8561
+ () => (
8562
+ // Pass argv array so sh -c sees a literal string, not an interpolated one.
8563
+ // shellQuote() guards against paths that somehow slipped the METACHAR_REGEX.
8564
+ launch("xterm", ["-e", "sh", "-c", `cd ${shellQuote(resolved)} && ${process.env["SHELL"] ?? "sh"}`])
8565
+ )
7770
8566
  )
7771
8567
  );
7772
8568
  }
@@ -7785,9 +8581,9 @@ async function handleGitInfo(ws, projectRoot) {
7785
8581
  const cwd = projectRoot || void 0;
7786
8582
  try {
7787
8583
  const { execFile: ef } = await import("child_process");
7788
- const git = (args) => new Promise((resolve9) => {
8584
+ const git = (args) => new Promise((resolve10) => {
7789
8585
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
7790
- resolve9(err ? "" : stdout.trim());
8586
+ resolve10(err ? "" : stdout.trim());
7791
8587
  });
7792
8588
  });
7793
8589
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -7813,12 +8609,12 @@ async function handleGitInfo(ws, projectRoot) {
7813
8609
  function makeGit(cwd) {
7814
8610
  return async (args) => {
7815
8611
  const { execFile: ef } = await import("child_process");
7816
- return new Promise((resolve9) => {
8612
+ return new Promise((resolve10) => {
7817
8613
  ef(
7818
8614
  "git",
7819
8615
  args,
7820
8616
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
7821
- (err, stdout) => resolve9(err ? "" : stdout)
8617
+ (err, stdout) => resolve10(err ? "" : stdout)
7822
8618
  );
7823
8619
  });
7824
8620
  };
@@ -7842,15 +8638,15 @@ async function handleGitChanges(ws, projectRoot) {
7842
8638
  if (!m) continue;
7843
8639
  const added = m[1] === "-" ? 0 : Number(m[1]);
7844
8640
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
7845
- let path17 = m[3] ?? "";
7846
- if (path17 === "") {
8641
+ let path18 = m[3] ?? "";
8642
+ if (path18 === "") {
7847
8643
  i += 1;
7848
- path17 = parts[i + 1] ?? parts[i] ?? "";
8644
+ path18 = parts[i + 1] ?? parts[i] ?? "";
7849
8645
  i += 1;
7850
8646
  }
7851
- if (!path17) continue;
7852
- const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
7853
- counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
8647
+ if (!path18) continue;
8648
+ const prev = counts.get(path18) ?? { added: 0, deleted: 0 };
8649
+ counts.set(path18, { added: prev.added + added, deleted: prev.deleted + deleted });
7854
8650
  }
7855
8651
  };
7856
8652
  parseNumstat(unstagedNumstat);
@@ -7862,7 +8658,7 @@ async function handleGitChanges(ws, projectRoot) {
7862
8658
  if (!rec || rec.length < 3) continue;
7863
8659
  const x = rec[0] ?? " ";
7864
8660
  const y = rec[1] ?? " ";
7865
- const path17 = rec.slice(3);
8661
+ const path18 = rec.slice(3);
7866
8662
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
7867
8663
  if (isRename) i += 1;
7868
8664
  let status;
@@ -7874,13 +8670,13 @@ async function handleGitChanges(ws, projectRoot) {
7874
8670
  else if (x === "D" || y === "D") status = "D";
7875
8671
  else status = "M";
7876
8672
  const staged = x !== " " && x !== "?";
7877
- let added = counts.get(path17)?.added ?? 0;
7878
- let deleted = counts.get(path17)?.deleted ?? 0;
8673
+ let added = counts.get(path18)?.added ?? 0;
8674
+ let deleted = counts.get(path18)?.deleted ?? 0;
7879
8675
  if (status === "?") {
7880
8676
  added = 0;
7881
8677
  deleted = 0;
7882
8678
  }
7883
- files.push({ path: path17, status, added, deleted, staged });
8679
+ files.push({ path: path18, status, added, deleted, staged });
7884
8680
  }
7885
8681
  send(ws, { type: "git.changes", payload: { files } });
7886
8682
  } catch (err) {
@@ -7891,21 +8687,21 @@ async function handleGitChanges(ws, projectRoot) {
7891
8687
  }
7892
8688
  }
7893
8689
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
7894
- async function handleGitDiff(ws, projectRoot, path17) {
8690
+ async function handleGitDiff(ws, projectRoot, path18) {
7895
8691
  const cwd = projectRoot || void 0;
7896
- const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
7897
- if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
8692
+ const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path18, ...extra } });
8693
+ if (!path18 || path18.includes("\0") || path18.includes("..") || nodePath.isAbsolute(path18)) {
7898
8694
  reply({ oldText: "", newText: "", error: "invalid path" });
7899
8695
  return;
7900
8696
  }
7901
8697
  try {
7902
8698
  const git = makeGit(cwd);
7903
8699
  const { readFile: readFile9 } = await import("fs/promises");
7904
- const { join: join12 } = await import("path");
7905
- const oldText = await git(["show", `HEAD:${path17}`]);
8700
+ const { join: join14 } = await import("path");
8701
+ const oldText = await git(["show", `HEAD:${path18}`]);
7906
8702
  let newText = "";
7907
8703
  try {
7908
- const abs = cwd ? join12(cwd, path17) : path17;
8704
+ const abs = cwd ? join14(cwd, path18) : path18;
7909
8705
  const buf = await readFile9(abs);
7910
8706
  if (buf.includes(0)) {
7911
8707
  reply({ oldText: "", newText: "", binary: true });
@@ -7986,10 +8782,10 @@ async function handleProcessKillAll(ws) {
7986
8782
  }
7987
8783
 
7988
8784
  // src/server/goal-handlers.ts
7989
- import { resolveWstackPaths } from "@wrongstack/core/utils";
8785
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
7990
8786
  async function handleGoalGet(projectRoot, broadcast2) {
7991
8787
  try {
7992
- const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
8788
+ const goalPath = resolveWstackPaths3({ projectRoot }).projectGoal;
7993
8789
  const { readFile: readFile9 } = await import("fs/promises");
7994
8790
  const raw = await readFile9(goalPath, "utf8");
7995
8791
  const goal = JSON.parse(raw);
@@ -8047,7 +8843,7 @@ async function startWebUI(opts = {}) {
8047
8843
  const write = async () => {
8048
8844
  let raw;
8049
8845
  try {
8050
- raw = await fs13.readFile(globalConfigPath, "utf8");
8846
+ raw = await fs14.readFile(globalConfigPath, "utf8");
8051
8847
  } catch {
8052
8848
  raw = "{}";
8053
8849
  }
@@ -8121,6 +8917,7 @@ async function startWebUI(opts = {}) {
8121
8917
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
8122
8918
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
8123
8919
  applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
8920
+ applyToolResultRenderModes(toolRegistry, config.tools?.resultRenderMode);
8124
8921
  configureExecPolicy(config.tools?.exec ?? {});
8125
8922
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
8126
8923
  const mcpRegistry = new MCPRegistry({
@@ -8165,7 +8962,7 @@ async function startWebUI(opts = {}) {
8165
8962
  sessionId: session.id,
8166
8963
  projectSlug: wpaths.projectSlug,
8167
8964
  projectRoot,
8168
- projectName: path16.basename(projectRoot),
8965
+ projectName: path17.basename(projectRoot),
8169
8966
  workingDir,
8170
8967
  clientType: "webui",
8171
8968
  pid: process.pid,
@@ -8185,7 +8982,7 @@ async function startWebUI(opts = {}) {
8185
8982
  const hqTelemetry = createHqPublisherFromEnv({
8186
8983
  clientKind: "webui",
8187
8984
  projectRoot,
8188
- projectName: path16.basename(projectRoot),
8985
+ projectName: path17.basename(projectRoot),
8189
8986
  appConfig: config,
8190
8987
  socketFactory: (url) => new WebSocket2(url)
8191
8988
  });
@@ -8197,7 +8994,7 @@ async function startWebUI(opts = {}) {
8197
8994
  events,
8198
8995
  sessionId: session.id,
8199
8996
  projectRoot,
8200
- projectName: path16.basename(projectRoot),
8997
+ projectName: path17.basename(projectRoot),
8201
8998
  globalRoot: wpaths.globalRoot,
8202
8999
  initialAgents: statusTracker?.getAgents(),
8203
9000
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -8253,19 +9050,39 @@ async function startWebUI(opts = {}) {
8253
9050
  };
8254
9051
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
8255
9052
  const skillInstaller = config.features.skills ? new SkillInstaller({
8256
- manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
8257
- projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
8258
- globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
9053
+ manifestPath: path17.join(wstackGlobalRoot3(), "installed-skills.json"),
9054
+ projectSkillsDir: path17.join(projectRoot, ".wrongstack", "skills"),
9055
+ globalSkillsDir: path17.join(wstackGlobalRoot3(), "skills"),
8259
9056
  projectHash: projectHash(projectRoot),
8260
9057
  skillLoader
8261
9058
  }) : void 0;
9059
+ const promptsEnabled = config.features.prompts !== false;
9060
+ const bundledPromptsDir = promptsEnabled ? (() => {
9061
+ try {
9062
+ const req = createRequire2(import.meta.url);
9063
+ return path17.join(
9064
+ path17.dirname(req.resolve("@wrongstack/core/package.json")),
9065
+ "data",
9066
+ "prompts"
9067
+ );
9068
+ } catch {
9069
+ return void 0;
9070
+ }
9071
+ })() : void 0;
9072
+ const promptLoader = promptsEnabled ? new DefaultPromptLoader({ paths: wpaths, bundledDir: bundledPromptsDir }) : void 0;
9073
+ const promptUsage = new PromptUsageStore(wpaths.promptUsage);
9074
+ const promptsCtx = { promptLoader, promptUsage };
8262
9075
  const systemPromptBuilder = new DefaultSystemPromptBuilder3({
8263
9076
  memoryStore,
8264
9077
  skillLoader,
8265
9078
  modeStore,
8266
9079
  modeId,
8267
9080
  modePrompt,
8268
- modelCapabilities: () => modelCapabilitiesRef.current
9081
+ modelCapabilities: () => modelCapabilitiesRef.current,
9082
+ instructionPaths: {
9083
+ globalDir: wpaths.globalInstructions,
9084
+ projectDir: wpaths.inProjectInstructions
9085
+ }
8269
9086
  });
8270
9087
  let onlineAgents = [];
8271
9088
  try {
@@ -8360,6 +9177,10 @@ async function startWebUI(opts = {}) {
8360
9177
  context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
8361
9178
  context.meta["nextPrediction"] = config.nextPrediction ?? false;
8362
9179
  context.meta["fallbackModels"] = config.fallbackModels ?? [];
9180
+ context.meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
9181
+ context.meta["favoriteModels"] = config.favoriteModels ?? [];
9182
+ context.meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
9183
+ context.meta["modelMatrix"] = config.modelMatrix ?? {};
8363
9184
  context.meta["fallbackAuto"] = config.fallbackAuto !== false;
8364
9185
  context.meta["featureMcp"] = config.features.mcp !== false;
8365
9186
  context.meta["featurePlugins"] = config.features.plugins !== false;
@@ -8372,6 +9193,20 @@ async function startWebUI(opts = {}) {
8372
9193
  context.meta["logLevel"] = config.log?.level ?? "info";
8373
9194
  context.meta["auditLevel"] = config.session?.auditLevel ?? "standard";
8374
9195
  context.meta["maxIterations"] = config.tools?.maxIterations ?? 500;
9196
+ context.meta["contextMode"] = config.context?.mode ?? "balanced";
9197
+ {
9198
+ const tsm = config.features?.tokenSavingMode;
9199
+ context.meta["tokenSavingTier"] = typeof tsm === "string" ? tsm : tsm ? "medium" : "off";
9200
+ }
9201
+ context.meta["maxConcurrent"] = typeof config.maxConcurrent === "number" ? config.maxConcurrent : 10;
9202
+ context.meta["titleAnimation"] = autonomyCfg["terminalTitleAnimation"] !== false;
9203
+ {
9204
+ const mr = config.modelRuntime ?? {};
9205
+ context.meta["reasoningMode"] = mr.reasoning?.mode ?? "auto";
9206
+ context.meta["reasoningEffort"] = mr.reasoning?.effort ?? "high";
9207
+ context.meta["reasoningPreserve"] = mr.reasoning?.preserve === true;
9208
+ context.meta["cacheTtl"] = mr.cache?.ttl ?? "default";
9209
+ }
8375
9210
  const hqConfig = config.hq;
8376
9211
  context.meta["hqEnabled"] = hqConfig?.enabled === true;
8377
9212
  context.meta["hqUrl"] = hqConfig?.url ?? "";
@@ -8405,6 +9240,10 @@ async function startWebUI(opts = {}) {
8405
9240
  "indexOnStart",
8406
9241
  "contextAutoCompact",
8407
9242
  "contextStrategy",
9243
+ "contextMode",
9244
+ "tokenSavingTier",
9245
+ "maxConcurrent",
9246
+ "titleAnimation",
8408
9247
  "logLevel",
8409
9248
  "auditLevel",
8410
9249
  "hqEnabled",
@@ -8420,6 +9259,10 @@ async function startWebUI(opts = {}) {
8420
9259
  "reasoningPreserve",
8421
9260
  "cacheTtl",
8422
9261
  "fallbackModels",
9262
+ "fallbackProfiles",
9263
+ "favoriteModels",
9264
+ "favoriteModelsOnly",
9265
+ "modelMatrix",
8423
9266
  "fallbackAuto"
8424
9267
  ];
8425
9268
  const prefSnapshot = () => {
@@ -8452,6 +9295,15 @@ async function startWebUI(opts = {}) {
8452
9295
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
8453
9296
  if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
8454
9297
  if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
9298
+ if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
9299
+ decrypted.fallbackProfiles = payload["fallbackProfiles"];
9300
+ }
9301
+ if (Array.isArray(payload["favoriteModels"])) decrypted.favoriteModels = payload["favoriteModels"];
9302
+ if (typeof payload["favoriteModelsOnly"] === "boolean")
9303
+ decrypted.favoriteModelsOnly = payload["favoriteModelsOnly"];
9304
+ if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
9305
+ decrypted.modelMatrix = payload["modelMatrix"];
9306
+ }
8455
9307
  if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
8456
9308
  const FEATURE_MAP = {
8457
9309
  featureMcp: "mcp",
@@ -8467,12 +9319,26 @@ async function startWebUI(opts = {}) {
8467
9319
  decrypted.features = feats;
8468
9320
  }
8469
9321
  }
8470
- if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string") {
9322
+ if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string" || typeof payload["contextMode"] === "string") {
8471
9323
  const ctxCfg = decrypted.context ?? {};
8472
9324
  if (typeof payload["contextAutoCompact"] === "boolean") ctxCfg.autoCompact = payload["contextAutoCompact"];
8473
9325
  if (typeof payload["contextStrategy"] === "string") ctxCfg.strategy = payload["contextStrategy"];
9326
+ if (typeof payload["contextMode"] === "string") ctxCfg.mode = payload["contextMode"];
8474
9327
  decrypted.context = ctxCfg;
8475
9328
  }
9329
+ if (typeof payload["tokenSavingTier"] === "string") {
9330
+ const featsCfg = decrypted.features ?? {};
9331
+ featsCfg.tokenSavingMode = payload["tokenSavingTier"];
9332
+ decrypted.features = featsCfg;
9333
+ }
9334
+ if (typeof payload["maxConcurrent"] === "number") {
9335
+ decrypted.maxConcurrent = payload["maxConcurrent"];
9336
+ }
9337
+ if (typeof payload["titleAnimation"] === "boolean") {
9338
+ const autoCfg = decrypted.autonomy ?? {};
9339
+ autoCfg.terminalTitleAnimation = payload["titleAnimation"];
9340
+ decrypted.autonomy = autoCfg;
9341
+ }
8476
9342
  if (typeof payload["logLevel"] === "string") {
8477
9343
  const logCfg = decrypted.log ?? {};
8478
9344
  logCfg.level = payload["logLevel"];
@@ -8543,6 +9409,7 @@ async function startWebUI(opts = {}) {
8543
9409
  const collabInject = collabInjectMiddleware(collabBus, { logger });
8544
9410
  Object.defineProperty(collabInject, "name", { value: "collab-inject" });
8545
9411
  pipelines.toolCall.prepend(collabInject);
9412
+ installDesignStudioMiddleware({ pipelines, ctx: context });
8546
9413
  const codebaseIndexing = setupWebUICodebaseIndexing({
8547
9414
  config,
8548
9415
  context,
@@ -8638,6 +9505,17 @@ async function startWebUI(opts = {}) {
8638
9505
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
8639
9506
  tracer: void 0
8640
9507
  });
9508
+ const webuiLogger = container.resolve(TOKENS.Logger);
9509
+ void discoverMailboxBridgeForWebui({
9510
+ projectRoot,
9511
+ config,
9512
+ logger: webuiLogger,
9513
+ ctx: context
9514
+ }).catch((err) => {
9515
+ webuiLogger.warn("mailbox bridge discovery threw on webui boot", {
9516
+ err: err instanceof Error ? err.message : String(err)
9517
+ });
9518
+ });
8641
9519
  const agent = new Agent({
8642
9520
  container,
8643
9521
  tools: toolRegistry,
@@ -8734,7 +9612,18 @@ async function startWebUI(opts = {}) {
8734
9612
  projectRoot
8735
9613
  );
8736
9614
  const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
8737
- const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
9615
+ const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards, void 0, {
9616
+ projectRoot,
9617
+ paths: {
9618
+ projectSpecs: wpaths.projectSpecs,
9619
+ projectTaskGraphs: wpaths.projectTaskGraphs,
9620
+ projectSddSession: wpaths.projectSddSession,
9621
+ projectSddBoards: wpaths.projectSddBoards
9622
+ }
9623
+ });
9624
+ void cleanupStaleSddWorktrees3({ projectRoot, boardsDir: wpaths.projectSddBoards }).catch(
9625
+ () => void 0
9626
+ );
8738
9627
  const sddWizardHandler = new SddWizardWebSocketHandler(
8739
9628
  buildSddWizardDeps({
8740
9629
  agent,
@@ -8756,7 +9645,10 @@ async function startWebUI(opts = {}) {
8756
9645
  }
8757
9646
  })
8758
9647
  );
8759
- const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
9648
+ const worktreeHandler = new WorktreeWebSocketHandler(events, logger, {
9649
+ projectRoot,
9650
+ boardsDir: wpaths.projectSddBoards
9651
+ });
8760
9652
  const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
8761
9653
  const collabHandler = new CollaborationWebSocketHandler(
8762
9654
  events,
@@ -8795,7 +9687,7 @@ async function startWebUI(opts = {}) {
8795
9687
  inputCost,
8796
9688
  outputCost,
8797
9689
  cacheReadCost,
8798
- projectName: path16.basename(projectRoot) || projectRoot,
9690
+ projectName: path17.basename(projectRoot) || projectRoot,
8799
9691
  projectRoot,
8800
9692
  cwd: workingDir,
8801
9693
  mode: modeId,
@@ -8944,8 +9836,8 @@ async function startWebUI(opts = {}) {
8944
9836
  clients.delete(ws);
8945
9837
  if (closing) rateLimits.delete(closing.connId);
8946
9838
  if (pendingConfirms.size > 0) {
8947
- for (const [id, resolve9] of pendingConfirms) {
8948
- resolve9("no");
9839
+ for (const [id, resolve10] of pendingConfirms) {
9840
+ resolve10("no");
8949
9841
  pendingConfirms.delete(id);
8950
9842
  }
8951
9843
  }
@@ -9025,21 +9917,21 @@ async function startWebUI(opts = {}) {
9025
9917
  });
9026
9918
  }
9027
9919
  async function touchProjectEntry(root, workDir) {
9028
- const resolved = path16.resolve(root);
9920
+ const resolved = path17.resolve(root);
9029
9921
  const manifest = await loadManifest(globalConfigPath);
9030
9922
  const now = (/* @__PURE__ */ new Date()).toISOString();
9031
- const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
9923
+ const existing = manifest.projects.find((p) => path17.resolve(p.root) === resolved);
9032
9924
  if (existing) {
9033
9925
  existing.lastSeen = now;
9034
- if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
9926
+ if (workDir) existing.lastWorkingDir = path17.resolve(workDir);
9035
9927
  } else {
9036
9928
  manifest.projects.push({
9037
- name: path16.basename(resolved),
9929
+ name: path17.basename(resolved),
9038
9930
  root: resolved,
9039
9931
  slug: generateProjectSlug(resolved),
9040
9932
  createdAt: now,
9041
9933
  lastSeen: now,
9042
- lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
9934
+ lastWorkingDir: workDir ? path17.resolve(workDir) : void 0
9043
9935
  });
9044
9936
  }
9045
9937
  await saveManifest(manifest, globalConfigPath);
@@ -9084,6 +9976,8 @@ async function startWebUI(opts = {}) {
9084
9976
  if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
9085
9977
  if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
9086
9978
  if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
9979
+ if (msg.type.startsWith("worktree.") && await worktreeHandler.handleMessage(msg))
9980
+ return;
9087
9981
  switch (msg.type) {
9088
9982
  // Collaboration messages short-circuit the user/agent flow.
9089
9983
  // They don't touch runLock, the agent loop, or the message queue —
@@ -9154,10 +10048,10 @@ async function startWebUI(opts = {}) {
9154
10048
  }
9155
10049
  case "tool.confirm_result": {
9156
10050
  const { id, decision } = msg.payload;
9157
- const resolve9 = pendingConfirms.get(id);
9158
- if (resolve9) {
10051
+ const resolve10 = pendingConfirms.get(id);
10052
+ if (resolve10) {
9159
10053
  pendingConfirms.delete(id);
9160
- resolve9(decision);
10054
+ resolve10(decision);
9161
10055
  }
9162
10056
  break;
9163
10057
  }
@@ -9241,6 +10135,48 @@ async function startWebUI(opts = {}) {
9241
10135
  case "skills.export":
9242
10136
  await handleSkillsExport(ws, { skillLoader, skillInstaller, projectRoot });
9243
10137
  break;
10138
+ // Prompt library — shared handlers (prompts-handlers.ts).
10139
+ case "prompts.list":
10140
+ await handlePromptsList(ws, promptsCtx);
10141
+ break;
10142
+ case "prompts.search":
10143
+ await handlePromptsSearch(ws, promptsCtx, msg);
10144
+ break;
10145
+ case "prompts.content":
10146
+ await handlePromptsContent(ws, promptsCtx, msg);
10147
+ break;
10148
+ case "prompts.favorite":
10149
+ await handlePromptsFavorite(ws, promptsCtx, msg);
10150
+ break;
10151
+ case "prompts.create":
10152
+ await handlePromptsCreate(ws, promptsCtx, msg);
10153
+ break;
10154
+ case "prompts.used":
10155
+ await handlePromptsUsed(ws, promptsCtx, msg);
10156
+ break;
10157
+ case "prompts.recent":
10158
+ await handlePromptsRecent(ws, promptsCtx);
10159
+ break;
10160
+ // Design Studio — shared handlers (design-handlers.ts). agentMeta is the
10161
+ // live context so design.use pins the active kit for the next turn.
10162
+ case "design.list":
10163
+ await handleDesignList(ws, { projectRoot, agentMeta: context });
10164
+ break;
10165
+ case "design.use":
10166
+ await handleDesignUse(ws, { projectRoot, agentMeta: context }, msg);
10167
+ break;
10168
+ case "design.state":
10169
+ await handleDesignState(ws, { projectRoot, agentMeta: context });
10170
+ break;
10171
+ case "design.set":
10172
+ await handleDesignSet(ws, { projectRoot, agentMeta: context }, msg);
10173
+ break;
10174
+ case "design.materialize":
10175
+ await handleDesignMaterialize(ws, { projectRoot, agentMeta: context }, msg);
10176
+ break;
10177
+ case "design.verify":
10178
+ await handleDesignVerify(ws, { projectRoot, agentMeta: context });
10179
+ break;
9244
10180
  case "diag.get": {
9245
10181
  const usage = tokenCounter.total();
9246
10182
  send(ws, {
@@ -9321,11 +10257,29 @@ async function startWebUI(opts = {}) {
9321
10257
  messages: context.messages.length,
9322
10258
  readFiles: context.readFiles.size,
9323
10259
  tools: toolRegistry.list().length,
10260
+ sideEffectCount: context.sideEffects?.length ?? 0,
9324
10261
  elapsedMs: Date.now() - sessionStartedAt
9325
10262
  }
9326
10263
  });
9327
10264
  break;
9328
10265
  }
10266
+ case "side_effects.list": {
10267
+ const sideEffects = context.sideEffects ?? [];
10268
+ send(ws, {
10269
+ type: "side_effects",
10270
+ payload: {
10271
+ sideEffects: sideEffects.slice(-50).map((se) => ({
10272
+ toolUseId: se.toolUseId,
10273
+ toolName: se.toolName,
10274
+ ts: se.ts,
10275
+ input: se.input,
10276
+ outcome: se.outcome,
10277
+ risk: se.risk
10278
+ }))
10279
+ }
10280
+ });
10281
+ break;
10282
+ }
9329
10283
  case "process.list": {
9330
10284
  await handleProcessList(ws);
9331
10285
  break;
@@ -9580,6 +10534,7 @@ async function startWebUI(opts = {}) {
9580
10534
  toolRegistry,
9581
10535
  config,
9582
10536
  projectRoot,
10537
+ globalRoot: wpaths.globalRoot,
9583
10538
  clients,
9584
10539
  setModeId: (id) => {
9585
10540
  modeId = id;
@@ -9616,6 +10571,16 @@ async function startWebUI(opts = {}) {
9616
10571
  config.features.modelsRegistry = payload["featureModelsRegistry"];
9617
10572
  if (Array.isArray(payload["fallbackModels"]))
9618
10573
  config.fallbackModels = payload["fallbackModels"];
10574
+ if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
10575
+ config.fallbackProfiles = payload["fallbackProfiles"];
10576
+ }
10577
+ if (Array.isArray(payload["favoriteModels"]))
10578
+ config.favoriteModels = payload["favoriteModels"];
10579
+ if (typeof payload["favoriteModelsOnly"] === "boolean")
10580
+ config.favoriteModelsOnly = payload["favoriteModelsOnly"];
10581
+ if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
10582
+ config.modelMatrix = payload["modelMatrix"];
10583
+ }
9619
10584
  if (typeof payload["fallbackAuto"] === "boolean")
9620
10585
  config.fallbackAuto = payload["fallbackAuto"];
9621
10586
  if (typeof payload["contextAutoCompact"] === "boolean") {
@@ -9667,7 +10632,7 @@ async function startWebUI(opts = {}) {
9667
10632
  sendResult2(ws, false, parsed.message);
9668
10633
  return;
9669
10634
  }
9670
- return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
10635
+ return handleMailboxMessages(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
9671
10636
  },
9672
10637
  agents: (ws, msg) => {
9673
10638
  const parsed = validateMailboxAgentsPayload(msg.payload);
@@ -9675,16 +10640,16 @@ async function startWebUI(opts = {}) {
9675
10640
  sendResult2(ws, false, parsed.message);
9676
10641
  return;
9677
10642
  }
9678
- return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
10643
+ return handleMailboxAgents(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
9679
10644
  },
9680
- clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
10645
+ clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }),
9681
10646
  purge: (ws, msg) => {
9682
10647
  const parsed = validateMailboxPurgePayload(msg.payload);
9683
10648
  if (!parsed.ok) {
9684
10649
  sendResult2(ws, false, parsed.message);
9685
10650
  return;
9686
10651
  }
9687
- return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
10652
+ return handleMailboxPurge(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
9688
10653
  }
9689
10654
  };
9690
10655
  mcpRoutes = {
@@ -9764,7 +10729,7 @@ async function startWebUI(opts = {}) {
9764
10729
  };
9765
10730
  const httpServer = createHttpServer({
9766
10731
  host: wsHost,
9767
- distDir: path16.resolve(import.meta.dirname, "../../dist"),
10732
+ distDir: path17.resolve(import.meta.dirname, "../../dist"),
9768
10733
  wsPort,
9769
10734
  publicWsUrl,
9770
10735
  globalRoot: wpaths.globalRoot,
@@ -9775,7 +10740,7 @@ async function startWebUI(opts = {}) {
9775
10740
  void fleetBroadcast?.();
9776
10741
  }
9777
10742
  });
9778
- const registryBaseDir = path16.dirname(globalConfigPath);
10743
+ const registryBaseDir = path17.dirname(globalConfigPath);
9779
10744
  httpServer.listen(httpPort, wsHost, () => {
9780
10745
  const openUrl = buildWebUIAccessUrl({
9781
10746
  host: wsHost,
@@ -9792,7 +10757,7 @@ async function startWebUI(opts = {}) {
9792
10757
  wsPort,
9793
10758
  host: wsHost,
9794
10759
  projectRoot,
9795
- projectName: path16.basename(projectRoot) || projectRoot,
10760
+ projectName: path17.basename(projectRoot) || projectRoot,
9796
10761
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9797
10762
  url: buildWebUIAccessUrl({ host: wsHost, port: httpPort, publicUrl })
9798
10763
  },
@@ -9860,6 +10825,12 @@ export {
9860
10825
  formatInstances,
9861
10826
  generateAuthToken,
9862
10827
  handleCompletionRequest,
10828
+ handleDesignList,
10829
+ handleDesignMaterialize,
10830
+ handleDesignSet,
10831
+ handleDesignState,
10832
+ handleDesignUse,
10833
+ handleDesignVerify,
9863
10834
  handleFilesList,
9864
10835
  handleFilesRead,
9865
10836
  handleFilesTree,
@@ -9880,6 +10851,13 @@ export {
9880
10851
  handleMemoryForget,
9881
10852
  handleMemoryList,
9882
10853
  handleMemoryRemember,
10854
+ handlePromptsContent,
10855
+ handlePromptsCreate,
10856
+ handlePromptsFavorite,
10857
+ handlePromptsList,
10858
+ handlePromptsRecent,
10859
+ handlePromptsSearch,
10860
+ handlePromptsUsed,
9883
10861
  handleShellOpen,
9884
10862
  handleSkillsContent,
9885
10863
  handleSkillsCreate,