@wrongstack/webui-server 0.296.2 → 0.296.3

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 (33) hide show
  1. package/dist/index.js +837 -238
  2. package/dist/index.js.map +4 -4
  3. package/dist/protocol/client-workspace.d.ts +1 -1
  4. package/dist/protocol/client-workspace.d.ts.map +1 -1
  5. package/dist/protocol/index.js +4 -1
  6. package/dist/protocol/index.js.map +2 -2
  7. package/dist/protocol/projections.d.ts +6 -0
  8. package/dist/protocol/projections.d.ts.map +1 -1
  9. package/dist/protocol/registry.d.ts +2 -2
  10. package/dist/protocol/registry.d.ts.map +1 -1
  11. package/dist/protocol/server-workspace.d.ts +1 -1
  12. package/dist/protocol/server-workspace.d.ts.map +1 -1
  13. package/dist/server/backend-services.d.ts.map +1 -1
  14. package/dist/server/connections-health-route.d.ts +8 -0
  15. package/dist/server/connections-health-route.d.ts.map +1 -1
  16. package/dist/server/embedded-host-adapters.d.ts +3 -0
  17. package/dist/server/embedded-host-adapters.d.ts.map +1 -1
  18. package/dist/server/embedded-message-router.d.ts.map +1 -1
  19. package/dist/server/entry.js +559 -209
  20. package/dist/server/entry.js.map +4 -4
  21. package/dist/server/http-server.d.ts.map +1 -1
  22. package/dist/server/project-handlers.d.ts +1 -0
  23. package/dist/server/project-handlers.d.ts.map +1 -1
  24. package/dist/server/routes.d.ts +2 -0
  25. package/dist/server/routes.d.ts.map +1 -1
  26. package/dist/server/sdd-board-ws-handler.d.ts +8 -1
  27. package/dist/server/sdd-board-ws-handler.d.ts.map +1 -1
  28. package/dist/server/session-handlers.d.ts +3 -0
  29. package/dist/server/session-handlers.d.ts.map +1 -1
  30. package/dist/server/setup-events.d.ts.map +1 -1
  31. package/dist/server/start-webui.d.ts +12 -0
  32. package/dist/server/start-webui.d.ts.map +1 -1
  33. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -134,85 +134,85 @@ var ENUM_PREF_KEYS = {
134
134
  autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
135
135
  fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"])
136
136
  };
137
- function validateModelRuntimeValue(modelRuntime, path33) {
137
+ function validateModelRuntimeValue(modelRuntime, path34) {
138
138
  const reasoning = modelRuntime["reasoning"];
139
139
  if (reasoning !== void 0) {
140
- if (!isRecord(reasoning)) return `${path33}.reasoning must be an object when provided`;
140
+ if (!isRecord(reasoning)) return `${path34}.reasoning must be an object when provided`;
141
141
  const mode = reasoning["mode"];
142
142
  const effort = reasoning["effort"];
143
143
  const preserve = reasoning["preserve"];
144
144
  if (mode !== void 0 && (typeof mode !== "string" || !REASONING_MODE_VALUES.has(mode))) {
145
- return `${path33}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
145
+ return `${path34}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
146
146
  }
147
147
  if (effort !== void 0 && (typeof effort !== "string" || !REASONING_EFFORT_VALUES.has(effort))) {
148
- return `${path33}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
148
+ return `${path34}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
149
149
  }
150
150
  if (preserve !== void 0 && typeof preserve !== "boolean") {
151
- return `${path33}.reasoning.preserve must be a boolean when provided`;
151
+ return `${path34}.reasoning.preserve must be a boolean when provided`;
152
152
  }
153
153
  }
154
154
  const cache2 = modelRuntime["cache"];
155
155
  if (cache2 !== void 0) {
156
- if (!isRecord(cache2)) return `${path33}.cache must be an object when provided`;
156
+ if (!isRecord(cache2)) return `${path34}.cache must be an object when provided`;
157
157
  const ttl = cache2["ttl"];
158
158
  if (ttl !== void 0 && (typeof ttl !== "string" || !CACHE_TTL_VALUES.has(ttl) || ttl === "default")) {
159
- return `${path33}.cache.ttl must be one of: 5m, 1h`;
159
+ return `${path34}.cache.ttl must be one of: 5m, 1h`;
160
160
  }
161
161
  }
162
162
  const parameters = modelRuntime["parameters"];
163
163
  if (parameters !== void 0 && !isRecord(parameters)) {
164
- return `${path33}.parameters must be an object when provided`;
164
+ return `${path34}.parameters must be an object when provided`;
165
165
  }
166
166
  return null;
167
167
  }
168
- function validateModelBlackoutRule(rule, path33) {
168
+ function validateModelBlackoutRule(rule, path34) {
169
169
  const id = rule["id"];
170
170
  if (typeof id !== "string" || id.trim().length === 0) {
171
- return `${path33}.id must be a non-empty string`;
171
+ return `${path34}.id must be a non-empty string`;
172
172
  }
173
173
  const start = rule["start"];
174
174
  if (typeof start !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(start)) {
175
- return `${path33}.start must be a string in HH:mm (00:00-23:59) format`;
175
+ return `${path34}.start must be a string in HH:mm (00:00-23:59) format`;
176
176
  }
177
177
  const end = rule["end"];
178
178
  if (typeof end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(end)) {
179
- return `${path33}.end must be a string in HH:mm (00:00-23:59) format`;
179
+ return `${path34}.end must be a string in HH:mm (00:00-23:59) format`;
180
180
  }
181
181
  if (rule["enabled"] !== void 0 && typeof rule["enabled"] !== "boolean") {
182
- return `${path33}.enabled must be a boolean when provided`;
182
+ return `${path34}.enabled must be a boolean when provided`;
183
183
  }
184
184
  if (rule["provider"] !== void 0 && typeof rule["provider"] !== "string") {
185
- return `${path33}.provider must be a string when provided`;
185
+ return `${path34}.provider must be a string when provided`;
186
186
  }
187
187
  if (rule["model"] !== void 0 && typeof rule["model"] !== "string") {
188
- return `${path33}.model must be a string when provided`;
188
+ return `${path34}.model must be a string when provided`;
189
189
  }
190
190
  if (rule["days"] !== void 0) {
191
- if (!Array.isArray(rule["days"])) return `${path33}.days must be an array when provided`;
191
+ if (!Array.isArray(rule["days"])) return `${path34}.days must be an array when provided`;
192
192
  const seen = /* @__PURE__ */ new Set();
193
193
  for (const d of rule["days"]) {
194
194
  if (typeof d !== "number" || !Number.isInteger(d) || d < 0 || d > 6) {
195
- return `${path33}.days elements must be integers 0-6 when provided`;
195
+ return `${path34}.days elements must be integers 0-6 when provided`;
196
196
  }
197
- if (seen.has(d)) return `${path33}.days contains duplicate day: ${d}`;
197
+ if (seen.has(d)) return `${path34}.days contains duplicate day: ${d}`;
198
198
  seen.add(d);
199
199
  }
200
200
  }
201
201
  if (rule["timezone"] !== void 0) {
202
202
  if (typeof rule["timezone"] !== "string") {
203
- return `${path33}.timezone must be a string when provided`;
203
+ return `${path34}.timezone must be a string when provided`;
204
204
  }
205
205
  try {
206
206
  Intl.DateTimeFormat(void 0, { timeZone: rule["timezone"] });
207
207
  } catch {
208
- return `${path33}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
208
+ return `${path34}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
209
209
  }
210
210
  }
211
211
  if (rule["label"] !== void 0 && typeof rule["label"] !== "string") {
212
- return `${path33}.label must be a string when provided`;
212
+ return `${path34}.label must be a string when provided`;
213
213
  }
214
214
  if (rule["mode"] !== void 0 && rule["mode"] !== "blackout" && rule["mode"] !== "allow_only") {
215
- return `${path33}.mode must be 'blackout' or 'allow_only' when provided`;
215
+ return `${path34}.mode must be 'blackout' or 'allow_only' when provided`;
216
216
  }
217
217
  return null;
218
218
  }
@@ -760,8 +760,8 @@ function validateShellOpenPayload(payload) {
760
760
  if (!isRecord2(payload)) {
761
761
  return { ok: false, message: "shell.open payload must be an object with string path" };
762
762
  }
763
- const path33 = payload["path"];
764
- if (typeof path33 !== "string" || path33.trim().length === 0) {
763
+ const path34 = payload["path"];
764
+ if (typeof path34 !== "string" || path34.trim().length === 0) {
765
765
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
766
766
  }
767
767
  const target = payload["target"];
@@ -774,7 +774,7 @@ function validateShellOpenPayload(payload) {
774
774
  return {
775
775
  ok: true,
776
776
  value: {
777
- path: path33,
777
+ path: path34,
778
778
  ...target !== void 0 ? { target } : {}
779
779
  }
780
780
  };
@@ -783,14 +783,14 @@ function validateGitDiffPayload(payload) {
783
783
  if (!isRecord2(payload)) {
784
784
  return { ok: false, message: "git.diff payload must be an object" };
785
785
  }
786
- const path33 = payload["path"];
787
- if (path33 === void 0 || path33 === null) {
786
+ const path34 = payload["path"];
787
+ if (path34 === void 0 || path34 === null) {
788
788
  return { ok: true, value: { path: "" } };
789
789
  }
790
- if (typeof path33 !== "string") {
790
+ if (typeof path34 !== "string") {
791
791
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
792
792
  }
793
- return { ok: true, value: { path: path33 } };
793
+ return { ok: true, value: { path: path34 } };
794
794
  }
795
795
  function validateProjectsAddPayload(payload) {
796
796
  if (!isRecord2(payload)) {
@@ -4376,8 +4376,8 @@ function jsonByteLength(value) {
4376
4376
  return MAX_PAYLOAD_BYTES + 1;
4377
4377
  }
4378
4378
  }
4379
- function error(errors, path33, code, message) {
4380
- errors.push({ path: path33, code, message });
4379
+ function error(errors, path34, code, message) {
4380
+ errors.push({ path: path34, code, message });
4381
4381
  }
4382
4382
  function isMessageRole(value) {
4383
4383
  return value === "user" || value === "assistant" || value === "system";
@@ -4385,25 +4385,25 @@ function isMessageRole(value) {
4385
4385
  function isPlainJsonObject(value) {
4386
4386
  return isRecord3(value);
4387
4387
  }
4388
- function validateCacheControl(value, path33, errors) {
4388
+ function validateCacheControl(value, path34, errors) {
4389
4389
  if (value === void 0) return void 0;
4390
4390
  if (!isRecord3(value) || value["type"] !== "ephemeral") {
4391
- error(errors, path33, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4391
+ error(errors, path34, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4392
4392
  return void 0;
4393
4393
  }
4394
4394
  return { type: "ephemeral" };
4395
4395
  }
4396
- function validateProviderMeta(value, path33, errors) {
4396
+ function validateProviderMeta(value, path34, errors) {
4397
4397
  if (value === void 0) return void 0;
4398
4398
  if (!isPlainJsonObject(value)) {
4399
- error(errors, path33, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4399
+ error(errors, path34, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4400
4400
  return void 0;
4401
4401
  }
4402
4402
  return value;
4403
4403
  }
4404
- function validateBlock(value, path33, errors) {
4404
+ function validateBlock(value, path34, errors) {
4405
4405
  if (!isRecord3(value)) {
4406
- error(errors, path33, "INVALID_BLOCK", "Content block must be an object.");
4406
+ error(errors, path34, "INVALID_BLOCK", "Content block must be an object.");
4407
4407
  return void 0;
4408
4408
  }
4409
4409
  const type = value["type"];
@@ -4411,14 +4411,14 @@ function validateBlock(value, path33, errors) {
4411
4411
  case "text": {
4412
4412
  const text2 = value["text"];
4413
4413
  if (typeof text2 !== "string") {
4414
- error(errors, `${path33}/text`, "INVALID_TEXT", "Text block text must be a string.");
4414
+ error(errors, `${path34}/text`, "INVALID_TEXT", "Text block text must be a string.");
4415
4415
  return void 0;
4416
4416
  }
4417
4417
  if (text2.length > MAX_STRING_LENGTH) {
4418
- error(errors, `${path33}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4418
+ error(errors, `${path34}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4419
4419
  return void 0;
4420
4420
  }
4421
- const cacheControl = validateCacheControl(value["cache_control"], `${path33}/cache_control`, errors);
4421
+ const cacheControl = validateCacheControl(value["cache_control"], `${path34}/cache_control`, errors);
4422
4422
  return cacheControl ? { type: "text", text: text2, cache_control: cacheControl } : { type: "text", text: text2 };
4423
4423
  }
4424
4424
  case "tool_use": {
@@ -4426,15 +4426,15 @@ function validateBlock(value, path33, errors) {
4426
4426
  const name2 = value["name"];
4427
4427
  const input = value["input"];
4428
4428
  if (typeof id !== "string" || id.length === 0) {
4429
- error(errors, `${path33}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4429
+ error(errors, `${path34}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4430
4430
  }
4431
4431
  if (typeof name2 !== "string" || name2.length === 0) {
4432
- error(errors, `${path33}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4432
+ error(errors, `${path34}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4433
4433
  }
4434
4434
  if (!isPlainJsonObject(input)) {
4435
- error(errors, `${path33}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4435
+ error(errors, `${path34}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4436
4436
  }
4437
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path33}/providerMeta`, errors);
4437
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path34}/providerMeta`, errors);
4438
4438
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4439
4439
  return void 0;
4440
4440
  }
@@ -4446,18 +4446,18 @@ function validateBlock(value, path33, errors) {
4446
4446
  const content = value["content"];
4447
4447
  const isError = value["is_error"];
4448
4448
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4449
- error(errors, `${path33}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4449
+ error(errors, `${path34}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4450
4450
  }
4451
4451
  if (name2 !== void 0 && typeof name2 !== "string") {
4452
- error(errors, `${path33}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4452
+ error(errors, `${path34}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4453
4453
  }
4454
4454
  if (typeof content !== "string") {
4455
- error(errors, `${path33}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4455
+ error(errors, `${path34}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4456
4456
  } else if (content.length > MAX_STRING_LENGTH) {
4457
- error(errors, `${path33}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4457
+ error(errors, `${path34}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4458
4458
  }
4459
4459
  if (isError !== void 0 && typeof isError !== "boolean") {
4460
- error(errors, `${path33}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4460
+ error(errors, `${path34}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4461
4461
  }
4462
4462
  if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4463
4463
  return {
@@ -4471,25 +4471,25 @@ function validateBlock(value, path33, errors) {
4471
4471
  case "image": {
4472
4472
  const source = value["source"];
4473
4473
  if (!isRecord3(source)) {
4474
- error(errors, `${path33}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4474
+ error(errors, `${path34}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4475
4475
  return void 0;
4476
4476
  }
4477
4477
  const sourceType = source["type"];
4478
4478
  if (sourceType !== "base64" && sourceType !== "url") {
4479
- error(errors, `${path33}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4479
+ error(errors, `${path34}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4480
4480
  return void 0;
4481
4481
  }
4482
4482
  const mediaType = source["media_type"];
4483
4483
  const data = source["data"];
4484
4484
  const url = source["url"];
4485
4485
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4486
- error(errors, `${path33}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4486
+ error(errors, `${path34}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4487
4487
  }
4488
4488
  if (data !== void 0 && typeof data !== "string") {
4489
- error(errors, `${path33}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4489
+ error(errors, `${path34}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4490
4490
  }
4491
4491
  if (url !== void 0 && typeof url !== "string") {
4492
- error(errors, `${path33}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4492
+ error(errors, `${path34}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4493
4493
  }
4494
4494
  return {
4495
4495
  type: "image",
@@ -4505,13 +4505,13 @@ function validateBlock(value, path33, errors) {
4505
4505
  const thinking = value["thinking"];
4506
4506
  const signature = value["signature"];
4507
4507
  if (typeof thinking !== "string") {
4508
- error(errors, `${path33}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4508
+ error(errors, `${path34}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4509
4509
  return void 0;
4510
4510
  }
4511
4511
  if (signature !== void 0 && typeof signature !== "string") {
4512
- error(errors, `${path33}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4512
+ error(errors, `${path34}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4513
4513
  }
4514
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path33}/providerMeta`, errors);
4514
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path34}/providerMeta`, errors);
4515
4515
  return {
4516
4516
  type: "thinking",
4517
4517
  thinking,
@@ -4520,7 +4520,7 @@ function validateBlock(value, path33, errors) {
4520
4520
  };
4521
4521
  }
4522
4522
  default:
4523
- error(errors, `${path33}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4523
+ error(errors, `${path34}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4524
4524
  return void 0;
4525
4525
  }
4526
4526
  }
@@ -4538,39 +4538,39 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4538
4538
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
4539
4539
  }
4540
4540
  value.forEach((item, index) => {
4541
- const path33 = `/messages/${index}`;
4541
+ const path34 = `/messages/${index}`;
4542
4542
  if (!isRecord3(item)) {
4543
- error(errors, path33, "INVALID_MESSAGE", "Message must be an object.");
4543
+ error(errors, path34, "INVALID_MESSAGE", "Message must be an object.");
4544
4544
  return;
4545
4545
  }
4546
4546
  const role = item["role"];
4547
4547
  if (!isMessageRole(role)) {
4548
- error(errors, `${path33}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4548
+ error(errors, `${path34}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4549
4549
  return;
4550
4550
  }
4551
4551
  const rawContent = item["content"];
4552
4552
  let content;
4553
4553
  if (typeof rawContent === "string") {
4554
4554
  if (rawContent.length > MAX_STRING_LENGTH) {
4555
- error(errors, `${path33}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4555
+ error(errors, `${path34}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4556
4556
  return;
4557
4557
  }
4558
4558
  content = rawContent;
4559
4559
  } else if (Array.isArray(rawContent)) {
4560
4560
  const blocks = [];
4561
4561
  rawContent.forEach((block, blockIndex) => {
4562
- const parsed = validateBlock(block, `${path33}/content/${blockIndex}`, errors);
4562
+ const parsed = validateBlock(block, `${path34}/content/${blockIndex}`, errors);
4563
4563
  if (parsed) blocks.push(parsed);
4564
4564
  });
4565
4565
  content = blocks;
4566
4566
  } else {
4567
- error(errors, `${path33}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4567
+ error(errors, `${path34}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4568
4568
  return;
4569
4569
  }
4570
4570
  const ts = item["ts"];
4571
4571
  if (ts !== void 0) {
4572
4572
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4573
- error(errors, `${path33}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4573
+ error(errors, `${path34}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4574
4574
  return;
4575
4575
  }
4576
4576
  }
@@ -4944,7 +4944,11 @@ function createConnectionLifecycle(options) {
4944
4944
  }
4945
4945
 
4946
4946
  // src/server/connections-health-route.ts
4947
- import { createChronicleProjectAccess as createChronicleProjectAccess2 } from "@wrongstack/core/chronicle";
4947
+ import {
4948
+ ChronicleProjectServerClient,
4949
+ createChronicleProjectAccess as createChronicleProjectAccess2,
4950
+ resolveChronicleProjectServerOptions
4951
+ } from "@wrongstack/core/chronicle";
4948
4952
  import {
4949
4953
  isMailboxProjectServerAvailable,
4950
4954
  MailboxProjectServerConnection
@@ -4952,7 +4956,11 @@ import {
4952
4956
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
4953
4957
  import { getKanbanServerConnection } from "@wrongstack/kanban";
4954
4958
  import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
4955
- import { checkCodebaseIndexServerHealth, getIndexState } from "@wrongstack/tools";
4959
+ import {
4960
+ checkCodebaseIndexServerHealth,
4961
+ getIndexState,
4962
+ shutdownCodebaseIndexServer
4963
+ } from "@wrongstack/tools";
4956
4964
  async function handleConnectionsHealthRoute(context, ws, message) {
4957
4965
  if (message.type !== "connections.health") return false;
4958
4966
  try {
@@ -5269,6 +5277,244 @@ async function mailboxHealth(projectRoot) {
5269
5277
  connection.close();
5270
5278
  }
5271
5279
  }
5280
+ async function handleConnectionsServiceAction(ws, message, context) {
5281
+ if (message.type !== "connections.service_action") return false;
5282
+ const payload = message.payload;
5283
+ const serviceId = payload?.serviceId;
5284
+ const rawAction = payload?.action ?? "shutdown";
5285
+ if (!serviceId) {
5286
+ context.send(ws, {
5287
+ type: "connections.service_action_result",
5288
+ payload: {
5289
+ serviceId: null,
5290
+ action: rawAction,
5291
+ success: false,
5292
+ message: "Missing serviceId in payload"
5293
+ }
5294
+ });
5295
+ return true;
5296
+ }
5297
+ if (rawAction !== "shutdown") {
5298
+ context.send(ws, {
5299
+ type: "connections.service_action_result",
5300
+ payload: {
5301
+ serviceId,
5302
+ action: rawAction,
5303
+ success: false,
5304
+ message: `Unsupported action "${rawAction}" \u2014 only "shutdown" is currently supported`
5305
+ }
5306
+ });
5307
+ return true;
5308
+ }
5309
+ const action = rawAction;
5310
+ if (serviceId === "webui") {
5311
+ context.send(ws, {
5312
+ type: "connections.service_action_result",
5313
+ payload: {
5314
+ serviceId: "webui",
5315
+ action,
5316
+ success: false,
5317
+ message: "Cannot shut down the WebUI transport itself"
5318
+ }
5319
+ });
5320
+ return true;
5321
+ }
5322
+ try {
5323
+ const result = await executeServiceAction(
5324
+ serviceId,
5325
+ action,
5326
+ context.getProjectRoot(),
5327
+ context.getIndexDir()
5328
+ );
5329
+ context.send(ws, {
5330
+ type: "connections.service_action_result",
5331
+ payload: result
5332
+ });
5333
+ } catch (error2) {
5334
+ context.send(ws, {
5335
+ type: "connections.service_action_result",
5336
+ payload: {
5337
+ serviceId,
5338
+ action,
5339
+ success: false,
5340
+ message: error2 instanceof Error ? error2.message : String(error2)
5341
+ }
5342
+ });
5343
+ }
5344
+ return true;
5345
+ }
5346
+ async function executeServiceAction(serviceId, action, projectRoot, indexDir) {
5347
+ switch (serviceId) {
5348
+ case "kanban":
5349
+ return killKanbanServer(projectRoot, action);
5350
+ case "sage":
5351
+ return killSageServer(projectRoot, action);
5352
+ case "chronicle":
5353
+ return killChronicleServer(projectRoot, action);
5354
+ case "codebase-index":
5355
+ return killCodebaseIndexServer(projectRoot, indexDir, action);
5356
+ case "mailbox":
5357
+ return killMailboxServer(projectRoot, action);
5358
+ default:
5359
+ return {
5360
+ serviceId,
5361
+ action,
5362
+ success: false,
5363
+ message: `Unknown service: ${serviceId}`
5364
+ };
5365
+ }
5366
+ }
5367
+ async function killKanbanServer(projectRoot, action) {
5368
+ if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
5369
+ return {
5370
+ serviceId: "kanban",
5371
+ action,
5372
+ success: false,
5373
+ message: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0"
5374
+ };
5375
+ }
5376
+ let connection;
5377
+ try {
5378
+ connection = await getKanbanServerConnection(projectRoot);
5379
+ } catch (error2) {
5380
+ return {
5381
+ serviceId: "kanban",
5382
+ action,
5383
+ success: false,
5384
+ message: error2 instanceof Error ? error2.message : String(error2)
5385
+ };
5386
+ }
5387
+ if (!connection) {
5388
+ return {
5389
+ serviceId: "kanban",
5390
+ action,
5391
+ success: false,
5392
+ message: "Kanban IPC daemon is not running"
5393
+ };
5394
+ }
5395
+ try {
5396
+ const result = await connection.request("shutdown", {
5397
+ reason: `WebUI request: ${action}`
5398
+ });
5399
+ return {
5400
+ serviceId: "kanban",
5401
+ action,
5402
+ success: result.stopping,
5403
+ message: result.stopping ? `Kanban IPC daemon ${action} requested` : `Kanban IPC daemon ${action} failed (shutdown not confirmed)`
5404
+ };
5405
+ } catch (error2) {
5406
+ return {
5407
+ serviceId: "kanban",
5408
+ action,
5409
+ success: false,
5410
+ message: error2 instanceof Error ? error2.message : String(error2)
5411
+ };
5412
+ }
5413
+ }
5414
+ async function killSageServer(projectRoot, action) {
5415
+ if (!isSageProjectServerAvailable()) {
5416
+ return {
5417
+ serviceId: "sage",
5418
+ action,
5419
+ success: false,
5420
+ message: "SAGE project server is unavailable in this runtime"
5421
+ };
5422
+ }
5423
+ const connection = new SageProjectServerConnection(projectRoot);
5424
+ try {
5425
+ const result = await connection.shutdown(`WebUI request: ${action}`);
5426
+ return {
5427
+ serviceId: "sage",
5428
+ action,
5429
+ success: result.stopped,
5430
+ message: result.stopped ? `SAGE memory server ${action} requested` : `SAGE memory server ${action} failed: ${result.reason ?? "unknown"}`
5431
+ };
5432
+ } catch (error2) {
5433
+ return {
5434
+ serviceId: "sage",
5435
+ action,
5436
+ success: false,
5437
+ message: error2 instanceof Error ? error2.message : String(error2)
5438
+ };
5439
+ } finally {
5440
+ connection.close();
5441
+ }
5442
+ }
5443
+ async function killChronicleServer(projectRoot, action) {
5444
+ const options = resolveChronicleProjectServerOptions({ projectRoot });
5445
+ const client = new ChronicleProjectServerClient(options);
5446
+ try {
5447
+ const result = await client.shutdown(`WebUI request: ${action}`);
5448
+ return {
5449
+ serviceId: "chronicle",
5450
+ action,
5451
+ success: result.stopped,
5452
+ message: result.stopped ? `Chronicle telemetry server ${action} requested` : `Chronicle telemetry server ${action} failed: ${result.reason ?? "unknown"}`
5453
+ };
5454
+ } catch (error2) {
5455
+ return {
5456
+ serviceId: "chronicle",
5457
+ action,
5458
+ success: false,
5459
+ message: error2 instanceof Error ? error2.message : String(error2)
5460
+ };
5461
+ } finally {
5462
+ client.close();
5463
+ }
5464
+ }
5465
+ async function killCodebaseIndexServer(projectRoot, indexDir, action) {
5466
+ try {
5467
+ const result = await shutdownCodebaseIndexServer(
5468
+ projectRoot,
5469
+ indexDir,
5470
+ `websocket-request:${action}`
5471
+ );
5472
+ return {
5473
+ serviceId: "codebase-index",
5474
+ action,
5475
+ success: result.stopped,
5476
+ message: result.stopped ? `Codebase index server ${action} requested` : `Codebase index server ${action} failed: ${result.reason ?? "unknown"}`
5477
+ };
5478
+ } catch (error2) {
5479
+ return {
5480
+ serviceId: "codebase-index",
5481
+ action,
5482
+ success: false,
5483
+ message: error2 instanceof Error ? error2.message : String(error2)
5484
+ };
5485
+ }
5486
+ }
5487
+ async function killMailboxServer(projectRoot, action) {
5488
+ if (!isMailboxProjectServerAvailable()) {
5489
+ return {
5490
+ serviceId: "mailbox",
5491
+ action,
5492
+ success: false,
5493
+ message: "Mailbox project server is unavailable in this runtime"
5494
+ };
5495
+ }
5496
+ const connection = new MailboxProjectServerConnection(
5497
+ resolveWstackPaths2({ projectRoot }).projectDir
5498
+ );
5499
+ try {
5500
+ const result = await connection.shutdown(`WebUI request: ${action}`);
5501
+ return {
5502
+ serviceId: "mailbox",
5503
+ action,
5504
+ success: result.stopped,
5505
+ message: result.stopped ? `Mailbox IPC server ${action} requested` : `Mailbox IPC server ${action} failed: ${result.reason ?? "unknown"}`
5506
+ };
5507
+ } catch (error2) {
5508
+ return {
5509
+ serviceId: "mailbox",
5510
+ action,
5511
+ success: false,
5512
+ message: error2 instanceof Error ? error2.message : String(error2)
5513
+ };
5514
+ } finally {
5515
+ connection.close();
5516
+ }
5517
+ }
5272
5518
  function failureService(id, label, required, mode, error2, latencyMs) {
5273
5519
  const message = error2 instanceof Error ? error2.message : String(error2);
5274
5520
  return {
@@ -5429,9 +5675,9 @@ async function handleGitInfo(ws, projectRoot) {
5429
5675
  const cwd = projectRoot || void 0;
5430
5676
  try {
5431
5677
  const { execFile: ef } = await import("node:child_process");
5432
- const git = (args) => new Promise((resolve15) => {
5678
+ const git = (args) => new Promise((resolve16) => {
5433
5679
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
5434
- resolve15(err ? "" : stdout.trim());
5680
+ resolve16(err ? "" : stdout.trim());
5435
5681
  });
5436
5682
  });
5437
5683
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -5457,12 +5703,12 @@ async function handleGitInfo(ws, projectRoot) {
5457
5703
  function makeGit(cwd) {
5458
5704
  return async (args) => {
5459
5705
  const { execFile: ef } = await import("node:child_process");
5460
- return new Promise((resolve15) => {
5706
+ return new Promise((resolve16) => {
5461
5707
  ef(
5462
5708
  "git",
5463
5709
  args,
5464
5710
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
5465
- (err, stdout) => resolve15(err ? "" : stdout)
5711
+ (err, stdout) => resolve16(err ? "" : stdout)
5466
5712
  );
5467
5713
  });
5468
5714
  };
@@ -5486,15 +5732,15 @@ async function handleGitChanges(ws, projectRoot) {
5486
5732
  if (!m) continue;
5487
5733
  const added = m[1] === "-" ? 0 : Number(m[1]);
5488
5734
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5489
- let path33 = m[3] ?? "";
5490
- if (path33 === "") {
5735
+ let path34 = m[3] ?? "";
5736
+ if (path34 === "") {
5491
5737
  i += 1;
5492
- path33 = parts[i + 1] ?? parts[i] ?? "";
5738
+ path34 = parts[i + 1] ?? parts[i] ?? "";
5493
5739
  i += 1;
5494
5740
  }
5495
- if (!path33) continue;
5496
- const prev = counts.get(path33) ?? { added: 0, deleted: 0 };
5497
- counts.set(path33, { added: prev.added + added, deleted: prev.deleted + deleted });
5741
+ if (!path34) continue;
5742
+ const prev = counts.get(path34) ?? { added: 0, deleted: 0 };
5743
+ counts.set(path34, { added: prev.added + added, deleted: prev.deleted + deleted });
5498
5744
  }
5499
5745
  };
5500
5746
  parseNumstat(unstagedNumstat);
@@ -5506,7 +5752,7 @@ async function handleGitChanges(ws, projectRoot) {
5506
5752
  if (!rec || rec.length < 3) continue;
5507
5753
  const x = rec[0] ?? " ";
5508
5754
  const y = rec[1] ?? " ";
5509
- const path33 = rec.slice(3);
5755
+ const path34 = rec.slice(3);
5510
5756
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5511
5757
  if (isRename) i += 1;
5512
5758
  let status;
@@ -5518,13 +5764,13 @@ async function handleGitChanges(ws, projectRoot) {
5518
5764
  else if (x === "D" || y === "D") status = "D";
5519
5765
  else status = "M";
5520
5766
  const staged = x !== " " && x !== "?";
5521
- let added = counts.get(path33)?.added ?? 0;
5522
- let deleted = counts.get(path33)?.deleted ?? 0;
5767
+ let added = counts.get(path34)?.added ?? 0;
5768
+ let deleted = counts.get(path34)?.deleted ?? 0;
5523
5769
  if (status === "?") {
5524
5770
  added = 0;
5525
5771
  deleted = 0;
5526
5772
  }
5527
- files.push({ path: path33, status, added, deleted, staged });
5773
+ files.push({ path: path34, status, added, deleted, staged });
5528
5774
  }
5529
5775
  send(ws, { type: "git.changes", payload: { files } });
5530
5776
  } catch (err) {
@@ -5535,10 +5781,10 @@ async function handleGitChanges(ws, projectRoot) {
5535
5781
  }
5536
5782
  }
5537
5783
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5538
- async function handleGitDiff(ws, projectRoot, path33) {
5784
+ async function handleGitDiff(ws, projectRoot, path34) {
5539
5785
  const cwd = projectRoot || void 0;
5540
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path33, ...extra } });
5541
- if (!path33 || path33.includes("\0") || path33.includes("..") || nodePath.isAbsolute(path33)) {
5786
+ const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path34, ...extra } });
5787
+ if (!path34 || path34.includes("\0") || path34.includes("..") || nodePath.isAbsolute(path34)) {
5542
5788
  reply2({ oldText: "", newText: "", error: "invalid path" });
5543
5789
  return;
5544
5790
  }
@@ -5546,10 +5792,10 @@ async function handleGitDiff(ws, projectRoot, path33) {
5546
5792
  const git = makeGit(cwd);
5547
5793
  const { readFile: readFile13 } = await import("node:fs/promises");
5548
5794
  const { join: join18 } = await import("node:path");
5549
- const oldText = await git(["show", `HEAD:${path33}`]);
5795
+ const oldText = await git(["show", `HEAD:${path34}`]);
5550
5796
  let newText = "";
5551
5797
  try {
5552
- const abs = cwd ? join18(cwd, path33) : path33;
5798
+ const abs = cwd ? join18(cwd, path34) : path34;
5553
5799
  const buf = await readFile13(abs);
5554
5800
  if (buf.includes(0)) {
5555
5801
  reply2({ oldText: "", newText: "", binary: true });
@@ -5629,7 +5875,7 @@ import { execFile } from "node:child_process";
5629
5875
  var GIT_TIMEOUT_MS = 1e4;
5630
5876
  var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
5631
5877
  function gitStdout(cwd, args) {
5632
- return new Promise((resolve15) => {
5878
+ return new Promise((resolve16) => {
5633
5879
  execFile(
5634
5880
  "git",
5635
5881
  [...args],
@@ -5640,7 +5886,7 @@ function gitStdout(cwd, args) {
5640
5886
  timeout: GIT_TIMEOUT_MS,
5641
5887
  maxBuffer: GIT_MAX_OUTPUT_BYTES
5642
5888
  },
5643
- (error2, stdout) => resolve15(error2 ? null : stdout)
5889
+ (error2, stdout) => resolve16(error2 ? null : stdout)
5644
5890
  );
5645
5891
  });
5646
5892
  }
@@ -5906,13 +6152,13 @@ var GoalWebSocketHandler = class {
5906
6152
  const cwd = env?.cwd ?? this.projectRoot;
5907
6153
  try {
5908
6154
  const { exec } = await import("node:child_process");
5909
- const result = await new Promise((resolve15) => {
6155
+ const result = await new Promise((resolve16) => {
5910
6156
  exec("npx tsc --noEmit", { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
5911
6157
  if (err && err.code === "ENOENT") {
5912
- resolve15("[verify] tsc not found \u2014 skipping");
6158
+ resolve16("[verify] tsc not found \u2014 skipping");
5913
6159
  return;
5914
6160
  }
5915
- resolve15(stdout + stderr);
6161
+ resolve16(stdout + stderr);
5916
6162
  });
5917
6163
  });
5918
6164
  if (result.includes("[verify]") || result.trim().length === 0) {
@@ -6591,7 +6837,7 @@ function pushEvent(event) {
6591
6837
  }
6592
6838
  }
6593
6839
  function parseBody(req) {
6594
- return new Promise((resolve15, reject) => {
6840
+ return new Promise((resolve16, reject) => {
6595
6841
  let body = "";
6596
6842
  let bodyBytes = 0;
6597
6843
  let tooLarge = false;
@@ -6611,7 +6857,7 @@ function parseBody(req) {
6611
6857
  return;
6612
6858
  }
6613
6859
  try {
6614
- resolve15(JSON.parse(body));
6860
+ resolve16(JSON.parse(body));
6615
6861
  } catch {
6616
6862
  reject(new Error("Invalid JSON"));
6617
6863
  }
@@ -6696,7 +6942,7 @@ function getAnalyticsBuffer() {
6696
6942
  // src/server/http-server.ts
6697
6943
  import * as fs9 from "node:fs/promises";
6698
6944
  import * as http from "node:http";
6699
- import * as path11 from "node:path";
6945
+ import * as path12 from "node:path";
6700
6946
  import * as v8 from "node:v8";
6701
6947
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6702
6948
 
@@ -6776,6 +7022,156 @@ async function handleCodemapSymbols(res, deps2, file) {
6776
7022
  );
6777
7023
  }
6778
7024
 
7025
+ // src/server/deadcode-handlers.ts
7026
+ import * as path10 from "node:path";
7027
+ import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
7028
+ var MAX_BODY_BYTES = 10 * 1024 * 1024;
7029
+ function readJsonBody(req) {
7030
+ return new Promise((resolve16, reject) => {
7031
+ const chunks = [];
7032
+ let total = 0;
7033
+ req.on("data", (chunk) => {
7034
+ total += chunk.length;
7035
+ if (total > MAX_BODY_BYTES) {
7036
+ req.destroy(new Error("Request body too large"));
7037
+ reject(new Error("Request body exceeds 10 MiB limit"));
7038
+ return;
7039
+ }
7040
+ chunks.push(chunk);
7041
+ });
7042
+ req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
7043
+ req.on("error", (err) => reject(err));
7044
+ });
7045
+ }
7046
+ async function handleDeadCodeScan(res, deps2, req) {
7047
+ try {
7048
+ let body = {};
7049
+ const raw = await readJsonBody(req);
7050
+ if (raw) {
7051
+ try {
7052
+ body = JSON.parse(raw);
7053
+ } catch {
7054
+ res.writeHead(400, { "Content-Type": "application/json" });
7055
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
7056
+ return;
7057
+ }
7058
+ }
7059
+ const scanIndexDir = body.indexDir ?? deps2.indexDir;
7060
+ if (scanIndexDir) {
7061
+ const resolvedRoot = path10.resolve(deps2.projectRoot);
7062
+ const resolvedIndex = path10.resolve(deps2.projectRoot, scanIndexDir);
7063
+ if (resolvedIndex !== resolvedRoot && !resolvedIndex.startsWith(resolvedRoot + path10.sep)) {
7064
+ res.writeHead(400, { "Content-Type": "application/json" });
7065
+ res.end(JSON.stringify({ error: "Invalid indexDir: must be within project root" }));
7066
+ return;
7067
+ }
7068
+ }
7069
+ const result = runDeadCodeScan(deps2.projectRoot, {
7070
+ indexDir: scanIndexDir,
7071
+ userEntryPoints: body.entryPoints
7072
+ });
7073
+ res.writeHead(200, { "Content-Type": "application/json" });
7074
+ res.end(JSON.stringify(result));
7075
+ } catch (err) {
7076
+ res.writeHead(500, { "Content-Type": "application/json" });
7077
+ res.end(
7078
+ JSON.stringify({
7079
+ error: "Dead-code scan failed",
7080
+ detail: err instanceof Error ? err.message : String(err)
7081
+ })
7082
+ );
7083
+ }
7084
+ }
7085
+ function handleDeadCodeActionPlan(res, _deps, req) {
7086
+ return readJsonBody(req).then((raw) => {
7087
+ let parsed;
7088
+ try {
7089
+ parsed = JSON.parse(raw);
7090
+ } catch {
7091
+ res.writeHead(400, { "Content-Type": "application/json" });
7092
+ res.end(JSON.stringify({ error: "Invalid scan result JSON" }));
7093
+ return;
7094
+ }
7095
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.deadPackages) || !Array.isArray(parsed.deadFiles) || !Array.isArray(parsed.deadSymbols)) {
7096
+ res.writeHead(400, { "Content-Type": "application/json" });
7097
+ res.end(
7098
+ JSON.stringify({
7099
+ error: "Invalid scan result: missing or malformed required fields (deadPackages, deadFiles, deadSymbols)"
7100
+ })
7101
+ );
7102
+ return;
7103
+ }
7104
+ const result = parsed;
7105
+ const plan = buildActionPlan(result);
7106
+ res.writeHead(200, { "Content-Type": "application/json" });
7107
+ res.end(JSON.stringify(plan));
7108
+ }).catch((err) => {
7109
+ res.writeHead(500, { "Content-Type": "application/json" });
7110
+ res.end(
7111
+ JSON.stringify({
7112
+ error: "Failed to read request body",
7113
+ detail: err instanceof Error ? err.message : String(err)
7114
+ })
7115
+ );
7116
+ });
7117
+ }
7118
+ function buildActionPlan(result) {
7119
+ const files = /* @__PURE__ */ new Map();
7120
+ for (const dp of result.deadPackages) {
7121
+ const pseudoFile = {
7122
+ file: `${dp.package}/ (package)`,
7123
+ symbolCount: dp.fileCount,
7124
+ symbols: [`remove package ${dp.package} (${dp.fileCount} files, path: ${dp.path})`],
7125
+ priority: 0
7126
+ };
7127
+ files.set(pseudoFile.file, pseudoFile);
7128
+ }
7129
+ for (const df of result.deadFiles) {
7130
+ const existing = files.get(df.file);
7131
+ if (existing) {
7132
+ if (existing.priority > 1) existing.priority = 1;
7133
+ existing.symbolCount += df.symbolCount;
7134
+ continue;
7135
+ }
7136
+ files.set(df.file, {
7137
+ file: df.file,
7138
+ symbolCount: df.symbolCount,
7139
+ symbols: [`entire file (${df.symbolCount} symbols) is dead`],
7140
+ priority: 1
7141
+ });
7142
+ }
7143
+ const deadInAliveFiles = /* @__PURE__ */ new Map();
7144
+ const deadFileSet = new Set(result.deadFiles.map((df) => df.file));
7145
+ for (const ds of result.deadSymbols) {
7146
+ if (deadFileSet.has(ds.file)) continue;
7147
+ const list = deadInAliveFiles.get(ds.file) ?? [];
7148
+ list.push(`${ds.kind} ${ds.name} (line ${ds.line})`);
7149
+ deadInAliveFiles.set(ds.file, list);
7150
+ }
7151
+ for (const [file, symbols] of deadInAliveFiles) {
7152
+ const existing = files.get(file);
7153
+ if (existing) {
7154
+ existing.symbols.push(...symbols);
7155
+ existing.symbolCount += symbols.length;
7156
+ continue;
7157
+ }
7158
+ files.set(file, {
7159
+ file,
7160
+ symbolCount: symbols.length,
7161
+ symbols,
7162
+ priority: 2
7163
+ });
7164
+ }
7165
+ const sorted = [...files.values()].sort((a, b) => a.priority - b.priority || a.file.localeCompare(b.file));
7166
+ return {
7167
+ summary: `Dead-code scan found ${result.stats.dead} dead symbols across ${result.deadFiles.length} dead files and ${result.deadPackages.length} dead packages. Action plan has ${sorted.length} file group(s) to address.`,
7168
+ files: sorted,
7169
+ totalDeadSymbols: result.stats.dead,
7170
+ totalDeadFiles: result.deadFiles.length,
7171
+ totalDeadPackages: result.deadPackages.length
7172
+ };
7173
+ }
7174
+
6779
7175
  // src/server/http-server/api-handlers.ts
6780
7176
  async function handleApiSessions(res, globalRoot) {
6781
7177
  if (!globalRoot) {
@@ -6997,8 +7393,8 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
6997
7393
  res.end(JSON.stringify({ error: String(err) }));
6998
7394
  }
6999
7395
  }
7000
- function readJsonBody(req) {
7001
- return new Promise((resolve15, reject) => {
7396
+ function readJsonBody2(req) {
7397
+ return new Promise((resolve16, reject) => {
7002
7398
  let data = "";
7003
7399
  req.on("data", (chunk) => {
7004
7400
  data += chunk;
@@ -7009,7 +7405,7 @@ function readJsonBody(req) {
7009
7405
  });
7010
7406
  req.on("end", () => {
7011
7407
  try {
7012
- resolve15(data ? JSON.parse(data) : {});
7408
+ resolve16(data ? JSON.parse(data) : {});
7013
7409
  } catch (err) {
7014
7410
  reject(err instanceof Error ? err : new Error(String(err)));
7015
7411
  }
@@ -7025,7 +7421,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
7025
7421
  }
7026
7422
  let body;
7027
7423
  try {
7028
- body = await readJsonBody(req);
7424
+ body = await readJsonBody2(req);
7029
7425
  } catch {
7030
7426
  res.writeHead(400, { "Content-Type": "application/json" });
7031
7427
  res.end(JSON.stringify({ error: "Invalid request body" }));
@@ -7127,7 +7523,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7127
7523
  }
7128
7524
  let body = {};
7129
7525
  try {
7130
- body = await readJsonBody(req);
7526
+ body = await readJsonBody2(req);
7131
7527
  } catch {
7132
7528
  }
7133
7529
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
@@ -7168,7 +7564,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7168
7564
  }
7169
7565
  let body;
7170
7566
  try {
7171
- body = await readJsonBody(req);
7567
+ body = await readJsonBody2(req);
7172
7568
  } catch {
7173
7569
  res.writeHead(400, { "Content-Type": "application/json" });
7174
7570
  res.end(JSON.stringify({ error: "Invalid request body" }));
@@ -7232,12 +7628,12 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7232
7628
 
7233
7629
  // src/server/projects-manifest.ts
7234
7630
  import * as fs8 from "node:fs/promises";
7235
- import * as path10 from "node:path";
7631
+ import * as path11 from "node:path";
7236
7632
  import { ConfigError } from "@wrongstack/core/types";
7237
7633
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7238
7634
  function projectsJsonPath(globalConfigPath) {
7239
- const base = path10.dirname(globalConfigPath);
7240
- return path10.join(base, "projects.json");
7635
+ const base = path11.dirname(globalConfigPath);
7636
+ return path11.join(base, "projects.json");
7241
7637
  }
7242
7638
  async function loadManifest(globalConfigPath) {
7243
7639
  try {
@@ -7250,37 +7646,37 @@ async function loadManifest(globalConfigPath) {
7250
7646
  }
7251
7647
  async function saveManifest(manifest, globalConfigPath) {
7252
7648
  const file = projectsJsonPath(globalConfigPath);
7253
- await fs8.mkdir(path10.dirname(file), { recursive: true });
7649
+ await fs8.mkdir(path11.dirname(file), { recursive: true });
7254
7650
  await fs8.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7255
7651
  }
7256
7652
  function generateProjectSlug(rootPath) {
7257
7653
  return projectSlug(rootPath);
7258
7654
  }
7259
7655
  async function ensureProjectDataDir(slug, globalConfigPath) {
7260
- const base = path10.dirname(globalConfigPath);
7261
- const dir = path10.join(base, "projects", slug);
7656
+ const base = path11.dirname(globalConfigPath);
7657
+ const dir = path11.join(base, "projects", slug);
7262
7658
  await fs8.mkdir(dir, { recursive: true });
7263
7659
  return dir;
7264
7660
  }
7265
7661
  async function touchProjectInManifest(options, globalConfigPath) {
7266
- const root = path10.resolve(options.projectRoot);
7662
+ const root = path11.resolve(options.projectRoot);
7267
7663
  const file = projectsJsonPath(globalConfigPath);
7268
7664
  let entry;
7269
7665
  await withFileLock(file, async () => {
7270
7666
  const manifest = await loadManifest(globalConfigPath);
7271
7667
  const now = (/* @__PURE__ */ new Date()).toISOString();
7272
- entry = manifest.projects.find((candidate) => path10.resolve(candidate.root) === root);
7668
+ entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7273
7669
  if (entry) {
7274
7670
  entry.lastSeen = now;
7275
- if (options.workingDir) entry.lastWorkingDir = path10.resolve(options.workingDir);
7671
+ if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7276
7672
  } else {
7277
7673
  entry = {
7278
- name: options.name ?? path10.basename(root),
7674
+ name: options.name ?? path11.basename(root),
7279
7675
  root,
7280
7676
  slug: generateProjectSlug(root),
7281
7677
  createdAt: now,
7282
7678
  lastSeen: now,
7283
- lastWorkingDir: options.workingDir ? path10.resolve(options.workingDir) : void 0
7679
+ lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7284
7680
  };
7285
7681
  manifest.projects.push(entry);
7286
7682
  }
@@ -7691,9 +8087,9 @@ function buildCspHeader(publicWsUrl, host, port) {
7691
8087
  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'`;
7692
8088
  }
7693
8089
  function isInsideDist(candidate, distDir) {
7694
- const root = path11.resolve(distDir);
7695
- const resolved = path11.resolve(candidate);
7696
- return resolved === root || resolved.startsWith(root + path11.sep);
8090
+ const root = path12.resolve(distDir);
8091
+ const resolved = path12.resolve(candidate);
8092
+ return resolved === root || resolved.startsWith(root + path12.sep);
7697
8093
  }
7698
8094
  function decodeSessionId(segment) {
7699
8095
  try {
@@ -7713,7 +8109,7 @@ function strictDecodeParam(segment, res) {
7713
8109
  }
7714
8110
  function createHttpServer(opts) {
7715
8111
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
7716
- const distDir = path11.resolve(opts.distDir);
8112
+ const distDir = path12.resolve(opts.distDir);
7717
8113
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
7718
8114
  let techStackRuntime = null;
7719
8115
  const getTechStackRuntime = async () => {
@@ -7932,6 +8328,42 @@ function createHttpServer(opts) {
7932
8328
  );
7933
8329
  return;
7934
8330
  }
8331
+ if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
8332
+ if (requireAccessToken && !accessTokenOk) {
8333
+ res.writeHead(401, { "Content-Type": "application/json" });
8334
+ res.end(JSON.stringify({ error: "Unauthorized" }));
8335
+ return;
8336
+ }
8337
+ if (!opts.projectRoot) {
8338
+ res.writeHead(503, { "Content-Type": "application/json" });
8339
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8340
+ return;
8341
+ }
8342
+ const deadCodeDeps = {
8343
+ projectRoot: opts.projectRoot,
8344
+ ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8345
+ };
8346
+ await handleDeadCodeScan(res, deadCodeDeps, req);
8347
+ return;
8348
+ }
8349
+ if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8350
+ if (requireAccessToken && !accessTokenOk) {
8351
+ res.writeHead(401, { "Content-Type": "application/json" });
8352
+ res.end(JSON.stringify({ error: "Unauthorized" }));
8353
+ return;
8354
+ }
8355
+ if (!opts.projectRoot) {
8356
+ res.writeHead(503, { "Content-Type": "application/json" });
8357
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8358
+ return;
8359
+ }
8360
+ const deadCodeDeps = {
8361
+ projectRoot: opts.projectRoot,
8362
+ ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8363
+ };
8364
+ await handleDeadCodeActionPlan(res, deadCodeDeps, req);
8365
+ return;
8366
+ }
7935
8367
  if (url.pathname.startsWith("/api/techstack/")) {
7936
8368
  if (requireAccessToken && !accessTokenOk) {
7937
8369
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -8064,17 +8496,17 @@ function createHttpServer(opts) {
8064
8496
  }
8065
8497
  let filePath;
8066
8498
  if (url.pathname === "/" || url.pathname === "") {
8067
- filePath = path11.join(distDir, "index.html");
8499
+ filePath = path12.join(distDir, "index.html");
8068
8500
  } else {
8069
- filePath = path11.join(distDir, url.pathname);
8501
+ filePath = path12.join(distDir, url.pathname);
8070
8502
  }
8071
- const resolvedPath = path11.resolve(filePath);
8503
+ const resolvedPath = path12.resolve(filePath);
8072
8504
  if (!isInsideDist(resolvedPath, distDir)) {
8073
8505
  res.writeHead(403, { "Content-Type": "text/plain" });
8074
8506
  res.end("Forbidden");
8075
8507
  return;
8076
8508
  }
8077
- const ext = path11.extname(resolvedPath);
8509
+ const ext = path12.extname(resolvedPath);
8078
8510
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8079
8511
  res.setHeader("Content-Type", contentType);
8080
8512
  setStaticSecurityHeaders(res);
@@ -8098,7 +8530,7 @@ function createHttpServer(opts) {
8098
8530
  } catch (err) {
8099
8531
  if (err.code === "ENOENT") {
8100
8532
  try {
8101
- const html = await fs9.readFile(path11.join(distDir, "index.html"), "utf8");
8533
+ const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8102
8534
  setStaticSecurityHeaders(res);
8103
8535
  res.writeHead(200, {
8104
8536
  "Content-Type": "text/html",
@@ -8128,14 +8560,14 @@ function createHttpServer(opts) {
8128
8560
 
8129
8561
  // src/server/instance-registry.ts
8130
8562
  import * as os from "node:os";
8131
- import * as path12 from "node:path";
8563
+ import * as path13 from "node:path";
8132
8564
  import * as fs10 from "node:fs/promises";
8133
8565
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8134
8566
  function defaultBaseDir() {
8135
- return path12.join(os.homedir(), ".wrongstack");
8567
+ return path13.join(os.homedir(), ".wrongstack");
8136
8568
  }
8137
8569
  function registryPath(baseDir = defaultBaseDir()) {
8138
- return path12.join(baseDir, "webui-instances.json");
8570
+ return path13.join(baseDir, "webui-instances.json");
8139
8571
  }
8140
8572
  function isPidAlive(pid) {
8141
8573
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -11610,16 +12042,16 @@ function getSurfaceDefaultPorts(surface) {
11610
12042
  return { http: SURFACE_DEFAULT_PORTS[surface].http };
11611
12043
  }
11612
12044
  function isPortFree(host, port) {
11613
- return new Promise((resolve15) => {
12045
+ return new Promise((resolve16) => {
11614
12046
  const srv = net.createServer();
11615
- srv.once("error", () => resolve15(false));
12047
+ srv.once("error", () => resolve16(false));
11616
12048
  srv.once("listening", () => {
11617
- srv.close(() => resolve15(true));
12049
+ srv.close(() => resolve16(true));
11618
12050
  });
11619
12051
  try {
11620
12052
  srv.listen(port, host);
11621
12053
  } catch {
11622
- resolve15(false);
12054
+ resolve16(false);
11623
12055
  }
11624
12056
  });
11625
12057
  }
@@ -11644,10 +12076,10 @@ async function findFreePort(host, startPort, opts = {}) {
11644
12076
  import { spawn as spawn2 } from "node:child_process";
11645
12077
  import { existsSync } from "node:fs";
11646
12078
  import { findPackageJSON } from "node:module";
11647
- import * as path13 from "node:path";
12079
+ import * as path14 from "node:path";
11648
12080
  function resolveDistDir(input) {
11649
12081
  const options = typeof input === "string" ? { explicitDistDir: input } : input ?? {};
11650
- if (options.explicitDistDir) return path13.resolve(options.explicitDistDir);
12082
+ if (options.explicitDistDir) return path14.resolve(options.explicitDistDir);
11651
12083
  const exists = options.exists ?? existsSync;
11652
12084
  let packageTarget;
11653
12085
  try {
@@ -11659,15 +12091,15 @@ function resolveDistDir(input) {
11659
12091
  );
11660
12092
  }
11661
12093
  if (!packageTarget) return null;
11662
- const distDir = path13.basename(packageTarget) === "package.json" ? path13.join(path13.dirname(packageTarget), "dist") : path13.dirname(packageTarget);
12094
+ const distDir = path14.basename(packageTarget) === "package.json" ? path14.join(path14.dirname(packageTarget), "dist") : path14.dirname(packageTarget);
11663
12095
  if (options.exists === void 0 && options.resolvePackageJson) return distDir;
11664
- return exists(path13.join(distDir, "index.html")) ? distDir : null;
12096
+ return exists(path14.join(distDir, "index.html")) ? distDir : null;
11665
12097
  }
11666
12098
  async function ensureDistDir(explicitDistDir, deps2 = {}) {
11667
12099
  const exists = deps2.exists ?? existsSync;
11668
12100
  if (explicitDistDir) {
11669
- const resolved2 = path13.resolve(explicitDistDir);
11670
- return exists(path13.join(resolved2, "index.html")) ? resolved2 : null;
12101
+ const resolved2 = path14.resolve(explicitDistDir);
12102
+ return exists(path14.join(resolved2, "index.html")) ? resolved2 : null;
11671
12103
  }
11672
12104
  const resolveOptions = {
11673
12105
  resolvePackageJson: deps2.resolvePackageJson,
@@ -11679,7 +12111,7 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
11679
12111
  try {
11680
12112
  const packageJson = deps2.resolvePackageJson ? deps2.resolvePackageJson("@wrongstack/webui/package.json") : findPackageJSON("@wrongstack/webui", import.meta.url);
11681
12113
  if (!packageJson) throw new Error("not found");
11682
- packageDir = path13.dirname(packageJson);
12114
+ packageDir = path14.dirname(packageJson);
11683
12115
  } catch {
11684
12116
  throw new Error(
11685
12117
  "@wrongstack/webui package could not be resolved. Install workspace dependencies and rebuild the CLI."
@@ -11688,8 +12120,8 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
11688
12120
  const findRoot = deps2.findWorkspaceRoot ?? ((pkgDir) => {
11689
12121
  let dir = pkgDir;
11690
12122
  for (let i = 0; i < 10; i++) {
11691
- if (existsSync(path13.join(dir, "pnpm-workspace.yaml"))) return dir;
11692
- const parent = path13.dirname(dir);
12123
+ if (existsSync(path14.join(dir, "pnpm-workspace.yaml"))) return dir;
12124
+ const parent = path14.dirname(dir);
11693
12125
  if (parent === dir) return null;
11694
12126
  dir = parent;
11695
12127
  }
@@ -11748,7 +12180,7 @@ async function startStaticServe(opts, deps2 = {}) {
11748
12180
  return { server, port: opts.httpPort };
11749
12181
  }
11750
12182
  function runPnpmBuild(cwd, workspace, timeoutMs) {
11751
- return new Promise((resolve15, reject) => {
12183
+ return new Promise((resolve16, reject) => {
11752
12184
  const child = spawn2("pnpm", ["--filter", workspace, "build"], {
11753
12185
  cwd,
11754
12186
  shell: process.platform === "win32",
@@ -11766,14 +12198,14 @@ function runPnpmBuild(cwd, workspace, timeoutMs) {
11766
12198
  });
11767
12199
  child.once("close", (code) => {
11768
12200
  clearTimeout(timer);
11769
- if (code === 0) resolve15();
12201
+ if (code === 0) resolve16();
11770
12202
  else reject(new Error(`pnpm build exited with code ${String(code)}`));
11771
12203
  });
11772
12204
  });
11773
12205
  }
11774
12206
 
11775
12207
  // src/server/embedded-lifecycle.ts
11776
- import * as path14 from "node:path";
12208
+ import * as path15 from "node:path";
11777
12209
 
11778
12210
  // src/server/network-info.ts
11779
12211
  import * as os2 from "node:os";
@@ -11839,7 +12271,7 @@ function registerWebuiInstance(p, deps2 = {}) {
11839
12271
  httpPort: p.httpPort,
11840
12272
  host: p.host,
11841
12273
  projectRoot: p.projectRoot,
11842
- projectName: path14.basename(p.projectRoot) || p.projectRoot,
12274
+ projectName: path15.basename(p.projectRoot) || p.projectRoot,
11843
12275
  startedAt: p.startedAt,
11844
12276
  url: buildWebUIAccessUrl({
11845
12277
  host: p.host,
@@ -12059,7 +12491,7 @@ ${text2}` : text2;
12059
12491
 
12060
12492
  // src/server/client-presence.ts
12061
12493
  import * as crypto2 from "node:crypto";
12062
- import * as path15 from "node:path";
12494
+ import * as path16 from "node:path";
12063
12495
  import {
12064
12496
  getSharedProjectMailbox as getSharedProjectMailbox2,
12065
12497
  resolveProjectDir as resolveProjectDir2
@@ -12077,7 +12509,7 @@ function createWebuiClientPresence(deps2) {
12077
12509
  if (!deps2.projectRoot) return null;
12078
12510
  try {
12079
12511
  const projectRoot = deps2.projectRoot;
12080
- const projectName = path15.basename(projectRoot);
12512
+ const projectName = path16.basename(projectRoot);
12081
12513
  const nextMailbox = getSharedProjectMailbox2(
12082
12514
  resolveProjectDir2(projectRoot, wstackGlobalRoot()),
12083
12515
  deps2.events,
@@ -13035,7 +13467,7 @@ function seedContextMeta(config, context) {
13035
13467
  meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
13036
13468
  meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
13037
13469
  meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
13038
- meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 5e3;
13470
+ meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
13039
13471
  meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
13040
13472
  meta["autoReviewMaxConcurrentReviews"] = typeof autoReviewExt?.["maxConcurrentReviews"] === "number" && autoReviewExt["maxConcurrentReviews"] >= 1 ? autoReviewExt["maxConcurrentReviews"] : 2;
13041
13473
  const cascade = autoReviewExt?.["cascadeOn"];
@@ -13055,7 +13487,7 @@ function seedContextMeta(config, context) {
13055
13487
 
13056
13488
  // src/server/pref-helpers.ts
13057
13489
  import * as fs12 from "node:fs/promises";
13058
- import * as path16 from "node:path";
13490
+ import * as path17 from "node:path";
13059
13491
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
13060
13492
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
13061
13493
  var PREF_KEYS = [
@@ -13151,7 +13583,7 @@ function prefSnapshot(contextMeta) {
13151
13583
  return snapshot;
13152
13584
  }
13153
13585
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
13154
- const globalRoot = path16.dirname(filePath);
13586
+ const globalRoot = path17.dirname(filePath);
13155
13587
  await backupConfigFile(filePath, { globalRoot });
13156
13588
  let raw;
13157
13589
  try {
@@ -13631,7 +14063,7 @@ async function handleProcessRoute(ws, msg, handlers) {
13631
14063
 
13632
14064
  // src/server/embedded-host-adapters.ts
13633
14065
  import * as fs15 from "node:fs/promises";
13634
- import * as path18 from "node:path";
14066
+ import * as path19 from "node:path";
13635
14067
  import { TOKENS } from "@wrongstack/core/kernel";
13636
14068
  import { DefaultSessionStore as DefaultSessionStore2 } from "@wrongstack/core/storage";
13637
14069
  import { toErrorMessage as toErrorMessage7, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
@@ -13639,7 +14071,7 @@ import { makeProviderFromConfig } from "@wrongstack/providers";
13639
14071
 
13640
14072
  // src/server/project-handlers.ts
13641
14073
  import * as fs13 from "node:fs/promises";
13642
- import * as path17 from "node:path";
14074
+ import * as path18 from "node:path";
13643
14075
  import { DefaultSessionStore } from "@wrongstack/core/storage";
13644
14076
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
13645
14077
  function createProjectHandlers(ctx) {
@@ -13691,8 +14123,8 @@ function createProjectHandlers(ctx) {
13691
14123
  });
13692
14124
  return;
13693
14125
  }
13694
- const resolved = path17.resolve(parsed.value.root);
13695
- const name2 = parsed.value.name?.trim() || path17.basename(resolved);
14126
+ const resolved = path18.resolve(parsed.value.root);
14127
+ const name2 = parsed.value.name?.trim() || path18.basename(resolved);
13696
14128
  try {
13697
14129
  const stat3 = await fs13.stat(resolved).catch(() => null);
13698
14130
  if (!stat3?.isDirectory()) {
@@ -13703,7 +14135,7 @@ function createProjectHandlers(ctx) {
13703
14135
  return;
13704
14136
  }
13705
14137
  const before = await loadManifest(ctx.globalConfigPath);
13706
- const already = before.projects.some((project) => path17.resolve(project.root) === resolved);
14138
+ const already = before.projects.some((project) => path18.resolve(project.root) === resolved);
13707
14139
  const entry = await touchProjectInManifest(
13708
14140
  { projectRoot: resolved, workingDir: resolved, name: name2 },
13709
14141
  ctx.globalConfigPath
@@ -13733,8 +14165,8 @@ function createProjectHandlers(ctx) {
13733
14165
  });
13734
14166
  return;
13735
14167
  }
13736
- const resolved = path17.resolve(parsed.value.root);
13737
- const name2 = parsed.value.name?.trim() || path17.basename(resolved);
14168
+ const resolved = path18.resolve(parsed.value.root);
14169
+ const name2 = parsed.value.name?.trim() || path18.basename(resolved);
13738
14170
  if (!ctx.allowProjectMutations) {
13739
14171
  sendTo(ws, {
13740
14172
  type: "projects.selected",
@@ -13769,6 +14201,17 @@ function createProjectHandlers(ctx) {
13769
14201
  });
13770
14202
  const previous = ctx.getSession();
13771
14203
  const previousId = previous.id;
14204
+ const previousProjectRoot = ctx.getProjectRoot();
14205
+ const previousPaths = resolveWstackPaths4({
14206
+ projectRoot: previousProjectRoot,
14207
+ globalRoot: ctx.wpaths.globalRoot
14208
+ });
14209
+ const previousIdentityTarget = {
14210
+ projectSlug: previousPaths.projectSlug,
14211
+ projectRoot: previousProjectRoot,
14212
+ projectName: path18.basename(previousProjectRoot),
14213
+ workingDir: ctx.context.workingDir
14214
+ };
13772
14215
  const previousUsage = ctx.tokenCounter.total();
13773
14216
  const config = ctx.getConfig?.() ?? ctx.config;
13774
14217
  const next = await store.create({
@@ -13794,7 +14237,16 @@ function createProjectHandlers(ctx) {
13794
14237
  };
13795
14238
  try {
13796
14239
  await ctx.onSessionSwapped?.(next.id, identityTarget);
14240
+ await ctx.onBeforeSessionTodosReplaced?.(next.id, paths.projectSessions);
13797
14241
  } catch (err) {
14242
+ try {
14243
+ await ctx.onBeforeSessionTodosReplaced?.(previous.id, previousPaths.projectSessions);
14244
+ } catch {
14245
+ }
14246
+ try {
14247
+ await ctx.onSessionSwapped?.(previous.id, previousIdentityTarget);
14248
+ } catch {
14249
+ }
13798
14250
  await next.close().catch(() => void 0);
13799
14251
  await store.delete(next.id).catch(() => void 0);
13800
14252
  throw err;
@@ -14880,6 +15332,7 @@ var CLIENT_WORKSPACE_MESSAGE_TYPES = [
14880
15332
  var CLIENT_CONFIGURATION_MESSAGE_TYPES = [
14881
15333
  "codebase.index.server.shutdown",
14882
15334
  "connections.health",
15335
+ "connections.service_action",
14883
15336
  "diag.get",
14884
15337
  "key.add",
14885
15338
  "key.delete",
@@ -15153,6 +15606,7 @@ var SERVER_CONFIGURATION_MESSAGE_TYPES = [
15153
15606
  "codebase.index.server.shutdown_result",
15154
15607
  "connections.health_error",
15155
15608
  "connections.health_result",
15609
+ "connections.service_action_result",
15156
15610
  "diag.get",
15157
15611
  "key.operation_result",
15158
15612
  "model.switch_result",
@@ -15197,13 +15651,13 @@ function isRegisteredMessageType(type, direction) {
15197
15651
  // src/protocol/decoder.ts
15198
15652
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
15199
15653
  var MAX_PAYLOAD_DEPTH = 32;
15200
- function inspectValue(value, path33, depth) {
15654
+ function inspectValue(value, path34, depth) {
15201
15655
  if (depth > MAX_PAYLOAD_DEPTH) {
15202
- return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path33 };
15656
+ return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path34 };
15203
15657
  }
15204
15658
  if (value === null || typeof value !== "object") return null;
15205
15659
  for (const key of Object.keys(value)) {
15206
- const childPath = `${path33}.${key}`;
15660
+ const childPath = `${path34}.${key}`;
15207
15661
  if (FORBIDDEN_KEYS.has(key)) {
15208
15662
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
15209
15663
  }
@@ -15344,7 +15798,8 @@ function projectToolMessage(message) {
15344
15798
  name: text(payload["name"]),
15345
15799
  ok: payload["ok"] !== false,
15346
15800
  durationMs: finite(payload["durationMs"]),
15347
- ...typeof payload["output"] === "string" ? { output: payload["output"] } : {}
15801
+ ...typeof payload["output"] === "string" ? { output: payload["output"] } : {},
15802
+ ...Array.isArray(payload["sage"]) ? { sage: payload["sage"].filter((line) => typeof line === "string") } : {}
15348
15803
  };
15349
15804
  }
15350
15805
  return null;
@@ -15584,6 +16039,7 @@ function createSessionHandlers(ctx) {
15584
16039
  ctx.context.session = next;
15585
16040
  ctx.context.state.replaceMessages(messages);
15586
16041
  await ctx.context.flushConversationJournal?.();
16042
+ await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
15587
16043
  ctx.context.state.replaceTodos(todos);
15588
16044
  resetContextAccounting();
15589
16045
  ctx.context.readFiles.clear();
@@ -15980,7 +16436,10 @@ function createSessionHandlers(ctx) {
15980
16436
  rollbackClaim = await ctx.claimSession?.(canonicalId);
15981
16437
  const resumed = await store.resume(canonicalId);
15982
16438
  const restoredTodos = await loadTodosCheckpoint(
15983
- sessionScopedPath(sessionsDirectory(), resumed.writer.id, ".todos.json")
16439
+ sessionScopedPath(sessionsDirectory(), resumed.writer.id, ".todos.json"),
16440
+ ctx.events,
16441
+ ctx.context.traceId,
16442
+ resumed.writer.id
15984
16443
  ).catch(() => null) ?? [];
15985
16444
  activated = true;
15986
16445
  await activateSession(
@@ -16132,7 +16591,7 @@ function createEmbeddedConversationRoutes(ctx) {
16132
16591
  }
16133
16592
  function sessionStoreFor(opts) {
16134
16593
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot;
16135
- return opts.sessionStore ?? new DefaultSessionStore2({ dir: path18.join(projectRoot, ".wrongstack", "sessions"), projectRoot });
16594
+ return opts.sessionStore ?? new DefaultSessionStore2({ dir: path19.join(projectRoot, ".wrongstack", "sessions"), projectRoot });
16136
16595
  }
16137
16596
  function createEmbeddedSessionRoutes(ctx) {
16138
16597
  const { opts } = ctx;
@@ -16142,6 +16601,7 @@ function createEmbeddedSessionRoutes(ctx) {
16142
16601
  config: { model: actx.model ?? "", provider: actx.provider?.id ?? "" },
16143
16602
  getConfig: () => ({ model: actx.model ?? "", provider: actx.provider?.id ?? "" }),
16144
16603
  context: actx,
16604
+ events: opts.events,
16145
16605
  listTools: () => opts.agent.tools.list(),
16146
16606
  getCompactor: () => opts.agent.container.resolve(TOKENS.Compactor),
16147
16607
  getCustomModeStore: ctx.getCustomModeStore,
@@ -16150,11 +16610,12 @@ function createEmbeddedSessionRoutes(ctx) {
16150
16610
  getSession: () => actx.session ?? opts.session,
16151
16611
  getSessionStore: () => sessionStoreFor(opts),
16152
16612
  canSwapSessions: () => opts.sessionStore !== void 0,
16153
- getSessionsDir: () => opts.sessionsDir ?? path18.join(getProjectRoot(), ".wrongstack", "sessions"),
16613
+ getSessionsDir: () => opts.sessionsDir ?? path19.join(getProjectRoot(), ".wrongstack", "sessions"),
16154
16614
  setSession: (next) => {
16155
16615
  actx.session = next;
16156
16616
  },
16157
16617
  claimSession: opts.claimSession,
16618
+ onBeforeSessionTodosReplaced: async (sessionId, sessionsDir) => opts.onBeforeSessionTodosReplaced?.(sessionId, sessionsDir),
16158
16619
  onSessionSwapped: async (sessionId, target) => opts.onSessionSwapped?.(sessionId, target),
16159
16620
  abortActiveRun: ctx.abortActiveRun,
16160
16621
  isRunActive: ctx.isRunActive,
@@ -16166,7 +16627,7 @@ function createEmbeddedSessionRoutes(ctx) {
16166
16627
  async function broadcastEmbeddedGoalSnapshot(ctx) {
16167
16628
  const projectRoot = ctx.opts.projectRoot ?? ctx.opts.agent.ctx.projectRoot;
16168
16629
  try {
16169
- const raw = await fs15.readFile(path18.join(projectRoot, ".wrongstack", "goal.json"), "utf8");
16630
+ const raw = await fs15.readFile(path19.join(projectRoot, ".wrongstack", "goal.json"), "utf8");
16170
16631
  ctx.broadcast({ type: "goal-state.updated", payload: JSON.parse(raw) });
16171
16632
  } catch {
16172
16633
  ctx.broadcast({ type: "goal-state.updated", payload: null });
@@ -16175,10 +16636,10 @@ async function broadcastEmbeddedGoalSnapshot(ctx) {
16175
16636
  function createEmbeddedProjectRoutes(ctx) {
16176
16637
  const { opts } = ctx;
16177
16638
  const actx = opts.agent.ctx;
16178
- const globalConfigPath = opts.globalConfigPath ?? path18.join(wstackGlobalRoot2(), "config.json");
16639
+ const globalConfigPath = opts.globalConfigPath ?? path19.join(wstackGlobalRoot2(), "config.json");
16179
16640
  return createProjectHandlers({
16180
16641
  globalConfigPath,
16181
- wpaths: { globalRoot: path18.dirname(globalConfigPath) },
16642
+ wpaths: { globalRoot: path19.dirname(globalConfigPath) },
16182
16643
  context: actx,
16183
16644
  tokenCounter: actx.tokenCounter,
16184
16645
  config: { model: actx.model, provider: actx.provider.id },
@@ -16203,6 +16664,7 @@ function createEmbeddedProjectRoutes(ctx) {
16203
16664
  for (const controller of ctx.abortControllers.values()) controller.abort();
16204
16665
  ctx.abortControllers.clear();
16205
16666
  },
16667
+ onBeforeSessionTodosReplaced: async (sessionId, sessionsDir) => opts.onBeforeSessionTodosReplaced?.(sessionId, sessionsDir),
16206
16668
  onSessionSwapped: async (sessionId, target) => opts.onSessionSwapped?.(sessionId, target),
16207
16669
  allowProjectMutations: true,
16208
16670
  sessionStartPayload: ctx.buildSessionStart,
@@ -16658,7 +17120,7 @@ ${String(p.content ?? "")}`;
16658
17120
  };
16659
17121
 
16660
17122
  // src/server/codebase-index-server-control.ts
16661
- import { shutdownCodebaseIndexServer } from "@wrongstack/tools";
17123
+ import { shutdownCodebaseIndexServer as shutdownCodebaseIndexServer2 } from "@wrongstack/tools";
16662
17124
  async function handleCodebaseIndexServerControl(ws, message, deps2) {
16663
17125
  if (message.type !== "codebase.index.server.shutdown") return false;
16664
17126
  const requestId = message.payload && typeof message.payload === "object" && typeof message.payload.requestId === "string" ? message.payload.requestId : "";
@@ -16685,7 +17147,7 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
16685
17147
  });
16686
17148
  return true;
16687
17149
  }
16688
- const result = await shutdownCodebaseIndexServer(
17150
+ const result = await shutdownCodebaseIndexServer2(
16689
17151
  projectRoot,
16690
17152
  deps2.getIndexDir(),
16691
17153
  "websocket-request"
@@ -17226,7 +17688,7 @@ function createRouteFamilyDispatcher(options) {
17226
17688
 
17227
17689
  // src/server/shell-open.ts
17228
17690
  import * as fs16 from "node:fs/promises";
17229
- import * as path19 from "node:path";
17691
+ import * as path20 from "node:path";
17230
17692
  import { spawn as spawn3 } from "node:child_process";
17231
17693
  function normalizeShellOpenTarget(target) {
17232
17694
  return target === "terminal" ? "terminal" : "file-manager";
@@ -17237,11 +17699,11 @@ function shellQuote(s) {
17237
17699
  }
17238
17700
  async function handleShellOpen(req, logger, options) {
17239
17701
  try {
17240
- const resolved = path19.resolve(req.path);
17702
+ const resolved = path20.resolve(req.path);
17241
17703
  if (options?.projectRoot) {
17242
- const root = path19.resolve(options.projectRoot);
17243
- const relative5 = path19.relative(root, resolved);
17244
- const escapes = relative5.startsWith("..") || path19.isAbsolute(relative5);
17704
+ const root = path20.resolve(options.projectRoot);
17705
+ const relative5 = path20.relative(root, resolved);
17706
+ const escapes = relative5.startsWith("..") || path20.isAbsolute(relative5);
17245
17707
  if (escapes) {
17246
17708
  return {
17247
17709
  success: false,
@@ -17632,6 +18094,13 @@ function createEmbeddedMessageRouter(deps2) {
17632
18094
  message
17633
18095
  ))
17634
18096
  return;
18097
+ if (await handleConnectionsServiceAction(ws, message, {
18098
+ getProjectRoot: projectRoot,
18099
+ getIndexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
18100
+ send: send2,
18101
+ backend: "cli-embedded"
18102
+ }))
18103
+ return;
17635
18104
  if (await handleCodebaseIndexServerControl(ws, message, {
17636
18105
  trustBoundary: deps2.trustBoundary,
17637
18106
  logger: deps2.logger,
@@ -17646,7 +18115,7 @@ function createEmbeddedMessageRouter(deps2) {
17646
18115
  }
17647
18116
 
17648
18117
  // src/server/provider-config-standalone.ts
17649
- import * as path20 from "node:path";
18118
+ import * as path21 from "node:path";
17650
18119
  import { DefaultSecretVault } from "@wrongstack/core/security";
17651
18120
  function createProviderConfigIO(configPath) {
17652
18121
  const keyFile = vaultKeyFileForConfigPath(configPath);
@@ -17657,10 +18126,10 @@ function createProviderConfigIO(configPath) {
17657
18126
  };
17658
18127
  }
17659
18128
  function vaultKeyFileForConfigPath(configPath) {
17660
- const configDir = path20.dirname(configPath);
17661
- const parentDir = path20.dirname(configDir);
17662
- const globalRoot = path20.basename(parentDir) === "profiles" ? path20.dirname(parentDir) : configDir;
17663
- return path20.join(globalRoot, ".key");
18129
+ const configDir = path21.dirname(configPath);
18130
+ const parentDir = path21.dirname(configDir);
18131
+ const globalRoot = path21.basename(parentDir) === "profiles" ? path21.dirname(parentDir) : configDir;
18132
+ return path21.join(globalRoot, ".key");
17664
18133
  }
17665
18134
 
17666
18135
  // src/server/provider-store.ts
@@ -17677,8 +18146,8 @@ function createConfigWriteLock() {
17677
18146
  acquire() {
17678
18147
  const prev = lock;
17679
18148
  let release = () => void 0;
17680
- lock = new Promise((resolve15) => {
17681
- release = resolve15;
18149
+ lock = new Promise((resolve16) => {
18150
+ release = resolve16;
17682
18151
  });
17683
18152
  return { prev, release };
17684
18153
  }
@@ -17756,6 +18225,7 @@ function createProviderStore(deps2) {
17756
18225
  import { listBoards as listBoards5 } from "@wrongstack/kanban";
17757
18226
  import {
17758
18227
  applySddLifecycle,
18228
+ extractVerificationCommand,
17759
18229
  SddBoardStore
17760
18230
  } from "@wrongstack/sdd";
17761
18231
  var CONTROL_TYPES = /* @__PURE__ */ new Set([
@@ -17778,14 +18248,16 @@ var SddBoardWebSocketHandler = class {
17778
18248
  store;
17779
18249
  clients = /* @__PURE__ */ new Set();
17780
18250
  lifecycle;
18251
+ security;
17781
18252
  diskPollingEnabled;
17782
18253
  latest = null;
17783
18254
  poll = null;
17784
18255
  pollInFlight = false;
17785
18256
  unsub = null;
17786
- constructor(boardsDir, events, lifecycle) {
18257
+ constructor(boardsDir, events, lifecycle, security) {
17787
18258
  this.store = new SddBoardStore({ baseDir: boardsDir });
17788
18259
  this.lifecycle = lifecycle;
18260
+ this.security = security;
17789
18261
  this.diskPollingEnabled = events === void 0;
17790
18262
  if (events) {
17791
18263
  const handler = (e) => {
@@ -17831,6 +18303,43 @@ var SddBoardWebSocketHandler = class {
17831
18303
  return;
17832
18304
  }
17833
18305
  if (CONTROL_TYPES.has(action)) {
18306
+ const verificationCommands = [];
18307
+ if (action === "set_task_verification") {
18308
+ const command = msg.payload?.verificationCommand;
18309
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
18310
+ if (typeof command === "string" && command.trim()) {
18311
+ verificationCommands.push({ command, operation: "sdd.set_task_verification" });
18312
+ }
18313
+ } else if (action === "split_task") {
18314
+ const subtasks = msg.payload?.subtasks;
18315
+ if (Array.isArray(subtasks)) {
18316
+ for (const subtask of subtasks) {
18317
+ if (!subtask || typeof subtask !== "object") continue;
18318
+ const criterion = subtask.successCriterion;
18319
+ if (criterion === void 0) continue;
18320
+ if (typeof criterion !== "string") return;
18321
+ const command = extractVerificationCommand([criterion]);
18322
+ if (!command) continue;
18323
+ if (command.length > 8192) return;
18324
+ verificationCommands.push({ command, operation: "sdd.split_task_verification" });
18325
+ }
18326
+ }
18327
+ }
18328
+ for (const { command, operation } of verificationCommands) {
18329
+ if (!this.security) return;
18330
+ const authorization = await authorizeWebUIAction(
18331
+ this.security.trustBoundary,
18332
+ {
18333
+ capability: "process.spawn",
18334
+ subject: { kind: "command", id: command },
18335
+ risk: "high",
18336
+ cwd: this.lifecycle?.projectRoot,
18337
+ metadata: { operation }
18338
+ },
18339
+ this.security.logger
18340
+ );
18341
+ if (!authorization.allowed) return;
18342
+ }
17834
18343
  const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
17835
18344
  if (runId) {
17836
18345
  await this.store.appendControl(runId, {
@@ -17943,7 +18452,7 @@ var SddBoardWebSocketHandler = class {
17943
18452
  };
17944
18453
 
17945
18454
  // src/server/sdd-wizard-wiring.ts
17946
- import * as path21 from "node:path";
18455
+ import * as path22 from "node:path";
17947
18456
  import {
17948
18457
  DefaultTaskStore,
17949
18458
  TaskTracker
@@ -18057,7 +18566,7 @@ function buildSddWizardDeps(opts) {
18057
18566
  }).catch(() => {
18058
18567
  projectContext = "";
18059
18568
  });
18060
- const sessionPath = opts.paths.projectSddSession ?? path21.join(opts.paths.projectDir, "sdd-session.json");
18569
+ const sessionPath = opts.paths.projectSddSession ?? path22.join(opts.paths.projectDir, "sdd-session.json");
18061
18570
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
18062
18571
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
18063
18572
  const runIsolatedTurn = async (prompt, name2) => {
@@ -18339,7 +18848,7 @@ var SddWizardWebSocketHandler = class {
18339
18848
  return;
18340
18849
  }
18341
18850
  const { runId } = await this.deps.startRun(this.driver, opts);
18342
- this.driver.setLastRunId(runId);
18851
+ await this.driver.setLastRunId(runId);
18343
18852
  if (this.driver.phase() !== "executing" && this.driver.phase() !== "done") {
18344
18853
  try {
18345
18854
  if (this.driver.phase() === "task_review") await this.driver.approve();
@@ -18398,7 +18907,7 @@ var SddWizardWebSocketHandler = class {
18398
18907
  this.lastAgentText = text2;
18399
18908
  if (this.driver) {
18400
18909
  await this.driver.ingestAgentOutput(text2);
18401
- this.driver.setLastAgentText(text2);
18910
+ await this.driver.setLastAgentText(text2);
18402
18911
  }
18403
18912
  this.broadcast({ type: "sdd.spec.agent_text", payload: { text: text2 } });
18404
18913
  } finally {
@@ -18429,10 +18938,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
18429
18938
 
18430
18939
  // src/server/setup-events-fleet-broadcaster.ts
18431
18940
  import { watch as fsWatch } from "node:fs";
18432
- import * as path22 from "node:path";
18941
+ import * as path23 from "node:path";
18433
18942
  function registerSetupEventsFleetBroadcaster(deps2) {
18434
18943
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
18435
- const globalRoot = globalConfigPath ? path22.dirname(globalConfigPath) : void 0;
18944
+ const globalRoot = globalConfigPath ? path23.dirname(globalConfigPath) : void 0;
18436
18945
  if (!globalRoot) return void 0;
18437
18946
  const disposers = [];
18438
18947
  const broadcastSessions = async () => {
@@ -18442,8 +18951,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
18442
18951
  const sessions = await registry.list();
18443
18952
  const ownEntry = sessions.find((s) => s.pid === process.pid);
18444
18953
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
18445
- const myRoot = path22.resolve(context.projectRoot);
18446
- const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path22.resolve(s.projectRoot) === myRoot).map((s) => ({
18954
+ const myRoot = path23.resolve(context.projectRoot);
18955
+ const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path23.resolve(s.projectRoot) === myRoot).map((s) => ({
18447
18956
  sessionId: s.sessionId,
18448
18957
  projectName: s.projectName,
18449
18958
  projectSlug: s.projectSlug,
@@ -18634,13 +19143,13 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
18634
19143
  // src/server/setup-events-status-watcher.ts
18635
19144
  import { watch as fsWatch2 } from "node:fs";
18636
19145
  import * as fs18 from "node:fs/promises";
18637
- import * as path24 from "node:path";
19146
+ import * as path25 from "node:path";
18638
19147
 
18639
19148
  // src/server/setup-events-watcher.ts
18640
- import * as path23 from "node:path";
19149
+ import * as path24 from "node:path";
18641
19150
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
18642
19151
  const raw = String(filename);
18643
- const relative5 = path23.isAbsolute(raw) ? path23.relative(projectsDir, raw) : raw;
19152
+ const relative5 = path24.isAbsolute(raw) ? path24.relative(projectsDir, raw) : raw;
18644
19153
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
18645
19154
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
18646
19155
  return parts.at(-2) ?? null;
@@ -18675,7 +19184,7 @@ function logFileWatcherMetrics(metrics) {
18675
19184
  function registerSetupEventsStatusWatcher(deps2) {
18676
19185
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
18677
19186
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
18678
- const projectsDir = path24.join(wpaths.globalRoot, "projects");
19187
+ const projectsDir = path25.join(wpaths.globalRoot, "projects");
18679
19188
  const knownProjectHashes = /* @__PURE__ */ new Set();
18680
19189
  const debounceTimers = /* @__PURE__ */ new Map();
18681
19190
  const DEBOUNCE_MS2 = 150;
@@ -18734,7 +19243,7 @@ function registerSetupEventsStatusWatcher(deps2) {
18734
19243
  if (!knownProjectHashes.has(projectHash)) return;
18735
19244
  if (watcherMetrics) watcherMetrics.filesProcessed++;
18736
19245
  try {
18737
- const targetFile = path24.join(projectsDir, projectHash, "status.json");
19246
+ const targetFile = path25.join(projectsDir, projectHash, "status.json");
18738
19247
  const content = await fs18.readFile(targetFile, "utf-8");
18739
19248
  const statusData = JSON.parse(content);
18740
19249
  scheduleBroadcast(projectHash, statusData);
@@ -18793,7 +19302,7 @@ function registerSetupEventsStatusWatcher(deps2) {
18793
19302
 
18794
19303
  // src/server/setup-events-core-watchers.ts
18795
19304
  import * as fs19 from "node:fs/promises";
18796
- import * as path25 from "node:path";
19305
+ import * as path26 from "node:path";
18797
19306
  function registerSetupEventsCoreWatchers(deps2) {
18798
19307
  const { broadcast: broadcast2, clients, context } = deps2;
18799
19308
  const disposers = [];
@@ -18829,7 +19338,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
18829
19338
  if (wpaths?.projectStatus) {
18830
19339
  try {
18831
19340
  const statusFile = wpaths.projectStatus(e.projectHash);
18832
- const dir = path25.dirname(statusFile);
19341
+ const dir = path26.dirname(statusFile);
18833
19342
  await fs19.mkdir(dir, { recursive: true });
18834
19343
  await fs19.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
18835
19344
  } catch (err) {
@@ -19019,6 +19528,9 @@ function setupEvents(deps2) {
19019
19528
  input: scrub(e.input),
19020
19529
  fileTargets: extractCodeMapFileTargets(projectRoot || ".", e.name, e.input),
19021
19530
  output: scrub(e.output),
19531
+ // SAGE-injected memory rides beside the tool text so the client renders
19532
+ // it as a memory card. Never folded back into `output`.
19533
+ ...e.sage && e.sage.length > 0 ? { sage: e.sage.map((line) => scrub(line)) } : {},
19022
19534
  outputBytes: e.outputBytes,
19023
19535
  outputTokens: e.outputTokens,
19024
19536
  outputLines: e.outputLines,
@@ -19853,17 +20365,24 @@ var SpecsWebSocketHandler = class {
19853
20365
  // src/server/start-webui.ts
19854
20366
  import { randomUUID as randomUUID6 } from "node:crypto";
19855
20367
  import * as http2 from "node:http";
19856
- import * as path32 from "node:path";
20368
+ import * as path33 from "node:path";
19857
20369
  import { createDefaultPipelines } from "@wrongstack/core/agent";
19858
20370
  import { getSharedProjectMailbox as getSharedProjectMailbox5, resolveProjectDir as resolveProjectDir4 } from "@wrongstack/core/coordination";
19859
20371
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
19860
20372
  import {
20373
+ attachTodosCheckpoint,
19861
20374
  createSessionEventBridge,
19862
20375
  resolveSessionLoggingConfig,
19863
20376
  watchProviderConfig
19864
20377
  } from "@wrongstack/core/storage";
19865
20378
  import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } from "@wrongstack/core/types";
19866
- import { expectDefined as expectDefined4, startHeapWatchdog, toErrorMessage as toErrorMessage14, wstackGlobalRoot as wstackGlobalRoot4 } from "@wrongstack/core/utils";
20379
+ import {
20380
+ expectDefined as expectDefined4,
20381
+ sessionScopedPath as sessionScopedPath3,
20382
+ startHeapWatchdog,
20383
+ toErrorMessage as toErrorMessage14,
20384
+ wstackGlobalRoot as wstackGlobalRoot4
20385
+ } from "@wrongstack/core/utils";
19867
20386
  import { makeProviderFromConfig as makeProviderFromConfig5 } from "@wrongstack/providers";
19868
20387
  import { toLanguagePackageInput } from "@wrongstack/techstack";
19869
20388
  import { ensureSessionShell } from "@wrongstack/tools";
@@ -20050,7 +20569,7 @@ function findWorkspaceCliEntry(projectRoot) {
20050
20569
  return null;
20051
20570
  }
20052
20571
  function sleep(ms) {
20053
- return new Promise((resolve15) => setTimeout(resolve15, ms));
20572
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
20054
20573
  }
20055
20574
 
20056
20575
  // src/server/terminal-ws-handler.ts
@@ -20286,7 +20805,7 @@ function clampDim(value, fallback) {
20286
20805
  }
20287
20806
 
20288
20807
  // src/server/worktree-ws-handler.ts
20289
- import { join as join14, resolve as resolve12, sep as sep4 } from "node:path";
20808
+ import { join as join14, resolve as resolve13, sep as sep5 } from "node:path";
20290
20809
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
20291
20810
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
20292
20811
  import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
@@ -20347,13 +20866,13 @@ var WorktreeWebSocketHandler = class {
20347
20866
  // ── orphan management ─────────────────────────────────────────────────────
20348
20867
  /** Absolute managed-worktrees root for this project. */
20349
20868
  worktreesRoot() {
20350
- return resolve12(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
20869
+ return resolve13(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
20351
20870
  }
20352
20871
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
20353
20872
  underRoot(dir) {
20354
- const abs = resolve12(dir);
20873
+ const abs = resolve13(dir);
20355
20874
  const root = this.worktreesRoot();
20356
- return abs !== root && abs.startsWith(root + sep4);
20875
+ return abs !== root && abs.startsWith(root + sep5);
20357
20876
  }
20358
20877
  /** Branches of worktrees a live in-session run currently owns. */
20359
20878
  liveActiveBranches() {
@@ -20501,7 +21020,7 @@ var WorktreeWebSocketHandler = class {
20501
21020
  }
20502
21021
  const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
20503
21022
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
20504
- const summary = await wt.diffSummary(resolve12(dir), base);
21023
+ const summary = await wt.diffSummary(resolve13(dir), base);
20505
21024
  this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
20506
21025
  }
20507
21026
  // ── internals ───────────────────────────────────────────────────────────
@@ -20650,7 +21169,9 @@ async function createAgentServices(input) {
20650
21169
  memory: memoryRetrieval,
20651
21170
  maxHintsPerTool: config.Sage?.inject?.maxHintsPerTool,
20652
21171
  maxCharsPerTool: config.Sage?.inject?.maxCharsPerTool,
21172
+ taskAware: config.Sage?.inject?.taskAware,
20653
21173
  minScore: config.Sage?.inject?.minScore,
21174
+ minImportance: config.Sage?.inject?.minImportance,
20654
21175
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
20655
21176
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
20656
21177
  triggers: config.Sage?.inject?.triggers
@@ -20953,15 +21474,20 @@ async function createAgentServices(input) {
20953
21474
  projectRoot
20954
21475
  );
20955
21476
  const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
20956
- const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards, void 0, {
20957
- projectRoot,
20958
- paths: {
20959
- projectSpecs: wpaths.projectSpecs,
20960
- projectTaskGraphs: wpaths.projectTaskGraphs,
20961
- projectSddSession: wpaths.projectSddSession,
20962
- projectSddBoards: wpaths.projectSddBoards
20963
- }
20964
- });
21477
+ const sddBoardHandler = new SddBoardWebSocketHandler(
21478
+ wpaths.projectSddBoards,
21479
+ void 0,
21480
+ {
21481
+ projectRoot,
21482
+ paths: {
21483
+ projectSpecs: wpaths.projectSpecs,
21484
+ projectTaskGraphs: wpaths.projectTaskGraphs,
21485
+ projectSddSession: wpaths.projectSddSession,
21486
+ projectSddBoards: wpaths.projectSddBoards
21487
+ }
21488
+ },
21489
+ { trustBoundary: input.trustBoundary, logger }
21490
+ );
20965
21491
  const sddWizardHandler = new SddWizardWebSocketHandler(
20966
21492
  buildSddWizardDeps({
20967
21493
  agent,
@@ -20973,7 +21499,16 @@ async function createAgentServices(input) {
20973
21499
  providerRegistry,
20974
21500
  toolRegistry,
20975
21501
  session: input.sessionGetter(),
20976
- projectRoot
21502
+ projectRoot,
21503
+ // Thread the container-provided ProviderModelStatusTracker so a 429
21504
+ // from this subagent's first call transitions the (provider, model)
21505
+ // pair to `state: 'blocked'` instead of silently no-op'ing. The
21506
+ // runtime container binds a default `ProviderModelStatusTracker`
21507
+ // (see packages/runtime/src/container.ts); without this dep, the
21508
+ // subagent's fallback extension's tracker hooks are undefined and
21509
+ // round-robin keeps reassigning the doomed model. Mirrors the CLI
21510
+ // factory wiring at host-subagent-factory.ts:337.
21511
+ statusTracker: container.safeResolve(TOKENS2.ProviderModelStatusTracker)
20977
21512
  }),
20978
21513
  paths: {
20979
21514
  projectSpecs: wpaths.projectSpecs,
@@ -21116,7 +21651,7 @@ function createConnectionHandler(options) {
21116
21651
  }
21117
21652
 
21118
21653
  // src/server/message-dispatcher.ts
21119
- import path26 from "node:path";
21654
+ import path27 from "node:path";
21120
21655
  function createMessageDispatcher(opts) {
21121
21656
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
21122
21657
  function makeWorklistContext() {
@@ -21137,7 +21672,7 @@ function createMessageDispatcher(opts) {
21137
21672
  skillLoader: deps2.skillLoader,
21138
21673
  skillInstaller: deps2.skillInstaller,
21139
21674
  projectRoot,
21140
- projectSkillsDir: path26.join(projectRoot, ".wrongstack", "skills"),
21675
+ projectSkillsDir: path27.join(projectRoot, ".wrongstack", "skills"),
21141
21676
  globalSkillsDir: deps2.wpaths.globalSkills
21142
21677
  };
21143
21678
  }
@@ -21389,7 +21924,7 @@ function createMessageDispatcher(opts) {
21389
21924
 
21390
21925
  // src/server/pre-context-services.ts
21391
21926
  import { createRequire as createRequire3 } from "node:module";
21392
- import * as path29 from "node:path";
21927
+ import * as path30 from "node:path";
21393
21928
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
21394
21929
  import {
21395
21930
  getSharedProjectMailbox as getSharedProjectMailbox4,
@@ -21444,7 +21979,7 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
21444
21979
 
21445
21980
  // src/server/model-auto-discovery.ts
21446
21981
  import * as fs20 from "node:fs/promises";
21447
- import * as path27 from "node:path";
21982
+ import * as path28 from "node:path";
21448
21983
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
21449
21984
  function isOverlayRegistry(value) {
21450
21985
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -21480,7 +22015,7 @@ async function discoverAndMergeWebuiProviders(opts) {
21480
22015
  if (!isOverlayRegistry(registry)) return;
21481
22016
  const targets = eligibleProviders(opts.config);
21482
22017
  if (targets.length === 0) return;
21483
- const cacheFile = path27.join(opts.cacheDir, "discovered-models-cache.json");
22018
+ const cacheFile = path28.join(opts.cacheDir, "discovered-models-cache.json");
21484
22019
  const cache2 = await readCache(cacheFile);
21485
22020
  let cacheDirty = false;
21486
22021
  await Promise.all(
@@ -21517,7 +22052,7 @@ async function discoverAndMergeWebuiProviders(opts) {
21517
22052
  );
21518
22053
  if (cacheDirty) {
21519
22054
  try {
21520
- await fs20.mkdir(path27.dirname(cacheFile), { recursive: true });
22055
+ await fs20.mkdir(path28.dirname(cacheFile), { recursive: true });
21521
22056
  await fs20.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
21522
22057
  } catch {
21523
22058
  opts.logger?.debug?.("provider auto-discovery cache write failed");
@@ -21614,7 +22149,7 @@ function resolveSetupProvider(opts) {
21614
22149
  }
21615
22150
 
21616
22151
  // src/server/standalone-session-identity.ts
21617
- import * as path28 from "node:path";
22152
+ import * as path29 from "node:path";
21618
22153
  import {
21619
22154
  AgentStatusTracker,
21620
22155
  FleetNotifier,
@@ -21633,7 +22168,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
21633
22168
  let activeTarget = {
21634
22169
  projectSlug: paths.projectSlug,
21635
22170
  projectRoot: paths.projectRoot,
21636
- projectName: path28.basename(paths.projectRoot),
22171
+ projectName: path29.basename(paths.projectRoot),
21637
22172
  workingDir: opts.workingDir
21638
22173
  };
21639
22174
  let pendingClaim;
@@ -21864,7 +22399,7 @@ async function createPreContextServices(input) {
21864
22399
  await discoverAndMergeWebuiProviders({
21865
22400
  config,
21866
22401
  registry: modelsRegistry,
21867
- cacheDir: path29.dirname(wpaths.modelsCache),
22402
+ cacheDir: path30.dirname(wpaths.modelsCache),
21868
22403
  logger
21869
22404
  });
21870
22405
  } catch (err) {
@@ -21916,7 +22451,7 @@ async function createPreContextServices(input) {
21916
22451
  configureChildEnvGitIdentity(config.git?.identity ?? null);
21917
22452
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
21918
22453
  const mcpTokenStore = new MCPVaultTokenStore(
21919
- path29.join(wpaths.projectDir, "mcp-auth.json"),
22454
+ path30.join(wpaths.projectDir, "mcp-auth.json"),
21920
22455
  vault
21921
22456
  );
21922
22457
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -22022,7 +22557,7 @@ async function createPreContextServices(input) {
22022
22557
  };
22023
22558
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
22024
22559
  const skillInstaller = config.features.skills ? new SkillInstaller({
22025
- manifestPath: path29.join(wpaths.configDir, "installed-skills.json"),
22560
+ manifestPath: path30.join(wpaths.configDir, "installed-skills.json"),
22026
22561
  projectSkillsDir: wpaths.inProjectSkills,
22027
22562
  globalSkillsDir: wpaths.globalSkills,
22028
22563
  projectHash: wpaths.projectHash,
@@ -22032,8 +22567,8 @@ async function createPreContextServices(input) {
22032
22567
  const bundledPromptsDir = promptsEnabled ? (() => {
22033
22568
  try {
22034
22569
  const req = createRequire3(import.meta.url);
22035
- return path29.join(
22036
- path29.dirname(req.resolve("@wrongstack/core/package.json")),
22570
+ return path30.join(
22571
+ path30.dirname(req.resolve("@wrongstack/core/package.json")),
22037
22572
  "data",
22038
22573
  "prompts"
22039
22574
  );
@@ -22137,7 +22672,7 @@ async function createPreContextServices(input) {
22137
22672
  }
22138
22673
 
22139
22674
  // src/server/routes.ts
22140
- import path30 from "node:path";
22675
+ import path31 from "node:path";
22141
22676
  import { makeProviderFromConfig as makeProviderFromConfig4, withCatalogCapabilities } from "@wrongstack/providers";
22142
22677
 
22143
22678
  // src/server/mode-handlers.ts
@@ -22262,6 +22797,7 @@ function buildRoutes(state, deps2, cb) {
22262
22797
  config: state.getConfig(),
22263
22798
  clients: state.getClients(),
22264
22799
  context: deps2.context,
22800
+ events: deps2.events,
22265
22801
  toolRegistry: deps2.toolRegistry,
22266
22802
  compactor: deps2.compactor,
22267
22803
  customModeStore: deps2.customModeStore,
@@ -22273,6 +22809,7 @@ function buildRoutes(state, deps2, cb) {
22273
22809
  setSession: state.setSession,
22274
22810
  setSessionStartedAt: state.setSessionStartedAt,
22275
22811
  claimSession: cb.claimSession,
22812
+ onBeforeSessionTodosReplaced: cb.onBeforeSessionTodosReplaced,
22276
22813
  onSessionSwapped: cb.onSessionSwapped,
22277
22814
  abortActiveRun: state.abortRunLock,
22278
22815
  isRunActive: state.isRunActive,
@@ -22294,6 +22831,8 @@ function buildRoutes(state, deps2, cb) {
22294
22831
  setSessionStore: state.setSessionStore,
22295
22832
  setSessionStartedAt: state.setSessionStartedAt,
22296
22833
  abortRunLock: state.abortRunLock,
22834
+ onBeforeSessionTodosReplaced: cb.onBeforeSessionTodosReplaced,
22835
+ onSessionSwapped: cb.onSessionSwapped,
22297
22836
  sessionStartPayload: cb.sessionStartPayload
22298
22837
  });
22299
22838
  const modeRoutes = createModeHandlers({
@@ -22425,7 +22964,7 @@ function buildRoutes(state, deps2, cb) {
22425
22964
  };
22426
22965
  const mailboxRoutes = createMailboxRouteHandlers({
22427
22966
  getProjectRoot: state.getProjectRoot,
22428
- getGlobalRoot: () => path30.dirname(deps2.globalConfigPath),
22967
+ getGlobalRoot: () => path31.dirname(deps2.globalConfigPath),
22429
22968
  events: deps2.events
22430
22969
  });
22431
22970
  const mcpRoutes = {
@@ -22498,7 +23037,7 @@ function buildRoutes(state, deps2, cb) {
22498
23037
  }
22499
23038
 
22500
23039
  // src/server/server-runtime.ts
22501
- import * as path31 from "node:path";
23040
+ import * as path32 from "node:path";
22502
23041
  import { createRequire as createRequire4 } from "node:module";
22503
23042
  import { fileURLToPath } from "node:url";
22504
23043
  import { WebSocketServer } from "ws";
@@ -22559,7 +23098,7 @@ function createSessionStartPayload(g) {
22559
23098
  inputCost,
22560
23099
  outputCost,
22561
23100
  cacheReadCost,
22562
- projectName: path31.basename(projectRoot) || projectRoot,
23101
+ projectName: path32.basename(projectRoot) || projectRoot,
22563
23102
  projectRoot,
22564
23103
  cwd: g.getWorkingDir(),
22565
23104
  mode: g.getModeId(),
@@ -22647,13 +23186,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
22647
23186
  };
22648
23187
  }
22649
23188
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
22650
- if (explicitDistDir) return path31.resolve(explicitDistDir);
23189
+ if (explicitDistDir) return path32.resolve(explicitDistDir);
22651
23190
  try {
22652
23191
  const requireFromHere2 = createRequire4(fromUrl);
22653
23192
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
22654
- return path31.dirname(serverEntry);
23193
+ return path32.dirname(serverEntry);
22655
23194
  } catch {
22656
- return path31.resolve(path31.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
23195
+ return path32.resolve(path32.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
22657
23196
  }
22658
23197
  }
22659
23198
  function startHttpServer(opts) {
@@ -22684,6 +23223,56 @@ function registerShutdown(deps2) {
22684
23223
  }
22685
23224
 
22686
23225
  // src/server/start-webui.ts
23226
+ function createStandaloneTodosCheckpointLifecycle(input) {
23227
+ let checkpointSessionId = input.sessionId;
23228
+ let checkpointSessionsDir = input.sessionsDir;
23229
+ const attachCheckpoint = (sessionId, sessionsDir) => attachTodosCheckpoint(
23230
+ input.state,
23231
+ sessionScopedPath3(sessionsDir, sessionId, ".todos.json"),
23232
+ sessionId,
23233
+ input.events,
23234
+ input.traceId,
23235
+ input.warn
23236
+ );
23237
+ let detachCurrent = attachCheckpoint(input.sessionId, input.sessionsDir);
23238
+ let checkpointAttached = true;
23239
+ const detachCurrentCheckpoint = async () => {
23240
+ if (!checkpointAttached) return;
23241
+ checkpointAttached = false;
23242
+ await detachCurrent();
23243
+ };
23244
+ let transitionTail = Promise.resolve();
23245
+ const rebind = (nextSessionId, sessionsDir) => {
23246
+ const transition = transitionTail.then(async () => {
23247
+ if (checkpointAttached && nextSessionId === checkpointSessionId && sessionsDir === checkpointSessionsDir) {
23248
+ return;
23249
+ }
23250
+ let detachFailed = false;
23251
+ let detachError;
23252
+ try {
23253
+ await detachCurrentCheckpoint();
23254
+ } catch (error2) {
23255
+ detachFailed = true;
23256
+ detachError = error2;
23257
+ }
23258
+ const nextDetach = attachCheckpoint(nextSessionId, sessionsDir);
23259
+ checkpointSessionId = nextSessionId;
23260
+ checkpointSessionsDir = sessionsDir;
23261
+ detachCurrent = nextDetach;
23262
+ checkpointAttached = true;
23263
+ if (detachFailed) throw detachError;
23264
+ });
23265
+ transitionTail = transition.catch(() => void 0);
23266
+ return transition;
23267
+ };
23268
+ return {
23269
+ rebind,
23270
+ detach: async () => {
23271
+ await transitionTail;
23272
+ await detachCurrentCheckpoint();
23273
+ }
23274
+ };
23275
+ }
22687
23276
  async function startWebUI(opts = {}) {
22688
23277
  ensureSessionShell();
22689
23278
  const ports = await resolvePorts(opts);
@@ -22751,6 +23340,14 @@ async function startWebUI(opts = {}) {
22751
23340
  } = preContext;
22752
23341
  let sessionStore = preContext.sessionStore;
22753
23342
  let session = preContext.session;
23343
+ const todosCheckpoint = createStandaloneTodosCheckpointLifecycle({
23344
+ state: context.state,
23345
+ sessionsDir: wpaths.projectSessions,
23346
+ sessionId: session.id,
23347
+ events,
23348
+ traceId: context.traceId,
23349
+ warn: (message) => logger.warn(message)
23350
+ });
22754
23351
  let sessionStartedAt = preContext.sessionStartedAt;
22755
23352
  let modeId = preContext.modeId;
22756
23353
  const needsSetup = preContext.needsSetup;
@@ -22877,7 +23474,7 @@ async function startWebUI(opts = {}) {
22877
23474
  if (events.listenerCount("tool.confirm_needed") === 0) {
22878
23475
  throw new Error("No permission confirmation surface is connected");
22879
23476
  }
22880
- const decision = await new Promise((resolve15) => {
23477
+ const decision = await new Promise((resolve16) => {
22881
23478
  events.emit("tool.confirm_needed", {
22882
23479
  sessionId: context.session.id,
22883
23480
  tool: confirmTool,
@@ -22887,7 +23484,7 @@ async function startWebUI(opts = {}) {
22887
23484
  decisionSource: pending.decisionSource,
22888
23485
  riskTier: pending.riskTier,
22889
23486
  boundaryReason: pending.boundaryReason,
22890
- resolve: resolve15
23487
+ resolve: resolve16
22891
23488
  });
22892
23489
  });
22893
23490
  const rule = { tool: "language_package", pattern: pending.suggestedPattern };
@@ -22987,21 +23584,21 @@ async function startWebUI(opts = {}) {
22987
23584
  });
22988
23585
  }
22989
23586
  async function touchProjectEntry(root, workDir) {
22990
- const resolved = path32.resolve(root);
23587
+ const resolved = path33.resolve(root);
22991
23588
  const manifest = await loadManifest(globalConfigPath);
22992
23589
  const now = (/* @__PURE__ */ new Date()).toISOString();
22993
- const existing = manifest.projects.find((p) => path32.resolve(p.root) === resolved);
23590
+ const existing = manifest.projects.find((p) => path33.resolve(p.root) === resolved);
22994
23591
  if (existing) {
22995
23592
  existing.lastSeen = now;
22996
- if (workDir) existing.lastWorkingDir = path32.resolve(workDir);
23593
+ if (workDir) existing.lastWorkingDir = path33.resolve(workDir);
22997
23594
  } else {
22998
23595
  manifest.projects.push({
22999
- name: path32.basename(resolved),
23596
+ name: path33.basename(resolved),
23000
23597
  root: resolved,
23001
23598
  slug: generateProjectSlug(resolved),
23002
23599
  createdAt: now,
23003
23600
  lastSeen: now,
23004
- lastWorkingDir: workDir ? path32.resolve(workDir) : void 0
23601
+ lastWorkingDir: workDir ? path33.resolve(workDir) : void 0
23005
23602
  });
23006
23603
  }
23007
23604
  await saveManifest(manifest, globalConfigPath);
@@ -23102,6 +23699,7 @@ async function startWebUI(opts = {}) {
23102
23699
  const cb = {
23103
23700
  sessionStartPayload,
23104
23701
  claimSession: (sessionId, target) => sessionIdentity.claim(sessionId, target),
23702
+ onBeforeSessionTodosReplaced: todosCheckpoint.rebind,
23105
23703
  onSessionSwapped: async (sessionId, target) => {
23106
23704
  await sessionIdentity.activate(sessionId, target);
23107
23705
  const { hydrateSessionKanban: hydrateSessionKanban2 } = await import("@wrongstack/tools/session-kanban");
@@ -23252,6 +23850,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23252
23850
  ...wssSecondary ? [wssSecondary] : []
23253
23851
  ],
23254
23852
  onShutdown: async () => {
23853
+ await todosCheckpoint.detach();
23255
23854
  await stopHeapWatchdog();
23256
23855
  credentialWatcherClose?.();
23257
23856
  brainMonitor.stop();
@@ -23277,7 +23876,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23277
23876
  await memoryStore.dispose().catch(
23278
23877
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage14(err)}`)
23279
23878
  );
23280
- await unregisterInstance(process.pid, path32.dirname(globalConfigPath));
23879
+ await unregisterInstance(process.pid, path33.dirname(globalConfigPath));
23281
23880
  }
23282
23881
  });
23283
23882
  }