@wrongstack/webui-server 0.296.4 → 0.298.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.
Files changed (200) hide show
  1. package/dist/index.js +2138 -793
  2. package/dist/protocol/client-conversation.d.ts +1 -1
  3. package/dist/protocol/index.js +7 -1
  4. package/dist/protocol/projections.d.ts +5 -1
  5. package/dist/protocol/registry.d.ts +2 -2
  6. package/dist/protocol/server-conversation.d.ts +1 -1
  7. package/dist/server/backend-services.d.ts +8 -2
  8. package/dist/server/codemap-cache.d.ts +1 -0
  9. package/dist/server/collaboration-ws-handler.d.ts +2 -2
  10. package/dist/server/connection-lifecycle.d.ts +13 -0
  11. package/dist/server/connections-health-route.d.ts +7 -1
  12. package/dist/server/context-editor.d.ts +1 -2
  13. package/dist/server/embedded-lifecycle.d.ts +2 -0
  14. package/dist/server/entry.js +1760 -744
  15. package/dist/server/frontend-static-serve.d.ts +7 -0
  16. package/dist/server/goal-ws-handler.d.ts +3 -1
  17. package/dist/server/governance-runtime.d.ts +30 -0
  18. package/dist/server/http-server.d.ts +7 -0
  19. package/dist/server/kanban-supervisor.d.ts +6 -0
  20. package/dist/server/mailbox-handlers.d.ts +2 -1
  21. package/dist/server/mailbox-routes.d.ts +1 -0
  22. package/dist/server/mcp-handlers.d.ts +3 -1
  23. package/dist/server/memory-diagnostics.d.ts +69 -0
  24. package/dist/server/memory-handlers.d.ts +3 -3
  25. package/dist/server/pending-confirms.d.ts +9 -0
  26. package/dist/server/sdd-board-ws-handler.d.ts +15 -8
  27. package/dist/server/sdd-wizard-wiring.d.ts +5 -6
  28. package/dist/server/sdd-wizard-ws-handler.d.ts +18 -6
  29. package/dist/server/specs-ws-handler.d.ts +1 -0
  30. package/dist/server/types.d.ts +5 -1
  31. package/dist/server/worktree-ws-handler.d.ts +30 -2
  32. package/dist/server/ws-auth.d.ts +24 -0
  33. package/dist/server/ws-payload-validation.d.ts +26 -0
  34. package/dist/server/ws-utils.d.ts +12 -1
  35. package/package.json +12 -11
  36. package/dist/index.d.ts.map +0 -1
  37. package/dist/index.js.map +0 -7
  38. package/dist/protocol/client-conversation.d.ts.map +0 -1
  39. package/dist/protocol/client-integrations.d.ts.map +0 -1
  40. package/dist/protocol/client-operations.d.ts.map +0 -1
  41. package/dist/protocol/client-workspace.d.ts.map +0 -1
  42. package/dist/protocol/connection-fsm.d.ts.map +0 -1
  43. package/dist/protocol/decoder.d.ts.map +0 -1
  44. package/dist/protocol/index.d.ts.map +0 -1
  45. package/dist/protocol/index.js.map +0 -7
  46. package/dist/protocol/projections.d.ts.map +0 -1
  47. package/dist/protocol/registry.d.ts.map +0 -1
  48. package/dist/protocol/replay-payload.d.ts.map +0 -1
  49. package/dist/protocol/server-conversation.d.ts.map +0 -1
  50. package/dist/protocol/server-integrations.d.ts.map +0 -1
  51. package/dist/protocol/server-operations.d.ts.map +0 -1
  52. package/dist/protocol/server-workspace.d.ts.map +0 -1
  53. package/dist/protocol/types.d.ts.map +0 -1
  54. package/dist/protocol/version.d.ts.map +0 -1
  55. package/dist/server/agent-roster-handlers.d.ts.map +0 -1
  56. package/dist/server/agent-roster-routes.d.ts.map +0 -1
  57. package/dist/server/autonomy-routes.d.ts.map +0 -1
  58. package/dist/server/backend-services.d.ts.map +0 -1
  59. package/dist/server/boot.d.ts.map +0 -1
  60. package/dist/server/brain-handlers.d.ts.map +0 -1
  61. package/dist/server/brain-routes.d.ts.map +0 -1
  62. package/dist/server/chronicle-routes.d.ts.map +0 -1
  63. package/dist/server/client-presence.d.ts.map +0 -1
  64. package/dist/server/client-transport-routes.d.ts.map +0 -1
  65. package/dist/server/codebase-index-server-control.d.ts.map +0 -1
  66. package/dist/server/codebase-indexing.d.ts.map +0 -1
  67. package/dist/server/codemap-cache.d.ts.map +0 -1
  68. package/dist/server/codemap-handlers.d.ts.map +0 -1
  69. package/dist/server/codemap-telemetry.d.ts.map +0 -1
  70. package/dist/server/collaboration-ws-handler.d.ts.map +0 -1
  71. package/dist/server/completion-handlers.d.ts.map +0 -1
  72. package/dist/server/completion-routes.d.ts.map +0 -1
  73. package/dist/server/config-doctor.d.ts.map +0 -1
  74. package/dist/server/connection-handler.d.ts.map +0 -1
  75. package/dist/server/connection-lifecycle.d.ts.map +0 -1
  76. package/dist/server/connections-health-route.d.ts.map +0 -1
  77. package/dist/server/content-routes.d.ts.map +0 -1
  78. package/dist/server/context-editor.d.ts.map +0 -1
  79. package/dist/server/context-meta.d.ts.map +0 -1
  80. package/dist/server/conversation-operations.d.ts.map +0 -1
  81. package/dist/server/conversation-routes.d.ts.map +0 -1
  82. package/dist/server/custom-context-modes.d.ts.map +0 -1
  83. package/dist/server/deadcode-handlers.d.ts.map +0 -1
  84. package/dist/server/design-handlers.d.ts.map +0 -1
  85. package/dist/server/discover-mailbox-bridge.d.ts.map +0 -1
  86. package/dist/server/embedded-host-adapters.d.ts.map +0 -1
  87. package/dist/server/embedded-lifecycle.d.ts.map +0 -1
  88. package/dist/server/embedded-message-router.d.ts.map +0 -1
  89. package/dist/server/entry.d.ts.map +0 -1
  90. package/dist/server/entry.js.map +0 -7
  91. package/dist/server/eternal-iteration-broadcast.d.ts.map +0 -1
  92. package/dist/server/file-handlers.d.ts.map +0 -1
  93. package/dist/server/file-picker.d.ts.map +0 -1
  94. package/dist/server/frontend-static-serve.d.ts.map +0 -1
  95. package/dist/server/git-handlers.d.ts.map +0 -1
  96. package/dist/server/git-process.d.ts.map +0 -1
  97. package/dist/server/goal-handlers.d.ts.map +0 -1
  98. package/dist/server/goal-routes.d.ts.map +0 -1
  99. package/dist/server/goal-snapshot-routes.d.ts.map +0 -1
  100. package/dist/server/goal-ws-handler.d.ts.map +0 -1
  101. package/dist/server/handlers/index.d.ts.map +0 -1
  102. package/dist/server/handlers/worklist-handlers.d.ts.map +0 -1
  103. package/dist/server/handlers.js.map +0 -7
  104. package/dist/server/host-routes.d.ts.map +0 -1
  105. package/dist/server/http-server/analytics-handler.d.ts.map +0 -1
  106. package/dist/server/http-server/api-handlers.d.ts.map +0 -1
  107. package/dist/server/http-server.d.ts.map +0 -1
  108. package/dist/server/index.d.ts.map +0 -1
  109. package/dist/server/instance-registry.d.ts.map +0 -1
  110. package/dist/server/introspection-routes.d.ts.map +0 -1
  111. package/dist/server/kanban-board-watcher.d.ts.map +0 -1
  112. package/dist/server/kanban-daemon-subscriber.d.ts.map +0 -1
  113. package/dist/server/kanban-decomposition-routes.d.ts.map +0 -1
  114. package/dist/server/kanban-dispatch.d.ts.map +0 -1
  115. package/dist/server/kanban-host-routes.d.ts.map +0 -1
  116. package/dist/server/kanban-route-helpers.d.ts.map +0 -1
  117. package/dist/server/kanban-route-pagination.d.ts.map +0 -1
  118. package/dist/server/kanban-route-protocol.d.ts.map +0 -1
  119. package/dist/server/kanban-routes.d.ts.map +0 -1
  120. package/dist/server/kanban-run-mirror.d.ts.map +0 -1
  121. package/dist/server/kanban-supervisor.d.ts.map +0 -1
  122. package/dist/server/kanban-task-routes.d.ts.map +0 -1
  123. package/dist/server/lifecycle.d.ts.map +0 -1
  124. package/dist/server/mailbox-handlers.d.ts.map +0 -1
  125. package/dist/server/mailbox-routes.d.ts.map +0 -1
  126. package/dist/server/mcp-handlers.d.ts.map +0 -1
  127. package/dist/server/mcp-routes.d.ts.map +0 -1
  128. package/dist/server/memory-handlers.d.ts.map +0 -1
  129. package/dist/server/memory-routes.d.ts.map +0 -1
  130. package/dist/server/message-dispatcher.d.ts.map +0 -1
  131. package/dist/server/mode-handlers.d.ts.map +0 -1
  132. package/dist/server/mode-operations.d.ts.map +0 -1
  133. package/dist/server/mode-routes.d.ts.map +0 -1
  134. package/dist/server/model-auto-discovery.d.ts.map +0 -1
  135. package/dist/server/model-catalog-search.d.ts.map +0 -1
  136. package/dist/server/model-catalog.d.ts.map +0 -1
  137. package/dist/server/model-operations.d.ts.map +0 -1
  138. package/dist/server/network-info.d.ts.map +0 -1
  139. package/dist/server/open-browser.d.ts.map +0 -1
  140. package/dist/server/path-containment.d.ts.map +0 -1
  141. package/dist/server/pending-confirms.d.ts.map +0 -1
  142. package/dist/server/port-utils.d.ts.map +0 -1
  143. package/dist/server/pre-context-services.d.ts.map +0 -1
  144. package/dist/server/pref-helpers.d.ts.map +0 -1
  145. package/dist/server/prefs-handlers.d.ts.map +0 -1
  146. package/dist/server/prefs-routes.d.ts.map +0 -1
  147. package/dist/server/privileged-actions.d.ts.map +0 -1
  148. package/dist/server/process-handlers.d.ts.map +0 -1
  149. package/dist/server/process-routes.d.ts.map +0 -1
  150. package/dist/server/project-handlers.d.ts.map +0 -1
  151. package/dist/server/project-routes.d.ts.map +0 -1
  152. package/dist/server/projects-manifest.d.ts.map +0 -1
  153. package/dist/server/prompts-handlers.d.ts.map +0 -1
  154. package/dist/server/provider-config-io.d.ts.map +0 -1
  155. package/dist/server/provider-config-standalone.d.ts.map +0 -1
  156. package/dist/server/provider-handlers.d.ts.map +0 -1
  157. package/dist/server/provider-keys.d.ts.map +0 -1
  158. package/dist/server/provider-routes.d.ts.map +0 -1
  159. package/dist/server/provider-store.d.ts.map +0 -1
  160. package/dist/server/route-family-dispatcher.d.ts.map +0 -1
  161. package/dist/server/routes.d.ts.map +0 -1
  162. package/dist/server/sdd-board-routes.d.ts.map +0 -1
  163. package/dist/server/sdd-board-ws-handler.d.ts.map +0 -1
  164. package/dist/server/sdd-wizard-routes.d.ts.map +0 -1
  165. package/dist/server/sdd-wizard-wiring.d.ts.map +0 -1
  166. package/dist/server/sdd-wizard-ws-handler.d.ts.map +0 -1
  167. package/dist/server/server-runtime.d.ts.map +0 -1
  168. package/dist/server/session-handlers.d.ts.map +0 -1
  169. package/dist/server/session-history.d.ts.map +0 -1
  170. package/dist/server/session-routes.d.ts.map +0 -1
  171. package/dist/server/setup-event-projection.d.ts.map +0 -1
  172. package/dist/server/setup-events-core-watchers.d.ts.map +0 -1
  173. package/dist/server/setup-events-fleet-broadcaster.d.ts.map +0 -1
  174. package/dist/server/setup-events-provider-handlers.d.ts.map +0 -1
  175. package/dist/server/setup-events-session-helpers.d.ts.map +0 -1
  176. package/dist/server/setup-events-status-watcher.d.ts.map +0 -1
  177. package/dist/server/setup-events-watcher.d.ts.map +0 -1
  178. package/dist/server/setup-events.d.ts.map +0 -1
  179. package/dist/server/setup-screen.d.ts.map +0 -1
  180. package/dist/server/shell-git-routes.d.ts.map +0 -1
  181. package/dist/server/shell-open.d.ts.map +0 -1
  182. package/dist/server/skills-handlers.d.ts.map +0 -1
  183. package/dist/server/specs-routes.d.ts.map +0 -1
  184. package/dist/server/specs-ws-handler.d.ts.map +0 -1
  185. package/dist/server/standalone-session-identity.d.ts.map +0 -1
  186. package/dist/server/start-webui.d.ts.map +0 -1
  187. package/dist/server/stream-coalescer.d.ts.map +0 -1
  188. package/dist/server/techstack-handlers.d.ts.map +0 -1
  189. package/dist/server/terminal-ws-handler.d.ts.map +0 -1
  190. package/dist/server/token-estimator.d.ts.map +0 -1
  191. package/dist/server/types.d.ts.map +0 -1
  192. package/dist/server/usage-cost.d.ts.map +0 -1
  193. package/dist/server/worklist-routes.d.ts.map +0 -1
  194. package/dist/server/worktree-routes.d.ts.map +0 -1
  195. package/dist/server/worktree-ws-handler.d.ts.map +0 -1
  196. package/dist/server/ws-auth.d.ts.map +0 -1
  197. package/dist/server/ws-payload-preferences.d.ts.map +0 -1
  198. package/dist/server/ws-payload-validation.d.ts.map +0 -1
  199. package/dist/server/ws-utils.d.ts.map +0 -1
  200. package/dist/server/zip.d.ts.map +0 -1
@@ -6,9 +6,13 @@ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/ty
6
6
  import { getProcessRegistry } from "@wrongstack/tools";
7
7
 
8
8
  // src/server/pending-confirms.ts
9
+ function isDestructivePendingConfirm(confirm) {
10
+ return confirm.riskTier === "destructive" || confirm.decisionSource === "yolo_destructive";
11
+ }
9
12
  function resolveYoloEligiblePendingConfirms(pendingConfirms) {
10
13
  for (const [id, confirm] of pendingConfirms) {
11
14
  if (confirm.boundaryReason) continue;
15
+ if (isDestructivePendingConfirm(confirm)) continue;
12
16
  pendingConfirms.delete(id);
13
17
  confirm.resolve("yes");
14
18
  }
@@ -72,7 +76,18 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
72
76
  "showModelReasoning",
73
77
  // Display-only toggles (purely visual, persisted in localStorage via Zustand).
74
78
  "groupToolCalls",
75
- "showThinkingLogs"
79
+ "showThinkingLogs",
80
+ // v11 Display parity: agent-swarm panel + inverse fsAccess flag.
81
+ "showAgentSwarmPanel",
82
+ "allowOutsideProjectRoot",
83
+ // v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
84
+ // includes codebase-index symbols, and SAGE memory-inject blocks are
85
+ // surfaced in tool results. Both are pure WebUI-display toggles mirrored
86
+ // against the TUI's settings picker; the server mirrors them through
87
+ // `prefs.update` exactly so the panel does not error with
88
+ // "unknown preference key".
89
+ "readSymbols",
90
+ "showSageMemoryInject"
76
91
  ]);
77
92
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set([
78
93
  "fallbackModels",
@@ -93,14 +108,39 @@ var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
93
108
  "maxIterations",
94
109
  "maxConcurrent",
95
110
  "enhanceDelayMs",
111
+ "enhanceCountdownMs",
96
112
  "tgLongToolMs",
97
113
  "breakerAutoKillResetMs",
98
114
  // Chimera + auto-review numeric knobs
99
115
  "chimeraMaxFiles",
100
116
  "autoReviewDebounceMs",
101
117
  "autoReviewMaxFilesPerBatch",
102
- "autoReviewMaxConcurrentReviews"
118
+ "autoReviewMaxConcurrentReviews",
119
+ // v13 Display parity (TUI SettingsPicker fields 41, 44, 21): the
120
+ // pre-refine grace countdown (seconds), the SAGE memory-inject relation
121
+ // floor (0..1), and the minimum file count for the multi-diff summary
122
+ // footer. All three are driven by the WebUI sliders in DisplaySection
123
+ // and previously tripped the "unknown preference key" rejection.
124
+ "preRefineSeconds",
125
+ "sageMemoryInjectThreshold",
126
+ "multiDiffSummaryThreshold"
103
127
  ]);
128
+ var NUMBER_PREF_BOUNDS = {
129
+ // Iteration / concurrency — must be at least 1 to make progress.
130
+ maxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
131
+ maxConcurrent: { min: 1, max: Number.POSITIVE_INFINITY },
132
+ autoProceedMaxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
133
+ chimeraMaxFiles: { min: 1, max: Number.POSITIVE_INFINITY },
134
+ autoReviewMaxFilesPerBatch: { min: 1, max: Number.POSITIVE_INFINITY },
135
+ autoReviewMaxConcurrentReviews: { min: 1, max: Number.POSITIVE_INFINITY },
136
+ // Counts (must be >= 1 to render a footer at all).
137
+ multiDiffSummaryThreshold: { min: 1, max: Number.POSITIVE_INFINITY },
138
+ preRefineSeconds: { min: 0, max: Number.POSITIVE_INFINITY },
139
+ // Probability — must lie in [0, 1].
140
+ sageMemoryInjectThreshold: { min: 0, max: 1 },
141
+ // Debounce / delay — non-negative ms.
142
+ autoReviewDebounceMs: { min: 0, max: Number.POSITIVE_INFINITY }
143
+ };
104
144
  var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
105
145
  "hqUrl",
106
146
  "hqToken",
@@ -135,85 +175,85 @@ var ENUM_PREF_KEYS = {
135
175
  autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
136
176
  fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"])
137
177
  };
138
- function validateModelRuntimeValue(modelRuntime, path29) {
178
+ function validateModelRuntimeValue(modelRuntime, path30) {
139
179
  const reasoning = modelRuntime["reasoning"];
140
180
  if (reasoning !== void 0) {
141
- if (!isRecord(reasoning)) return `${path29}.reasoning must be an object when provided`;
181
+ if (!isRecord(reasoning)) return `${path30}.reasoning must be an object when provided`;
142
182
  const mode = reasoning["mode"];
143
183
  const effort = reasoning["effort"];
144
184
  const preserve = reasoning["preserve"];
145
185
  if (mode !== void 0 && (typeof mode !== "string" || !REASONING_MODE_VALUES.has(mode))) {
146
- return `${path29}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
186
+ return `${path30}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
147
187
  }
148
188
  if (effort !== void 0 && (typeof effort !== "string" || !REASONING_EFFORT_VALUES.has(effort))) {
149
- return `${path29}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
189
+ return `${path30}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
150
190
  }
151
191
  if (preserve !== void 0 && typeof preserve !== "boolean") {
152
- return `${path29}.reasoning.preserve must be a boolean when provided`;
192
+ return `${path30}.reasoning.preserve must be a boolean when provided`;
153
193
  }
154
194
  }
155
195
  const cache2 = modelRuntime["cache"];
156
196
  if (cache2 !== void 0) {
157
- if (!isRecord(cache2)) return `${path29}.cache must be an object when provided`;
197
+ if (!isRecord(cache2)) return `${path30}.cache must be an object when provided`;
158
198
  const ttl = cache2["ttl"];
159
199
  if (ttl !== void 0 && (typeof ttl !== "string" || !CACHE_TTL_VALUES.has(ttl) || ttl === "default")) {
160
- return `${path29}.cache.ttl must be one of: 5m, 1h`;
200
+ return `${path30}.cache.ttl must be one of: 5m, 1h`;
161
201
  }
162
202
  }
163
203
  const parameters = modelRuntime["parameters"];
164
204
  if (parameters !== void 0 && !isRecord(parameters)) {
165
- return `${path29}.parameters must be an object when provided`;
205
+ return `${path30}.parameters must be an object when provided`;
166
206
  }
167
207
  return null;
168
208
  }
169
- function validateModelBlackoutRule(rule, path29) {
209
+ function validateModelBlackoutRule(rule, path30) {
170
210
  const id = rule["id"];
171
211
  if (typeof id !== "string" || id.trim().length === 0) {
172
- return `${path29}.id must be a non-empty string`;
212
+ return `${path30}.id must be a non-empty string`;
173
213
  }
174
214
  const start = rule["start"];
175
215
  if (typeof start !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(start)) {
176
- return `${path29}.start must be a string in HH:mm (00:00-23:59) format`;
216
+ return `${path30}.start must be a string in HH:mm (00:00-23:59) format`;
177
217
  }
178
218
  const end = rule["end"];
179
219
  if (typeof end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(end)) {
180
- return `${path29}.end must be a string in HH:mm (00:00-23:59) format`;
220
+ return `${path30}.end must be a string in HH:mm (00:00-23:59) format`;
181
221
  }
182
222
  if (rule["enabled"] !== void 0 && typeof rule["enabled"] !== "boolean") {
183
- return `${path29}.enabled must be a boolean when provided`;
223
+ return `${path30}.enabled must be a boolean when provided`;
184
224
  }
185
225
  if (rule["provider"] !== void 0 && typeof rule["provider"] !== "string") {
186
- return `${path29}.provider must be a string when provided`;
226
+ return `${path30}.provider must be a string when provided`;
187
227
  }
188
228
  if (rule["model"] !== void 0 && typeof rule["model"] !== "string") {
189
- return `${path29}.model must be a string when provided`;
229
+ return `${path30}.model must be a string when provided`;
190
230
  }
191
231
  if (rule["days"] !== void 0) {
192
- if (!Array.isArray(rule["days"])) return `${path29}.days must be an array when provided`;
232
+ if (!Array.isArray(rule["days"])) return `${path30}.days must be an array when provided`;
193
233
  const seen = /* @__PURE__ */ new Set();
194
234
  for (const d of rule["days"]) {
195
235
  if (typeof d !== "number" || !Number.isInteger(d) || d < 0 || d > 6) {
196
- return `${path29}.days elements must be integers 0-6 when provided`;
236
+ return `${path30}.days elements must be integers 0-6 when provided`;
197
237
  }
198
- if (seen.has(d)) return `${path29}.days contains duplicate day: ${d}`;
238
+ if (seen.has(d)) return `${path30}.days contains duplicate day: ${d}`;
199
239
  seen.add(d);
200
240
  }
201
241
  }
202
242
  if (rule["timezone"] !== void 0) {
203
243
  if (typeof rule["timezone"] !== "string") {
204
- return `${path29}.timezone must be a string when provided`;
244
+ return `${path30}.timezone must be a string when provided`;
205
245
  }
206
246
  try {
207
247
  Intl.DateTimeFormat(void 0, { timeZone: rule["timezone"] });
208
248
  } catch {
209
- return `${path29}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
249
+ return `${path30}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
210
250
  }
211
251
  }
212
252
  if (rule["label"] !== void 0 && typeof rule["label"] !== "string") {
213
- return `${path29}.label must be a string when provided`;
253
+ return `${path30}.label must be a string when provided`;
214
254
  }
215
255
  if (rule["mode"] !== void 0 && rule["mode"] !== "blackout" && rule["mode"] !== "allow_only") {
216
- return `${path29}.mode must be 'blackout' or 'allow_only' when provided`;
256
+ return `${path30}.mode must be 'blackout' or 'allow_only' when provided`;
217
257
  }
218
258
  return null;
219
259
  }
@@ -222,7 +262,15 @@ function validatePreferenceValue(key, value) {
222
262
  return typeof value === "boolean" ? null : `prefs.update payload.${key} must be a boolean`;
223
263
  }
224
264
  if (NUMBER_PREF_KEYS.has(key)) {
225
- return typeof value === "number" && Number.isFinite(value) ? null : `prefs.update payload.${key} must be a finite number`;
265
+ if (typeof value !== "number" || !Number.isFinite(value)) {
266
+ return `prefs.update payload.${key} must be a finite number`;
267
+ }
268
+ const bounds = NUMBER_PREF_BOUNDS[key];
269
+ if (bounds && (value < bounds.min || value > bounds.max)) {
270
+ const maxStr = bounds.max === Number.POSITIVE_INFINITY ? "\u221E" : String(bounds.max);
271
+ return `prefs.update payload.${key} must be in [${bounds.min}, ${maxStr}]`;
272
+ }
273
+ return null;
226
274
  }
227
275
  if (STRING_PREF_KEYS.has(key)) {
228
276
  return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
@@ -246,9 +294,16 @@ function validatePreferenceValue(key, value) {
246
294
  return null;
247
295
  }
248
296
  if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
249
- return isRecord(value) && Object.values(value).every(
297
+ if (!isRecord(value) || !Object.values(value).every(
250
298
  (v) => Array.isArray(v) && v.every((item) => typeof item === "string")
251
- ) ? null : `prefs.update payload.${key} must be an object of string arrays`;
299
+ )) {
300
+ return `prefs.update payload.${key} must be an object of string arrays`;
301
+ }
302
+ const badKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
303
+ if (badKey) {
304
+ return `prefs.update payload.${key} contains a forbidden key: ${badKey}`;
305
+ }
306
+ return null;
252
307
  }
253
308
  if (BOOLEAN_RECORD_PREF_KEYS.has(key)) {
254
309
  if (!isRecord(value) || !Object.values(value).every((v) => typeof v === "boolean")) {
@@ -262,6 +317,10 @@ function validatePreferenceValue(key, value) {
262
317
  }
263
318
  if (MODEL_MATRIX_PREF_KEYS.has(key)) {
264
319
  if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
320
+ const badMatrixKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
321
+ if (badMatrixKey) {
322
+ return `prefs.update payload.${key} contains a forbidden key: ${badMatrixKey}`;
323
+ }
265
324
  for (const entry of Object.values(value)) {
266
325
  if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
267
326
  const provider = entry["provider"];
@@ -386,6 +445,37 @@ function validateMailboxMessagesPayload(payload) {
386
445
  }
387
446
  };
388
447
  }
448
+ var MAILBOX_ACTIONS = /* @__PURE__ */ new Set(["mark-read", "acknowledge", "reopen", "soft-delete"]);
449
+ function validateMailboxActionPayload(payload) {
450
+ if (!isRecord2(payload)) {
451
+ return { ok: false, message: "mailbox.action payload must be an object" };
452
+ }
453
+ const requestId = payload["requestId"];
454
+ const mailId = payload["mailId"];
455
+ const action = payload["action"];
456
+ const readerId = payload["readerId"];
457
+ if (typeof requestId !== "string" || requestId.trim().length === 0) {
458
+ return { ok: false, message: "mailbox.action payload.requestId must be a non-empty string" };
459
+ }
460
+ if (typeof mailId !== "string" || mailId.trim().length === 0) {
461
+ return { ok: false, message: "mailbox.action payload.mailId must be a non-empty string" };
462
+ }
463
+ if (typeof action !== "string" || !MAILBOX_ACTIONS.has(action)) {
464
+ return { ok: false, message: "mailbox.action payload.action must be a supported action" };
465
+ }
466
+ if (typeof readerId !== "string" || readerId.trim().length === 0) {
467
+ return { ok: false, message: "mailbox.action payload.readerId must be a non-empty string" };
468
+ }
469
+ return {
470
+ ok: true,
471
+ value: {
472
+ requestId: requestId.trim(),
473
+ mailId: mailId.trim(),
474
+ action,
475
+ readerId: readerId.trim()
476
+ }
477
+ };
478
+ }
389
479
  var MAILBOX_SEND_TYPES = /* @__PURE__ */ new Set([
390
480
  "note",
391
481
  "ask",
@@ -402,6 +492,7 @@ function validateMailboxSendPayload(payload) {
402
492
  return { ok: false, message: "mailbox.send payload must be an object" };
403
493
  }
404
494
  const requestId = payload["requestId"];
495
+ const rawFrom = payload["from"];
405
496
  const rawTo = payload["to"];
406
497
  const rawType = payload["type"];
407
498
  const rawAudience = payload["audience"];
@@ -412,6 +503,9 @@ function validateMailboxSendPayload(payload) {
412
503
  if (typeof requestId !== "string" || requestId.trim().length === 0) {
413
504
  return { ok: false, message: "mailbox.send payload.requestId must be a non-empty string" };
414
505
  }
506
+ if (rawFrom !== void 0 && (typeof rawFrom !== "string" || rawFrom.trim().length === 0)) {
507
+ return { ok: false, message: "mailbox.send payload.from must be a non-empty string when provided" };
508
+ }
415
509
  if (typeof rawTo !== "string" || rawTo.trim().length === 0) {
416
510
  return { ok: false, message: "mailbox.send payload.to must be a non-empty string" };
417
511
  }
@@ -448,6 +542,7 @@ function validateMailboxSendPayload(payload) {
448
542
  ok: true,
449
543
  value: {
450
544
  requestId: requestId.trim(),
545
+ ...rawFrom !== void 0 ? { from: rawFrom.trim() } : {},
451
546
  to,
452
547
  type,
453
548
  audience: rawAudience,
@@ -737,8 +832,8 @@ function validateShellOpenPayload(payload) {
737
832
  if (!isRecord2(payload)) {
738
833
  return { ok: false, message: "shell.open payload must be an object with string path" };
739
834
  }
740
- const path29 = payload["path"];
741
- if (typeof path29 !== "string" || path29.trim().length === 0) {
835
+ const path30 = payload["path"];
836
+ if (typeof path30 !== "string" || path30.trim().length === 0) {
742
837
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
743
838
  }
744
839
  const target = payload["target"];
@@ -751,7 +846,7 @@ function validateShellOpenPayload(payload) {
751
846
  return {
752
847
  ok: true,
753
848
  value: {
754
- path: path29,
849
+ path: path30,
755
850
  ...target !== void 0 ? { target } : {}
756
851
  }
757
852
  };
@@ -760,14 +855,14 @@ function validateGitDiffPayload(payload) {
760
855
  if (!isRecord2(payload)) {
761
856
  return { ok: false, message: "git.diff payload must be an object" };
762
857
  }
763
- const path29 = payload["path"];
764
- if (path29 === void 0 || path29 === null) {
858
+ const path30 = payload["path"];
859
+ if (path30 === void 0 || path30 === null) {
765
860
  return { ok: true, value: { path: "" } };
766
861
  }
767
- if (typeof path29 !== "string") {
862
+ if (typeof path30 !== "string") {
768
863
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
769
864
  }
770
- return { ok: true, value: { path: path29 } };
865
+ return { ok: true, value: { path: path30 } };
771
866
  }
772
867
  function validateProjectsAddPayload(payload) {
773
868
  if (!isRecord2(payload)) {
@@ -803,6 +898,58 @@ function validateProjectsSelectPayload(payload) {
803
898
  value: { root, ...typeof name2 === "string" ? { name: name2 } : {} }
804
899
  };
805
900
  }
901
+ var MCP_MAX_STRING = 4096;
902
+ var MCP_MAX_ARRAY = 256;
903
+ function isStringArray(value, max = MCP_MAX_ARRAY) {
904
+ return Array.isArray(value) && value.length <= max && value.every((item) => typeof item === "string" && item.length <= MCP_MAX_STRING);
905
+ }
906
+ function isStringRecord(value) {
907
+ if (!isRecord2(value)) return false;
908
+ const entries = Object.entries(value);
909
+ if (entries.length > MCP_MAX_ARRAY) return false;
910
+ return entries.every(
911
+ ([key, item]) => typeof key === "string" && key.length <= MCP_MAX_STRING && typeof item === "string" && item.length <= MCP_MAX_STRING
912
+ );
913
+ }
914
+ function validateMcpServerPayload(payload, label) {
915
+ if (!isRecord2(payload)) {
916
+ return { ok: false, message: `${label} payload must be an object` };
917
+ }
918
+ const name2 = payload["name"];
919
+ if (typeof name2 !== "string" || name2.trim().length === 0 || name2.length > MCP_MAX_STRING) {
920
+ return { ok: false, message: `${label} payload.name must be a non-empty string` };
921
+ }
922
+ for (const key of ["description", "command", "url", "transport", "permission"]) {
923
+ const value = payload[key];
924
+ if (value !== void 0 && (typeof value !== "string" || value.length > MCP_MAX_STRING)) {
925
+ if (!(key === "transport" && isRecord2(value))) {
926
+ return { ok: false, message: `${label} payload.${key} must be a string` };
927
+ }
928
+ }
929
+ }
930
+ for (const key of ["enabled", "lazy"]) {
931
+ const value = payload[key];
932
+ if (value !== void 0 && typeof value !== "boolean") {
933
+ return { ok: false, message: `${label} payload.${key} must be a boolean` };
934
+ }
935
+ }
936
+ for (const key of ["args", "allowedTools", "passthroughEnv"]) {
937
+ const value = payload[key];
938
+ if (value !== void 0 && !isStringArray(value)) {
939
+ return { ok: false, message: `${label} payload.${key} must be an array of strings` };
940
+ }
941
+ }
942
+ for (const key of ["env", "headers"]) {
943
+ const value = payload[key];
944
+ if (value !== void 0 && !isStringRecord(value)) {
945
+ return {
946
+ ok: false,
947
+ message: `${label} payload.${key} must be an object of string values`
948
+ };
949
+ }
950
+ }
951
+ return { ok: true, value: payload };
952
+ }
806
953
 
807
954
  // src/server/prefs-handlers.ts
808
955
  function sendResult(ctx, ws, success, message) {
@@ -839,6 +986,9 @@ function handlePrefsUpdate(ctx, ws, input) {
839
986
  const payload = parsed.value.prefs;
840
987
  for (const [key, value] of Object.entries(payload)) ctx.meta[key] = value;
841
988
  void ctx.persist(payload);
989
+ if (typeof payload["autonomy"] === "string") {
990
+ ctx.setAutonomy?.(payload["autonomy"]);
991
+ }
842
992
  if (typeof payload["yolo"] === "boolean") {
843
993
  ctx.setYolo?.(payload["yolo"]);
844
994
  if (payload["yolo"]) resolveYoloEligiblePendingConfirms(ctx.pendingConfirms);
@@ -875,6 +1025,7 @@ function handleAutonomySwitch(ctx, ws, mode) {
875
1025
 
876
1026
  // src/server/ws-utils.ts
877
1027
  import { randomBytes } from "node:crypto";
1028
+ import { scrubErrorDetail } from "@wrongstack/core/security";
878
1029
  import { WebSocket } from "ws";
879
1030
  var WEBUI_WS_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
880
1031
  function sendSerialized(ws, data) {
@@ -912,7 +1063,7 @@ function sendResult2(ws, success, message) {
912
1063
  send(ws, { type: "key.operation_result", payload: { success, message } });
913
1064
  }
914
1065
  function errMessage(err) {
915
- return err instanceof Error ? err.message : String(err);
1066
+ return scrubErrorDetail(err);
916
1067
  }
917
1068
  function generateAuthToken() {
918
1069
  return randomBytes(16).toString("hex");
@@ -1205,7 +1356,16 @@ import * as path from "node:path";
1205
1356
  import { resolveIndexDir } from "@wrongstack/tools/codebase-index/index";
1206
1357
  var DB_FILE = "index.db";
1207
1358
  var MAX_CACHE_ENTRIES = 128;
1359
+ var MAX_CACHE_BODY_CHARS = 4 * 1024 * 1024;
1360
+ var MAX_CACHE_TOTAL_CHARS = 16 * 1024 * 1024;
1208
1361
  var cache = /* @__PURE__ */ new Map();
1362
+ var cacheChars = 0;
1363
+ function deleteCachedCodemapBody(key) {
1364
+ const entry = cache.get(key);
1365
+ if (!entry) return false;
1366
+ cacheChars = Math.max(0, cacheChars - entry.body.length);
1367
+ return cache.delete(key);
1368
+ }
1209
1369
  function indexDbVersion(projectRoot, indexDir) {
1210
1370
  try {
1211
1371
  const dir = resolveIndexDir(projectRoot, indexDir);
@@ -1222,7 +1382,7 @@ function getCachedCodemapBody(key, version) {
1222
1382
  const entry = cache.get(key);
1223
1383
  if (!entry) return void 0;
1224
1384
  if (entry.version !== version) {
1225
- cache.delete(key);
1385
+ deleteCachedCodemapBody(key);
1226
1386
  return void 0;
1227
1387
  }
1228
1388
  cache.delete(key);
@@ -1230,16 +1390,19 @@ function getCachedCodemapBody(key, version) {
1230
1390
  return entry.body;
1231
1391
  }
1232
1392
  function setCachedCodemapBody(key, version, body) {
1233
- if (cache.has(key)) cache.delete(key);
1393
+ if (cache.has(key)) deleteCachedCodemapBody(key);
1394
+ if (body.length > MAX_CACHE_BODY_CHARS) return;
1234
1395
  cache.set(key, { version, body });
1235
- while (cache.size > MAX_CACHE_ENTRIES) {
1396
+ cacheChars += body.length;
1397
+ while (cache.size > MAX_CACHE_ENTRIES || cacheChars > MAX_CACHE_TOTAL_CHARS) {
1236
1398
  const oldest = cache.keys().next().value;
1237
1399
  if (oldest === void 0) break;
1238
- cache.delete(oldest);
1400
+ deleteCachedCodemapBody(oldest);
1239
1401
  }
1240
1402
  }
1241
1403
  function clearCodemapGraphCache() {
1242
1404
  cache.clear();
1405
+ cacheChars = 0;
1243
1406
  }
1244
1407
 
1245
1408
  // src/server/codebase-indexing.ts
@@ -1529,6 +1692,8 @@ var CollaborationWebSocketHandler = class {
1529
1692
  for (const off of this.offs) off();
1530
1693
  this.offs.length = 0;
1531
1694
  this.stopBroadcast();
1695
+ this.clients.clear();
1696
+ this.bySession.clear();
1532
1697
  }
1533
1698
  // ── Inbound client messages ────────────────────────────────────────────
1534
1699
  /**
@@ -1588,9 +1753,7 @@ var CollaborationWebSocketHandler = class {
1588
1753
  if (activeSessionId !== void 0 && sessionId !== activeSessionId) {
1589
1754
  this.send(
1590
1755
  ws,
1591
- this.errorMessage(
1592
- `collab.join sessionId mismatch (active: ${activeSessionId})`
1593
- )
1756
+ this.errorMessage(`collab.join sessionId mismatch (active: ${activeSessionId})`)
1594
1757
  );
1595
1758
  return;
1596
1759
  }
@@ -1601,26 +1764,19 @@ var CollaborationWebSocketHandler = class {
1601
1764
  if (role === "controller" && !this.bus) {
1602
1765
  this.send(
1603
1766
  ws,
1604
- this.errorMessage(
1605
- `role 'controller' is not available: server has no CollaborationBus`
1606
- )
1767
+ this.errorMessage(`role 'controller' is not available: server has no CollaborationBus`)
1607
1768
  );
1608
1769
  return;
1609
1770
  }
1610
1771
  if (role === "annotator" && !this.annotations) {
1611
1772
  this.send(
1612
1773
  ws,
1613
- this.errorMessage(
1614
- `role 'annotator' is not available: server has no annotations store`
1615
- )
1774
+ this.errorMessage(`role 'annotator' is not available: server has no annotations store`)
1616
1775
  );
1617
1776
  return;
1618
1777
  }
1619
1778
  if (role !== "observer" && this.options.authorizeRole?.({ ws, sessionId, requestedRole: role }) !== true) {
1620
- this.send(
1621
- ws,
1622
- this.errorMessage(`role '${role}' requires explicit server authorization`)
1623
- );
1779
+ this.send(ws, this.errorMessage(`role '${role}' requires explicit server authorization`));
1624
1780
  return;
1625
1781
  }
1626
1782
  const participant = {
@@ -1649,14 +1805,10 @@ var CollaborationWebSocketHandler = class {
1649
1805
  this.broadcast(sessionId, this.stateMessage(sessionId));
1650
1806
  if (this.reader) {
1651
1807
  this.replayHistory(ws, sessionId).catch((err) => {
1652
- this.logger.debug?.(
1653
- `collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`
1654
- );
1808
+ this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`);
1655
1809
  });
1656
1810
  }
1657
- this.logger.debug?.(
1658
- `collab: participant ${participant.participantId} joined ${sessionId}`
1659
- );
1811
+ this.logger.debug?.(`collab: participant ${participant.participantId} joined ${sessionId}`);
1660
1812
  }
1661
1813
  leave(ws) {
1662
1814
  this.handleDisconnect(ws);
@@ -1727,18 +1879,13 @@ var CollaborationWebSocketHandler = class {
1727
1879
  }
1728
1880
  const payload = raw;
1729
1881
  if (!payload?.sessionId || typeof payload.atEventIndex !== "number" || typeof payload.text !== "string") {
1730
- this.send(
1731
- ws,
1732
- this.errorMessage("annotate requires { sessionId, atEventIndex, text }")
1733
- );
1882
+ this.send(ws, this.errorMessage("annotate requires { sessionId, atEventIndex, text }"));
1734
1883
  return;
1735
1884
  }
1736
1885
  if (payload.sessionId !== participant.sessionId) {
1737
1886
  this.send(
1738
1887
  ws,
1739
- this.errorMessage(
1740
- `annotate sessionId mismatch (joined: ${participant.sessionId})`
1741
- )
1888
+ this.errorMessage(`annotate sessionId mismatch (joined: ${participant.sessionId})`)
1742
1889
  );
1743
1890
  return;
1744
1891
  }
@@ -1765,12 +1912,7 @@ var CollaborationWebSocketHandler = class {
1765
1912
  }
1766
1913
  });
1767
1914
  } catch (err) {
1768
- this.send(
1769
- ws,
1770
- this.errorMessage(
1771
- `annotation rejected: ${toErrorMessage(err)}`
1772
- )
1773
- );
1915
+ this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage(err)}`));
1774
1916
  }
1775
1917
  }
1776
1918
  async handleResolve(ws, raw) {
@@ -1786,26 +1928,19 @@ var CollaborationWebSocketHandler = class {
1786
1928
  if (participant.role !== "annotator") {
1787
1929
  this.send(
1788
1930
  ws,
1789
- this.errorMessage(
1790
- `resolve requires the 'annotator' role (current: '${participant.role}')`
1791
- )
1931
+ this.errorMessage(`resolve requires the 'annotator' role (current: '${participant.role}')`)
1792
1932
  );
1793
1933
  return;
1794
1934
  }
1795
1935
  const payload = raw;
1796
1936
  if (!payload?.sessionId || !payload.annotationId) {
1797
- this.send(
1798
- ws,
1799
- this.errorMessage("resolve requires { sessionId, annotationId }")
1800
- );
1937
+ this.send(ws, this.errorMessage("resolve requires { sessionId, annotationId }"));
1801
1938
  return;
1802
1939
  }
1803
1940
  if (payload.sessionId !== participant.sessionId) {
1804
1941
  this.send(
1805
1942
  ws,
1806
- this.errorMessage(
1807
- `resolve sessionId mismatch (joined: ${participant.sessionId})`
1808
- )
1943
+ this.errorMessage(`resolve sessionId mismatch (joined: ${participant.sessionId})`)
1809
1944
  );
1810
1945
  return;
1811
1946
  }
@@ -1816,10 +1951,7 @@ var CollaborationWebSocketHandler = class {
1816
1951
  resolvedBy: participant.participantId
1817
1952
  });
1818
1953
  if (!updated) {
1819
- this.send(
1820
- ws,
1821
- this.errorMessage(`annotation not found: ${payload.annotationId}`)
1822
- );
1954
+ this.send(ws, this.errorMessage(`annotation not found: ${payload.annotationId}`));
1823
1955
  return;
1824
1956
  }
1825
1957
  this.broadcast(payload.sessionId, {
@@ -1832,12 +1964,7 @@ var CollaborationWebSocketHandler = class {
1832
1964
  }
1833
1965
  });
1834
1966
  } catch (err) {
1835
- this.send(
1836
- ws,
1837
- this.errorMessage(
1838
- `resolve failed: ${toErrorMessage(err)}`
1839
- )
1840
- );
1967
+ this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage(err)}`));
1841
1968
  }
1842
1969
  }
1843
1970
  // ── Event subscription (live mirror) ───────────────────────────────────
@@ -1908,9 +2035,7 @@ var CollaborationWebSocketHandler = class {
1908
2035
  seen++;
1909
2036
  }
1910
2037
  } catch (err) {
1911
- this.logger.debug?.(
1912
- `collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`
1913
- );
2038
+ this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`);
1914
2039
  return;
1915
2040
  }
1916
2041
  const tail2 = seen <= REPLAY_LIMIT ? ring.slice(0, seen) : (
@@ -1992,9 +2117,7 @@ var CollaborationWebSocketHandler = class {
1992
2117
  try {
1993
2118
  sendSerialized(p.ws, data);
1994
2119
  } catch (err) {
1995
- this.logger.debug?.(
1996
- `collab broadcast failed: ${toErrorMessage(err)}`
1997
- );
2120
+ this.logger.debug?.(`collab broadcast failed: ${toErrorMessage(err)}`);
1998
2121
  }
1999
2122
  }
2000
2123
  }
@@ -2021,9 +2144,7 @@ var CollaborationWebSocketHandler = class {
2021
2144
  if (participant.role !== "controller") {
2022
2145
  this.send(
2023
2146
  ws,
2024
- this.errorMessage(
2025
- `pause requires the 'controller' role (current: '${participant.role}')`
2026
- )
2147
+ this.errorMessage(`pause requires the 'controller' role (current: '${participant.role}')`)
2027
2148
  );
2028
2149
  return;
2029
2150
  }
@@ -2068,9 +2189,7 @@ var CollaborationWebSocketHandler = class {
2068
2189
  if (participant.role !== "controller") {
2069
2190
  this.send(
2070
2191
  ws,
2071
- this.errorMessage(
2072
- `resume requires the 'controller' role (current: '${participant.role}')`
2073
- )
2192
+ this.errorMessage(`resume requires the 'controller' role (current: '${participant.role}')`)
2074
2193
  );
2075
2194
  return;
2076
2195
  }
@@ -2168,9 +2287,7 @@ var CollaborationWebSocketHandler = class {
2168
2287
  if (payload.sessionId !== participant.sessionId) {
2169
2288
  this.send(
2170
2289
  ws,
2171
- this.errorMessage(
2172
- `inject_tool sessionId mismatch (joined: ${participant.sessionId})`
2173
- )
2290
+ this.errorMessage(`inject_tool sessionId mismatch (joined: ${participant.sessionId})`)
2174
2291
  );
2175
2292
  return;
2176
2293
  }
@@ -2184,9 +2301,7 @@ var CollaborationWebSocketHandler = class {
2184
2301
  if (!queued) {
2185
2302
  this.send(
2186
2303
  ws,
2187
- this.errorMessage(
2188
- `an injection for toolUseId ${payload.toolUseId} is already queued`
2189
- )
2304
+ this.errorMessage(`an injection for toolUseId ${payload.toolUseId} is already queued`)
2190
2305
  );
2191
2306
  return;
2192
2307
  }
@@ -2782,6 +2897,7 @@ import {
2782
2897
  materializeTokens,
2783
2898
  recordKitChoice,
2784
2899
  recordOverrides,
2900
+ resolveMaterializeTarget,
2785
2901
  resolveSemanticTune,
2786
2902
  runDesignVerify,
2787
2903
  setActiveKit,
@@ -2993,7 +3109,7 @@ async function handleDesignMaterialize(ws, ctx, msg) {
2993
3109
  kitId: active.kit,
2994
3110
  outPath: typeof payload.out === "string" ? payload.out : void 0
2995
3111
  });
2996
- const abs = path5.join(ctx.projectRoot, result.path);
3112
+ const abs = await resolveMaterializeTarget(result.path, ctx.projectRoot);
2997
3113
  await fs3.mkdir(path5.dirname(abs), { recursive: true });
2998
3114
  await fs3.writeFile(abs, result.content);
2999
3115
  send(ws, {
@@ -3146,7 +3262,29 @@ async function resolveFileInsideProject(projectRoot, filePath) {
3146
3262
  if (!isPathInside(realProjectRoot, realFull)) {
3147
3263
  throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3148
3264
  }
3149
- return realFull;
3265
+ const realFinal = await canonicalizeFinalComponent(realFull);
3266
+ if (!isPathInside(realProjectRoot, realFinal)) {
3267
+ throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3268
+ }
3269
+ return realFinal;
3270
+ }
3271
+ async function canonicalizeFinalComponent(p) {
3272
+ let stat3;
3273
+ try {
3274
+ stat3 = await fs5.lstat(p);
3275
+ } catch (err) {
3276
+ if (err.code === "ENOENT") return p;
3277
+ throw err;
3278
+ }
3279
+ if (!stat3.isSymbolicLink()) return p;
3280
+ try {
3281
+ return await fs5.realpath(p);
3282
+ } catch (err) {
3283
+ if (err.code === "ENOENT") {
3284
+ throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3285
+ }
3286
+ throw err;
3287
+ }
3150
3288
  }
3151
3289
  function splitParentAndBase(p) {
3152
3290
  const base = path7.basename(p);
@@ -4179,13 +4317,12 @@ function createConversationOperations(ctx) {
4179
4317
  },
4180
4318
  ping: (ws) => ctx.send(ws, { type: "pong", payload: {} }),
4181
4319
  confirmTool: (_ws, msg) => {
4182
- const requested = requestedSessionId(msg);
4183
- const current2 = ctx.getSessionId();
4184
- if (requested && current2 && requested !== current2) return;
4185
4320
  const { id, decision } = msg.payload ?? {};
4186
4321
  if (typeof id !== "string") return;
4187
4322
  const confirm = ctx.pendingConfirms.get(id);
4188
4323
  if (!confirm) return;
4324
+ const claimedSession = requestedSessionId(msg) ?? ctx.getSessionId();
4325
+ if (confirm.sessionId !== void 0 && confirm.sessionId !== claimedSession) return;
4189
4326
  ctx.pendingConfirms.delete(id);
4190
4327
  confirm.resolve(decision);
4191
4328
  }
@@ -4194,7 +4331,17 @@ function createConversationOperations(ctx) {
4194
4331
 
4195
4332
  // src/server/context-editor.ts
4196
4333
  import { createHash } from "node:crypto";
4197
- import { repairToolUseAdjacency } from "@wrongstack/core/utils";
4334
+ import net from "node:net";
4335
+ import {
4336
+ ALLOWED_IMAGE_MEDIA_TYPES,
4337
+ base64DecodedBytes,
4338
+ isAllowedImageMediaType,
4339
+ isPrivateIPv4,
4340
+ isPrivateIPv6,
4341
+ isValidImageBase64,
4342
+ MAX_INCOMING_IMAGE_BYTES,
4343
+ repairToolUseAdjacency
4344
+ } from "@wrongstack/core/utils";
4198
4345
 
4199
4346
  // src/server/token-estimator.ts
4200
4347
  function estimateTokens(s) {
@@ -4258,6 +4405,40 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
4258
4405
  var MAX_MESSAGE_COUNT_GROWTH = 10;
4259
4406
  var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
4260
4407
  var MAX_STRING_LENGTH = 8 * 1024 * 1024;
4408
+ var MAX_IMAGE_URL_LENGTH = 2048;
4409
+ function imageUrlRejectionReason(url) {
4410
+ if (url.length > MAX_IMAGE_URL_LENGTH) {
4411
+ return `image.source.url exceeds ${MAX_IMAGE_URL_LENGTH} characters.`;
4412
+ }
4413
+ let parsed;
4414
+ try {
4415
+ parsed = new URL(url);
4416
+ } catch {
4417
+ return "image.source.url must be an absolute URL.";
4418
+ }
4419
+ if (parsed.protocol !== "https:") {
4420
+ return `image.source.url must use https (got "${parsed.protocol}").`;
4421
+ }
4422
+ if (parsed.username !== "" || parsed.password !== "") {
4423
+ return "image.source.url must not embed credentials.";
4424
+ }
4425
+ const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
4426
+ const bareHost = host.endsWith(".") ? host.slice(0, -1) : host;
4427
+ if (bareHost === "") {
4428
+ return "image.source.url must include a hostname.";
4429
+ }
4430
+ if (bareHost === "localhost" || bareHost.endsWith(".localhost")) {
4431
+ return "image.source.url must not target localhost.";
4432
+ }
4433
+ const family = net.isIP(bareHost);
4434
+ if (family === 4 && isPrivateIPv4(bareHost)) {
4435
+ return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4436
+ }
4437
+ if (family === 6 && isPrivateIPv6(bareHost)) {
4438
+ return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4439
+ }
4440
+ return void 0;
4441
+ }
4261
4442
  function isRecord3(value) {
4262
4443
  return value !== null && typeof value === "object" && !Array.isArray(value);
4263
4444
  }
@@ -4288,8 +4469,8 @@ function jsonByteLength(value) {
4288
4469
  return MAX_PAYLOAD_BYTES + 1;
4289
4470
  }
4290
4471
  }
4291
- function error(errors, path29, code, message) {
4292
- errors.push({ path: path29, code, message });
4472
+ function error(errors, path30, code, message) {
4473
+ errors.push({ path: path30, code, message });
4293
4474
  }
4294
4475
  function isMessageRole(value) {
4295
4476
  return value === "user" || value === "assistant" || value === "system";
@@ -4297,25 +4478,25 @@ function isMessageRole(value) {
4297
4478
  function isPlainJsonObject(value) {
4298
4479
  return isRecord3(value);
4299
4480
  }
4300
- function validateCacheControl(value, path29, errors) {
4481
+ function validateCacheControl(value, path30, errors) {
4301
4482
  if (value === void 0) return void 0;
4302
4483
  if (!isRecord3(value) || value["type"] !== "ephemeral") {
4303
- error(errors, path29, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4484
+ error(errors, path30, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4304
4485
  return void 0;
4305
4486
  }
4306
4487
  return { type: "ephemeral" };
4307
4488
  }
4308
- function validateProviderMeta(value, path29, errors) {
4489
+ function validateProviderMeta(value, path30, errors) {
4309
4490
  if (value === void 0) return void 0;
4310
4491
  if (!isPlainJsonObject(value)) {
4311
- error(errors, path29, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4492
+ error(errors, path30, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4312
4493
  return void 0;
4313
4494
  }
4314
4495
  return value;
4315
4496
  }
4316
- function validateBlock(value, path29, errors) {
4497
+ function validateBlock(value, path30, errors) {
4317
4498
  if (!isRecord3(value)) {
4318
- error(errors, path29, "INVALID_BLOCK", "Content block must be an object.");
4499
+ error(errors, path30, "INVALID_BLOCK", "Content block must be an object.");
4319
4500
  return void 0;
4320
4501
  }
4321
4502
  const type = value["type"];
@@ -4323,14 +4504,18 @@ function validateBlock(value, path29, errors) {
4323
4504
  case "text": {
4324
4505
  const text = value["text"];
4325
4506
  if (typeof text !== "string") {
4326
- error(errors, `${path29}/text`, "INVALID_TEXT", "Text block text must be a string.");
4507
+ error(errors, `${path30}/text`, "INVALID_TEXT", "Text block text must be a string.");
4327
4508
  return void 0;
4328
4509
  }
4329
4510
  if (text.length > MAX_STRING_LENGTH) {
4330
- error(errors, `${path29}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4511
+ error(errors, `${path30}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4331
4512
  return void 0;
4332
4513
  }
4333
- const cacheControl = validateCacheControl(value["cache_control"], `${path29}/cache_control`, errors);
4514
+ const cacheControl = validateCacheControl(
4515
+ value["cache_control"],
4516
+ `${path30}/cache_control`,
4517
+ errors
4518
+ );
4334
4519
  return cacheControl ? { type: "text", text, cache_control: cacheControl } : { type: "text", text };
4335
4520
  }
4336
4521
  case "tool_use": {
@@ -4338,15 +4523,29 @@ function validateBlock(value, path29, errors) {
4338
4523
  const name2 = value["name"];
4339
4524
  const input = value["input"];
4340
4525
  if (typeof id !== "string" || id.length === 0) {
4341
- error(errors, `${path29}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4526
+ error(
4527
+ errors,
4528
+ `${path30}/id`,
4529
+ "INVALID_TOOL_USE_ID",
4530
+ "tool_use.id must be a non-empty string."
4531
+ );
4342
4532
  }
4343
4533
  if (typeof name2 !== "string" || name2.length === 0) {
4344
- error(errors, `${path29}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4534
+ error(
4535
+ errors,
4536
+ `${path30}/name`,
4537
+ "INVALID_TOOL_NAME",
4538
+ "tool_use.name must be a non-empty string."
4539
+ );
4345
4540
  }
4346
4541
  if (!isPlainJsonObject(input)) {
4347
- error(errors, `${path29}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4542
+ error(errors, `${path30}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4348
4543
  }
4349
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path29}/providerMeta`, errors);
4544
+ const providerMeta = validateProviderMeta(
4545
+ value["providerMeta"],
4546
+ `${path30}/providerMeta`,
4547
+ errors
4548
+ );
4350
4549
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4351
4550
  return void 0;
4352
4551
  }
@@ -4358,20 +4557,46 @@ function validateBlock(value, path29, errors) {
4358
4557
  const content = value["content"];
4359
4558
  const isError = value["is_error"];
4360
4559
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4361
- error(errors, `${path29}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4560
+ error(
4561
+ errors,
4562
+ `${path30}/tool_use_id`,
4563
+ "INVALID_TOOL_RESULT_ID",
4564
+ "tool_result.tool_use_id must be a non-empty string."
4565
+ );
4362
4566
  }
4363
4567
  if (name2 !== void 0 && typeof name2 !== "string") {
4364
- error(errors, `${path29}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4568
+ error(
4569
+ errors,
4570
+ `${path30}/name`,
4571
+ "INVALID_TOOL_RESULT_NAME",
4572
+ "tool_result.name must be a string."
4573
+ );
4365
4574
  }
4366
4575
  if (typeof content !== "string") {
4367
- error(errors, `${path29}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4576
+ error(
4577
+ errors,
4578
+ `${path30}/content`,
4579
+ "INVALID_TOOL_RESULT_CONTENT",
4580
+ "tool_result.content must be a string."
4581
+ );
4368
4582
  } else if (content.length > MAX_STRING_LENGTH) {
4369
- error(errors, `${path29}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4583
+ error(
4584
+ errors,
4585
+ `${path30}/content`,
4586
+ "TOOL_RESULT_TOO_LARGE",
4587
+ "tool_result.content is too large."
4588
+ );
4370
4589
  }
4371
4590
  if (isError !== void 0 && typeof isError !== "boolean") {
4372
- error(errors, `${path29}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4591
+ error(
4592
+ errors,
4593
+ `${path30}/is_error`,
4594
+ "INVALID_TOOL_RESULT_ERROR",
4595
+ "tool_result.is_error must be boolean."
4596
+ );
4373
4597
  }
4374
- if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4598
+ if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string")
4599
+ return void 0;
4375
4600
  return {
4376
4601
  type: "tool_result",
4377
4602
  tool_use_id: toolUseId,
@@ -4383,33 +4608,97 @@ function validateBlock(value, path29, errors) {
4383
4608
  case "image": {
4384
4609
  const source = value["source"];
4385
4610
  if (!isRecord3(source)) {
4386
- error(errors, `${path29}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4611
+ error(errors, `${path30}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4387
4612
  return void 0;
4388
4613
  }
4389
4614
  const sourceType = source["type"];
4390
4615
  if (sourceType !== "base64" && sourceType !== "url") {
4391
- error(errors, `${path29}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4616
+ error(
4617
+ errors,
4618
+ `${path30}/source/type`,
4619
+ "INVALID_IMAGE_SOURCE_TYPE",
4620
+ "image.source.type must be base64 or url."
4621
+ );
4392
4622
  return void 0;
4393
4623
  }
4394
4624
  const mediaType = source["media_type"];
4395
4625
  const data = source["data"];
4396
4626
  const url = source["url"];
4397
4627
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4398
- error(errors, `${path29}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4628
+ error(
4629
+ errors,
4630
+ `${path30}/source/media_type`,
4631
+ "INVALID_IMAGE_MEDIA_TYPE",
4632
+ "image.source.media_type must be a string."
4633
+ );
4634
+ return void 0;
4399
4635
  }
4400
- if (data !== void 0 && typeof data !== "string") {
4401
- error(errors, `${path29}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4636
+ if (typeof mediaType === "string" && !isAllowedImageMediaType(mediaType)) {
4637
+ error(
4638
+ errors,
4639
+ `${path30}/source/media_type`,
4640
+ "UNSUPPORTED_IMAGE_MEDIA_TYPE",
4641
+ `image.source.media_type must be one of: ${[...ALLOWED_IMAGE_MEDIA_TYPES].join(", ")}.`
4642
+ );
4643
+ return void 0;
4644
+ }
4645
+ if (sourceType === "base64") {
4646
+ if (typeof data !== "string" || data.length === 0) {
4647
+ error(
4648
+ errors,
4649
+ `${path30}/source/data`,
4650
+ "INVALID_IMAGE_DATA",
4651
+ "image.source.data must be a non-empty string for a base64 source."
4652
+ );
4653
+ return void 0;
4654
+ }
4655
+ if (!isValidImageBase64(data)) {
4656
+ error(
4657
+ errors,
4658
+ `${path30}/source/data`,
4659
+ "INVALID_IMAGE_DATA",
4660
+ "image.source.data is not valid base64."
4661
+ );
4662
+ return void 0;
4663
+ }
4664
+ if (base64DecodedBytes(data) > MAX_INCOMING_IMAGE_BYTES) {
4665
+ error(
4666
+ errors,
4667
+ `${path30}/source/data`,
4668
+ "IMAGE_TOO_LARGE",
4669
+ `image.source.data exceeds the ${MAX_INCOMING_IMAGE_BYTES / (1024 * 1024)} MB limit.`
4670
+ );
4671
+ return void 0;
4672
+ }
4673
+ return {
4674
+ type: "image",
4675
+ source: {
4676
+ type: "base64",
4677
+ ...typeof mediaType === "string" ? { media_type: mediaType } : {},
4678
+ data
4679
+ }
4680
+ };
4402
4681
  }
4403
- if (url !== void 0 && typeof url !== "string") {
4404
- error(errors, `${path29}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4682
+ if (typeof url !== "string" || url.length === 0) {
4683
+ error(
4684
+ errors,
4685
+ `${path30}/source/url`,
4686
+ "INVALID_IMAGE_URL",
4687
+ "image.source.url must be a non-empty string for a url source."
4688
+ );
4689
+ return void 0;
4690
+ }
4691
+ const urlError = imageUrlRejectionReason(url);
4692
+ if (urlError !== void 0) {
4693
+ error(errors, `${path30}/source/url`, "UNSAFE_IMAGE_URL", urlError);
4694
+ return void 0;
4405
4695
  }
4406
4696
  return {
4407
4697
  type: "image",
4408
4698
  source: {
4409
- type: sourceType,
4699
+ type: "url",
4410
4700
  ...typeof mediaType === "string" ? { media_type: mediaType } : {},
4411
- ...typeof data === "string" ? { data } : {},
4412
- ...typeof url === "string" ? { url } : {}
4701
+ url
4413
4702
  }
4414
4703
  };
4415
4704
  }
@@ -4417,13 +4706,27 @@ function validateBlock(value, path29, errors) {
4417
4706
  const thinking = value["thinking"];
4418
4707
  const signature = value["signature"];
4419
4708
  if (typeof thinking !== "string") {
4420
- error(errors, `${path29}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4709
+ error(
4710
+ errors,
4711
+ `${path30}/thinking`,
4712
+ "INVALID_THINKING",
4713
+ "thinking.thinking must be a string."
4714
+ );
4421
4715
  return void 0;
4422
4716
  }
4423
4717
  if (signature !== void 0 && typeof signature !== "string") {
4424
- error(errors, `${path29}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4718
+ error(
4719
+ errors,
4720
+ `${path30}/signature`,
4721
+ "INVALID_THINKING_SIGNATURE",
4722
+ "thinking.signature must be a string."
4723
+ );
4425
4724
  }
4426
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path29}/providerMeta`, errors);
4725
+ const providerMeta = validateProviderMeta(
4726
+ value["providerMeta"],
4727
+ `${path30}/providerMeta`,
4728
+ errors
4729
+ );
4427
4730
  return {
4428
4731
  type: "thinking",
4429
4732
  thinking,
@@ -4432,7 +4735,12 @@ function validateBlock(value, path29, errors) {
4432
4735
  };
4433
4736
  }
4434
4737
  default:
4435
- error(errors, `${path29}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4738
+ error(
4739
+ errors,
4740
+ `${path30}/type`,
4741
+ "UNKNOWN_BLOCK_TYPE",
4742
+ `Unknown content block type: ${String(type)}`
4743
+ );
4436
4744
  return void 0;
4437
4745
  }
4438
4746
  }
@@ -4444,45 +4752,65 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4444
4752
  return { messages, errors };
4445
4753
  }
4446
4754
  if (value.length > currentMessageCount + MAX_MESSAGE_COUNT_GROWTH) {
4447
- error(errors, "/messages", "TOO_MANY_MESSAGES", "Context editor is deletion-oriented; proposed message count grew too much.");
4755
+ error(
4756
+ errors,
4757
+ "/messages",
4758
+ "TOO_MANY_MESSAGES",
4759
+ "Context editor is deletion-oriented; proposed message count grew too much."
4760
+ );
4448
4761
  }
4449
4762
  if (jsonByteLength(value) > MAX_PAYLOAD_BYTES) {
4450
4763
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
4451
4764
  }
4452
4765
  value.forEach((item, index) => {
4453
- const path29 = `/messages/${index}`;
4766
+ const path30 = `/messages/${index}`;
4454
4767
  if (!isRecord3(item)) {
4455
- error(errors, path29, "INVALID_MESSAGE", "Message must be an object.");
4768
+ error(errors, path30, "INVALID_MESSAGE", "Message must be an object.");
4456
4769
  return;
4457
4770
  }
4458
4771
  const role = item["role"];
4459
4772
  if (!isMessageRole(role)) {
4460
- error(errors, `${path29}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4773
+ error(
4774
+ errors,
4775
+ `${path30}/role`,
4776
+ "INVALID_ROLE",
4777
+ "Message role must be user, assistant, or system."
4778
+ );
4461
4779
  return;
4462
4780
  }
4463
4781
  const rawContent = item["content"];
4464
4782
  let content;
4465
4783
  if (typeof rawContent === "string") {
4466
4784
  if (rawContent.length > MAX_STRING_LENGTH) {
4467
- error(errors, `${path29}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4785
+ error(errors, `${path30}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4468
4786
  return;
4469
4787
  }
4470
4788
  content = rawContent;
4471
4789
  } else if (Array.isArray(rawContent)) {
4472
4790
  const blocks = [];
4473
4791
  rawContent.forEach((block, blockIndex) => {
4474
- const parsed = validateBlock(block, `${path29}/content/${blockIndex}`, errors);
4792
+ const parsed = validateBlock(block, `${path30}/content/${blockIndex}`, errors);
4475
4793
  if (parsed) blocks.push(parsed);
4476
4794
  });
4477
4795
  content = blocks;
4478
4796
  } else {
4479
- error(errors, `${path29}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4797
+ error(
4798
+ errors,
4799
+ `${path30}/content`,
4800
+ "INVALID_CONTENT",
4801
+ "Message content must be a string or content block array."
4802
+ );
4480
4803
  return;
4481
4804
  }
4482
4805
  const ts = item["ts"];
4483
4806
  if (ts !== void 0) {
4484
4807
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4485
- error(errors, `${path29}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4808
+ error(
4809
+ errors,
4810
+ `${path30}/ts`,
4811
+ "INVALID_TIMESTAMP",
4812
+ "Message ts must be an ISO-like timestamp string."
4813
+ );
4486
4814
  return;
4487
4815
  }
4488
4816
  }
@@ -4492,7 +4820,8 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4492
4820
  }
4493
4821
  function countBlocks(messages) {
4494
4822
  let total = 0;
4495
- for (const message of messages) total += Array.isArray(message.content) ? message.content.length : 1;
4823
+ for (const message of messages)
4824
+ total += Array.isArray(message.content) ? message.content.length : 1;
4496
4825
  return total;
4497
4826
  }
4498
4827
  function toolDiagnostics(messages) {
@@ -4556,11 +4885,13 @@ function metricFor(ctx, messages, tools) {
4556
4885
  };
4557
4886
  }
4558
4887
  function buildContextEditorSnapshot(ctx, tools) {
4559
- const messages = ctx.messages.map((message) => ({
4560
- role: message.role,
4561
- content: message.content,
4562
- ...message.ts ? { ts: message.ts } : {}
4563
- }));
4888
+ const messages = ctx.messages.map(
4889
+ (message) => ({
4890
+ role: message.role,
4891
+ content: message.content,
4892
+ ...message.ts ? { ts: message.ts } : {}
4893
+ })
4894
+ );
4564
4895
  const breakdown = estimateContextBreakdown({
4565
4896
  systemPrompt: ctx.systemPrompt,
4566
4897
  tools: tools ?? ctx.tools ?? [],
@@ -4605,7 +4936,10 @@ function validateContextEditorProposal(input) {
4605
4936
  validationErrors: [],
4606
4937
  warnings: [],
4607
4938
  repair: emptyRepair,
4608
- conflict: { code: "RUN_ACTIVE", message: "The agent is currently running. Abort or wait before applying context edits." }
4939
+ conflict: {
4940
+ code: "RUN_ACTIVE",
4941
+ message: "The agent is currently running. Abort or wait before applying context edits."
4942
+ }
4609
4943
  };
4610
4944
  }
4611
4945
  if (input.baseRevision !== currentRevision) {
@@ -4617,7 +4951,10 @@ function validateContextEditorProposal(input) {
4617
4951
  validationErrors: [],
4618
4952
  warnings: [],
4619
4953
  repair: emptyRepair,
4620
- conflict: { code: "CONTEXT_REVISION_CONFLICT", message: "Context changed while the editor was open. Reload before applying." }
4954
+ conflict: {
4955
+ code: "CONTEXT_REVISION_CONFLICT",
4956
+ message: "Context changed while the editor was open. Reload before applying."
4957
+ }
4621
4958
  };
4622
4959
  }
4623
4960
  const parsed = validateContextEditorMessages(input.messages, input.ctx.messages.length);
@@ -4700,6 +5037,7 @@ async function applyContextEditorProposal(input) {
4700
5037
  }
4701
5038
 
4702
5039
  // src/server/connection-lifecycle.ts
5040
+ var RATE_LIMIT_EXEMPT_TYPES = /* @__PURE__ */ new Set(["ping", "pong"]);
4703
5041
  function createConnectionLifecycle(options) {
4704
5042
  const rateLimitMax = options.rateLimitMax ?? 0;
4705
5043
  const rateLimitWindowMs = options.rateLimitWindowMs ?? 6e4;
@@ -4770,25 +5108,31 @@ function createConnectionLifecycle(options) {
4770
5108
  }
4771
5109
  let messageCount = 0;
4772
5110
  let rateWindowResetAt = Date.now() + rateLimitWindowMs;
4773
- ws.on("message", async (data) => {
4774
- if (rateLimitMax > 0) {
4775
- const now = Date.now();
4776
- if (now > rateWindowResetAt) {
4777
- messageCount = 0;
4778
- rateWindowResetAt = now + rateLimitWindowMs;
4779
- }
4780
- if (++messageCount > rateLimitMax) {
4781
- options.send(ws, {
4782
- type: "error",
4783
- payload: options.sessionPayload({
4784
- phase: "rate_limit",
4785
- message: options.rateLimitMessage ?? "Too many messages. Please wait."
4786
- })
4787
- });
4788
- return;
4789
- }
5111
+ let rateLimitNotified = false;
5112
+ const chargeRateLimit = () => {
5113
+ if (!Number.isFinite(rateLimitMax) || rateLimitMax <= 0) return true;
5114
+ const now = Date.now();
5115
+ if (now > rateWindowResetAt) {
5116
+ messageCount = 0;
5117
+ rateWindowResetAt = now + rateLimitWindowMs;
5118
+ rateLimitNotified = false;
5119
+ }
5120
+ if (++messageCount <= rateLimitMax) return true;
5121
+ if (!rateLimitNotified) {
5122
+ rateLimitNotified = true;
5123
+ options.send(ws, {
5124
+ type: "error",
5125
+ payload: options.sessionPayload({
5126
+ phase: "rate_limit",
5127
+ message: options.rateLimitMessage ?? "Too many messages. Please wait."
5128
+ })
5129
+ });
4790
5130
  }
5131
+ return false;
5132
+ };
5133
+ ws.on("message", async (data) => {
4791
5134
  const decoded = options.decode(data.toString());
5135
+ if (!decoded.ok && !chargeRateLimit()) return;
4792
5136
  if (!decoded.ok) {
4793
5137
  const eventName = decoded.issue.message === "Protocol frame is not valid JSON" ? "webui_server.message_parse_failed" : "webui_server.message_rejected";
4794
5138
  const level = decoded.issue.code === "unsafe_key" || decoded.issue.code === "too_deep" ? "error" : "warn";
@@ -4826,6 +5170,10 @@ function createConnectionLifecycle(options) {
4826
5170
  });
4827
5171
  return;
4828
5172
  }
5173
+ const messageType = decoded.message.type;
5174
+ if ((typeof messageType !== "string" || !RATE_LIMIT_EXEMPT_TYPES.has(messageType)) && !chargeRateLimit()) {
5175
+ return;
5176
+ }
4829
5177
  try {
4830
5178
  await options.dispatch(ws, client, decoded.message);
4831
5179
  } catch (error2) {
@@ -4866,10 +5214,16 @@ import {
4866
5214
  MailboxProjectServerConnection
4867
5215
  } from "@wrongstack/core/coordination";
4868
5216
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
4869
- import { getKanbanServerConnection } from "@wrongstack/kanban";
5217
+ import {
5218
+ closeKanbanServerConnections,
5219
+ getKanbanServerConnection,
5220
+ isKanbanServerAvailable
5221
+ } from "@wrongstack/kanban";
5222
+ import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
4870
5223
  import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
4871
5224
  import {
4872
5225
  checkCodebaseIndexServerHealth,
5226
+ ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
4873
5227
  getIndexState,
4874
5228
  resolveProjectIndexDaemonAvailability,
4875
5229
  shutdownCodebaseIndexServer
@@ -4898,7 +5252,8 @@ async function collectConnectionsHealth(options) {
4898
5252
  codebaseIndexHealth(options.projectRoot, options.indexDir),
4899
5253
  sageHealth(options.projectRoot),
4900
5254
  kanbanHealth(options.projectRoot),
4901
- mailboxHealth(options.projectRoot)
5255
+ mailboxHealth(options.projectRoot),
5256
+ governanceHealth(options.projectRoot)
4902
5257
  ]);
4903
5258
  const required = services.filter((service) => service.required);
4904
5259
  const overall = required.some((service) => service.status === "error") ? "error" : required.some((service) => service.status !== "healthy") ? "degraded" : "healthy";
@@ -5203,6 +5558,42 @@ async function mailboxHealth(projectRoot) {
5203
5558
  connection.close();
5204
5559
  }
5205
5560
  }
5561
+ async function governanceHealth(projectRoot) {
5562
+ const startedAt = Date.now();
5563
+ const result = await readGovernanceDaemonOperatorStatus(projectRoot);
5564
+ if (!result.available) {
5565
+ const missing = result.code === "broker_missing";
5566
+ return {
5567
+ id: "governance",
5568
+ label: "Governance control plane",
5569
+ status: missing ? "offline" : "error",
5570
+ required: false,
5571
+ mode: missing ? "compatibility-default-off" : "project-daemon",
5572
+ detail: missing ? "Not active for this project. Existing agent and model execution remains unchanged." : `Read-only governance status is unavailable: ${result.message}`,
5573
+ latencyMs: Date.now() - startedAt,
5574
+ control: "none",
5575
+ ...missing ? {} : { lastError: result.message }
5576
+ };
5577
+ }
5578
+ const { status } = result;
5579
+ return {
5580
+ id: "governance",
5581
+ label: "Governance control plane",
5582
+ status: status.signal.level === "healthy" ? "healthy" : "degraded",
5583
+ required: false,
5584
+ mode: "project-daemon-advisory",
5585
+ detail: `${status.signal.message} Execution continues; no automatic task or model stop.`,
5586
+ ownerPid: status.pid,
5587
+ uptimeMs: Math.max(0, Date.now() - Date.parse(status.startedAt)),
5588
+ latencyMs: Date.now() - startedAt,
5589
+ control: "none",
5590
+ advisory: {
5591
+ code: status.signal.code,
5592
+ operatorAction: status.signal.operatorAction,
5593
+ executionDisposition: status.signal.executionDisposition
5594
+ }
5595
+ };
5596
+ }
5206
5597
  function failureService(id, label, required, mode, error2, latencyMs) {
5207
5598
  const message = error2 instanceof Error ? error2.message : String(error2);
5208
5599
  return {
@@ -5420,15 +5811,15 @@ async function handleGitChanges(ws, projectRoot) {
5420
5811
  if (!m) continue;
5421
5812
  const added = m[1] === "-" ? 0 : Number(m[1]);
5422
5813
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5423
- let path29 = m[3] ?? "";
5424
- if (path29 === "") {
5814
+ let path30 = m[3] ?? "";
5815
+ if (path30 === "") {
5425
5816
  i += 1;
5426
- path29 = parts[i + 1] ?? parts[i] ?? "";
5817
+ path30 = parts[i + 1] ?? parts[i] ?? "";
5427
5818
  i += 1;
5428
5819
  }
5429
- if (!path29) continue;
5430
- const prev = counts.get(path29) ?? { added: 0, deleted: 0 };
5431
- counts.set(path29, { added: prev.added + added, deleted: prev.deleted + deleted });
5820
+ if (!path30) continue;
5821
+ const prev = counts.get(path30) ?? { added: 0, deleted: 0 };
5822
+ counts.set(path30, { added: prev.added + added, deleted: prev.deleted + deleted });
5432
5823
  }
5433
5824
  };
5434
5825
  parseNumstat(unstagedNumstat);
@@ -5440,7 +5831,7 @@ async function handleGitChanges(ws, projectRoot) {
5440
5831
  if (!rec || rec.length < 3) continue;
5441
5832
  const x = rec[0] ?? " ";
5442
5833
  const y = rec[1] ?? " ";
5443
- const path29 = rec.slice(3);
5834
+ const path30 = rec.slice(3);
5444
5835
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5445
5836
  if (isRename) i += 1;
5446
5837
  let status;
@@ -5452,13 +5843,13 @@ async function handleGitChanges(ws, projectRoot) {
5452
5843
  else if (x === "D" || y === "D") status = "D";
5453
5844
  else status = "M";
5454
5845
  const staged = x !== " " && x !== "?";
5455
- let added = counts.get(path29)?.added ?? 0;
5456
- let deleted = counts.get(path29)?.deleted ?? 0;
5846
+ let added = counts.get(path30)?.added ?? 0;
5847
+ let deleted = counts.get(path30)?.deleted ?? 0;
5457
5848
  if (status === "?") {
5458
5849
  added = 0;
5459
5850
  deleted = 0;
5460
5851
  }
5461
- files.push({ path: path29, status, added, deleted, staged });
5852
+ files.push({ path: path30, status, added, deleted, staged });
5462
5853
  }
5463
5854
  send(ws, { type: "git.changes", payload: { files } });
5464
5855
  } catch (err) {
@@ -5469,10 +5860,10 @@ async function handleGitChanges(ws, projectRoot) {
5469
5860
  }
5470
5861
  }
5471
5862
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5472
- async function handleGitDiff(ws, projectRoot, path29) {
5863
+ async function handleGitDiff(ws, projectRoot, path30) {
5473
5864
  const cwd = projectRoot || void 0;
5474
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path29, ...extra } });
5475
- if (!path29 || path29.includes("\0") || path29.includes("..") || nodePath.isAbsolute(path29)) {
5865
+ const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path30, ...extra } });
5866
+ if (!path30 || path30.includes("\0") || path30.includes("..") || nodePath.isAbsolute(path30)) {
5476
5867
  reply2({ oldText: "", newText: "", error: "invalid path" });
5477
5868
  return;
5478
5869
  }
@@ -5480,10 +5871,10 @@ async function handleGitDiff(ws, projectRoot, path29) {
5480
5871
  const git = makeGit(cwd);
5481
5872
  const { readFile: readFile11 } = await import("node:fs/promises");
5482
5873
  const { join: join15 } = await import("node:path");
5483
- const oldText = await git(["show", `HEAD:${path29}`]);
5874
+ const oldText = await git(["show", `HEAD:${path30}`]);
5484
5875
  let newText = "";
5485
5876
  try {
5486
- const abs = cwd ? join15(cwd, path29) : path29;
5877
+ const abs = cwd ? join15(cwd, path30) : path30;
5487
5878
  const buf = await readFile11(abs);
5488
5879
  if (buf.includes(0)) {
5489
5880
  reply2({ oldText: "", newText: "", binary: true });
@@ -5545,10 +5936,7 @@ async function handleGoalSnapshotRoute(ws, msg, handlers) {
5545
5936
  }
5546
5937
 
5547
5938
  // src/server/goal-ws-handler.ts
5548
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5549
- import {
5550
- assignNickname
5551
- } from "@wrongstack/core/coordination";
5939
+ import { assignNickname } from "@wrongstack/core/coordination";
5552
5940
  import {
5553
5941
  GoalAssessor,
5554
5942
  GoalPlanner,
@@ -5556,6 +5944,7 @@ import {
5556
5944
  PhaseOrchestrator,
5557
5945
  PhaseStore
5558
5946
  } from "@wrongstack/core/goal";
5947
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5559
5948
  import { WorktreeManager } from "@wrongstack/core/worktree";
5560
5949
 
5561
5950
  // src/server/git-process.ts
@@ -5592,12 +5981,7 @@ function deriveTitle(goal) {
5592
5981
  return trimmed || "Goal";
5593
5982
  }
5594
5983
  async function commitsSince(cwd, baseSha, branch) {
5595
- const output = await gitStdout(cwd, [
5596
- "log",
5597
- "--reverse",
5598
- "--format=%H",
5599
- `${baseSha}..${branch}`
5600
- ]);
5984
+ const output = await gitStdout(cwd, ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`]);
5601
5985
  if (output === null) return [];
5602
5986
  return output.split("\n").map((s) => s.trim()).filter(Boolean);
5603
5987
  }
@@ -5648,6 +6032,20 @@ var GoalWebSocketHandler = class {
5648
6032
  ws.on("error", () => this.clients.delete(client));
5649
6033
  this.sendState(client);
5650
6034
  }
6035
+ /** Release timers, in-flight work, and socket references owned by this host. */
6036
+ dispose() {
6037
+ this.stopping = true;
6038
+ this.abort?.abort();
6039
+ this.abort = null;
6040
+ this.assessAbort?.abort();
6041
+ this.assessAbort = null;
6042
+ this.orchestrator?.stop();
6043
+ this.orchestrator = null;
6044
+ this.stopBroadcast();
6045
+ this.clients.clear();
6046
+ this.usedNicknames.clear();
6047
+ this.worktrees = null;
6048
+ }
5651
6049
  async handleMessage(ws, msg) {
5652
6050
  switch (msg.type) {
5653
6051
  case "goal.assess":
@@ -5695,7 +6093,8 @@ var GoalWebSocketHandler = class {
5695
6093
  }
5696
6094
  case "goal.assignTask": {
5697
6095
  const { taskId, agentId, agentName } = msg.payload;
5698
- if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
6096
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName))
6097
+ this.afterBoardMutation();
5699
6098
  break;
5700
6099
  }
5701
6100
  case "goal.addTask": {
@@ -5740,7 +6139,10 @@ var GoalWebSocketHandler = class {
5740
6139
  this.graph = graph;
5741
6140
  this.broadcast({ type: "goal.state", payload: this.buildState() });
5742
6141
  } else {
5743
- this.broadcast({ type: "goal.error", payload: { message: `Graph not found: ${graphId}` } });
6142
+ this.broadcast({
6143
+ type: "goal.error",
6144
+ payload: { message: `Graph not found: ${graphId}` }
6145
+ });
5744
6146
  }
5745
6147
  }
5746
6148
  break;
@@ -5763,10 +6165,13 @@ var GoalWebSocketHandler = class {
5763
6165
  const mySeq = ++this.assessSeq;
5764
6166
  const sendResult7 = (result) => {
5765
6167
  if (mySeq !== this.assessSeq) return;
5766
- sendSerialized(ws, JSON.stringify({
5767
- type: "goal.assess.result",
5768
- payload: { ...result, reqSeq: seq }
5769
- }));
6168
+ sendSerialized(
6169
+ ws,
6170
+ JSON.stringify({
6171
+ type: "goal.assess.result",
6172
+ payload: { ...result, reqSeq: seq }
6173
+ })
6174
+ );
5770
6175
  };
5771
6176
  if (!goal.trim()) {
5772
6177
  sendResult7({
@@ -5812,15 +6217,24 @@ var GoalWebSocketHandler = class {
5812
6217
  const multiBoard = payload?.multiBoard ?? false;
5813
6218
  const verifyTasks = payload?.verifyTasks ?? false;
5814
6219
  const chimeraReview = payload?.chimeraReview ?? false;
5815
- this.abort = new AbortController();
6220
+ const runAbort = new AbortController();
6221
+ this.abort = runAbort;
5816
6222
  this.stopping = false;
5817
- const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
5818
- if (this.stopping || this.abort.signal.aborted) {
6223
+ const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, runAbort.signal);
6224
+ if (this.stopping || runAbort.signal.aborted) {
5819
6225
  this.broadcast({ type: "goal.stopped", payload: { title } });
5820
6226
  return;
5821
6227
  }
5822
6228
  this.logger.info(`[Goal] Starting: ${title}`);
5823
- const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous, multiBoard, verifyTasks, chimeraReview }).build();
6229
+ const graph = await new PhaseGraphBuilder({
6230
+ title,
6231
+ description: goal,
6232
+ phases,
6233
+ autonomous,
6234
+ multiBoard,
6235
+ verifyTasks,
6236
+ chimeraReview
6237
+ }).build();
5824
6238
  this.graph = graph;
5825
6239
  await this.store.save(graph);
5826
6240
  const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_GOAL_WORKTREES"] !== "0";
@@ -5839,9 +6253,10 @@ var GoalWebSocketHandler = class {
5839
6253
  maybeVerify.verifyPhase = (async (phase, env) => {
5840
6254
  const cwd = env?.cwd ?? this.projectRoot;
5841
6255
  try {
5842
- const { exec } = await import("node:child_process");
6256
+ const { execFile: execFile2 } = await import("node:child_process");
5843
6257
  const result = await new Promise((resolve15) => {
5844
- exec("npx tsc --noEmit", { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
6258
+ const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
6259
+ execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
5845
6260
  if (err && err.code === "ENOENT") {
5846
6261
  resolve15("[verify] tsc not found \u2014 skipping");
5847
6262
  return;
@@ -5983,11 +6398,41 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
5983
6398
  /** Generic fallback phases when the LLM planner produces nothing usable. */
5984
6399
  defaultPhases() {
5985
6400
  return [
5986
- { name: "Discovery", description: "Requirements gathering", priority: "high", estimateHours: 2, parallelizable: false },
5987
- { name: "Design", description: "Architecture and design", priority: "critical", estimateHours: 4, parallelizable: false },
5988
- { name: "Implementation", description: "Core development", priority: "critical", estimateHours: 12, parallelizable: false },
5989
- { name: "Testing", description: "Unit and integration tests", priority: "high", estimateHours: 6, parallelizable: true },
5990
- { name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
6401
+ {
6402
+ name: "Discovery",
6403
+ description: "Requirements gathering",
6404
+ priority: "high",
6405
+ estimateHours: 2,
6406
+ parallelizable: false
6407
+ },
6408
+ {
6409
+ name: "Design",
6410
+ description: "Architecture and design",
6411
+ priority: "critical",
6412
+ estimateHours: 4,
6413
+ parallelizable: false
6414
+ },
6415
+ {
6416
+ name: "Implementation",
6417
+ description: "Core development",
6418
+ priority: "critical",
6419
+ estimateHours: 12,
6420
+ parallelizable: false
6421
+ },
6422
+ {
6423
+ name: "Testing",
6424
+ description: "Unit and integration tests",
6425
+ priority: "high",
6426
+ estimateHours: 6,
6427
+ parallelizable: true
6428
+ },
6429
+ {
6430
+ name: "Deployment",
6431
+ description: "Deploy to production",
6432
+ priority: "medium",
6433
+ estimateHours: 2,
6434
+ parallelizable: false
6435
+ }
5991
6436
  ];
5992
6437
  }
5993
6438
  /** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
@@ -6068,8 +6513,10 @@ Type: ${task.type}`;
6068
6513
  try {
6069
6514
  const result_ = await this.agent.run(reviewPrompt);
6070
6515
  if (result_.status === "done" && result_.finalText) {
6071
- this.logger.info(`[Goal] Chimera review for "${task.title}":
6072
- ${result_.finalText.slice(0, 2e3)}`);
6516
+ this.logger.info(
6517
+ `[Goal] Chimera review for "${task.title}":
6518
+ ${result_.finalText.slice(0, 2e3)}`
6519
+ );
6073
6520
  }
6074
6521
  } catch (err) {
6075
6522
  this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
@@ -6122,7 +6569,16 @@ ${result_.finalText.slice(0, 2e3)}`);
6122
6569
  }
6123
6570
  buildState(activePhaseId) {
6124
6571
  if (!this.graph) {
6125
- return { phases: [], tasks: [], overallPercent: 0, autonomous: true, title: "", multiBoard: false, verifyTasks: false, chimeraReview: false };
6572
+ return {
6573
+ phases: [],
6574
+ tasks: [],
6575
+ overallPercent: 0,
6576
+ autonomous: true,
6577
+ title: "",
6578
+ multiBoard: false,
6579
+ verifyTasks: false,
6580
+ chimeraReview: false
6581
+ };
6126
6582
  }
6127
6583
  const phases = Array.from(this.graph.phases.values());
6128
6584
  const currentActiveId = activePhaseId || phases.find((p) => p.status === "running")?.id || phases[0]?.id || "";
@@ -6620,88 +7076,13 @@ async function handleApiAnalyticsSummary(res) {
6620
7076
  }
6621
7077
 
6622
7078
  // src/server/http-server.ts
6623
- import * as fs9 from "node:fs/promises";
7079
+ import * as fs10 from "node:fs/promises";
6624
7080
  import * as http from "node:http";
6625
- import * as path12 from "node:path";
7081
+ import * as path13 from "node:path";
6626
7082
  import * as v8 from "node:v8";
7083
+ import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
6627
7084
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6628
7085
 
6629
- // src/server/codemap-handlers.ts
6630
- import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
6631
- function sendJsonBody(res, status, body, cacheStatus) {
6632
- res.writeHead(status, {
6633
- "Content-Type": "application/json",
6634
- "X-Codemap-Cache": cacheStatus
6635
- });
6636
- res.end(body);
6637
- }
6638
- function sendJson(res, status, data, cacheStatus = "BYPASS") {
6639
- sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
6640
- }
6641
- async function serveCachedGraph(res, deps2, scope, compute) {
6642
- try {
6643
- const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
6644
- const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
6645
- const hit = getCachedCodemapBody(key, version);
6646
- if (hit !== void 0) {
6647
- sendJsonBody(res, 200, hit, "HIT");
6648
- return;
6649
- }
6650
- const graph = await compute();
6651
- const body = JSON.stringify(graph);
6652
- if (version !== "missing") {
6653
- setCachedCodemapBody(key, version, body);
6654
- }
6655
- sendJsonBody(res, 200, body, "MISS");
6656
- } catch (err) {
6657
- const msg = err instanceof Error ? err.message : String(err);
6658
- sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
6659
- }
6660
- }
6661
- async function handleCodemapPackages(res, deps2) {
6662
- await serveCachedGraph(
6663
- res,
6664
- deps2,
6665
- "packages",
6666
- () => packageGraphService({
6667
- projectRoot: deps2.projectRoot,
6668
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
6669
- })
6670
- );
6671
- }
6672
- async function handleCodemapFiles(res, deps2, pkg) {
6673
- if (!pkg) {
6674
- sendJson(res, 400, { error: 'Missing "package" query parameter' });
6675
- return;
6676
- }
6677
- await serveCachedGraph(
6678
- res,
6679
- deps2,
6680
- `files:${pkg}`,
6681
- () => fileGraphService({
6682
- projectRoot: deps2.projectRoot,
6683
- packageFilter: pkg,
6684
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
6685
- })
6686
- );
6687
- }
6688
- async function handleCodemapSymbols(res, deps2, file) {
6689
- if (!file) {
6690
- sendJson(res, 400, { error: 'Missing "file" query parameter' });
6691
- return;
6692
- }
6693
- await serveCachedGraph(
6694
- res,
6695
- deps2,
6696
- `symbols:${file}`,
6697
- () => symbolGraphService({
6698
- projectRoot: deps2.projectRoot,
6699
- fileFilter: file,
6700
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
6701
- })
6702
- );
6703
- }
6704
-
6705
7086
  // src/server/deadcode-handlers.ts
6706
7087
  import * as path10 from "node:path";
6707
7088
  import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
@@ -6852,7 +7233,84 @@ function buildActionPlan(result) {
6852
7233
  };
6853
7234
  }
6854
7235
 
7236
+ // src/server/codemap-handlers.ts
7237
+ import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
7238
+ function sendJsonBody(res, status, body, cacheStatus) {
7239
+ res.writeHead(status, {
7240
+ "Content-Type": "application/json",
7241
+ "X-Codemap-Cache": cacheStatus
7242
+ });
7243
+ res.end(body);
7244
+ }
7245
+ function sendJson(res, status, data, cacheStatus = "BYPASS") {
7246
+ sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
7247
+ }
7248
+ async function serveCachedGraph(res, deps2, scope, compute) {
7249
+ try {
7250
+ const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
7251
+ const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
7252
+ const hit = getCachedCodemapBody(key, version);
7253
+ if (hit !== void 0) {
7254
+ sendJsonBody(res, 200, hit, "HIT");
7255
+ return;
7256
+ }
7257
+ const graph = await compute();
7258
+ const body = JSON.stringify(graph);
7259
+ if (version !== "missing") {
7260
+ setCachedCodemapBody(key, version, body);
7261
+ }
7262
+ sendJsonBody(res, 200, body, "MISS");
7263
+ } catch (err) {
7264
+ const msg = err instanceof Error ? err.message : String(err);
7265
+ sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
7266
+ }
7267
+ }
7268
+ async function handleCodemapPackages(res, deps2) {
7269
+ await serveCachedGraph(
7270
+ res,
7271
+ deps2,
7272
+ "packages",
7273
+ () => packageGraphService({
7274
+ projectRoot: deps2.projectRoot,
7275
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7276
+ })
7277
+ );
7278
+ }
7279
+ async function handleCodemapFiles(res, deps2, pkg) {
7280
+ if (!pkg) {
7281
+ sendJson(res, 400, { error: 'Missing "package" query parameter' });
7282
+ return;
7283
+ }
7284
+ await serveCachedGraph(
7285
+ res,
7286
+ deps2,
7287
+ `files:${pkg}`,
7288
+ () => fileGraphService({
7289
+ projectRoot: deps2.projectRoot,
7290
+ packageFilter: pkg,
7291
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7292
+ })
7293
+ );
7294
+ }
7295
+ async function handleCodemapSymbols(res, deps2, file) {
7296
+ if (!file) {
7297
+ sendJson(res, 400, { error: 'Missing "file" query parameter' });
7298
+ return;
7299
+ }
7300
+ await serveCachedGraph(
7301
+ res,
7302
+ deps2,
7303
+ `symbols:${file}`,
7304
+ () => symbolGraphService({
7305
+ projectRoot: deps2.projectRoot,
7306
+ fileFilter: file,
7307
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7308
+ })
7309
+ );
7310
+ }
7311
+
6855
7312
  // src/server/http-server/api-handlers.ts
7313
+ import { sanitizeApiError } from "@wrongstack/core/security";
6856
7314
  async function handleApiSessions(res, globalRoot) {
6857
7315
  if (!globalRoot) {
6858
7316
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -6888,7 +7346,7 @@ async function handleApiSessions(res, globalRoot) {
6888
7346
  res.end(JSON.stringify(result));
6889
7347
  } catch (err) {
6890
7348
  res.writeHead(500, { "Content-Type": "application/json" });
6891
- res.end(JSON.stringify({ error: String(err) }));
7349
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
6892
7350
  }
6893
7351
  }
6894
7352
  async function handleApiSessionAgents(res, globalRoot, sessionId) {
@@ -6923,7 +7381,7 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
6923
7381
  }));
6924
7382
  } catch (err) {
6925
7383
  res.writeHead(500, { "Content-Type": "application/json" });
6926
- res.end(JSON.stringify({ error: String(err) }));
7384
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
6927
7385
  }
6928
7386
  }
6929
7387
  function blocksToText(content) {
@@ -7070,11 +7528,16 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
7070
7528
  );
7071
7529
  } catch (err) {
7072
7530
  res.writeHead(500, { "Content-Type": "application/json" });
7073
- res.end(JSON.stringify({ error: String(err) }));
7531
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7074
7532
  }
7075
7533
  }
7076
7534
  function readJsonBody2(req) {
7077
7535
  return new Promise((resolve15, reject) => {
7536
+ const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
7537
+ if (contentType !== "application/json") {
7538
+ reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
7539
+ return;
7540
+ }
7078
7541
  let data = "";
7079
7542
  req.on("data", (chunk) => {
7080
7543
  data += chunk;
@@ -7139,7 +7602,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
7139
7602
  res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
7140
7603
  } catch (err) {
7141
7604
  res.writeHead(500, { "Content-Type": "application/json" });
7142
- res.end(JSON.stringify({ error: String(err) }));
7605
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7143
7606
  }
7144
7607
  }
7145
7608
  async function handleApiSessionMailbox(res, globalRoot, sessionId) {
@@ -7192,7 +7655,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
7192
7655
  res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
7193
7656
  } catch (err) {
7194
7657
  res.writeHead(500, { "Content-Type": "application/json" });
7195
- res.end(JSON.stringify({ error: String(err) }));
7658
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7196
7659
  }
7197
7660
  }
7198
7661
  async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
@@ -7233,7 +7696,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7233
7696
  res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
7234
7697
  } catch (err) {
7235
7698
  res.writeHead(500, { "Content-Type": "application/json" });
7236
- res.end(JSON.stringify({ error: String(err) }));
7699
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7237
7700
  }
7238
7701
  }
7239
7702
  async function handleApiFleetBroadcast(res, req, globalRoot) {
@@ -7302,22 +7765,157 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7302
7765
  res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
7303
7766
  } catch (err) {
7304
7767
  res.writeHead(500, { "Content-Type": "application/json" });
7305
- res.end(JSON.stringify({ error: String(err) }));
7768
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7306
7769
  }
7307
7770
  }
7308
7771
 
7309
- // src/server/projects-manifest.ts
7772
+ // src/server/memory-diagnostics.ts
7310
7773
  import * as fs8 from "node:fs/promises";
7311
7774
  import * as path11 from "node:path";
7775
+ var DEFAULT_TAIL_BYTES = 1024 * 1024;
7776
+ var MAX_PROCESSES = 32;
7777
+ function finiteNumber(record, key) {
7778
+ const value = record[key];
7779
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7780
+ }
7781
+ function stringValue(record, key) {
7782
+ const value = record[key];
7783
+ return typeof value === "string" && value.length > 0 ? value : void 0;
7784
+ }
7785
+ function toProcessDiagnostic(record) {
7786
+ const pid = finiteNumber(record, "pid");
7787
+ const ts = stringValue(record, "ts");
7788
+ if (pid === void 0 || ts === void 0) return null;
7789
+ const queueEntries = finiteNumber(record, "hqQueueEntries");
7790
+ const queueBytes = finiteNumber(record, "hqQueueBytes");
7791
+ const queueMaxBytes = finiteNumber(record, "hqQueueMaxBytes");
7792
+ const snapshotInFlight = finiteNumber(record, "hqSnapshotInFlight");
7793
+ const snapshotPending = finiteNumber(record, "hqSnapshotPending");
7794
+ const snapshotTimerScheduled = finiteNumber(record, "hqSnapshotTimerScheduled");
7795
+ const eventInFlight = finiteNumber(record, "hqEventInFlight");
7796
+ const eventPending = finiteNumber(record, "hqEventPending");
7797
+ const eventCoalesced = finiteNumber(record, "hqEventCoalesced");
7798
+ const eventDropped = finiteNumber(record, "hqEventDropped");
7799
+ return {
7800
+ pid,
7801
+ surface: stringValue(record, "surface") ?? "unknown",
7802
+ ...stringValue(record, "sessionId") !== void 0 ? { sessionId: stringValue(record, "sessionId") } : {},
7803
+ ts,
7804
+ memory: {
7805
+ rss: finiteNumber(record, "rss") ?? 0,
7806
+ heapUsed: finiteNumber(record, "heapUsed") ?? 0,
7807
+ heapTotal: finiteNumber(record, "heapTotal") ?? 0,
7808
+ retainedHeapUsed: finiteNumber(record, "retainedHeapUsed"),
7809
+ nativeResidual: finiteNumber(record, "nativeResidual"),
7810
+ external: finiteNumber(record, "external"),
7811
+ arrayBuffers: finiteNumber(record, "arrayBuffers")
7812
+ },
7813
+ signal: stringValue(record, "memorySignal"),
7814
+ heapGrowthBytesPerHour: finiteNumber(record, "heapGrowthBytesPerHour"),
7815
+ rssGrowthBytesPerHour: finiteNumber(record, "rssGrowthBytesPerHour"),
7816
+ workload: {
7817
+ messages: finiteNumber(record, "messages"),
7818
+ messageEstimatedTokens: finiteNumber(record, "messageEstimatedTokens"),
7819
+ historyEntries: finiteNumber(record, "historyEntries"),
7820
+ historyMountedEntries: finiteNumber(record, "historyMountedEntries"),
7821
+ historyCachedGroups: finiteNumber(record, "historyCachedGroups"),
7822
+ appRenders: finiteNumber(record, "appRenders"),
7823
+ metricsDroppedObservations: finiteNumber(record, "metricsDroppedObservations"),
7824
+ kanbanSyncActive: finiteNumber(record, "kanbanSyncActive") === 1,
7825
+ kanbanSyncPendingBoards: finiteNumber(record, "kanbanSyncPendingBoards"),
7826
+ kanbanSyncFullRescanPending: finiteNumber(record, "kanbanSyncFullRescanPending") === 1,
7827
+ kanbanSyncRemoteApplyQueued: finiteNumber(record, "kanbanSyncRemoteApplyQueued") === 1,
7828
+ kanbanSyncPendingRemoteBoards: finiteNumber(record, "kanbanSyncPendingRemoteBoards"),
7829
+ kanbanSyncPublishRuns: finiteNumber(record, "kanbanSyncPublishRuns"),
7830
+ kanbanSyncCoalescedRefreshes: finiteNumber(record, "kanbanSyncCoalescedRefreshes"),
7831
+ kanbanSupervisorSnapshots: finiteNumber(record, "kanbanSupervisorSnapshots"),
7832
+ kanbanSupervisorScheduledBoards: finiteNumber(record, "kanbanSupervisorScheduledBoards"),
7833
+ kanbanSupervisorAgentCooldowns: finiteNumber(record, "kanbanSupervisorAgentCooldowns"),
7834
+ kanbanSupervisorRunningAgents: finiteNumber(record, "kanbanSupervisorRunningAgents")
7835
+ },
7836
+ resources: {
7837
+ active: finiteNumber(record, "activeResources"),
7838
+ types: stringValue(record, "activeResourceTypes")
7839
+ },
7840
+ ...queueEntries !== void 0 && queueBytes !== void 0 && queueMaxBytes !== void 0 ? {
7841
+ hqQueue: {
7842
+ entries: queueEntries,
7843
+ bytes: queueBytes,
7844
+ maxBytes: queueMaxBytes,
7845
+ droppedFrames: finiteNumber(record, "hqQueueDroppedFrames") ?? 0,
7846
+ droppedBytes: finiteNumber(record, "hqQueueDroppedBytes") ?? 0,
7847
+ coalescedFrames: finiteNumber(record, "hqQueueCoalescedFrames") ?? 0,
7848
+ coalescedBytes: finiteNumber(record, "hqQueueCoalescedBytes") ?? 0
7849
+ }
7850
+ } : {},
7851
+ ...snapshotInFlight !== void 0 || snapshotPending !== void 0 || snapshotTimerScheduled !== void 0 ? {
7852
+ hqSnapshot: {
7853
+ inFlight: snapshotInFlight === 1,
7854
+ pending: snapshotPending === 1,
7855
+ timerScheduled: snapshotTimerScheduled === 1,
7856
+ eventInFlight: eventInFlight === 1,
7857
+ pendingEvents: eventPending ?? 0,
7858
+ coalescedEvents: eventCoalesced ?? 0,
7859
+ droppedEvents: eventDropped ?? 0
7860
+ }
7861
+ } : {},
7862
+ profileTopStack: stringValue(record, "memoryProfileTopStack"),
7863
+ profileTopStackBytes: finiteNumber(record, "memoryProfileTopStackBytes")
7864
+ };
7865
+ }
7866
+ async function readRecentProcessMemoryDiagnostics(globalRoot, tailBytes = DEFAULT_TAIL_BYTES) {
7867
+ if (!globalRoot) return [];
7868
+ const heapLog = path11.join(globalRoot, "logs", "heap.jsonl");
7869
+ let handle;
7870
+ try {
7871
+ handle = await fs8.open(heapLog, "r");
7872
+ const stat3 = await handle.stat();
7873
+ const length = Math.min(stat3.size, Math.max(1, tailBytes));
7874
+ if (length === 0) return [];
7875
+ const start = stat3.size - length;
7876
+ const buffer = Buffer.allocUnsafe(length);
7877
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
7878
+ let text = buffer.subarray(0, bytesRead).toString("utf8");
7879
+ if (start > 0) {
7880
+ const firstNewline = text.indexOf("\n");
7881
+ if (firstNewline === -1) return [];
7882
+ text = text.slice(firstNewline + 1);
7883
+ }
7884
+ const newestByPid = /* @__PURE__ */ new Map();
7885
+ const lines = text.split(/\r?\n/u);
7886
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
7887
+ const line = lines[index]?.trim();
7888
+ if (!line) continue;
7889
+ try {
7890
+ const parsed = JSON.parse(line);
7891
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
7892
+ const diagnostic = toProcessDiagnostic(parsed);
7893
+ if (!diagnostic || newestByPid.has(diagnostic.pid)) continue;
7894
+ newestByPid.set(diagnostic.pid, diagnostic);
7895
+ if (newestByPid.size >= MAX_PROCESSES) break;
7896
+ } catch {
7897
+ }
7898
+ }
7899
+ return [...newestByPid.values()].sort((a, b) => b.ts.localeCompare(a.ts));
7900
+ } catch {
7901
+ return [];
7902
+ } finally {
7903
+ await handle?.close().catch(() => void 0);
7904
+ }
7905
+ }
7906
+
7907
+ // src/server/projects-manifest.ts
7908
+ import * as fs9 from "node:fs/promises";
7909
+ import * as path12 from "node:path";
7312
7910
  import { ConfigError } from "@wrongstack/core/types";
7313
7911
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7314
7912
  function projectsJsonPath(globalConfigPath) {
7315
- const base = path11.dirname(globalConfigPath);
7316
- return path11.join(base, "projects.json");
7913
+ const base = path12.dirname(globalConfigPath);
7914
+ return path12.join(base, "projects.json");
7317
7915
  }
7318
7916
  async function loadManifest(globalConfigPath) {
7319
7917
  try {
7320
- const raw = await fs8.readFile(projectsJsonPath(globalConfigPath), "utf8");
7918
+ const raw = await fs9.readFile(projectsJsonPath(globalConfigPath), "utf8");
7321
7919
  const parsed = JSON.parse(raw);
7322
7920
  return { projects: parsed.projects ?? [] };
7323
7921
  } catch {
@@ -7326,37 +7924,37 @@ async function loadManifest(globalConfigPath) {
7326
7924
  }
7327
7925
  async function saveManifest(manifest, globalConfigPath) {
7328
7926
  const file = projectsJsonPath(globalConfigPath);
7329
- await fs8.mkdir(path11.dirname(file), { recursive: true });
7330
- await fs8.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7927
+ await fs9.mkdir(path12.dirname(file), { recursive: true });
7928
+ await fs9.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7331
7929
  }
7332
7930
  function generateProjectSlug(rootPath) {
7333
7931
  return projectSlug(rootPath);
7334
7932
  }
7335
7933
  async function ensureProjectDataDir(slug, globalConfigPath) {
7336
- const base = path11.dirname(globalConfigPath);
7337
- const dir = path11.join(base, "projects", slug);
7338
- await fs8.mkdir(dir, { recursive: true });
7934
+ const base = path12.dirname(globalConfigPath);
7935
+ const dir = path12.join(base, "projects", slug);
7936
+ await fs9.mkdir(dir, { recursive: true });
7339
7937
  return dir;
7340
7938
  }
7341
7939
  async function touchProjectInManifest(options, globalConfigPath) {
7342
- const root = path11.resolve(options.projectRoot);
7940
+ const root = path12.resolve(options.projectRoot);
7343
7941
  const file = projectsJsonPath(globalConfigPath);
7344
7942
  let entry;
7345
7943
  await withFileLock(file, async () => {
7346
7944
  const manifest = await loadManifest(globalConfigPath);
7347
7945
  const now = (/* @__PURE__ */ new Date()).toISOString();
7348
- entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7946
+ entry = manifest.projects.find((candidate) => path12.resolve(candidate.root) === root);
7349
7947
  if (entry) {
7350
7948
  entry.lastSeen = now;
7351
- if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7949
+ if (options.workingDir) entry.lastWorkingDir = path12.resolve(options.workingDir);
7352
7950
  } else {
7353
7951
  entry = {
7354
- name: options.name ?? path11.basename(root),
7952
+ name: options.name ?? path12.basename(root),
7355
7953
  root,
7356
7954
  slug: generateProjectSlug(root),
7357
7955
  createdAt: now,
7358
7956
  lastSeen: now,
7359
- lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7957
+ lastWorkingDir: options.workingDir ? path12.resolve(options.workingDir) : void 0
7360
7958
  };
7361
7959
  manifest.projects.push(entry);
7362
7960
  }
@@ -7598,11 +8196,20 @@ import { timingSafeEqual } from "node:crypto";
7598
8196
  function isLoopbackHostname(hostname) {
7599
8197
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
7600
8198
  }
7601
- function isTrustedLoopbackOrigin(origin) {
8199
+ function effectivePort(url) {
8200
+ if (url.port) return url.port;
8201
+ return url.protocol === "https:" ? "443" : "80";
8202
+ }
8203
+ function isTrustedLoopbackOrigin(origin, hostHeader) {
7602
8204
  try {
7603
8205
  const url = new URL(origin);
7604
8206
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
7605
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
8207
+ if (!isLoopbackHostname(url.hostname)) return false;
8208
+ const host = (hostHeader ?? "").trim();
8209
+ if (!host) return false;
8210
+ const hostUrl = new URL(`${url.protocol}//${host}`);
8211
+ if (!isLoopbackHostname(hostUrl.hostname)) return false;
8212
+ return effectivePort(url) === effectivePort(hostUrl);
7606
8213
  } catch {
7607
8214
  return false;
7608
8215
  }
@@ -7661,6 +8268,23 @@ function hostHeaderOk(input) {
7661
8268
  }
7662
8269
  return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
7663
8270
  }
8271
+ function httpRequestOriginOk(input) {
8272
+ const { origin, hostHeader, wsHost, allowedHostnames } = input;
8273
+ if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
8274
+ if (origin === void 0 || origin === "") return true;
8275
+ if (origin === "null") return false;
8276
+ try {
8277
+ const parsed = new URL(origin);
8278
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
8279
+ const rawHost = (hostHeader ?? "").trim();
8280
+ if (!rawHost) return false;
8281
+ const requestHost = new URL(`${parsed.protocol}//${rawHost}`).host.toLowerCase();
8282
+ if (parsed.host.toLowerCase() === requestHost) return true;
8283
+ return allowedHostname(parsed.hostname, allowedHostnames);
8284
+ } catch {
8285
+ return false;
8286
+ }
8287
+ }
7664
8288
  function verifyClient(input) {
7665
8289
  const {
7666
8290
  origin,
@@ -7681,13 +8305,13 @@ function verifyClient(input) {
7681
8305
  const remoteIp = remoteAddress ?? "";
7682
8306
  const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
7683
8307
  if (!isRemoteLoopback && isWildcardBind(wsHost) && !urlTokenOk && !cookieTokenOk) return false;
7684
- return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
8308
+ return urlTokenOk || cookieTokenOk;
7685
8309
  }
7686
8310
  try {
7687
8311
  const { hostname: originHostname } = new URL(origin);
7688
8312
  if (isLoopbackHostname(originHostname)) {
7689
8313
  if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
7690
- return isTrustedLoopbackOrigin(origin);
8314
+ return cookieTokenOk || isTrustedLoopbackOrigin(origin, hostHeader);
7691
8315
  }
7692
8316
  return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
7693
8317
  } catch {
@@ -7749,7 +8373,7 @@ function cspSourceFromUrl(rawUrl) {
7749
8373
  return void 0;
7750
8374
  }
7751
8375
  }
7752
- var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'", "'unsafe-inline'"];
8376
+ var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'"];
7753
8377
  function buildCspHeader(publicWsUrl, host, port) {
7754
8378
  const connect = /* @__PURE__ */ new Set(["'self'"]);
7755
8379
  const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
@@ -7767,9 +8391,9 @@ function buildCspHeader(publicWsUrl, host, port) {
7767
8391
  return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; connect-src ${Array.from(connect).join(" ")}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
7768
8392
  }
7769
8393
  function isInsideDist(candidate, distDir) {
7770
- const root = path12.resolve(distDir);
7771
- const resolved = path12.resolve(candidate);
7772
- return resolved === root || resolved.startsWith(root + path12.sep);
8394
+ const root = path13.resolve(distDir);
8395
+ const resolved = path13.resolve(candidate);
8396
+ return resolved === root || resolved.startsWith(root + path13.sep);
7773
8397
  }
7774
8398
  function decodeSessionId(segment) {
7775
8399
  try {
@@ -7789,8 +8413,18 @@ function strictDecodeParam(segment, res) {
7789
8413
  }
7790
8414
  function createHttpServer(opts) {
7791
8415
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
7792
- const distDir = path12.resolve(opts.distDir);
8416
+ const distDir = path13.resolve(opts.distDir);
7793
8417
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
8418
+ const trustedHostnames = (() => {
8419
+ const names = [...opts.allowedHostnames ?? []];
8420
+ if (opts.publicWsUrl) {
8421
+ try {
8422
+ names.push(new URL(opts.publicWsUrl).hostname);
8423
+ } catch {
8424
+ }
8425
+ }
8426
+ return names;
8427
+ })();
7794
8428
  let techStackRuntime = null;
7795
8429
  const getTechStackRuntime = async () => {
7796
8430
  if (!opts.projectRoot) throw new Error("Project root not configured");
@@ -7805,6 +8439,16 @@ function createHttpServer(opts) {
7805
8439
  const server = http.createServer(async (req, res) => {
7806
8440
  try {
7807
8441
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
8442
+ if (!httpRequestOriginOk({
8443
+ origin: req.headers.origin,
8444
+ hostHeader: req.headers.host,
8445
+ wsHost: opts.host,
8446
+ allowedHostnames: trustedHostnames
8447
+ })) {
8448
+ res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
8449
+ res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
8450
+ return;
8451
+ }
7808
8452
  const providedAccessToken = requestToken(req, url);
7809
8453
  const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
7810
8454
  const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
@@ -8008,42 +8652,6 @@ function createHttpServer(opts) {
8008
8652
  );
8009
8653
  return;
8010
8654
  }
8011
- if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
8012
- if (requireAccessToken && !accessTokenOk) {
8013
- res.writeHead(401, { "Content-Type": "application/json" });
8014
- res.end(JSON.stringify({ error: "Unauthorized" }));
8015
- return;
8016
- }
8017
- if (!opts.projectRoot) {
8018
- res.writeHead(503, { "Content-Type": "application/json" });
8019
- res.end(JSON.stringify({ error: "Project root not configured" }));
8020
- return;
8021
- }
8022
- const deadCodeDeps = {
8023
- projectRoot: opts.projectRoot,
8024
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8025
- };
8026
- await handleDeadCodeScan(res, deadCodeDeps, req);
8027
- return;
8028
- }
8029
- if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8030
- if (requireAccessToken && !accessTokenOk) {
8031
- res.writeHead(401, { "Content-Type": "application/json" });
8032
- res.end(JSON.stringify({ error: "Unauthorized" }));
8033
- return;
8034
- }
8035
- if (!opts.projectRoot) {
8036
- res.writeHead(503, { "Content-Type": "application/json" });
8037
- res.end(JSON.stringify({ error: "Project root not configured" }));
8038
- return;
8039
- }
8040
- const deadCodeDeps = {
8041
- projectRoot: opts.projectRoot,
8042
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8043
- };
8044
- await handleDeadCodeActionPlan(res, deadCodeDeps, req);
8045
- return;
8046
- }
8047
8655
  if (url.pathname.startsWith("/api/techstack/")) {
8048
8656
  if (requireAccessToken && !accessTokenOk) {
8049
8657
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -8117,11 +8725,7 @@ function createHttpServer(opts) {
8117
8725
  if (researchMatch && req.method === "POST") {
8118
8726
  const pkg = strictDecodeParam(researchMatch[1], res);
8119
8727
  if (pkg === null) return;
8120
- await handleTechStackDependencyResearch(
8121
- res,
8122
- deps2,
8123
- pkg
8124
- );
8728
+ await handleTechStackDependencyResearch(res, deps2, pkg);
8125
8729
  return;
8126
8730
  }
8127
8731
  } catch (error2) {
@@ -8129,7 +8733,7 @@ function createHttpServer(opts) {
8129
8733
  res.end(
8130
8734
  JSON.stringify({
8131
8735
  error: "TechStack store unavailable",
8132
- detail: error2 instanceof Error ? error2.message : String(error2)
8736
+ detail: sanitizeApiError2(error2)
8133
8737
  })
8134
8738
  );
8135
8739
  return;
@@ -8157,6 +8761,7 @@ function createHttpServer(opts) {
8157
8761
  return;
8158
8762
  }
8159
8763
  if (url.pathname === "/debug/system" && req.method === "GET") {
8764
+ const processes = await readRecentProcessMemoryDiagnostics(opts.globalRoot);
8160
8765
  res.writeHead(200, {
8161
8766
  "Content-Type": "application/json",
8162
8767
  "Cache-Control": "no-store"
@@ -8169,31 +8774,56 @@ function createHttpServer(opts) {
8169
8774
  uptime: process.uptime(),
8170
8775
  cpuUsage: process.cpuUsage(),
8171
8776
  codebaseIndexServer: getIndexState2().server,
8777
+ processes,
8172
8778
  timestamp: Date.now()
8173
8779
  })
8174
8780
  );
8175
8781
  return;
8176
8782
  }
8783
+ if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
8784
+ if (!opts.projectRoot) {
8785
+ res.writeHead(400, { "Content-Type": "application/json" });
8786
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8787
+ return;
8788
+ }
8789
+ await handleDeadCodeScan(res, {
8790
+ projectRoot: opts.projectRoot,
8791
+ indexDir: opts.indexDir
8792
+ }, req);
8793
+ return;
8794
+ }
8795
+ if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8796
+ await handleDeadCodeActionPlan(res, {
8797
+ projectRoot: opts.projectRoot ?? "",
8798
+ indexDir: opts.indexDir
8799
+ }, req);
8800
+ return;
8801
+ }
8802
+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
8803
+ res.writeHead(404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
8804
+ res.end(JSON.stringify({ error: "Not found" }));
8805
+ return;
8806
+ }
8177
8807
  let filePath;
8178
8808
  if (url.pathname === "/" || url.pathname === "") {
8179
- filePath = path12.join(distDir, "index.html");
8809
+ filePath = path13.join(distDir, "index.html");
8180
8810
  } else {
8181
- filePath = path12.join(distDir, url.pathname);
8811
+ filePath = path13.join(distDir, url.pathname);
8182
8812
  }
8183
- const resolvedPath = path12.resolve(filePath);
8813
+ const resolvedPath = path13.resolve(filePath);
8184
8814
  if (!isInsideDist(resolvedPath, distDir)) {
8185
8815
  res.writeHead(403, { "Content-Type": "text/plain" });
8186
8816
  res.end("Forbidden");
8187
8817
  return;
8188
8818
  }
8189
- const ext = path12.extname(resolvedPath);
8819
+ const ext = path13.extname(resolvedPath);
8190
8820
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8191
8821
  res.setHeader("Content-Type", contentType);
8192
8822
  setStaticSecurityHeaders(res);
8193
8823
  if (ext === ".html") {
8194
8824
  if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
8195
8825
  res.setHeader("Content-Security-Policy", buildCspHeader(opts.publicWsUrl, opts.host, port));
8196
- const html = await fs9.readFile(resolvedPath, "utf8");
8826
+ const html = await fs10.readFile(resolvedPath, "utf8");
8197
8827
  res.writeHead(200);
8198
8828
  res.end(injectWsConfig(html, { publicWsUrl: opts.publicWsUrl }));
8199
8829
  return;
@@ -8204,13 +8834,13 @@ function createHttpServer(opts) {
8204
8834
  url.pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=3600"
8205
8835
  );
8206
8836
  }
8207
- const fileContent = await fs9.readFile(resolvedPath);
8837
+ const fileContent = await fs10.readFile(resolvedPath);
8208
8838
  res.writeHead(200);
8209
8839
  res.end(fileContent);
8210
8840
  } catch (err) {
8211
8841
  if (err.code === "ENOENT") {
8212
8842
  try {
8213
- const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8843
+ const html = await fs10.readFile(path13.join(distDir, "index.html"), "utf8");
8214
8844
  setStaticSecurityHeaders(res);
8215
8845
  res.writeHead(200, {
8216
8846
  "Content-Type": "text/html",
@@ -8240,14 +8870,14 @@ function createHttpServer(opts) {
8240
8870
 
8241
8871
  // src/server/instance-registry.ts
8242
8872
  import * as os from "node:os";
8243
- import * as path13 from "node:path";
8244
- import * as fs10 from "node:fs/promises";
8873
+ import * as path14 from "node:path";
8874
+ import * as fs11 from "node:fs/promises";
8245
8875
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8246
8876
  function defaultBaseDir() {
8247
- return path13.join(os.homedir(), ".wrongstack");
8877
+ return path14.join(os.homedir(), ".wrongstack");
8248
8878
  }
8249
8879
  function registryPath(baseDir = defaultBaseDir()) {
8250
- return path13.join(baseDir, "webui-instances.json");
8880
+ return path14.join(baseDir, "webui-instances.json");
8251
8881
  }
8252
8882
  function isPidAlive(pid) {
8253
8883
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -8260,7 +8890,7 @@ function isPidAlive(pid) {
8260
8890
  }
8261
8891
  async function load(file) {
8262
8892
  try {
8263
- const raw = await fs10.readFile(file, "utf8");
8893
+ const raw = await fs11.readFile(file, "utf8");
8264
8894
  const parsed = JSON.parse(raw);
8265
8895
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
8266
8896
  return parsed;
@@ -8472,6 +9102,11 @@ async function handleIntrospectionRoute(ctx, ws, message) {
8472
9102
  cwd: ctx.getProjectRoot(),
8473
9103
  sessionId: ctx.getSessionId(),
8474
9104
  tools: { count: tools.length, names: tools.map((tool) => tool.name) },
9105
+ maxTools: actx.provider.maxToolsCount ?? 0,
9106
+ droppedTools: (() => {
9107
+ const mt = actx.provider.maxToolsCount ?? 0;
9108
+ return mt > 0 ? Math.max(0, tools.length - mt) : 0;
9109
+ })(),
8475
9110
  features: {
8476
9111
  memory: !!config.features?.memory,
8477
9112
  skills: !!config.features?.skills,
@@ -8573,40 +9208,17 @@ async function handleIntrospectionRoute(ctx, ws, message) {
8573
9208
  }
8574
9209
 
8575
9210
  // src/server/kanban-dispatch.ts
8576
- import { randomUUID as randomUUID3 } from "node:crypto";
8577
9211
  import {
8578
9212
  areDependenciesMet,
8579
9213
  getServerKanbanStore
8580
9214
  } from "@wrongstack/kanban";
8581
9215
  import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
8582
- function parseResolvedDispatchRoute(summary) {
8583
- const tags = summary.match(/Spawned subagent\s+\S+\s+\((.*?)\)\s+for task/i)?.[1];
8584
- if (!tags) return {};
8585
- const parts = tags.split(/\s+\/\s+/).map((part) => part.trim());
8586
- const positional = parts.filter((part) => !part.includes("=") && !part.startsWith('"'));
8587
- const fallbackProfile = parts.find((part) => part.startsWith("profile="))?.slice(8);
8588
- const fallback = parts.find((part) => part.startsWith("fallback="))?.slice(9);
8589
- return {
8590
- ...positional[0] ? { provider: positional[0] } : {},
8591
- ...positional[1] ? { model: positional[1] } : {},
8592
- ...fallbackProfile ? { fallbackProfile } : {},
8593
- ...fallback ? { fallbackModels: fallback.split(",").filter(Boolean) } : {}
8594
- };
8595
- }
8596
9216
  function reply(ws, type, success, value) {
8597
9217
  send(ws, {
8598
9218
  type,
8599
9219
  payload: success ? { success: true, data: value } : { success: false, error: String(value) }
8600
9220
  });
8601
9221
  }
8602
- function activityContext(ctx, note, expectedLeaseId) {
8603
- const sessionId = ctx.context?.session?.id;
8604
- return {
8605
- ...sessionId ? { sessionId } : {},
8606
- ...note?.trim() ? { note: note.trim() } : {},
8607
- ...expectedLeaseId ? { expectedLeaseId } : {}
8608
- };
8609
- }
8610
9222
  async function handleKanbanTaskDispatch(ws, payload, ctx) {
8611
9223
  const boardId = payload?.boardId;
8612
9224
  const taskId = payload?.taskId;
@@ -8645,45 +9257,39 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
8645
9257
  }
8646
9258
  const modelRouting = payload?.modelRouting ?? task.assignment?.modelRouting ?? (payload?.provider || payload?.model || task.assignment?.provider || task.assignment?.model ? "fixed" : "session");
8647
9259
  const useSessionModel = modelRouting === "session";
8648
- const leaseId = randomUUID3();
8649
- const now = (/* @__PURE__ */ new Date()).toISOString();
8650
9260
  const leaseTtlMs = 30 * 60 * 1e3;
8651
- const leaseExpiresAt = new Date(Date.now() + leaseTtlMs).toISOString();
8652
- const assignment = {
8653
- agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
8654
- name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
8655
- role: payload?.role ?? task.assignment?.role,
8656
- modelRouting,
8657
- provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
8658
- model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
8659
- fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
8660
- fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
8661
- skills: payload?.skills ?? task.assignment?.skills,
8662
- tools: payload?.tools ?? task.assignment?.tools,
8663
- allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities,
8664
- maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
8665
- costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
8666
- retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy,
8667
- attempt: (task.assignment?.attempt ?? 0) + 1,
8668
- status: "queued",
8669
- dispatchedAt: now,
8670
- leaseId,
8671
- claimedAt: now,
8672
- heartbeatAt: now,
8673
- leaseExpiresAt
8674
- };
8675
- const assignedBoard = await store.assignTask(
9261
+ const reserved = await store.reserveKanbanDispatch({
8676
9262
  boardId,
8677
- task.id,
8678
- assignment,
8679
- activityContext(ctx, payload?.activityNote)
8680
- );
8681
- if (!assignedBoard) {
8682
- reply(ws, "kanban.task.dispatch", false, `Failed to assign task "${task.title}". The task may have been removed or the board does not allow external assignment.`);
9263
+ taskId: task.id,
9264
+ routing: {
9265
+ agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
9266
+ name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
9267
+ role: payload?.role ?? task.assignment?.role,
9268
+ modelRouting,
9269
+ provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
9270
+ model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
9271
+ fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
9272
+ fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
9273
+ skills: payload?.skills ?? task.assignment?.skills,
9274
+ tools: payload?.tools ?? task.assignment?.tools,
9275
+ allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities
9276
+ },
9277
+ budget: {
9278
+ maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
9279
+ costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
9280
+ retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy
9281
+ },
9282
+ leaseTtlMs,
9283
+ heartbeatIntervalMs: Math.floor(leaseTtlMs / 2)
9284
+ });
9285
+ if (!reserved) {
9286
+ reply(ws, "kanban.task.dispatch", false, `Failed to reserve task "${task.title}". The task may have been claimed by another agent or removed.`);
8683
9287
  return;
8684
9288
  }
9289
+ const dispatchLeaseId = reserved.lease.leaseId;
8685
9290
  try {
8686
- const summary = await ctx.dispatchTask(buildKanbanAgentPrompt(board, task, assignment, leaseId), {
9291
+ const assignment = reserved.task.assignment ?? { status: "queued" };
9292
+ const summary = await ctx.dispatchTask(buildKanbanAgentPrompt(board, task, assignment, dispatchLeaseId), {
8687
9293
  ...assignment.provider ? { provider: assignment.provider } : {},
8688
9294
  ...assignment.model ? { model: assignment.model } : {},
8689
9295
  ...assignment.fallbackModels ? { fallbackModels: assignment.fallbackModels } : {},
@@ -8692,69 +9298,40 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
8692
9298
  ...assignment.tools ? { tools: assignment.tools } : {},
8693
9299
  ...assignment.name ? { name: assignment.name } : {},
8694
9300
  ...assignment.allowedCapabilities ? { allowedCapabilities: assignment.allowedCapabilities } : {},
8695
- context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId } },
9301
+ context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId: dispatchLeaseId } },
8696
9302
  onDone: async (result) => {
8697
- const terminalWrite = await store.updateTaskAssignment(
8698
- boardId,
8699
- task.id,
8700
- {
8701
- ...assignment,
8702
- status: result.status,
8703
- ...result.result !== void 0 ? { lastResult: result.result } : {},
8704
- ...result.error !== void 0 ? { error: result.error } : {}
8705
- },
8706
- activityContext(ctx, void 0, leaseId)
8707
- );
8708
- if (result.status === "completed" && terminalWrite) {
8709
- try {
8710
- const finalized = await store.finalizeTaskCompletion(boardId, task.id, {
8711
- eventContext: activityContext(ctx, void 0, leaseId)
8712
- });
8713
- if (finalized?.gate.report && ctx.context) {
8714
- recordKanbanVerificationEvidence(ctx.context, finalized.gate.report);
8715
- }
8716
- } catch (error2) {
8717
- console.warn(
8718
- `[kanban-dispatch] completion gate failed for task ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
8719
- );
8720
- }
8721
- if (terminalWrite.lifecycle?.mode === "managed") {
8722
- const managedTask = terminalWrite.tasks.find(
8723
- (candidate) => candidate.id === task.id
8724
- );
8725
- if (managedTask && managedTask.lifecycle?.currentStage === "running") {
8726
- const actor = ctx.context?.agentId ?? "kanban-agent";
8727
- const comment = typeof result.result === "string" && result.result.trim().length > 0 ? result.result.trim().slice(0, 1e3) : "Work completed.";
8728
- try {
8729
- await store.transitionTask(boardId, task.id, {
8730
- to: "review",
8731
- actor,
8732
- comment,
8733
- attachment: {
8734
- url: `kanban://task/${task.id}/result`,
8735
- title: "Worker completion result",
8736
- type: "file"
8737
- }
8738
- });
8739
- } catch (transitionError) {
8740
- console.warn(
8741
- `[kanban-dispatch] auto-transition to review failed for task ${task.id}: ${transitionError instanceof Error ? transitionError.message : String(transitionError)}`
8742
- );
8743
- }
8744
- }
9303
+ if (result.status === "completed") {
9304
+ const completed = await store.completeKanbanDispatch({
9305
+ boardId,
9306
+ taskId: task.id,
9307
+ leaseId: dispatchLeaseId,
9308
+ actor: ctx.context?.agentId ?? "kanban-agent",
9309
+ result: result.result,
9310
+ evidence: result.result ? { url: `kanban://task/${task.id}/result`, title: "Worker completion result", type: "file" } : void 0
9311
+ });
9312
+ if (completed?.task.verificationReport && ctx.context) {
9313
+ recordKanbanVerificationEvidence(ctx.context, completed.task.verificationReport);
8745
9314
  }
9315
+ } else {
9316
+ await store.failKanbanDispatch({
9317
+ boardId,
9318
+ taskId: task.id,
9319
+ leaseId: dispatchLeaseId,
9320
+ actor: ctx.context?.agentId ?? "kanban-agent",
9321
+ error: result.error ?? "Task failed."
9322
+ });
8746
9323
  }
8747
9324
  const reconciled = await store.reconcileBoard(boardId);
8748
- const completed = reconciled?.board ?? await store.getBoard(boardId);
8749
- const completedTask = completed?.tasks.find((candidate) => candidate.id === task.id) ?? task;
9325
+ const completedBoard = reconciled?.board ?? await store.getBoard(boardId);
9326
+ const completedTask = completedBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
8750
9327
  ctx.broadcast?.({
8751
9328
  type: "kanban.task.update",
8752
9329
  payload: { success: true, data: { boardId: board.id, task: completedTask } }
8753
9330
  });
8754
- if (completed) {
9331
+ if (completedBoard) {
8755
9332
  ctx.broadcast?.({
8756
9333
  type: "kanban.get",
8757
- payload: { success: true, data: { board: completed } }
9334
+ payload: { success: true, data: { board: completedBoard } }
8758
9335
  });
8759
9336
  }
8760
9337
  ctx.broadcast?.({
@@ -8765,55 +9342,32 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
8765
9342
  });
8766
9343
  const subagentId = summary.match(/Spawned subagent\s+([^\s]+)/)?.[1];
8767
9344
  const runTaskId = summary.match(/\bfor task\s+([^\s.]+)/i)?.[1];
8768
- const resolvedRoute = parseResolvedDispatchRoute(summary);
8769
- const updated = await store.updateTaskAssignment(
9345
+ const started = await store.startKanbanDispatch({
8770
9346
  boardId,
8771
- task.id,
8772
- {
8773
- ...assignment,
8774
- ...resolvedRoute,
8775
- status: "running",
8776
- ...subagentId ? { subagentId } : {},
8777
- ...runTaskId ? { runTaskId } : {},
8778
- lastResult: summary
8779
- },
8780
- activityContext(ctx, void 0, leaseId)
8781
- );
8782
- let transitionBoard = updated;
8783
- if (updated?.lifecycle?.mode === "managed") {
8784
- const managedTask = updated.tasks.find((candidate) => candidate.id === task.id);
8785
- if (managedTask && managedTask.lifecycle?.currentStage === "todo") {
8786
- try {
8787
- const tr = await store.transitionTask(boardId, task.id, {
8788
- to: "running",
8789
- actor: ctx.context?.agentId ?? "kanban-agent",
8790
- comment: "Work started."
8791
- });
8792
- if (tr) transitionBoard = tr.board;
8793
- } catch (transitionErr) {
8794
- console.warn(
8795
- `[kanban-dispatch] auto-transition to Running failed for task ${task.id}: ${transitionErr instanceof Error ? transitionErr.message : String(transitionErr)}`
8796
- );
8797
- }
8798
- }
8799
- }
8800
- const runningTask = transitionBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
9347
+ taskId: task.id,
9348
+ leaseId: dispatchLeaseId,
9349
+ actor: ctx.context?.agentId ?? "kanban-agent",
9350
+ ...subagentId ? { subagentId } : {},
9351
+ ...runTaskId ? { runTaskId } : {}
9352
+ });
9353
+ const runningTask = started?.task ?? (started?.board.tasks.find((candidate) => candidate.id === task.id) ?? task);
8801
9354
  ctx.broadcast?.({
8802
9355
  type: "kanban.task.update",
8803
9356
  payload: { success: true, data: { boardId: board.id, task: runningTask } }
8804
9357
  });
8805
- if (updated) {
8806
- ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: updated } } });
9358
+ if (started?.board) {
9359
+ ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: started.board } } });
8807
9360
  }
8808
9361
  reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
8809
9362
  } catch (error2) {
8810
9363
  const message = error2 instanceof Error ? error2.message : String(error2);
8811
- await store.updateTaskAssignment(
9364
+ await store.failKanbanDispatch({
8812
9365
  boardId,
8813
- task.id,
8814
- { ...assignment, status: "failed", error: message },
8815
- activityContext(ctx, void 0, leaseId)
8816
- );
9366
+ taskId: task.id,
9367
+ leaseId: dispatchLeaseId,
9368
+ actor: ctx.context?.agentId ?? "kanban-agent",
9369
+ error: message
9370
+ });
8817
9371
  reply(ws, "kanban.task.dispatch", false, message);
8818
9372
  }
8819
9373
  }
@@ -8967,7 +9521,7 @@ function fail(ws, type, message) {
8967
9521
  function has(payload, key) {
8968
9522
  return payload !== void 0 && Object.hasOwn(payload, key);
8969
9523
  }
8970
- function activityContext2(ctx, actor, note) {
9524
+ function activityContext(ctx, actor, note) {
8971
9525
  const sessionId = ctx.context?.session?.id;
8972
9526
  return {
8973
9527
  ...sessionId ? { sessionId } : {},
@@ -9204,7 +9758,7 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
9204
9758
  outcome,
9205
9759
  ...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
9206
9760
  },
9207
- activityContext2(
9761
+ activityContext(
9208
9762
  ctx,
9209
9763
  payload?.actor ?? ctx.context?.agentId ?? "webui"
9210
9764
  )
@@ -9518,7 +10072,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9518
10072
  ...payload?.labels ? { labels: payload.labels } : {},
9519
10073
  ...has(payload, "boundary") ? { boundary: payload?.boundary } : {}
9520
10074
  },
9521
- activityContext2(ctx, "webui", payload?.activityNote)
10075
+ activityContext(ctx, "webui", payload?.activityNote)
9522
10076
  );
9523
10077
  result ? ok(ws, type, result.task) : fail(ws, type, `Board not found: ${boardId}`);
9524
10078
  return true;
@@ -9602,7 +10156,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9602
10156
  boundary: payload?.boundary ?? null
9603
10157
  } : {}
9604
10158
  },
9605
- activityContext2(ctx, "webui", payload?.activityNote)
10159
+ activityContext(ctx, "webui", payload?.activityNote)
9606
10160
  );
9607
10161
  if (!board) {
9608
10162
  fail(ws, type, "Board or task not found");
@@ -9653,7 +10207,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9653
10207
  taskId,
9654
10208
  columnId,
9655
10209
  payload?.order,
9656
- activityContext2(ctx, "webui", payload?.activityNote)
10210
+ activityContext(ctx, "webui", payload?.activityNote)
9657
10211
  );
9658
10212
  if (!board) {
9659
10213
  fail(ws, type, "Move failed");
@@ -9770,7 +10324,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9770
10324
  ...payload?.unit ? { unit: payload.unit } : {},
9771
10325
  ...payload?.notes ? { notes: payload.notes } : {}
9772
10326
  },
9773
- activityContext2(
10327
+ activityContext(
9774
10328
  ctx,
9775
10329
  "webui",
9776
10330
  payload?.activityNote ?? `Goal metric added: ${name2}.`
@@ -9800,7 +10354,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9800
10354
  ...payload?.unit ? { unit: payload.unit } : {},
9801
10355
  ...payload?.notes ? { notes: payload.notes } : {}
9802
10356
  },
9803
- activityContext2(
10357
+ activityContext(
9804
10358
  ctx,
9805
10359
  "webui",
9806
10360
  payload?.activityNote ?? "Goal metric updated in WebUI."
@@ -9837,7 +10391,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9837
10391
  ...payload?.costCeilingUsd !== void 0 ? { costCeilingUsd: Number(payload.costCeilingUsd) } : {},
9838
10392
  ...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
9839
10393
  },
9840
- activityContext2(ctx, void 0, payload?.activityNote)
10394
+ activityContext(ctx, void 0, payload?.activityNote)
9841
10395
  );
9842
10396
  board ? ok(ws, type, findTask2(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
9843
10397
  return true;
@@ -9859,7 +10413,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9859
10413
  type: payload?.checkType ?? "manual",
9860
10414
  status: payload?.status ?? "pending"
9861
10415
  },
9862
- activityContext2(
10416
+ activityContext(
9863
10417
  ctx,
9864
10418
  "webui",
9865
10419
  payload?.activityNote ?? `Acceptance check added: ${description}.`
@@ -9884,7 +10438,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9884
10438
  {
9885
10439
  ...has(payload, "status") ? { status: payload?.status } : {}
9886
10440
  },
9887
- activityContext2(
10441
+ activityContext(
9888
10442
  ctx,
9889
10443
  "webui",
9890
10444
  payload?.activityNote ?? `Acceptance check updated${payload?.status ? ` to ${String(payload.status)}` : ""}.`
@@ -9911,7 +10465,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9911
10465
  author,
9912
10466
  content
9913
10467
  },
9914
- activityContext2(ctx, author)
10468
+ activityContext(ctx, author)
9915
10469
  );
9916
10470
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
9917
10471
  return true;
@@ -10058,14 +10612,14 @@ function registerShutdownHandlers(res) {
10058
10612
  }
10059
10613
 
10060
10614
  // src/server/config-doctor.ts
10061
- import * as fs11 from "node:fs/promises";
10615
+ import * as fs12 from "node:fs/promises";
10062
10616
  import {
10063
10617
  repairConfigDefaults
10064
10618
  } from "@wrongstack/core/storage";
10065
10619
  import { atomicWrite as atomicWrite5 } from "@wrongstack/core/utils";
10066
10620
  import { decryptConfigSecrets } from "@wrongstack/core/security";
10067
10621
  async function readConfig(file, vault) {
10068
- const raw = await fs11.readFile(file, "utf8");
10622
+ const raw = await fs12.readFile(file, "utf8");
10069
10623
  const parsed = JSON.parse(raw);
10070
10624
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10071
10625
  throw new Error("Active profile config must contain a JSON object.");
@@ -10129,6 +10683,7 @@ async function handleConfigDoctor(ws, apply, deps2) {
10129
10683
 
10130
10684
  // src/server/mailbox-handlers.ts
10131
10685
  import {
10686
+ actionToAckInput,
10132
10687
  getSharedProjectMailbox,
10133
10688
  isMailboxMessageVisibleTo,
10134
10689
  MAILBOX_TYPE_PROPERTIES,
@@ -10145,6 +10700,43 @@ function getMailboxForDeps(deps2) {
10145
10700
  const dir = resolveProjectDir(projectRoot, globalRoot);
10146
10701
  return getSharedProjectMailbox(dir, deps2.events);
10147
10702
  }
10703
+ async function handleMailboxAction(ws, deps2, payload) {
10704
+ const mb = getMailboxForDeps(deps2);
10705
+ if (!mb) {
10706
+ send(ws, {
10707
+ type: "mailbox.action_result",
10708
+ payload: {
10709
+ requestId: payload.requestId,
10710
+ success: false,
10711
+ error: "No project root available"
10712
+ }
10713
+ });
10714
+ return;
10715
+ }
10716
+ try {
10717
+ const message = payload.action === "soft-delete" ? await mb.softDelete(payload.mailId, payload.readerId) : await mb.ack(actionToAckInput(payload.action, payload));
10718
+ send(ws, {
10719
+ type: "mailbox.action_result",
10720
+ payload: {
10721
+ requestId: payload.requestId,
10722
+ success: message !== null,
10723
+ action: payload.action,
10724
+ mailId: payload.mailId
10725
+ }
10726
+ });
10727
+ } catch (err) {
10728
+ send(ws, {
10729
+ type: "mailbox.action_result",
10730
+ payload: {
10731
+ requestId: payload.requestId,
10732
+ success: false,
10733
+ action: payload.action,
10734
+ mailId: payload.mailId,
10735
+ error: errMessage(err)
10736
+ }
10737
+ });
10738
+ }
10739
+ }
10148
10740
  async function handleMailboxSend(ws, deps2, payload) {
10149
10741
  const mb = getMailboxForDeps(deps2);
10150
10742
  if (!mb) {
@@ -10160,7 +10752,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10160
10752
  }
10161
10753
  try {
10162
10754
  const message = await mb.send({
10163
- from: "webui",
10755
+ from: payload.from ?? "webui",
10164
10756
  to: payload.to,
10165
10757
  type: payload.type,
10166
10758
  audience: payload.audience,
@@ -10175,6 +10767,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10175
10767
  requestId: payload.requestId,
10176
10768
  success: true,
10177
10769
  messageId: message.id,
10770
+ from: message.from,
10178
10771
  to: message.to,
10179
10772
  audience: message.audience ?? "all"
10180
10773
  }
@@ -10219,6 +10812,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
10219
10812
  send(ws, {
10220
10813
  type: "mailbox.messages",
10221
10814
  payload: {
10815
+ ...payload?.unreadOnly === true ? { unreadOnly: true } : {},
10222
10816
  messages: visibleMessages.map((m) => {
10223
10817
  const readByMe = payload?.agentId !== void 0 ? payload.agentId in m.readBy : false;
10224
10818
  const completedByMe = payload?.agentId !== void 0 ? m.completedBy === payload.agentId : false;
@@ -10334,6 +10928,14 @@ function createMailboxRouteHandlers(ctx) {
10334
10928
  ...ctx.events ? { events: ctx.events } : {}
10335
10929
  };
10336
10930
  return {
10931
+ action: (ws, msg) => {
10932
+ const parsed = validateMailboxActionPayload(msg.payload);
10933
+ if (!parsed.ok) {
10934
+ sendResult2(ws, false, parsed.message);
10935
+ return;
10936
+ }
10937
+ return handleMailboxAction(ws, deps2, parsed.value);
10938
+ },
10337
10939
  send: (ws, msg) => {
10338
10940
  const parsed = validateMailboxSendPayload(msg.payload);
10339
10941
  if (!parsed.ok) {
@@ -10384,6 +10986,9 @@ function createMailboxRouteHandlers(ctx) {
10384
10986
  }
10385
10987
  async function handleMailboxRoute(ws, msg, handlers) {
10386
10988
  switch (msg.type) {
10989
+ case "mailbox.action":
10990
+ await handlers.action(ws, msg);
10991
+ return true;
10387
10992
  case "mailbox.send":
10388
10993
  await handlers.send(ws, msg);
10389
10994
  return true;
@@ -10409,12 +11014,14 @@ async function handleMailboxRoute(ws, msg, handlers) {
10409
11014
 
10410
11015
  // src/server/mcp-handlers.ts
10411
11016
  import { allServers } from "@wrongstack/core/infrastructure";
11017
+ import { DefaultSecretScrubber, isSecretField } from "@wrongstack/core/security";
10412
11018
  import {
10413
11019
  addMcp,
10414
11020
  disableMcp,
10415
11021
  discoverMcp,
10416
11022
  enableMcp,
10417
11023
  listMcp,
11024
+ MCP_ENV_MASK,
10418
11025
  removeMcp,
10419
11026
  restartMcp,
10420
11027
  updateMcp
@@ -10434,6 +11041,15 @@ function mapStatus(raw) {
10434
11041
  return "stopped";
10435
11042
  }
10436
11043
  }
11044
+ var envScrubber = new DefaultSecretScrubber();
11045
+ function maskServerEnv(env) {
11046
+ const out = {};
11047
+ for (const [key, value] of Object.entries(env)) {
11048
+ const secret = isSecretField(key) || typeof value === "string" && envScrubber.scrub(value) !== value;
11049
+ out[key] = secret ? MCP_ENV_MASK : value;
11050
+ }
11051
+ return out;
11052
+ }
10437
11053
  function toView(info, health) {
10438
11054
  const view = {
10439
11055
  name: info.name,
@@ -10448,7 +11064,7 @@ function toView(info, health) {
10448
11064
  if (info.lazy !== void 0) view.lazy = info.lazy;
10449
11065
  if (info.command !== void 0) view.command = info.command;
10450
11066
  if (info.args !== void 0) view.args = info.args;
10451
- if (info.env !== void 0) view.env = info.env;
11067
+ if (info.env !== void 0) view.env = maskServerEnv(info.env);
10452
11068
  if (info.url !== void 0) view.url = info.url;
10453
11069
  if (health !== void 0) view.health = health;
10454
11070
  return view;
@@ -10487,7 +11103,15 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
10487
11103
  async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
10488
11104
  const d = deps(ws, globalConfigPath, mcpRegistry);
10489
11105
  if (!d) return;
10490
- const result = await addMcp(msg.payload, d);
11106
+ const validated = validateMcpServerPayload(msg.payload, "mcp.add");
11107
+ if (!validated.ok) {
11108
+ send(ws, {
11109
+ type: "mcp.operation_result",
11110
+ payload: { success: false, message: validated.message }
11111
+ });
11112
+ return;
11113
+ }
11114
+ const result = await addMcp(validated.value, d);
10491
11115
  if (result.ok && result.server) {
10492
11116
  send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
10493
11117
  if (result.registryError) {
@@ -10507,7 +11131,15 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
10507
11131
  async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
10508
11132
  const d = deps(ws, globalConfigPath, mcpRegistry);
10509
11133
  if (!d) return;
10510
- const result = await updateMcp(msg.payload, d);
11134
+ const validated = validateMcpServerPayload(msg.payload, "mcp.update");
11135
+ if (!validated.ok) {
11136
+ send(ws, {
11137
+ type: "mcp.operation_result",
11138
+ payload: { success: false, message: validated.message }
11139
+ });
11140
+ return;
11141
+ }
11142
+ const result = await updateMcp(validated.value, d);
10511
11143
  if (result.ok && result.server) {
10512
11144
  send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
10513
11145
  }
@@ -11024,7 +11656,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
11024
11656
  }
11025
11657
  try {
11026
11658
  await Sage.deleteSage(id, reason, {
11027
- force: force ?? true,
11659
+ force: force === true,
11028
11660
  neverInject: neverInject === true
11029
11661
  });
11030
11662
  send(ws, {
@@ -11607,11 +12239,11 @@ function createModelOperations(context) {
11607
12239
  }
11608
12240
 
11609
12241
  // src/server/port-utils.ts
11610
- import * as net from "node:net";
12242
+ import * as net2 from "node:net";
11611
12243
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
11612
12244
  function isPortFree(host, port) {
11613
12245
  return new Promise((resolve15) => {
11614
- const srv = net.createServer();
12246
+ const srv = net2.createServer();
11615
12247
  srv.once("error", () => resolve15(false));
11616
12248
  srv.once("listening", () => {
11617
12249
  srv.close(() => resolve15(true));
@@ -11839,6 +12471,7 @@ function seedContextMeta(config, context) {
11839
12471
  meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
11840
12472
  meta["nextPrediction"] = config.nextPrediction ?? false;
11841
12473
  meta["fallbackModels"] = config.fallbackModels ?? [];
12474
+ meta["fallbackBridge"] = config.fallbackBridge ?? "";
11842
12475
  meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
11843
12476
  meta["favoriteModels"] = config.favoriteModels ?? [];
11844
12477
  meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
@@ -11875,7 +12508,7 @@ function seedContextMeta(config, context) {
11875
12508
  meta["refinerModel"] = autonomyCfg["refinerModel"] ?? "";
11876
12509
  meta["refinerFallbackProfile"] = autonomyCfg["refinerFallbackProfile"] ?? "";
11877
12510
  meta["thinkingWord"] = autonomyCfg["thinkingWord"] ?? "thinking";
11878
- meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "detailed";
12511
+ meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "minimum";
11879
12512
  meta["animationStyle"] = autonomyCfg["animationStyle"] ?? "rainbow";
11880
12513
  meta["showModelReasoning"] = autonomyCfg["showModelReasoning"] !== false;
11881
12514
  meta["breakerEnabled"] = config.circuitBreaker?.enabled === true;
@@ -11921,7 +12554,10 @@ function seedContextMeta(config, context) {
11921
12554
  try {
11922
12555
  const mgr = new FallbackProfileManager(config);
11923
12556
  const named = autoReviewExt?.["fallbackProfile"];
11924
- resolvedChain = typeof named === "string" && named.length > 0 ? mgr.resolve(named) : mgr.resolveEffective({ fallbackAuto: true });
12557
+ resolvedChain = typeof named === "string" && named.length > 0 ? mgr.resolveEffective({ fallbackProfile: named, fallbackAuto: false }) : mgr.resolveEffective({
12558
+ fallbackModels: config.fallbackModels,
12559
+ fallbackAuto: config.fallbackAuto !== false
12560
+ });
11925
12561
  } catch {
11926
12562
  resolvedChain = [];
11927
12563
  }
@@ -11930,8 +12566,8 @@ function seedContextMeta(config, context) {
11930
12566
  }
11931
12567
 
11932
12568
  // src/server/pref-helpers.ts
11933
- import * as fs12 from "node:fs/promises";
11934
- import * as path14 from "node:path";
12569
+ import * as fs13 from "node:fs/promises";
12570
+ import * as path15 from "node:path";
11935
12571
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
11936
12572
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
11937
12573
  var PREF_KEYS = [
@@ -12027,11 +12663,11 @@ function prefSnapshot(contextMeta) {
12027
12663
  return snapshot;
12028
12664
  }
12029
12665
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
12030
- const globalRoot = path14.dirname(filePath);
12666
+ const globalRoot = path15.dirname(filePath);
12031
12667
  await backupConfigFile(filePath, { globalRoot });
12032
12668
  let raw;
12033
12669
  try {
12034
- raw = await fs12.readFile(filePath, "utf8");
12670
+ raw = await fs13.readFile(filePath, "utf8");
12035
12671
  } catch {
12036
12672
  raw = "{}";
12037
12673
  }
@@ -12065,7 +12701,28 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
12065
12701
  );
12066
12702
  }
12067
12703
  }
12068
- var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set(["groupToolCalls", "showThinkingLogs", "autoReviewFallbackModels"]);
12704
+ var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
12705
+ "groupToolCalls",
12706
+ "showThinkingLogs",
12707
+ "autoReviewFallbackModels",
12708
+ // v11 Display parity: agent-swarm panel + inverse fsAccess flag.
12709
+ // The TUI settings picker mirrors these so the browser can keep the
12710
+ // panel state in sync without round-tripping through config.
12711
+ "showAgentSwarmPanel",
12712
+ "allowOutsideProjectRoot",
12713
+ // v13 Display parity: codebase-index read-tool, SAGE memory-inject
12714
+ // surfacing, pre-refine countdown, inject threshold, multi-diff
12715
+ // footer threshold, refine-panel enhance countdown. All six are
12716
+ // pure panel-toggles; the underlying engine knobs (when applicable)
12717
+ // live in the TUI keyboard shortcut or the agent runtime, not in
12718
+ // config.
12719
+ "readSymbols",
12720
+ "showSageMemoryInject",
12721
+ "preRefineSeconds",
12722
+ "sageMemoryInjectThreshold",
12723
+ "multiDiffSummaryThreshold",
12724
+ "enhanceCountdownMs"
12725
+ ]);
12069
12726
  async function persistPrefsToConfig(deps2, holder, payload) {
12070
12727
  payload = { ...payload };
12071
12728
  for (const k of DISPLAY_ONLY_KEYS) delete payload[k];
@@ -12080,7 +12737,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
12080
12737
  autonomyCfg[key] = val;
12081
12738
  autonomyTouched = true;
12082
12739
  };
12083
- if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto"].includes(payload["autonomy"])) {
12740
+ if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto", "eternal", "eternal-parallel"].includes(payload["autonomy"])) {
12084
12741
  setAutonomy("defaultMode", payload["autonomy"]);
12085
12742
  }
12086
12743
  if (typeof payload["autonomyDelayMs"] === "number")
@@ -12376,14 +13033,14 @@ import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
12376
13033
  import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
12377
13034
 
12378
13035
  // src/server/privileged-actions.ts
12379
- import { randomUUID as randomUUID4 } from "node:crypto";
13036
+ import { randomUUID as randomUUID3 } from "node:crypto";
12380
13037
  import {
12381
13038
  isTrustDecisionAllowed
12382
13039
  } from "@wrongstack/core/security";
12383
13040
  async function authorizeWebUIAction(boundary, action, logger) {
12384
13041
  const request = {
12385
13042
  version: 1,
12386
- requestId: randomUUID4(),
13043
+ requestId: randomUUID3(),
12387
13044
  actor: {
12388
13045
  kind: "remote-client",
12389
13046
  ...action.sessionId ? { sessionId: action.sessionId } : {}
@@ -12506,8 +13163,8 @@ async function handleProcessRoute(ws, msg, handlers) {
12506
13163
  }
12507
13164
 
12508
13165
  // src/server/project-handlers.ts
12509
- import * as fs13 from "node:fs/promises";
12510
- import * as path15 from "node:path";
13166
+ import * as fs14 from "node:fs/promises";
13167
+ import * as path16 from "node:path";
12511
13168
  import { DefaultSessionStore } from "@wrongstack/core/storage";
12512
13169
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
12513
13170
  function createProjectHandlers(ctx) {
@@ -12559,10 +13216,10 @@ function createProjectHandlers(ctx) {
12559
13216
  });
12560
13217
  return;
12561
13218
  }
12562
- const resolved = path15.resolve(parsed.value.root);
12563
- const name2 = parsed.value.name?.trim() || path15.basename(resolved);
13219
+ const resolved = path16.resolve(parsed.value.root);
13220
+ const name2 = parsed.value.name?.trim() || path16.basename(resolved);
12564
13221
  try {
12565
- const stat3 = await fs13.stat(resolved).catch(() => null);
13222
+ const stat3 = await fs14.stat(resolved).catch(() => null);
12566
13223
  if (!stat3?.isDirectory()) {
12567
13224
  sendTo(ws, {
12568
13225
  type: "projects.added",
@@ -12571,7 +13228,7 @@ function createProjectHandlers(ctx) {
12571
13228
  return;
12572
13229
  }
12573
13230
  const before = await loadManifest(ctx.globalConfigPath);
12574
- const already = before.projects.some((project) => path15.resolve(project.root) === resolved);
13231
+ const already = before.projects.some((project) => path16.resolve(project.root) === resolved);
12575
13232
  const entry = await touchProjectInManifest(
12576
13233
  { projectRoot: resolved, workingDir: resolved, name: name2 },
12577
13234
  ctx.globalConfigPath
@@ -12601,8 +13258,8 @@ function createProjectHandlers(ctx) {
12601
13258
  });
12602
13259
  return;
12603
13260
  }
12604
- const resolved = path15.resolve(parsed.value.root);
12605
- const name2 = parsed.value.name?.trim() || path15.basename(resolved);
13261
+ const resolved = path16.resolve(parsed.value.root);
13262
+ const name2 = parsed.value.name?.trim() || path16.basename(resolved);
12606
13263
  if (!ctx.allowProjectMutations) {
12607
13264
  sendTo(ws, {
12608
13265
  type: "projects.selected",
@@ -12615,7 +13272,7 @@ function createProjectHandlers(ctx) {
12615
13272
  return;
12616
13273
  }
12617
13274
  try {
12618
- const stat3 = await fs13.stat(resolved).catch(() => null);
13275
+ const stat3 = await fs14.stat(resolved).catch(() => null);
12619
13276
  if (!stat3?.isDirectory()) {
12620
13277
  sendTo(ws, {
12621
13278
  type: "projects.selected",
@@ -12645,7 +13302,7 @@ function createProjectHandlers(ctx) {
12645
13302
  const previousIdentityTarget = {
12646
13303
  projectSlug: previousPaths.projectSlug,
12647
13304
  projectRoot: previousProjectRoot,
12648
- projectName: path15.basename(previousProjectRoot),
13305
+ projectName: path16.basename(previousProjectRoot),
12649
13306
  workingDir: ctx.context.workingDir
12650
13307
  };
12651
13308
  const previousUsage = ctx.tokenCounter.total();
@@ -12742,7 +13399,7 @@ function createProjectHandlers(ctx) {
12742
13399
 
12743
13400
  // src/server/provider-handlers.ts
12744
13401
  import { resolveProviderModelList } from "@wrongstack/core/models";
12745
- import { DefaultSecretScrubber } from "@wrongstack/core/security";
13402
+ import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
12746
13403
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
12747
13404
  import {
12748
13405
  beginOAuthLogin
@@ -12808,14 +13465,14 @@ async function searchCatalogModels(registry, rawQuery, limit = 8) {
12808
13465
  }
12809
13466
 
12810
13467
  // src/server/provider-config-io.ts
12811
- import * as fs14 from "node:fs/promises";
13468
+ import * as fs15 from "node:fs/promises";
12812
13469
  import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
12813
13470
  import { atomicWrite as atomicWrite7 } from "@wrongstack/core/utils";
12814
13471
  import { decryptConfigSecrets as decryptConfigSecrets3, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
12815
13472
  async function loadSavedProviders(configPath, vault) {
12816
13473
  let raw;
12817
13474
  try {
12818
- raw = await fs14.readFile(configPath, "utf8");
13475
+ raw = await fs15.readFile(configPath, "utf8");
12819
13476
  } catch {
12820
13477
  return {};
12821
13478
  }
@@ -12833,7 +13490,7 @@ async function saveProviders(configPath, vault, providers, profileConfigPath) {
12833
13490
  let raw;
12834
13491
  let fileExists = true;
12835
13492
  try {
12836
- raw = await fs14.readFile(targetPath, "utf8");
13493
+ raw = await fs15.readFile(targetPath, "utf8");
12837
13494
  } catch (err) {
12838
13495
  if (err.code !== "ENOENT") {
12839
13496
  throw new ConfigError2({
@@ -13014,7 +13671,7 @@ function projectSavedProviders(providers) {
13014
13671
  return view;
13015
13672
  });
13016
13673
  }
13017
- var probeScrubber = new DefaultSecretScrubber();
13674
+ var probeScrubber = new DefaultSecretScrubber2();
13018
13675
  async function probeModelDescriptors(cfg) {
13019
13676
  if (!cfg.baseUrl) return [];
13020
13677
  try {
@@ -13536,6 +14193,7 @@ var CLIENT_COLLABORATION_MESSAGE_TYPES = [
13536
14193
  "collab.resume",
13537
14194
  "collab.grant_control",
13538
14195
  "collab.inject_tool",
14196
+ "mailbox.action",
13539
14197
  "mailbox.agents",
13540
14198
  "mailbox.clear",
13541
14199
  "mailbox.compact",
@@ -13801,6 +14459,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
13801
14459
  "collab.pause.granted",
13802
14460
  "collab.pause.released",
13803
14461
  "collab.state",
14462
+ "mailbox.action_result",
13804
14463
  "mailbox.agent_registered",
13805
14464
  "mailbox.agents",
13806
14465
  "mailbox.cleared",
@@ -14024,13 +14683,13 @@ function isRegisteredMessageType(type, direction) {
14024
14683
  // src/protocol/decoder.ts
14025
14684
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
14026
14685
  var MAX_PAYLOAD_DEPTH = 32;
14027
- function inspectValue(value, path29, depth) {
14686
+ function inspectValue(value, path30, depth) {
14028
14687
  if (depth > MAX_PAYLOAD_DEPTH) {
14029
- return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path29 };
14688
+ return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path30 };
14030
14689
  }
14031
14690
  if (value === null || typeof value !== "object") return null;
14032
14691
  for (const key of Object.keys(value)) {
14033
- const childPath = `${path29}.${key}`;
14692
+ const childPath = `${path30}.${key}`;
14034
14693
  if (FORBIDDEN_KEYS.has(key)) {
14035
14694
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
14036
14695
  }
@@ -15726,8 +16385,8 @@ function createRouteFamilyDispatcher(options) {
15726
16385
  }
15727
16386
 
15728
16387
  // src/server/shell-open.ts
15729
- import * as fs15 from "node:fs/promises";
15730
- import * as path16 from "node:path";
16388
+ import * as fs16 from "node:fs/promises";
16389
+ import * as path17 from "node:path";
15731
16390
  import { spawn } from "node:child_process";
15732
16391
  function normalizeShellOpenTarget(target) {
15733
16392
  return target === "terminal" ? "terminal" : "file-manager";
@@ -15738,11 +16397,11 @@ function shellQuote(s) {
15738
16397
  }
15739
16398
  async function handleShellOpen(req, logger, options) {
15740
16399
  try {
15741
- const resolved = path16.resolve(req.path);
16400
+ const resolved = path17.resolve(req.path);
15742
16401
  if (options?.projectRoot) {
15743
- const root = path16.resolve(options.projectRoot);
15744
- const relative5 = path16.relative(root, resolved);
15745
- const escapes = relative5.startsWith("..") || path16.isAbsolute(relative5);
16402
+ const root = path17.resolve(options.projectRoot);
16403
+ const relative5 = path17.relative(root, resolved);
16404
+ const escapes = relative5.startsWith("..") || path17.isAbsolute(relative5);
15746
16405
  if (escapes) {
15747
16406
  return {
15748
16407
  success: false,
@@ -15750,7 +16409,7 @@ async function handleShellOpen(req, logger, options) {
15750
16409
  };
15751
16410
  }
15752
16411
  }
15753
- await fs15.access(resolved);
16412
+ await fs16.access(resolved);
15754
16413
  if (METACHAR_REGEX.test(resolved)) {
15755
16414
  return { success: false, message: "Path contains unsupported characters." };
15756
16415
  }
@@ -15801,7 +16460,12 @@ async function handleShellOpen(req, logger, options) {
15801
16460
  }
15802
16461
 
15803
16462
  // src/server/sdd-board-ws-handler.ts
15804
- import { listBoards as listBoards3 } from "@wrongstack/kanban";
16463
+ import {
16464
+ enqueueKanbanWorkflowCommand,
16465
+ kanbanWorkflowId,
16466
+ listBoards as listBoards3,
16467
+ listKanbanWorkflowStates
16468
+ } from "@wrongstack/kanban";
15805
16469
  import {
15806
16470
  applySddLifecycle,
15807
16471
  extractVerificationCommand,
@@ -15828,16 +16492,17 @@ var SddBoardWebSocketHandler = class {
15828
16492
  clients = /* @__PURE__ */ new Set();
15829
16493
  lifecycle;
15830
16494
  security;
15831
- diskPollingEnabled;
16495
+ standalonePollingEnabled;
15832
16496
  latest = null;
15833
16497
  poll = null;
15834
16498
  pollInFlight = false;
15835
16499
  unsub = null;
16500
+ disposed = false;
15836
16501
  constructor(boardsDir, events, lifecycle, security) {
15837
16502
  this.store = new SddBoardStore({ baseDir: boardsDir });
15838
16503
  this.lifecycle = lifecycle;
15839
16504
  this.security = security;
15840
- this.diskPollingEnabled = events === void 0;
16505
+ this.standalonePollingEnabled = events === void 0;
15841
16506
  if (events) {
15842
16507
  const handler = (e) => {
15843
16508
  this.latest = e.snapshot;
@@ -15847,6 +16512,7 @@ var SddBoardWebSocketHandler = class {
15847
16512
  }
15848
16513
  }
15849
16514
  addClient(ws) {
16515
+ if (this.disposed) return;
15850
16516
  const client = { ws, id: crypto.randomUUID() };
15851
16517
  this.clients.add(client);
15852
16518
  const remove = () => {
@@ -15872,7 +16538,7 @@ var SddBoardWebSocketHandler = class {
15872
16538
  return;
15873
16539
  }
15874
16540
  if (msg.type === "sdd.board.list") {
15875
- const boards = await this.store.list();
16541
+ const boards = await this.listBoardEntries();
15876
16542
  this.broadcast({ type: "sdd.board.list", payload: { boards } });
15877
16543
  return;
15878
16544
  }
@@ -15885,7 +16551,8 @@ var SddBoardWebSocketHandler = class {
15885
16551
  const verificationCommands = [];
15886
16552
  if (action === "set_task_verification") {
15887
16553
  const command = msg.payload?.verificationCommand;
15888
- if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
16554
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192))
16555
+ return;
15889
16556
  if (typeof command === "string" && command.trim()) {
15890
16557
  verificationCommands.push({ command, operation: "sdd.set_task_verification" });
15891
16558
  }
@@ -15919,18 +16586,26 @@ var SddBoardWebSocketHandler = class {
15919
16586
  );
15920
16587
  if (!authorization.allowed) return;
15921
16588
  }
15922
- const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
16589
+ const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.listBoardEntries())[0]?.runId;
15923
16590
  if (runId) {
15924
- await this.store.appendControl(runId, {
15925
- ts: Date.now(),
15926
- type: action,
15927
- payload: msg.payload
15928
- });
16591
+ if (this.lifecycle && this.lifecycle.controlTransport !== "legacy-file") {
16592
+ await enqueueKanbanWorkflowCommand(
16593
+ this.lifecycle.projectRoot,
16594
+ kanbanWorkflowId("sdd", runId),
16595
+ { type: action, payload: msg.payload }
16596
+ );
16597
+ } else {
16598
+ await this.store.appendControl(runId, {
16599
+ ts: Date.now(),
16600
+ type: action,
16601
+ payload: msg.payload
16602
+ });
16603
+ }
15929
16604
  }
15930
16605
  }
15931
16606
  }
15932
16607
  /**
15933
- * Apply a cleanup/rollback/destroy from disk and broadcast a structured
16608
+ * Apply a cleanup/rollback/destroy from durable state and broadcast a structured
15934
16609
  * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
15935
16610
  * the user must stop it first; the UI gates the buttons on `!active` and the
15936
16611
  * Destroy flow auto-stops then waits before sending `destroy`.
@@ -15939,7 +16614,11 @@ var SddBoardWebSocketHandler = class {
15939
16614
  if (!this.lifecycle) {
15940
16615
  this.broadcast({
15941
16616
  type: "sdd.board.lifecycle_result",
15942
- payload: { op, ok: false, reason: "Lifecycle operations are not available in this session." }
16617
+ payload: {
16618
+ op,
16619
+ ok: false,
16620
+ reason: "Lifecycle operations are not available in this session."
16621
+ }
15943
16622
  });
15944
16623
  return;
15945
16624
  }
@@ -15955,6 +16634,7 @@ var SddBoardWebSocketHandler = class {
15955
16634
  projectRoot: this.lifecycle.projectRoot,
15956
16635
  paths: this.lifecycle.paths,
15957
16636
  runId,
16637
+ stateTransport: this.lifecycle.stateTransport ?? "kanban",
15958
16638
  revertMerged: payload?.revertMerged === true
15959
16639
  });
15960
16640
  this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
@@ -15974,31 +16654,40 @@ var SddBoardWebSocketHandler = class {
15974
16654
  }
15975
16655
  }
15976
16656
  dispose() {
16657
+ this.disposed = true;
15977
16658
  this.stopPolling();
15978
16659
  this.unsub?.();
15979
16660
  this.unsub = null;
16661
+ this.clients.clear();
16662
+ this.latest = null;
15980
16663
  }
15981
16664
  // ── internal ────────────────────────────────────────────────────────────
15982
16665
  async pollLatest() {
15983
16666
  if (this.pollInFlight) return;
15984
16667
  this.pollInFlight = true;
15985
16668
  try {
15986
- const entry = await this.store.latest();
15987
- if (!entry) return;
15988
- if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
16669
+ const snap = await this.loadLatestSnapshot();
16670
+ if (this.disposed) return;
16671
+ if (!snap) return;
16672
+ if (this.latest && this.latest.updatedAt >= snap.updatedAt && this.latest.runId === snap.runId) {
15989
16673
  return;
15990
16674
  }
15991
- const snap = await this.store.load(entry.runId);
15992
- if (snap) {
15993
- this.latest = snap;
15994
- this.broadcast({ type: "sdd.board.snapshot", payload: snap });
15995
- }
16675
+ this.latest = snap;
16676
+ this.broadcast({ type: "sdd.board.snapshot", payload: snap });
16677
+ } catch (err) {
16678
+ console.warn(
16679
+ JSON.stringify({
16680
+ level: "warn",
16681
+ event: "sdd_board.poll_failed",
16682
+ message: err instanceof Error ? err.message : String(err)
16683
+ })
16684
+ );
15996
16685
  } finally {
15997
16686
  this.pollInFlight = false;
15998
16687
  }
15999
16688
  }
16000
16689
  startPolling() {
16001
- if (!this.diskPollingEnabled || this.poll !== null || this.clients.size === 0) return;
16690
+ if (!this.standalonePollingEnabled || this.poll !== null || this.clients.size === 0) return;
16002
16691
  this.poll = setInterval(() => void this.pollLatest(), 1e3);
16003
16692
  this.poll.unref?.();
16004
16693
  }
@@ -16008,17 +16697,44 @@ var SddBoardWebSocketHandler = class {
16008
16697
  this.poll = null;
16009
16698
  }
16010
16699
  async sendCurrent(client) {
16011
- const snap = this.latest ?? await this.loadLatestFromDisk();
16700
+ const snap = this.latest ?? await this.loadLatestSnapshot();
16012
16701
  if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
16013
16702
  }
16014
16703
  async broadcastCurrent() {
16015
- const snap = this.latest ?? await this.loadLatestFromDisk();
16704
+ const snap = this.latest ?? await this.loadLatestSnapshot();
16016
16705
  if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
16017
16706
  }
16018
- async loadLatestFromDisk() {
16707
+ async loadLatestSnapshot() {
16708
+ if (this.usesKanbanState()) {
16709
+ const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
16710
+ const snapshots = states.map((state) => state.value).filter(isSddBoardSnapshot).sort((a, b) => b.updatedAt - a.updatedAt);
16711
+ return snapshots[0] ?? null;
16712
+ }
16019
16713
  const entry = await this.store.latest();
16020
16714
  return entry ? this.store.load(entry.runId) : null;
16021
16715
  }
16716
+ async listBoardEntries() {
16717
+ if (!this.usesKanbanState()) return this.store.list();
16718
+ const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
16719
+ return states.flatMap((state) => {
16720
+ if (!isSddBoardSnapshot(state.value)) return [];
16721
+ const snapshot = state.value;
16722
+ return [
16723
+ {
16724
+ runId: snapshot.runId,
16725
+ ...snapshot.specId ? { specId: snapshot.specId } : {},
16726
+ title: snapshot.title,
16727
+ status: snapshot.status,
16728
+ total: snapshot.progress.total,
16729
+ completed: snapshot.progress.completed,
16730
+ updatedAt: snapshot.updatedAt
16731
+ }
16732
+ ];
16733
+ });
16734
+ }
16735
+ usesKanbanState() {
16736
+ return Boolean(this.lifecycle && this.lifecycle.stateTransport !== "legacy-file");
16737
+ }
16022
16738
  broadcast(msg) {
16023
16739
  const data = JSON.stringify(msg);
16024
16740
  for (const client of this.clients) {
@@ -16029,17 +16745,20 @@ var SddBoardWebSocketHandler = class {
16029
16745
  sendSerialized(client.ws, JSON.stringify(msg));
16030
16746
  }
16031
16747
  };
16748
+ function isSddBoardSnapshot(value) {
16749
+ if (!value || typeof value !== "object") return false;
16750
+ const snapshot = value;
16751
+ return typeof snapshot.runId === "string" && typeof snapshot.title === "string" && typeof snapshot.status === "string" && typeof snapshot.updatedAt === "number" && Array.isArray(snapshot.tasks) && Boolean(snapshot.progress && typeof snapshot.progress.total === "number");
16752
+ }
16032
16753
 
16033
16754
  // src/server/sdd-wizard-wiring.ts
16034
- import * as path17 from "node:path";
16035
- import {
16036
- DefaultTaskStore,
16037
- TaskTracker
16038
- } from "@wrongstack/core/tasking";
16755
+ import * as path18 from "node:path";
16756
+ import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
16039
16757
  import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
16040
16758
  import { WorktreeManager as WorktreeManager2 } from "@wrongstack/core/worktree";
16041
16759
  import {
16042
16760
  cleanupStaleSddWorktrees,
16761
+ createKanbanSddSessionPersistence,
16043
16762
  decomposeNonAtomicTasks,
16044
16763
  gatherProjectContext,
16045
16764
  makeAcceptanceCriteriaVerifier,
@@ -16057,7 +16776,7 @@ import {
16057
16776
  } from "@wrongstack/sdd";
16058
16777
  async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
16059
16778
  const runTracker = tracker ?? (() => {
16060
- const t = new TaskTracker({ store: new DefaultTaskStore() });
16779
+ const t = new TaskTracker({ store: deps2.taskStore ?? new DefaultTaskStore() });
16061
16780
  t.setGraph(graph);
16062
16781
  return t;
16063
16782
  })();
@@ -16066,7 +16785,8 @@ async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
16066
16785
  if (worktreesEnabled && await isGitWorkTree(deps2.projectRoot)) {
16067
16786
  void cleanupStaleSddWorktrees({
16068
16787
  projectRoot: deps2.projectRoot,
16069
- boardsDir: deps2.projectSddBoards
16788
+ boardsDir: deps2.projectSddBoards,
16789
+ stateTransport: "kanban"
16070
16790
  }).catch(() => void 0);
16071
16791
  worktrees = new WorktreeManager2({
16072
16792
  projectRoot: deps2.projectRoot,
@@ -16145,7 +16865,8 @@ function buildSddWizardDeps(opts) {
16145
16865
  }).catch(() => {
16146
16866
  projectContext = "";
16147
16867
  });
16148
- const sessionPath = opts.paths.projectSddSession ?? path17.join(opts.paths.projectDir, "sdd-session.json");
16868
+ const legacySessionPath = opts.paths.projectSddSession ?? path18.join(opts.paths.projectDir, "sdd-session.json");
16869
+ const sessionPersistence = createKanbanSddSessionPersistence(opts.projectRoot, legacySessionPath);
16149
16870
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
16150
16871
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
16151
16872
  const runIsolatedTurn = async (prompt, name2) => {
@@ -16166,7 +16887,7 @@ function buildSddWizardDeps(opts) {
16166
16887
  const makeDriver = () => new SddInterviewDriver({
16167
16888
  specStore,
16168
16889
  graphStore,
16169
- sessionPath,
16890
+ sessionPersistence,
16170
16891
  projectContext
16171
16892
  });
16172
16893
  const launchFromGraph = async (graph, config, tracker) => {
@@ -16178,6 +16899,7 @@ function buildSddWizardDeps(opts) {
16178
16899
  projectRoot: opts.projectRoot,
16179
16900
  subagentFactory: opts.subagentFactory,
16180
16901
  projectSddBoards: opts.paths.projectSddBoards,
16902
+ taskStore: graphStore,
16181
16903
  registry,
16182
16904
  runIsolatedTurn,
16183
16905
  ...opts.brain ? { brain: opts.brain } : {}
@@ -16247,8 +16969,19 @@ var SddWizardWebSocketHandler = class {
16247
16969
  lastAgentText = "";
16248
16970
  /** Guards against overlapping interview turns (one in flight at a time). */
16249
16971
  busy = false;
16250
- /** Resolves once project-context gather + disk resume finish. */
16972
+ /** Set when authoritative session state could not be read during bootstrap. */
16973
+ resumeError = null;
16974
+ /** Resolves once project-context gather + durable resume finish. */
16251
16975
  ready;
16976
+ /**
16977
+ * Single-flight slot for the resume probe. `handleMessage` awaits this
16978
+ * when `resumeError` is set, so concurrent frames cannot race the retry
16979
+ * — one probe either clears the gate or re-latches it, then every
16980
+ * queued frame re-checks and proceeds. `null` when no probe is in
16981
+ * flight (or after the probe has settled).
16982
+ */
16983
+ resumeProbe = null;
16984
+ disposed = false;
16252
16985
  async bootstrap() {
16253
16986
  try {
16254
16987
  await this.deps.ensureReady?.();
@@ -16256,26 +16989,37 @@ var SddWizardWebSocketHandler = class {
16256
16989
  }
16257
16990
  await this.tryResume();
16258
16991
  }
16259
- /** Rehydrate a persisted interview if one exists on disk. */
16992
+ /** Rehydrate a persisted interview if one exists. */
16260
16993
  async tryResume() {
16261
16994
  if (this.driver) return;
16262
16995
  try {
16263
16996
  const driver = this.deps.makeDriver();
16264
- if (await driver.loadExisting()) {
16997
+ const loaded = await driver.loadExisting();
16998
+ if (this.disposed) return;
16999
+ if (loaded) {
16265
17000
  this.driver = driver;
16266
17001
  this.lastAgentText = driver.getLastAgentText() ?? "";
16267
17002
  }
16268
- } catch {
17003
+ this.resumeError = null;
17004
+ } catch (error2) {
17005
+ if (this.disposed) return;
16269
17006
  this.driver = null;
16270
17007
  this.lastAgentText = "";
17008
+ this.resumeError = error2 instanceof Error ? error2.message : String(error2);
16271
17009
  }
16272
17010
  }
16273
17011
  addClient(ws) {
17012
+ if (this.disposed) return;
16274
17013
  const client = { ws, id: crypto.randomUUID() };
16275
17014
  this.clients.add(client);
16276
17015
  ws.on("close", () => this.clients.delete(client));
16277
17016
  ws.on("error", () => this.clients.delete(client));
16278
17017
  void this.ready.then(() => {
17018
+ if (this.disposed || !this.clients.has(client)) return;
17019
+ if (this.resumeError) {
17020
+ this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
17021
+ return;
17022
+ }
16279
17023
  if (this.driver) {
16280
17024
  this.send(client, this.snapshotMsg());
16281
17025
  if (this.lastAgentText) {
@@ -16284,9 +17028,34 @@ var SddWizardWebSocketHandler = class {
16284
17028
  }
16285
17029
  });
16286
17030
  }
17031
+ dispose() {
17032
+ this.disposed = true;
17033
+ this.clients.clear();
17034
+ this.driver = null;
17035
+ this.lastAgentText = "";
17036
+ this.busy = false;
17037
+ this.resumeProbe = null;
17038
+ }
16287
17039
  async handleMessage(msg) {
17040
+ if (this.disposed) return;
16288
17041
  try {
16289
17042
  await this.ready;
17043
+ if (this.resumeError) {
17044
+ if (this.resumeProbe === null) {
17045
+ const probe = Promise.resolve().then(() => this.tryResume());
17046
+ this.resumeProbe = probe;
17047
+ void probe.finally(() => {
17048
+ setTimeout(() => {
17049
+ if (this.resumeProbe === probe) this.resumeProbe = null;
17050
+ }, 0);
17051
+ });
17052
+ }
17053
+ await this.resumeProbe;
17054
+ if (this.resumeError) {
17055
+ this.broadcast({ type: "sdd.spec.error", payload: { message: this.resumeError } });
17056
+ return;
17057
+ }
17058
+ }
16290
17059
  switch (msg.type) {
16291
17060
  case "sdd.spec.start":
16292
17061
  await this.onStart(String(msg.payload?.goal ?? "").trim(), {
@@ -16303,7 +17072,10 @@ var SddWizardWebSocketHandler = class {
16303
17072
  if (this.driver) {
16304
17073
  this.broadcast(this.snapshotMsg());
16305
17074
  if (this.lastAgentText) {
16306
- this.broadcast({ type: "sdd.spec.agent_text", payload: { text: this.lastAgentText } });
17075
+ this.broadcast({
17076
+ type: "sdd.spec.agent_text",
17077
+ payload: { text: this.lastAgentText }
17078
+ });
16307
17079
  }
16308
17080
  }
16309
17081
  break;
@@ -16483,6 +17255,7 @@ var SddWizardWebSocketHandler = class {
16483
17255
  this.broadcast(this.snapshotMsg());
16484
17256
  try {
16485
17257
  const text = await this.deps.runInterviewTurn(prompt);
17258
+ if (this.disposed) return;
16486
17259
  this.lastAgentText = text;
16487
17260
  if (this.driver) {
16488
17261
  await this.driver.ingestAgentOutput(text);
@@ -16508,6 +17281,7 @@ var SddWizardWebSocketHandler = class {
16508
17281
  }
16509
17282
  }
16510
17283
  send(client, msg) {
17284
+ if (!this.clients.has(client)) return;
16511
17285
  sendSerialized(client.ws, JSON.stringify(msg));
16512
17286
  }
16513
17287
  };
@@ -16517,10 +17291,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
16517
17291
 
16518
17292
  // src/server/setup-events-fleet-broadcaster.ts
16519
17293
  import { watch as fsWatch } from "node:fs";
16520
- import * as path18 from "node:path";
17294
+ import * as path19 from "node:path";
16521
17295
  function registerSetupEventsFleetBroadcaster(deps2) {
16522
17296
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
16523
- const globalRoot = globalConfigPath ? path18.dirname(globalConfigPath) : void 0;
17297
+ const globalRoot = globalConfigPath ? path19.dirname(globalConfigPath) : void 0;
16524
17298
  if (!globalRoot) return void 0;
16525
17299
  const disposers = [];
16526
17300
  const broadcastSessions = async () => {
@@ -16530,8 +17304,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
16530
17304
  const sessions = await registry.list();
16531
17305
  const ownEntry = sessions.find((s) => s.pid === process.pid);
16532
17306
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
16533
- const myRoot = path18.resolve(context.projectRoot);
16534
- const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path18.resolve(s.projectRoot) === myRoot).map((s) => ({
17307
+ const myRoot = path19.resolve(context.projectRoot);
17308
+ const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path19.resolve(s.projectRoot) === myRoot).map((s) => ({
16535
17309
  sessionId: s.sessionId,
16536
17310
  projectName: s.projectName,
16537
17311
  projectSlug: s.projectSlug,
@@ -16721,14 +17495,14 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
16721
17495
 
16722
17496
  // src/server/setup-events-status-watcher.ts
16723
17497
  import { watch as fsWatch2 } from "node:fs";
16724
- import * as fs16 from "node:fs/promises";
16725
- import * as path20 from "node:path";
17498
+ import * as fs17 from "node:fs/promises";
17499
+ import * as path21 from "node:path";
16726
17500
 
16727
17501
  // src/server/setup-events-watcher.ts
16728
- import * as path19 from "node:path";
17502
+ import * as path20 from "node:path";
16729
17503
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
16730
17504
  const raw = String(filename);
16731
- const relative5 = path19.isAbsolute(raw) ? path19.relative(projectsDir, raw) : raw;
17505
+ const relative5 = path20.isAbsolute(raw) ? path20.relative(projectsDir, raw) : raw;
16732
17506
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
16733
17507
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
16734
17508
  return parts.at(-2) ?? null;
@@ -16763,7 +17537,7 @@ function logFileWatcherMetrics(metrics) {
16763
17537
  function registerSetupEventsStatusWatcher(deps2) {
16764
17538
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
16765
17539
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
16766
- const projectsDir = path20.join(wpaths.globalRoot, "projects");
17540
+ const projectsDir = path21.join(wpaths.globalRoot, "projects");
16767
17541
  const knownProjectHashes = /* @__PURE__ */ new Set();
16768
17542
  const debounceTimers = /* @__PURE__ */ new Map();
16769
17543
  const DEBOUNCE_MS = 150;
@@ -16808,7 +17582,7 @@ function registerSetupEventsStatusWatcher(deps2) {
16808
17582
  let watcher;
16809
17583
  const startWatcher = async () => {
16810
17584
  try {
16811
- await fs16.mkdir(projectsDir, { recursive: true });
17585
+ await fs17.mkdir(projectsDir, { recursive: true });
16812
17586
  if (isDisposed()) return;
16813
17587
  watcher = fsWatch2(
16814
17588
  projectsDir,
@@ -16822,8 +17596,8 @@ function registerSetupEventsStatusWatcher(deps2) {
16822
17596
  if (!knownProjectHashes.has(projectHash)) return;
16823
17597
  if (watcherMetrics) watcherMetrics.filesProcessed++;
16824
17598
  try {
16825
- const targetFile = path20.join(projectsDir, projectHash, "status.json");
16826
- const content = await fs16.readFile(targetFile, "utf-8");
17599
+ const targetFile = path21.join(projectsDir, projectHash, "status.json");
17600
+ const content = await fs17.readFile(targetFile, "utf-8");
16827
17601
  const statusData = JSON.parse(content);
16828
17602
  scheduleBroadcast(projectHash, statusData);
16829
17603
  } catch {
@@ -16880,8 +17654,8 @@ function registerSetupEventsStatusWatcher(deps2) {
16880
17654
  }
16881
17655
 
16882
17656
  // src/server/setup-events-core-watchers.ts
16883
- import * as fs17 from "node:fs/promises";
16884
- import * as path21 from "node:path";
17657
+ import * as fs18 from "node:fs/promises";
17658
+ import * as path22 from "node:path";
16885
17659
  function registerSetupEventsCoreWatchers(deps2) {
16886
17660
  const { broadcast: broadcast2, clients, context } = deps2;
16887
17661
  const disposers = [];
@@ -16917,9 +17691,9 @@ function registerSetupEventsClientStatusWriter(deps2) {
16917
17691
  if (wpaths?.projectStatus) {
16918
17692
  try {
16919
17693
  const statusFile = wpaths.projectStatus(e.projectHash);
16920
- const dir = path21.dirname(statusFile);
16921
- await fs17.mkdir(dir, { recursive: true });
16922
- await fs17.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
17694
+ const dir = path22.dirname(statusFile);
17695
+ await fs18.mkdir(dir, { recursive: true });
17696
+ await fs18.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
16923
17697
  } catch (err) {
16924
17698
  console.error(
16925
17699
  JSON.stringify({
@@ -17274,6 +18048,7 @@ function setupEvents(deps2) {
17274
18048
  });
17275
18049
  pendingConfirms.set(id, {
17276
18050
  resolve: e.resolve,
18051
+ sessionId: e.sessionId,
17277
18052
  decisionSource: e.decisionSource,
17278
18053
  riskTier: e.riskTier,
17279
18054
  boundaryReason: e.boundaryReason,
@@ -17802,6 +18577,9 @@ var SpecsWebSocketHandler = class {
17802
18577
  ws.on("error", () => this.clients.delete(client));
17803
18578
  void this.sendList(client);
17804
18579
  }
18580
+ dispose() {
18581
+ this.clients.clear();
18582
+ }
17805
18583
  async handleMessage(msg) {
17806
18584
  switch (msg.type) {
17807
18585
  case "specs.list":
@@ -17937,14 +18715,15 @@ var SpecsWebSocketHandler = class {
17937
18715
  }
17938
18716
  }
17939
18717
  send(client, msg) {
18718
+ if (!this.clients.has(client)) return;
17940
18719
  sendSerialized(client.ws, JSON.stringify(msg));
17941
18720
  }
17942
18721
  };
17943
18722
 
17944
18723
  // src/server/start-webui.ts
17945
- import { randomUUID as randomUUID5 } from "node:crypto";
18724
+ import { randomUUID as randomUUID4 } from "node:crypto";
17946
18725
  import * as http2 from "node:http";
17947
- import * as path28 from "node:path";
18726
+ import * as path29 from "node:path";
17948
18727
  import { createDefaultPipelines } from "@wrongstack/core/agent";
17949
18728
  import { getSharedProjectMailbox as getSharedProjectMailbox4, resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
17950
18729
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
@@ -17958,7 +18737,7 @@ import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } fro
17958
18737
  import {
17959
18738
  expectDefined as expectDefined3,
17960
18739
  sessionScopedPath as sessionScopedPath3,
17961
- startHeapWatchdog,
18740
+ startSharedHeapWatchdog,
17962
18741
  toErrorMessage as toErrorMessage13,
17963
18742
  wstackGlobalRoot as wstackGlobalRoot2
17964
18743
  } from "@wrongstack/core/utils";
@@ -17968,9 +18747,7 @@ import { ensureSessionShell } from "@wrongstack/tools";
17968
18747
 
17969
18748
  // src/server/backend-services.ts
17970
18749
  import { join as join12 } from "node:path";
17971
- import {
17972
- Agent
17973
- } from "@wrongstack/core/agent";
18750
+ import { Agent } from "@wrongstack/core/agent";
17974
18751
  import {
17975
18752
  BrainDecisionLedger,
17976
18753
  BrainMonitor,
@@ -17982,8 +18759,8 @@ import {
17982
18759
  mailboxSessionTag,
17983
18760
  ObservableBrainArbiter as ObservableBrainArbiterCtor
17984
18761
  } from "@wrongstack/core/coordination";
18762
+ import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
17985
18763
  import { installDesignStudioMiddleware } from "@wrongstack/core/design";
17986
- import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
17987
18764
  import {
17988
18765
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
17989
18766
  createBrainRuntime,
@@ -17993,7 +18770,9 @@ import {
17993
18770
  } from "@wrongstack/core/execution";
17994
18771
  import { TOKENS } from "@wrongstack/core/kernel";
17995
18772
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
17996
- import { resolveContextWindowPolicy as resolveContextWindowPolicy2 } from "@wrongstack/core/types";
18773
+ import {
18774
+ resolveContextWindowPolicy as resolveContextWindowPolicy2
18775
+ } from "@wrongstack/core/types";
17997
18776
  import {
17998
18777
  estimateRequestTokensCalibrated,
17999
18778
  toErrorMessage as toErrorMessage9
@@ -18385,9 +19164,9 @@ function clampDim(value, fallback) {
18385
19164
 
18386
19165
  // src/server/worktree-ws-handler.ts
18387
19166
  import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
19167
+ import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
18388
19168
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
18389
19169
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
18390
- import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
18391
19170
  var MAX_ACTIVITY = 6;
18392
19171
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
18393
19172
  var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
@@ -18406,6 +19185,20 @@ var WorktreeWebSocketHandler = class {
18406
19185
  baseBranch = "";
18407
19186
  broadcastInterval = null;
18408
19187
  offs = [];
19188
+ /**
19189
+ * Single-flight guard for orphan scans. Two concurrent `scanAndBroadcast()`
19190
+ * calls would race: the first call reads `wt.listManaged()`, then the
19191
+ * second call's mutation completes (e.g. `removeOne`), then the first call
19192
+ * broadcasts a stale orphan list — the user sees a row persist and clicks
19193
+ * Remove a second time, hitting "remove failed (not a managed worktree?)".
19194
+ *
19195
+ * We coalesce concurrent triggers: while a scan is in flight, additional
19196
+ * callers are remembered as "need a re-scan" and exactly one follow-up
19197
+ * scan runs after the in-flight one finishes. The re-scan picks up any
19198
+ * mutations that landed during the previous scan's `listManaged()` window.
19199
+ */
19200
+ scanInFlight = null;
19201
+ scanRescanNeeded = false;
18409
19202
  addClient(ws) {
18410
19203
  this.clients.add(ws);
18411
19204
  ws.on("close", () => this.clients.delete(ws));
@@ -18424,7 +19217,10 @@ var WorktreeWebSocketHandler = class {
18424
19217
  return true;
18425
19218
  }
18426
19219
  if (msg.type === "worktree.remove") {
18427
- await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
19220
+ await this.removeOne(
19221
+ msg.payload?.["dir"],
19222
+ msg.payload?.["branch"]
19223
+ );
18428
19224
  return true;
18429
19225
  }
18430
19226
  if (msg.type === "worktree.merge") {
@@ -18432,7 +19228,10 @@ var WorktreeWebSocketHandler = class {
18432
19228
  return true;
18433
19229
  }
18434
19230
  if (msg.type === "worktree.diff") {
18435
- await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
19231
+ await this.diffOne(
19232
+ msg.payload?.["dir"],
19233
+ msg.payload?.["baseBranch"]
19234
+ );
18436
19235
  return true;
18437
19236
  }
18438
19237
  return false;
@@ -18441,6 +19240,10 @@ var WorktreeWebSocketHandler = class {
18441
19240
  for (const off of this.offs) off();
18442
19241
  this.offs.length = 0;
18443
19242
  this.stopBroadcast();
19243
+ this.clients.clear();
19244
+ this.handles.clear();
19245
+ this.baseBranch = "";
19246
+ this.scanRescanNeeded = false;
18444
19247
  }
18445
19248
  // ── orphan management ─────────────────────────────────────────────────────
18446
19249
  /** Absolute managed-worktrees root for this project. */
@@ -18461,12 +19264,43 @@ var WorktreeWebSocketHandler = class {
18461
19264
  }
18462
19265
  return live;
18463
19266
  }
19267
+ /**
19268
+ * Coalesced orphan scan entry point. Multiple concurrent callers share one
19269
+ * in-flight scan; if a caller arrives during a scan, a single follow-up
19270
+ * scan is scheduled to pick up mutations that landed mid-scan. Returns the
19271
+ * shared promise so `await this.scanAndBroadcast()` still works for callers
19272
+ * that want to chain after the next broadcast.
19273
+ */
19274
+ scanAndBroadcast() {
19275
+ if (this.scanInFlight) {
19276
+ this.scanRescanNeeded = true;
19277
+ return this.scanInFlight;
19278
+ }
19279
+ this.scanInFlight = this.runScanAndMaybeRescan();
19280
+ return this.scanInFlight;
19281
+ }
19282
+ /**
19283
+ * Run the actual scan, then drain a pending re-scan request if one came in
19284
+ * during the scan. The re-scan is bounded (one follow-up) — additional
19285
+ * requests that arrive during the follow-up coalesce into the next one.
19286
+ */
19287
+ async runScanAndMaybeRescan() {
19288
+ try {
19289
+ await this.runScanOnce();
19290
+ while (this.scanRescanNeeded) {
19291
+ this.scanRescanNeeded = false;
19292
+ await this.runScanOnce();
19293
+ }
19294
+ } finally {
19295
+ this.scanInFlight = null;
19296
+ }
19297
+ }
18464
19298
  /**
18465
19299
  * Scan the disk for managed worktrees/branches NOT owned by a live in-session
18466
19300
  * run and broadcast them as orphans, with whether it is safe to clean now.
18467
19301
  * No-op (empty inventory) when management deps were not wired.
18468
19302
  */
18469
- async scanAndBroadcast() {
19303
+ async runScanOnce() {
18470
19304
  if (!this.management) {
18471
19305
  this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
18472
19306
  return;
@@ -18522,7 +19356,8 @@ var WorktreeWebSocketHandler = class {
18522
19356
  }
18523
19357
  const res = await cleanupStaleSddWorktrees2({
18524
19358
  projectRoot: this.management.projectRoot,
18525
- boardsDir: this.management.boardsDir
19359
+ boardsDir: this.management.boardsDir,
19360
+ stateTransport: "kanban"
18526
19361
  });
18527
19362
  if (res.skippedReason) {
18528
19363
  this.broadcast({
@@ -18535,26 +19370,45 @@ var WorktreeWebSocketHandler = class {
18535
19370
  for (const [id, h] of [...this.handles]) {
18536
19371
  if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
18537
19372
  }
18538
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: true, removed: res.removed } });
19373
+ this.broadcast({
19374
+ type: "worktree.cleanup_result",
19375
+ payload: { ok: true, removed: res.removed }
19376
+ });
18539
19377
  this.broadcastState();
18540
19378
  await this.scanAndBroadcast();
18541
19379
  }
18542
19380
  /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
18543
19381
  async removeOne(dir, branch) {
18544
19382
  if (!this.management || !dir && !branch) {
18545
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "nothing to remove" } });
19383
+ this.broadcast({
19384
+ type: "worktree.cleanup_result",
19385
+ payload: { ok: false, removed: 0, reason: "nothing to remove" }
19386
+ });
18546
19387
  return;
18547
19388
  }
18548
19389
  if (branch && !MANAGED_BRANCH_RE.test(branch)) {
18549
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "not a managed worktree branch" } });
19390
+ this.broadcast({
19391
+ type: "worktree.cleanup_result",
19392
+ payload: { ok: false, removed: 0, reason: "not a managed worktree branch" }
19393
+ });
18550
19394
  return;
18551
19395
  }
18552
19396
  if (dir && !this.underRoot(dir)) {
18553
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" } });
19397
+ this.broadcast({
19398
+ type: "worktree.cleanup_result",
19399
+ payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" }
19400
+ });
18554
19401
  return;
18555
19402
  }
18556
19403
  if (branch && this.liveActiveBranches().has(branch)) {
18557
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "a run is live on this worktree \u2014 stop it first" } });
19404
+ this.broadcast({
19405
+ type: "worktree.cleanup_result",
19406
+ payload: {
19407
+ ok: false,
19408
+ removed: 0,
19409
+ reason: "a run is live on this worktree \u2014 stop it first"
19410
+ }
19411
+ });
18558
19412
  return;
18559
19413
  }
18560
19414
  let removed = false;
@@ -18563,31 +19417,54 @@ var WorktreeWebSocketHandler = class {
18563
19417
  ({ removed } = await wt.removeOne(dir, branch));
18564
19418
  }
18565
19419
  for (const [id, h] of [...this.handles]) {
18566
- if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId)) this.handles.delete(id);
19420
+ if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId))
19421
+ this.handles.delete(id);
18567
19422
  }
18568
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: removed, removed: removed ? 1 : 0, reason: removed ? void 0 : "remove failed (not a managed worktree?)" } });
19423
+ this.broadcast({
19424
+ type: "worktree.cleanup_result",
19425
+ payload: {
19426
+ ok: removed,
19427
+ removed: removed ? 1 : 0,
19428
+ reason: removed ? void 0 : "remove failed (not a managed worktree?)"
19429
+ }
19430
+ });
18569
19431
  this.broadcastState();
18570
19432
  await this.scanAndBroadcast();
18571
19433
  }
18572
19434
  /** Squash-merge ONE branch into base. Refused while a live run owns it. */
18573
19435
  async mergeBranch(branch) {
18574
19436
  if (!this.management || !branch) {
18575
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch: branch ?? "", reason: "no branch" } });
19437
+ this.broadcast({
19438
+ type: "worktree.merge_result",
19439
+ payload: { ok: false, branch: branch ?? "", reason: "no branch" }
19440
+ });
18576
19441
  return;
18577
19442
  }
18578
19443
  if (!MANAGED_BRANCH_RE.test(branch)) {
18579
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "not a managed worktree branch" } });
19444
+ this.broadcast({
19445
+ type: "worktree.merge_result",
19446
+ payload: { ok: false, branch, reason: "not a managed worktree branch" }
19447
+ });
18580
19448
  return;
18581
19449
  }
18582
19450
  if (this.liveActiveBranches().has(branch)) {
18583
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" } });
19451
+ this.broadcast({
19452
+ type: "worktree.merge_result",
19453
+ payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" }
19454
+ });
18584
19455
  return;
18585
19456
  }
18586
19457
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
18587
19458
  const res = await wt.mergeBranch(branch);
18588
19459
  this.broadcast({
18589
19460
  type: "worktree.merge_result",
18590
- payload: { ok: res.ok, branch, conflict: res.conflict, conflictFiles: res.conflictFiles, reason: res.reason }
19461
+ payload: {
19462
+ ok: res.ok,
19463
+ branch,
19464
+ conflict: res.conflict,
19465
+ conflictFiles: res.conflictFiles,
19466
+ reason: res.reason
19467
+ }
18591
19468
  });
18592
19469
  await this.scanAndBroadcast();
18593
19470
  }
@@ -18629,8 +19506,14 @@ var WorktreeWebSocketHandler = class {
18629
19506
  }),
18630
19507
  on("worktree.committed", (p) => {
18631
19508
  const e = p;
18632
- this.patch(e.handleId, { status: "committing", insertions: e.insertions, deletions: e.deletions, files: e.files });
18633
- if (e.committed) this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
19509
+ this.patch(e.handleId, {
19510
+ status: "committing",
19511
+ insertions: e.insertions,
19512
+ deletions: e.deletions,
19513
+ files: e.files
19514
+ });
19515
+ if (e.committed)
19516
+ this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
18634
19517
  this.broadcastState();
18635
19518
  }),
18636
19519
  on("worktree.merged", (p) => {
@@ -18671,10 +19554,15 @@ var WorktreeWebSocketHandler = class {
18671
19554
  activity(id, kind, text) {
18672
19555
  const cur = this.handles.get(id);
18673
19556
  if (cur) {
18674
- const recentActivity = [...cur.recentActivity, { kind, text, at: Date.now() }].slice(-MAX_ACTIVITY);
19557
+ const recentActivity = [...cur.recentActivity, { kind, text, at: Date.now() }].slice(
19558
+ -MAX_ACTIVITY
19559
+ );
18675
19560
  this.handles.set(id, { ...cur, recentActivity });
18676
19561
  }
18677
- this.broadcast({ type: "worktree.event", payload: { kind, handleId: id, text, at: Date.now() } });
19562
+ this.broadcast({
19563
+ type: "worktree.event",
19564
+ payload: { kind, handleId: id, text, at: Date.now() }
19565
+ });
18678
19566
  }
18679
19567
  stateMessage() {
18680
19568
  return {
@@ -18753,7 +19641,13 @@ async function createAgentServices(input) {
18753
19641
  minImportance: config.Sage?.inject?.minImportance,
18754
19642
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
18755
19643
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
18756
- triggers: config.Sage?.inject?.triggers
19644
+ triggers: config.Sage?.inject?.triggers,
19645
+ // Resolve the live session so retrieval and cooldown are session-scoped
19646
+ // (matching the CLI wiring in wiring/sage.ts). Without this, the
19647
+ // middleware falls back to ctx.session.id for cooldown but passes
19648
+ // undefined to retrieval, causing owned session-scoped memories to be
19649
+ // silently excluded from tool-call injection.
19650
+ getSessionId: () => input.sessionGetter().id
18757
19651
  })
18758
19652
  );
18759
19653
  }
@@ -18877,6 +19771,7 @@ async function createAgentServices(input) {
18877
19771
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
18878
19772
  tracer: void 0
18879
19773
  });
19774
+ input.installToolBoundary?.(pipelines);
18880
19775
  const webuiLogger = container.resolve(TOKENS.Logger);
18881
19776
  void discoverMailboxBridgeForWebui({
18882
19777
  projectRoot,
@@ -18981,33 +19876,35 @@ async function createAgentServices(input) {
18981
19876
  brainLog.push(entry);
18982
19877
  if (brainLog.length > 20) brainLog.shift();
18983
19878
  };
18984
- events.on(
18985
- "brain.decision_answered",
18986
- (e) => pushBrainLog({
18987
- at: e.at,
18988
- kind: "answered",
18989
- question: e.request.question,
18990
- outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
18991
- })
18992
- );
18993
- events.on(
18994
- "brain.decision_ask_human",
18995
- (e) => pushBrainLog({
18996
- at: e.at,
18997
- kind: "ask_human",
18998
- question: e.request.question,
18999
- outcome: "needs human judgement"
19000
- })
19001
- );
19002
- events.on(
19003
- "brain.decision_denied",
19004
- (e) => pushBrainLog({
19005
- at: e.at,
19006
- kind: "denied",
19007
- question: e.request.question,
19008
- outcome: e.decision.type === "deny" ? e.decision.reason : ""
19009
- })
19010
- );
19879
+ const brainLogOffs = [
19880
+ events.on(
19881
+ "brain.decision_answered",
19882
+ (e) => pushBrainLog({
19883
+ at: e.at,
19884
+ kind: "answered",
19885
+ question: e.request.question,
19886
+ outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
19887
+ })
19888
+ ),
19889
+ events.on(
19890
+ "brain.decision_ask_human",
19891
+ (e) => pushBrainLog({
19892
+ at: e.at,
19893
+ kind: "ask_human",
19894
+ question: e.request.question,
19895
+ outcome: "needs human judgement"
19896
+ })
19897
+ ),
19898
+ events.on(
19899
+ "brain.decision_denied",
19900
+ (e) => pushBrainLog({
19901
+ at: e.at,
19902
+ kind: "denied",
19903
+ question: e.request.question,
19904
+ outcome: e.decision.type === "deny" ? e.decision.reason : ""
19905
+ })
19906
+ )
19907
+ ];
19011
19908
  const brainMailbox = getSharedProjectMailbox2(wpaths.projectDir, events);
19012
19909
  brainMonitor = new BrainMonitor({
19013
19910
  events,
@@ -19087,7 +19984,8 @@ async function createAgentServices(input) {
19087
19984
  // subagent's fallback extension's tracker hooks are undefined and
19088
19985
  // round-robin keeps reassigning the doomed model. Mirrors the CLI
19089
19986
  // factory wiring at host-subagent-factory.ts:337.
19090
- statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker)
19987
+ statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker),
19988
+ ...input.installToolBoundary ? { installToolBoundary: input.installToolBoundary } : {}
19091
19989
  }),
19092
19990
  paths: {
19093
19991
  projectSpecs: wpaths.projectSpecs,
@@ -19119,6 +20017,19 @@ async function createAgentServices(input) {
19119
20017
  getActiveSessionId: () => context.session.id
19120
20018
  }
19121
20019
  );
20020
+ let realtimeHandlersDisposed = false;
20021
+ const disposeRealtimeHandlers = () => {
20022
+ if (realtimeHandlersDisposed) return;
20023
+ realtimeHandlersDisposed = true;
20024
+ for (const off of brainLogOffs) off();
20025
+ goalHandler.dispose();
20026
+ sddBoardHandler.dispose();
20027
+ sddWizardHandler.dispose();
20028
+ specsHandler.dispose();
20029
+ worktreeHandler.dispose();
20030
+ terminalHandler.dispose();
20031
+ collabHandler.dispose();
20032
+ };
19122
20033
  return {
19123
20034
  collabBus,
19124
20035
  compactor,
@@ -19144,12 +20055,16 @@ async function createAgentServices(input) {
19144
20055
  worktreeHandler,
19145
20056
  terminalHandler,
19146
20057
  collabHandler,
20058
+ disposeRealtimeHandlers,
19147
20059
  updateAutoCompactionMaxContext
19148
20060
  };
19149
20061
  }
19150
20062
 
19151
20063
  // src/server/connection-handler.ts
19152
- var RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
20064
+ var DEFAULT_WS_RATE_LIMIT = 600;
20065
+ var rawRateLimit = process.env["WEBUI_RATE_LIMIT"];
20066
+ var parsedRateLimit = rawRateLimit ? Number.parseInt(rawRateLimit, 10) : DEFAULT_WS_RATE_LIMIT;
20067
+ var RATE_LIMIT_MESSAGES = Number.isFinite(parsedRateLimit) && parsedRateLimit >= 0 ? parsedRateLimit : DEFAULT_WS_RATE_LIMIT;
19153
20068
  var VERBOSE_WS_LOGGING = process.env["WEBUI_VERBOSE_WS"] === "1" || process.env["WEBUI_VERBOSE_WS"] === "true";
19154
20069
  function readRemoteAddress(ws) {
19155
20070
  const socket = ws._socket;
@@ -19229,8 +20144,74 @@ function createConnectionHandler(options) {
19229
20144
  };
19230
20145
  }
19231
20146
 
20147
+ // src/server/governance-runtime.ts
20148
+ import {
20149
+ createGovernanceMutationSnapshotBridge
20150
+ } from "@wrongstack/runtime/governance-mutation-snapshot-bridge";
20151
+ import { sanitizeGovernanceMessage } from "@wrongstack/runtime/governance-sanitize";
20152
+ var WRONGSTACK_GOVERNANCE_ENV = "WRONGSTACK_GOVERNANCE";
20153
+ var DEFAULT_DEPENDENCIES = {
20154
+ load: () => import("@wrongstack/runtime/governance-bootstrap"),
20155
+ createMutationBridge: createGovernanceMutationSnapshotBridge
20156
+ };
20157
+ function webUiGovernanceRuntimeEnabled(environment) {
20158
+ return environment[WRONGSTACK_GOVERNANCE_ENV]?.trim() === "1";
20159
+ }
20160
+ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES) {
20161
+ if (!webUiGovernanceRuntimeEnabled(input.environment)) return void 0;
20162
+ let loaded;
20163
+ try {
20164
+ loaded = await dependencies.load();
20165
+ } catch (error2) {
20166
+ input.logger.warn("governance: standalone WebUI runtime module unavailable", {
20167
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
20168
+ });
20169
+ return void 0;
20170
+ }
20171
+ let result;
20172
+ try {
20173
+ result = await loaded.bootstrapGovernanceRuntime({
20174
+ projectRoot: input.projectRoot,
20175
+ projectId: input.projectId,
20176
+ adminClientId: `webui-control-${process.pid}-${input.sessionId}`,
20177
+ modelClientId: `webui-model-${input.sessionId}`,
20178
+ modelCapabilities: ["task_read", "command_submit", "shadow_observe"]
20179
+ });
20180
+ } catch (error2) {
20181
+ input.logger.warn("governance: standalone WebUI bootstrap failed open", {
20182
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
20183
+ });
20184
+ return void 0;
20185
+ }
20186
+ if (result.mode !== "governed") {
20187
+ input.contextMeta["governance"] = result;
20188
+ input.logger.warn(`governance: standalone WebUI legacy fallback (${result.code})`, {
20189
+ phase: result.phase,
20190
+ cleanup: result.cleanup,
20191
+ message: result.message
20192
+ });
20193
+ return void 0;
20194
+ }
20195
+ const snapshot = result.handle.snapshot();
20196
+ input.contextMeta["governance"] = snapshot;
20197
+ input.logger.info(`governance: ${snapshot.source} standalone WebUI control plane`);
20198
+ const mutationSnapshots = dependencies.createMutationBridge({
20199
+ events: input.events,
20200
+ sink: result.handle,
20201
+ captureWorkspaceCheckpoint: input.captureWorkspaceCheckpoint,
20202
+ logger: input.logger
20203
+ });
20204
+ return Object.freeze({
20205
+ installToolBoundary: (pipelines) => mutationSnapshots.installToolBoundary(pipelines),
20206
+ close: async () => {
20207
+ await mutationSnapshots.close();
20208
+ return result.handle.close();
20209
+ }
20210
+ });
20211
+ }
20212
+
19232
20213
  // src/server/message-dispatcher.ts
19233
- import path22 from "node:path";
20214
+ import path23 from "node:path";
19234
20215
  function createMessageDispatcher(opts) {
19235
20216
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
19236
20217
  function makeWorklistContext() {
@@ -19251,7 +20232,7 @@ function createMessageDispatcher(opts) {
19251
20232
  skillLoader: deps2.skillLoader,
19252
20233
  skillInstaller: deps2.skillInstaller,
19253
20234
  projectRoot,
19254
- projectSkillsDir: path22.join(projectRoot, ".wrongstack", "skills"),
20235
+ projectSkillsDir: path23.join(projectRoot, ".wrongstack", "skills"),
19255
20236
  globalSkillsDir: deps2.wpaths.globalSkills
19256
20237
  };
19257
20238
  }
@@ -19503,7 +20484,7 @@ function createMessageDispatcher(opts) {
19503
20484
 
19504
20485
  // src/server/pre-context-services.ts
19505
20486
  import { createRequire as createRequire3 } from "node:module";
19506
- import * as path25 from "node:path";
20487
+ import * as path26 from "node:path";
19507
20488
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
19508
20489
  import {
19509
20490
  getSharedProjectMailbox as getSharedProjectMailbox3,
@@ -19557,8 +20538,8 @@ import { configureDangerBypass, configureExecPolicy } from "@wrongstack/tools";
19557
20538
  import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/tools/session-kanban";
19558
20539
 
19559
20540
  // src/server/model-auto-discovery.ts
19560
- import * as fs18 from "node:fs/promises";
19561
- import * as path23 from "node:path";
20541
+ import * as fs19 from "node:fs/promises";
20542
+ import * as path24 from "node:path";
19562
20543
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
19563
20544
  function isOverlayRegistry(value) {
19564
20545
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -19584,7 +20565,7 @@ function eligibleProviders(config) {
19584
20565
  }
19585
20566
  async function readCache(file) {
19586
20567
  try {
19587
- return JSON.parse(await fs18.readFile(file, "utf8"));
20568
+ return JSON.parse(await fs19.readFile(file, "utf8"));
19588
20569
  } catch {
19589
20570
  return {};
19590
20571
  }
@@ -19594,7 +20575,7 @@ async function discoverAndMergeWebuiProviders(opts) {
19594
20575
  if (!isOverlayRegistry(registry)) return;
19595
20576
  const targets = eligibleProviders(opts.config);
19596
20577
  if (targets.length === 0) return;
19597
- const cacheFile = path23.join(opts.cacheDir, "discovered-models-cache.json");
20578
+ const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
19598
20579
  const cache2 = await readCache(cacheFile);
19599
20580
  let cacheDirty = false;
19600
20581
  await Promise.all(
@@ -19631,8 +20612,8 @@ async function discoverAndMergeWebuiProviders(opts) {
19631
20612
  );
19632
20613
  if (cacheDirty) {
19633
20614
  try {
19634
- await fs18.mkdir(path23.dirname(cacheFile), { recursive: true });
19635
- await fs18.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
20615
+ await fs19.mkdir(path24.dirname(cacheFile), { recursive: true });
20616
+ await fs19.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
19636
20617
  } catch {
19637
20618
  opts.logger?.debug?.("provider auto-discovery cache write failed");
19638
20619
  }
@@ -19728,7 +20709,7 @@ function resolveSetupProvider(opts) {
19728
20709
  }
19729
20710
 
19730
20711
  // src/server/standalone-session-identity.ts
19731
- import * as path24 from "node:path";
20712
+ import * as path25 from "node:path";
19732
20713
  import {
19733
20714
  AgentStatusTracker,
19734
20715
  FleetNotifier,
@@ -19747,7 +20728,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
19747
20728
  let activeTarget = {
19748
20729
  projectSlug: paths.projectSlug,
19749
20730
  projectRoot: paths.projectRoot,
19750
- projectName: path24.basename(paths.projectRoot),
20731
+ projectName: path25.basename(paths.projectRoot),
19751
20732
  workingDir: opts.workingDir
19752
20733
  };
19753
20734
  let pendingClaim;
@@ -19978,7 +20959,7 @@ async function createPreContextServices(input) {
19978
20959
  await discoverAndMergeWebuiProviders({
19979
20960
  config,
19980
20961
  registry: modelsRegistry,
19981
- cacheDir: path25.dirname(wpaths.modelsCache),
20962
+ cacheDir: path26.dirname(wpaths.modelsCache),
19982
20963
  logger
19983
20964
  });
19984
20965
  } catch (err) {
@@ -20030,7 +21011,7 @@ async function createPreContextServices(input) {
20030
21011
  configureChildEnvGitIdentity(config.git?.identity ?? null);
20031
21012
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
20032
21013
  const mcpTokenStore = new MCPVaultTokenStore(
20033
- path25.join(wpaths.projectDir, "mcp-auth.json"),
21014
+ path26.join(wpaths.projectDir, "mcp-auth.json"),
20034
21015
  vault
20035
21016
  );
20036
21017
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -20136,7 +21117,7 @@ async function createPreContextServices(input) {
20136
21117
  };
20137
21118
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
20138
21119
  const skillInstaller = config.features.skills ? new SkillInstaller({
20139
- manifestPath: path25.join(wpaths.configDir, "installed-skills.json"),
21120
+ manifestPath: path26.join(wpaths.configDir, "installed-skills.json"),
20140
21121
  projectSkillsDir: wpaths.inProjectSkills,
20141
21122
  globalSkillsDir: wpaths.globalSkills,
20142
21123
  projectHash: wpaths.projectHash,
@@ -20146,8 +21127,8 @@ async function createPreContextServices(input) {
20146
21127
  const bundledPromptsDir = promptsEnabled ? (() => {
20147
21128
  try {
20148
21129
  const req = createRequire3(import.meta.url);
20149
- return path25.join(
20150
- path25.dirname(req.resolve("@wrongstack/core/package.json")),
21130
+ return path26.join(
21131
+ path26.dirname(req.resolve("@wrongstack/core/package.json")),
20151
21132
  "data",
20152
21133
  "prompts"
20153
21134
  );
@@ -20251,7 +21232,7 @@ async function createPreContextServices(input) {
20251
21232
  }
20252
21233
 
20253
21234
  // src/server/routes.ts
20254
- import path26 from "node:path";
21235
+ import path27 from "node:path";
20255
21236
  import { makeProviderFromConfig as makeProviderFromConfig2, withCatalogCapabilities } from "@wrongstack/providers";
20256
21237
 
20257
21238
  // src/server/mode-handlers.ts
@@ -20543,7 +21524,7 @@ function buildRoutes(state, deps2, cb) {
20543
21524
  };
20544
21525
  const mailboxRoutes = createMailboxRouteHandlers({
20545
21526
  getProjectRoot: state.getProjectRoot,
20546
- getGlobalRoot: () => path26.dirname(deps2.globalConfigPath),
21527
+ getGlobalRoot: () => path27.dirname(deps2.globalConfigPath),
20547
21528
  events: deps2.events
20548
21529
  });
20549
21530
  const mcpRoutes = {
@@ -20616,7 +21597,7 @@ function buildRoutes(state, deps2, cb) {
20616
21597
  }
20617
21598
 
20618
21599
  // src/server/server-runtime.ts
20619
- import * as path27 from "node:path";
21600
+ import * as path28 from "node:path";
20620
21601
  import { createRequire as createRequire4 } from "node:module";
20621
21602
  import { fileURLToPath } from "node:url";
20622
21603
  import { WebSocketServer } from "ws";
@@ -20677,7 +21658,7 @@ function createSessionStartPayload(g) {
20677
21658
  inputCost,
20678
21659
  outputCost,
20679
21660
  cacheReadCost,
20680
- projectName: path27.basename(projectRoot) || projectRoot,
21661
+ projectName: path28.basename(projectRoot) || projectRoot,
20681
21662
  projectRoot,
20682
21663
  cwd: g.getWorkingDir(),
20683
21664
  mode: g.getModeId(),
@@ -20765,13 +21746,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
20765
21746
  };
20766
21747
  }
20767
21748
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
20768
- if (explicitDistDir) return path27.resolve(explicitDistDir);
21749
+ if (explicitDistDir) return path28.resolve(explicitDistDir);
20769
21750
  try {
20770
21751
  const requireFromHere2 = createRequire4(fromUrl);
20771
21752
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
20772
- return path27.dirname(serverEntry);
21753
+ return path28.dirname(serverEntry);
20773
21754
  } catch {
20774
- return path27.resolve(path27.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
21755
+ return path28.resolve(path28.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
20775
21756
  }
20776
21757
  }
20777
21758
  function startHttpServer(opts) {
@@ -20933,6 +21914,17 @@ async function startWebUI(opts = {}) {
20933
21914
  const prefSnapshot2 = () => prefSnapshot(context.meta);
20934
21915
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
20935
21916
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
21917
+ const governanceHandle = opts.installToolBoundary === void 0 ? await setupWebUiGovernance({
21918
+ environment: process.env,
21919
+ projectRoot,
21920
+ projectId: wpaths.projectSlug,
21921
+ sessionId: session.id,
21922
+ contextMeta: context.meta,
21923
+ events,
21924
+ logger,
21925
+ captureWorkspaceCheckpoint: async () => sessionStore.captureWorkspaceCheckpoint?.(session.id, 0)
21926
+ }) : void 0;
21927
+ const installToolBoundary = opts.installToolBoundary ?? governanceHandle?.installToolBoundary;
20936
21928
  const agentServices = await createAgentServices({
20937
21929
  trustBoundary,
20938
21930
  config,
@@ -20955,6 +21947,7 @@ async function startWebUI(opts = {}) {
20955
21947
  skillInstaller,
20956
21948
  tokenCounter,
20957
21949
  pipelines: createDefaultPipelines(),
21950
+ ...installToolBoundary ? { installToolBoundary } : {},
20958
21951
  modelCapabilitiesRef,
20959
21952
  sessionGetter: () => session,
20960
21953
  sessionReader,
@@ -20985,6 +21978,7 @@ async function startWebUI(opts = {}) {
20985
21978
  worktreeHandler,
20986
21979
  terminalHandler,
20987
21980
  collabHandler,
21981
+ disposeRealtimeHandlers,
20988
21982
  updateAutoCompactionMaxContext
20989
21983
  } = agentServices;
20990
21984
  if (typeof context.meta["yolo"] === "boolean") {
@@ -21035,7 +22029,7 @@ async function startWebUI(opts = {}) {
21035
22029
  const input = toLanguagePackageInput(operation, workspace);
21036
22030
  const use = {
21037
22031
  type: "tool_use",
21038
- id: `techstack-remediation-${randomUUID5()}`,
22032
+ id: `techstack-remediation-${randomUUID4()}`,
21039
22033
  name: "language_package",
21040
22034
  input: { ...input }
21041
22035
  };
@@ -21163,21 +22157,21 @@ async function startWebUI(opts = {}) {
21163
22157
  });
21164
22158
  }
21165
22159
  async function touchProjectEntry(root, workDir) {
21166
- const resolved = path28.resolve(root);
22160
+ const resolved = path29.resolve(root);
21167
22161
  const manifest = await loadManifest(globalConfigPath);
21168
22162
  const now = (/* @__PURE__ */ new Date()).toISOString();
21169
- const existing = manifest.projects.find((p) => path28.resolve(p.root) === resolved);
22163
+ const existing = manifest.projects.find((p) => path29.resolve(p.root) === resolved);
21170
22164
  if (existing) {
21171
22165
  existing.lastSeen = now;
21172
- if (workDir) existing.lastWorkingDir = path28.resolve(workDir);
22166
+ if (workDir) existing.lastWorkingDir = path29.resolve(workDir);
21173
22167
  } else {
21174
22168
  manifest.projects.push({
21175
- name: path28.basename(resolved),
22169
+ name: path29.basename(resolved),
21176
22170
  root: resolved,
21177
22171
  slug: generateProjectSlug(resolved),
21178
22172
  createdAt: now,
21179
22173
  lastSeen: now,
21180
- lastWorkingDir: workDir ? path28.resolve(workDir) : void 0
22174
+ lastWorkingDir: workDir ? path29.resolve(workDir) : void 0
21181
22175
  });
21182
22176
  }
21183
22177
  await saveManifest(manifest, globalConfigPath);
@@ -21304,7 +22298,8 @@ async function startWebUI(opts = {}) {
21304
22298
  patchConfig(state.getConfig(), {
21305
22299
  providers: snapshot.providers,
21306
22300
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
21307
- ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
22301
+ ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
22302
+ fallbackBridge: snapshot.fallbackBridge ?? ""
21308
22303
  })
21309
22304
  );
21310
22305
  deps2.configStore.update({
@@ -21312,6 +22307,7 @@ async function startWebUI(opts = {}) {
21312
22307
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
21313
22308
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
21314
22309
  ...snapshot.fallbackModels !== void 0 ? { fallbackModels: snapshot.fallbackModels } : {},
22310
+ fallbackBridge: snapshot.fallbackBridge ?? "",
21315
22311
  ...snapshot.fallbackProfiles !== void 0 ? { fallbackProfiles: snapshot.fallbackProfiles } : {},
21316
22312
  ...snapshot.favoriteModels !== void 0 ? { favoriteModels: snapshot.favoriteModels } : {},
21317
22313
  ...snapshot.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snapshot.favoriteModelsOnly } : {},
@@ -21354,7 +22350,20 @@ async function startWebUI(opts = {}) {
21354
22350
  );
21355
22351
  credentialWatcherClose = credentialWatcher.close;
21356
22352
  }
21357
- const stopHeapWatchdog = startHeapWatchdog();
22353
+ const stopHeapWatchdog = startSharedHeapWatchdog({
22354
+ collectStats: () => ({
22355
+ surface: opts.surface ?? "webui",
22356
+ sessionId: context.session.id,
22357
+ messages: context.state.messages.length,
22358
+ messageEstimatedTokens: context.state.messages.reduce(
22359
+ (sum, message) => sum + (message._estTokens ?? 0),
22360
+ 0
22361
+ ),
22362
+ webClients: clients.size,
22363
+ pendingConfirms: pendingConfirms.size,
22364
+ runActive: runLockControl.get() !== null
22365
+ })
22366
+ });
21358
22367
  const routes = buildRoutes(state, deps2, cb);
21359
22368
  const handleMessage = createMessageDispatcher({
21360
22369
  state,
@@ -21432,6 +22441,13 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21432
22441
  await todosCheckpoint.detach();
21433
22442
  await stopHeapWatchdog();
21434
22443
  credentialWatcherClose?.();
22444
+ disposeRealtimeHandlers();
22445
+ const governanceCleanup = await governanceHandle?.close();
22446
+ if (governanceCleanup && !governanceCleanup.ok) {
22447
+ logger.warn(`governance: standalone WebUI ${governanceCleanup.action} cleanup failed`, {
22448
+ message: governanceCleanup.message
22449
+ });
22450
+ }
21435
22451
  brainMonitor.stop();
21436
22452
  await agentServices.brainLedger?.stop().catch(() => {
21437
22453
  });
@@ -21455,7 +22471,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21455
22471
  await memoryStore.dispose().catch(
21456
22472
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
21457
22473
  );
21458
- await unregisterInstance(process.pid, path28.dirname(globalConfigPath));
22474
+ await unregisterInstance(process.pid, path29.dirname(globalConfigPath));
21459
22475
  }
21460
22476
  });
21461
22477
  }
@@ -21548,7 +22564,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
21548
22564
  console.error(err instanceof Error ? err.message : String(err));
21549
22565
  process.exit(1);
21550
22566
  }
21551
- const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
22567
+ const open2 = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
21552
22568
  const requireToken = argv.includes("--require-token") || envFlag2("WEBUI_REQUIRE_TOKEN");
21553
22569
  console.log(`[WebUI] Starting standalone server on ${wsHost} (http:${httpPort})...`);
21554
22570
  startWebUI({
@@ -21558,7 +22574,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
21558
22574
  publicUrl,
21559
22575
  publicWsUrl,
21560
22576
  requireToken,
21561
- open,
22577
+ open: open2,
21562
22578
  distDir
21563
22579
  }).catch((err) => {
21564
22580
  console.error(JSON.stringify({