@wrongstack/webui 0.272.2 → 0.273.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.
@@ -7,7 +7,10 @@ function isRecord(value) {
7
7
  }
8
8
  function validateModelSwitchPayload(payload) {
9
9
  if (!isRecord(payload)) {
10
- return { ok: false, message: "model.switch payload must be an object with string provider and model" };
10
+ return {
11
+ ok: false,
12
+ message: "model.switch payload must be an object with string provider and model"
13
+ };
11
14
  }
12
15
  const provider = payload["provider"];
13
16
  const model = payload["model"];
@@ -29,13 +32,22 @@ function validateMailboxMessagesPayload(payload) {
29
32
  const agentId = payload["agentId"];
30
33
  const unreadOnly = payload["unreadOnly"];
31
34
  if (limit !== void 0 && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1)) {
32
- return { ok: false, message: "mailbox.messages payload.limit must be a positive number when provided" };
35
+ return {
36
+ ok: false,
37
+ message: "mailbox.messages payload.limit must be a positive number when provided"
38
+ };
33
39
  }
34
40
  if (agentId !== void 0 && typeof agentId !== "string") {
35
- return { ok: false, message: "mailbox.messages payload.agentId must be a string when provided" };
41
+ return {
42
+ ok: false,
43
+ message: "mailbox.messages payload.agentId must be a string when provided"
44
+ };
36
45
  }
37
46
  if (unreadOnly !== void 0 && typeof unreadOnly !== "boolean") {
38
- return { ok: false, message: "mailbox.messages payload.unreadOnly must be a boolean when provided" };
47
+ return {
48
+ ok: false,
49
+ message: "mailbox.messages payload.unreadOnly must be a boolean when provided"
50
+ };
39
51
  }
40
52
  return { ok: true, value: { limit, agentId, unreadOnly } };
41
53
  }
@@ -46,7 +58,10 @@ function validateMailboxAgentsPayload(payload) {
46
58
  }
47
59
  const onlineOnly = payload["onlineOnly"];
48
60
  if (onlineOnly !== void 0 && typeof onlineOnly !== "boolean") {
49
- return { ok: false, message: "mailbox.agents payload.onlineOnly must be a boolean when provided" };
61
+ return {
62
+ ok: false,
63
+ message: "mailbox.agents payload.onlineOnly must be a boolean when provided"
64
+ };
50
65
  }
51
66
  return { ok: true, value: { onlineOnly } };
52
67
  }
@@ -58,10 +73,16 @@ function validateMailboxPurgePayload(payload) {
58
73
  const completedMaxAgeMs = payload["completedMaxAgeMs"];
59
74
  const incompleteMaxAgeMs = payload["incompleteMaxAgeMs"];
60
75
  if (completedMaxAgeMs !== void 0 && (typeof completedMaxAgeMs !== "number" || !Number.isFinite(completedMaxAgeMs) || completedMaxAgeMs < 0)) {
61
- return { ok: false, message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided" };
76
+ return {
77
+ ok: false,
78
+ message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided"
79
+ };
62
80
  }
63
81
  if (incompleteMaxAgeMs !== void 0 && (typeof incompleteMaxAgeMs !== "number" || !Number.isFinite(incompleteMaxAgeMs) || incompleteMaxAgeMs < 0)) {
64
- return { ok: false, message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided" };
82
+ return {
83
+ ok: false,
84
+ message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided"
85
+ };
65
86
  }
66
87
  return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };
67
88
  }
@@ -72,7 +93,10 @@ function validateBrainRiskPayload(payload) {
72
93
  }
73
94
  const level = payload["level"];
74
95
  if (typeof level !== "string" || !BRAIN_RISK_VALUES.has(level)) {
75
- return { ok: false, message: "brain.risk payload.level must be one of off, low, medium, high, all" };
96
+ return {
97
+ ok: false,
98
+ message: "brain.risk payload.level must be one of off, low, medium, high, all"
99
+ };
76
100
  }
77
101
  return { ok: true, value: { level } };
78
102
  }
@@ -98,7 +122,10 @@ function validateAutonomySwitchPayload(payload) {
98
122
  }
99
123
  function validatePlanTemplateUsePayload(payload) {
100
124
  if (!isRecord(payload)) {
101
- return { ok: false, message: "plan.template_use payload must be an object with string template" };
125
+ return {
126
+ ok: false,
127
+ message: "plan.template_use payload must be an object with string template"
128
+ };
102
129
  }
103
130
  const template = payload["template"];
104
131
  if (typeof template !== "string" || template.trim().length === 0) {
@@ -113,7 +140,15 @@ var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
113
140
  var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
114
141
  var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
115
142
  var REASONING_MODE_VALUES = /* @__PURE__ */ new Set(["auto", "on", "off"]);
116
- var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
143
+ var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
144
+ "none",
145
+ "minimal",
146
+ "low",
147
+ "medium",
148
+ "high",
149
+ "xhigh",
150
+ "max"
151
+ ]);
117
152
  var CACHE_TTL_VALUES = /* @__PURE__ */ new Set(["default", "5m", "1h"]);
118
153
  var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
119
154
  "yolo",
@@ -134,8 +169,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
134
169
  "tgDelegate",
135
170
  "reasoningPreserve",
136
171
  "hqEnabled",
137
- "hqRawContent"
172
+ "hqRawContent",
173
+ "fallbackAuto"
138
174
  ]);
175
+ var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
139
176
  var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
140
177
  "autonomyDelayMs",
141
178
  "autoProceedMaxIterations",
@@ -167,6 +204,9 @@ function validatePreferenceValue(key, value) {
167
204
  if (STRING_PREF_KEYS.has(key)) {
168
205
  return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
169
206
  }
207
+ if (STRING_ARRAY_PREF_KEYS.has(key)) {
208
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
209
+ }
170
210
  const allowed = ENUM_PREF_KEYS[key];
171
211
  if (allowed) {
172
212
  return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
@@ -287,16 +327,25 @@ function validateContextModeCreatePayload(payload) {
287
327
  return { ok: false, message: "context.mode.create payload.description must be a string" };
288
328
  }
289
329
  if (!isRecord(thresholds)) {
290
- return { ok: false, message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers" };
330
+ return {
331
+ ok: false,
332
+ message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers"
333
+ };
291
334
  }
292
335
  if (!isFiniteNumber(thresholds["warn"]) || !isFiniteNumber(thresholds["soft"]) || !isFiniteNumber(thresholds["hard"])) {
293
- return { ok: false, message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers" };
336
+ return {
337
+ ok: false,
338
+ message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers"
339
+ };
294
340
  }
295
341
  if (!isFiniteNumber(preserveK)) {
296
342
  return { ok: false, message: "context.mode.create payload.preserveK must be a finite number" };
297
343
  }
298
344
  if (!isFiniteNumber(eliseThreshold)) {
299
- return { ok: false, message: "context.mode.create payload.eliseThreshold must be a finite number" };
345
+ return {
346
+ ok: false,
347
+ message: "context.mode.create payload.eliseThreshold must be a finite number"
348
+ };
300
349
  }
301
350
  return {
302
351
  ok: true,
@@ -320,22 +369,34 @@ function validateContextModeUpdatePayload(payload) {
320
369
  }
321
370
  const name2 = payload["name"];
322
371
  if (name2 !== void 0 && typeof name2 !== "string") {
323
- return { ok: false, message: "context.mode.update payload.name must be a string when provided" };
372
+ return {
373
+ ok: false,
374
+ message: "context.mode.update payload.name must be a string when provided"
375
+ };
324
376
  }
325
377
  const description = payload["description"];
326
378
  if (description !== void 0 && typeof description !== "string") {
327
- return { ok: false, message: "context.mode.update payload.description must be a string when provided" };
379
+ return {
380
+ ok: false,
381
+ message: "context.mode.update payload.description must be a string when provided"
382
+ };
328
383
  }
329
384
  const thresholds = payload["thresholds"];
330
385
  let validatedThresholds;
331
386
  if (thresholds !== void 0) {
332
387
  if (!isRecord(thresholds)) {
333
- return { ok: false, message: "context.mode.update payload.thresholds must be an object when provided" };
388
+ return {
389
+ ok: false,
390
+ message: "context.mode.update payload.thresholds must be an object when provided"
391
+ };
334
392
  }
335
393
  for (const key of ["warn", "soft", "hard"]) {
336
394
  const val = thresholds[key];
337
395
  if (val !== void 0 && !isFiniteNumber(val)) {
338
- return { ok: false, message: `context.mode.update payload.thresholds.${key} must be a finite number when provided` };
396
+ return {
397
+ ok: false,
398
+ message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`
399
+ };
339
400
  }
340
401
  }
341
402
  validatedThresholds = {
@@ -346,11 +407,17 @@ function validateContextModeUpdatePayload(payload) {
346
407
  }
347
408
  const preserveK = payload["preserveK"];
348
409
  if (preserveK !== void 0 && !isFiniteNumber(preserveK)) {
349
- return { ok: false, message: "context.mode.update payload.preserveK must be a finite number when provided" };
410
+ return {
411
+ ok: false,
412
+ message: "context.mode.update payload.preserveK must be a finite number when provided"
413
+ };
350
414
  }
351
415
  const eliseThreshold = payload["eliseThreshold"];
352
416
  if (eliseThreshold !== void 0 && !isFiniteNumber(eliseThreshold)) {
353
- return { ok: false, message: "context.mode.update payload.eliseThreshold must be a finite number when provided" };
417
+ return {
418
+ ok: false,
419
+ message: "context.mode.update payload.eliseThreshold must be a finite number when provided"
420
+ };
354
421
  }
355
422
  return {
356
423
  ok: true,
@@ -368,28 +435,31 @@ function validateShellOpenPayload(payload) {
368
435
  if (!isRecord(payload)) {
369
436
  return { ok: false, message: "shell.open payload must be an object with string path" };
370
437
  }
371
- const path16 = payload["path"];
372
- if (typeof path16 !== "string" || path16.trim().length === 0) {
438
+ const path17 = payload["path"];
439
+ if (typeof path17 !== "string" || path17.trim().length === 0) {
373
440
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
374
441
  }
375
442
  const target = payload["target"];
376
443
  if (target !== void 0 && target !== "file" && target !== "terminal") {
377
- return { ok: false, message: 'shell.open payload.target must be "file" or "terminal" when provided' };
444
+ return {
445
+ ok: false,
446
+ message: 'shell.open payload.target must be "file" or "terminal" when provided'
447
+ };
378
448
  }
379
- return { ok: true, value: { path: path16, target } };
449
+ return { ok: true, value: { path: path17, target } };
380
450
  }
381
451
  function validateGitDiffPayload(payload) {
382
452
  if (!isRecord(payload)) {
383
453
  return { ok: false, message: "git.diff payload must be an object" };
384
454
  }
385
- const path16 = payload["path"];
386
- if (path16 === void 0 || path16 === null) {
455
+ const path17 = payload["path"];
456
+ if (path17 === void 0 || path17 === null) {
387
457
  return { ok: true, value: { path: "" } };
388
458
  }
389
- if (typeof path16 !== "string") {
459
+ if (typeof path17 !== "string") {
390
460
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
391
461
  }
392
- return { ok: true, value: { path: path16 } };
462
+ return { ok: true, value: { path: path17 } };
393
463
  }
394
464
  function validateProjectsAddPayload(payload) {
395
465
  if (!isRecord(payload)) {
@@ -569,7 +639,7 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
569
639
  return;
570
640
  }
571
641
  try {
572
- const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
642
+ const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
573
643
  let changed = false;
574
644
  const plan = await mutatePlan(planPath, sessionId, async (p) => {
575
645
  const before = p.updatedAt;
@@ -649,7 +719,7 @@ import {
649
719
  createTieredBrainArbiter
650
720
  } from "@wrongstack/core";
651
721
  import * as fs13 from "fs/promises";
652
- import * as path15 from "path";
722
+ import * as path16 from "path";
653
723
 
654
724
  // src/server/http-server.ts
655
725
  import * as fs from "fs/promises";
@@ -2812,6 +2882,7 @@ import {
2812
2882
  DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
2813
2883
  DEFAULT_SESSION_PRUNE_DAYS,
2814
2884
  DEFAULT_TOOLS_CONFIG,
2885
+ applyToolDescriptionModes,
2815
2886
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
2816
2887
  enhanceUserPrompt,
2817
2888
  gatedEnhancerReasoning,
@@ -2856,7 +2927,8 @@ function createDefaultContainer(opts) {
2856
2927
  () => new DefaultErrorHandler(
2857
2928
  buildRecoveryStrategies({
2858
2929
  compactor: container.resolve(TOKENS.Compactor),
2859
- modelsRegistry
2930
+ modelsRegistry,
2931
+ getConfig: () => configStore.get()
2860
2932
  })
2861
2933
  )
2862
2934
  );
@@ -2943,6 +3015,7 @@ function patchConfig(config, updates) {
2943
3015
  import { spawnSync } from "child_process";
2944
3016
  import { toErrorMessage } from "@wrongstack/core/utils";
2945
3017
  import {
3018
+ assignNickname,
2946
3019
  AutoPhasePlanner,
2947
3020
  PhaseGraphBuilder,
2948
3021
  PhaseOrchestrator,
@@ -2980,6 +3053,8 @@ var AutoPhaseWebSocketHandler = class {
2980
3053
  abort = null;
2981
3054
  /** Optional per-phase git-worktree isolation (lazily created at start). */
2982
3055
  worktrees = null;
3056
+ /** Per-run worker identities so the board can show "who is on what". */
3057
+ usedNicknames = /* @__PURE__ */ new Set();
2983
3058
  addClient(ws) {
2984
3059
  const client = { ws, id: crypto.randomUUID() };
2985
3060
  this.clients.add(client);
@@ -3022,6 +3097,29 @@ var AutoPhaseWebSocketHandler = class {
3022
3097
  await this.handleTaskStatusChange(taskId, status);
3023
3098
  break;
3024
3099
  }
3100
+ case "autophase.moveTask": {
3101
+ const { taskId, toPhaseId } = msg.payload;
3102
+ if (this.orchestrator?.moveTask(taskId, toPhaseId)) this.afterBoardMutation();
3103
+ break;
3104
+ }
3105
+ case "autophase.assignTask": {
3106
+ const { taskId, agentId, agentName } = msg.payload;
3107
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
3108
+ break;
3109
+ }
3110
+ case "autophase.addTask": {
3111
+ const { phaseId, title, description, type, priority } = msg.payload;
3112
+ if (title?.trim() && this.orchestrator?.addTask(phaseId, { title: title.trim(), description, type, priority })) {
3113
+ this.afterBoardMutation();
3114
+ }
3115
+ break;
3116
+ }
3117
+ case "autophase.retryTask":
3118
+ case "autophase.runTask": {
3119
+ const { taskId } = msg.payload;
3120
+ if (this.orchestrator?.requeueTask(taskId)) this.afterBoardMutation();
3121
+ break;
3122
+ }
3025
3123
  case "autophase.toggleAutonomous": {
3026
3124
  const autonomous = msg.payload?.autonomous ?? !this.graph?.autonomous;
3027
3125
  if (this.graph) {
@@ -3149,6 +3247,13 @@ var AutoPhaseWebSocketHandler = class {
3149
3247
  return this.defaultPhases();
3150
3248
  }
3151
3249
  async executeTaskWithAgent(task, phaseId, env) {
3250
+ if (!task.assignee) {
3251
+ const nick = assignNickname("executor", this.usedNicknames);
3252
+ this.usedNicknames.add(nick.key);
3253
+ task.assignee = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3254
+ task.updatedAt = Date.now();
3255
+ this.broadcastState();
3256
+ }
3152
3257
  const prompt = `Execute task: ${task.title}
3153
3258
 
3154
3259
  Description: ${task.description}
@@ -3164,6 +3269,11 @@ Type: ${task.type}`;
3164
3269
  this.context.cwd = prevCwd;
3165
3270
  }
3166
3271
  }
3272
+ /** Persist + broadcast after an interactive board mutation. */
3273
+ afterBoardMutation() {
3274
+ if (this.graph) void this.store.save(this.graph);
3275
+ this.broadcastState();
3276
+ }
3167
3277
  async handleTaskStatusChange(taskId, status) {
3168
3278
  if (!this.graph) return;
3169
3279
  for (const phase of this.graph.phases.values()) {
@@ -3207,23 +3317,7 @@ Type: ${task.type}`;
3207
3317
  (sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3208
3318
  0
3209
3319
  );
3210
- const phaseItems = phases.map((p) => ({
3211
- id: p.id,
3212
- name: p.name,
3213
- description: p.description,
3214
- status: p.status,
3215
- priority: p.priority,
3216
- estimateHours: p.estimateHours,
3217
- actualDurationMs: p.actualDurationMs,
3218
- startedAt: p.startedAt,
3219
- completedAt: p.completedAt,
3220
- progressPercent: p.taskGraph.nodes.size > 0 ? Math.round(Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length / p.taskGraph.nodes.size * 100) : 0,
3221
- taskCount: p.taskGraph.nodes.size,
3222
- completedTasks: Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3223
- assignedAgents: p.assignedAgents,
3224
- isActive: p.id === currentActiveId
3225
- }));
3226
- const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
3320
+ const mapTask = (t) => ({
3227
3321
  id: t.id,
3228
3322
  title: t.title,
3229
3323
  description: t.description,
@@ -3236,7 +3330,31 @@ Type: ${task.type}`;
3236
3330
  tags: t.tags || [],
3237
3331
  startedAt: t.startedAt,
3238
3332
  completedAt: t.completedAt
3239
- })) : [];
3333
+ });
3334
+ const phaseItems = phases.map((p) => {
3335
+ const nodes = Array.from(p.taskGraph.nodes.values());
3336
+ const done = nodes.filter((t) => t.status === "completed").length;
3337
+ return {
3338
+ id: p.id,
3339
+ name: p.name,
3340
+ description: p.description,
3341
+ status: p.status,
3342
+ priority: p.priority,
3343
+ estimateHours: p.estimateHours,
3344
+ actualDurationMs: p.actualDurationMs,
3345
+ startedAt: p.startedAt,
3346
+ completedAt: p.completedAt,
3347
+ progressPercent: nodes.length > 0 ? Math.round(done / nodes.length * 100) : 0,
3348
+ taskCount: nodes.length,
3349
+ completedTasks: done,
3350
+ assignedAgents: p.assignedAgents,
3351
+ isActive: p.id === currentActiveId,
3352
+ // Every phase carries its full task list so the board can render each
3353
+ // phase as a column (not just the selected one).
3354
+ tasks: nodes.map(mapTask)
3355
+ };
3356
+ });
3357
+ const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
3240
3358
  const completedPhases = phases.filter((p) => p.status === "completed").length;
3241
3359
  return {
3242
3360
  title: this.graph.title,
@@ -3269,6 +3387,513 @@ Type: ${task.type}`;
3269
3387
  }
3270
3388
  };
3271
3389
 
3390
+ // src/server/specs-ws-handler.ts
3391
+ import {
3392
+ computeTaskProgress,
3393
+ SpecStore,
3394
+ TaskGraphStore
3395
+ } from "@wrongstack/core";
3396
+ var SpecsWebSocketHandler = class {
3397
+ specStore;
3398
+ graphStore;
3399
+ clients = /* @__PURE__ */ new Set();
3400
+ constructor(specsDir, taskGraphsDir) {
3401
+ this.specStore = new SpecStore({ baseDir: specsDir });
3402
+ this.graphStore = new TaskGraphStore({ baseDir: taskGraphsDir });
3403
+ }
3404
+ addClient(ws) {
3405
+ const client = { ws, id: crypto.randomUUID() };
3406
+ this.clients.add(client);
3407
+ ws.on("close", () => this.clients.delete(client));
3408
+ ws.on("error", () => this.clients.delete(client));
3409
+ void this.sendList(client);
3410
+ }
3411
+ async handleMessage(msg) {
3412
+ switch (msg.type) {
3413
+ case "specs.list":
3414
+ await this.broadcastList();
3415
+ break;
3416
+ case "specs.get": {
3417
+ const specId = msg.payload?.specId;
3418
+ if (specId) await this.broadcastDetail(specId);
3419
+ break;
3420
+ }
3421
+ case "specs.taskStatus": {
3422
+ const { graphId, taskId, status } = msg.payload;
3423
+ await this.updateTaskStatus(graphId, taskId, status);
3424
+ break;
3425
+ }
3426
+ }
3427
+ }
3428
+ // ── List ──────────────────────────────────────────────────────────────────
3429
+ async buildList() {
3430
+ const [specs, graphs] = await Promise.all([this.specStore.list(), this.graphStore.list()]);
3431
+ return specs.map((s, i) => {
3432
+ const graph = graphs.find((g) => g.specId === s.id);
3433
+ return {
3434
+ id: s.id,
3435
+ // FORGE-style display id (spec-001…). The real UUID stays in `id`.
3436
+ displayId: `spec-${String(i + 1).padStart(3, "0")}`,
3437
+ title: s.title,
3438
+ status: s.status,
3439
+ graphId: graph?.id,
3440
+ total: graph?.nodeCount ?? 0,
3441
+ completed: graph?.completedCount ?? 0
3442
+ };
3443
+ });
3444
+ }
3445
+ async broadcastList() {
3446
+ this.broadcast({ type: "specs.list", payload: { specs: await this.buildList() } });
3447
+ }
3448
+ async sendList(client) {
3449
+ this.send(client, { type: "specs.list", payload: { specs: await this.buildList() } });
3450
+ }
3451
+ // ── Detail (dependency board) ───────────────────────────────────────────────
3452
+ async broadcastDetail(specId) {
3453
+ const spec = await this.specStore.load(specId);
3454
+ const graph = await this.findGraphForSpec(specId);
3455
+ if (!spec || !graph) {
3456
+ this.broadcast({ type: "specs.detail", payload: { specId, columns: [], notFound: true } });
3457
+ return;
3458
+ }
3459
+ this.broadcast({ type: "specs.detail", payload: this.buildDetail(spec, graph) });
3460
+ }
3461
+ async findGraphForSpec(specId) {
3462
+ const entry = (await this.graphStore.list()).find((g) => g.specId === specId);
3463
+ if (!entry) return null;
3464
+ return this.graphStore.load(entry.id);
3465
+ }
3466
+ buildDetail(spec, graph) {
3467
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
3468
+ const shortId = /* @__PURE__ */ new Map();
3469
+ nodes.forEach((n, i) => {
3470
+ shortId.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
3471
+ });
3472
+ const blockers = /* @__PURE__ */ new Map();
3473
+ for (const n of nodes) blockers.set(n.id, []);
3474
+ for (const e of graph.edges) {
3475
+ if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
3476
+ }
3477
+ const statusOf = (id) => graph.nodes.get(id)?.status;
3478
+ const depthCache = /* @__PURE__ */ new Map();
3479
+ const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
3480
+ const cached = depthCache.get(id);
3481
+ if (cached !== void 0) return cached;
3482
+ if (seen.has(id)) return 0;
3483
+ seen.add(id);
3484
+ const deps2 = blockers.get(id) ?? [];
3485
+ const d = deps2.length === 0 ? 0 : 1 + Math.max(...deps2.map((b) => depthOf(b, seen)));
3486
+ depthCache.set(id, d);
3487
+ return d;
3488
+ };
3489
+ const toBoardTask = (n) => {
3490
+ const deps2 = blockers.get(n.id) ?? [];
3491
+ const allDepsDone = deps2.every((b) => statusOf(b) === "completed");
3492
+ const displayStatus = n.status === "pending" && deps2.length > 0 && allDepsDone ? "queued" : n.status;
3493
+ return {
3494
+ id: n.id,
3495
+ shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
3496
+ title: n.title,
3497
+ description: n.description,
3498
+ priority: n.priority,
3499
+ type: n.type,
3500
+ status: n.status,
3501
+ displayStatus,
3502
+ deps: deps2.map((b) => shortId.get(b) ?? b.slice(0, 6))
3503
+ };
3504
+ };
3505
+ const byDepth = /* @__PURE__ */ new Map();
3506
+ for (const n of nodes) {
3507
+ const d = depthOf(n.id);
3508
+ if (!byDepth.has(d)) byDepth.set(d, []);
3509
+ byDepth.get(d)?.push(toBoardTask(n));
3510
+ }
3511
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, tasks: byDepth.get(d) ?? [] }));
3512
+ const progress = computeTaskProgress(graph);
3513
+ return {
3514
+ specId: spec.id,
3515
+ graphId: graph.id,
3516
+ title: spec.title,
3517
+ overview: spec.overview,
3518
+ status: spec.status,
3519
+ total: progress.total,
3520
+ completed: progress.completed,
3521
+ running: progress.inProgress,
3522
+ pending: progress.pending,
3523
+ columns
3524
+ };
3525
+ }
3526
+ async updateTaskStatus(graphId, taskId, status) {
3527
+ const graph = await this.graphStore.load(graphId);
3528
+ const node = graph?.nodes.get(taskId);
3529
+ if (!graph || !node) return;
3530
+ node.status = status;
3531
+ node.updatedAt = Date.now();
3532
+ graph.updatedAt = Date.now();
3533
+ await this.graphStore.save(graph);
3534
+ this.broadcastDetail(graph.specId).catch(() => {
3535
+ });
3536
+ await this.broadcastList();
3537
+ }
3538
+ // ── Transport ───────────────────────────────────────────────────────────────
3539
+ broadcast(msg) {
3540
+ const data = JSON.stringify(msg);
3541
+ for (const client of this.clients) {
3542
+ if (client.ws.readyState === 1) client.ws.send(data);
3543
+ }
3544
+ }
3545
+ send(client, msg) {
3546
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3547
+ }
3548
+ };
3549
+
3550
+ // src/server/sdd-board-ws-handler.ts
3551
+ import { SddBoardStore } from "@wrongstack/core";
3552
+ var CONTROL_TYPES = /* @__PURE__ */ new Set([
3553
+ "pause",
3554
+ "resume",
3555
+ "stop",
3556
+ "retry",
3557
+ "retry_all_failed",
3558
+ "reassign",
3559
+ // Per-task model / fallback / verification assignment + stop/delete (drained by start-sdd-run).
3560
+ "set_task_model",
3561
+ "set_task_fallbacks",
3562
+ "set_task_verification",
3563
+ "cancel_task",
3564
+ "delete_task",
3565
+ "split_task",
3566
+ // Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
3567
+ "cleanup_worktrees",
3568
+ "rollback"
3569
+ ]);
3570
+ var SddBoardWebSocketHandler = class {
3571
+ store;
3572
+ clients = /* @__PURE__ */ new Set();
3573
+ latest = null;
3574
+ poll = null;
3575
+ unsub = null;
3576
+ constructor(boardsDir, events) {
3577
+ this.store = new SddBoardStore({ baseDir: boardsDir });
3578
+ if (events) {
3579
+ const handler = (e) => {
3580
+ this.latest = e.snapshot;
3581
+ this.broadcast({ type: "sdd.board.snapshot", payload: e.snapshot });
3582
+ };
3583
+ this.unsub = events.on("sdd.board.snapshot", handler);
3584
+ } else {
3585
+ this.poll = setInterval(() => void this.pollLatest(), 1e3);
3586
+ }
3587
+ }
3588
+ addClient(ws) {
3589
+ const client = { ws, id: crypto.randomUUID() };
3590
+ this.clients.add(client);
3591
+ ws.on("close", () => this.clients.delete(client));
3592
+ ws.on("error", () => this.clients.delete(client));
3593
+ void this.sendCurrent(client);
3594
+ }
3595
+ async handleMessage(msg) {
3596
+ if (msg.type === "sdd.board.get") {
3597
+ await this.broadcastCurrent();
3598
+ return;
3599
+ }
3600
+ if (msg.type === "sdd.board.list") {
3601
+ const boards = await this.store.list();
3602
+ this.broadcast({ type: "sdd.board.list", payload: { boards } });
3603
+ return;
3604
+ }
3605
+ const action = msg.type.replace(/^sdd\.board\./, "");
3606
+ if (CONTROL_TYPES.has(action)) {
3607
+ const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
3608
+ if (runId) {
3609
+ await this.store.appendControl(runId, {
3610
+ ts: Date.now(),
3611
+ type: action,
3612
+ payload: msg.payload
3613
+ });
3614
+ }
3615
+ }
3616
+ }
3617
+ dispose() {
3618
+ if (this.poll) clearInterval(this.poll);
3619
+ this.unsub?.();
3620
+ this.poll = null;
3621
+ this.unsub = null;
3622
+ }
3623
+ // ── internal ────────────────────────────────────────────────────────────
3624
+ async pollLatest() {
3625
+ const entry = (await this.store.list())[0];
3626
+ if (!entry) return;
3627
+ if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
3628
+ return;
3629
+ }
3630
+ const snap = await this.store.load(entry.runId);
3631
+ if (snap) {
3632
+ this.latest = snap;
3633
+ this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3634
+ }
3635
+ }
3636
+ async sendCurrent(client) {
3637
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3638
+ if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
3639
+ }
3640
+ async broadcastCurrent() {
3641
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3642
+ if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3643
+ }
3644
+ async loadLatestFromDisk() {
3645
+ const entry = (await this.store.list())[0];
3646
+ return entry ? this.store.load(entry.runId) : null;
3647
+ }
3648
+ broadcast(msg) {
3649
+ const data = JSON.stringify(msg);
3650
+ for (const client of this.clients) {
3651
+ if (client.ws.readyState === 1) client.ws.send(data);
3652
+ }
3653
+ }
3654
+ send(client, msg) {
3655
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3656
+ }
3657
+ };
3658
+
3659
+ // src/server/sdd-wizard-ws-handler.ts
3660
+ var SddWizardWebSocketHandler = class {
3661
+ constructor(deps2) {
3662
+ this.deps = deps2;
3663
+ }
3664
+ deps;
3665
+ clients = /* @__PURE__ */ new Set();
3666
+ driver = null;
3667
+ /** The agent's most recent question — paired with the next user answer. */
3668
+ lastAgentText = "";
3669
+ /** Guards against overlapping interview turns (one in flight at a time). */
3670
+ busy = false;
3671
+ addClient(ws) {
3672
+ const client = { ws, id: crypto.randomUUID() };
3673
+ this.clients.add(client);
3674
+ ws.on("close", () => this.clients.delete(client));
3675
+ ws.on("error", () => this.clients.delete(client));
3676
+ if (this.driver) this.send(client, this.snapshotMsg());
3677
+ }
3678
+ async handleMessage(msg) {
3679
+ try {
3680
+ switch (msg.type) {
3681
+ case "sdd.spec.start":
3682
+ await this.onStart(String(msg.payload?.goal ?? "").trim());
3683
+ break;
3684
+ case "sdd.spec.message":
3685
+ await this.onMessage(String(msg.payload?.text ?? ""));
3686
+ break;
3687
+ case "sdd.spec.approve":
3688
+ await this.onApprove();
3689
+ break;
3690
+ case "sdd.spec.get":
3691
+ if (this.driver) this.broadcast(this.snapshotMsg());
3692
+ break;
3693
+ case "sdd.run.start":
3694
+ await this.onRunStart({
3695
+ parallelSlots: msg.payload?.parallelSlots,
3696
+ defaultModel: msg.payload?.model,
3697
+ defaultProvider: msg.payload?.provider,
3698
+ fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
3699
+ });
3700
+ break;
3701
+ }
3702
+ } catch (err) {
3703
+ this.busy = false;
3704
+ this.broadcast({
3705
+ type: "sdd.spec.error",
3706
+ payload: { message: err instanceof Error ? err.message : String(err) }
3707
+ });
3708
+ }
3709
+ }
3710
+ // ── message handlers ──────────────────────────────────────────────────────
3711
+ async onStart(goal) {
3712
+ if (!goal) {
3713
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "A goal is required." } });
3714
+ return;
3715
+ }
3716
+ if (this.busy) return;
3717
+ this.driver = this.deps.makeDriver();
3718
+ const prompt = this.driver.start(goal);
3719
+ await this.runTurn(prompt);
3720
+ }
3721
+ async onMessage(text) {
3722
+ if (!this.driver || this.busy) return;
3723
+ if (this.driver.phase() === "questioning" && this.lastAgentText) {
3724
+ this.driver.submitAnswer(this.lastAgentText, text);
3725
+ } else {
3726
+ this.driver.submitAnswer(this.lastAgentText || "(feedback)", text);
3727
+ }
3728
+ await this.runTurn(this.driver.currentPrompt());
3729
+ }
3730
+ async onApprove() {
3731
+ if (!this.driver || this.busy) return;
3732
+ const { phase, prompt } = await this.driver.approve();
3733
+ if (phase === "executing") {
3734
+ this.broadcast(this.snapshotMsg());
3735
+ return;
3736
+ }
3737
+ await this.runTurn(prompt);
3738
+ }
3739
+ async onRunStart(opts) {
3740
+ if (!this.driver) {
3741
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "No active spec session." } });
3742
+ return;
3743
+ }
3744
+ const graph = await this.driver.ensureTaskGraph();
3745
+ if (!graph) {
3746
+ this.broadcast({
3747
+ type: "sdd.spec.error",
3748
+ payload: { message: "No spec yet \u2014 finish the interview before starting a run." }
3749
+ });
3750
+ return;
3751
+ }
3752
+ const { runId } = await this.deps.startRun(this.driver, opts);
3753
+ this.broadcast({ type: "sdd.run.started", payload: { runId } });
3754
+ }
3755
+ // ── internals ───────────────────────────────────────────────────────────
3756
+ /** Run one interview turn against the isolated agent, then ingest + broadcast. */
3757
+ async runTurn(prompt) {
3758
+ this.busy = true;
3759
+ this.broadcast(this.snapshotMsg());
3760
+ try {
3761
+ const text = await this.deps.runInterviewTurn(prompt);
3762
+ this.lastAgentText = text;
3763
+ if (this.driver) await this.driver.ingestAgentOutput(text);
3764
+ this.broadcast({ type: "sdd.spec.agent_text", payload: { text } });
3765
+ } finally {
3766
+ this.busy = false;
3767
+ this.broadcast(this.snapshotMsg());
3768
+ }
3769
+ }
3770
+ snapshotMsg() {
3771
+ const snap = this.driver?.snapshot();
3772
+ return {
3773
+ type: "sdd.spec.snapshot",
3774
+ payload: { ...snap, busy: this.busy }
3775
+ };
3776
+ }
3777
+ broadcast(msg) {
3778
+ const data = JSON.stringify(msg);
3779
+ for (const client of this.clients) {
3780
+ if (client.ws.readyState === 1) client.ws.send(data);
3781
+ }
3782
+ }
3783
+ send(client, msg) {
3784
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3785
+ }
3786
+ };
3787
+
3788
+ // src/server/sdd-wizard-wiring.ts
3789
+ import * as path6 from "path";
3790
+ import { spawnSync as spawnSync2 } from "child_process";
3791
+ import {
3792
+ makeCommandVerifier,
3793
+ makeLlmSubtaskGenerator,
3794
+ SddBoardStore as SddBoardStore2,
3795
+ SddInterviewDriver,
3796
+ SddRunRegistry,
3797
+ SddSupervisor,
3798
+ SpecStore as SpecStore2,
3799
+ startSddRun,
3800
+ TaskGraphStore as TaskGraphStore2,
3801
+ WorktreeManager as WorktreeManager2
3802
+ } from "@wrongstack/core";
3803
+ function buildSddWizardDeps(opts) {
3804
+ const registry = new SddRunRegistry();
3805
+ let isolatedSeq = 0;
3806
+ const runIsolatedTurn = async (prompt, name2) => {
3807
+ const result = await opts.subagentFactory({
3808
+ id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
3809
+ role: "executor",
3810
+ name: name2,
3811
+ disabledTools: ["delegate"],
3812
+ allowedCapabilities: ["fs.read", "net.outbound"]
3813
+ });
3814
+ try {
3815
+ const res = await result.agent.run([{ type: "text", text: prompt }]);
3816
+ return res.finalText ?? "";
3817
+ } finally {
3818
+ await result.dispose?.();
3819
+ }
3820
+ };
3821
+ return {
3822
+ makeDriver: () => new SddInterviewDriver({
3823
+ specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
3824
+ graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
3825
+ sessionPath: path6.join(opts.paths.projectDir, "sdd-wizard-session.json")
3826
+ }),
3827
+ runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
3828
+ startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
3829
+ const graph = driver.getGraph();
3830
+ const tracker = driver.getTracker();
3831
+ if (!graph || !tracker) {
3832
+ throw new Error("No task graph to run \u2014 finish the interview first.");
3833
+ }
3834
+ let worktrees;
3835
+ if (process.env["WRONGSTACK_SDD_WORKTREES"] !== "0") {
3836
+ const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
3837
+ cwd: opts.projectRoot,
3838
+ encoding: "utf8",
3839
+ windowsHide: true
3840
+ }).stdout?.trim() === "true";
3841
+ if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
3842
+ }
3843
+ const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
3844
+ const verifyTask = makeCommandVerifier();
3845
+ const superviseFailure = opts.brain ? new SddSupervisor({
3846
+ brain: opts.brain,
3847
+ // The run-level fallback chain (chosen in the wizard) doubles as the
3848
+ // supervisor's reassign options — a `reassign` verdict rotates the
3849
+ // worker model on retry. Empty/undefined → reassign option dropped.
3850
+ reassignModels: fallbackModels,
3851
+ // LLM auto-split: decompose a retry-exhausted task into smaller
3852
+ // sub-tasks on an isolated read-only turn. Heavily validated +
3853
+ // bounded; an empty result degrades the split into a retry.
3854
+ generateSubtasks: makeLlmSubtaskGenerator({
3855
+ run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
3856
+ }),
3857
+ // The standalone brain is a tiered policy→LLM arbiter with NO
3858
+ // human-escalation wrapper (see index.ts), so it never blocks on a
3859
+ // human prompt — an unresolved verdict degrades to a bounded retry.
3860
+ // Safe to let the LLM layer actually pick reassign/split.
3861
+ requestLlmVerdict: true
3862
+ }).superviseFailure : void 0;
3863
+ const handle = startSddRun({
3864
+ tracker,
3865
+ graph,
3866
+ agent: opts.agent,
3867
+ projectRoot: opts.projectRoot,
3868
+ events: opts.events,
3869
+ subagentFactory: opts.subagentFactory,
3870
+ worktrees,
3871
+ boardStore,
3872
+ registry,
3873
+ parallelSlots,
3874
+ defaultModel,
3875
+ defaultProvider,
3876
+ fallbackModels,
3877
+ verifyTask,
3878
+ superviseFailure
3879
+ });
3880
+ void handle.completion.catch(() => {
3881
+ });
3882
+ return { runId: handle.runId };
3883
+ }
3884
+ };
3885
+ }
3886
+
3887
+ // src/server/sdd-wizard-routes.ts
3888
+ async function handleSddWizardRoute(_ws, msg, handlers) {
3889
+ if (!(msg.type.startsWith("sdd.spec.") || msg.type.startsWith("sdd.run."))) return false;
3890
+ await handlers.handleMessage(msg);
3891
+ return true;
3892
+ }
3893
+
3894
+ // src/server/index.ts
3895
+ import { makeLightSubagentFactory } from "@wrongstack/runtime";
3896
+
3272
3897
  // src/server/collaboration-ws-handler.ts
3273
3898
  import { randomUUID } from "crypto";
3274
3899
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -3996,11 +4621,11 @@ var CollaborationWebSocketHandler = class {
3996
4621
 
3997
4622
  // src/server/projects-manifest.ts
3998
4623
  import * as fs5 from "fs/promises";
3999
- import * as path6 from "path";
4624
+ import * as path7 from "path";
4000
4625
  import { projectSlug } from "@wrongstack/core";
4001
4626
  function projectsJsonPath(globalConfigPath) {
4002
- const base = path6.dirname(globalConfigPath);
4003
- return path6.join(base, "projects.json");
4627
+ const base = path7.dirname(globalConfigPath);
4628
+ return path7.join(base, "projects.json");
4004
4629
  }
4005
4630
  async function loadManifest(globalConfigPath) {
4006
4631
  try {
@@ -4013,15 +4638,15 @@ async function loadManifest(globalConfigPath) {
4013
4638
  }
4014
4639
  async function saveManifest(manifest, globalConfigPath) {
4015
4640
  const file = projectsJsonPath(globalConfigPath);
4016
- await fs5.mkdir(path6.dirname(file), { recursive: true });
4641
+ await fs5.mkdir(path7.dirname(file), { recursive: true });
4017
4642
  await fs5.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4018
4643
  }
4019
4644
  function generateProjectSlug(rootPath) {
4020
4645
  return projectSlug(rootPath);
4021
4646
  }
4022
4647
  async function ensureProjectDataDir(slug, globalConfigPath) {
4023
- const base = path6.dirname(globalConfigPath);
4024
- const dir = path6.join(base, "projects", slug);
4648
+ const base = path7.dirname(globalConfigPath);
4649
+ const dir = path7.join(base, "projects", slug);
4025
4650
  await fs5.mkdir(dir, { recursive: true });
4026
4651
  return dir;
4027
4652
  }
@@ -4448,14 +5073,14 @@ function registerShutdownHandlers(res) {
4448
5073
 
4449
5074
  // src/server/instance-registry.ts
4450
5075
  import * as os from "os";
4451
- import * as path7 from "path";
5076
+ import * as path8 from "path";
4452
5077
  import * as fs6 from "fs/promises";
4453
5078
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
4454
5079
  function defaultBaseDir() {
4455
- return path7.join(os.homedir(), ".wrongstack");
5080
+ return path8.join(os.homedir(), ".wrongstack");
4456
5081
  }
4457
5082
  function registryPath(baseDir = defaultBaseDir()) {
4458
- return path7.join(baseDir, "webui-instances.json");
5083
+ return path8.join(baseDir, "webui-instances.json");
4459
5084
  }
4460
5085
  function isPidAlive(pid) {
4461
5086
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -4577,9 +5202,10 @@ function openBrowser(url, platform = process.platform) {
4577
5202
  if (child.pid) {
4578
5203
  try {
4579
5204
  import("@wrongstack/tools").then(({ getProcessRegistry }) => {
5205
+ const pid = child.pid;
5206
+ if (pid === void 0) return;
4580
5207
  getProcessRegistry().register({
4581
- // biome-ignore lint/style/noNonNullAssertion: pid always present after spawn
4582
- pid: child.pid,
5208
+ pid,
4583
5209
  name: "browser",
4584
5210
  command: `${command} ${args.join(" ")}`,
4585
5211
  startedAt: Date.now(),
@@ -4587,7 +5213,7 @@ function openBrowser(url, platform = process.platform) {
4587
5213
  protected: true
4588
5214
  });
4589
5215
  child.on("exit", () => {
4590
- getProcessRegistry().unregister(child.pid);
5216
+ getProcessRegistry().unregister(pid);
4591
5217
  });
4592
5218
  }).catch(() => {
4593
5219
  });
@@ -4617,7 +5243,7 @@ import { probeLocalLlm } from "@wrongstack/runtime/probe";
4617
5243
 
4618
5244
  // src/server/provider-config-io.ts
4619
5245
  import * as fs7 from "fs/promises";
4620
- import * as path8 from "path";
5246
+ import * as path9 from "path";
4621
5247
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
4622
5248
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
4623
5249
  import { DefaultSecretVault } from "@wrongstack/core";
@@ -4669,7 +5295,7 @@ async function saveProviders(configPath, vault, providers) {
4669
5295
  await atomicWrite4(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
4670
5296
  }
4671
5297
  function createProviderConfigIO(configPath) {
4672
- const keyFile = path8.join(path8.dirname(configPath), ".key");
5298
+ const keyFile = path9.join(path9.dirname(configPath), ".key");
4673
5299
  const vault = new DefaultSecretVault({ keyFile });
4674
5300
  return {
4675
5301
  load: () => loadSavedProviders(configPath, vault),
@@ -4823,7 +5449,10 @@ function createProviderHandlers(deps2) {
4823
5449
  try {
4824
5450
  const providers = await loadConfigProviders();
4825
5451
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
4826
- if (result.ok) await saveConfigProviders(providers);
5452
+ if (result.ok) {
5453
+ await saveConfigProviders(providers);
5454
+ broadcastSaved(providers);
5455
+ }
4827
5456
  sendResult2(ws, result.ok, result.message);
4828
5457
  } catch (err) {
4829
5458
  sendResult2(ws, false, errMessage(err));
@@ -4833,7 +5462,10 @@ function createProviderHandlers(deps2) {
4833
5462
  try {
4834
5463
  const providers = await loadConfigProviders();
4835
5464
  const result = deleteKey(providers, providerId, label);
4836
- if (result.ok) await saveConfigProviders(providers);
5465
+ if (result.ok) {
5466
+ await saveConfigProviders(providers);
5467
+ broadcastSaved(providers);
5468
+ }
4837
5469
  sendResult2(ws, result.ok, result.message);
4838
5470
  } catch (err) {
4839
5471
  sendResult2(ws, false, errMessage(err));
@@ -4843,7 +5475,10 @@ function createProviderHandlers(deps2) {
4843
5475
  try {
4844
5476
  const providers = await loadConfigProviders();
4845
5477
  const result = setActiveKey(providers, providerId, label);
4846
- if (result.ok) await saveConfigProviders(providers);
5478
+ if (result.ok) {
5479
+ await saveConfigProviders(providers);
5480
+ broadcastSaved(providers);
5481
+ }
4847
5482
  sendResult2(ws, result.ok, result.message);
4848
5483
  } catch (err) {
4849
5484
  sendResult2(ws, false, errMessage(err));
@@ -4853,11 +5488,13 @@ function createProviderHandlers(deps2) {
4853
5488
  try {
4854
5489
  const providers = await loadConfigProviders();
4855
5490
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
4856
- if (result.ok) await saveConfigProviders(providers);
5491
+ if (result.ok) {
5492
+ await saveConfigProviders(providers);
5493
+ broadcastSaved(providers);
5494
+ }
4857
5495
  sendResult2(ws, result.ok, result.message);
4858
5496
  if (result.ok) {
4859
5497
  console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
4860
- broadcastSaved(providers);
4861
5498
  }
4862
5499
  } catch (err) {
4863
5500
  sendResult2(ws, false, errMessage(err));
@@ -4867,7 +5504,10 @@ function createProviderHandlers(deps2) {
4867
5504
  try {
4868
5505
  const providers = await loadConfigProviders();
4869
5506
  const result = removeProvider(providers, providerId);
4870
- if (result.ok) await saveConfigProviders(providers);
5507
+ if (result.ok) {
5508
+ await saveConfigProviders(providers);
5509
+ broadcastSaved(providers);
5510
+ }
4871
5511
  sendResult2(ws, result.ok, result.message);
4872
5512
  } catch (err) {
4873
5513
  sendResult2(ws, false, errMessage(err));
@@ -5050,7 +5690,7 @@ function createModeHandlers(ctx) {
5050
5690
 
5051
5691
  // src/server/project-handlers.ts
5052
5692
  import * as fs9 from "fs/promises";
5053
- import * as path10 from "path";
5693
+ import * as path11 from "path";
5054
5694
  import {
5055
5695
  DefaultSessionStore as DefaultSessionStore2,
5056
5696
  DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
@@ -5059,13 +5699,13 @@ import {
5059
5699
 
5060
5700
  // src/server/path-containment.ts
5061
5701
  import * as fs8 from "fs/promises";
5062
- import * as path9 from "path";
5702
+ import * as path10 from "path";
5063
5703
  function isPathInside(root, target) {
5064
- const relative3 = path9.relative(root, target);
5065
- return relative3 === "" || !relative3.startsWith("..") && !path9.isAbsolute(relative3);
5704
+ const relative3 = path10.relative(root, target);
5705
+ return relative3 === "" || !relative3.startsWith("..") && !path10.isAbsolute(relative3);
5066
5706
  }
5067
5707
  async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
5068
- const resolved = path9.resolve(projectRoot, inputPath);
5708
+ const resolved = path10.resolve(projectRoot, inputPath);
5069
5709
  let stat3;
5070
5710
  try {
5071
5711
  stat3 = await fs8.stat(resolved);
@@ -5107,7 +5747,7 @@ function createProjectHandlers(ctx) {
5107
5747
  }
5108
5748
  const { root: addRoot, name: displayName } = parsed.value;
5109
5749
  try {
5110
- const resolved = path10.resolve(addRoot);
5750
+ const resolved = path11.resolve(addRoot);
5111
5751
  await fs9.access(resolved);
5112
5752
  const stat3 = await fs9.stat(resolved);
5113
5753
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
@@ -5125,7 +5765,7 @@ function createProjectHandlers(ctx) {
5125
5765
  });
5126
5766
  return;
5127
5767
  }
5128
- const name2 = displayName?.trim() || path10.basename(resolved);
5768
+ const name2 = displayName?.trim() || path11.basename(resolved);
5129
5769
  const slug = generateProjectSlug(resolved);
5130
5770
  await ensureProjectDataDir(slug, ctx.globalConfigPath);
5131
5771
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5138,7 +5778,7 @@ function createProjectHandlers(ctx) {
5138
5778
  } catch (err) {
5139
5779
  send(ws, {
5140
5780
  type: "projects.added",
5141
- payload: { name: path10.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5781
+ payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5142
5782
  });
5143
5783
  }
5144
5784
  },
@@ -5153,7 +5793,7 @@ function createProjectHandlers(ctx) {
5153
5793
  }
5154
5794
  const { root: selRoot, name: selName } = parsed.value;
5155
5795
  try {
5156
- const resolved = path10.resolve(selRoot);
5796
+ const resolved = path11.resolve(selRoot);
5157
5797
  try {
5158
5798
  await fs9.access(resolved);
5159
5799
  const stat3 = await fs9.stat(resolved);
@@ -5163,7 +5803,7 @@ function createProjectHandlers(ctx) {
5163
5803
  type: "projects.selected",
5164
5804
  payload: {
5165
5805
  root: selRoot,
5166
- name: selName || path10.basename(selRoot),
5806
+ name: selName || path11.basename(selRoot),
5167
5807
  message: `Cannot switch: ${errMessage(err)}`
5168
5808
  }
5169
5809
  });
@@ -5175,7 +5815,7 @@ function createProjectHandlers(ctx) {
5175
5815
  entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
5176
5816
  entry.lastWorkingDir = resolved;
5177
5817
  } else {
5178
- const name2 = selName?.trim() || path10.basename(resolved);
5818
+ const name2 = selName?.trim() || path11.basename(resolved);
5179
5819
  const slug = generateProjectSlug(resolved);
5180
5820
  manifest.projects.push({
5181
5821
  name: name2,
@@ -5214,8 +5854,8 @@ function createProjectHandlers(ctx) {
5214
5854
  });
5215
5855
  } catch {
5216
5856
  }
5217
- const newSessionsDir = path10.join(
5218
- path10.dirname(ctx.globalConfigPath),
5857
+ const newSessionsDir = path11.join(
5858
+ path11.dirname(ctx.globalConfigPath),
5219
5859
  "projects",
5220
5860
  switchSlug,
5221
5861
  "sessions"
@@ -5254,7 +5894,7 @@ function createProjectHandlers(ctx) {
5254
5894
  sessionId: newSession.id,
5255
5895
  projectSlug: switchSlug,
5256
5896
  projectRoot: resolved,
5257
- projectName: path10.basename(resolved),
5897
+ projectName: path11.basename(resolved),
5258
5898
  workingDir: resolved,
5259
5899
  clientType: "webui",
5260
5900
  pid: process.pid,
@@ -5266,8 +5906,8 @@ function createProjectHandlers(ctx) {
5266
5906
  type: "projects.selected",
5267
5907
  payload: {
5268
5908
  root: resolved,
5269
- name: selName || path10.basename(resolved),
5270
- message: `Switched to ${selName || path10.basename(resolved)}`
5909
+ name: selName || path11.basename(resolved),
5910
+ message: `Switched to ${selName || path11.basename(resolved)}`
5271
5911
  }
5272
5912
  });
5273
5913
  broadcast(ctx.clients, {
@@ -5287,7 +5927,7 @@ function createProjectHandlers(ctx) {
5287
5927
  type: "projects.selected",
5288
5928
  payload: {
5289
5929
  root: selRoot,
5290
- name: selName || path10.basename(selRoot),
5930
+ name: selName || path11.basename(selRoot),
5291
5931
  message: errMessage(err)
5292
5932
  }
5293
5933
  });
@@ -5318,7 +5958,7 @@ function createProjectHandlers(ctx) {
5318
5958
  }
5319
5959
 
5320
5960
  // src/server/session-handlers.ts
5321
- import * as path11 from "path";
5961
+ import * as path12 from "path";
5322
5962
  import {
5323
5963
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
5324
5964
  repairToolUseAdjacency,
@@ -5660,7 +6300,7 @@ function createSessionHandlers(ctx) {
5660
6300
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5661
6301
  const projectRoot = ctx.getProjectRoot();
5662
6302
  const rewinder = new DefaultSessionRewinder(
5663
- path11.join(projectRoot, ".wrongstack", "sessions"),
6303
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5664
6304
  projectRoot
5665
6305
  );
5666
6306
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
@@ -5675,7 +6315,7 @@ function createSessionHandlers(ctx) {
5675
6315
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5676
6316
  const projectRoot = ctx.getProjectRoot();
5677
6317
  const rewinder = new DefaultSessionRewinder(
5678
- path11.join(projectRoot, ".wrongstack", "sessions"),
6318
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5679
6319
  projectRoot
5680
6320
  );
5681
6321
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
@@ -5982,10 +6622,24 @@ async function handleAutoPhaseRoute(_ws, msg, handlers) {
5982
6622
  return true;
5983
6623
  }
5984
6624
 
6625
+ // src/server/specs-routes.ts
6626
+ async function handleSpecsRoute(_ws, msg, handlers) {
6627
+ if (!msg.type.startsWith("specs.")) return false;
6628
+ await handlers.handleMessage(msg);
6629
+ return true;
6630
+ }
6631
+
6632
+ // src/server/sdd-board-routes.ts
6633
+ async function handleSddBoardRoute(_ws, msg, handlers) {
6634
+ if (!msg.type.startsWith("sdd.board.")) return false;
6635
+ await handlers.handleMessage(msg);
6636
+ return true;
6637
+ }
6638
+
5985
6639
  // src/server/setup-events.ts
5986
6640
  import * as fs10 from "fs/promises";
5987
6641
  import { watch as fsWatch } from "fs";
5988
- import * as path12 from "path";
6642
+ import * as path13 from "path";
5989
6643
  function setupEvents(deps2) {
5990
6644
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
5991
6645
  const disposers = [];
@@ -6450,7 +7104,7 @@ function setupEvents(deps2) {
6450
7104
  if (wpaths?.projectStatus) {
6451
7105
  try {
6452
7106
  const statusFile = wpaths.projectStatus(e.projectHash);
6453
- const dir = path12.dirname(statusFile);
7107
+ const dir = path13.dirname(statusFile);
6454
7108
  await fs10.mkdir(dir, { recursive: true });
6455
7109
  await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
6456
7110
  } catch (err) {
@@ -6459,7 +7113,7 @@ function setupEvents(deps2) {
6459
7113
  }
6460
7114
  });
6461
7115
  if (wpaths?.projectStatus && wpaths.configDir) {
6462
- const projectsDir = path12.join(wpaths.configDir, "projects");
7116
+ const projectsDir = path13.join(wpaths.configDir, "projects");
6463
7117
  const knownProjectHashes = /* @__PURE__ */ new Set();
6464
7118
  const debounceTimers = /* @__PURE__ */ new Map();
6465
7119
  const DEBOUNCE_MS = 150;
@@ -6486,7 +7140,7 @@ function setupEvents(deps2) {
6486
7140
  );
6487
7141
  };
6488
7142
  const metricsInterval = setInterval(logWatcherMetrics, 6e4);
6489
- const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
7143
+ const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
6490
7144
  broadcast2(clients, { type: "client.status_update", payload: statusData });
6491
7145
  if (watcherMetrics) {
6492
7146
  watcherMetrics.broadcastsSent++;
@@ -6527,9 +7181,9 @@ function setupEvents(deps2) {
6527
7181
  if (eventType === "change") {
6528
7182
  if (filename == null) return;
6529
7183
  if (watcherMetrics) watcherMetrics.fileChangesDetected++;
6530
- const targetFile = path12.join(projectsDir, String(filename));
7184
+ const targetFile = path13.join(projectsDir, String(filename));
6531
7185
  if (targetFile.endsWith("status.json")) {
6532
- const projectHash2 = path12.basename(path12.dirname(targetFile));
7186
+ const projectHash2 = path13.basename(path13.dirname(targetFile));
6533
7187
  if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
6534
7188
  return;
6535
7189
  }
@@ -6587,7 +7241,7 @@ function setupEvents(deps2) {
6587
7241
  }
6588
7242
  });
6589
7243
  }
6590
- const globalRoot = globalConfigPath ? path12.dirname(globalConfigPath) : void 0;
7244
+ const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
6591
7245
  if (globalRoot) {
6592
7246
  const broadcastSessions = async () => {
6593
7247
  try {
@@ -6661,10 +7315,10 @@ function setupEvents(deps2) {
6661
7315
  // src/server/custom-context-modes.ts
6662
7316
  import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
6663
7317
  import * as fs11 from "fs/promises";
6664
- import * as path13 from "path";
7318
+ import * as path14 from "path";
6665
7319
  var STORE_FILENAME = "custom-context-modes.json";
6666
7320
  function storePath(wrongstackDir) {
6667
- return path13.join(wrongstackDir, STORE_FILENAME);
7321
+ return path14.join(wrongstackDir, STORE_FILENAME);
6668
7322
  }
6669
7323
  var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
6670
7324
  function createCustomModeStore(wrongstackDir) {
@@ -6796,12 +7450,12 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
6796
7450
 
6797
7451
  // src/server/shell-open.ts
6798
7452
  import * as fs12 from "fs/promises";
6799
- import * as path14 from "path";
7453
+ import * as path15 from "path";
6800
7454
  import { spawn as spawn2 } from "child_process";
6801
7455
  var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
6802
7456
  async function handleShellOpen(req, logger) {
6803
7457
  try {
6804
- const resolved = path14.resolve(req.path);
7458
+ const resolved = path15.resolve(req.path);
6805
7459
  await fs12.access(resolved);
6806
7460
  if (METACHAR_REGEX.test(resolved)) {
6807
7461
  return { success: false, message: "Path contains unsupported characters." };
@@ -6911,15 +7565,15 @@ async function handleGitChanges(ws, projectRoot) {
6911
7565
  if (!m) continue;
6912
7566
  const added = m[1] === "-" ? 0 : Number(m[1]);
6913
7567
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
6914
- let path16 = m[3] ?? "";
6915
- if (path16 === "") {
7568
+ let path17 = m[3] ?? "";
7569
+ if (path17 === "") {
6916
7570
  i += 1;
6917
- path16 = parts[i + 1] ?? parts[i] ?? "";
7571
+ path17 = parts[i + 1] ?? parts[i] ?? "";
6918
7572
  i += 1;
6919
7573
  }
6920
- if (!path16) continue;
6921
- const prev = counts.get(path16) ?? { added: 0, deleted: 0 };
6922
- counts.set(path16, { added: prev.added + added, deleted: prev.deleted + deleted });
7574
+ if (!path17) continue;
7575
+ const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
7576
+ counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
6923
7577
  }
6924
7578
  };
6925
7579
  parseNumstat(unstagedNumstat);
@@ -6931,7 +7585,7 @@ async function handleGitChanges(ws, projectRoot) {
6931
7585
  if (!rec || rec.length < 3) continue;
6932
7586
  const x = rec[0] ?? " ";
6933
7587
  const y = rec[1] ?? " ";
6934
- const path16 = rec.slice(3);
7588
+ const path17 = rec.slice(3);
6935
7589
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
6936
7590
  if (isRename) i += 1;
6937
7591
  let status;
@@ -6943,13 +7597,13 @@ async function handleGitChanges(ws, projectRoot) {
6943
7597
  else if (x === "D" || y === "D") status = "D";
6944
7598
  else status = "M";
6945
7599
  const staged = x !== " " && x !== "?";
6946
- let added = counts.get(path16)?.added ?? 0;
6947
- let deleted = counts.get(path16)?.deleted ?? 0;
7600
+ let added = counts.get(path17)?.added ?? 0;
7601
+ let deleted = counts.get(path17)?.deleted ?? 0;
6948
7602
  if (status === "?") {
6949
7603
  added = 0;
6950
7604
  deleted = 0;
6951
7605
  }
6952
- files.push({ path: path16, status, added, deleted, staged });
7606
+ files.push({ path: path17, status, added, deleted, staged });
6953
7607
  }
6954
7608
  send(ws, { type: "git.changes", payload: { files } });
6955
7609
  } catch (err) {
@@ -6960,21 +7614,21 @@ async function handleGitChanges(ws, projectRoot) {
6960
7614
  }
6961
7615
  }
6962
7616
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
6963
- async function handleGitDiff(ws, projectRoot, path16) {
7617
+ async function handleGitDiff(ws, projectRoot, path17) {
6964
7618
  const cwd = projectRoot || void 0;
6965
- const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path16, ...extra } });
6966
- if (!path16 || path16.includes("\0") || path16.includes("..") || nodePath.isAbsolute(path16)) {
7619
+ const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
7620
+ if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
6967
7621
  reply({ oldText: "", newText: "", error: "invalid path" });
6968
7622
  return;
6969
7623
  }
6970
7624
  try {
6971
7625
  const git = makeGit(cwd);
6972
7626
  const { readFile: readFile9 } = await import("fs/promises");
6973
- const { join: join11 } = await import("path");
6974
- const oldText = await git(["show", `HEAD:${path16}`]);
7627
+ const { join: join12 } = await import("path");
7628
+ const oldText = await git(["show", `HEAD:${path17}`]);
6975
7629
  let newText = "";
6976
7630
  try {
6977
- const abs = cwd ? join11(cwd, path16) : path16;
7631
+ const abs = cwd ? join12(cwd, path17) : path17;
6978
7632
  const buf = await readFile9(abs);
6979
7633
  if (buf.includes(0)) {
6980
7634
  reply({ oldText: "", newText: "", binary: true });
@@ -7185,6 +7839,7 @@ async function startWebUI(opts = {}) {
7185
7839
  toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
7186
7840
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
7187
7841
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
7842
+ applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
7188
7843
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7189
7844
  const mcpRegistry = new MCPRegistry({
7190
7845
  toolRegistry,
@@ -7228,7 +7883,7 @@ async function startWebUI(opts = {}) {
7228
7883
  sessionId: session.id,
7229
7884
  projectSlug: wpaths.projectSlug,
7230
7885
  projectRoot,
7231
- projectName: path15.basename(projectRoot),
7886
+ projectName: path16.basename(projectRoot),
7232
7887
  workingDir,
7233
7888
  clientType: "webui",
7234
7889
  pid: process.pid,
@@ -7248,7 +7903,7 @@ async function startWebUI(opts = {}) {
7248
7903
  const hqTelemetry = createHqPublisherFromEnv({
7249
7904
  clientKind: "webui",
7250
7905
  projectRoot,
7251
- projectName: path15.basename(projectRoot),
7906
+ projectName: path16.basename(projectRoot),
7252
7907
  appConfig: config,
7253
7908
  socketFactory: (url) => new WebSocket2(url)
7254
7909
  });
@@ -7260,7 +7915,7 @@ async function startWebUI(opts = {}) {
7260
7915
  events,
7261
7916
  sessionId: session.id,
7262
7917
  projectRoot,
7263
- projectName: path15.basename(projectRoot),
7918
+ projectName: path16.basename(projectRoot),
7264
7919
  globalRoot: wpaths.globalRoot,
7265
7920
  initialAgents: statusTracker?.getAgents(),
7266
7921
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -7316,9 +7971,9 @@ async function startWebUI(opts = {}) {
7316
7971
  };
7317
7972
  const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
7318
7973
  const skillInstaller = config.features.skills ? new SkillInstaller({
7319
- manifestPath: path15.join(wstackGlobalRoot2(), "installed-skills.json"),
7320
- projectSkillsDir: path15.join(projectRoot, ".wrongstack", "skills"),
7321
- globalSkillsDir: path15.join(wstackGlobalRoot2(), "skills"),
7974
+ manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
7975
+ projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
7976
+ globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
7322
7977
  projectHash: projectHash(projectRoot),
7323
7978
  skillLoader
7324
7979
  }) : void 0;
@@ -7422,6 +8077,8 @@ async function startWebUI(opts = {}) {
7422
8077
  context.meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
7423
8078
  context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
7424
8079
  context.meta["nextPrediction"] = config.nextPrediction ?? false;
8080
+ context.meta["fallbackModels"] = config.fallbackModels ?? [];
8081
+ context.meta["fallbackAuto"] = config.fallbackAuto !== false;
7425
8082
  context.meta["featureMcp"] = config.features.mcp !== false;
7426
8083
  context.meta["featurePlugins"] = config.features.plugins !== false;
7427
8084
  context.meta["featureMemory"] = config.features.memory !== false;
@@ -7479,7 +8136,9 @@ async function startWebUI(opts = {}) {
7479
8136
  "reasoningMode",
7480
8137
  "reasoningEffort",
7481
8138
  "reasoningPreserve",
7482
- "cacheTtl"
8139
+ "cacheTtl",
8140
+ "fallbackModels",
8141
+ "fallbackAuto"
7483
8142
  ];
7484
8143
  const prefSnapshot = () => {
7485
8144
  const snapshot = {};
@@ -7510,6 +8169,8 @@ async function startWebUI(opts = {}) {
7510
8169
  if (typeof payload["enhanceLanguage"] === "string") setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
7511
8170
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
7512
8171
  if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
8172
+ if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
8173
+ if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
7513
8174
  const FEATURE_MAP = {
7514
8175
  featureMcp: "mcp",
7515
8176
  featurePlugins: "plugins",
@@ -7790,6 +8451,29 @@ async function startWebUI(opts = {}) {
7790
8451
  events,
7791
8452
  projectRoot
7792
8453
  );
8454
+ const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
8455
+ const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
8456
+ const sddWizardHandler = new SddWizardWebSocketHandler(
8457
+ buildSddWizardDeps({
8458
+ agent,
8459
+ events,
8460
+ projectRoot,
8461
+ brain,
8462
+ subagentFactory: makeLightSubagentFactory({
8463
+ container,
8464
+ providerRegistry,
8465
+ toolRegistry,
8466
+ session,
8467
+ projectRoot
8468
+ }),
8469
+ paths: {
8470
+ projectSpecs: wpaths.projectSpecs,
8471
+ projectTaskGraphs: wpaths.projectTaskGraphs,
8472
+ projectSddBoards: wpaths.projectSddBoards,
8473
+ projectDir: wpaths.projectDir
8474
+ }
8475
+ })
8476
+ );
7793
8477
  const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
7794
8478
  const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
7795
8479
  const collabHandler = new CollaborationWebSocketHandler(
@@ -7829,7 +8513,7 @@ async function startWebUI(opts = {}) {
7829
8513
  inputCost,
7830
8514
  outputCost,
7831
8515
  cacheReadCost,
7832
- projectName: path15.basename(projectRoot) || projectRoot,
8516
+ projectName: path16.basename(projectRoot) || projectRoot,
7833
8517
  projectRoot,
7834
8518
  cwd: workingDir,
7835
8519
  mode: modeId,
@@ -7921,6 +8605,9 @@ async function startWebUI(opts = {}) {
7921
8605
  }));
7922
8606
  });
7923
8607
  autoPhaseHandler.addClient(ws);
8608
+ specsHandler.addClient(ws);
8609
+ sddBoardHandler.addClient(ws);
8610
+ sddWizardHandler.addClient(ws);
7924
8611
  worktreeHandler.addClient(ws);
7925
8612
  collabHandler.addClient(ws);
7926
8613
  terminalHandler.addClient(ws);
@@ -8045,21 +8732,21 @@ async function startWebUI(opts = {}) {
8045
8732
  });
8046
8733
  }
8047
8734
  async function touchProjectEntry(root, workDir) {
8048
- const resolved = path15.resolve(root);
8735
+ const resolved = path16.resolve(root);
8049
8736
  const manifest = await loadManifest(globalConfigPath);
8050
8737
  const now = (/* @__PURE__ */ new Date()).toISOString();
8051
- const existing = manifest.projects.find((p) => path15.resolve(p.root) === resolved);
8738
+ const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
8052
8739
  if (existing) {
8053
8740
  existing.lastSeen = now;
8054
- if (workDir) existing.lastWorkingDir = path15.resolve(workDir);
8741
+ if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
8055
8742
  } else {
8056
8743
  manifest.projects.push({
8057
- name: path15.basename(resolved),
8744
+ name: path16.basename(resolved),
8058
8745
  root: resolved,
8059
8746
  slug: generateProjectSlug(resolved),
8060
8747
  createdAt: now,
8061
8748
  lastSeen: now,
8062
- lastWorkingDir: workDir ? path15.resolve(workDir) : void 0
8749
+ lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
8063
8750
  });
8064
8751
  }
8065
8752
  await saveManifest(manifest, globalConfigPath);
@@ -8085,6 +8772,9 @@ async function startWebUI(opts = {}) {
8085
8772
  let mailboxRoutes;
8086
8773
  let brainRoutes;
8087
8774
  let autoPhaseRoutes;
8775
+ let specsRoutes;
8776
+ let sddBoardRoutes;
8777
+ let sddWizardRoutes;
8088
8778
  async function handleMessage(ws, _client, msg) {
8089
8779
  if (await handleProviderRoute(ws, msg, providerRoutes)) return;
8090
8780
  if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
@@ -8094,6 +8784,9 @@ async function startWebUI(opts = {}) {
8094
8784
  if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
8095
8785
  if (await handleBrainRoute(ws, msg, brainRoutes)) return;
8096
8786
  if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
8787
+ if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
8788
+ if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
8789
+ if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
8097
8790
  switch (msg.type) {
8098
8791
  // Collaboration messages short-circuit the user/agent flow.
8099
8792
  // They don't touch runLock, the agent loop, or the message queue —
@@ -8390,6 +9083,10 @@ async function startWebUI(opts = {}) {
8390
9083
  config.features.skills = payload["featureSkills"];
8391
9084
  if (typeof payload["featureModelsRegistry"] === "boolean")
8392
9085
  config.features.modelsRegistry = payload["featureModelsRegistry"];
9086
+ if (Array.isArray(payload["fallbackModels"]))
9087
+ config.fallbackModels = payload["fallbackModels"];
9088
+ if (typeof payload["fallbackAuto"] === "boolean")
9089
+ config.fallbackAuto = payload["fallbackAuto"];
8393
9090
  if (typeof payload["contextAutoCompact"] === "boolean") {
8394
9091
  if (payload["contextAutoCompact"] && autoCompactor) {
8395
9092
  pipelines.contextWindow.remove("AutoCompaction", { optional: true });
@@ -8452,22 +9149,7 @@ async function startWebUI(opts = {}) {
8452
9149
  const saved = await providerHandlers.loadConfigProviders();
8453
9150
  send(ws, {
8454
9151
  type: "providers.saved",
8455
- payload: {
8456
- providers: Object.entries(saved).map(([id, cfg]) => {
8457
- const keys = normalizeKeys(cfg);
8458
- return {
8459
- id,
8460
- family: cfg.family ?? id,
8461
- baseUrl: cfg.baseUrl,
8462
- apiKeys: keys.map((k) => ({
8463
- label: k.label,
8464
- maskedKey: maskedKey(k.apiKey),
8465
- isActive: k.label === cfg.activeKey,
8466
- createdAt: k.createdAt
8467
- }))
8468
- };
8469
- })
8470
- }
9152
+ payload: { providers: projectSavedProviders(saved) }
8471
9153
  });
8472
9154
  },
8473
9155
  listProviderModels: async (ws, msg) => {
@@ -8677,7 +9359,7 @@ async function startWebUI(opts = {}) {
8677
9359
  sendResult2(ws, false, parsed.message);
8678
9360
  return;
8679
9361
  }
8680
- return handleMailboxMessages(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9362
+ return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8681
9363
  },
8682
9364
  agents: (ws, msg) => {
8683
9365
  const parsed = validateMailboxAgentsPayload(msg.payload);
@@ -8685,16 +9367,16 @@ async function startWebUI(opts = {}) {
8685
9367
  sendResult2(ws, false, parsed.message);
8686
9368
  return;
8687
9369
  }
8688
- return handleMailboxAgents(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9370
+ return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8689
9371
  },
8690
- clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }),
9372
+ clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
8691
9373
  purge: (ws, msg) => {
8692
9374
  const parsed = validateMailboxPurgePayload(msg.payload);
8693
9375
  if (!parsed.ok) {
8694
9376
  sendResult2(ws, false, parsed.message);
8695
9377
  return;
8696
9378
  }
8697
- return handleMailboxPurge(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9379
+ return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8698
9380
  }
8699
9381
  };
8700
9382
  brainRoutes = {
@@ -8741,6 +9423,15 @@ async function startWebUI(opts = {}) {
8741
9423
  autoPhaseRoutes = {
8742
9424
  handleMessage: (msg) => autoPhaseHandler.handleMessage(msg)
8743
9425
  };
9426
+ specsRoutes = {
9427
+ handleMessage: (msg) => specsHandler.handleMessage(msg)
9428
+ };
9429
+ sddBoardRoutes = {
9430
+ handleMessage: (msg) => sddBoardHandler.handleMessage(msg)
9431
+ };
9432
+ sddWizardRoutes = {
9433
+ handleMessage: (msg) => sddWizardHandler.handleMessage(msg)
9434
+ };
8744
9435
  const watcherMetrics = {
8745
9436
  fileChangesDetected: 0,
8746
9437
  filesProcessed: 0,
@@ -8753,7 +9444,7 @@ async function startWebUI(opts = {}) {
8753
9444
  };
8754
9445
  const httpServer = createHttpServer({
8755
9446
  host: wsHost,
8756
- distDir: path15.resolve(import.meta.dirname, "../../dist"),
9447
+ distDir: path16.resolve(import.meta.dirname, "../../dist"),
8757
9448
  wsPort,
8758
9449
  globalRoot: wpaths.globalRoot,
8759
9450
  apiToken: wsToken,
@@ -8762,7 +9453,7 @@ async function startWebUI(opts = {}) {
8762
9453
  void fleetBroadcast?.();
8763
9454
  }
8764
9455
  });
8765
- const registryBaseDir = path15.dirname(globalConfigPath);
9456
+ const registryBaseDir = path16.dirname(globalConfigPath);
8766
9457
  httpServer.listen(httpPort, wsHost, () => {
8767
9458
  const openUrl = `http://${wsHost}:${httpPort}`;
8768
9459
  console.log(`[WebUI] HTTP server running on ${openUrl}`);
@@ -8774,7 +9465,7 @@ async function startWebUI(opts = {}) {
8774
9465
  wsPort,
8775
9466
  host: wsHost,
8776
9467
  projectRoot,
8777
- projectName: path15.basename(projectRoot) || projectRoot,
9468
+ projectName: path16.basename(projectRoot) || projectRoot,
8778
9469
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8779
9470
  url: `http://${wsHost}:${httpPort}`
8780
9471
  },
@@ -8817,11 +9508,15 @@ async function startWebUI(opts = {}) {
8817
9508
  }
8818
9509
  export {
8819
9510
  AutoPhaseWebSocketHandler,
9511
+ SddBoardWebSocketHandler,
9512
+ SddWizardWebSocketHandler,
9513
+ SpecsWebSocketHandler,
8820
9514
  WorktreeWebSocketHandler,
8821
9515
  addProvider,
8822
9516
  broadcast,
8823
9517
  browserOpenCommand,
8824
9518
  buildCspHeader,
9519
+ buildSddWizardDeps,
8825
9520
  createCustomModeStore,
8826
9521
  createEternalSubscription,
8827
9522
  createHttpServer,