@wrongstack/webui-server 0.302.2 → 0.305.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,14 @@ function isRecord(value) {
32
32
  var AUTONOMY_VALUES = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
33
33
  var CONTEXT_STRATEGY_VALUES = /* @__PURE__ */ new Set(["hybrid", "intelligent", "selective"]);
34
34
  var CONTEXT_MODE_VALUES = /* @__PURE__ */ new Set(["balanced", "frugal", "deep"]);
35
- var TOKEN_SAVING_TIER_VALUES = /* @__PURE__ */ new Set(["off", "minimal", "light", "medium", "aggressive"]);
35
+ var TOKEN_SAVING_TIER_VALUES = /* @__PURE__ */ new Set([
36
+ "auto",
37
+ "off",
38
+ "minimal",
39
+ "light",
40
+ "medium",
41
+ "aggressive"
42
+ ]);
36
43
  var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
37
44
  var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
38
45
  var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
@@ -289,12 +296,8 @@ function validatePreferenceValue(key, value) {
289
296
  if (!Array.isArray(value)) return `prefs.update payload.${key} must be an array`;
290
297
  for (let i = 0; i < value.length; i++) {
291
298
  const item = value[i];
292
- if (!isRecord(item))
293
- return `prefs.update payload.${key}[${i}] must be an object`;
294
- const error2 = arrayValidator(
295
- item,
296
- `prefs.update payload.${key}[${i}]`
297
- );
299
+ if (!isRecord(item)) return `prefs.update payload.${key}[${i}] must be an object`;
300
+ const error2 = arrayValidator(item, `prefs.update payload.${key}[${i}]`);
298
301
  if (error2) return error2;
299
302
  }
300
303
  return null;
@@ -1079,11 +1082,11 @@ import { randomBytes } from "node:crypto";
1079
1082
  import { scrubErrorDetail } from "@wrongstack/core/security";
1080
1083
  import { WebSocket } from "ws";
1081
1084
  var WEBUI_WS_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
1082
- function sendSerialized(ws, data) {
1085
+ function sendSerialized(ws, data, frameBytes) {
1083
1086
  if (ws.readyState !== WebSocket.OPEN) return false;
1084
1087
  const buffered = Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
1085
- const frameBytes = Buffer.byteLength(data, "utf8");
1086
- if (buffered + frameBytes > WEBUI_WS_MAX_BUFFERED_BYTES) {
1088
+ const bytes = frameBytes ?? Buffer.byteLength(data, "utf8");
1089
+ if (buffered + bytes > WEBUI_WS_MAX_BUFFERED_BYTES) {
1087
1090
  try {
1088
1091
  ws.terminate();
1089
1092
  } catch {
@@ -1106,8 +1109,9 @@ function send(ws, msg) {
1106
1109
  }
1107
1110
  function broadcast(clients, msg) {
1108
1111
  const data = JSON.stringify(msg);
1112
+ const frameBytes = Buffer.byteLength(data, "utf8");
1109
1113
  for (const [ws] of clients) {
1110
- sendSerialized(ws, data);
1114
+ sendSerialized(ws, data, frameBytes);
1111
1115
  }
1112
1116
  }
1113
1117
  function sendResult2(ws, success, message) {
@@ -1421,7 +1425,13 @@ function indexDbVersion(projectRoot, indexDir) {
1421
1425
  try {
1422
1426
  const dir = resolveIndexDir(projectRoot, indexDir);
1423
1427
  const st = fs.statSync(path.join(dir, DB_FILE));
1424
- return `${st.mtimeMs}:${st.size}`;
1428
+ let wal = "";
1429
+ try {
1430
+ const walSt = fs.statSync(path.join(dir, `${DB_FILE}-wal`));
1431
+ wal = `:${walSt.mtimeMs}:${walSt.size}`;
1432
+ } catch {
1433
+ }
1434
+ return `${st.mtimeMs}:${st.size}${wal}`;
1425
1435
  } catch {
1426
1436
  return "missing";
1427
1437
  }
@@ -2153,6 +2163,7 @@ var CollaborationWebSocketHandler = class {
2153
2163
  this.broadcast(sessionId, this.stateMessage(sessionId));
2154
2164
  }
2155
2165
  }, 2e3);
2166
+ this.broadcastInterval.unref?.();
2156
2167
  }
2157
2168
  stopBroadcast() {
2158
2169
  if (this.broadcastInterval) {
@@ -7125,13 +7136,18 @@ var GoalWebSocketHandler = class {
7125
7136
  const { execFile: execFile2 } = await import("node:child_process");
7126
7137
  const result = await new Promise((resolve16) => {
7127
7138
  const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
7128
- execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
7129
- if (err && err.code === "ENOENT") {
7130
- resolve16("[verify] tsc not found \u2014 skipping");
7131
- return;
7139
+ execFile2(
7140
+ npxCommand,
7141
+ ["tsc", "--noEmit"],
7142
+ { cwd, timeout: 6e4 },
7143
+ (err, stdout, stderr) => {
7144
+ if (err && err.code === "ENOENT") {
7145
+ resolve16("[verify] tsc not found \u2014 skipping");
7146
+ return;
7147
+ }
7148
+ resolve16(stdout + stderr);
7132
7149
  }
7133
- resolve16(stdout + stderr);
7134
- });
7150
+ );
7135
7151
  });
7136
7152
  if (result.includes("[verify]") || result.trim().length === 0) {
7137
7153
  return { ok: true };
@@ -7433,6 +7449,7 @@ ${result_.finalText.slice(0, 2e3)}`
7433
7449
  if (progress) this.broadcast({ type: "goal.progress", payload: progress });
7434
7450
  this.broadcastState();
7435
7451
  }, 2e3);
7452
+ this.broadcastInterval.unref?.();
7436
7453
  }
7437
7454
  stopBroadcast() {
7438
7455
  if (this.broadcastInterval) {
@@ -7557,8 +7574,9 @@ ${result_.finalText.slice(0, 2e3)}`
7557
7574
  }
7558
7575
  broadcast(msg) {
7559
7576
  const data = JSON.stringify(msg);
7577
+ const frameBytes = Buffer.byteLength(data, "utf8");
7560
7578
  for (const client of this.clients) {
7561
- sendSerialized(client.ws, data);
7579
+ sendSerialized(client.ws, data, frameBytes);
7562
7580
  }
7563
7581
  }
7564
7582
  send(client, msg) {
@@ -7603,12 +7621,34 @@ function handleTodosGet(ctx, ws) {
7603
7621
  payload: sessionPayload(ctx, { todos: [...ctx.context.todos] })
7604
7622
  });
7605
7623
  }
7606
- function handleTodosClear(ctx, ws) {
7607
- ctx.replaceTodos?.([]);
7608
- sendResult3(ctx, ws, true, "Todos cleared");
7609
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: [] }) });
7624
+ async function commitTodos(ctx, todos) {
7625
+ if (ctx.mutateTodos) {
7626
+ const result = await ctx.mutateTodos(todos);
7627
+ return { todos: result.todos, warnings: result.warnings ?? [] };
7628
+ }
7629
+ ctx.replaceTodos?.(todos);
7630
+ return { todos: [...todos], warnings: [] };
7631
+ }
7632
+ function managedProjectionMessage() {
7633
+ return "Kanban-bound todos are task projections. Change or remove the task from Kanban.";
7634
+ }
7635
+ async function handleTodosClear(ctx, ws) {
7636
+ if (ctx.context.todos.some((todo) => todo.kanbanBoardId && todo.kanbanTaskId)) {
7637
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7638
+ return;
7639
+ }
7640
+ try {
7641
+ const result = await commitTodos(ctx, []);
7642
+ sendResult3(ctx, ws, true, "Todos cleared");
7643
+ ctx.broadcast({
7644
+ type: "todos.updated",
7645
+ payload: sessionPayload(ctx, { todos: result.todos })
7646
+ });
7647
+ } catch (error2) {
7648
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7649
+ }
7610
7650
  }
7611
- function handleTodosRemove(ctx, ws, payload) {
7651
+ async function handleTodosRemove(ctx, ws, payload) {
7612
7652
  if (!payload) {
7613
7653
  sendResult3(ctx, ws, false, "Missing id or index");
7614
7654
  return;
@@ -7625,12 +7665,27 @@ function handleTodosRemove(ctx, ws, payload) {
7625
7665
  sendResult3(ctx, ws, false, "Todo not found");
7626
7666
  return;
7627
7667
  }
7668
+ if (removed.kanbanBoardId && removed.kanbanTaskId) {
7669
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7670
+ return;
7671
+ }
7628
7672
  const next = [...todos.slice(0, targetIndex), ...todos.slice(targetIndex + 1)];
7629
- ctx.replaceTodos?.(next);
7630
- sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
7631
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
7673
+ try {
7674
+ const result = await commitTodos(ctx, next);
7675
+ sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
7676
+ ctx.broadcast({
7677
+ type: "todos.updated",
7678
+ payload: sessionPayload(ctx, { todos: result.todos })
7679
+ });
7680
+ } catch (error2) {
7681
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7682
+ }
7632
7683
  }
7633
- function handleTodoUpdate(ctx, ws, payload) {
7684
+ async function handleTodoUpdate(ctx, ws, payload) {
7685
+ if (!payload || typeof payload.id !== "string" || payload.status !== void 0 && payload.status !== "pending" && payload.status !== "in_progress" && payload.status !== "completed" || payload.activeForm !== void 0 && typeof payload.activeForm !== "string") {
7686
+ sendResult3(ctx, ws, false, "Invalid todo update payload");
7687
+ return;
7688
+ }
7634
7689
  const index = ctx.context.todos.findIndex((todo) => todo.id === payload.id);
7635
7690
  const existing = ctx.context.todos[index];
7636
7691
  if (index === -1 || !existing) {
@@ -7643,9 +7698,25 @@ function handleTodoUpdate(ctx, ws, payload) {
7643
7698
  status: payload.status ?? existing.status,
7644
7699
  activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
7645
7700
  };
7646
- ctx.replaceTodos?.(next);
7647
- sendResult3(ctx, ws, true, `Todo "${existing.content}" updated`);
7648
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
7701
+ try {
7702
+ const result = await commitTodos(ctx, next);
7703
+ const projected = result.todos.find((todo) => todo.id === existing.id);
7704
+ const requestedStatus = payload.status ?? existing.status;
7705
+ const projectionRejected = Boolean(existing.kanbanBoardId && existing.kanbanTaskId) && projected?.status !== requestedStatus;
7706
+ const warning = result.warnings[0];
7707
+ sendResult3(
7708
+ ctx,
7709
+ ws,
7710
+ !projectionRejected,
7711
+ projectionRejected ? warning ?? `Kanban kept "${existing.content}" at ${projected?.status ?? "its current state"}.` : warning ? `Todo "${existing.content}" updated. ${warning}` : `Todo "${existing.content}" updated`
7712
+ );
7713
+ ctx.broadcast({
7714
+ type: "todos.updated",
7715
+ payload: sessionPayload(ctx, { todos: result.todos })
7716
+ });
7717
+ } catch (error2) {
7718
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7719
+ }
7649
7720
  }
7650
7721
  async function handleTasksGet(ctx, ws) {
7651
7722
  const taskPath = taskPathOf(ctx);
@@ -7673,14 +7744,32 @@ async function handleTaskUpdate(ctx, ws, payload) {
7673
7744
  return;
7674
7745
  }
7675
7746
  try {
7676
- const file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
7677
- const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
7678
- if (!task) return tasks;
7679
- task.status = payload.status;
7680
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7681
- return tasks;
7682
- });
7683
- sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
7747
+ let file;
7748
+ if (ctx.mutateTaskStatus) {
7749
+ const result = await ctx.mutateTaskStatus(payload.id, payload.status);
7750
+ if (!result.ok) {
7751
+ sendResult3(ctx, ws, false, result.message);
7752
+ return;
7753
+ }
7754
+ file = await loadTasks(taskPath);
7755
+ if (!file) throw new Error("Task mutation succeeded but its persisted snapshot is missing.");
7756
+ sendResult3(ctx, ws, true, result.message);
7757
+ } else {
7758
+ let matched = false;
7759
+ file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
7760
+ const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
7761
+ if (!task) return tasks;
7762
+ matched = true;
7763
+ task.status = payload.status;
7764
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7765
+ return tasks;
7766
+ });
7767
+ if (!matched) {
7768
+ sendResult3(ctx, ws, false, `Task "${payload.id}" not found.`);
7769
+ return;
7770
+ }
7771
+ sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
7772
+ }
7684
7773
  ctx.broadcast({
7685
7774
  type: "tasks.updated",
7686
7775
  payload: sessionPayload(ctx, { tasks: file.tasks })
@@ -7727,6 +7816,18 @@ async function handlePlanTemplateUse(ctx, ws, template) {
7727
7816
  return;
7728
7817
  }
7729
7818
  try {
7819
+ if (ctx.mutatePlan) {
7820
+ const result = await ctx.mutatePlan({ action: "template_use", template });
7821
+ if (!result.ok) {
7822
+ sendResult3(ctx, ws, false, result.message);
7823
+ return;
7824
+ }
7825
+ const plan2 = await loadPlan(planPath);
7826
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7827
+ sendResult3(ctx, ws, true, result.message);
7828
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7829
+ return;
7830
+ }
7730
7831
  const templateDefinition = getPlanTemplate(template);
7731
7832
  if (!templateDefinition) {
7732
7833
  sendResult3(ctx, ws, false, `Unknown template "${template}".`);
@@ -7755,6 +7856,22 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
7755
7856
  return;
7756
7857
  }
7757
7858
  try {
7859
+ if (ctx.mutatePlan) {
7860
+ const result = await ctx.mutatePlan({
7861
+ action: "status",
7862
+ target: payload.target,
7863
+ status: payload.status
7864
+ });
7865
+ if (!result.ok) {
7866
+ sendResult3(ctx, ws, false, result.message);
7867
+ return;
7868
+ }
7869
+ const plan2 = await loadPlan(planPath);
7870
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7871
+ sendResult3(ctx, ws, true, result.message);
7872
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7873
+ return;
7874
+ }
7758
7875
  let changed = false;
7759
7876
  const plan = await mutatePlan(planPath, currentSessionId(ctx), async (currentPlan) => {
7760
7877
  const before = currentPlan.updatedAt;
@@ -7778,13 +7895,17 @@ async function handleWorklistMessage(ctx, ws, message) {
7778
7895
  handleTodosGet(ctx, ws);
7779
7896
  return;
7780
7897
  case "todos.clear":
7781
- handleTodosClear(ctx, ws);
7898
+ await handleTodosClear(ctx, ws);
7782
7899
  return;
7783
7900
  case "todos.remove":
7784
- handleTodosRemove(ctx, ws, message.payload);
7901
+ await handleTodosRemove(
7902
+ ctx,
7903
+ ws,
7904
+ message.payload
7905
+ );
7785
7906
  return;
7786
7907
  case "todo.update":
7787
- handleTodoUpdate(
7908
+ await handleTodoUpdate(
7788
7909
  ctx,
7789
7910
  ws,
7790
7911
  message.payload
@@ -9358,7 +9479,19 @@ function isTrustedLoopbackOrigin(origin, hostHeader) {
9358
9479
  if (!host) return false;
9359
9480
  const hostUrl = new URL(`${url.protocol}//${host}`);
9360
9481
  if (!isLoopbackHostname(hostUrl.hostname)) return false;
9361
- return effectivePort(url) === effectivePort(hostUrl);
9482
+ return normalizeHostname(url.hostname) === normalizeHostname(hostUrl.hostname) && effectivePort(url) === effectivePort(hostUrl);
9483
+ } catch {
9484
+ return false;
9485
+ }
9486
+ }
9487
+ function originMatchesHost(origin, hostHeader) {
9488
+ try {
9489
+ const originUrl = new URL(origin);
9490
+ if (originUrl.protocol !== "http:" && originUrl.protocol !== "https:") return false;
9491
+ const host = (hostHeader ?? "").trim();
9492
+ if (!host) return false;
9493
+ const requestUrl = new URL(`${originUrl.protocol}//${host}`);
9494
+ return normalizeHostname(originUrl.hostname) === normalizeHostname(requestUrl.hostname) && effectivePort(originUrl) === effectivePort(requestUrl);
9362
9495
  } catch {
9363
9496
  return false;
9364
9497
  }
@@ -9463,13 +9596,15 @@ function verifyClient(input) {
9463
9596
  try {
9464
9597
  const { hostname: originHostname } = new URL(origin);
9465
9598
  if (isLoopbackHostname(originHostname)) {
9466
- if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
9599
+ if (requireToken || !isLoopbackBind(wsHost)) {
9600
+ return cookieTokenOk && (originMatchesHost(origin, hostHeader) || Boolean(allowCrossPortLoopbackCookie));
9601
+ }
9467
9602
  if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
9468
9603
  return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
9469
9604
  }
9470
9605
  return true;
9471
9606
  }
9472
- return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
9607
+ return cookieTokenOk && originMatchesHost(origin, hostHeader) || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
9473
9608
  } catch {
9474
9609
  return false;
9475
9610
  }
@@ -10359,7 +10494,8 @@ async function handleIntrospectionRoute(ctx, ws, message) {
10359
10494
  switch (message.type) {
10360
10495
  case "diag.get": {
10361
10496
  if (!sessionAllowed(ctx, ws, message)) return true;
10362
- const tools = ctx.agent.tools.list();
10497
+ const registry = ctx.agent.tools;
10498
+ const tools = registry.listForProvider?.() ?? registry.list();
10363
10499
  ctx.send(ws, {
10364
10500
  type: "diag.get",
10365
10501
  payload: {
@@ -10442,6 +10578,7 @@ async function handleIntrospectionRoute(ctx, ws, message) {
10442
10578
  description: tool.description ?? "",
10443
10579
  params: schema.properties ? Object.keys(schema.properties) : [],
10444
10580
  disabled: registry.isDisabled?.(tool.name) ?? false,
10581
+ direct: registry.isExposedToProvider?.(tool.name) ?? true,
10445
10582
  mutating: !!tool.mutating,
10446
10583
  permission: tool.permission ?? "auto"
10447
10584
  };
@@ -10737,7 +10874,6 @@ async function handleKanbanHostRoute(ws, msg, handlers) {
10737
10874
  import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core/tasking";
10738
10875
  import {
10739
10876
  addCheckToTask,
10740
- addColumn,
10741
10877
  addGoalMetricToTask,
10742
10878
  addNoteToTask,
10743
10879
  addTask,
@@ -10745,14 +10881,17 @@ import {
10745
10881
  claimReadyTask,
10746
10882
  copyTaskToBoard,
10747
10883
  createBoard,
10884
+ createBoardFromText,
10748
10885
  duplicateBoard,
10749
10886
  exportBoardToTaskGraph,
10750
- createBoardFromText,
10751
10887
  getBoard,
10752
10888
  getKanbanOrchestrationSnapshot,
10753
10889
  getKanbanQueueHealth,
10754
10890
  getTaskChain,
10891
+ hasKanbanQueueAnomalies,
10892
+ kanbanQueueAnomalyCount,
10755
10893
  listBoards as listBoards2,
10894
+ listBoardHistory,
10756
10895
  listReadyTasks,
10757
10896
  mergeTasks,
10758
10897
  moveTask,
@@ -10761,7 +10900,6 @@ import {
10761
10900
  recoverStaleTaskAssignments,
10762
10901
  releaseTaskClaim,
10763
10902
  removeBoard,
10764
- removeColumn,
10765
10903
  setTaskChain,
10766
10904
  splitTask,
10767
10905
  syncBoardFromTaskGraph,
@@ -10773,14 +10911,16 @@ import {
10773
10911
  updateTask as updateTask2
10774
10912
  } from "@wrongstack/kanban";
10775
10913
 
10776
- // src/server/kanban-decomposition-routes.ts
10914
+ // src/server/kanban-contract-routes.ts
10777
10915
  import {
10778
- listBoards,
10779
- resolveDecompositionProposal,
10780
- updateTask,
10781
- verifyTaskCompletion
10916
+ addContractEdge,
10917
+ configureContractGraph,
10918
+ evaluateTaskContractGraph,
10919
+ getContractGraph,
10920
+ removeContractEdge,
10921
+ removeContractNode,
10922
+ upsertContractNode
10782
10923
  } from "@wrongstack/kanban";
10783
- import { recordKanbanVerificationEvidence as recordKanbanVerificationEvidence2 } from "@wrongstack/tools";
10784
10924
 
10785
10925
  // src/server/kanban-route-helpers.ts
10786
10926
  import { touchKanbanPresence } from "@wrongstack/kanban";
@@ -10832,7 +10972,150 @@ function findTask2(tasks, taskId) {
10832
10972
  return tasks.find((task) => task.id === taskId || task.id.startsWith(taskId));
10833
10973
  }
10834
10974
 
10975
+ // src/server/kanban-contract-routes.ts
10976
+ async function handleKanbanContractRoute(ws, type, payload, ctx) {
10977
+ switch (type) {
10978
+ case "kanban.contract.get":
10979
+ await handleGet(ws, type, payload, ctx);
10980
+ return true;
10981
+ case "kanban.contract.configure":
10982
+ await handleConfigure(ws, type, payload, ctx);
10983
+ return true;
10984
+ case "kanban.contract.node.upsert":
10985
+ await handleNodeUpsert(ws, type, payload, ctx);
10986
+ return true;
10987
+ case "kanban.contract.node.remove":
10988
+ await handleNodeRemove(ws, type, payload, ctx);
10989
+ return true;
10990
+ case "kanban.contract.edge.add":
10991
+ await handleEdgeAdd(ws, type, payload, ctx);
10992
+ return true;
10993
+ case "kanban.contract.edge.remove":
10994
+ await handleEdgeRemove(ws, type, payload, ctx);
10995
+ return true;
10996
+ default:
10997
+ return false;
10998
+ }
10999
+ }
11000
+ var str = (payload, key) => typeof payload?.[key] === "string" ? payload[key] : void 0;
11001
+ async function publishBoard(ctx, board) {
11002
+ await publishKanbanBoard((message) => ctx.broadcast?.(message), board);
11003
+ }
11004
+ async function handleGet(ws, type, payload, ctx) {
11005
+ const boardId = str(payload, "boardId");
11006
+ if (!boardId) return fail(ws, type, "boardId required");
11007
+ const found = await getContractGraph(ctx.projectRoot, boardId);
11008
+ if (!found) return fail(ws, type, "Board not found");
11009
+ const taskId = str(payload, "taskId");
11010
+ const evaluated = taskId ? await evaluateTaskContractGraph(ctx.projectRoot, boardId, taskId) : null;
11011
+ if (taskId && !evaluated) return fail(ws, type, "Task not found on this board");
11012
+ ok(ws, type, {
11013
+ boardId,
11014
+ graph: found.graph,
11015
+ ...evaluated ? { evaluation: evaluated.evaluation } : {}
11016
+ });
11017
+ }
11018
+ async function handleConfigure(ws, type, payload, ctx) {
11019
+ const boardId = str(payload, "boardId");
11020
+ if (!boardId) return fail(ws, type, "boardId required");
11021
+ const enforcement = str(payload, "enforcement") ?? "advisory";
11022
+ const board = await configureContractGraph(ctx.projectRoot, boardId, enforcement);
11023
+ if (!board) return fail(ws, type, "Board not found");
11024
+ await publishBoard(ctx, board);
11025
+ ok(ws, type, { boardId, graph: board.contractGraph ?? null });
11026
+ }
11027
+ async function handleNodeUpsert(ws, type, payload, ctx) {
11028
+ const boardId = str(payload, "boardId");
11029
+ const taskId = str(payload, "taskId");
11030
+ const kind = str(payload, "kind");
11031
+ const title = str(payload, "title");
11032
+ if (!boardId || !taskId || !kind || !title) {
11033
+ return fail(ws, type, "boardId, taskId, kind, and title required");
11034
+ }
11035
+ const state = str(payload, "state");
11036
+ const waiverActor = str(payload, "waiverActor");
11037
+ const waiverReason = str(payload, "waiverReason");
11038
+ if (state === "waived" && (!waiverActor?.trim() || !waiverReason?.trim())) {
11039
+ return fail(ws, type, "A waived contract node requires waiverActor and waiverReason");
11040
+ }
11041
+ try {
11042
+ const result = await upsertContractNode(ctx.projectRoot, boardId, {
11043
+ taskId,
11044
+ kind,
11045
+ title,
11046
+ ...str(payload, "nodeId") !== void 0 ? { id: str(payload, "nodeId") } : {},
11047
+ ...str(payload, "description") !== void 0 ? { description: str(payload, "description") } : {},
11048
+ ...state !== void 0 ? { state } : {},
11049
+ ...str(payload, "enforcement") !== void 0 ? { enforcement: str(payload, "enforcement") } : {},
11050
+ ...str(payload, "checkId") !== void 0 ? { checkId: str(payload, "checkId") } : {},
11051
+ ...str(payload, "metricId") !== void 0 ? { metricId: str(payload, "metricId") } : {},
11052
+ ...state === "waived" ? {
11053
+ waiver: {
11054
+ actor: waiverActor,
11055
+ reason: waiverReason,
11056
+ at: (/* @__PURE__ */ new Date()).toISOString()
11057
+ }
11058
+ } : {},
11059
+ ...str(payload, "createdBy") !== void 0 ? { createdBy: str(payload, "createdBy") } : { createdBy: "webui" }
11060
+ });
11061
+ if (!result) return fail(ws, type, "Board or task not found");
11062
+ await publishBoard(ctx, result.board);
11063
+ ok(ws, type, { boardId, node: result.node, graph: result.board.contractGraph ?? null });
11064
+ } catch (err) {
11065
+ fail(ws, type, err instanceof Error ? err.message : String(err));
11066
+ }
11067
+ }
11068
+ async function handleNodeRemove(ws, type, payload, ctx) {
11069
+ const boardId = str(payload, "boardId");
11070
+ const nodeId = str(payload, "nodeId");
11071
+ if (!boardId || !nodeId) return fail(ws, type, "boardId and nodeId required");
11072
+ const board = await removeContractNode(ctx.projectRoot, boardId, nodeId);
11073
+ if (!board) return fail(ws, type, "Contract node not found");
11074
+ await publishBoard(ctx, board);
11075
+ ok(ws, type, { boardId, graph: board.contractGraph ?? null });
11076
+ }
11077
+ async function handleEdgeAdd(ws, type, payload, ctx) {
11078
+ const boardId = str(payload, "boardId");
11079
+ const from = str(payload, "from");
11080
+ const to = str(payload, "to");
11081
+ const edgeType = str(payload, "edgeType");
11082
+ if (!boardId || !from || !to || !edgeType) {
11083
+ return fail(ws, type, "boardId, from, to, and edgeType required");
11084
+ }
11085
+ try {
11086
+ const result = await addContractEdge(ctx.projectRoot, boardId, {
11087
+ from,
11088
+ to,
11089
+ type: edgeType,
11090
+ ...str(payload, "enforcement") !== void 0 ? { enforcement: str(payload, "enforcement") } : {},
11091
+ ...str(payload, "rationale") !== void 0 ? { rationale: str(payload, "rationale") } : {},
11092
+ createdBy: str(payload, "createdBy") ?? "webui"
11093
+ });
11094
+ if (!result) return fail(ws, type, "Board not found");
11095
+ await publishBoard(ctx, result.board);
11096
+ ok(ws, type, { boardId, edge: result.edge, graph: result.board.contractGraph ?? null });
11097
+ } catch (err) {
11098
+ fail(ws, type, err instanceof Error ? err.message : String(err));
11099
+ }
11100
+ }
11101
+ async function handleEdgeRemove(ws, type, payload, ctx) {
11102
+ const boardId = str(payload, "boardId");
11103
+ const edgeId = str(payload, "edgeId");
11104
+ if (!boardId || !edgeId) return fail(ws, type, "boardId and edgeId required");
11105
+ const board = await removeContractEdge(ctx.projectRoot, boardId, edgeId);
11106
+ if (!board) return fail(ws, type, "Contract edge not found");
11107
+ await publishBoard(ctx, board);
11108
+ ok(ws, type, { boardId, graph: board.contractGraph ?? null });
11109
+ }
11110
+
10835
11111
  // src/server/kanban-decomposition-routes.ts
11112
+ import {
11113
+ listBoards,
11114
+ resolveDecompositionProposal,
11115
+ updateTask,
11116
+ verifyTaskCompletion
11117
+ } from "@wrongstack/kanban";
11118
+ import { recordKanbanVerificationEvidence as recordKanbanVerificationEvidence2 } from "@wrongstack/tools";
10836
11119
  async function handleKanbanDecompositionRoute(ws, type, payload, ctx) {
10837
11120
  switch (type) {
10838
11121
  case "kanban.decomposition.approve":
@@ -10935,8 +11218,35 @@ async function handleTaskVerification(ws, type, payload, ctx) {
10935
11218
  }
10936
11219
  }
10937
11220
 
11221
+ // src/server/kanban-route-pagination.ts
11222
+ function paginateKanbanBoards(boards, input) {
11223
+ const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
11224
+ const activeSessionIds = new Set(input.activeSessionIds ?? []);
11225
+ const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
11226
+ const sorted = [...boards].sort((left, right) => {
11227
+ const activityOrder = Number(isActive(right)) - Number(isActive(left));
11228
+ return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
11229
+ });
11230
+ const activeTotal = sorted.filter(isActive).length;
11231
+ const total = sorted.length;
11232
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
11233
+ const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
11234
+ const page = Math.min(totalPages, Math.max(1, requestedPage));
11235
+ const start = (page - 1) * pageSize;
11236
+ return {
11237
+ items: sorted.slice(start, start + pageSize),
11238
+ total,
11239
+ page,
11240
+ pageSize,
11241
+ totalPages,
11242
+ activeTotal,
11243
+ orphanedTotal: total - activeTotal
11244
+ };
11245
+ }
11246
+
10938
11247
  // src/server/kanban-task-routes.ts
10939
11248
  import {
11249
+ getKanbanWorkbench,
10940
11250
  getTask,
10941
11251
  listTaskActivity,
10942
11252
  recordTaskActivity,
@@ -10944,6 +11254,16 @@ import {
10944
11254
  } from "@wrongstack/kanban";
10945
11255
  async function handleKanbanTaskRoute(ws, type, payload, ctx) {
10946
11256
  switch (type) {
11257
+ case "kanban.workbench":
11258
+ ok(
11259
+ ws,
11260
+ type,
11261
+ await getKanbanWorkbench(ctx.projectRoot, {
11262
+ ...typeof payload?.limitPerLane === "number" ? { limitPerLane: payload.limitPerLane } : {},
11263
+ ...typeof payload?.alertLimit === "number" ? { alertLimit: payload.alertLimit } : {}
11264
+ })
11265
+ );
11266
+ return true;
10947
11267
  case "kanban.task.remove":
10948
11268
  await handleTaskRemove(ws, type, payload, ctx);
10949
11269
  return true;
@@ -11036,40 +11356,11 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
11036
11356
  outcome,
11037
11357
  ...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
11038
11358
  },
11039
- activityContext(
11040
- ctx,
11041
- payload?.actor ?? ctx.context?.agentId ?? "webui"
11042
- )
11359
+ activityContext(ctx, payload?.actor ?? ctx.context?.agentId ?? "webui")
11043
11360
  );
11044
11361
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
11045
11362
  }
11046
11363
 
11047
- // src/server/kanban-route-pagination.ts
11048
- function paginateKanbanBoards(boards, input) {
11049
- const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
11050
- const activeSessionIds = new Set(input.activeSessionIds ?? []);
11051
- const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
11052
- const sorted = [...boards].sort((left, right) => {
11053
- const activityOrder = Number(isActive(right)) - Number(isActive(left));
11054
- return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
11055
- });
11056
- const activeTotal = sorted.filter(isActive).length;
11057
- const total = sorted.length;
11058
- const totalPages = Math.max(1, Math.ceil(total / pageSize));
11059
- const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
11060
- const page = Math.min(totalPages, Math.max(1, requestedPage));
11061
- const start = (page - 1) * pageSize;
11062
- return {
11063
- items: sorted.slice(start, start + pageSize),
11064
- total,
11065
- page,
11066
- pageSize,
11067
- totalPages,
11068
- activeTotal,
11069
- orphanedTotal: total - activeTotal
11070
- };
11071
- }
11072
-
11073
11364
  // src/server/kanban-routes.ts
11074
11365
  async function handleKanbanRoute(ws, msg, ctx) {
11075
11366
  if (!msg.type.startsWith("kanban.")) return false;
@@ -11077,6 +11368,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
11077
11368
  const type = msg.type;
11078
11369
  try {
11079
11370
  if (await handleKanbanDecompositionRoute(ws, type, payload, ctx)) return true;
11371
+ if (await handleKanbanContractRoute(ws, type, payload, ctx)) return true;
11080
11372
  if (await handleKanbanTaskRoute(ws, type, payload, ctx)) return true;
11081
11373
  switch (type) {
11082
11374
  case "kanban.list": {
@@ -11115,7 +11407,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11115
11407
  fail(ws, type, "boardId required");
11116
11408
  return true;
11117
11409
  }
11118
- ok(ws, type, await getKanbanQueueHealth(ctx.projectRoot, { boardId: hBoardId }));
11410
+ ok(
11411
+ ws,
11412
+ type,
11413
+ await getKanbanQueueHealth(ctx.projectRoot, {
11414
+ boardId: hBoardId,
11415
+ includeClassifications: false
11416
+ })
11417
+ );
11119
11418
  return true;
11120
11419
  }
11121
11420
  case "kanban.supervisor.status":
@@ -11145,16 +11444,16 @@ async function handleKanbanRoute(ws, msg, ctx) {
11145
11444
  reason: "On-demand standalone Kanban supervisor audit."
11146
11445
  }) : null;
11147
11446
  if (recovered) health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
11148
- const anomalyCount = health.staleAssignments.count + health.dependencyBlocked.count + health.failedRetryable.count + health.counts.failed + health.counts.blocked;
11447
+ const anomalyCount = kanbanQueueAnomalyCount(health);
11149
11448
  ok(ws, type, {
11150
11449
  boardId,
11151
- status: board.supervisor?.enabled === false ? "disabled" : anomalyCount ? "attention" : "healthy",
11450
+ status: board.supervisor?.enabled === false ? "disabled" : hasKanbanQueueAnomalies(health) ? "attention" : "healthy",
11152
11451
  mode: board.supervisor?.mode ?? "deterministic",
11153
11452
  lastAuditAt: (/* @__PURE__ */ new Date()).toISOString(),
11154
11453
  reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
11155
11454
  staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
11156
11455
  anomalyCount,
11157
- summary: `${health.counts.running} running \xB7 ${health.counts.ready} ready \xB7 ${health.counts.review} review \xB7 ${health.counts.blocked} blocked \xB7 ${health.counts.failed} failed`
11456
+ summary: `${health.counts.running} running \xB7 ${health.counts.startable} ready \xB7 ${health.counts.review} review \xB7 ${health.counts.blocked} blocked \xB7 ${health.counts.failed} failed`
11158
11457
  });
11159
11458
  return true;
11160
11459
  }
@@ -11171,7 +11470,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11171
11470
  title,
11172
11471
  ...payload?.description ? { description: payload.description } : {},
11173
11472
  ...payload?.tags ? { tags: payload.tags } : {},
11174
- ...payload?.columns ? { columns: payload.columns } : {},
11175
11473
  ...has(payload, "lifecycle") ? { lifecycle: payload?.lifecycle } : {},
11176
11474
  ...has(payload, "boundary") ? { boundary: payload?.boundary } : {}
11177
11475
  })
@@ -11188,7 +11486,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11188
11486
  ...payload?.title ? { title: payload.title } : {},
11189
11487
  ...payload?.description ? { description: payload.description } : {},
11190
11488
  ...payload?.tags ? { tags: payload.tags } : {},
11191
- ...payload?.columns ? { columns: payload.columns } : {},
11192
11489
  ...has(payload, "lifecycle") ? {
11193
11490
  lifecycle: payload?.lifecycle ?? null
11194
11491
  } : {},
@@ -11235,6 +11532,12 @@ async function handleKanbanRoute(ws, msg, ctx) {
11235
11532
  });
11236
11533
  return true;
11237
11534
  }
11535
+ case "kanban.board.history": {
11536
+ const boardId = payload?.boardId;
11537
+ const history = await listBoardHistory(ctx.projectRoot, boardId);
11538
+ ok(ws, type, history);
11539
+ return true;
11540
+ }
11238
11541
  case "kanban.generate": {
11239
11542
  const description = payload?.description;
11240
11543
  if (!description) {
@@ -11696,8 +11999,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11696
11999
  taskId,
11697
12000
  {
11698
12001
  description,
12002
+ // The old cast listed `manual | auto | agent | test | review` —
12003
+ // three of which have no verifier plugin, while the six that do
12004
+ // (command, file_exists, file_matches, git_diff, metric, test)
12005
+ // were unreachable. `notes` carries the executable body every
12006
+ // deterministic plugin reads.
11699
12007
  type: payload?.checkType ?? "manual",
11700
- status: payload?.status ?? "pending"
12008
+ status: payload?.status ?? "pending",
12009
+ ...typeof payload?.notes === "string" ? { notes: payload.notes } : {}
11701
12010
  },
11702
12011
  activityContext(
11703
12012
  ctx,
@@ -11762,30 +12071,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11762
12071
  case "kanban.capabilities":
11763
12072
  ok(ws, type, { dispatchSupported: Boolean(ctx.dispatchTask) });
11764
12073
  return true;
11765
- case "kanban.column.add": {
11766
- const boardId = payload?.boardId;
11767
- const title = payload?.title;
11768
- if (!boardId || !title) {
11769
- fail(ws, type, "boardId and title required");
11770
- return true;
11771
- }
11772
- const result = await addColumn(ctx.projectRoot, boardId, { title });
11773
- result ? ok(ws, type, result.board.columns) : fail(ws, type, `Board not found: ${boardId}`);
11774
- return true;
11775
- }
11776
- case "kanban.column.remove": {
11777
- const boardId = payload?.boardId;
11778
- const columnId = payload?.columnId;
11779
- if (!boardId || !columnId) {
11780
- fail(ws, type, "boardId and columnId required");
11781
- return true;
11782
- }
11783
- const board = await removeColumn(ctx.projectRoot, boardId, columnId, {
11784
- moveTasksToColumnId: payload?.moveTasksToColumnId
11785
- });
11786
- board ? ok(ws, type, { removed: true, boardId: board.id, columnId, board }) : fail(ws, type, `Column not found: ${columnId}`);
11787
- return true;
11788
- }
11789
12074
  default:
11790
12075
  fail(ws, type, `Unknown kanban message type: ${type}`);
11791
12076
  return true;
@@ -11848,7 +12133,9 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
11848
12133
  projectRoot,
11849
12134
  async (event) => {
11850
12135
  const family = event.event?.split(".")[0];
11851
- if (family !== "board" && family !== "task" && family !== "column") return;
12136
+ if (family !== "board" && family !== "task" && family !== "column" && family !== "contract") {
12137
+ return;
12138
+ }
11852
12139
  const evData = event.data;
11853
12140
  const boardId = evData?.boardId;
11854
12141
  if (!boardId) return;
@@ -13048,6 +13335,34 @@ async function handleSageRecover(ws, msg, memoryStore) {
13048
13335
  send(ws, { type: "memory.sage.recover", payload: { error: errMessage(err) } });
13049
13336
  }
13050
13337
  }
13338
+ async function handleSageListCandidates(ws, msg, memoryStore) {
13339
+ const Sage = getSageSurface(memoryStore);
13340
+ if (!Sage) {
13341
+ send(ws, {
13342
+ type: "memory.sage.listCandidates",
13343
+ payload: { error: requiresSage("memory.sage.listCandidates") }
13344
+ });
13345
+ return;
13346
+ }
13347
+ try {
13348
+ const payload = msg.payload ?? {};
13349
+ const includeResolved = payload["includeResolved"] === true;
13350
+ if (typeof Sage.listCandidates !== "function") {
13351
+ send(ws, {
13352
+ type: "memory.sage.listCandidates",
13353
+ payload: { error: "listCandidates is not available on this SAGE surface" }
13354
+ });
13355
+ return;
13356
+ }
13357
+ const candidates = await Sage.listCandidates(includeResolved);
13358
+ send(ws, { type: "memory.sage.listCandidates", payload: { candidates } });
13359
+ } catch (err) {
13360
+ send(ws, {
13361
+ type: "memory.sage.listCandidates",
13362
+ payload: { error: errMessage(err) }
13363
+ });
13364
+ }
13365
+ }
13051
13366
  async function handleSageCandidateResolve(ws, msg, memoryStore) {
13052
13367
  const Sage = getSageSurface(memoryStore);
13053
13368
  if (!Sage) {
@@ -13160,14 +13475,17 @@ async function handleSageForFile(ws, msg, memoryStore) {
13160
13475
  send(ws, { type: "memory.sage.forFile", payload: { error: "filePath is required" } });
13161
13476
  return;
13162
13477
  }
13478
+ const includeSuperseded = typeof payload["showSuperseded"] === "boolean" ? payload["showSuperseded"] : typeof payload["includeSuperseded"] === "boolean" ? payload["includeSuperseded"] : void 0;
13479
+ const includeDeleted = payload["showDeleted"] === true || payload["includeDeleted"] === true;
13163
13480
  try {
13164
13481
  const response = await Sage.findMemoriesForFile(filePath, {
13165
13482
  ...typeof payload["lineStart"] === "number" ? { lineStart: payload["lineStart"] } : {},
13166
13483
  ...typeof payload["lineEnd"] === "number" ? { lineEnd: payload["lineEnd"] } : {},
13167
13484
  ...typeof payload["limit"] === "number" ? { limit: payload["limit"] } : {},
13168
- ...payload["includeDeleted"] === true ? { includeDeleted: true } : {}
13485
+ ...includeSuperseded !== void 0 ? { includeSuperseded } : {},
13486
+ ...includeDeleted ? { includeDeleted: true } : {}
13169
13487
  });
13170
- send(ws, { type: "memory.sage.forFile", payload: response });
13488
+ send(ws, { type: "memory.sage.forFile", payload: { response } });
13171
13489
  } catch (err) {
13172
13490
  send(ws, { type: "memory.sage.forFile", payload: { error: errMessage(err) } });
13173
13491
  }
@@ -13223,6 +13541,9 @@ async function handleMemoryRoute(ctx, ws, message) {
13223
13541
  case "memory.sage.recover":
13224
13542
  await handleSageRecover(ws, message, store);
13225
13543
  return true;
13544
+ case "memory.sage.listCandidates":
13545
+ await handleSageListCandidates(ws, message, store);
13546
+ return true;
13226
13547
  case "memory.sage.candidateResolve":
13227
13548
  await handleSageCandidateResolve(ws, message, store);
13228
13549
  return true;
@@ -13825,6 +14146,7 @@ import {
13825
14146
  finalizeTaskCompletion,
13826
14147
  getBoard as getBoard2,
13827
14148
  getKanbanQueueHealth as getKanbanQueueHealth2,
14149
+ kanbanQueueAnomalyCount as kanbanQueueAnomalyCount2,
13828
14150
  listBoards as listBoards3,
13829
14151
  reconcileKanbanBoard as reconcileKanbanBoard2,
13830
14152
  recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
@@ -13905,8 +14227,9 @@ function createKanbanSupervisor(deps2) {
13905
14227
  mode: config.recoveryMode ?? "auto",
13906
14228
  reason: "Kanban supervisor found an expired worker lease."
13907
14229
  }) : null;
13908
- if (recovered) health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
13909
- const anomalyCount = countAnomalies(health);
14230
+ if (recovered)
14231
+ health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
14232
+ const anomalyCount = kanbanQueueAnomalyCount2(health);
13910
14233
  const snapshot = {
13911
14234
  boardId: board.id,
13912
14235
  status: anomalyCount > 0 ? "attention" : "healthy",
@@ -14000,7 +14323,9 @@ function createKanbanSupervisor(deps2) {
14000
14323
  } else {
14001
14324
  const summaries = await listBoards3(resolveProjectRoot(deps2));
14002
14325
  pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
14003
- boards = (await Promise.all(summaries.map((summary) => getBoard2(resolveProjectRoot(deps2), summary.id)))).filter((board) => Boolean(board));
14326
+ boards = (await Promise.all(
14327
+ summaries.map((summary) => getBoard2(resolveProjectRoot(deps2), summary.id))
14328
+ )).filter((board) => Boolean(board));
14004
14329
  }
14005
14330
  const results = [];
14006
14331
  for (const board of boards) results.push(await auditBoard(board));
@@ -14103,13 +14428,10 @@ function dispatchRoute(routing) {
14103
14428
  ...routing.fallbackModels?.length ? { fallbackModels: routing.fallbackModels } : {}
14104
14429
  };
14105
14430
  }
14106
- function countAnomalies(health) {
14107
- return health.staleAssignments.count + health.heartbeatDue.count + health.counts.failed + health.counts.blocked;
14108
- }
14109
14431
  function healthSummary(health) {
14110
14432
  return [
14111
14433
  `${health.counts.running} running`,
14112
- `${health.counts.ready} ready`,
14434
+ `${health.counts.startable} ready`,
14113
14435
  `${health.counts.review} review`,
14114
14436
  `${health.counts.blocked} blocked`,
14115
14437
  `${health.counts.failed} failed`,
@@ -14863,7 +15185,7 @@ async function handleProcessRoute(ws, msg, handlers) {
14863
15185
  import * as fs14 from "node:fs/promises";
14864
15186
  import * as path16 from "node:path";
14865
15187
  import { DefaultSessionStore } from "@wrongstack/core/storage";
14866
- import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
15188
+ import { activateProjectStateGuard, resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
14867
15189
  function createProjectHandlers(ctx) {
14868
15190
  const sendTo = (ws, message) => {
14869
15191
  if (ctx.sendMessage) ctx.sendMessage(ws, message);
@@ -15028,6 +15350,7 @@ function createProjectHandlers(ctx) {
15028
15350
  try {
15029
15351
  await ctx.onSessionSwapped?.(next.id, identityTarget);
15030
15352
  await ctx.onBeforeSessionTodosReplaced?.(next.id, paths.projectSessions);
15353
+ await activateProjectStateGuard(resolved);
15031
15354
  } catch (err) {
15032
15355
  try {
15033
15356
  await ctx.onBeforeSessionTodosReplaced?.(previous.id, previousPaths.projectSessions);
@@ -15982,6 +16305,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
15982
16305
  "memory.sage.get",
15983
16306
  "memory.sage.graph",
15984
16307
  "memory.sage.list",
16308
+ "memory.sage.listCandidates",
15985
16309
  "memory.sage.listPage",
15986
16310
  "memory.sage.recover",
15987
16311
  "memory.sage.remember",
@@ -16261,6 +16585,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
16261
16585
  "memory.sage.get",
16262
16586
  "memory.sage.graph",
16263
16587
  "memory.sage.list",
16588
+ "memory.sage.listCandidates",
16264
16589
  "memory.sage.listPage",
16265
16590
  "memory.sage.recover",
16266
16591
  "memory.sage.remember",
@@ -16921,6 +17246,7 @@ function createSessionHandlers(ctx) {
16921
17246
  ctx.abortActiveRun?.(current2.id);
16922
17247
  } catch {
16923
17248
  }
17249
+ await ctx.context.flushConversationJournal?.().catch(() => void 0);
16924
17250
  await finalizeSession(current2);
16925
17251
  }
16926
17252
  ctx.setSession(next);
@@ -17451,85 +17777,55 @@ function createSessionHandlers(ctx) {
17451
17777
  // src/server/agent-roster-handlers.ts
17452
17778
  import {
17453
17779
  applyProjectAgentConfig,
17454
- buildConsolidationInstruction,
17455
17780
  captureLearnedFromAgentOutputDetailed,
17456
17781
  clearProjectAgentConsolidated,
17782
+ clearProjectSkillAugmentation,
17457
17783
  createProjectAgent,
17784
+ DEFAULT_EAGER_SKILL_LIMIT,
17458
17785
  detectLearnedConflicts,
17786
+ evaluateAutoOptimize,
17459
17787
  FLEET_ROSTER,
17460
17788
  getProjectAgentLearnStats,
17461
17789
  isConsolidated,
17462
17790
  listProjectAgentLearnedEntries,
17463
17791
  listProjectAgentRoles,
17792
+ listProjectSkillAugmentations,
17464
17793
  loadConsolidationMetadata,
17465
17794
  loadProjectAgentConfig,
17466
17795
  loadProjectAgentConsolidated,
17467
17796
  loadProjectAgentIdentity,
17468
17797
  loadProjectAgentLearned,
17469
17798
  loadProjectAgentProfile,
17799
+ loadProjectSkillAugmentation,
17800
+ loadSkillAffinity,
17801
+ optimizeProjectAgentLearning,
17802
+ rankRoleSkills,
17803
+ readQuarantinedDirectives,
17804
+ readRawLearnedEntries,
17470
17805
  resetProjectAgentIdentity,
17806
+ resolveAutoOptimizePolicy,
17807
+ resolveRoleSkillCandidates,
17471
17808
  saveProjectAgentConsolidated,
17809
+ saveProjectSkillAugmentation,
17810
+ scoreSkillAffinity,
17811
+ setSkillPinned,
17472
17812
  slugifyProjectAgentRole,
17473
17813
  updateProjectAgentConfig,
17474
17814
  updateProjectAgentIdentity,
17475
17815
  updateProjectAgentLearned,
17476
17816
  updateProjectAgentLearningPolicy
17477
17817
  } from "@wrongstack/core/coordination";
17478
- import { isTextBlock } from "@wrongstack/core/types";
17479
- var CONSOLIDATION_MAX_TOKENS = 8e3;
17480
- var CONSOLIDATION_TIMEOUT_MS = 12e4;
17481
17818
  var AgentRosterWSHandler = class {
17482
17819
  getProjectRoot;
17483
17820
  getLlm;
17484
17821
  broadcast;
17822
+ getAutoOptimizeSettings;
17485
17823
  constructor(opts) {
17486
17824
  this.getProjectRoot = typeof opts.projectRoot === "function" ? opts.projectRoot : () => opts.projectRoot;
17487
17825
  this.getLlm = opts.getLlm ?? (() => void 0);
17488
17826
  this.broadcast = opts.broadcast ?? (() => {
17489
17827
  });
17490
- }
17491
- /**
17492
- * Run the consolidation LLM synthesis headlessly and return the cleaned
17493
- * document text. Returns undefined when no LLM is available so the caller
17494
- * can fall back to the instruction-only path.
17495
- */
17496
- async synthesizeConsolidation(instruction) {
17497
- const llm = this.getLlm();
17498
- if (!llm) return void 0;
17499
- const req = {
17500
- model: llm.model,
17501
- system: [
17502
- {
17503
- type: "text",
17504
- text: "You are a precise technical editor. You consolidate an AI agent role's captured learning entries into a single, durable, role-scoped instruction document. Output ONLY the consolidated markdown document \u2014 no preamble, no code fences, no commentary about the consolidation process."
17505
- }
17506
- ],
17507
- messages: [{ role: "user", content: instruction }],
17508
- maxTokens: CONSOLIDATION_MAX_TOKENS
17509
- };
17510
- const timer = new AbortController();
17511
- let timedOut = false;
17512
- const to = setTimeout(() => {
17513
- timedOut = true;
17514
- timer.abort(new Error("consolidation timeout"));
17515
- }, CONSOLIDATION_TIMEOUT_MS);
17516
- to.unref();
17517
- try {
17518
- const res = await llm.provider.complete(req, { signal: timer.signal });
17519
- const text = res.content.filter(isTextBlock).map((block) => block.text).join("\n").trim();
17520
- const wholeDocFence = /^```(?:markdown|md)?[^\n]*\n([\s\S]*?)\n?```\s*$/i;
17521
- const wrapped = wholeDocFence.exec(text);
17522
- const inner = wrapped?.[1];
17523
- const unfenced = inner !== void 0 ? inner.trim() : text;
17524
- return { content: unfenced, model: llm.model };
17525
- } catch (err) {
17526
- if (timedOut) {
17527
- throw new Error(`consolidation timed out after ${CONSOLIDATION_TIMEOUT_MS}ms`);
17528
- }
17529
- throw err;
17530
- } finally {
17531
- clearTimeout(to);
17532
- }
17828
+ this.getAutoOptimizeSettings = opts.getAutoOptimizeSettings;
17533
17829
  }
17534
17830
  /** Handle an incoming client message. Returns a response payload. */
17535
17831
  async handleMessage(_ws, type, payload) {
@@ -17734,70 +18030,47 @@ ${String(p.content ?? "")}`;
17734
18030
  const conflicts = detectLearnedConflicts(projectRoot);
17735
18031
  return { type, payload: { conflicts } };
17736
18032
  }
17737
- // ── Consolidate learned entries (headless LLM synthesis) ──────────
17738
- // Runs the whole optimization on the server: read raw entries
17739
- // synthesize with the active model write consolidated.md +
17740
- // consolidation.json. No chat round-trip. When no LLM is available the
17741
- // handler degrades to returning the instruction so a caller (e.g. the
17742
- // chat agent) can still perform the consolidation manually.
17743
- case "agent-roster.consolidate": {
18033
+ // ── Optimize: distil captures into skill addenda + a consolidated doc,
18034
+ // then archive and reset the raw buffer. Shared implementation with the
18035
+ // CLI (`optimizeProjectAgentLearning`) so both surfaces persist the same
18036
+ // artifacts instead of the CLI producing markdown nobody saved.
18037
+ case "agent-roster.consolidate":
18038
+ case "agent-roster.optimize": {
17744
18039
  if (!role) return { type, payload: { error: "role required" } };
17745
- const { instruction, rawEntries, hasExistingConsolidation } = buildConsolidationInstruction(role, projectRoot);
17746
- if (rawEntries.length === 0) {
18040
+ const hasExistingConsolidation = isConsolidated(role, projectRoot);
18041
+ const pending = readRawLearnedEntries(role, projectRoot);
18042
+ if (pending.length === 0) {
17747
18043
  return {
17748
18044
  type: "agent-roster.consolidate",
17749
18045
  payload: {
17750
18046
  role,
17751
18047
  consolidated: false,
17752
18048
  rawEntryCount: 0,
18049
+ skills: [],
17753
18050
  hasExistingConsolidation,
17754
18051
  currentStats: getProjectAgentLearnStats(role, projectRoot)
17755
18052
  }
17756
18053
  };
17757
18054
  }
17758
- let synth;
17759
- try {
17760
- synth = await this.synthesizeConsolidation(instruction);
17761
- } catch (err) {
17762
- return {
17763
- type: "agent-roster.consolidate",
17764
- payload: {
17765
- role,
17766
- consolidated: false,
17767
- rawEntryCount: rawEntries.length,
17768
- hasExistingConsolidation,
17769
- currentStats: getProjectAgentLearnStats(role, projectRoot),
17770
- error: err instanceof Error ? err.message : "consolidation failed"
17771
- }
17772
- };
17773
- }
17774
- if (synth && synth.content.length > 0) {
17775
- let stats;
17776
- let metadata;
17777
- try {
17778
- saveProjectAgentConsolidated(role, synth.content, projectRoot, {
17779
- trigger: "manual",
17780
- model: synth.model
17781
- });
17782
- stats = getProjectAgentLearnStats(role, projectRoot);
17783
- metadata = loadConsolidationMetadata(role, projectRoot);
17784
- } catch (err) {
17785
- return {
17786
- type: "agent-roster.consolidate",
17787
- payload: {
17788
- role,
17789
- consolidated: false,
17790
- rawEntryCount: rawEntries.length,
17791
- hasExistingConsolidation,
17792
- currentStats: getProjectAgentLearnStats(role, projectRoot),
17793
- error: err instanceof Error ? err.message : "failed to persist consolidation"
17794
- }
17795
- };
17796
- }
18055
+ const llm = this.getLlm();
18056
+ const result = await optimizeProjectAgentLearning(role, projectRoot, {
18057
+ ...llm ? { llm } : {},
18058
+ trigger: "manual"
18059
+ });
18060
+ const currentStats = getProjectAgentLearnStats(role, projectRoot);
18061
+ const metadata = loadConsolidationMetadata(role, projectRoot);
18062
+ const basePayload = {
18063
+ role,
18064
+ rawEntryCount: result.rawEntryCount,
18065
+ skills: result.skills,
18066
+ hasExistingConsolidation,
18067
+ currentStats
18068
+ };
18069
+ if (result.status === "optimized") {
17797
18070
  try {
17798
18071
  this.broadcast({
17799
18072
  type: "agent-roster.updated",
17800
- payload: { role, reason: "consolidated", currentStats: stats, metadata }
18073
+ payload: { role, reason: "consolidated", currentStats, metadata }
17801
18074
  });
17802
18075
  } catch (e) {
17803
18076
  console.warn(
@@ -17813,44 +18086,111 @@ ${String(p.content ?? "")}`;
17813
18086
  return {
17814
18087
  type: "agent-roster.consolidate",
17815
18088
  payload: {
17816
- role,
18089
+ ...basePayload,
17817
18090
  consolidated: true,
17818
- rawEntryCount: rawEntries.length,
17819
- content: synth.content,
17820
- model: synth.model,
17821
- currentStats: stats,
18091
+ content: result.content,
18092
+ model: result.model,
18093
+ pruned: result.pruned,
17822
18094
  metadata
17823
18095
  }
17824
18096
  };
17825
18097
  }
17826
- if (synth) {
17827
- return {
17828
- type: "agent-roster.consolidate",
17829
- payload: {
17830
- role,
17831
- consolidated: false,
17832
- emptySynthesis: true,
17833
- model: synth.model,
17834
- rawEntryCount: rawEntries.length,
17835
- hasExistingConsolidation,
17836
- currentStats: getProjectAgentLearnStats(role, projectRoot)
17837
- }
17838
- };
17839
- }
17840
18098
  return {
17841
18099
  type: "agent-roster.consolidate",
17842
18100
  payload: {
17843
- role,
18101
+ ...basePayload,
17844
18102
  consolidated: false,
17845
- instruction,
17846
- rawEntryCount: rawEntries.length,
17847
- hasExistingConsolidation,
17848
- currentStats: getProjectAgentLearnStats(role, projectRoot),
17849
- // Instruction for the leader agent to execute the consolidation
17850
- leaderInstruction: `Optimize what the "${role}" agent has learned. Read its raw learned entries, synthesize them into a single narrowly-scoped document preserving every fact, and save the result. The instruction text contains the full details and raw entries.`
18103
+ ...result.status === "empty-synthesis" ? { emptySynthesis: true, model: result.model } : {},
18104
+ ...result.status === "failed" ? { error: result.error } : {},
18105
+ ...result.status === "no-llm" ? {
18106
+ instruction: result.instruction,
18107
+ leaderInstruction: `Optimize what the "${role}" agent has learned. Read its raw learned entries, synthesize them into a single narrowly-scoped document preserving every fact, and save the result. The instruction text contains the full details and raw entries.`
18108
+ } : {}
17851
18109
  }
17852
18110
  };
17853
18111
  }
18112
+ // ── Automatic-optimization status ─────────────────────────────────
18113
+ // Read-only: says whether the background scheduler considers each role
18114
+ // eligible right now, and why not when it does not. Surfacing the reason
18115
+ // is what keeps "nothing happened" from looking like a broken feature.
18116
+ case "agent-roster.auto-optimize-status": {
18117
+ const policy = resolveAutoOptimizePolicy(this.getAutoOptimizeSettings?.() ?? void 0);
18118
+ const roles = role ? [role] : listProjectAgentRoles(projectRoot);
18119
+ return {
18120
+ type,
18121
+ payload: {
18122
+ policy,
18123
+ roles: roles.map((current2) => {
18124
+ try {
18125
+ const decision = evaluateAutoOptimize(current2, projectRoot, policy);
18126
+ return { role: current2, ...decision };
18127
+ } catch {
18128
+ return { role: current2, eligible: false, reason: "disabled" };
18129
+ }
18130
+ })
18131
+ }
18132
+ };
18133
+ }
18134
+ // ── Skill layer: what this project has developed for each role skill ──
18135
+ case "agent-roster.skills": {
18136
+ if (!role) return { type, payload: { error: "role required" } };
18137
+ const candidates = resolveRoleSkillCandidates(role, projectRoot);
18138
+ const developed = listProjectSkillAugmentations(role, projectRoot);
18139
+ const affinity = loadSkillAffinity(role, projectRoot);
18140
+ const eager = new Set(rankRoleSkills(role, candidates, projectRoot));
18141
+ return {
18142
+ type,
18143
+ payload: {
18144
+ role,
18145
+ eagerLimit: DEFAULT_EAGER_SKILL_LIMIT,
18146
+ skills: candidates.map((skill) => ({
18147
+ skill,
18148
+ developed: developed.includes(skill),
18149
+ affinity: affinity.entries[skill] ?? null,
18150
+ score: scoreSkillAffinity(affinity.entries[skill]) + (developed.includes(skill) ? 1 : 0),
18151
+ eager: eager.has(skill)
18152
+ }))
18153
+ }
18154
+ };
18155
+ }
18156
+ // ── Directives the loop stopped believing ─────────────────────────────
18157
+ case "agent-roster.quarantine": {
18158
+ if (!role) return { type, payload: { error: "role required" } };
18159
+ return {
18160
+ type,
18161
+ payload: { role, retired: readQuarantinedDirectives(role, projectRoot) }
18162
+ };
18163
+ }
18164
+ case "agent-roster.read-skill": {
18165
+ const skill = typeof p.skill === "string" ? p.skill : "";
18166
+ if (!role || !skill) return { type, payload: { error: "role and skill required" } };
18167
+ return {
18168
+ type,
18169
+ payload: { role, skill, content: loadProjectSkillAugmentation(role, skill, projectRoot) }
18170
+ };
18171
+ }
18172
+ case "agent-roster.save-skill": {
18173
+ const skill = typeof p.skill === "string" ? p.skill : "";
18174
+ if (!role || !skill || typeof p.content !== "string") {
18175
+ return { type, payload: { error: "role, skill and content required" } };
18176
+ }
18177
+ const savedPath = saveProjectSkillAugmentation(role, skill, p.content, projectRoot);
18178
+ return { type, payload: { role, skill, path: savedPath, success: true } };
18179
+ }
18180
+ case "agent-roster.clear-skill": {
18181
+ const skill = typeof p.skill === "string" ? p.skill : "";
18182
+ if (!role) return { type, payload: { error: "role required" } };
18183
+ clearProjectSkillAugmentation(role, skill || void 0, projectRoot);
18184
+ return { type, payload: { role, skill: skill || null, success: true } };
18185
+ }
18186
+ case "agent-roster.pin-skill": {
18187
+ const skill = typeof p.skill === "string" ? p.skill : "";
18188
+ if (!role || !skill || typeof p.pinned !== "boolean") {
18189
+ return { type, payload: { error: "role, skill and boolean pinned required" } };
18190
+ }
18191
+ const affinity = setSkillPinned(role, skill, p.pinned, projectRoot);
18192
+ return { type, payload: { role, skill, pinned: p.pinned, affinity, success: true } };
18193
+ }
17854
18194
  // ── Save consolidated document ────────────────────────────────────
17855
18195
  case "agent-roster.save-consolidated": {
17856
18196
  if (!role || typeof p.content !== "string") {
@@ -19739,6 +20079,7 @@ function registerSetupEventsStatusWatcher(deps2) {
19739
20079
  const logWatcherMetricsEnabled = shouldLogWatcherStats();
19740
20080
  const logWatcherMetrics = () => logFileWatcherMetrics(watcherMetrics);
19741
20081
  const metricsInterval = logWatcherMetricsEnabled ? setInterval(logWatcherMetrics, 6e4) : void 0;
20082
+ metricsInterval?.unref?.();
19742
20083
  const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
19743
20084
  broadcast2(clients, { type: "client.status_update", payload: statusData });
19744
20085
  if (watcherMetrics) {
@@ -20958,14 +21299,7 @@ import {
20958
21299
  toErrorMessage as toErrorMessage9
20959
21300
  } from "@wrongstack/core/utils";
20960
21301
  import { makeLightSubagentFactory } from "@wrongstack/runtime";
20961
- import {
20962
- createSageContextMonitorMiddleware,
20963
- createSageToolCallMiddleware,
20964
- createSageTurnMiddleware,
20965
- getSageRetrieval,
20966
- getSageService,
20967
- InjectionTracker
20968
- } from "@wrongstack/sage";
21302
+ import { getSageService, setupSage } from "@wrongstack/sage";
20969
21303
 
20970
21304
  // src/server/discover-mailbox-bridge.ts
20971
21305
  import { spawn as spawn2 } from "node:child_process";
@@ -21768,6 +22102,7 @@ var WorktreeWebSocketHandler = class {
21768
22102
  this.broadcast(this.stateMessage());
21769
22103
  if (this.broadcastInterval) return;
21770
22104
  this.broadcastInterval = setInterval(() => this.broadcast(this.stateMessage()), 2e3);
22105
+ this.broadcastInterval.unref?.();
21771
22106
  }
21772
22107
  stopBroadcast() {
21773
22108
  this.broadcast(this.stateMessage());
@@ -21819,62 +22154,15 @@ async function createAgentServices(input) {
21819
22154
  const collabPause = collabPauseMiddleware(collabBus, { logger });
21820
22155
  pipelines.toolCall.prepend(collabPause);
21821
22156
  installDesignStudioMiddleware({ pipelines, ctx: context });
21822
- const memoryRetrieval = getSageRetrieval(memoryStore);
21823
- if (config.features.memory !== false && config.Sage?.enabled !== false && memoryRetrieval) {
21824
- const sageInjectionTracker = new InjectionTracker();
21825
- const getSageSessionId = () => input.sessionGetter().id;
21826
- if (config.Sage?.inject?.toolResults !== false) {
21827
- pipelines.toolCall.use(
21828
- createSageToolCallMiddleware({
21829
- memory: memoryRetrieval,
21830
- maxHintsPerTool: config.Sage?.inject?.maxHintsPerTool,
21831
- maxCharsPerTool: config.Sage?.inject?.maxCharsPerTool,
21832
- taskAware: config.Sage?.inject?.taskAware,
21833
- minScore: config.Sage?.inject?.minScore,
21834
- minImportance: config.Sage?.inject?.minImportance,
21835
- // Forward the explicit relation floor so an operator-configured
21836
- // `Sage.inject.relationFloor` is honored in WebUI sessions. Without
21837
- // this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
21838
- // is the CLI default but masks operator overrides.
21839
- relationFloor: config.Sage?.inject?.relationFloor,
21840
- repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
21841
- verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
21842
- triggers: config.Sage?.inject?.triggers,
21843
- // Resolve the live session so retrieval and cooldown are session-scoped
21844
- // (matching the CLI wiring in wiring/sage.ts). Without this, the
21845
- // middleware falls back to ctx.session.id for cooldown but passes
21846
- // undefined to retrieval, causing owned session-scoped memories to be
21847
- // silently excluded from tool-call injection.
21848
- getSessionId: getSageSessionId,
21849
- tracker: sageInjectionTracker,
21850
- events
21851
- })
21852
- );
21853
- }
21854
- if (config.Sage?.inject?.turnContext === true) {
21855
- pipelines.request.use(
21856
- createSageTurnMiddleware({
21857
- memory: memoryRetrieval,
21858
- maxMemories: config.Sage?.inject?.maxTurnMemories,
21859
- maxChars: config.Sage?.inject?.maxCharsPerTurn,
21860
- minScore: config.Sage?.inject?.minScore,
21861
- // CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
21862
- // value drives both runtimes instead of silently falling back to the
21863
- // 0.3 default. The undefined case keeps the middleware's own default.
21864
- metadataWeight: config.Sage?.retrieval?.metadataWeight,
21865
- getSessionId: getSageSessionId,
21866
- tracker: sageInjectionTracker
21867
- })
21868
- );
21869
- }
21870
- pipelines.request.use(
21871
- createSageContextMonitorMiddleware({
21872
- tracker: sageInjectionTracker,
21873
- events,
21874
- getSessionId: getSageSessionId
21875
- })
21876
- );
21877
- }
22157
+ const runSageSessionHygiene = setupSage({
22158
+ config,
22159
+ pipelines,
22160
+ memoryStore,
22161
+ logger,
22162
+ events,
22163
+ getSessionId: () => input.sessionGetter().id,
22164
+ projectRoot
22165
+ });
21878
22166
  const codebaseIndexing = setupWebUICodebaseIndexing({
21879
22167
  config,
21880
22168
  context,
@@ -21986,7 +22274,12 @@ async function createAgentServices(input) {
21986
22274
  confirmAwaiter: void 0,
21987
22275
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
21988
22276
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
21989
- tracer: void 0
22277
+ tracer: void 0,
22278
+ // Off unless the operator opts in. The WebUI drives the same agent as the
22279
+ // CLI, so it must resolve this identically — a surface-dependent gate
22280
+ // would mean the same repo governs under `wstack` and not in the browser.
22281
+ // See packages/cli/src/wiring/pipeline.ts.
22282
+ requireKanbanGovernance: config.tools?.kanbanGovernance ?? DEFAULT_TOOLS_CONFIG.kanbanGovernance
21990
22283
  });
21991
22284
  input.installToolBoundary?.(pipelines);
21992
22285
  const webuiLogger = container.resolve(TOKENS.Logger);
@@ -22006,6 +22299,7 @@ async function createAgentServices(input) {
22006
22299
  providers: providerRegistry,
22007
22300
  events,
22008
22301
  pipelines,
22302
+ refreshSystemPrompt: true,
22009
22303
  context,
22010
22304
  maxIterations: config.tools?.maxIterations ?? DEFAULT_TOOLS_CONFIG.maxIterations,
22011
22305
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
@@ -22273,6 +22567,7 @@ async function createAgentServices(input) {
22273
22567
  terminalHandler,
22274
22568
  collabHandler,
22275
22569
  disposeRealtimeHandlers,
22570
+ runSageSessionHygiene,
22276
22571
  updateAutoCompactionMaxContext
22277
22572
  };
22278
22573
  }
@@ -22429,6 +22724,7 @@ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES)
22429
22724
 
22430
22725
  // src/server/message-dispatcher.ts
22431
22726
  import path23 from "node:path";
22727
+ import { planTool, taskTool, todoTool } from "@wrongstack/tools";
22432
22728
  function createMessageDispatcher(opts) {
22433
22729
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
22434
22730
  function makeWorklistContext() {
@@ -22440,7 +22736,20 @@ function createMessageDispatcher(opts) {
22440
22736
  },
22441
22737
  send: (w, m) => send(w, m),
22442
22738
  broadcast: (m) => broadcast(state.getClients(), m),
22443
- replaceTodos: (todos) => deps2.context.state.replaceTodos(todos)
22739
+ replaceTodos: (todos) => deps2.context.state.replaceTodos(todos),
22740
+ mutateTodos: async (todos) => {
22741
+ const result = await todoTool.execute({ todos }, deps2.context, {
22742
+ signal: AbortSignal.timeout(3e4)
22743
+ });
22744
+ return {
22745
+ todos: [...deps2.context.todos],
22746
+ ...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
22747
+ };
22748
+ },
22749
+ mutateTaskStatus: async (id, status) => taskTool.execute({ action: "status", id, status }, deps2.context, {
22750
+ signal: AbortSignal.timeout(3e4)
22751
+ }),
22752
+ mutatePlan: async (operation) => planTool.execute(operation, deps2.context, { signal: AbortSignal.timeout(3e4) })
22444
22753
  };
22445
22754
  }
22446
22755
  function makeSkillsContext() {
@@ -22652,6 +22961,7 @@ function createMessageDispatcher(opts) {
22652
22961
  agentRoster: {
22653
22962
  rosterHandler: new AgentRosterWSHandler({
22654
22963
  projectRoot: state.getProjectRoot,
22964
+ getAutoOptimizeSettings: () => state.getConfig().fleet?.learning?.autoOptimize,
22655
22965
  getLlm: () => {
22656
22966
  const ctx = deps2.agent.ctx;
22657
22967
  return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
@@ -22737,15 +23047,9 @@ import {
22737
23047
  makeMailInboxTool,
22738
23048
  makeMailSendTool
22739
23049
  } from "@wrongstack/core/coordination";
22740
- import {
22741
- DefaultPromptLoader,
22742
- DefaultSkillLoader
22743
- } from "@wrongstack/core/execution";
22744
- import {
22745
- EventBus,
22746
- TOKENS as TOKENS2
22747
- } from "@wrongstack/core/kernel";
23050
+ import { DefaultPromptLoader, DefaultSkillLoader } from "@wrongstack/core/execution";
22748
23051
  import { DefaultTokenCounter } from "@wrongstack/core/infrastructure";
23052
+ import { EventBus, TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
22749
23053
  import { DefaultModelsRegistry, DefaultModeStore } from "@wrongstack/core/models";
22750
23054
  import { ProviderRegistry, ToolRegistry } from "@wrongstack/core/registry";
22751
23055
  import { SkillInstaller } from "@wrongstack/core/skills";
@@ -23403,6 +23707,7 @@ async function createPreContextServices(input) {
23403
23707
  modeId,
23404
23708
  modePrompt,
23405
23709
  modelCapabilities: () => modelCapabilitiesRef.current,
23710
+ tokenSavingMode: config.features.tokenSavingMode,
23406
23711
  instructionPaths: {
23407
23712
  globalDir: wpaths.globalInstructions,
23408
23713
  projectDir: wpaths.inProjectInstructions,
@@ -23423,7 +23728,8 @@ async function createPreContextServices(input) {
23423
23728
  const systemPrompt = await systemPromptBuilder.build({
23424
23729
  cwd: projectRoot,
23425
23730
  projectRoot,
23426
- tools: toolRegistry.list(),
23731
+ tools: toolRegistry.listForProvider(),
23732
+ catalogTools: toolRegistry.list(),
23427
23733
  provider: config.provider,
23428
23734
  model: config.model,
23429
23735
  onlineAgents
@@ -23441,6 +23747,7 @@ async function createPreContextServices(input) {
23441
23747
  projectRoot,
23442
23748
  model: config.model
23443
23749
  });
23750
+ context.meta["promptOnlineAgents"] = onlineAgents;
23444
23751
  const initialContextPolicy = resolveContextWindowPolicy3(config.context);
23445
23752
  context.meta["contextWindowMode"] = initialContextPolicy.id;
23446
23753
  context.meta["contextWindowPolicy"] = initialContextPolicy;
@@ -23513,6 +23820,7 @@ function createModeHandlers(context) {
23513
23820
  modeId: id,
23514
23821
  modePrompt,
23515
23822
  modelCapabilities: context.modelCapabilities,
23823
+ tokenSavingMode: config.features?.tokenSavingMode,
23516
23824
  instructionPaths: {
23517
23825
  globalDir: paths.globalInstructions,
23518
23826
  projectDir: paths.inProjectInstructions,
@@ -23522,7 +23830,8 @@ function createModeHandlers(context) {
23522
23830
  context.context.systemPrompt = await builder.build({
23523
23831
  cwd: context.projectRoot,
23524
23832
  projectRoot: context.projectRoot,
23525
- tools: context.toolRegistry.list(),
23833
+ tools: context.toolRegistry.listForProvider(),
23834
+ catalogTools: context.toolRegistry.list(),
23526
23835
  provider: config.provider,
23527
23836
  model: config.model
23528
23837
  });
@@ -23862,11 +24171,11 @@ function buildRoutes(state, deps2, cb) {
23862
24171
  }
23863
24172
 
23864
24173
  // src/server/server-runtime.ts
23865
- import * as path28 from "node:path";
23866
24174
  import { createRequire as createRequire4 } from "node:module";
24175
+ import * as path28 from "node:path";
23867
24176
  import { fileURLToPath } from "node:url";
23868
- import { WebSocketServer } from "ws";
23869
24177
  import { toErrorMessage as toErrorMessage12 } from "@wrongstack/core/utils";
24178
+ import { WebSocketServer } from "ws";
23870
24179
  async function resolvePorts(opts) {
23871
24180
  const surface = opts.surface ?? "webui";
23872
24181
  const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
@@ -24797,15 +25106,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
24797
25106
  eternalSubscription = null;
24798
25107
  }
24799
25108
  codebaseIndexing.dispose();
24800
- if (config.Sage?.enabled !== false && config.Sage?.hygiene?.autoAfterSession !== false) {
24801
- const candidate = memoryStore;
24802
- await candidate.hygiene?.({
24803
- retentionDays: config.Sage?.hygiene?.retentionDays,
24804
- archiveLowConfidenceAfterDays: config.Sage?.hygiene?.archiveLowConfidenceAfterDays
24805
- }).catch(
24806
- (err) => logger.warn(`sage session hygiene failed: ${toErrorMessage13(err)}`)
24807
- );
24808
- }
25109
+ await agentServices.runSageSessionHygiene().catch((err) => logger.warn(`sage session hygiene failed: ${toErrorMessage13(err)}`));
24809
25110
  await memoryStore.dispose().catch(
24810
25111
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
24811
25112
  );