@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.
@@ -8,7 +8,10 @@ function isRecord(value) {
8
8
  }
9
9
  function validateModelSwitchPayload(payload) {
10
10
  if (!isRecord(payload)) {
11
- return { ok: false, message: "model.switch payload must be an object with string provider and model" };
11
+ return {
12
+ ok: false,
13
+ message: "model.switch payload must be an object with string provider and model"
14
+ };
12
15
  }
13
16
  const provider = payload["provider"];
14
17
  const model = payload["model"];
@@ -30,13 +33,22 @@ function validateMailboxMessagesPayload(payload) {
30
33
  const agentId = payload["agentId"];
31
34
  const unreadOnly = payload["unreadOnly"];
32
35
  if (limit !== void 0 && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1)) {
33
- return { ok: false, message: "mailbox.messages payload.limit must be a positive number when provided" };
36
+ return {
37
+ ok: false,
38
+ message: "mailbox.messages payload.limit must be a positive number when provided"
39
+ };
34
40
  }
35
41
  if (agentId !== void 0 && typeof agentId !== "string") {
36
- return { ok: false, message: "mailbox.messages payload.agentId must be a string when provided" };
42
+ return {
43
+ ok: false,
44
+ message: "mailbox.messages payload.agentId must be a string when provided"
45
+ };
37
46
  }
38
47
  if (unreadOnly !== void 0 && typeof unreadOnly !== "boolean") {
39
- return { ok: false, message: "mailbox.messages payload.unreadOnly must be a boolean when provided" };
48
+ return {
49
+ ok: false,
50
+ message: "mailbox.messages payload.unreadOnly must be a boolean when provided"
51
+ };
40
52
  }
41
53
  return { ok: true, value: { limit, agentId, unreadOnly } };
42
54
  }
@@ -47,7 +59,10 @@ function validateMailboxAgentsPayload(payload) {
47
59
  }
48
60
  const onlineOnly = payload["onlineOnly"];
49
61
  if (onlineOnly !== void 0 && typeof onlineOnly !== "boolean") {
50
- return { ok: false, message: "mailbox.agents payload.onlineOnly must be a boolean when provided" };
62
+ return {
63
+ ok: false,
64
+ message: "mailbox.agents payload.onlineOnly must be a boolean when provided"
65
+ };
51
66
  }
52
67
  return { ok: true, value: { onlineOnly } };
53
68
  }
@@ -59,10 +74,16 @@ function validateMailboxPurgePayload(payload) {
59
74
  const completedMaxAgeMs = payload["completedMaxAgeMs"];
60
75
  const incompleteMaxAgeMs = payload["incompleteMaxAgeMs"];
61
76
  if (completedMaxAgeMs !== void 0 && (typeof completedMaxAgeMs !== "number" || !Number.isFinite(completedMaxAgeMs) || completedMaxAgeMs < 0)) {
62
- return { ok: false, message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided" };
77
+ return {
78
+ ok: false,
79
+ message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided"
80
+ };
63
81
  }
64
82
  if (incompleteMaxAgeMs !== void 0 && (typeof incompleteMaxAgeMs !== "number" || !Number.isFinite(incompleteMaxAgeMs) || incompleteMaxAgeMs < 0)) {
65
- return { ok: false, message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided" };
83
+ return {
84
+ ok: false,
85
+ message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided"
86
+ };
66
87
  }
67
88
  return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };
68
89
  }
@@ -73,7 +94,10 @@ function validateBrainRiskPayload(payload) {
73
94
  }
74
95
  const level = payload["level"];
75
96
  if (typeof level !== "string" || !BRAIN_RISK_VALUES.has(level)) {
76
- return { ok: false, message: "brain.risk payload.level must be one of off, low, medium, high, all" };
97
+ return {
98
+ ok: false,
99
+ message: "brain.risk payload.level must be one of off, low, medium, high, all"
100
+ };
77
101
  }
78
102
  return { ok: true, value: { level } };
79
103
  }
@@ -99,7 +123,10 @@ function validateAutonomySwitchPayload(payload) {
99
123
  }
100
124
  function validatePlanTemplateUsePayload(payload) {
101
125
  if (!isRecord(payload)) {
102
- return { ok: false, message: "plan.template_use payload must be an object with string template" };
126
+ return {
127
+ ok: false,
128
+ message: "plan.template_use payload must be an object with string template"
129
+ };
103
130
  }
104
131
  const template = payload["template"];
105
132
  if (typeof template !== "string" || template.trim().length === 0) {
@@ -114,7 +141,15 @@ var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
114
141
  var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
115
142
  var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
116
143
  var REASONING_MODE_VALUES = /* @__PURE__ */ new Set(["auto", "on", "off"]);
117
- var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
144
+ var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
145
+ "none",
146
+ "minimal",
147
+ "low",
148
+ "medium",
149
+ "high",
150
+ "xhigh",
151
+ "max"
152
+ ]);
118
153
  var CACHE_TTL_VALUES = /* @__PURE__ */ new Set(["default", "5m", "1h"]);
119
154
  var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
120
155
  "yolo",
@@ -135,8 +170,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
135
170
  "tgDelegate",
136
171
  "reasoningPreserve",
137
172
  "hqEnabled",
138
- "hqRawContent"
173
+ "hqRawContent",
174
+ "fallbackAuto"
139
175
  ]);
176
+ var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
140
177
  var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
141
178
  "autonomyDelayMs",
142
179
  "autoProceedMaxIterations",
@@ -168,6 +205,9 @@ function validatePreferenceValue(key, value) {
168
205
  if (STRING_PREF_KEYS.has(key)) {
169
206
  return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
170
207
  }
208
+ if (STRING_ARRAY_PREF_KEYS.has(key)) {
209
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
210
+ }
171
211
  const allowed = ENUM_PREF_KEYS[key];
172
212
  if (allowed) {
173
213
  return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
@@ -288,16 +328,25 @@ function validateContextModeCreatePayload(payload) {
288
328
  return { ok: false, message: "context.mode.create payload.description must be a string" };
289
329
  }
290
330
  if (!isRecord(thresholds)) {
291
- return { ok: false, message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers" };
331
+ return {
332
+ ok: false,
333
+ message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers"
334
+ };
292
335
  }
293
336
  if (!isFiniteNumber(thresholds["warn"]) || !isFiniteNumber(thresholds["soft"]) || !isFiniteNumber(thresholds["hard"])) {
294
- return { ok: false, message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers" };
337
+ return {
338
+ ok: false,
339
+ message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers"
340
+ };
295
341
  }
296
342
  if (!isFiniteNumber(preserveK)) {
297
343
  return { ok: false, message: "context.mode.create payload.preserveK must be a finite number" };
298
344
  }
299
345
  if (!isFiniteNumber(eliseThreshold)) {
300
- return { ok: false, message: "context.mode.create payload.eliseThreshold must be a finite number" };
346
+ return {
347
+ ok: false,
348
+ message: "context.mode.create payload.eliseThreshold must be a finite number"
349
+ };
301
350
  }
302
351
  return {
303
352
  ok: true,
@@ -321,22 +370,34 @@ function validateContextModeUpdatePayload(payload) {
321
370
  }
322
371
  const name2 = payload["name"];
323
372
  if (name2 !== void 0 && typeof name2 !== "string") {
324
- return { ok: false, message: "context.mode.update payload.name must be a string when provided" };
373
+ return {
374
+ ok: false,
375
+ message: "context.mode.update payload.name must be a string when provided"
376
+ };
325
377
  }
326
378
  const description = payload["description"];
327
379
  if (description !== void 0 && typeof description !== "string") {
328
- return { ok: false, message: "context.mode.update payload.description must be a string when provided" };
380
+ return {
381
+ ok: false,
382
+ message: "context.mode.update payload.description must be a string when provided"
383
+ };
329
384
  }
330
385
  const thresholds = payload["thresholds"];
331
386
  let validatedThresholds;
332
387
  if (thresholds !== void 0) {
333
388
  if (!isRecord(thresholds)) {
334
- return { ok: false, message: "context.mode.update payload.thresholds must be an object when provided" };
389
+ return {
390
+ ok: false,
391
+ message: "context.mode.update payload.thresholds must be an object when provided"
392
+ };
335
393
  }
336
394
  for (const key of ["warn", "soft", "hard"]) {
337
395
  const val = thresholds[key];
338
396
  if (val !== void 0 && !isFiniteNumber(val)) {
339
- return { ok: false, message: `context.mode.update payload.thresholds.${key} must be a finite number when provided` };
397
+ return {
398
+ ok: false,
399
+ message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`
400
+ };
340
401
  }
341
402
  }
342
403
  validatedThresholds = {
@@ -347,11 +408,17 @@ function validateContextModeUpdatePayload(payload) {
347
408
  }
348
409
  const preserveK = payload["preserveK"];
349
410
  if (preserveK !== void 0 && !isFiniteNumber(preserveK)) {
350
- return { ok: false, message: "context.mode.update payload.preserveK must be a finite number when provided" };
411
+ return {
412
+ ok: false,
413
+ message: "context.mode.update payload.preserveK must be a finite number when provided"
414
+ };
351
415
  }
352
416
  const eliseThreshold = payload["eliseThreshold"];
353
417
  if (eliseThreshold !== void 0 && !isFiniteNumber(eliseThreshold)) {
354
- return { ok: false, message: "context.mode.update payload.eliseThreshold must be a finite number when provided" };
418
+ return {
419
+ ok: false,
420
+ message: "context.mode.update payload.eliseThreshold must be a finite number when provided"
421
+ };
355
422
  }
356
423
  return {
357
424
  ok: true,
@@ -369,28 +436,31 @@ function validateShellOpenPayload(payload) {
369
436
  if (!isRecord(payload)) {
370
437
  return { ok: false, message: "shell.open payload must be an object with string path" };
371
438
  }
372
- const path16 = payload["path"];
373
- if (typeof path16 !== "string" || path16.trim().length === 0) {
439
+ const path17 = payload["path"];
440
+ if (typeof path17 !== "string" || path17.trim().length === 0) {
374
441
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
375
442
  }
376
443
  const target = payload["target"];
377
444
  if (target !== void 0 && target !== "file" && target !== "terminal") {
378
- return { ok: false, message: 'shell.open payload.target must be "file" or "terminal" when provided' };
445
+ return {
446
+ ok: false,
447
+ message: 'shell.open payload.target must be "file" or "terminal" when provided'
448
+ };
379
449
  }
380
- return { ok: true, value: { path: path16, target } };
450
+ return { ok: true, value: { path: path17, target } };
381
451
  }
382
452
  function validateGitDiffPayload(payload) {
383
453
  if (!isRecord(payload)) {
384
454
  return { ok: false, message: "git.diff payload must be an object" };
385
455
  }
386
- const path16 = payload["path"];
387
- if (path16 === void 0 || path16 === null) {
456
+ const path17 = payload["path"];
457
+ if (path17 === void 0 || path17 === null) {
388
458
  return { ok: true, value: { path: "" } };
389
459
  }
390
- if (typeof path16 !== "string") {
460
+ if (typeof path17 !== "string") {
391
461
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
392
462
  }
393
- return { ok: true, value: { path: path16 } };
463
+ return { ok: true, value: { path: path17 } };
394
464
  }
395
465
  function validateProjectsAddPayload(payload) {
396
466
  if (!isRecord(payload)) {
@@ -570,7 +640,7 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
570
640
  return;
571
641
  }
572
642
  try {
573
- const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
643
+ const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
574
644
  let changed = false;
575
645
  const plan = await mutatePlan(planPath, sessionId, async (p) => {
576
646
  const before = p.updatedAt;
@@ -650,7 +720,7 @@ import {
650
720
  createTieredBrainArbiter
651
721
  } from "@wrongstack/core";
652
722
  import * as fs13 from "fs/promises";
653
- import * as path15 from "path";
723
+ import * as path16 from "path";
654
724
 
655
725
  // src/server/http-server.ts
656
726
  import * as fs from "fs/promises";
@@ -2813,6 +2883,7 @@ import {
2813
2883
  DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
2814
2884
  DEFAULT_SESSION_PRUNE_DAYS,
2815
2885
  DEFAULT_TOOLS_CONFIG,
2886
+ applyToolDescriptionModes,
2816
2887
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
2817
2888
  enhanceUserPrompt,
2818
2889
  gatedEnhancerReasoning,
@@ -2857,7 +2928,8 @@ function createDefaultContainer(opts) {
2857
2928
  () => new DefaultErrorHandler(
2858
2929
  buildRecoveryStrategies({
2859
2930
  compactor: container.resolve(TOKENS.Compactor),
2860
- modelsRegistry
2931
+ modelsRegistry,
2932
+ getConfig: () => configStore.get()
2861
2933
  })
2862
2934
  )
2863
2935
  );
@@ -2944,6 +3016,7 @@ function patchConfig(config, updates) {
2944
3016
  import { spawnSync } from "child_process";
2945
3017
  import { toErrorMessage } from "@wrongstack/core/utils";
2946
3018
  import {
3019
+ assignNickname,
2947
3020
  AutoPhasePlanner,
2948
3021
  PhaseGraphBuilder,
2949
3022
  PhaseOrchestrator,
@@ -2981,6 +3054,8 @@ var AutoPhaseWebSocketHandler = class {
2981
3054
  abort = null;
2982
3055
  /** Optional per-phase git-worktree isolation (lazily created at start). */
2983
3056
  worktrees = null;
3057
+ /** Per-run worker identities so the board can show "who is on what". */
3058
+ usedNicknames = /* @__PURE__ */ new Set();
2984
3059
  addClient(ws) {
2985
3060
  const client = { ws, id: crypto.randomUUID() };
2986
3061
  this.clients.add(client);
@@ -3023,6 +3098,29 @@ var AutoPhaseWebSocketHandler = class {
3023
3098
  await this.handleTaskStatusChange(taskId, status);
3024
3099
  break;
3025
3100
  }
3101
+ case "autophase.moveTask": {
3102
+ const { taskId, toPhaseId } = msg.payload;
3103
+ if (this.orchestrator?.moveTask(taskId, toPhaseId)) this.afterBoardMutation();
3104
+ break;
3105
+ }
3106
+ case "autophase.assignTask": {
3107
+ const { taskId, agentId, agentName } = msg.payload;
3108
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
3109
+ break;
3110
+ }
3111
+ case "autophase.addTask": {
3112
+ const { phaseId, title, description, type, priority } = msg.payload;
3113
+ if (title?.trim() && this.orchestrator?.addTask(phaseId, { title: title.trim(), description, type, priority })) {
3114
+ this.afterBoardMutation();
3115
+ }
3116
+ break;
3117
+ }
3118
+ case "autophase.retryTask":
3119
+ case "autophase.runTask": {
3120
+ const { taskId } = msg.payload;
3121
+ if (this.orchestrator?.requeueTask(taskId)) this.afterBoardMutation();
3122
+ break;
3123
+ }
3026
3124
  case "autophase.toggleAutonomous": {
3027
3125
  const autonomous = msg.payload?.autonomous ?? !this.graph?.autonomous;
3028
3126
  if (this.graph) {
@@ -3150,6 +3248,13 @@ var AutoPhaseWebSocketHandler = class {
3150
3248
  return this.defaultPhases();
3151
3249
  }
3152
3250
  async executeTaskWithAgent(task, phaseId, env) {
3251
+ if (!task.assignee) {
3252
+ const nick = assignNickname("executor", this.usedNicknames);
3253
+ this.usedNicknames.add(nick.key);
3254
+ task.assignee = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3255
+ task.updatedAt = Date.now();
3256
+ this.broadcastState();
3257
+ }
3153
3258
  const prompt = `Execute task: ${task.title}
3154
3259
 
3155
3260
  Description: ${task.description}
@@ -3165,6 +3270,11 @@ Type: ${task.type}`;
3165
3270
  this.context.cwd = prevCwd;
3166
3271
  }
3167
3272
  }
3273
+ /** Persist + broadcast after an interactive board mutation. */
3274
+ afterBoardMutation() {
3275
+ if (this.graph) void this.store.save(this.graph);
3276
+ this.broadcastState();
3277
+ }
3168
3278
  async handleTaskStatusChange(taskId, status) {
3169
3279
  if (!this.graph) return;
3170
3280
  for (const phase of this.graph.phases.values()) {
@@ -3208,23 +3318,7 @@ Type: ${task.type}`;
3208
3318
  (sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3209
3319
  0
3210
3320
  );
3211
- const phaseItems = phases.map((p) => ({
3212
- id: p.id,
3213
- name: p.name,
3214
- description: p.description,
3215
- status: p.status,
3216
- priority: p.priority,
3217
- estimateHours: p.estimateHours,
3218
- actualDurationMs: p.actualDurationMs,
3219
- startedAt: p.startedAt,
3220
- completedAt: p.completedAt,
3221
- progressPercent: p.taskGraph.nodes.size > 0 ? Math.round(Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length / p.taskGraph.nodes.size * 100) : 0,
3222
- taskCount: p.taskGraph.nodes.size,
3223
- completedTasks: Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
3224
- assignedAgents: p.assignedAgents,
3225
- isActive: p.id === currentActiveId
3226
- }));
3227
- const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
3321
+ const mapTask = (t) => ({
3228
3322
  id: t.id,
3229
3323
  title: t.title,
3230
3324
  description: t.description,
@@ -3237,7 +3331,31 @@ Type: ${task.type}`;
3237
3331
  tags: t.tags || [],
3238
3332
  startedAt: t.startedAt,
3239
3333
  completedAt: t.completedAt
3240
- })) : [];
3334
+ });
3335
+ const phaseItems = phases.map((p) => {
3336
+ const nodes = Array.from(p.taskGraph.nodes.values());
3337
+ const done = nodes.filter((t) => t.status === "completed").length;
3338
+ return {
3339
+ id: p.id,
3340
+ name: p.name,
3341
+ description: p.description,
3342
+ status: p.status,
3343
+ priority: p.priority,
3344
+ estimateHours: p.estimateHours,
3345
+ actualDurationMs: p.actualDurationMs,
3346
+ startedAt: p.startedAt,
3347
+ completedAt: p.completedAt,
3348
+ progressPercent: nodes.length > 0 ? Math.round(done / nodes.length * 100) : 0,
3349
+ taskCount: nodes.length,
3350
+ completedTasks: done,
3351
+ assignedAgents: p.assignedAgents,
3352
+ isActive: p.id === currentActiveId,
3353
+ // Every phase carries its full task list so the board can render each
3354
+ // phase as a column (not just the selected one).
3355
+ tasks: nodes.map(mapTask)
3356
+ };
3357
+ });
3358
+ const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
3241
3359
  const completedPhases = phases.filter((p) => p.status === "completed").length;
3242
3360
  return {
3243
3361
  title: this.graph.title,
@@ -3270,6 +3388,513 @@ Type: ${task.type}`;
3270
3388
  }
3271
3389
  };
3272
3390
 
3391
+ // src/server/specs-ws-handler.ts
3392
+ import {
3393
+ computeTaskProgress,
3394
+ SpecStore,
3395
+ TaskGraphStore
3396
+ } from "@wrongstack/core";
3397
+ var SpecsWebSocketHandler = class {
3398
+ specStore;
3399
+ graphStore;
3400
+ clients = /* @__PURE__ */ new Set();
3401
+ constructor(specsDir, taskGraphsDir) {
3402
+ this.specStore = new SpecStore({ baseDir: specsDir });
3403
+ this.graphStore = new TaskGraphStore({ baseDir: taskGraphsDir });
3404
+ }
3405
+ addClient(ws) {
3406
+ const client = { ws, id: crypto.randomUUID() };
3407
+ this.clients.add(client);
3408
+ ws.on("close", () => this.clients.delete(client));
3409
+ ws.on("error", () => this.clients.delete(client));
3410
+ void this.sendList(client);
3411
+ }
3412
+ async handleMessage(msg) {
3413
+ switch (msg.type) {
3414
+ case "specs.list":
3415
+ await this.broadcastList();
3416
+ break;
3417
+ case "specs.get": {
3418
+ const specId = msg.payload?.specId;
3419
+ if (specId) await this.broadcastDetail(specId);
3420
+ break;
3421
+ }
3422
+ case "specs.taskStatus": {
3423
+ const { graphId, taskId, status } = msg.payload;
3424
+ await this.updateTaskStatus(graphId, taskId, status);
3425
+ break;
3426
+ }
3427
+ }
3428
+ }
3429
+ // ── List ──────────────────────────────────────────────────────────────────
3430
+ async buildList() {
3431
+ const [specs, graphs] = await Promise.all([this.specStore.list(), this.graphStore.list()]);
3432
+ return specs.map((s, i) => {
3433
+ const graph = graphs.find((g) => g.specId === s.id);
3434
+ return {
3435
+ id: s.id,
3436
+ // FORGE-style display id (spec-001…). The real UUID stays in `id`.
3437
+ displayId: `spec-${String(i + 1).padStart(3, "0")}`,
3438
+ title: s.title,
3439
+ status: s.status,
3440
+ graphId: graph?.id,
3441
+ total: graph?.nodeCount ?? 0,
3442
+ completed: graph?.completedCount ?? 0
3443
+ };
3444
+ });
3445
+ }
3446
+ async broadcastList() {
3447
+ this.broadcast({ type: "specs.list", payload: { specs: await this.buildList() } });
3448
+ }
3449
+ async sendList(client) {
3450
+ this.send(client, { type: "specs.list", payload: { specs: await this.buildList() } });
3451
+ }
3452
+ // ── Detail (dependency board) ───────────────────────────────────────────────
3453
+ async broadcastDetail(specId) {
3454
+ const spec = await this.specStore.load(specId);
3455
+ const graph = await this.findGraphForSpec(specId);
3456
+ if (!spec || !graph) {
3457
+ this.broadcast({ type: "specs.detail", payload: { specId, columns: [], notFound: true } });
3458
+ return;
3459
+ }
3460
+ this.broadcast({ type: "specs.detail", payload: this.buildDetail(spec, graph) });
3461
+ }
3462
+ async findGraphForSpec(specId) {
3463
+ const entry = (await this.graphStore.list()).find((g) => g.specId === specId);
3464
+ if (!entry) return null;
3465
+ return this.graphStore.load(entry.id);
3466
+ }
3467
+ buildDetail(spec, graph) {
3468
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
3469
+ const shortId = /* @__PURE__ */ new Map();
3470
+ nodes.forEach((n, i) => {
3471
+ shortId.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
3472
+ });
3473
+ const blockers = /* @__PURE__ */ new Map();
3474
+ for (const n of nodes) blockers.set(n.id, []);
3475
+ for (const e of graph.edges) {
3476
+ if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
3477
+ }
3478
+ const statusOf = (id) => graph.nodes.get(id)?.status;
3479
+ const depthCache = /* @__PURE__ */ new Map();
3480
+ const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
3481
+ const cached = depthCache.get(id);
3482
+ if (cached !== void 0) return cached;
3483
+ if (seen.has(id)) return 0;
3484
+ seen.add(id);
3485
+ const deps2 = blockers.get(id) ?? [];
3486
+ const d = deps2.length === 0 ? 0 : 1 + Math.max(...deps2.map((b) => depthOf(b, seen)));
3487
+ depthCache.set(id, d);
3488
+ return d;
3489
+ };
3490
+ const toBoardTask = (n) => {
3491
+ const deps2 = blockers.get(n.id) ?? [];
3492
+ const allDepsDone = deps2.every((b) => statusOf(b) === "completed");
3493
+ const displayStatus = n.status === "pending" && deps2.length > 0 && allDepsDone ? "queued" : n.status;
3494
+ return {
3495
+ id: n.id,
3496
+ shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
3497
+ title: n.title,
3498
+ description: n.description,
3499
+ priority: n.priority,
3500
+ type: n.type,
3501
+ status: n.status,
3502
+ displayStatus,
3503
+ deps: deps2.map((b) => shortId.get(b) ?? b.slice(0, 6))
3504
+ };
3505
+ };
3506
+ const byDepth = /* @__PURE__ */ new Map();
3507
+ for (const n of nodes) {
3508
+ const d = depthOf(n.id);
3509
+ if (!byDepth.has(d)) byDepth.set(d, []);
3510
+ byDepth.get(d)?.push(toBoardTask(n));
3511
+ }
3512
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, tasks: byDepth.get(d) ?? [] }));
3513
+ const progress = computeTaskProgress(graph);
3514
+ return {
3515
+ specId: spec.id,
3516
+ graphId: graph.id,
3517
+ title: spec.title,
3518
+ overview: spec.overview,
3519
+ status: spec.status,
3520
+ total: progress.total,
3521
+ completed: progress.completed,
3522
+ running: progress.inProgress,
3523
+ pending: progress.pending,
3524
+ columns
3525
+ };
3526
+ }
3527
+ async updateTaskStatus(graphId, taskId, status) {
3528
+ const graph = await this.graphStore.load(graphId);
3529
+ const node = graph?.nodes.get(taskId);
3530
+ if (!graph || !node) return;
3531
+ node.status = status;
3532
+ node.updatedAt = Date.now();
3533
+ graph.updatedAt = Date.now();
3534
+ await this.graphStore.save(graph);
3535
+ this.broadcastDetail(graph.specId).catch(() => {
3536
+ });
3537
+ await this.broadcastList();
3538
+ }
3539
+ // ── Transport ───────────────────────────────────────────────────────────────
3540
+ broadcast(msg) {
3541
+ const data = JSON.stringify(msg);
3542
+ for (const client of this.clients) {
3543
+ if (client.ws.readyState === 1) client.ws.send(data);
3544
+ }
3545
+ }
3546
+ send(client, msg) {
3547
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3548
+ }
3549
+ };
3550
+
3551
+ // src/server/sdd-board-ws-handler.ts
3552
+ import { SddBoardStore } from "@wrongstack/core";
3553
+ var CONTROL_TYPES = /* @__PURE__ */ new Set([
3554
+ "pause",
3555
+ "resume",
3556
+ "stop",
3557
+ "retry",
3558
+ "retry_all_failed",
3559
+ "reassign",
3560
+ // Per-task model / fallback / verification assignment + stop/delete (drained by start-sdd-run).
3561
+ "set_task_model",
3562
+ "set_task_fallbacks",
3563
+ "set_task_verification",
3564
+ "cancel_task",
3565
+ "delete_task",
3566
+ "split_task",
3567
+ // Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
3568
+ "cleanup_worktrees",
3569
+ "rollback"
3570
+ ]);
3571
+ var SddBoardWebSocketHandler = class {
3572
+ store;
3573
+ clients = /* @__PURE__ */ new Set();
3574
+ latest = null;
3575
+ poll = null;
3576
+ unsub = null;
3577
+ constructor(boardsDir, events) {
3578
+ this.store = new SddBoardStore({ baseDir: boardsDir });
3579
+ if (events) {
3580
+ const handler = (e) => {
3581
+ this.latest = e.snapshot;
3582
+ this.broadcast({ type: "sdd.board.snapshot", payload: e.snapshot });
3583
+ };
3584
+ this.unsub = events.on("sdd.board.snapshot", handler);
3585
+ } else {
3586
+ this.poll = setInterval(() => void this.pollLatest(), 1e3);
3587
+ }
3588
+ }
3589
+ addClient(ws) {
3590
+ const client = { ws, id: crypto.randomUUID() };
3591
+ this.clients.add(client);
3592
+ ws.on("close", () => this.clients.delete(client));
3593
+ ws.on("error", () => this.clients.delete(client));
3594
+ void this.sendCurrent(client);
3595
+ }
3596
+ async handleMessage(msg) {
3597
+ if (msg.type === "sdd.board.get") {
3598
+ await this.broadcastCurrent();
3599
+ return;
3600
+ }
3601
+ if (msg.type === "sdd.board.list") {
3602
+ const boards = await this.store.list();
3603
+ this.broadcast({ type: "sdd.board.list", payload: { boards } });
3604
+ return;
3605
+ }
3606
+ const action = msg.type.replace(/^sdd\.board\./, "");
3607
+ if (CONTROL_TYPES.has(action)) {
3608
+ const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
3609
+ if (runId) {
3610
+ await this.store.appendControl(runId, {
3611
+ ts: Date.now(),
3612
+ type: action,
3613
+ payload: msg.payload
3614
+ });
3615
+ }
3616
+ }
3617
+ }
3618
+ dispose() {
3619
+ if (this.poll) clearInterval(this.poll);
3620
+ this.unsub?.();
3621
+ this.poll = null;
3622
+ this.unsub = null;
3623
+ }
3624
+ // ── internal ────────────────────────────────────────────────────────────
3625
+ async pollLatest() {
3626
+ const entry = (await this.store.list())[0];
3627
+ if (!entry) return;
3628
+ if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
3629
+ return;
3630
+ }
3631
+ const snap = await this.store.load(entry.runId);
3632
+ if (snap) {
3633
+ this.latest = snap;
3634
+ this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3635
+ }
3636
+ }
3637
+ async sendCurrent(client) {
3638
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3639
+ if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
3640
+ }
3641
+ async broadcastCurrent() {
3642
+ const snap = this.latest ?? await this.loadLatestFromDisk();
3643
+ if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
3644
+ }
3645
+ async loadLatestFromDisk() {
3646
+ const entry = (await this.store.list())[0];
3647
+ return entry ? this.store.load(entry.runId) : null;
3648
+ }
3649
+ broadcast(msg) {
3650
+ const data = JSON.stringify(msg);
3651
+ for (const client of this.clients) {
3652
+ if (client.ws.readyState === 1) client.ws.send(data);
3653
+ }
3654
+ }
3655
+ send(client, msg) {
3656
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3657
+ }
3658
+ };
3659
+
3660
+ // src/server/sdd-wizard-ws-handler.ts
3661
+ var SddWizardWebSocketHandler = class {
3662
+ constructor(deps2) {
3663
+ this.deps = deps2;
3664
+ }
3665
+ deps;
3666
+ clients = /* @__PURE__ */ new Set();
3667
+ driver = null;
3668
+ /** The agent's most recent question — paired with the next user answer. */
3669
+ lastAgentText = "";
3670
+ /** Guards against overlapping interview turns (one in flight at a time). */
3671
+ busy = false;
3672
+ addClient(ws) {
3673
+ const client = { ws, id: crypto.randomUUID() };
3674
+ this.clients.add(client);
3675
+ ws.on("close", () => this.clients.delete(client));
3676
+ ws.on("error", () => this.clients.delete(client));
3677
+ if (this.driver) this.send(client, this.snapshotMsg());
3678
+ }
3679
+ async handleMessage(msg) {
3680
+ try {
3681
+ switch (msg.type) {
3682
+ case "sdd.spec.start":
3683
+ await this.onStart(String(msg.payload?.goal ?? "").trim());
3684
+ break;
3685
+ case "sdd.spec.message":
3686
+ await this.onMessage(String(msg.payload?.text ?? ""));
3687
+ break;
3688
+ case "sdd.spec.approve":
3689
+ await this.onApprove();
3690
+ break;
3691
+ case "sdd.spec.get":
3692
+ if (this.driver) this.broadcast(this.snapshotMsg());
3693
+ break;
3694
+ case "sdd.run.start":
3695
+ await this.onRunStart({
3696
+ parallelSlots: msg.payload?.parallelSlots,
3697
+ defaultModel: msg.payload?.model,
3698
+ defaultProvider: msg.payload?.provider,
3699
+ fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
3700
+ });
3701
+ break;
3702
+ }
3703
+ } catch (err) {
3704
+ this.busy = false;
3705
+ this.broadcast({
3706
+ type: "sdd.spec.error",
3707
+ payload: { message: err instanceof Error ? err.message : String(err) }
3708
+ });
3709
+ }
3710
+ }
3711
+ // ── message handlers ──────────────────────────────────────────────────────
3712
+ async onStart(goal) {
3713
+ if (!goal) {
3714
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "A goal is required." } });
3715
+ return;
3716
+ }
3717
+ if (this.busy) return;
3718
+ this.driver = this.deps.makeDriver();
3719
+ const prompt = this.driver.start(goal);
3720
+ await this.runTurn(prompt);
3721
+ }
3722
+ async onMessage(text) {
3723
+ if (!this.driver || this.busy) return;
3724
+ if (this.driver.phase() === "questioning" && this.lastAgentText) {
3725
+ this.driver.submitAnswer(this.lastAgentText, text);
3726
+ } else {
3727
+ this.driver.submitAnswer(this.lastAgentText || "(feedback)", text);
3728
+ }
3729
+ await this.runTurn(this.driver.currentPrompt());
3730
+ }
3731
+ async onApprove() {
3732
+ if (!this.driver || this.busy) return;
3733
+ const { phase, prompt } = await this.driver.approve();
3734
+ if (phase === "executing") {
3735
+ this.broadcast(this.snapshotMsg());
3736
+ return;
3737
+ }
3738
+ await this.runTurn(prompt);
3739
+ }
3740
+ async onRunStart(opts) {
3741
+ if (!this.driver) {
3742
+ this.broadcast({ type: "sdd.spec.error", payload: { message: "No active spec session." } });
3743
+ return;
3744
+ }
3745
+ const graph = await this.driver.ensureTaskGraph();
3746
+ if (!graph) {
3747
+ this.broadcast({
3748
+ type: "sdd.spec.error",
3749
+ payload: { message: "No spec yet \u2014 finish the interview before starting a run." }
3750
+ });
3751
+ return;
3752
+ }
3753
+ const { runId } = await this.deps.startRun(this.driver, opts);
3754
+ this.broadcast({ type: "sdd.run.started", payload: { runId } });
3755
+ }
3756
+ // ── internals ───────────────────────────────────────────────────────────
3757
+ /** Run one interview turn against the isolated agent, then ingest + broadcast. */
3758
+ async runTurn(prompt) {
3759
+ this.busy = true;
3760
+ this.broadcast(this.snapshotMsg());
3761
+ try {
3762
+ const text = await this.deps.runInterviewTurn(prompt);
3763
+ this.lastAgentText = text;
3764
+ if (this.driver) await this.driver.ingestAgentOutput(text);
3765
+ this.broadcast({ type: "sdd.spec.agent_text", payload: { text } });
3766
+ } finally {
3767
+ this.busy = false;
3768
+ this.broadcast(this.snapshotMsg());
3769
+ }
3770
+ }
3771
+ snapshotMsg() {
3772
+ const snap = this.driver?.snapshot();
3773
+ return {
3774
+ type: "sdd.spec.snapshot",
3775
+ payload: { ...snap, busy: this.busy }
3776
+ };
3777
+ }
3778
+ broadcast(msg) {
3779
+ const data = JSON.stringify(msg);
3780
+ for (const client of this.clients) {
3781
+ if (client.ws.readyState === 1) client.ws.send(data);
3782
+ }
3783
+ }
3784
+ send(client, msg) {
3785
+ if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
3786
+ }
3787
+ };
3788
+
3789
+ // src/server/sdd-wizard-wiring.ts
3790
+ import * as path6 from "path";
3791
+ import { spawnSync as spawnSync2 } from "child_process";
3792
+ import {
3793
+ makeCommandVerifier,
3794
+ makeLlmSubtaskGenerator,
3795
+ SddBoardStore as SddBoardStore2,
3796
+ SddInterviewDriver,
3797
+ SddRunRegistry,
3798
+ SddSupervisor,
3799
+ SpecStore as SpecStore2,
3800
+ startSddRun,
3801
+ TaskGraphStore as TaskGraphStore2,
3802
+ WorktreeManager as WorktreeManager2
3803
+ } from "@wrongstack/core";
3804
+ function buildSddWizardDeps(opts) {
3805
+ const registry = new SddRunRegistry();
3806
+ let isolatedSeq = 0;
3807
+ const runIsolatedTurn = async (prompt, name2) => {
3808
+ const result = await opts.subagentFactory({
3809
+ id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
3810
+ role: "executor",
3811
+ name: name2,
3812
+ disabledTools: ["delegate"],
3813
+ allowedCapabilities: ["fs.read", "net.outbound"]
3814
+ });
3815
+ try {
3816
+ const res = await result.agent.run([{ type: "text", text: prompt }]);
3817
+ return res.finalText ?? "";
3818
+ } finally {
3819
+ await result.dispose?.();
3820
+ }
3821
+ };
3822
+ return {
3823
+ makeDriver: () => new SddInterviewDriver({
3824
+ specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
3825
+ graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
3826
+ sessionPath: path6.join(opts.paths.projectDir, "sdd-wizard-session.json")
3827
+ }),
3828
+ runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
3829
+ startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
3830
+ const graph = driver.getGraph();
3831
+ const tracker = driver.getTracker();
3832
+ if (!graph || !tracker) {
3833
+ throw new Error("No task graph to run \u2014 finish the interview first.");
3834
+ }
3835
+ let worktrees;
3836
+ if (process.env["WRONGSTACK_SDD_WORKTREES"] !== "0") {
3837
+ const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
3838
+ cwd: opts.projectRoot,
3839
+ encoding: "utf8",
3840
+ windowsHide: true
3841
+ }).stdout?.trim() === "true";
3842
+ if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
3843
+ }
3844
+ const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
3845
+ const verifyTask = makeCommandVerifier();
3846
+ const superviseFailure = opts.brain ? new SddSupervisor({
3847
+ brain: opts.brain,
3848
+ // The run-level fallback chain (chosen in the wizard) doubles as the
3849
+ // supervisor's reassign options — a `reassign` verdict rotates the
3850
+ // worker model on retry. Empty/undefined → reassign option dropped.
3851
+ reassignModels: fallbackModels,
3852
+ // LLM auto-split: decompose a retry-exhausted task into smaller
3853
+ // sub-tasks on an isolated read-only turn. Heavily validated +
3854
+ // bounded; an empty result degrades the split into a retry.
3855
+ generateSubtasks: makeLlmSubtaskGenerator({
3856
+ run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
3857
+ }),
3858
+ // The standalone brain is a tiered policy→LLM arbiter with NO
3859
+ // human-escalation wrapper (see index.ts), so it never blocks on a
3860
+ // human prompt — an unresolved verdict degrades to a bounded retry.
3861
+ // Safe to let the LLM layer actually pick reassign/split.
3862
+ requestLlmVerdict: true
3863
+ }).superviseFailure : void 0;
3864
+ const handle = startSddRun({
3865
+ tracker,
3866
+ graph,
3867
+ agent: opts.agent,
3868
+ projectRoot: opts.projectRoot,
3869
+ events: opts.events,
3870
+ subagentFactory: opts.subagentFactory,
3871
+ worktrees,
3872
+ boardStore,
3873
+ registry,
3874
+ parallelSlots,
3875
+ defaultModel,
3876
+ defaultProvider,
3877
+ fallbackModels,
3878
+ verifyTask,
3879
+ superviseFailure
3880
+ });
3881
+ void handle.completion.catch(() => {
3882
+ });
3883
+ return { runId: handle.runId };
3884
+ }
3885
+ };
3886
+ }
3887
+
3888
+ // src/server/sdd-wizard-routes.ts
3889
+ async function handleSddWizardRoute(_ws, msg, handlers) {
3890
+ if (!(msg.type.startsWith("sdd.spec.") || msg.type.startsWith("sdd.run."))) return false;
3891
+ await handlers.handleMessage(msg);
3892
+ return true;
3893
+ }
3894
+
3895
+ // src/server/index.ts
3896
+ import { makeLightSubagentFactory } from "@wrongstack/runtime";
3897
+
3273
3898
  // src/server/collaboration-ws-handler.ts
3274
3899
  import { randomUUID } from "crypto";
3275
3900
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -3997,11 +4622,11 @@ var CollaborationWebSocketHandler = class {
3997
4622
 
3998
4623
  // src/server/projects-manifest.ts
3999
4624
  import * as fs5 from "fs/promises";
4000
- import * as path6 from "path";
4625
+ import * as path7 from "path";
4001
4626
  import { projectSlug } from "@wrongstack/core";
4002
4627
  function projectsJsonPath(globalConfigPath) {
4003
- const base = path6.dirname(globalConfigPath);
4004
- return path6.join(base, "projects.json");
4628
+ const base = path7.dirname(globalConfigPath);
4629
+ return path7.join(base, "projects.json");
4005
4630
  }
4006
4631
  async function loadManifest(globalConfigPath) {
4007
4632
  try {
@@ -4014,15 +4639,15 @@ async function loadManifest(globalConfigPath) {
4014
4639
  }
4015
4640
  async function saveManifest(manifest, globalConfigPath) {
4016
4641
  const file = projectsJsonPath(globalConfigPath);
4017
- await fs5.mkdir(path6.dirname(file), { recursive: true });
4642
+ await fs5.mkdir(path7.dirname(file), { recursive: true });
4018
4643
  await fs5.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
4019
4644
  }
4020
4645
  function generateProjectSlug(rootPath) {
4021
4646
  return projectSlug(rootPath);
4022
4647
  }
4023
4648
  async function ensureProjectDataDir(slug, globalConfigPath) {
4024
- const base = path6.dirname(globalConfigPath);
4025
- const dir = path6.join(base, "projects", slug);
4649
+ const base = path7.dirname(globalConfigPath);
4650
+ const dir = path7.join(base, "projects", slug);
4026
4651
  await fs5.mkdir(dir, { recursive: true });
4027
4652
  return dir;
4028
4653
  }
@@ -4449,14 +5074,14 @@ function registerShutdownHandlers(res) {
4449
5074
 
4450
5075
  // src/server/instance-registry.ts
4451
5076
  import * as os from "os";
4452
- import * as path7 from "path";
5077
+ import * as path8 from "path";
4453
5078
  import * as fs6 from "fs/promises";
4454
5079
  import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
4455
5080
  function defaultBaseDir() {
4456
- return path7.join(os.homedir(), ".wrongstack");
5081
+ return path8.join(os.homedir(), ".wrongstack");
4457
5082
  }
4458
5083
  function registryPath(baseDir = defaultBaseDir()) {
4459
- return path7.join(baseDir, "webui-instances.json");
5084
+ return path8.join(baseDir, "webui-instances.json");
4460
5085
  }
4461
5086
  function isPidAlive(pid) {
4462
5087
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -4578,9 +5203,10 @@ function openBrowser(url, platform = process.platform) {
4578
5203
  if (child.pid) {
4579
5204
  try {
4580
5205
  import("@wrongstack/tools").then(({ getProcessRegistry }) => {
5206
+ const pid = child.pid;
5207
+ if (pid === void 0) return;
4581
5208
  getProcessRegistry().register({
4582
- // biome-ignore lint/style/noNonNullAssertion: pid always present after spawn
4583
- pid: child.pid,
5209
+ pid,
4584
5210
  name: "browser",
4585
5211
  command: `${command} ${args.join(" ")}`,
4586
5212
  startedAt: Date.now(),
@@ -4588,7 +5214,7 @@ function openBrowser(url, platform = process.platform) {
4588
5214
  protected: true
4589
5215
  });
4590
5216
  child.on("exit", () => {
4591
- getProcessRegistry().unregister(child.pid);
5217
+ getProcessRegistry().unregister(pid);
4592
5218
  });
4593
5219
  }).catch(() => {
4594
5220
  });
@@ -4618,7 +5244,7 @@ import { probeLocalLlm } from "@wrongstack/runtime/probe";
4618
5244
 
4619
5245
  // src/server/provider-config-io.ts
4620
5246
  import * as fs7 from "fs/promises";
4621
- import * as path8 from "path";
5247
+ import * as path9 from "path";
4622
5248
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
4623
5249
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
4624
5250
  import { DefaultSecretVault } from "@wrongstack/core";
@@ -4816,7 +5442,10 @@ function createProviderHandlers(deps2) {
4816
5442
  try {
4817
5443
  const providers = await loadConfigProviders();
4818
5444
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
4819
- if (result.ok) await saveConfigProviders(providers);
5445
+ if (result.ok) {
5446
+ await saveConfigProviders(providers);
5447
+ broadcastSaved(providers);
5448
+ }
4820
5449
  sendResult2(ws, result.ok, result.message);
4821
5450
  } catch (err) {
4822
5451
  sendResult2(ws, false, errMessage(err));
@@ -4826,7 +5455,10 @@ function createProviderHandlers(deps2) {
4826
5455
  try {
4827
5456
  const providers = await loadConfigProviders();
4828
5457
  const result = deleteKey(providers, providerId, label);
4829
- if (result.ok) await saveConfigProviders(providers);
5458
+ if (result.ok) {
5459
+ await saveConfigProviders(providers);
5460
+ broadcastSaved(providers);
5461
+ }
4830
5462
  sendResult2(ws, result.ok, result.message);
4831
5463
  } catch (err) {
4832
5464
  sendResult2(ws, false, errMessage(err));
@@ -4836,7 +5468,10 @@ function createProviderHandlers(deps2) {
4836
5468
  try {
4837
5469
  const providers = await loadConfigProviders();
4838
5470
  const result = setActiveKey(providers, providerId, label);
4839
- if (result.ok) await saveConfigProviders(providers);
5471
+ if (result.ok) {
5472
+ await saveConfigProviders(providers);
5473
+ broadcastSaved(providers);
5474
+ }
4840
5475
  sendResult2(ws, result.ok, result.message);
4841
5476
  } catch (err) {
4842
5477
  sendResult2(ws, false, errMessage(err));
@@ -4846,11 +5481,13 @@ function createProviderHandlers(deps2) {
4846
5481
  try {
4847
5482
  const providers = await loadConfigProviders();
4848
5483
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
4849
- if (result.ok) await saveConfigProviders(providers);
5484
+ if (result.ok) {
5485
+ await saveConfigProviders(providers);
5486
+ broadcastSaved(providers);
5487
+ }
4850
5488
  sendResult2(ws, result.ok, result.message);
4851
5489
  if (result.ok) {
4852
5490
  console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
4853
- broadcastSaved(providers);
4854
5491
  }
4855
5492
  } catch (err) {
4856
5493
  sendResult2(ws, false, errMessage(err));
@@ -4860,7 +5497,10 @@ function createProviderHandlers(deps2) {
4860
5497
  try {
4861
5498
  const providers = await loadConfigProviders();
4862
5499
  const result = removeProvider(providers, providerId);
4863
- if (result.ok) await saveConfigProviders(providers);
5500
+ if (result.ok) {
5501
+ await saveConfigProviders(providers);
5502
+ broadcastSaved(providers);
5503
+ }
4864
5504
  sendResult2(ws, result.ok, result.message);
4865
5505
  } catch (err) {
4866
5506
  sendResult2(ws, false, errMessage(err));
@@ -5043,7 +5683,7 @@ function createModeHandlers(ctx) {
5043
5683
 
5044
5684
  // src/server/project-handlers.ts
5045
5685
  import * as fs9 from "fs/promises";
5046
- import * as path10 from "path";
5686
+ import * as path11 from "path";
5047
5687
  import {
5048
5688
  DefaultSessionStore as DefaultSessionStore2,
5049
5689
  DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
@@ -5052,13 +5692,13 @@ import {
5052
5692
 
5053
5693
  // src/server/path-containment.ts
5054
5694
  import * as fs8 from "fs/promises";
5055
- import * as path9 from "path";
5695
+ import * as path10 from "path";
5056
5696
  function isPathInside(root, target) {
5057
- const relative3 = path9.relative(root, target);
5058
- return relative3 === "" || !relative3.startsWith("..") && !path9.isAbsolute(relative3);
5697
+ const relative3 = path10.relative(root, target);
5698
+ return relative3 === "" || !relative3.startsWith("..") && !path10.isAbsolute(relative3);
5059
5699
  }
5060
5700
  async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
5061
- const resolved = path9.resolve(projectRoot, inputPath);
5701
+ const resolved = path10.resolve(projectRoot, inputPath);
5062
5702
  let stat3;
5063
5703
  try {
5064
5704
  stat3 = await fs8.stat(resolved);
@@ -5100,7 +5740,7 @@ function createProjectHandlers(ctx) {
5100
5740
  }
5101
5741
  const { root: addRoot, name: displayName } = parsed.value;
5102
5742
  try {
5103
- const resolved = path10.resolve(addRoot);
5743
+ const resolved = path11.resolve(addRoot);
5104
5744
  await fs9.access(resolved);
5105
5745
  const stat3 = await fs9.stat(resolved);
5106
5746
  if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
@@ -5118,7 +5758,7 @@ function createProjectHandlers(ctx) {
5118
5758
  });
5119
5759
  return;
5120
5760
  }
5121
- const name2 = displayName?.trim() || path10.basename(resolved);
5761
+ const name2 = displayName?.trim() || path11.basename(resolved);
5122
5762
  const slug = generateProjectSlug(resolved);
5123
5763
  await ensureProjectDataDir(slug, ctx.globalConfigPath);
5124
5764
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5131,7 +5771,7 @@ function createProjectHandlers(ctx) {
5131
5771
  } catch (err) {
5132
5772
  send(ws, {
5133
5773
  type: "projects.added",
5134
- payload: { name: path10.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5774
+ payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
5135
5775
  });
5136
5776
  }
5137
5777
  },
@@ -5146,7 +5786,7 @@ function createProjectHandlers(ctx) {
5146
5786
  }
5147
5787
  const { root: selRoot, name: selName } = parsed.value;
5148
5788
  try {
5149
- const resolved = path10.resolve(selRoot);
5789
+ const resolved = path11.resolve(selRoot);
5150
5790
  try {
5151
5791
  await fs9.access(resolved);
5152
5792
  const stat3 = await fs9.stat(resolved);
@@ -5156,7 +5796,7 @@ function createProjectHandlers(ctx) {
5156
5796
  type: "projects.selected",
5157
5797
  payload: {
5158
5798
  root: selRoot,
5159
- name: selName || path10.basename(selRoot),
5799
+ name: selName || path11.basename(selRoot),
5160
5800
  message: `Cannot switch: ${errMessage(err)}`
5161
5801
  }
5162
5802
  });
@@ -5168,7 +5808,7 @@ function createProjectHandlers(ctx) {
5168
5808
  entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
5169
5809
  entry.lastWorkingDir = resolved;
5170
5810
  } else {
5171
- const name2 = selName?.trim() || path10.basename(resolved);
5811
+ const name2 = selName?.trim() || path11.basename(resolved);
5172
5812
  const slug = generateProjectSlug(resolved);
5173
5813
  manifest.projects.push({
5174
5814
  name: name2,
@@ -5207,8 +5847,8 @@ function createProjectHandlers(ctx) {
5207
5847
  });
5208
5848
  } catch {
5209
5849
  }
5210
- const newSessionsDir = path10.join(
5211
- path10.dirname(ctx.globalConfigPath),
5850
+ const newSessionsDir = path11.join(
5851
+ path11.dirname(ctx.globalConfigPath),
5212
5852
  "projects",
5213
5853
  switchSlug,
5214
5854
  "sessions"
@@ -5247,7 +5887,7 @@ function createProjectHandlers(ctx) {
5247
5887
  sessionId: newSession.id,
5248
5888
  projectSlug: switchSlug,
5249
5889
  projectRoot: resolved,
5250
- projectName: path10.basename(resolved),
5890
+ projectName: path11.basename(resolved),
5251
5891
  workingDir: resolved,
5252
5892
  clientType: "webui",
5253
5893
  pid: process.pid,
@@ -5259,8 +5899,8 @@ function createProjectHandlers(ctx) {
5259
5899
  type: "projects.selected",
5260
5900
  payload: {
5261
5901
  root: resolved,
5262
- name: selName || path10.basename(resolved),
5263
- message: `Switched to ${selName || path10.basename(resolved)}`
5902
+ name: selName || path11.basename(resolved),
5903
+ message: `Switched to ${selName || path11.basename(resolved)}`
5264
5904
  }
5265
5905
  });
5266
5906
  broadcast(ctx.clients, {
@@ -5280,7 +5920,7 @@ function createProjectHandlers(ctx) {
5280
5920
  type: "projects.selected",
5281
5921
  payload: {
5282
5922
  root: selRoot,
5283
- name: selName || path10.basename(selRoot),
5923
+ name: selName || path11.basename(selRoot),
5284
5924
  message: errMessage(err)
5285
5925
  }
5286
5926
  });
@@ -5311,7 +5951,7 @@ function createProjectHandlers(ctx) {
5311
5951
  }
5312
5952
 
5313
5953
  // src/server/session-handlers.ts
5314
- import * as path11 from "path";
5954
+ import * as path12 from "path";
5315
5955
  import {
5316
5956
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
5317
5957
  repairToolUseAdjacency,
@@ -5653,7 +6293,7 @@ function createSessionHandlers(ctx) {
5653
6293
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5654
6294
  const projectRoot = ctx.getProjectRoot();
5655
6295
  const rewinder = new DefaultSessionRewinder(
5656
- path11.join(projectRoot, ".wrongstack", "sessions"),
6296
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5657
6297
  projectRoot
5658
6298
  );
5659
6299
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
@@ -5668,7 +6308,7 @@ function createSessionHandlers(ctx) {
5668
6308
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
5669
6309
  const projectRoot = ctx.getProjectRoot();
5670
6310
  const rewinder = new DefaultSessionRewinder(
5671
- path11.join(projectRoot, ".wrongstack", "sessions"),
6311
+ path12.join(projectRoot, ".wrongstack", "sessions"),
5672
6312
  projectRoot
5673
6313
  );
5674
6314
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
@@ -5975,10 +6615,24 @@ async function handleAutoPhaseRoute(_ws, msg, handlers) {
5975
6615
  return true;
5976
6616
  }
5977
6617
 
6618
+ // src/server/specs-routes.ts
6619
+ async function handleSpecsRoute(_ws, msg, handlers) {
6620
+ if (!msg.type.startsWith("specs.")) return false;
6621
+ await handlers.handleMessage(msg);
6622
+ return true;
6623
+ }
6624
+
6625
+ // src/server/sdd-board-routes.ts
6626
+ async function handleSddBoardRoute(_ws, msg, handlers) {
6627
+ if (!msg.type.startsWith("sdd.board.")) return false;
6628
+ await handlers.handleMessage(msg);
6629
+ return true;
6630
+ }
6631
+
5978
6632
  // src/server/setup-events.ts
5979
6633
  import * as fs10 from "fs/promises";
5980
6634
  import { watch as fsWatch } from "fs";
5981
- import * as path12 from "path";
6635
+ import * as path13 from "path";
5982
6636
  function setupEvents(deps2) {
5983
6637
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
5984
6638
  const disposers = [];
@@ -6443,7 +7097,7 @@ function setupEvents(deps2) {
6443
7097
  if (wpaths?.projectStatus) {
6444
7098
  try {
6445
7099
  const statusFile = wpaths.projectStatus(e.projectHash);
6446
- const dir = path12.dirname(statusFile);
7100
+ const dir = path13.dirname(statusFile);
6447
7101
  await fs10.mkdir(dir, { recursive: true });
6448
7102
  await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
6449
7103
  } catch (err) {
@@ -6452,7 +7106,7 @@ function setupEvents(deps2) {
6452
7106
  }
6453
7107
  });
6454
7108
  if (wpaths?.projectStatus && wpaths.configDir) {
6455
- const projectsDir = path12.join(wpaths.configDir, "projects");
7109
+ const projectsDir = path13.join(wpaths.configDir, "projects");
6456
7110
  const knownProjectHashes = /* @__PURE__ */ new Set();
6457
7111
  const debounceTimers = /* @__PURE__ */ new Map();
6458
7112
  const DEBOUNCE_MS = 150;
@@ -6479,7 +7133,7 @@ function setupEvents(deps2) {
6479
7133
  );
6480
7134
  };
6481
7135
  const metricsInterval = setInterval(logWatcherMetrics, 6e4);
6482
- const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
7136
+ const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
6483
7137
  broadcast2(clients, { type: "client.status_update", payload: statusData });
6484
7138
  if (watcherMetrics) {
6485
7139
  watcherMetrics.broadcastsSent++;
@@ -6520,9 +7174,9 @@ function setupEvents(deps2) {
6520
7174
  if (eventType === "change") {
6521
7175
  if (filename == null) return;
6522
7176
  if (watcherMetrics) watcherMetrics.fileChangesDetected++;
6523
- const targetFile = path12.join(projectsDir, String(filename));
7177
+ const targetFile = path13.join(projectsDir, String(filename));
6524
7178
  if (targetFile.endsWith("status.json")) {
6525
- const projectHash2 = path12.basename(path12.dirname(targetFile));
7179
+ const projectHash2 = path13.basename(path13.dirname(targetFile));
6526
7180
  if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
6527
7181
  return;
6528
7182
  }
@@ -6580,7 +7234,7 @@ function setupEvents(deps2) {
6580
7234
  }
6581
7235
  });
6582
7236
  }
6583
- const globalRoot = globalConfigPath ? path12.dirname(globalConfigPath) : void 0;
7237
+ const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
6584
7238
  if (globalRoot) {
6585
7239
  const broadcastSessions = async () => {
6586
7240
  try {
@@ -6654,10 +7308,10 @@ function setupEvents(deps2) {
6654
7308
  // src/server/custom-context-modes.ts
6655
7309
  import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
6656
7310
  import * as fs11 from "fs/promises";
6657
- import * as path13 from "path";
7311
+ import * as path14 from "path";
6658
7312
  var STORE_FILENAME = "custom-context-modes.json";
6659
7313
  function storePath(wrongstackDir) {
6660
- return path13.join(wrongstackDir, STORE_FILENAME);
7314
+ return path14.join(wrongstackDir, STORE_FILENAME);
6661
7315
  }
6662
7316
  var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
6663
7317
  function createCustomModeStore(wrongstackDir) {
@@ -6789,12 +7443,12 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
6789
7443
 
6790
7444
  // src/server/shell-open.ts
6791
7445
  import * as fs12 from "fs/promises";
6792
- import * as path14 from "path";
7446
+ import * as path15 from "path";
6793
7447
  import { spawn as spawn2 } from "child_process";
6794
7448
  var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
6795
7449
  async function handleShellOpen(req, logger) {
6796
7450
  try {
6797
- const resolved = path14.resolve(req.path);
7451
+ const resolved = path15.resolve(req.path);
6798
7452
  await fs12.access(resolved);
6799
7453
  if (METACHAR_REGEX.test(resolved)) {
6800
7454
  return { success: false, message: "Path contains unsupported characters." };
@@ -6904,15 +7558,15 @@ async function handleGitChanges(ws, projectRoot) {
6904
7558
  if (!m) continue;
6905
7559
  const added = m[1] === "-" ? 0 : Number(m[1]);
6906
7560
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
6907
- let path16 = m[3] ?? "";
6908
- if (path16 === "") {
7561
+ let path17 = m[3] ?? "";
7562
+ if (path17 === "") {
6909
7563
  i += 1;
6910
- path16 = parts[i + 1] ?? parts[i] ?? "";
7564
+ path17 = parts[i + 1] ?? parts[i] ?? "";
6911
7565
  i += 1;
6912
7566
  }
6913
- if (!path16) continue;
6914
- const prev = counts.get(path16) ?? { added: 0, deleted: 0 };
6915
- counts.set(path16, { added: prev.added + added, deleted: prev.deleted + deleted });
7567
+ if (!path17) continue;
7568
+ const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
7569
+ counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
6916
7570
  }
6917
7571
  };
6918
7572
  parseNumstat(unstagedNumstat);
@@ -6924,7 +7578,7 @@ async function handleGitChanges(ws, projectRoot) {
6924
7578
  if (!rec || rec.length < 3) continue;
6925
7579
  const x = rec[0] ?? " ";
6926
7580
  const y = rec[1] ?? " ";
6927
- const path16 = rec.slice(3);
7581
+ const path17 = rec.slice(3);
6928
7582
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
6929
7583
  if (isRename) i += 1;
6930
7584
  let status;
@@ -6936,13 +7590,13 @@ async function handleGitChanges(ws, projectRoot) {
6936
7590
  else if (x === "D" || y === "D") status = "D";
6937
7591
  else status = "M";
6938
7592
  const staged = x !== " " && x !== "?";
6939
- let added = counts.get(path16)?.added ?? 0;
6940
- let deleted = counts.get(path16)?.deleted ?? 0;
7593
+ let added = counts.get(path17)?.added ?? 0;
7594
+ let deleted = counts.get(path17)?.deleted ?? 0;
6941
7595
  if (status === "?") {
6942
7596
  added = 0;
6943
7597
  deleted = 0;
6944
7598
  }
6945
- files.push({ path: path16, status, added, deleted, staged });
7599
+ files.push({ path: path17, status, added, deleted, staged });
6946
7600
  }
6947
7601
  send(ws, { type: "git.changes", payload: { files } });
6948
7602
  } catch (err) {
@@ -6953,21 +7607,21 @@ async function handleGitChanges(ws, projectRoot) {
6953
7607
  }
6954
7608
  }
6955
7609
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
6956
- async function handleGitDiff(ws, projectRoot, path16) {
7610
+ async function handleGitDiff(ws, projectRoot, path17) {
6957
7611
  const cwd = projectRoot || void 0;
6958
- const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path16, ...extra } });
6959
- if (!path16 || path16.includes("\0") || path16.includes("..") || nodePath.isAbsolute(path16)) {
7612
+ const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
7613
+ if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
6960
7614
  reply({ oldText: "", newText: "", error: "invalid path" });
6961
7615
  return;
6962
7616
  }
6963
7617
  try {
6964
7618
  const git = makeGit(cwd);
6965
7619
  const { readFile: readFile9 } = await import("fs/promises");
6966
- const { join: join11 } = await import("path");
6967
- const oldText = await git(["show", `HEAD:${path16}`]);
7620
+ const { join: join12 } = await import("path");
7621
+ const oldText = await git(["show", `HEAD:${path17}`]);
6968
7622
  let newText = "";
6969
7623
  try {
6970
- const abs = cwd ? join11(cwd, path16) : path16;
7624
+ const abs = cwd ? join12(cwd, path17) : path17;
6971
7625
  const buf = await readFile9(abs);
6972
7626
  if (buf.includes(0)) {
6973
7627
  reply({ oldText: "", newText: "", binary: true });
@@ -7178,6 +7832,7 @@ async function startWebUI(opts = {}) {
7178
7832
  toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
7179
7833
  toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
7180
7834
  toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
7835
+ applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
7181
7836
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7182
7837
  const mcpRegistry = new MCPRegistry({
7183
7838
  toolRegistry,
@@ -7221,7 +7876,7 @@ async function startWebUI(opts = {}) {
7221
7876
  sessionId: session.id,
7222
7877
  projectSlug: wpaths.projectSlug,
7223
7878
  projectRoot,
7224
- projectName: path15.basename(projectRoot),
7879
+ projectName: path16.basename(projectRoot),
7225
7880
  workingDir,
7226
7881
  clientType: "webui",
7227
7882
  pid: process.pid,
@@ -7241,7 +7896,7 @@ async function startWebUI(opts = {}) {
7241
7896
  const hqTelemetry = createHqPublisherFromEnv({
7242
7897
  clientKind: "webui",
7243
7898
  projectRoot,
7244
- projectName: path15.basename(projectRoot),
7899
+ projectName: path16.basename(projectRoot),
7245
7900
  appConfig: config,
7246
7901
  socketFactory: (url) => new WebSocket2(url)
7247
7902
  });
@@ -7253,7 +7908,7 @@ async function startWebUI(opts = {}) {
7253
7908
  events,
7254
7909
  sessionId: session.id,
7255
7910
  projectRoot,
7256
- projectName: path15.basename(projectRoot),
7911
+ projectName: path16.basename(projectRoot),
7257
7912
  globalRoot: wpaths.globalRoot,
7258
7913
  initialAgents: statusTracker?.getAgents(),
7259
7914
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -7309,9 +7964,9 @@ async function startWebUI(opts = {}) {
7309
7964
  };
7310
7965
  const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
7311
7966
  const skillInstaller = config.features.skills ? new SkillInstaller({
7312
- manifestPath: path15.join(wstackGlobalRoot2(), "installed-skills.json"),
7313
- projectSkillsDir: path15.join(projectRoot, ".wrongstack", "skills"),
7314
- globalSkillsDir: path15.join(wstackGlobalRoot2(), "skills"),
7967
+ manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
7968
+ projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
7969
+ globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
7315
7970
  projectHash: projectHash(projectRoot),
7316
7971
  skillLoader
7317
7972
  }) : void 0;
@@ -7415,6 +8070,8 @@ async function startWebUI(opts = {}) {
7415
8070
  context.meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
7416
8071
  context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
7417
8072
  context.meta["nextPrediction"] = config.nextPrediction ?? false;
8073
+ context.meta["fallbackModels"] = config.fallbackModels ?? [];
8074
+ context.meta["fallbackAuto"] = config.fallbackAuto !== false;
7418
8075
  context.meta["featureMcp"] = config.features.mcp !== false;
7419
8076
  context.meta["featurePlugins"] = config.features.plugins !== false;
7420
8077
  context.meta["featureMemory"] = config.features.memory !== false;
@@ -7472,7 +8129,9 @@ async function startWebUI(opts = {}) {
7472
8129
  "reasoningMode",
7473
8130
  "reasoningEffort",
7474
8131
  "reasoningPreserve",
7475
- "cacheTtl"
8132
+ "cacheTtl",
8133
+ "fallbackModels",
8134
+ "fallbackAuto"
7476
8135
  ];
7477
8136
  const prefSnapshot = () => {
7478
8137
  const snapshot = {};
@@ -7503,6 +8162,8 @@ async function startWebUI(opts = {}) {
7503
8162
  if (typeof payload["enhanceLanguage"] === "string") setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
7504
8163
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
7505
8164
  if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
8165
+ if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
8166
+ if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
7506
8167
  const FEATURE_MAP = {
7507
8168
  featureMcp: "mcp",
7508
8169
  featurePlugins: "plugins",
@@ -7783,6 +8444,29 @@ async function startWebUI(opts = {}) {
7783
8444
  events,
7784
8445
  projectRoot
7785
8446
  );
8447
+ const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
8448
+ const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
8449
+ const sddWizardHandler = new SddWizardWebSocketHandler(
8450
+ buildSddWizardDeps({
8451
+ agent,
8452
+ events,
8453
+ projectRoot,
8454
+ brain,
8455
+ subagentFactory: makeLightSubagentFactory({
8456
+ container,
8457
+ providerRegistry,
8458
+ toolRegistry,
8459
+ session,
8460
+ projectRoot
8461
+ }),
8462
+ paths: {
8463
+ projectSpecs: wpaths.projectSpecs,
8464
+ projectTaskGraphs: wpaths.projectTaskGraphs,
8465
+ projectSddBoards: wpaths.projectSddBoards,
8466
+ projectDir: wpaths.projectDir
8467
+ }
8468
+ })
8469
+ );
7786
8470
  const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
7787
8471
  const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
7788
8472
  const collabHandler = new CollaborationWebSocketHandler(
@@ -7822,7 +8506,7 @@ async function startWebUI(opts = {}) {
7822
8506
  inputCost,
7823
8507
  outputCost,
7824
8508
  cacheReadCost,
7825
- projectName: path15.basename(projectRoot) || projectRoot,
8509
+ projectName: path16.basename(projectRoot) || projectRoot,
7826
8510
  projectRoot,
7827
8511
  cwd: workingDir,
7828
8512
  mode: modeId,
@@ -7914,6 +8598,9 @@ async function startWebUI(opts = {}) {
7914
8598
  }));
7915
8599
  });
7916
8600
  autoPhaseHandler.addClient(ws);
8601
+ specsHandler.addClient(ws);
8602
+ sddBoardHandler.addClient(ws);
8603
+ sddWizardHandler.addClient(ws);
7917
8604
  worktreeHandler.addClient(ws);
7918
8605
  collabHandler.addClient(ws);
7919
8606
  terminalHandler.addClient(ws);
@@ -8038,21 +8725,21 @@ async function startWebUI(opts = {}) {
8038
8725
  });
8039
8726
  }
8040
8727
  async function touchProjectEntry(root, workDir) {
8041
- const resolved = path15.resolve(root);
8728
+ const resolved = path16.resolve(root);
8042
8729
  const manifest = await loadManifest(globalConfigPath);
8043
8730
  const now = (/* @__PURE__ */ new Date()).toISOString();
8044
- const existing = manifest.projects.find((p) => path15.resolve(p.root) === resolved);
8731
+ const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
8045
8732
  if (existing) {
8046
8733
  existing.lastSeen = now;
8047
- if (workDir) existing.lastWorkingDir = path15.resolve(workDir);
8734
+ if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
8048
8735
  } else {
8049
8736
  manifest.projects.push({
8050
- name: path15.basename(resolved),
8737
+ name: path16.basename(resolved),
8051
8738
  root: resolved,
8052
8739
  slug: generateProjectSlug(resolved),
8053
8740
  createdAt: now,
8054
8741
  lastSeen: now,
8055
- lastWorkingDir: workDir ? path15.resolve(workDir) : void 0
8742
+ lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
8056
8743
  });
8057
8744
  }
8058
8745
  await saveManifest(manifest, globalConfigPath);
@@ -8078,6 +8765,9 @@ async function startWebUI(opts = {}) {
8078
8765
  let mailboxRoutes;
8079
8766
  let brainRoutes;
8080
8767
  let autoPhaseRoutes;
8768
+ let specsRoutes;
8769
+ let sddBoardRoutes;
8770
+ let sddWizardRoutes;
8081
8771
  async function handleMessage(ws, _client, msg) {
8082
8772
  if (await handleProviderRoute(ws, msg, providerRoutes)) return;
8083
8773
  if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
@@ -8087,6 +8777,9 @@ async function startWebUI(opts = {}) {
8087
8777
  if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
8088
8778
  if (await handleBrainRoute(ws, msg, brainRoutes)) return;
8089
8779
  if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
8780
+ if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
8781
+ if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
8782
+ if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
8090
8783
  switch (msg.type) {
8091
8784
  // Collaboration messages short-circuit the user/agent flow.
8092
8785
  // They don't touch runLock, the agent loop, or the message queue —
@@ -8383,6 +9076,10 @@ async function startWebUI(opts = {}) {
8383
9076
  config.features.skills = payload["featureSkills"];
8384
9077
  if (typeof payload["featureModelsRegistry"] === "boolean")
8385
9078
  config.features.modelsRegistry = payload["featureModelsRegistry"];
9079
+ if (Array.isArray(payload["fallbackModels"]))
9080
+ config.fallbackModels = payload["fallbackModels"];
9081
+ if (typeof payload["fallbackAuto"] === "boolean")
9082
+ config.fallbackAuto = payload["fallbackAuto"];
8386
9083
  if (typeof payload["contextAutoCompact"] === "boolean") {
8387
9084
  if (payload["contextAutoCompact"] && autoCompactor) {
8388
9085
  pipelines.contextWindow.remove("AutoCompaction", { optional: true });
@@ -8445,22 +9142,7 @@ async function startWebUI(opts = {}) {
8445
9142
  const saved = await providerHandlers.loadConfigProviders();
8446
9143
  send(ws, {
8447
9144
  type: "providers.saved",
8448
- payload: {
8449
- providers: Object.entries(saved).map(([id, cfg]) => {
8450
- const keys = normalizeKeys(cfg);
8451
- return {
8452
- id,
8453
- family: cfg.family ?? id,
8454
- baseUrl: cfg.baseUrl,
8455
- apiKeys: keys.map((k) => ({
8456
- label: k.label,
8457
- maskedKey: maskedKey(k.apiKey),
8458
- isActive: k.label === cfg.activeKey,
8459
- createdAt: k.createdAt
8460
- }))
8461
- };
8462
- })
8463
- }
9145
+ payload: { providers: projectSavedProviders(saved) }
8464
9146
  });
8465
9147
  },
8466
9148
  listProviderModels: async (ws, msg) => {
@@ -8670,7 +9352,7 @@ async function startWebUI(opts = {}) {
8670
9352
  sendResult2(ws, false, parsed.message);
8671
9353
  return;
8672
9354
  }
8673
- return handleMailboxMessages(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9355
+ return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8674
9356
  },
8675
9357
  agents: (ws, msg) => {
8676
9358
  const parsed = validateMailboxAgentsPayload(msg.payload);
@@ -8678,16 +9360,16 @@ async function startWebUI(opts = {}) {
8678
9360
  sendResult2(ws, false, parsed.message);
8679
9361
  return;
8680
9362
  }
8681
- return handleMailboxAgents(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9363
+ return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8682
9364
  },
8683
- clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }),
9365
+ clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
8684
9366
  purge: (ws, msg) => {
8685
9367
  const parsed = validateMailboxPurgePayload(msg.payload);
8686
9368
  if (!parsed.ok) {
8687
9369
  sendResult2(ws, false, parsed.message);
8688
9370
  return;
8689
9371
  }
8690
- return handleMailboxPurge(ws, { projectRoot, globalRoot: path15.dirname(globalConfigPath) }, parsed.value);
9372
+ return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
8691
9373
  }
8692
9374
  };
8693
9375
  brainRoutes = {
@@ -8734,6 +9416,15 @@ async function startWebUI(opts = {}) {
8734
9416
  autoPhaseRoutes = {
8735
9417
  handleMessage: (msg) => autoPhaseHandler.handleMessage(msg)
8736
9418
  };
9419
+ specsRoutes = {
9420
+ handleMessage: (msg) => specsHandler.handleMessage(msg)
9421
+ };
9422
+ sddBoardRoutes = {
9423
+ handleMessage: (msg) => sddBoardHandler.handleMessage(msg)
9424
+ };
9425
+ sddWizardRoutes = {
9426
+ handleMessage: (msg) => sddWizardHandler.handleMessage(msg)
9427
+ };
8737
9428
  const watcherMetrics = {
8738
9429
  fileChangesDetected: 0,
8739
9430
  filesProcessed: 0,
@@ -8746,7 +9437,7 @@ async function startWebUI(opts = {}) {
8746
9437
  };
8747
9438
  const httpServer = createHttpServer({
8748
9439
  host: wsHost,
8749
- distDir: path15.resolve(import.meta.dirname, "../../dist"),
9440
+ distDir: path16.resolve(import.meta.dirname, "../../dist"),
8750
9441
  wsPort,
8751
9442
  globalRoot: wpaths.globalRoot,
8752
9443
  apiToken: wsToken,
@@ -8755,7 +9446,7 @@ async function startWebUI(opts = {}) {
8755
9446
  void fleetBroadcast?.();
8756
9447
  }
8757
9448
  });
8758
- const registryBaseDir = path15.dirname(globalConfigPath);
9449
+ const registryBaseDir = path16.dirname(globalConfigPath);
8759
9450
  httpServer.listen(httpPort, wsHost, () => {
8760
9451
  const openUrl = `http://${wsHost}:${httpPort}`;
8761
9452
  console.log(`[WebUI] HTTP server running on ${openUrl}`);
@@ -8767,7 +9458,7 @@ async function startWebUI(opts = {}) {
8767
9458
  wsPort,
8768
9459
  host: wsHost,
8769
9460
  projectRoot,
8770
- projectName: path15.basename(projectRoot) || projectRoot,
9461
+ projectName: path16.basename(projectRoot) || projectRoot,
8771
9462
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8772
9463
  url: `http://${wsHost}:${httpPort}`
8773
9464
  },