@wrongstack/webui 0.274.0 → 0.275.0

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.
@@ -171,9 +171,12 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
171
171
  "reasoningPreserve",
172
172
  "hqEnabled",
173
173
  "hqRawContent",
174
- "fallbackAuto"
174
+ "fallbackAuto",
175
+ "favoriteModelsOnly"
175
176
  ]);
176
- var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
177
+ var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels", "favoriteModels"]);
178
+ var STRING_ARRAY_RECORD_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackProfiles"]);
179
+ var MODEL_MATRIX_PREF_KEYS = /* @__PURE__ */ new Set(["modelMatrix"]);
177
180
  var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
178
181
  "autonomyDelayMs",
179
182
  "autoProceedMaxIterations",
@@ -208,6 +211,33 @@ function validatePreferenceValue(key, value) {
208
211
  if (STRING_ARRAY_PREF_KEYS.has(key)) {
209
212
  return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
210
213
  }
214
+ if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
215
+ return isRecord(value) && Object.values(value).every(
216
+ (v) => Array.isArray(v) && v.every((item) => typeof item === "string")
217
+ ) ? null : `prefs.update payload.${key} must be an object of string arrays`;
218
+ }
219
+ if (MODEL_MATRIX_PREF_KEYS.has(key)) {
220
+ if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
221
+ for (const entry of Object.values(value)) {
222
+ if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
223
+ const provider = entry["provider"];
224
+ const model = entry["model"];
225
+ const fallbackProfile = entry["fallbackProfile"];
226
+ if (provider !== void 0 && typeof provider !== "string") {
227
+ return `prefs.update payload.${key}.provider must be a string when provided`;
228
+ }
229
+ if (model !== void 0 && typeof model !== "string") {
230
+ return `prefs.update payload.${key}.model must be a string when provided`;
231
+ }
232
+ if (fallbackProfile !== void 0 && typeof fallbackProfile !== "string") {
233
+ return `prefs.update payload.${key}.fallbackProfile must be a string when provided`;
234
+ }
235
+ if (model === void 0 && fallbackProfile === void 0) {
236
+ return `prefs.update payload.${key} entries require model or fallbackProfile`;
237
+ }
238
+ }
239
+ return null;
240
+ }
211
241
  const allowed = ENUM_PREF_KEYS[key];
212
242
  if (allowed) {
213
243
  return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
@@ -436,8 +466,8 @@ function validateShellOpenPayload(payload) {
436
466
  if (!isRecord(payload)) {
437
467
  return { ok: false, message: "shell.open payload must be an object with string path" };
438
468
  }
439
- const path17 = payload["path"];
440
- if (typeof path17 !== "string" || path17.trim().length === 0) {
469
+ const path18 = payload["path"];
470
+ if (typeof path18 !== "string" || path18.trim().length === 0) {
441
471
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
442
472
  }
443
473
  const target = payload["target"];
@@ -447,20 +477,20 @@ function validateShellOpenPayload(payload) {
447
477
  message: 'shell.open payload.target must be "file" or "terminal" when provided'
448
478
  };
449
479
  }
450
- return { ok: true, value: { path: path17, target } };
480
+ return { ok: true, value: { path: path18, target } };
451
481
  }
452
482
  function validateGitDiffPayload(payload) {
453
483
  if (!isRecord(payload)) {
454
484
  return { ok: false, message: "git.diff payload must be an object" };
455
485
  }
456
- const path17 = payload["path"];
457
- if (path17 === void 0 || path17 === null) {
486
+ const path18 = payload["path"];
487
+ if (path18 === void 0 || path18 === null) {
458
488
  return { ok: true, value: { path: "" } };
459
489
  }
460
- if (typeof path17 !== "string") {
490
+ if (typeof path18 !== "string") {
461
491
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
462
492
  }
463
- return { ok: true, value: { path: path17 } };
493
+ return { ok: true, value: { path: path18 } };
464
494
  }
465
495
  function validateProjectsAddPayload(payload) {
466
496
  if (!isRecord(payload)) {
@@ -710,8 +740,57 @@ async function handleWorklistMessage(ctx, ws, msg) {
710
740
 
711
741
  // src/server/index.ts
712
742
  import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
713
- import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as wstackGlobalRoot2, projectHash } from "@wrongstack/core/utils";
743
+ import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as wstackGlobalRoot3, projectHash } from "@wrongstack/core/utils";
714
744
  import { SkillInstaller } from "@wrongstack/core/skills";
745
+
746
+ // src/server/discover-mailbox-bridge.ts
747
+ import { resolveProjectDir, wstackGlobalRoot } from "@wrongstack/core";
748
+ import { readLiveLock } from "@wrongstack/core/coordination";
749
+ async function discoverMailboxBridgeForWebui(params) {
750
+ const mode = params.config?.features?.mailboxBridge ?? "auto";
751
+ if (mode === "off") return;
752
+ const projectDir = resolveProjectDir(params.projectRoot, wstackGlobalRoot());
753
+ const result = await readLiveLock(projectDir);
754
+ switch (result.kind) {
755
+ case "live": {
756
+ params.logger.debug("webui joined existing mailbox bridge", {
757
+ url: result.lock.url,
758
+ lockPath: projectDir
759
+ });
760
+ params.ctx.meta["mailboxBridge"] = {
761
+ url: result.lock.url,
762
+ token: result.lock.token,
763
+ lockPath: projectDir,
764
+ childPid: null,
765
+ source: "joined"
766
+ };
767
+ break;
768
+ }
769
+ case "probe-failed": {
770
+ params.logger.warn(
771
+ "mailbox bridge present but /healthz unreachable; webui will start without external-agent connectivity",
772
+ { url: result.lock.url, lockPath: projectDir }
773
+ );
774
+ params.ctx.meta["mailboxBridge"] = {
775
+ url: result.lock.url,
776
+ token: result.lock.token,
777
+ lockPath: projectDir,
778
+ childPid: null,
779
+ source: "unhealthy"
780
+ };
781
+ break;
782
+ }
783
+ case "absent": {
784
+ params.logger.info(
785
+ "no mailbox bridge running; webui will start without external-agent connectivity. Run `wstack mailbox serve` or a CLI surface to bring one up.",
786
+ { projectDir }
787
+ );
788
+ break;
789
+ }
790
+ }
791
+ }
792
+
793
+ // src/server/index.ts
715
794
  import {
716
795
  BrainMonitor,
717
796
  DefaultBrainArbiter,
@@ -719,8 +798,9 @@ import {
719
798
  createAutonomyBrain,
720
799
  createTieredBrainArbiter
721
800
  } from "@wrongstack/core";
722
- import * as fs13 from "fs/promises";
723
- import * as path16 from "path";
801
+ import * as fs14 from "fs/promises";
802
+ import * as path17 from "path";
803
+ import { createRequire as createRequire2 } from "module";
724
804
 
725
805
  // src/server/http-server.ts
726
806
  import * as fs from "fs/promises";
@@ -897,7 +977,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
897
977
  return;
898
978
  }
899
979
  try {
900
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
980
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
901
981
  const registry = new SessionRegistry(globalRoot);
902
982
  const entry = await registry.get(sessionId);
903
983
  if (!entry) {
@@ -905,7 +985,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
905
985
  res.end(JSON.stringify({ error: "Session not found" }));
906
986
  return;
907
987
  }
908
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
988
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
909
989
  const store = new DefaultSessionStore3({ dir: paths.projectSessions });
910
990
  const reader = new DefaultSessionReader2({ store });
911
991
  const rawEntries = [];
@@ -932,7 +1012,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
932
1012
  }
933
1013
  }
934
1014
  function readJsonBody(req) {
935
- return new Promise((resolve9, reject) => {
1015
+ return new Promise((resolve10, reject) => {
936
1016
  let data = "";
937
1017
  req.on("data", (chunk) => {
938
1018
  data += chunk;
@@ -943,7 +1023,7 @@ function readJsonBody(req) {
943
1023
  });
944
1024
  req.on("end", () => {
945
1025
  try {
946
- resolve9(data ? JSON.parse(data) : {});
1026
+ resolve10(data ? JSON.parse(data) : {});
947
1027
  } catch (err) {
948
1028
  reject(err instanceof Error ? err : new Error(String(err)));
949
1029
  }
@@ -979,7 +1059,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
979
1059
  const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
980
1060
  const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
981
1061
  try {
982
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1062
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
983
1063
  const registry = new SessionRegistry(globalRoot);
984
1064
  const entry = await registry.get(sessionId);
985
1065
  if (!entry) {
@@ -987,7 +1067,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
987
1067
  res.end(JSON.stringify({ error: "Session not found" }));
988
1068
  return;
989
1069
  }
990
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
1070
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
991
1071
  const mailbox = new GlobalMailbox3(paths.projectDir);
992
1072
  const to = `leader@${mailboxSessionTag2(sessionId)}`;
993
1073
  const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
@@ -1005,7 +1085,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
1005
1085
  return;
1006
1086
  }
1007
1087
  try {
1008
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1088
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1009
1089
  const registry = new SessionRegistry(globalRoot);
1010
1090
  const entry = await registry.get(sessionId);
1011
1091
  if (!entry) {
@@ -1013,7 +1093,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
1013
1093
  res.end(JSON.stringify({ error: "Session not found" }));
1014
1094
  return;
1015
1095
  }
1016
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
1096
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
1017
1097
  const mailbox = new GlobalMailbox3(paths.projectDir);
1018
1098
  const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
1019
1099
  const [inbound, outbound] = await Promise.all([
@@ -1063,7 +1143,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
1063
1143
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
1064
1144
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
1065
1145
  try {
1066
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1146
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1067
1147
  const registry = new SessionRegistry(globalRoot);
1068
1148
  const entry = await registry.get(sessionId);
1069
1149
  if (!entry) {
@@ -1071,7 +1151,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
1071
1151
  res.end(JSON.stringify({ error: "Session not found" }));
1072
1152
  return;
1073
1153
  }
1074
- const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
1154
+ const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
1075
1155
  const mailbox = new GlobalMailbox3(paths.projectDir);
1076
1156
  const to = `leader@${mailboxSessionTag2(sessionId)}`;
1077
1157
  const sent = await mailbox.send({
@@ -1111,7 +1191,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
1111
1191
  }
1112
1192
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
1113
1193
  try {
1114
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1194
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
1115
1195
  const registry = new SessionRegistry(globalRoot);
1116
1196
  const all = await registry.list();
1117
1197
  const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
@@ -1123,7 +1203,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
1123
1203
  }
1124
1204
  const mbByDir = /* @__PURE__ */ new Map();
1125
1205
  const mailboxFor = (projectRoot) => {
1126
- const dir = resolveWstackPaths2({ projectRoot, globalRoot }).projectDir;
1206
+ const dir = resolveWstackPaths4({ projectRoot, globalRoot }).projectDir;
1127
1207
  let mb = mbByDir.get(dir);
1128
1208
  if (!mb) {
1129
1209
  mb = new GlobalMailbox3(dir);
@@ -1334,7 +1414,7 @@ function buildCspHeader(wsPort, requestHost, publicWsUrl) {
1334
1414
  `ws://127.0.0.1:${wsPort}`,
1335
1415
  `wss://127.0.0.1:${wsPort}`
1336
1416
  ]);
1337
- if (requestHost && requestHost !== "127.0.0.1") {
1417
+ if (requestHost && requestHost !== "127.0.0.1" && requestHost !== "::1" && requestHost !== "[::1]") {
1338
1418
  const host = formatCspHostname(requestHost);
1339
1419
  connect.add(`ws://${host}:${wsPort}`);
1340
1420
  connect.add(`wss://${host}:${wsPort}`);
@@ -1859,6 +1939,16 @@ async function realpathAllowMissing(p) {
1859
1939
  }
1860
1940
  }
1861
1941
  }
1942
+ function validatedPayload(msg, label) {
1943
+ if (msg == null || typeof msg !== "object") {
1944
+ throw new TypeError(`Expected object for ${label}, got ${msg}`);
1945
+ }
1946
+ const payload = msg.payload;
1947
+ if (payload == null || typeof payload !== "object") {
1948
+ throw new TypeError(`Expected payload object for ${label}, got ${payload}`);
1949
+ }
1950
+ return payload;
1951
+ }
1862
1952
  async function handleFilesTree(ws, msg, projectRoot) {
1863
1953
  const payload = msg.payload;
1864
1954
  const rawPath = payload?.path?.trim();
@@ -1929,7 +2019,13 @@ async function handleFilesTree(ws, msg, projectRoot) {
1929
2019
  }
1930
2020
  }
1931
2021
  async function handleFilesRead(ws, msg, projectRoot) {
1932
- const { filePath } = msg.payload;
2022
+ let filePath;
2023
+ try {
2024
+ ({ filePath } = validatedPayload(msg, "files.read"));
2025
+ } catch {
2026
+ send(ws, { type: "files.read", payload: { filePath: "", content: "", error: "Malformed request" } });
2027
+ return;
2028
+ }
1933
2029
  let realResolved;
1934
2030
  try {
1935
2031
  realResolved = await resolveFileInsideProject(projectRoot, filePath);
@@ -1948,7 +2044,14 @@ async function handleFilesRead(ws, msg, projectRoot) {
1948
2044
  }
1949
2045
  }
1950
2046
  async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
1951
- const { filePath, content } = msg.payload;
2047
+ let filePath;
2048
+ let content;
2049
+ try {
2050
+ ({ filePath, content } = validatedPayload(msg, "files.write"));
2051
+ } catch {
2052
+ send(ws, { type: "files.written", payload: { filePath: "", success: false, error: "Malformed request" } });
2053
+ return;
2054
+ }
1952
2055
  let realResolved;
1953
2056
  try {
1954
2057
  realResolved = await resolveFileInsideProject(projectRoot, filePath);
@@ -2786,7 +2889,7 @@ import { promises as fs5 } from "fs";
2786
2889
  import path6 from "path";
2787
2890
  import JSZip from "jszip";
2788
2891
  import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
2789
- import { wstackGlobalRoot } from "@wrongstack/core/utils";
2892
+ import { wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
2790
2893
  async function handleSkillsList(ws, ctx) {
2791
2894
  if (!ctx.skillLoader) {
2792
2895
  send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
@@ -2956,7 +3059,7 @@ async function handleSkillsCreate(ws, ctx, msg) {
2956
3059
  }
2957
3060
  const createPayload = parsed.value;
2958
3061
  try {
2959
- const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
3062
+ const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
2960
3063
  try {
2961
3064
  await fs5.access(targetDir);
2962
3065
  send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
@@ -3076,6 +3179,416 @@ async function handleSkillsExport(ws, ctx) {
3076
3179
  }
3077
3180
  }
3078
3181
 
3182
+ // src/server/prompts-handlers.ts
3183
+ function parseVariablesPayload(raw) {
3184
+ if (!Array.isArray(raw)) return void 0;
3185
+ const out = [];
3186
+ for (const item of raw) {
3187
+ if (!item || typeof item !== "object") continue;
3188
+ const o = item;
3189
+ if (typeof o["name"] !== "string" || !o["name"].trim()) continue;
3190
+ const enumVals = Array.isArray(o["enum"]) && o["enum"].every((x) => typeof x === "string") ? o["enum"].map((s) => s.trim()).filter(Boolean) : void 0;
3191
+ const v = { name: o["name"].trim() };
3192
+ if (typeof o["description"] === "string" && o["description"].trim()) {
3193
+ v.description = o["description"].trim();
3194
+ }
3195
+ if (o["required"] === true) v.required = true;
3196
+ if (o["multiline"] === true) v.multiline = true;
3197
+ if (enumVals && enumVals.length > 0) v.enum = enumVals;
3198
+ out.push(v);
3199
+ }
3200
+ return out.length > 0 ? out : void 0;
3201
+ }
3202
+ function toMeta(e) {
3203
+ return {
3204
+ id: e.id,
3205
+ slug: e.slug,
3206
+ title: e.title,
3207
+ description: e.description,
3208
+ category: e.category,
3209
+ tags: e.tags,
3210
+ source: e.source,
3211
+ favorite: e.favorite,
3212
+ variables: e.variables ?? []
3213
+ };
3214
+ }
3215
+ async function handlePromptsList(ws, ctx) {
3216
+ if (!ctx.promptLoader) {
3217
+ send(ws, { type: "prompts.list", payload: { enabled: false, prompts: [], categories: [] } });
3218
+ return;
3219
+ }
3220
+ try {
3221
+ const [all, categories] = await Promise.all([
3222
+ ctx.promptLoader.list(),
3223
+ ctx.promptLoader.categories()
3224
+ ]);
3225
+ send(ws, {
3226
+ type: "prompts.list",
3227
+ payload: { enabled: true, prompts: all.map(toMeta), categories }
3228
+ });
3229
+ } catch (err) {
3230
+ send(ws, {
3231
+ type: "prompts.list",
3232
+ payload: { enabled: true, prompts: [], categories: [], error: errMessage(err) }
3233
+ });
3234
+ }
3235
+ }
3236
+ async function handlePromptsSearch(ws, ctx, msg) {
3237
+ if (!ctx.promptLoader) {
3238
+ send(ws, { type: "prompts.search", payload: { enabled: false, prompts: [] } });
3239
+ return;
3240
+ }
3241
+ const payload = msg.payload ?? {};
3242
+ try {
3243
+ const results = await ctx.promptLoader.search(payload.query ?? "", {
3244
+ ...payload.category ? { category: payload.category } : {},
3245
+ limit: 50
3246
+ });
3247
+ send(ws, { type: "prompts.search", payload: { enabled: true, prompts: results.map(toMeta) } });
3248
+ } catch (err) {
3249
+ send(ws, {
3250
+ type: "prompts.search",
3251
+ payload: { enabled: true, prompts: [], error: errMessage(err) }
3252
+ });
3253
+ }
3254
+ }
3255
+ async function handlePromptsContent(ws, ctx, msg) {
3256
+ const slug = msg.payload?.slug;
3257
+ if (!ctx.promptLoader || !slug) {
3258
+ send(ws, {
3259
+ type: "prompts.content",
3260
+ payload: { slug: slug ?? "", found: false, content: "", variables: [] }
3261
+ });
3262
+ return;
3263
+ }
3264
+ try {
3265
+ const entry = await ctx.promptLoader.find(slug);
3266
+ if (!entry) {
3267
+ send(ws, {
3268
+ type: "prompts.content",
3269
+ payload: { slug, found: false, content: "", variables: [] }
3270
+ });
3271
+ return;
3272
+ }
3273
+ send(ws, {
3274
+ type: "prompts.content",
3275
+ payload: {
3276
+ slug: entry.slug,
3277
+ found: true,
3278
+ title: entry.title,
3279
+ content: entry.content,
3280
+ variables: entry.variables ?? [],
3281
+ category: entry.category,
3282
+ source: entry.source
3283
+ }
3284
+ });
3285
+ } catch (err) {
3286
+ send(ws, {
3287
+ type: "prompts.content",
3288
+ payload: { slug, found: false, content: "", variables: [], error: errMessage(err) }
3289
+ });
3290
+ }
3291
+ }
3292
+ async function handlePromptsFavorite(ws, ctx, msg) {
3293
+ const payload = msg.payload;
3294
+ if (!ctx.promptLoader || !payload?.slug) {
3295
+ send(ws, {
3296
+ type: "prompts.favorite",
3297
+ payload: { success: false, error: "Prompt library unavailable" }
3298
+ });
3299
+ return;
3300
+ }
3301
+ try {
3302
+ const updated = await ctx.promptLoader.setFavorite(payload.slug, payload.favorite !== false);
3303
+ if (!updated) {
3304
+ send(ws, {
3305
+ type: "prompts.favorite",
3306
+ payload: { success: false, error: "Prompt not found" }
3307
+ });
3308
+ return;
3309
+ }
3310
+ send(ws, {
3311
+ type: "prompts.favorite",
3312
+ payload: { success: true, slug: updated.slug, favorite: updated.favorite }
3313
+ });
3314
+ } catch (err) {
3315
+ send(ws, { type: "prompts.favorite", payload: { success: false, error: errMessage(err) } });
3316
+ }
3317
+ }
3318
+ async function handlePromptsCreate(ws, ctx, msg) {
3319
+ const p = msg.payload;
3320
+ if (!ctx.promptLoader || !p) {
3321
+ send(ws, {
3322
+ type: "prompts.created",
3323
+ payload: { success: false, error: "Prompt library unavailable" }
3324
+ });
3325
+ return;
3326
+ }
3327
+ const title = typeof p["title"] === "string" ? p["title"].trim() : "";
3328
+ const content = typeof p["content"] === "string" ? p["content"] : "";
3329
+ if (!title || !content) {
3330
+ send(ws, {
3331
+ type: "prompts.created",
3332
+ payload: { success: false, error: "Title and content are required" }
3333
+ });
3334
+ return;
3335
+ }
3336
+ try {
3337
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3338
+ const tags = Array.isArray(p["tags"]) ? p["tags"].filter((t) => typeof t === "string") : [];
3339
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "prompt";
3340
+ const variables = parseVariablesPayload(p["variables"]);
3341
+ const entry = {
3342
+ id: slug,
3343
+ slug,
3344
+ title,
3345
+ description: typeof p["description"] === "string" ? p["description"] : "",
3346
+ content,
3347
+ category: typeof p["category"] === "string" && p["category"] ? p["category"] : "uncategorized",
3348
+ tags,
3349
+ source: "user",
3350
+ favorite: false,
3351
+ ...variables ? { variables } : {},
3352
+ createdAt: now,
3353
+ updatedAt: now
3354
+ };
3355
+ await ctx.promptLoader.save(entry);
3356
+ send(ws, { type: "prompts.created", payload: { success: true, slug } });
3357
+ } catch (err) {
3358
+ send(ws, { type: "prompts.created", payload: { success: false, error: errMessage(err) } });
3359
+ }
3360
+ }
3361
+ async function handlePromptsUsed(ws, ctx, msg) {
3362
+ const slug = msg.payload?.slug;
3363
+ if (!ctx.promptUsage || !slug) {
3364
+ send(ws, { type: "prompts.used", payload: { success: false } });
3365
+ return;
3366
+ }
3367
+ try {
3368
+ await ctx.promptUsage.record(slug);
3369
+ send(ws, { type: "prompts.used", payload: { success: true, slug } });
3370
+ } catch {
3371
+ send(ws, { type: "prompts.used", payload: { success: false } });
3372
+ }
3373
+ }
3374
+ async function handlePromptsRecent(ws, ctx) {
3375
+ if (!ctx.promptUsage) {
3376
+ send(ws, { type: "prompts.recent", payload: { slugs: [] } });
3377
+ return;
3378
+ }
3379
+ try {
3380
+ const recent = await ctx.promptUsage.recent(50);
3381
+ send(ws, { type: "prompts.recent", payload: { slugs: recent.map((r) => r.slug) } });
3382
+ } catch (err) {
3383
+ send(ws, { type: "prompts.recent", payload: { slugs: [], error: errMessage(err) } });
3384
+ }
3385
+ }
3386
+
3387
+ // src/server/design-handlers.ts
3388
+ import * as fs6 from "fs/promises";
3389
+ import * as path7 from "path";
3390
+ import {
3391
+ applyTokenOverrides,
3392
+ getDesignKitLoader,
3393
+ getDesignState,
3394
+ isDesignStack,
3395
+ loadActiveKit,
3396
+ materializeTokens,
3397
+ recordKitChoice,
3398
+ recordOverrides,
3399
+ runDesignVerify,
3400
+ setActiveKit,
3401
+ setDesignOverrides
3402
+ } from "@wrongstack/core";
3403
+ function readOverrides(value) {
3404
+ const out = {};
3405
+ if (value && typeof value === "object") {
3406
+ for (const [k, v] of Object.entries(value)) {
3407
+ if (typeof v === "string") out[k] = v;
3408
+ }
3409
+ }
3410
+ return out;
3411
+ }
3412
+ var FOUNDATIONS_ID = "_foundations";
3413
+ async function buildListPayload(ctx) {
3414
+ const loader = getDesignKitLoader(ctx.projectRoot);
3415
+ const manifests = (await loader.list()).filter((k) => k.id !== FOUNDATIONS_ID);
3416
+ const kits = [];
3417
+ for (const m of manifests) {
3418
+ const tokens = await loader.readTokens(m.id);
3419
+ kits.push({
3420
+ id: m.id,
3421
+ name: m.name,
3422
+ aesthetic: m.aesthetic,
3423
+ bestFor: m.bestFor,
3424
+ stacks: m.stacks,
3425
+ tags: m.tags,
3426
+ light: tokens?.light ?? {},
3427
+ dark: tokens?.dark ?? {}
3428
+ });
3429
+ }
3430
+ const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
3431
+ const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
3432
+ return {
3433
+ kits,
3434
+ activeKit: state?.activeKit ?? persisted?.kit ?? null,
3435
+ stack: state?.stack ?? persisted?.stack ?? null,
3436
+ overrides: state?.overrides ?? persisted?.overrides ?? {}
3437
+ };
3438
+ }
3439
+ async function handleDesignList(ws, ctx) {
3440
+ try {
3441
+ send(ws, { type: "design.list", payload: await buildListPayload(ctx) });
3442
+ } catch (err) {
3443
+ send(ws, {
3444
+ type: "design.list",
3445
+ payload: { kits: [], activeKit: null, stack: null, error: String(err) }
3446
+ });
3447
+ }
3448
+ }
3449
+ async function handleDesignState(ws, ctx) {
3450
+ const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
3451
+ send(ws, {
3452
+ type: "design.state",
3453
+ payload: {
3454
+ activeKit: state?.activeKit ?? null,
3455
+ stack: state?.stack ?? null,
3456
+ overrides: state?.overrides ?? {}
3457
+ }
3458
+ });
3459
+ }
3460
+ async function handleDesignUse(ws, ctx, msg) {
3461
+ const payload = msg.payload ?? {};
3462
+ const kitId = typeof payload.kit === "string" ? payload.kit.trim() : "";
3463
+ if (!kitId) {
3464
+ send(ws, { type: "design.use", payload: { ok: false, error: "No kit id provided" } });
3465
+ return;
3466
+ }
3467
+ try {
3468
+ const loader = getDesignKitLoader(ctx.projectRoot);
3469
+ const kit = await loader.find(kitId);
3470
+ if (!kit) {
3471
+ send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: "Kit not found" } });
3472
+ return;
3473
+ }
3474
+ const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
3475
+ const stack = stackArg && isDesignStack(stackArg) ? stackArg : kit.stacks[0] ?? "web";
3476
+ const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
3477
+ const keep = persisted?.kit === kit.id ? persisted.overrides ?? {} : {};
3478
+ const overrides = { ...keep, ...readOverrides(payload.overrides) };
3479
+ if (ctx.agentMeta) setActiveKit(ctx.agentMeta, kit.id, stack, overrides);
3480
+ await recordKitChoice(
3481
+ ctx.projectRoot,
3482
+ kit.id,
3483
+ stack,
3484
+ "webui",
3485
+ (/* @__PURE__ */ new Date()).toISOString(),
3486
+ Object.keys(overrides).length ? overrides : void 0
3487
+ );
3488
+ const body = await loader.readBody(kit.id, stack);
3489
+ const rawTokens = await loader.readTokens(kit.id);
3490
+ const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
3491
+ send(ws, {
3492
+ type: "design.use",
3493
+ payload: {
3494
+ ok: true,
3495
+ kit: kit.id,
3496
+ name: kit.name,
3497
+ aesthetic: kit.aesthetic,
3498
+ stack,
3499
+ body,
3500
+ overrides,
3501
+ light: tokens?.light ?? {},
3502
+ dark: tokens?.dark ?? {}
3503
+ }
3504
+ });
3505
+ } catch (err) {
3506
+ send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: String(err) } });
3507
+ }
3508
+ }
3509
+ async function handleDesignSet(ws, ctx, msg) {
3510
+ const patch = readOverrides(msg.payload?.overrides);
3511
+ if (Object.keys(patch).length === 0) {
3512
+ send(ws, { type: "design.set", payload: { ok: false, error: "No overrides provided" } });
3513
+ return;
3514
+ }
3515
+ try {
3516
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
3517
+ if (!merged) {
3518
+ send(ws, { type: "design.set", payload: { ok: false, error: "No active kit" } });
3519
+ return;
3520
+ }
3521
+ if (ctx.agentMeta) setDesignOverrides(ctx.agentMeta, merged);
3522
+ send(ws, { type: "design.set", payload: { ok: true, overrides: merged } });
3523
+ } catch (err) {
3524
+ send(ws, { type: "design.set", payload: { ok: false, error: String(err) } });
3525
+ }
3526
+ }
3527
+ async function handleDesignMaterialize(ws, ctx, msg) {
3528
+ const payload = msg.payload ?? {};
3529
+ try {
3530
+ const active = await loadActiveKit(ctx.projectRoot);
3531
+ if (!active) {
3532
+ send(ws, { type: "design.materialize", payload: { ok: false, error: "No active kit" } });
3533
+ return;
3534
+ }
3535
+ const loader = getDesignKitLoader(ctx.projectRoot);
3536
+ const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
3537
+ const stack = stackArg && isDesignStack(stackArg) ? stackArg : active.stack && isDesignStack(active.stack) ? active.stack : "web";
3538
+ const raw = await loader.readTokens(active.kit);
3539
+ if (!raw) {
3540
+ send(ws, { type: "design.materialize", payload: { ok: false, error: "Kit has no tokens" } });
3541
+ return;
3542
+ }
3543
+ const tokens = applyTokenOverrides(raw, active.overrides);
3544
+ const result = materializeTokens({
3545
+ tokens,
3546
+ stack,
3547
+ kitId: active.kit,
3548
+ outPath: typeof payload.out === "string" ? payload.out : void 0
3549
+ });
3550
+ const abs = path7.join(ctx.projectRoot, result.path);
3551
+ await fs6.mkdir(path7.dirname(abs), { recursive: true });
3552
+ await fs6.writeFile(abs, result.content);
3553
+ send(ws, {
3554
+ type: "design.materialize",
3555
+ payload: { ok: true, path: result.path, format: result.format, stack }
3556
+ });
3557
+ } catch (err) {
3558
+ send(ws, { type: "design.materialize", payload: { ok: false, error: String(err) } });
3559
+ }
3560
+ }
3561
+ async function handleDesignVerify(ws, ctx) {
3562
+ try {
3563
+ const active = await loadActiveKit(ctx.projectRoot);
3564
+ if (!active) {
3565
+ send(ws, { type: "design.verify", payload: { ok: false, error: "No active kit" } });
3566
+ return;
3567
+ }
3568
+ const loader = getDesignKitLoader(ctx.projectRoot);
3569
+ const raw = await loader.readTokens(active.kit);
3570
+ if (!raw) {
3571
+ send(ws, { type: "design.verify", payload: { ok: false, error: "Kit has no tokens" } });
3572
+ return;
3573
+ }
3574
+ const tokens = applyTokenOverrides(raw, active.overrides);
3575
+ const report = await runDesignVerify(ctx.projectRoot, tokens);
3576
+ send(ws, {
3577
+ type: "design.verify",
3578
+ payload: {
3579
+ ok: true,
3580
+ kit: active.kit,
3581
+ filesScanned: report.filesScanned,
3582
+ score: report.score,
3583
+ violations: report.violations.slice(0, 50),
3584
+ violationCount: report.violations.length
3585
+ }
3586
+ });
3587
+ } catch (err) {
3588
+ send(ws, { type: "design.verify", payload: { ok: false, error: String(err) } });
3589
+ }
3590
+ }
3591
+
3079
3592
  // src/server/index.ts
3080
3593
  import {
3081
3594
  Agent,
@@ -3087,6 +3600,8 @@ import {
3087
3600
  DefaultSessionReader,
3088
3601
  DefaultSessionStore as DefaultSessionStore2,
3089
3602
  DefaultSkillLoader,
3603
+ DefaultPromptLoader,
3604
+ PromptUsageStore,
3090
3605
  DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
3091
3606
  DefaultTokenCounter,
3092
3607
  AnnotationsStore,
@@ -3101,17 +3616,20 @@ import {
3101
3616
  ToolRegistry,
3102
3617
  atomicWrite as atomicWrite6,
3103
3618
  createDefaultPipelines,
3619
+ installDesignStudioMiddleware,
3104
3620
  createSessionEventBridge,
3105
3621
  resolveSessionLoggingConfig,
3106
3622
  DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
3107
3623
  DEFAULT_SESSION_PRUNE_DAYS,
3108
3624
  DEFAULT_TOOLS_CONFIG,
3109
3625
  applyToolDescriptionModes,
3626
+ applyToolResultRenderModes,
3110
3627
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
3111
3628
  enhanceUserPrompt,
3112
3629
  gatedEnhancerReasoning,
3113
3630
  recentTextTurns,
3114
- resolveProviderModelList
3631
+ resolveProviderModelList,
3632
+ cleanupStaleSddWorktrees as cleanupStaleSddWorktrees3
3115
3633
  } from "@wrongstack/core";
3116
3634
  import { ToolExecutor } from "@wrongstack/core/execution";
3117
3635
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
@@ -3794,7 +4312,7 @@ var SpecsWebSocketHandler = class {
3794
4312
  };
3795
4313
 
3796
4314
  // src/server/sdd-board-ws-handler.ts
3797
- import { SddBoardStore } from "@wrongstack/core";
4315
+ import { applySddLifecycle, SddBoardStore } from "@wrongstack/core";
3798
4316
  var CONTROL_TYPES = /* @__PURE__ */ new Set([
3799
4317
  "pause",
3800
4318
  "resume",
@@ -3808,19 +4326,19 @@ var CONTROL_TYPES = /* @__PURE__ */ new Set([
3808
4326
  "set_task_verification",
3809
4327
  "cancel_task",
3810
4328
  "delete_task",
3811
- "split_task",
3812
- // Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
3813
- "cleanup_worktrees",
3814
- "rollback"
4329
+ "split_task"
3815
4330
  ]);
4331
+ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set(["cleanup_worktrees", "rollback", "destroy"]);
3816
4332
  var SddBoardWebSocketHandler = class {
3817
4333
  store;
3818
4334
  clients = /* @__PURE__ */ new Set();
4335
+ lifecycle;
3819
4336
  latest = null;
3820
4337
  poll = null;
3821
4338
  unsub = null;
3822
- constructor(boardsDir, events) {
4339
+ constructor(boardsDir, events, lifecycle) {
3823
4340
  this.store = new SddBoardStore({ baseDir: boardsDir });
4341
+ this.lifecycle = lifecycle;
3824
4342
  if (events) {
3825
4343
  const handler = (e) => {
3826
4344
  this.latest = e.snapshot;
@@ -3849,6 +4367,10 @@ var SddBoardWebSocketHandler = class {
3849
4367
  return;
3850
4368
  }
3851
4369
  const action = msg.type.replace(/^sdd\.board\./, "");
4370
+ if (LIFECYCLE_TYPES.has(action)) {
4371
+ await this.applyLifecycle(action, msg.payload);
4372
+ return;
4373
+ }
3852
4374
  if (CONTROL_TYPES.has(action)) {
3853
4375
  const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
3854
4376
  if (runId) {
@@ -3860,6 +4382,40 @@ var SddBoardWebSocketHandler = class {
3860
4382
  }
3861
4383
  }
3862
4384
  }
4385
+ /**
4386
+ * Apply a cleanup/rollback/destroy from disk and broadcast a structured
4387
+ * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
4388
+ * the user must stop it first; the UI gates the buttons on `!active` and the
4389
+ * Destroy flow auto-stops then waits before sending `destroy`.
4390
+ */
4391
+ async applyLifecycle(op, payload) {
4392
+ if (!this.lifecycle) {
4393
+ this.broadcast({
4394
+ type: "sdd.board.lifecycle_result",
4395
+ payload: { op, ok: false, reason: "Lifecycle operations are not available in this session." }
4396
+ });
4397
+ return;
4398
+ }
4399
+ if (this.latest && (this.latest.status === "running" || this.latest.status === "paused")) {
4400
+ this.broadcast({
4401
+ type: "sdd.board.lifecycle_result",
4402
+ payload: { op, ok: false, reason: "Stop the run first, then retry." }
4403
+ });
4404
+ return;
4405
+ }
4406
+ const runId = payload?.runId ?? this.latest?.runId;
4407
+ const result = await applySddLifecycle(op, {
4408
+ projectRoot: this.lifecycle.projectRoot,
4409
+ paths: this.lifecycle.paths,
4410
+ runId,
4411
+ revertMerged: payload?.revertMerged === true
4412
+ });
4413
+ this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
4414
+ if (op === "destroy" && result.ok) {
4415
+ this.latest = null;
4416
+ this.broadcast({ type: "sdd.board.snapshot", payload: null });
4417
+ }
4418
+ }
3863
4419
  dispose() {
3864
4420
  if (this.poll) clearInterval(this.poll);
3865
4421
  this.unsub?.();
@@ -4039,9 +4595,10 @@ var SddWizardWebSocketHandler = class {
4039
4595
  };
4040
4596
 
4041
4597
  // src/server/sdd-wizard-wiring.ts
4042
- import * as path7 from "path";
4598
+ import * as path8 from "path";
4043
4599
  import { spawnSync as spawnSync2 } from "child_process";
4044
4600
  import {
4601
+ cleanupStaleSddWorktrees,
4045
4602
  makeCommandVerifier,
4046
4603
  makeLlmSubtaskGenerator,
4047
4604
  SddBoardStore as SddBoardStore2,
@@ -4076,7 +4633,7 @@ function buildSddWizardDeps(opts) {
4076
4633
  makeDriver: () => new SddInterviewDriver({
4077
4634
  specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
4078
4635
  graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
4079
- sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
4636
+ sessionPath: path8.join(opts.paths.projectDir, "sdd-wizard-session.json")
4080
4637
  }),
4081
4638
  runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
4082
4639
  startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels, worktrees: useWorktrees }) => {
@@ -4093,7 +4650,13 @@ function buildSddWizardDeps(opts) {
4093
4650
  encoding: "utf8",
4094
4651
  windowsHide: true
4095
4652
  }).stdout?.trim() === "true";
4096
- if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
4653
+ if (inGit) {
4654
+ await cleanupStaleSddWorktrees({
4655
+ projectRoot: opts.projectRoot,
4656
+ boardsDir: opts.paths.projectSddBoards
4657
+ }).catch(() => void 0);
4658
+ worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
4659
+ }
4097
4660
  }
4098
4661
  const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
4099
4662
  const verifyTask = makeCommandVerifier();
@@ -4872,16 +5435,16 @@ var CollaborationWebSocketHandler = class {
4872
5435
  };
4873
5436
 
4874
5437
  // src/server/projects-manifest.ts
4875
- import * as fs6 from "fs/promises";
4876
- import * as path8 from "path";
5438
+ import * as fs7 from "fs/promises";
5439
+ import * as path9 from "path";
4877
5440
  import { projectSlug } from "@wrongstack/core";
4878
5441
  function projectsJsonPath(globalConfigPath) {
4879
- const base = path8.dirname(globalConfigPath);
4880
- return path8.join(base, "projects.json");
5442
+ const base = path9.dirname(globalConfigPath);
5443
+ return path9.join(base, "projects.json");
4881
5444
  }
4882
5445
  async function loadManifest(globalConfigPath) {
4883
5446
  try {
4884
- const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
5447
+ const raw = await fs7.readFile(projectsJsonPath(globalConfigPath), "utf8");
4885
5448
  const parsed = JSON.parse(raw);
4886
5449
  return { projects: parsed.projects ?? [] };
4887
5450
  } catch {
@@ -4890,16 +5453,16 @@ async function loadManifest(globalConfigPath) {
4890
5453
  }
4891
5454
  async function saveManifest(manifest, globalConfigPath) {
4892
5455
  const file = projectsJsonPath(globalConfigPath);
4893
- await fs6.mkdir(path8.dirname(file), { recursive: true });
4894
- await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
5456
+ await fs7.mkdir(path9.dirname(file), { recursive: true });
5457
+ await fs7.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4895
5458
  }
4896
5459
  function generateProjectSlug(rootPath) {
4897
5460
  return projectSlug(rootPath);
4898
5461
  }
4899
5462
  async function ensureProjectDataDir(slug, globalConfigPath) {
4900
- const base = path8.dirname(globalConfigPath);
4901
- const dir = path8.join(base, "projects", slug);
4902
- await fs6.mkdir(dir, { recursive: true });
5463
+ const base = path9.dirname(globalConfigPath);
5464
+ const dir = path9.join(base, "projects", slug);
5465
+ await fs7.mkdir(dir, { recursive: true });
4903
5466
  return dir;
4904
5467
  }
4905
5468
 
@@ -5061,16 +5624,22 @@ function clampDim(value, fallback) {
5061
5624
  }
5062
5625
 
5063
5626
  // src/server/worktree-ws-handler.ts
5627
+ import { join as join6, resolve as resolve6, sep as sep4 } from "path";
5628
+ import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2, WorktreeManager as WorktreeManager3 } from "@wrongstack/core";
5064
5629
  import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
5065
5630
  var MAX_ACTIVITY = 6;
5631
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
5632
+ var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
5066
5633
  var WorktreeWebSocketHandler = class {
5067
- constructor(events, logger) {
5634
+ constructor(events, logger, management) {
5068
5635
  this.events = events;
5069
5636
  this.logger = logger;
5637
+ this.management = management;
5070
5638
  this.subscribe();
5071
5639
  }
5072
5640
  events;
5073
5641
  logger;
5642
+ management;
5074
5643
  clients = /* @__PURE__ */ new Set();
5075
5644
  handles = /* @__PURE__ */ new Map();
5076
5645
  baseBranch = "";
@@ -5081,12 +5650,197 @@ var WorktreeWebSocketHandler = class {
5081
5650
  ws.on("close", () => this.clients.delete(ws));
5082
5651
  ws.on("error", () => this.clients.delete(ws));
5083
5652
  this.send(ws, this.stateMessage());
5653
+ void this.scanAndBroadcast();
5654
+ }
5655
+ /** Handle worktree-panel control messages (scan / clean / per-row ops). */
5656
+ async handleMessage(msg) {
5657
+ if (msg.type === "worktree.scan") {
5658
+ await this.scanAndBroadcast();
5659
+ return true;
5660
+ }
5661
+ if (msg.type === "worktree.cleanup") {
5662
+ await this.cleanupOrphans();
5663
+ return true;
5664
+ }
5665
+ if (msg.type === "worktree.remove") {
5666
+ await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
5667
+ return true;
5668
+ }
5669
+ if (msg.type === "worktree.merge") {
5670
+ await this.mergeBranch(msg.payload?.["branch"]);
5671
+ return true;
5672
+ }
5673
+ if (msg.type === "worktree.diff") {
5674
+ await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
5675
+ return true;
5676
+ }
5677
+ return false;
5084
5678
  }
5085
5679
  dispose() {
5086
5680
  for (const off of this.offs) off();
5087
5681
  this.offs.length = 0;
5088
5682
  this.stopBroadcast();
5089
5683
  }
5684
+ // ── orphan management ─────────────────────────────────────────────────────
5685
+ /** Absolute managed-worktrees root for this project. */
5686
+ worktreesRoot() {
5687
+ return resolve6(join6(this.management.projectRoot, ".wrongstack", "worktrees"));
5688
+ }
5689
+ /** True iff `dir` resolves strictly inside the managed worktrees root. */
5690
+ underRoot(dir) {
5691
+ const abs = resolve6(dir);
5692
+ const root = this.worktreesRoot();
5693
+ return abs !== root && abs.startsWith(root + sep4);
5694
+ }
5695
+ /** Branches of worktrees a live in-session run currently owns. */
5696
+ liveActiveBranches() {
5697
+ const live = /* @__PURE__ */ new Set();
5698
+ for (const h of this.handles.values()) {
5699
+ if (ACTIVE_STATUSES.has(h.status) && h.branch) live.add(h.branch);
5700
+ }
5701
+ return live;
5702
+ }
5703
+ /**
5704
+ * Scan the disk for managed worktrees/branches NOT owned by a live in-session
5705
+ * run and broadcast them as orphans, with whether it is safe to clean now.
5706
+ * No-op (empty inventory) when management deps were not wired.
5707
+ */
5708
+ async scanAndBroadcast() {
5709
+ if (!this.management) {
5710
+ this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
5711
+ return;
5712
+ }
5713
+ try {
5714
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5715
+ const { worktrees, branches } = await wt.listManaged();
5716
+ const live = this.liveActiveBranches();
5717
+ const orphans = [];
5718
+ const seenBranches = /* @__PURE__ */ new Set();
5719
+ for (const w of worktrees) {
5720
+ if (w.branch && live.has(w.branch)) continue;
5721
+ if (w.branch) seenBranches.add(w.branch);
5722
+ orphans.push({ kind: "worktree", dir: w.dir, branch: w.branch });
5723
+ }
5724
+ for (const b of branches) {
5725
+ if (live.has(b) || seenBranches.has(b)) continue;
5726
+ orphans.push({ kind: "branch", branch: b });
5727
+ }
5728
+ const canClean = this.liveActiveBranches().size === 0;
5729
+ this.broadcast({
5730
+ type: "worktree.orphans",
5731
+ payload: {
5732
+ orphans,
5733
+ canClean,
5734
+ reason: canClean ? void 0 : "a run is live in this session"
5735
+ }
5736
+ });
5737
+ } catch (err) {
5738
+ this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage4(err)}`);
5739
+ this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
5740
+ }
5741
+ }
5742
+ /**
5743
+ * Force-remove every orphaned worktree + branch. Refused while a run is live —
5744
+ * in this session (active handles) OR another process (the SDD board liveness
5745
+ * guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
5746
+ */
5747
+ async cleanupOrphans() {
5748
+ if (!this.management) {
5749
+ this.broadcast({
5750
+ type: "worktree.cleanup_result",
5751
+ payload: { ok: false, removed: 0, reason: "cleanup is not available in this session" }
5752
+ });
5753
+ return;
5754
+ }
5755
+ if (this.liveActiveBranches().size > 0) {
5756
+ this.broadcast({
5757
+ type: "worktree.cleanup_result",
5758
+ payload: { ok: false, removed: 0, reason: "a run is live in this session \u2014 stop it first" }
5759
+ });
5760
+ return;
5761
+ }
5762
+ const res = await cleanupStaleSddWorktrees2({
5763
+ projectRoot: this.management.projectRoot,
5764
+ boardsDir: this.management.boardsDir
5765
+ });
5766
+ if (res.skippedReason) {
5767
+ this.broadcast({
5768
+ type: "worktree.cleanup_result",
5769
+ payload: { ok: false, removed: 0, reason: res.skippedReason }
5770
+ });
5771
+ await this.scanAndBroadcast();
5772
+ return;
5773
+ }
5774
+ for (const [id, h] of [...this.handles]) {
5775
+ if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
5776
+ }
5777
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: true, removed: res.removed } });
5778
+ this.broadcastState();
5779
+ await this.scanAndBroadcast();
5780
+ }
5781
+ /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
5782
+ async removeOne(dir, branch) {
5783
+ if (!this.management || !dir && !branch) {
5784
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "nothing to remove" } });
5785
+ return;
5786
+ }
5787
+ if (branch && !MANAGED_BRANCH_RE.test(branch)) {
5788
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "not a managed worktree branch" } });
5789
+ return;
5790
+ }
5791
+ if (dir && !this.underRoot(dir)) {
5792
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" } });
5793
+ return;
5794
+ }
5795
+ if (branch && this.liveActiveBranches().has(branch)) {
5796
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "a run is live on this worktree \u2014 stop it first" } });
5797
+ return;
5798
+ }
5799
+ let removed = false;
5800
+ if (dir) {
5801
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5802
+ ({ removed } = await wt.removeOne(dir, branch));
5803
+ }
5804
+ for (const [id, h] of [...this.handles]) {
5805
+ if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId)) this.handles.delete(id);
5806
+ }
5807
+ this.broadcast({ type: "worktree.cleanup_result", payload: { ok: removed, removed: removed ? 1 : 0, reason: removed ? void 0 : "remove failed (not a managed worktree?)" } });
5808
+ this.broadcastState();
5809
+ await this.scanAndBroadcast();
5810
+ }
5811
+ /** Squash-merge ONE branch into base. Refused while a live run owns it. */
5812
+ async mergeBranch(branch) {
5813
+ if (!this.management || !branch) {
5814
+ this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch: branch ?? "", reason: "no branch" } });
5815
+ return;
5816
+ }
5817
+ if (!MANAGED_BRANCH_RE.test(branch)) {
5818
+ this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "not a managed worktree branch" } });
5819
+ return;
5820
+ }
5821
+ if (this.liveActiveBranches().has(branch)) {
5822
+ this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" } });
5823
+ return;
5824
+ }
5825
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5826
+ const res = await wt.mergeBranch(branch);
5827
+ this.broadcast({
5828
+ type: "worktree.merge_result",
5829
+ payload: { ok: res.ok, branch, conflict: res.conflict, conflictFiles: res.conflictFiles, reason: res.reason }
5830
+ });
5831
+ await this.scanAndBroadcast();
5832
+ }
5833
+ /** Compact change summary for one worktree checkout. */
5834
+ async diffOne(dir, baseBranch) {
5835
+ if (!this.management || !dir || !this.underRoot(dir)) {
5836
+ this.broadcast({ type: "worktree.diff_result", payload: { dir: dir ?? "", summary: null } });
5837
+ return;
5838
+ }
5839
+ const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
5840
+ const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
5841
+ const summary = await wt.diffSummary(resolve6(dir), base);
5842
+ this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
5843
+ }
5090
5844
  // ── internals ───────────────────────────────────────────────────────────
5091
5845
  subscribe() {
5092
5846
  const on = this.events.on.bind(this.events);
@@ -5098,6 +5852,7 @@ var WorktreeWebSocketHandler = class {
5098
5852
  handleId: e.handleId,
5099
5853
  ownerId: e.ownerId,
5100
5854
  ownerLabel: e.ownerLabel,
5855
+ dir: e.dir,
5101
5856
  branch: e.branch,
5102
5857
  baseBranch: e.baseBranch,
5103
5858
  status: "active",
@@ -5200,10 +5955,10 @@ var WorktreeWebSocketHandler = class {
5200
5955
  };
5201
5956
 
5202
5957
  // src/server/mailbox-handlers.ts
5203
- import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
5958
+ import { GlobalMailbox, resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core";
5204
5959
  async function handleMailboxMessages(ws, deps2, payload) {
5205
5960
  try {
5206
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
5961
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5207
5962
  const mb = new GlobalMailbox(dir);
5208
5963
  const messages = await mb.query({
5209
5964
  limit: payload?.limit ?? 30,
@@ -5239,7 +5994,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
5239
5994
  }
5240
5995
  async function handleMailboxAgents(ws, deps2, payload) {
5241
5996
  try {
5242
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
5997
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5243
5998
  const mb = new GlobalMailbox(dir);
5244
5999
  const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
5245
6000
  send(ws, {
@@ -5268,7 +6023,7 @@ async function handleMailboxAgents(ws, deps2, payload) {
5268
6023
  }
5269
6024
  async function handleMailboxClear(ws, deps2) {
5270
6025
  try {
5271
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
6026
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5272
6027
  const mb = new GlobalMailbox(dir);
5273
6028
  await mb.clearAll();
5274
6029
  send(ws, { type: "mailbox.cleared", payload: {} });
@@ -5278,7 +6033,7 @@ async function handleMailboxClear(ws, deps2) {
5278
6033
  }
5279
6034
  async function handleMailboxPurge(ws, deps2, opts) {
5280
6035
  try {
5281
- const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
6036
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
5282
6037
  const mb = new GlobalMailbox(dir);
5283
6038
  const result = await mb.purgeStale(opts);
5284
6039
  send(ws, { type: "mailbox.purged", payload: result });
@@ -5325,14 +6080,14 @@ function registerShutdownHandlers(res) {
5325
6080
 
5326
6081
  // src/server/instance-registry.ts
5327
6082
  import * as os from "os";
5328
- import * as path9 from "path";
5329
- import * as fs7 from "fs/promises";
6083
+ import * as path10 from "path";
6084
+ import * as fs8 from "fs/promises";
5330
6085
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
5331
6086
  function defaultBaseDir() {
5332
- return path9.join(os.homedir(), ".wrongstack");
6087
+ return path10.join(os.homedir(), ".wrongstack");
5333
6088
  }
5334
6089
  function registryPath(baseDir = defaultBaseDir()) {
5335
- return path9.join(baseDir, "webui-instances.json");
6090
+ return path10.join(baseDir, "webui-instances.json");
5336
6091
  }
5337
6092
  function isPidAlive(pid) {
5338
6093
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -5345,7 +6100,7 @@ function isPidAlive(pid) {
5345
6100
  }
5346
6101
  async function load(file) {
5347
6102
  try {
5348
- const raw = await fs7.readFile(file, "utf8");
6103
+ const raw = await fs8.readFile(file, "utf8");
5349
6104
  const parsed = JSON.parse(raw);
5350
6105
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
5351
6106
  return parsed;
@@ -5404,16 +6159,16 @@ function formatInstances(instances) {
5404
6159
  // src/server/port-utils.ts
5405
6160
  import * as net from "net";
5406
6161
  function isPortFree(host, port) {
5407
- return new Promise((resolve9) => {
6162
+ return new Promise((resolve10) => {
5408
6163
  const srv = net.createServer();
5409
- srv.once("error", () => resolve9(false));
6164
+ srv.once("error", () => resolve10(false));
5410
6165
  srv.once("listening", () => {
5411
- srv.close(() => resolve9(true));
6166
+ srv.close(() => resolve10(true));
5412
6167
  });
5413
6168
  try {
5414
6169
  srv.listen(port, host);
5415
6170
  } catch {
5416
- resolve9(false);
6171
+ resolve10(false);
5417
6172
  }
5418
6173
  });
5419
6174
  }
@@ -5494,15 +6249,15 @@ import { DefaultSecretScrubber } from "@wrongstack/core";
5494
6249
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
5495
6250
 
5496
6251
  // src/server/provider-config-io.ts
5497
- import * as fs8 from "fs/promises";
5498
- import * as path10 from "path";
6252
+ import * as fs9 from "fs/promises";
6253
+ import * as path11 from "path";
5499
6254
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
5500
6255
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
5501
6256
  import { DefaultSecretVault } from "@wrongstack/core";
5502
6257
  async function loadSavedProviders(configPath, vault) {
5503
6258
  let raw;
5504
6259
  try {
5505
- raw = await fs8.readFile(configPath, "utf8");
6260
+ raw = await fs9.readFile(configPath, "utf8");
5506
6261
  } catch {
5507
6262
  return {};
5508
6263
  }
@@ -5519,7 +6274,7 @@ async function saveProviders(configPath, vault, providers) {
5519
6274
  let raw;
5520
6275
  let fileExists = true;
5521
6276
  try {
5522
- raw = await fs8.readFile(configPath, "utf8");
6277
+ raw = await fs9.readFile(configPath, "utf8");
5523
6278
  } catch (err) {
5524
6279
  if (err.code !== "ENOENT") {
5525
6280
  throw new Error(
@@ -5857,7 +6612,8 @@ function createProviderHandlers(deps2) {
5857
6612
 
5858
6613
  // src/server/mode-handlers.ts
5859
6614
  import {
5860
- DefaultSystemPromptBuilder
6615
+ DefaultSystemPromptBuilder,
6616
+ resolveWstackPaths
5861
6617
  } from "@wrongstack/core";
5862
6618
  function createModeHandlers(ctx) {
5863
6619
  return {
@@ -5905,13 +6661,18 @@ function createModeHandlers(ctx) {
5905
6661
  }
5906
6662
  ctx.setModeId(id);
5907
6663
  const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
6664
+ const paths = resolveWstackPaths({ projectRoot: ctx.projectRoot, globalRoot: ctx.globalRoot });
5908
6665
  const freshBuilder = new DefaultSystemPromptBuilder({
5909
6666
  memoryStore: ctx.memoryStore,
5910
6667
  skillLoader: ctx.skillLoader,
5911
6668
  modeStore: ctx.modeStore,
5912
6669
  modeId: id,
5913
6670
  modePrompt,
5914
- modelCapabilities: ctx.modelCapabilities
6671
+ modelCapabilities: ctx.modelCapabilities,
6672
+ instructionPaths: {
6673
+ globalDir: paths.globalInstructions,
6674
+ projectDir: paths.inProjectInstructions
6675
+ }
5915
6676
  });
5916
6677
  ctx.context.systemPrompt = await freshBuilder.build({
5917
6678
  cwd: ctx.projectRoot,
@@ -5933,12 +6694,13 @@ function createModeHandlers(ctx) {
5933
6694
  }
5934
6695
 
5935
6696
  // src/server/project-handlers.ts
5936
- import * as fs9 from "fs/promises";
5937
- import * as path11 from "path";
6697
+ import * as fs10 from "fs/promises";
6698
+ import * as path12 from "path";
5938
6699
  import {
5939
6700
  DefaultSessionStore,
5940
6701
  DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
5941
- getSessionRegistry
6702
+ getSessionRegistry,
6703
+ resolveWstackPaths as resolveWstackPaths2
5942
6704
  } from "@wrongstack/core";
5943
6705
  function createProjectHandlers(ctx) {
5944
6706
  return {
@@ -5961,9 +6723,9 @@ function createProjectHandlers(ctx) {
5961
6723
  }
5962
6724
  const { root: addRoot, name: displayName } = parsed.value;
5963
6725
  try {
5964
- const resolved = path11.resolve(addRoot);
5965
- await fs9.access(resolved);
5966
- const stat3 = await fs9.stat(resolved);
6726
+ const resolved = path12.resolve(addRoot);
6727
+ await fs10.access(resolved);
6728
+ const stat3 = await fs10.stat(resolved);
5967
6729
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
5968
6730
  const manifest = await loadManifest(ctx.globalConfigPath);
5969
6731
  const existing = manifest.projects.find((p) => p.root === resolved);
@@ -5979,7 +6741,7 @@ function createProjectHandlers(ctx) {
5979
6741
  });
5980
6742
  return;
5981
6743
  }
5982
- const name2 = displayName?.trim() || path11.basename(resolved);
6744
+ const name2 = displayName?.trim() || path12.basename(resolved);
5983
6745
  const slug = generateProjectSlug(resolved);
5984
6746
  await ensureProjectDataDir(slug, ctx.globalConfigPath);
5985
6747
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5992,7 +6754,7 @@ function createProjectHandlers(ctx) {
5992
6754
  } catch (err) {
5993
6755
  send(ws, {
5994
6756
  type: "projects.added",
5995
- payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
6757
+ payload: { name: path12.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5996
6758
  });
5997
6759
  }
5998
6760
  },
@@ -6007,17 +6769,17 @@ function createProjectHandlers(ctx) {
6007
6769
  }
6008
6770
  const { root: selRoot, name: selName } = parsed.value;
6009
6771
  try {
6010
- const resolved = path11.resolve(selRoot);
6772
+ const resolved = path12.resolve(selRoot);
6011
6773
  try {
6012
- await fs9.access(resolved);
6013
- const stat3 = await fs9.stat(resolved);
6774
+ await fs10.access(resolved);
6775
+ const stat3 = await fs10.stat(resolved);
6014
6776
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
6015
6777
  } catch (err) {
6016
6778
  send(ws, {
6017
6779
  type: "projects.selected",
6018
6780
  payload: {
6019
6781
  root: selRoot,
6020
- name: selName || path11.basename(selRoot),
6782
+ name: selName || path12.basename(selRoot),
6021
6783
  message: `Cannot switch: ${errMessage(err)}`
6022
6784
  }
6023
6785
  });
@@ -6029,7 +6791,7 @@ function createProjectHandlers(ctx) {
6029
6791
  entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
6030
6792
  entry.lastWorkingDir = resolved;
6031
6793
  } else {
6032
- const name2 = selName?.trim() || path11.basename(resolved);
6794
+ const name2 = selName?.trim() || path12.basename(resolved);
6033
6795
  const slug = generateProjectSlug(resolved);
6034
6796
  manifest.projects.push({
6035
6797
  name: name2,
@@ -6051,13 +6813,21 @@ function createProjectHandlers(ctx) {
6051
6813
  try {
6052
6814
  const modeId = ctx.getModeId();
6053
6815
  const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
6816
+ const switchPaths = resolveWstackPaths2({
6817
+ projectRoot: resolved,
6818
+ globalRoot: ctx.wpaths.globalRoot
6819
+ });
6054
6820
  const switchBuilder = new DefaultSystemPromptBuilder2({
6055
6821
  memoryStore: ctx.memoryStore,
6056
6822
  skillLoader: ctx.skillLoader,
6057
6823
  modeStore: ctx.modeStore,
6058
6824
  modeId,
6059
6825
  modePrompt: switchMode?.prompt ?? "",
6060
- modelCapabilities: ctx.modelCapabilities
6826
+ modelCapabilities: ctx.modelCapabilities,
6827
+ instructionPaths: {
6828
+ globalDir: switchPaths.globalInstructions,
6829
+ projectDir: switchPaths.inProjectInstructions
6830
+ }
6061
6831
  });
6062
6832
  ctx.context.systemPrompt = await switchBuilder.build({
6063
6833
  cwd: resolved,
@@ -6068,13 +6838,13 @@ function createProjectHandlers(ctx) {
6068
6838
  });
6069
6839
  } catch {
6070
6840
  }
6071
- const newSessionsDir = path11.join(
6072
- path11.dirname(ctx.globalConfigPath),
6841
+ const newSessionsDir = path12.join(
6842
+ path12.dirname(ctx.globalConfigPath),
6073
6843
  "projects",
6074
6844
  switchSlug,
6075
6845
  "sessions"
6076
6846
  );
6077
- await fs9.mkdir(newSessionsDir, { recursive: true });
6847
+ await fs10.mkdir(newSessionsDir, { recursive: true });
6078
6848
  const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
6079
6849
  const oldSession = ctx.getSession();
6080
6850
  const oldSessionId = oldSession.id;
@@ -6108,7 +6878,7 @@ function createProjectHandlers(ctx) {
6108
6878
  sessionId: newSession.id,
6109
6879
  projectSlug: switchSlug,
6110
6880
  projectRoot: resolved,
6111
- projectName: path11.basename(resolved),
6881
+ projectName: path12.basename(resolved),
6112
6882
  workingDir: resolved,
6113
6883
  clientType: "webui",
6114
6884
  pid: process.pid,
@@ -6120,8 +6890,8 @@ function createProjectHandlers(ctx) {
6120
6890
  type: "projects.selected",
6121
6891
  payload: {
6122
6892
  root: resolved,
6123
- name: selName || path11.basename(resolved),
6124
- message: `Switched to ${selName || path11.basename(resolved)}`
6893
+ name: selName || path12.basename(resolved),
6894
+ message: `Switched to ${selName || path12.basename(resolved)}`
6125
6895
  }
6126
6896
  });
6127
6897
  broadcast(ctx.clients, {
@@ -6141,7 +6911,7 @@ function createProjectHandlers(ctx) {
6141
6911
  type: "projects.selected",
6142
6912
  payload: {
6143
6913
  root: selRoot,
6144
- name: selName || path11.basename(selRoot),
6914
+ name: selName || path12.basename(selRoot),
6145
6915
  message: errMessage(err)
6146
6916
  }
6147
6917
  });
@@ -6172,7 +6942,7 @@ function createProjectHandlers(ctx) {
6172
6942
  }
6173
6943
 
6174
6944
  // src/server/session-handlers.ts
6175
- import * as path12 from "path";
6945
+ import * as path13 from "path";
6176
6946
  import {
6177
6947
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
6178
6948
  repairToolUseAdjacency,
@@ -6514,7 +7284,7 @@ function createSessionHandlers(ctx) {
6514
7284
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
6515
7285
  const projectRoot = ctx.getProjectRoot();
6516
7286
  const rewinder = new DefaultSessionRewinder(
6517
- path12.join(projectRoot, ".wrongstack", "sessions"),
7287
+ path13.join(projectRoot, ".wrongstack", "sessions"),
6518
7288
  projectRoot
6519
7289
  );
6520
7290
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
@@ -6529,7 +7299,7 @@ function createSessionHandlers(ctx) {
6529
7299
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
6530
7300
  const projectRoot = ctx.getProjectRoot();
6531
7301
  const rewinder = new DefaultSessionRewinder(
6532
- path12.join(projectRoot, ".wrongstack", "sessions"),
7302
+ path13.join(projectRoot, ".wrongstack", "sessions"),
6533
7303
  projectRoot
6534
7304
  );
6535
7305
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
@@ -6905,9 +7675,9 @@ async function handleSddBoardRoute(_ws, msg, handlers) {
6905
7675
  }
6906
7676
 
6907
7677
  // src/server/setup-events.ts
6908
- import * as fs10 from "fs/promises";
7678
+ import * as fs11 from "fs/promises";
6909
7679
  import { watch as fsWatch } from "fs";
6910
- import * as path13 from "path";
7680
+ import * as path14 from "path";
6911
7681
  function setupEvents(deps2) {
6912
7682
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
6913
7683
  const disposers = [];
@@ -6996,6 +7766,22 @@ function setupEvents(deps2) {
6996
7766
  }).catch(() => {
6997
7767
  });
6998
7768
  broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
7769
+ const sideEffects = context.sideEffects ?? [];
7770
+ if (sideEffects.length > 0) {
7771
+ broadcast2(clients, {
7772
+ type: "side_effects",
7773
+ payload: {
7774
+ sideEffects: sideEffects.slice(-50).map((se) => ({
7775
+ toolUseId: se.toolUseId,
7776
+ toolName: se.toolName,
7777
+ ts: se.ts,
7778
+ input: se.input,
7779
+ outcome: se.outcome,
7780
+ risk: se.risk
7781
+ }))
7782
+ }
7783
+ });
7784
+ }
6999
7785
  if (e.name === "task" || e.name === "plan" || e.name === "todo") {
7000
7786
  void (async () => {
7001
7787
  try {
@@ -7374,16 +8160,16 @@ function setupEvents(deps2) {
7374
8160
  if (wpaths?.projectStatus) {
7375
8161
  try {
7376
8162
  const statusFile = wpaths.projectStatus(e.projectHash);
7377
- const dir = path13.dirname(statusFile);
7378
- await fs10.mkdir(dir, { recursive: true });
7379
- await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
8163
+ const dir = path14.dirname(statusFile);
8164
+ await fs11.mkdir(dir, { recursive: true });
8165
+ await fs11.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
7380
8166
  } catch (err) {
7381
8167
  console.error("[setup-events] Failed to write status.json:", err);
7382
8168
  }
7383
8169
  }
7384
8170
  });
7385
8171
  if (wpaths?.projectStatus && wpaths.configDir) {
7386
- const projectsDir = path13.join(wpaths.configDir, "projects");
8172
+ const projectsDir = path14.join(wpaths.configDir, "projects");
7387
8173
  const knownProjectHashes = /* @__PURE__ */ new Set();
7388
8174
  const debounceTimers = /* @__PURE__ */ new Map();
7389
8175
  const DEBOUNCE_MS = 150;
@@ -7446,20 +8232,20 @@ function setupEvents(deps2) {
7446
8232
  let watcher;
7447
8233
  const startWatcher = async () => {
7448
8234
  try {
7449
- await fs10.mkdir(projectsDir, { recursive: true });
8235
+ await fs11.mkdir(projectsDir, { recursive: true });
7450
8236
  watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
7451
8237
  if (eventType === "change") {
7452
8238
  if (filename == null) return;
7453
8239
  if (watcherMetrics) watcherMetrics.fileChangesDetected++;
7454
- const targetFile = path13.join(projectsDir, String(filename));
8240
+ const targetFile = path14.join(projectsDir, String(filename));
7455
8241
  if (targetFile.endsWith("status.json")) {
7456
- const projectHash2 = path13.basename(path13.dirname(targetFile));
8242
+ const projectHash2 = path14.basename(path14.dirname(targetFile));
7457
8243
  if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
7458
8244
  return;
7459
8245
  }
7460
8246
  if (watcherMetrics) watcherMetrics.filesProcessed++;
7461
8247
  try {
7462
- const content = await fs10.readFile(targetFile, "utf-8");
8248
+ const content = await fs11.readFile(targetFile, "utf-8");
7463
8249
  const statusData = JSON.parse(content);
7464
8250
  if (statusData.projectHash) {
7465
8251
  const hash = String(statusData.projectHash);
@@ -7511,7 +8297,7 @@ function setupEvents(deps2) {
7511
8297
  }
7512
8298
  });
7513
8299
  }
7514
- const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
8300
+ const globalRoot = globalConfigPath ? path14.dirname(globalConfigPath) : void 0;
7515
8301
  if (globalRoot) {
7516
8302
  const broadcastSessions = async () => {
7517
8303
  try {
@@ -7584,11 +8370,11 @@ function setupEvents(deps2) {
7584
8370
 
7585
8371
  // src/server/custom-context-modes.ts
7586
8372
  import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
7587
- import * as fs11 from "fs/promises";
7588
- import * as path14 from "path";
8373
+ import * as fs12 from "fs/promises";
8374
+ import * as path15 from "path";
7589
8375
  var STORE_FILENAME = "custom-context-modes.json";
7590
8376
  function storePath(wrongstackDir) {
7591
- return path14.join(wrongstackDir, STORE_FILENAME);
8377
+ return path15.join(wrongstackDir, STORE_FILENAME);
7592
8378
  }
7593
8379
  var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
7594
8380
  function createCustomModeStore(wrongstackDir) {
@@ -7596,7 +8382,7 @@ function createCustomModeStore(wrongstackDir) {
7596
8382
  const load2 = async () => {
7597
8383
  modes.clear();
7598
8384
  try {
7599
- const raw = await fs11.readFile(storePath(wrongstackDir), "utf8");
8385
+ const raw = await fs12.readFile(storePath(wrongstackDir), "utf8");
7600
8386
  const parsed = JSON.parse(raw);
7601
8387
  if (Array.isArray(parsed.modes)) {
7602
8388
  for (const m of parsed.modes) {
@@ -7642,7 +8428,8 @@ function createCustomModeStore(wrongstackDir) {
7642
8428
  custom: true
7643
8429
  };
7644
8430
  modes.set(mode.id, entry);
7645
- void save2();
8431
+ void save2().catch(() => {
8432
+ });
7646
8433
  return { ok: true };
7647
8434
  };
7648
8435
  const update = (id, patch) => {
@@ -7668,7 +8455,8 @@ function createCustomModeStore(wrongstackDir) {
7668
8455
  if (patch.targetLoad !== void 0) next.targetLoad = patch.targetLoad;
7669
8456
  if (patch.aggressiveOn !== void 0) next.aggressiveOn = patch.aggressiveOn;
7670
8457
  modes.set(id, next);
7671
- void save2();
8458
+ void save2().catch(() => {
8459
+ });
7672
8460
  return { ok: true };
7673
8461
  };
7674
8462
  const remove = (id) => {
@@ -7678,7 +8466,8 @@ function createCustomModeStore(wrongstackDir) {
7678
8466
  if (!modes.delete(id)) {
7679
8467
  return { ok: false, error: `Mode "${id}" not found` };
7680
8468
  }
7681
- void save2();
8469
+ void save2().catch(() => {
8470
+ });
7682
8471
  return { ok: true };
7683
8472
  };
7684
8473
  const list = () => {
@@ -7719,14 +8508,17 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
7719
8508
  }
7720
8509
 
7721
8510
  // src/server/shell-open.ts
7722
- import * as fs12 from "fs/promises";
7723
- import * as path15 from "path";
8511
+ import * as fs13 from "fs/promises";
8512
+ import * as path16 from "path";
7724
8513
  import { spawn as spawn2 } from "child_process";
7725
- var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
8514
+ var METACHAR_REGEX = /[&|<>^"'`'\n\r]/;
8515
+ function shellQuote(s) {
8516
+ return s.replaceAll("'", `'"'"'`);
8517
+ }
7726
8518
  async function handleShellOpen(req, logger) {
7727
8519
  try {
7728
- const resolved = path15.resolve(req.path);
7729
- await fs12.access(resolved);
8520
+ const resolved = path16.resolve(req.path);
8521
+ await fs13.access(resolved);
7730
8522
  if (METACHAR_REGEX.test(resolved)) {
7731
8523
  return { success: false, message: "Path contains unsupported characters." };
7732
8524
  }
@@ -7759,7 +8551,11 @@ async function handleShellOpen(req, logger) {
7759
8551
  () => launch(
7760
8552
  "gnome-terminal",
7761
8553
  [`--working-directory=${resolved}`],
7762
- () => launch("xterm", ["-e", `cd '${resolved}' && ${process.env["SHELL"] ?? "sh"}`])
8554
+ () => (
8555
+ // Pass argv array so sh -c sees a literal string, not an interpolated one.
8556
+ // shellQuote() guards against paths that somehow slipped the METACHAR_REGEX.
8557
+ launch("xterm", ["-e", "sh", "-c", `cd ${shellQuote(resolved)} && ${process.env["SHELL"] ?? "sh"}`])
8558
+ )
7763
8559
  )
7764
8560
  );
7765
8561
  }
@@ -7778,9 +8574,9 @@ async function handleGitInfo(ws, projectRoot) {
7778
8574
  const cwd = projectRoot || void 0;
7779
8575
  try {
7780
8576
  const { execFile: ef } = await import("child_process");
7781
- const git = (args) => new Promise((resolve9) => {
8577
+ const git = (args) => new Promise((resolve10) => {
7782
8578
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
7783
- resolve9(err ? "" : stdout.trim());
8579
+ resolve10(err ? "" : stdout.trim());
7784
8580
  });
7785
8581
  });
7786
8582
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -7806,12 +8602,12 @@ async function handleGitInfo(ws, projectRoot) {
7806
8602
  function makeGit(cwd) {
7807
8603
  return async (args) => {
7808
8604
  const { execFile: ef } = await import("child_process");
7809
- return new Promise((resolve9) => {
8605
+ return new Promise((resolve10) => {
7810
8606
  ef(
7811
8607
  "git",
7812
8608
  args,
7813
8609
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
7814
- (err, stdout) => resolve9(err ? "" : stdout)
8610
+ (err, stdout) => resolve10(err ? "" : stdout)
7815
8611
  );
7816
8612
  });
7817
8613
  };
@@ -7835,15 +8631,15 @@ async function handleGitChanges(ws, projectRoot) {
7835
8631
  if (!m) continue;
7836
8632
  const added = m[1] === "-" ? 0 : Number(m[1]);
7837
8633
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
7838
- let path17 = m[3] ?? "";
7839
- if (path17 === "") {
8634
+ let path18 = m[3] ?? "";
8635
+ if (path18 === "") {
7840
8636
  i += 1;
7841
- path17 = parts[i + 1] ?? parts[i] ?? "";
8637
+ path18 = parts[i + 1] ?? parts[i] ?? "";
7842
8638
  i += 1;
7843
8639
  }
7844
- if (!path17) continue;
7845
- const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
7846
- counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
8640
+ if (!path18) continue;
8641
+ const prev = counts.get(path18) ?? { added: 0, deleted: 0 };
8642
+ counts.set(path18, { added: prev.added + added, deleted: prev.deleted + deleted });
7847
8643
  }
7848
8644
  };
7849
8645
  parseNumstat(unstagedNumstat);
@@ -7855,7 +8651,7 @@ async function handleGitChanges(ws, projectRoot) {
7855
8651
  if (!rec || rec.length < 3) continue;
7856
8652
  const x = rec[0] ?? " ";
7857
8653
  const y = rec[1] ?? " ";
7858
- const path17 = rec.slice(3);
8654
+ const path18 = rec.slice(3);
7859
8655
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
7860
8656
  if (isRename) i += 1;
7861
8657
  let status;
@@ -7867,13 +8663,13 @@ async function handleGitChanges(ws, projectRoot) {
7867
8663
  else if (x === "D" || y === "D") status = "D";
7868
8664
  else status = "M";
7869
8665
  const staged = x !== " " && x !== "?";
7870
- let added = counts.get(path17)?.added ?? 0;
7871
- let deleted = counts.get(path17)?.deleted ?? 0;
8666
+ let added = counts.get(path18)?.added ?? 0;
8667
+ let deleted = counts.get(path18)?.deleted ?? 0;
7872
8668
  if (status === "?") {
7873
8669
  added = 0;
7874
8670
  deleted = 0;
7875
8671
  }
7876
- files.push({ path: path17, status, added, deleted, staged });
8672
+ files.push({ path: path18, status, added, deleted, staged });
7877
8673
  }
7878
8674
  send(ws, { type: "git.changes", payload: { files } });
7879
8675
  } catch (err) {
@@ -7884,21 +8680,21 @@ async function handleGitChanges(ws, projectRoot) {
7884
8680
  }
7885
8681
  }
7886
8682
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
7887
- async function handleGitDiff(ws, projectRoot, path17) {
8683
+ async function handleGitDiff(ws, projectRoot, path18) {
7888
8684
  const cwd = projectRoot || void 0;
7889
- const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
7890
- if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
8685
+ const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path18, ...extra } });
8686
+ if (!path18 || path18.includes("\0") || path18.includes("..") || nodePath.isAbsolute(path18)) {
7891
8687
  reply({ oldText: "", newText: "", error: "invalid path" });
7892
8688
  return;
7893
8689
  }
7894
8690
  try {
7895
8691
  const git = makeGit(cwd);
7896
8692
  const { readFile: readFile9 } = await import("fs/promises");
7897
- const { join: join12 } = await import("path");
7898
- const oldText = await git(["show", `HEAD:${path17}`]);
8693
+ const { join: join14 } = await import("path");
8694
+ const oldText = await git(["show", `HEAD:${path18}`]);
7899
8695
  let newText = "";
7900
8696
  try {
7901
- const abs = cwd ? join12(cwd, path17) : path17;
8697
+ const abs = cwd ? join14(cwd, path18) : path18;
7902
8698
  const buf = await readFile9(abs);
7903
8699
  if (buf.includes(0)) {
7904
8700
  reply({ oldText: "", newText: "", binary: true });
@@ -7979,10 +8775,10 @@ async function handleProcessKillAll(ws) {
7979
8775
  }
7980
8776
 
7981
8777
  // src/server/goal-handlers.ts
7982
- import { resolveWstackPaths } from "@wrongstack/core/utils";
8778
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
7983
8779
  async function handleGoalGet(projectRoot, broadcast2) {
7984
8780
  try {
7985
- const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
8781
+ const goalPath = resolveWstackPaths3({ projectRoot }).projectGoal;
7986
8782
  const { readFile: readFile9 } = await import("fs/promises");
7987
8783
  const raw = await readFile9(goalPath, "utf8");
7988
8784
  const goal = JSON.parse(raw);
@@ -8040,7 +8836,7 @@ async function startWebUI(opts = {}) {
8040
8836
  const write = async () => {
8041
8837
  let raw;
8042
8838
  try {
8043
- raw = await fs13.readFile(globalConfigPath, "utf8");
8839
+ raw = await fs14.readFile(globalConfigPath, "utf8");
8044
8840
  } catch {
8045
8841
  raw = "{}";
8046
8842
  }
@@ -8114,6 +8910,7 @@ async function startWebUI(opts = {}) {
8114
8910
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
8115
8911
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
8116
8912
  applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
8913
+ applyToolResultRenderModes(toolRegistry, config.tools?.resultRenderMode);
8117
8914
  configureExecPolicy(config.tools?.exec ?? {});
8118
8915
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
8119
8916
  const mcpRegistry = new MCPRegistry({
@@ -8158,7 +8955,7 @@ async function startWebUI(opts = {}) {
8158
8955
  sessionId: session.id,
8159
8956
  projectSlug: wpaths.projectSlug,
8160
8957
  projectRoot,
8161
- projectName: path16.basename(projectRoot),
8958
+ projectName: path17.basename(projectRoot),
8162
8959
  workingDir,
8163
8960
  clientType: "webui",
8164
8961
  pid: process.pid,
@@ -8178,7 +8975,7 @@ async function startWebUI(opts = {}) {
8178
8975
  const hqTelemetry = createHqPublisherFromEnv({
8179
8976
  clientKind: "webui",
8180
8977
  projectRoot,
8181
- projectName: path16.basename(projectRoot),
8978
+ projectName: path17.basename(projectRoot),
8182
8979
  appConfig: config,
8183
8980
  socketFactory: (url) => new WebSocket2(url)
8184
8981
  });
@@ -8190,7 +8987,7 @@ async function startWebUI(opts = {}) {
8190
8987
  events,
8191
8988
  sessionId: session.id,
8192
8989
  projectRoot,
8193
- projectName: path16.basename(projectRoot),
8990
+ projectName: path17.basename(projectRoot),
8194
8991
  globalRoot: wpaths.globalRoot,
8195
8992
  initialAgents: statusTracker?.getAgents(),
8196
8993
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -8246,19 +9043,39 @@ async function startWebUI(opts = {}) {
8246
9043
  };
8247
9044
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
8248
9045
  const skillInstaller = config.features.skills ? new SkillInstaller({
8249
- manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
8250
- projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
8251
- globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
9046
+ manifestPath: path17.join(wstackGlobalRoot3(), "installed-skills.json"),
9047
+ projectSkillsDir: path17.join(projectRoot, ".wrongstack", "skills"),
9048
+ globalSkillsDir: path17.join(wstackGlobalRoot3(), "skills"),
8252
9049
  projectHash: projectHash(projectRoot),
8253
9050
  skillLoader
8254
9051
  }) : void 0;
9052
+ const promptsEnabled = config.features.prompts !== false;
9053
+ const bundledPromptsDir = promptsEnabled ? (() => {
9054
+ try {
9055
+ const req = createRequire2(import.meta.url);
9056
+ return path17.join(
9057
+ path17.dirname(req.resolve("@wrongstack/core/package.json")),
9058
+ "data",
9059
+ "prompts"
9060
+ );
9061
+ } catch {
9062
+ return void 0;
9063
+ }
9064
+ })() : void 0;
9065
+ const promptLoader = promptsEnabled ? new DefaultPromptLoader({ paths: wpaths, bundledDir: bundledPromptsDir }) : void 0;
9066
+ const promptUsage = new PromptUsageStore(wpaths.promptUsage);
9067
+ const promptsCtx = { promptLoader, promptUsage };
8255
9068
  const systemPromptBuilder = new DefaultSystemPromptBuilder3({
8256
9069
  memoryStore,
8257
9070
  skillLoader,
8258
9071
  modeStore,
8259
9072
  modeId,
8260
9073
  modePrompt,
8261
- modelCapabilities: () => modelCapabilitiesRef.current
9074
+ modelCapabilities: () => modelCapabilitiesRef.current,
9075
+ instructionPaths: {
9076
+ globalDir: wpaths.globalInstructions,
9077
+ projectDir: wpaths.inProjectInstructions
9078
+ }
8262
9079
  });
8263
9080
  let onlineAgents = [];
8264
9081
  try {
@@ -8353,6 +9170,10 @@ async function startWebUI(opts = {}) {
8353
9170
  context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
8354
9171
  context.meta["nextPrediction"] = config.nextPrediction ?? false;
8355
9172
  context.meta["fallbackModels"] = config.fallbackModels ?? [];
9173
+ context.meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
9174
+ context.meta["favoriteModels"] = config.favoriteModels ?? [];
9175
+ context.meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
9176
+ context.meta["modelMatrix"] = config.modelMatrix ?? {};
8356
9177
  context.meta["fallbackAuto"] = config.fallbackAuto !== false;
8357
9178
  context.meta["featureMcp"] = config.features.mcp !== false;
8358
9179
  context.meta["featurePlugins"] = config.features.plugins !== false;
@@ -8365,6 +9186,20 @@ async function startWebUI(opts = {}) {
8365
9186
  context.meta["logLevel"] = config.log?.level ?? "info";
8366
9187
  context.meta["auditLevel"] = config.session?.auditLevel ?? "standard";
8367
9188
  context.meta["maxIterations"] = config.tools?.maxIterations ?? 500;
9189
+ context.meta["contextMode"] = config.context?.mode ?? "balanced";
9190
+ {
9191
+ const tsm = config.features?.tokenSavingMode;
9192
+ context.meta["tokenSavingTier"] = typeof tsm === "string" ? tsm : tsm ? "medium" : "off";
9193
+ }
9194
+ context.meta["maxConcurrent"] = typeof config.maxConcurrent === "number" ? config.maxConcurrent : 10;
9195
+ context.meta["titleAnimation"] = autonomyCfg["terminalTitleAnimation"] !== false;
9196
+ {
9197
+ const mr = config.modelRuntime ?? {};
9198
+ context.meta["reasoningMode"] = mr.reasoning?.mode ?? "auto";
9199
+ context.meta["reasoningEffort"] = mr.reasoning?.effort ?? "high";
9200
+ context.meta["reasoningPreserve"] = mr.reasoning?.preserve === true;
9201
+ context.meta["cacheTtl"] = mr.cache?.ttl ?? "default";
9202
+ }
8368
9203
  const hqConfig = config.hq;
8369
9204
  context.meta["hqEnabled"] = hqConfig?.enabled === true;
8370
9205
  context.meta["hqUrl"] = hqConfig?.url ?? "";
@@ -8398,6 +9233,10 @@ async function startWebUI(opts = {}) {
8398
9233
  "indexOnStart",
8399
9234
  "contextAutoCompact",
8400
9235
  "contextStrategy",
9236
+ "contextMode",
9237
+ "tokenSavingTier",
9238
+ "maxConcurrent",
9239
+ "titleAnimation",
8401
9240
  "logLevel",
8402
9241
  "auditLevel",
8403
9242
  "hqEnabled",
@@ -8413,6 +9252,10 @@ async function startWebUI(opts = {}) {
8413
9252
  "reasoningPreserve",
8414
9253
  "cacheTtl",
8415
9254
  "fallbackModels",
9255
+ "fallbackProfiles",
9256
+ "favoriteModels",
9257
+ "favoriteModelsOnly",
9258
+ "modelMatrix",
8416
9259
  "fallbackAuto"
8417
9260
  ];
8418
9261
  const prefSnapshot = () => {
@@ -8445,6 +9288,15 @@ async function startWebUI(opts = {}) {
8445
9288
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
8446
9289
  if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
8447
9290
  if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
9291
+ if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
9292
+ decrypted.fallbackProfiles = payload["fallbackProfiles"];
9293
+ }
9294
+ if (Array.isArray(payload["favoriteModels"])) decrypted.favoriteModels = payload["favoriteModels"];
9295
+ if (typeof payload["favoriteModelsOnly"] === "boolean")
9296
+ decrypted.favoriteModelsOnly = payload["favoriteModelsOnly"];
9297
+ if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
9298
+ decrypted.modelMatrix = payload["modelMatrix"];
9299
+ }
8448
9300
  if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
8449
9301
  const FEATURE_MAP = {
8450
9302
  featureMcp: "mcp",
@@ -8460,12 +9312,26 @@ async function startWebUI(opts = {}) {
8460
9312
  decrypted.features = feats;
8461
9313
  }
8462
9314
  }
8463
- if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string") {
9315
+ if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string" || typeof payload["contextMode"] === "string") {
8464
9316
  const ctxCfg = decrypted.context ?? {};
8465
9317
  if (typeof payload["contextAutoCompact"] === "boolean") ctxCfg.autoCompact = payload["contextAutoCompact"];
8466
9318
  if (typeof payload["contextStrategy"] === "string") ctxCfg.strategy = payload["contextStrategy"];
9319
+ if (typeof payload["contextMode"] === "string") ctxCfg.mode = payload["contextMode"];
8467
9320
  decrypted.context = ctxCfg;
8468
9321
  }
9322
+ if (typeof payload["tokenSavingTier"] === "string") {
9323
+ const featsCfg = decrypted.features ?? {};
9324
+ featsCfg.tokenSavingMode = payload["tokenSavingTier"];
9325
+ decrypted.features = featsCfg;
9326
+ }
9327
+ if (typeof payload["maxConcurrent"] === "number") {
9328
+ decrypted.maxConcurrent = payload["maxConcurrent"];
9329
+ }
9330
+ if (typeof payload["titleAnimation"] === "boolean") {
9331
+ const autoCfg = decrypted.autonomy ?? {};
9332
+ autoCfg.terminalTitleAnimation = payload["titleAnimation"];
9333
+ decrypted.autonomy = autoCfg;
9334
+ }
8469
9335
  if (typeof payload["logLevel"] === "string") {
8470
9336
  const logCfg = decrypted.log ?? {};
8471
9337
  logCfg.level = payload["logLevel"];
@@ -8536,6 +9402,7 @@ async function startWebUI(opts = {}) {
8536
9402
  const collabInject = collabInjectMiddleware(collabBus, { logger });
8537
9403
  Object.defineProperty(collabInject, "name", { value: "collab-inject" });
8538
9404
  pipelines.toolCall.prepend(collabInject);
9405
+ installDesignStudioMiddleware({ pipelines, ctx: context });
8539
9406
  const codebaseIndexing = setupWebUICodebaseIndexing({
8540
9407
  config,
8541
9408
  context,
@@ -8631,6 +9498,17 @@ async function startWebUI(opts = {}) {
8631
9498
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
8632
9499
  tracer: void 0
8633
9500
  });
9501
+ const webuiLogger = container.resolve(TOKENS.Logger);
9502
+ void discoverMailboxBridgeForWebui({
9503
+ projectRoot,
9504
+ config,
9505
+ logger: webuiLogger,
9506
+ ctx: context
9507
+ }).catch((err) => {
9508
+ webuiLogger.warn("mailbox bridge discovery threw on webui boot", {
9509
+ err: err instanceof Error ? err.message : String(err)
9510
+ });
9511
+ });
8634
9512
  const agent = new Agent({
8635
9513
  container,
8636
9514
  tools: toolRegistry,
@@ -8727,7 +9605,18 @@ async function startWebUI(opts = {}) {
8727
9605
  projectRoot
8728
9606
  );
8729
9607
  const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
8730
- const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
9608
+ const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards, void 0, {
9609
+ projectRoot,
9610
+ paths: {
9611
+ projectSpecs: wpaths.projectSpecs,
9612
+ projectTaskGraphs: wpaths.projectTaskGraphs,
9613
+ projectSddSession: wpaths.projectSddSession,
9614
+ projectSddBoards: wpaths.projectSddBoards
9615
+ }
9616
+ });
9617
+ void cleanupStaleSddWorktrees3({ projectRoot, boardsDir: wpaths.projectSddBoards }).catch(
9618
+ () => void 0
9619
+ );
8731
9620
  const sddWizardHandler = new SddWizardWebSocketHandler(
8732
9621
  buildSddWizardDeps({
8733
9622
  agent,
@@ -8749,7 +9638,10 @@ async function startWebUI(opts = {}) {
8749
9638
  }
8750
9639
  })
8751
9640
  );
8752
- const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
9641
+ const worktreeHandler = new WorktreeWebSocketHandler(events, logger, {
9642
+ projectRoot,
9643
+ boardsDir: wpaths.projectSddBoards
9644
+ });
8753
9645
  const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
8754
9646
  const collabHandler = new CollaborationWebSocketHandler(
8755
9647
  events,
@@ -8788,7 +9680,7 @@ async function startWebUI(opts = {}) {
8788
9680
  inputCost,
8789
9681
  outputCost,
8790
9682
  cacheReadCost,
8791
- projectName: path16.basename(projectRoot) || projectRoot,
9683
+ projectName: path17.basename(projectRoot) || projectRoot,
8792
9684
  projectRoot,
8793
9685
  cwd: workingDir,
8794
9686
  mode: modeId,
@@ -8937,8 +9829,8 @@ async function startWebUI(opts = {}) {
8937
9829
  clients.delete(ws);
8938
9830
  if (closing) rateLimits.delete(closing.connId);
8939
9831
  if (pendingConfirms.size > 0) {
8940
- for (const [id, resolve9] of pendingConfirms) {
8941
- resolve9("no");
9832
+ for (const [id, resolve10] of pendingConfirms) {
9833
+ resolve10("no");
8942
9834
  pendingConfirms.delete(id);
8943
9835
  }
8944
9836
  }
@@ -9018,21 +9910,21 @@ async function startWebUI(opts = {}) {
9018
9910
  });
9019
9911
  }
9020
9912
  async function touchProjectEntry(root, workDir) {
9021
- const resolved = path16.resolve(root);
9913
+ const resolved = path17.resolve(root);
9022
9914
  const manifest = await loadManifest(globalConfigPath);
9023
9915
  const now = (/* @__PURE__ */ new Date()).toISOString();
9024
- const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
9916
+ const existing = manifest.projects.find((p) => path17.resolve(p.root) === resolved);
9025
9917
  if (existing) {
9026
9918
  existing.lastSeen = now;
9027
- if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
9919
+ if (workDir) existing.lastWorkingDir = path17.resolve(workDir);
9028
9920
  } else {
9029
9921
  manifest.projects.push({
9030
- name: path16.basename(resolved),
9922
+ name: path17.basename(resolved),
9031
9923
  root: resolved,
9032
9924
  slug: generateProjectSlug(resolved),
9033
9925
  createdAt: now,
9034
9926
  lastSeen: now,
9035
- lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
9927
+ lastWorkingDir: workDir ? path17.resolve(workDir) : void 0
9036
9928
  });
9037
9929
  }
9038
9930
  await saveManifest(manifest, globalConfigPath);
@@ -9077,6 +9969,8 @@ async function startWebUI(opts = {}) {
9077
9969
  if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
9078
9970
  if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
9079
9971
  if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
9972
+ if (msg.type.startsWith("worktree.") && await worktreeHandler.handleMessage(msg))
9973
+ return;
9080
9974
  switch (msg.type) {
9081
9975
  // Collaboration messages short-circuit the user/agent flow.
9082
9976
  // They don't touch runLock, the agent loop, or the message queue —
@@ -9147,10 +10041,10 @@ async function startWebUI(opts = {}) {
9147
10041
  }
9148
10042
  case "tool.confirm_result": {
9149
10043
  const { id, decision } = msg.payload;
9150
- const resolve9 = pendingConfirms.get(id);
9151
- if (resolve9) {
10044
+ const resolve10 = pendingConfirms.get(id);
10045
+ if (resolve10) {
9152
10046
  pendingConfirms.delete(id);
9153
- resolve9(decision);
10047
+ resolve10(decision);
9154
10048
  }
9155
10049
  break;
9156
10050
  }
@@ -9234,6 +10128,48 @@ async function startWebUI(opts = {}) {
9234
10128
  case "skills.export":
9235
10129
  await handleSkillsExport(ws, { skillLoader, skillInstaller, projectRoot });
9236
10130
  break;
10131
+ // Prompt library — shared handlers (prompts-handlers.ts).
10132
+ case "prompts.list":
10133
+ await handlePromptsList(ws, promptsCtx);
10134
+ break;
10135
+ case "prompts.search":
10136
+ await handlePromptsSearch(ws, promptsCtx, msg);
10137
+ break;
10138
+ case "prompts.content":
10139
+ await handlePromptsContent(ws, promptsCtx, msg);
10140
+ break;
10141
+ case "prompts.favorite":
10142
+ await handlePromptsFavorite(ws, promptsCtx, msg);
10143
+ break;
10144
+ case "prompts.create":
10145
+ await handlePromptsCreate(ws, promptsCtx, msg);
10146
+ break;
10147
+ case "prompts.used":
10148
+ await handlePromptsUsed(ws, promptsCtx, msg);
10149
+ break;
10150
+ case "prompts.recent":
10151
+ await handlePromptsRecent(ws, promptsCtx);
10152
+ break;
10153
+ // Design Studio — shared handlers (design-handlers.ts). agentMeta is the
10154
+ // live context so design.use pins the active kit for the next turn.
10155
+ case "design.list":
10156
+ await handleDesignList(ws, { projectRoot, agentMeta: context });
10157
+ break;
10158
+ case "design.use":
10159
+ await handleDesignUse(ws, { projectRoot, agentMeta: context }, msg);
10160
+ break;
10161
+ case "design.state":
10162
+ await handleDesignState(ws, { projectRoot, agentMeta: context });
10163
+ break;
10164
+ case "design.set":
10165
+ await handleDesignSet(ws, { projectRoot, agentMeta: context }, msg);
10166
+ break;
10167
+ case "design.materialize":
10168
+ await handleDesignMaterialize(ws, { projectRoot, agentMeta: context }, msg);
10169
+ break;
10170
+ case "design.verify":
10171
+ await handleDesignVerify(ws, { projectRoot, agentMeta: context });
10172
+ break;
9237
10173
  case "diag.get": {
9238
10174
  const usage = tokenCounter.total();
9239
10175
  send(ws, {
@@ -9314,11 +10250,29 @@ async function startWebUI(opts = {}) {
9314
10250
  messages: context.messages.length,
9315
10251
  readFiles: context.readFiles.size,
9316
10252
  tools: toolRegistry.list().length,
10253
+ sideEffectCount: context.sideEffects?.length ?? 0,
9317
10254
  elapsedMs: Date.now() - sessionStartedAt
9318
10255
  }
9319
10256
  });
9320
10257
  break;
9321
10258
  }
10259
+ case "side_effects.list": {
10260
+ const sideEffects = context.sideEffects ?? [];
10261
+ send(ws, {
10262
+ type: "side_effects",
10263
+ payload: {
10264
+ sideEffects: sideEffects.slice(-50).map((se) => ({
10265
+ toolUseId: se.toolUseId,
10266
+ toolName: se.toolName,
10267
+ ts: se.ts,
10268
+ input: se.input,
10269
+ outcome: se.outcome,
10270
+ risk: se.risk
10271
+ }))
10272
+ }
10273
+ });
10274
+ break;
10275
+ }
9322
10276
  case "process.list": {
9323
10277
  await handleProcessList(ws);
9324
10278
  break;
@@ -9573,6 +10527,7 @@ async function startWebUI(opts = {}) {
9573
10527
  toolRegistry,
9574
10528
  config,
9575
10529
  projectRoot,
10530
+ globalRoot: wpaths.globalRoot,
9576
10531
  clients,
9577
10532
  setModeId: (id) => {
9578
10533
  modeId = id;
@@ -9609,6 +10564,16 @@ async function startWebUI(opts = {}) {
9609
10564
  config.features.modelsRegistry = payload["featureModelsRegistry"];
9610
10565
  if (Array.isArray(payload["fallbackModels"]))
9611
10566
  config.fallbackModels = payload["fallbackModels"];
10567
+ if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
10568
+ config.fallbackProfiles = payload["fallbackProfiles"];
10569
+ }
10570
+ if (Array.isArray(payload["favoriteModels"]))
10571
+ config.favoriteModels = payload["favoriteModels"];
10572
+ if (typeof payload["favoriteModelsOnly"] === "boolean")
10573
+ config.favoriteModelsOnly = payload["favoriteModelsOnly"];
10574
+ if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
10575
+ config.modelMatrix = payload["modelMatrix"];
10576
+ }
9612
10577
  if (typeof payload["fallbackAuto"] === "boolean")
9613
10578
  config.fallbackAuto = payload["fallbackAuto"];
9614
10579
  if (typeof payload["contextAutoCompact"] === "boolean") {
@@ -9660,7 +10625,7 @@ async function startWebUI(opts = {}) {
9660
10625
  sendResult2(ws, false, parsed.message);
9661
10626
  return;
9662
10627
  }
9663
- return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
10628
+ return handleMailboxMessages(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
9664
10629
  },
9665
10630
  agents: (ws, msg) => {
9666
10631
  const parsed = validateMailboxAgentsPayload(msg.payload);
@@ -9668,16 +10633,16 @@ async function startWebUI(opts = {}) {
9668
10633
  sendResult2(ws, false, parsed.message);
9669
10634
  return;
9670
10635
  }
9671
- return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
10636
+ return handleMailboxAgents(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
9672
10637
  },
9673
- clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
10638
+ clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }),
9674
10639
  purge: (ws, msg) => {
9675
10640
  const parsed = validateMailboxPurgePayload(msg.payload);
9676
10641
  if (!parsed.ok) {
9677
10642
  sendResult2(ws, false, parsed.message);
9678
10643
  return;
9679
10644
  }
9680
- return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
10645
+ return handleMailboxPurge(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
9681
10646
  }
9682
10647
  };
9683
10648
  mcpRoutes = {
@@ -9757,7 +10722,7 @@ async function startWebUI(opts = {}) {
9757
10722
  };
9758
10723
  const httpServer = createHttpServer({
9759
10724
  host: wsHost,
9760
- distDir: path16.resolve(import.meta.dirname, "../../dist"),
10725
+ distDir: path17.resolve(import.meta.dirname, "../../dist"),
9761
10726
  wsPort,
9762
10727
  publicWsUrl,
9763
10728
  globalRoot: wpaths.globalRoot,
@@ -9768,7 +10733,7 @@ async function startWebUI(opts = {}) {
9768
10733
  void fleetBroadcast?.();
9769
10734
  }
9770
10735
  });
9771
- const registryBaseDir = path16.dirname(globalConfigPath);
10736
+ const registryBaseDir = path17.dirname(globalConfigPath);
9772
10737
  httpServer.listen(httpPort, wsHost, () => {
9773
10738
  const openUrl = buildWebUIAccessUrl({
9774
10739
  host: wsHost,
@@ -9785,7 +10750,7 @@ async function startWebUI(opts = {}) {
9785
10750
  wsPort,
9786
10751
  host: wsHost,
9787
10752
  projectRoot,
9788
- projectName: path16.basename(projectRoot) || projectRoot,
10753
+ projectName: path17.basename(projectRoot) || projectRoot,
9789
10754
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9790
10755
  url: buildWebUIAccessUrl({ host: wsHost, port: httpPort, publicUrl })
9791
10756
  },