@wrongstack/webui-server 0.303.0 → 0.305.1

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) {
@@ -2159,6 +2163,7 @@ var CollaborationWebSocketHandler = class {
2159
2163
  this.broadcast(sessionId, this.stateMessage(sessionId));
2160
2164
  }
2161
2165
  }, 2e3);
2166
+ this.broadcastInterval.unref?.();
2162
2167
  }
2163
2168
  stopBroadcast() {
2164
2169
  if (this.broadcastInterval) {
@@ -7131,13 +7136,18 @@ var GoalWebSocketHandler = class {
7131
7136
  const { execFile: execFile2 } = await import("node:child_process");
7132
7137
  const result = await new Promise((resolve16) => {
7133
7138
  const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
7134
- execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
7135
- if (err && err.code === "ENOENT") {
7136
- resolve16("[verify] tsc not found \u2014 skipping");
7137
- 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);
7138
7149
  }
7139
- resolve16(stdout + stderr);
7140
- });
7150
+ );
7141
7151
  });
7142
7152
  if (result.includes("[verify]") || result.trim().length === 0) {
7143
7153
  return { ok: true };
@@ -7439,6 +7449,7 @@ ${result_.finalText.slice(0, 2e3)}`
7439
7449
  if (progress) this.broadcast({ type: "goal.progress", payload: progress });
7440
7450
  this.broadcastState();
7441
7451
  }, 2e3);
7452
+ this.broadcastInterval.unref?.();
7442
7453
  }
7443
7454
  stopBroadcast() {
7444
7455
  if (this.broadcastInterval) {
@@ -7563,8 +7574,9 @@ ${result_.finalText.slice(0, 2e3)}`
7563
7574
  }
7564
7575
  broadcast(msg) {
7565
7576
  const data = JSON.stringify(msg);
7577
+ const frameBytes = Buffer.byteLength(data, "utf8");
7566
7578
  for (const client of this.clients) {
7567
- sendSerialized(client.ws, data);
7579
+ sendSerialized(client.ws, data, frameBytes);
7568
7580
  }
7569
7581
  }
7570
7582
  send(client, msg) {
@@ -9467,7 +9479,19 @@ function isTrustedLoopbackOrigin(origin, hostHeader) {
9467
9479
  if (!host) return false;
9468
9480
  const hostUrl = new URL(`${url.protocol}//${host}`);
9469
9481
  if (!isLoopbackHostname(hostUrl.hostname)) return false;
9470
- 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);
9471
9495
  } catch {
9472
9496
  return false;
9473
9497
  }
@@ -9572,13 +9596,15 @@ function verifyClient(input) {
9572
9596
  try {
9573
9597
  const { hostname: originHostname } = new URL(origin);
9574
9598
  if (isLoopbackHostname(originHostname)) {
9575
- if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
9599
+ if (requireToken || !isLoopbackBind(wsHost)) {
9600
+ return cookieTokenOk && (originMatchesHost(origin, hostHeader) || Boolean(allowCrossPortLoopbackCookie));
9601
+ }
9576
9602
  if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
9577
9603
  return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
9578
9604
  }
9579
9605
  return true;
9580
9606
  }
9581
- return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
9607
+ return cookieTokenOk && originMatchesHost(origin, hostHeader) || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
9582
9608
  } catch {
9583
9609
  return false;
9584
9610
  }
@@ -10468,7 +10494,8 @@ async function handleIntrospectionRoute(ctx, ws, message) {
10468
10494
  switch (message.type) {
10469
10495
  case "diag.get": {
10470
10496
  if (!sessionAllowed(ctx, ws, message)) return true;
10471
- const tools = ctx.agent.tools.list();
10497
+ const registry = ctx.agent.tools;
10498
+ const tools = registry.listForProvider?.() ?? registry.list();
10472
10499
  ctx.send(ws, {
10473
10500
  type: "diag.get",
10474
10501
  payload: {
@@ -10551,6 +10578,7 @@ async function handleIntrospectionRoute(ctx, ws, message) {
10551
10578
  description: tool.description ?? "",
10552
10579
  params: schema.properties ? Object.keys(schema.properties) : [],
10553
10580
  disabled: registry.isDisabled?.(tool.name) ?? false,
10581
+ direct: registry.isExposedToProvider?.(tool.name) ?? true,
10554
10582
  mutating: !!tool.mutating,
10555
10583
  permission: tool.permission ?? "auto"
10556
10584
  };
@@ -10846,7 +10874,6 @@ async function handleKanbanHostRoute(ws, msg, handlers) {
10846
10874
  import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core/tasking";
10847
10875
  import {
10848
10876
  addCheckToTask,
10849
- addColumn,
10850
10877
  addGoalMetricToTask,
10851
10878
  addNoteToTask,
10852
10879
  addTask,
@@ -10861,7 +10888,10 @@ import {
10861
10888
  getKanbanOrchestrationSnapshot,
10862
10889
  getKanbanQueueHealth,
10863
10890
  getTaskChain,
10891
+ hasKanbanQueueAnomalies,
10892
+ kanbanQueueAnomalyCount,
10864
10893
  listBoards as listBoards2,
10894
+ listBoardHistory,
10865
10895
  listReadyTasks,
10866
10896
  mergeTasks,
10867
10897
  moveTask,
@@ -10870,7 +10900,6 @@ import {
10870
10900
  recoverStaleTaskAssignments,
10871
10901
  releaseTaskClaim,
10872
10902
  removeBoard,
10873
- removeColumn,
10874
10903
  setTaskChain,
10875
10904
  splitTask,
10876
10905
  syncBoardFromTaskGraph,
@@ -10882,14 +10911,16 @@ import {
10882
10911
  updateTask as updateTask2
10883
10912
  } from "@wrongstack/kanban";
10884
10913
 
10885
- // src/server/kanban-decomposition-routes.ts
10914
+ // src/server/kanban-contract-routes.ts
10886
10915
  import {
10887
- listBoards,
10888
- resolveDecompositionProposal,
10889
- updateTask,
10890
- verifyTaskCompletion
10916
+ addContractEdge,
10917
+ configureContractGraph,
10918
+ evaluateTaskContractGraph,
10919
+ getContractGraph,
10920
+ removeContractEdge,
10921
+ removeContractNode,
10922
+ upsertContractNode
10891
10923
  } from "@wrongstack/kanban";
10892
- import { recordKanbanVerificationEvidence as recordKanbanVerificationEvidence2 } from "@wrongstack/tools";
10893
10924
 
10894
10925
  // src/server/kanban-route-helpers.ts
10895
10926
  import { touchKanbanPresence } from "@wrongstack/kanban";
@@ -10941,7 +10972,150 @@ function findTask2(tasks, taskId) {
10941
10972
  return tasks.find((task) => task.id === taskId || task.id.startsWith(taskId));
10942
10973
  }
10943
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
+
10944
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";
10945
11119
  async function handleKanbanDecompositionRoute(ws, type, payload, ctx) {
10946
11120
  switch (type) {
10947
11121
  case "kanban.decomposition.approve":
@@ -11194,6 +11368,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
11194
11368
  const type = msg.type;
11195
11369
  try {
11196
11370
  if (await handleKanbanDecompositionRoute(ws, type, payload, ctx)) return true;
11371
+ if (await handleKanbanContractRoute(ws, type, payload, ctx)) return true;
11197
11372
  if (await handleKanbanTaskRoute(ws, type, payload, ctx)) return true;
11198
11373
  switch (type) {
11199
11374
  case "kanban.list": {
@@ -11232,7 +11407,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11232
11407
  fail(ws, type, "boardId required");
11233
11408
  return true;
11234
11409
  }
11235
- 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
+ );
11236
11418
  return true;
11237
11419
  }
11238
11420
  case "kanban.supervisor.status":
@@ -11262,16 +11444,16 @@ async function handleKanbanRoute(ws, msg, ctx) {
11262
11444
  reason: "On-demand standalone Kanban supervisor audit."
11263
11445
  }) : null;
11264
11446
  if (recovered) health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
11265
- const anomalyCount = health.staleAssignments.count + health.dependencyBlocked.count + health.failedRetryable.count + health.counts.failed + health.counts.blocked;
11447
+ const anomalyCount = kanbanQueueAnomalyCount(health);
11266
11448
  ok(ws, type, {
11267
11449
  boardId,
11268
- status: board.supervisor?.enabled === false ? "disabled" : anomalyCount ? "attention" : "healthy",
11450
+ status: board.supervisor?.enabled === false ? "disabled" : hasKanbanQueueAnomalies(health) ? "attention" : "healthy",
11269
11451
  mode: board.supervisor?.mode ?? "deterministic",
11270
11452
  lastAuditAt: (/* @__PURE__ */ new Date()).toISOString(),
11271
11453
  reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
11272
11454
  staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
11273
11455
  anomalyCount,
11274
- 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`
11275
11457
  });
11276
11458
  return true;
11277
11459
  }
@@ -11288,7 +11470,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11288
11470
  title,
11289
11471
  ...payload?.description ? { description: payload.description } : {},
11290
11472
  ...payload?.tags ? { tags: payload.tags } : {},
11291
- ...payload?.columns ? { columns: payload.columns } : {},
11292
11473
  ...has(payload, "lifecycle") ? { lifecycle: payload?.lifecycle } : {},
11293
11474
  ...has(payload, "boundary") ? { boundary: payload?.boundary } : {}
11294
11475
  })
@@ -11305,7 +11486,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11305
11486
  ...payload?.title ? { title: payload.title } : {},
11306
11487
  ...payload?.description ? { description: payload.description } : {},
11307
11488
  ...payload?.tags ? { tags: payload.tags } : {},
11308
- ...payload?.columns ? { columns: payload.columns } : {},
11309
11489
  ...has(payload, "lifecycle") ? {
11310
11490
  lifecycle: payload?.lifecycle ?? null
11311
11491
  } : {},
@@ -11352,6 +11532,12 @@ async function handleKanbanRoute(ws, msg, ctx) {
11352
11532
  });
11353
11533
  return true;
11354
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
+ }
11355
11541
  case "kanban.generate": {
11356
11542
  const description = payload?.description;
11357
11543
  if (!description) {
@@ -11813,8 +11999,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11813
11999
  taskId,
11814
12000
  {
11815
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.
11816
12007
  type: payload?.checkType ?? "manual",
11817
- status: payload?.status ?? "pending"
12008
+ status: payload?.status ?? "pending",
12009
+ ...typeof payload?.notes === "string" ? { notes: payload.notes } : {}
11818
12010
  },
11819
12011
  activityContext(
11820
12012
  ctx,
@@ -11879,30 +12071,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11879
12071
  case "kanban.capabilities":
11880
12072
  ok(ws, type, { dispatchSupported: Boolean(ctx.dispatchTask) });
11881
12073
  return true;
11882
- case "kanban.column.add": {
11883
- const boardId = payload?.boardId;
11884
- const title = payload?.title;
11885
- if (!boardId || !title) {
11886
- fail(ws, type, "boardId and title required");
11887
- return true;
11888
- }
11889
- const result = await addColumn(ctx.projectRoot, boardId, { title });
11890
- result ? ok(ws, type, result.board.columns) : fail(ws, type, `Board not found: ${boardId}`);
11891
- return true;
11892
- }
11893
- case "kanban.column.remove": {
11894
- const boardId = payload?.boardId;
11895
- const columnId = payload?.columnId;
11896
- if (!boardId || !columnId) {
11897
- fail(ws, type, "boardId and columnId required");
11898
- return true;
11899
- }
11900
- const board = await removeColumn(ctx.projectRoot, boardId, columnId, {
11901
- moveTasksToColumnId: payload?.moveTasksToColumnId
11902
- });
11903
- board ? ok(ws, type, { removed: true, boardId: board.id, columnId, board }) : fail(ws, type, `Column not found: ${columnId}`);
11904
- return true;
11905
- }
11906
12074
  default:
11907
12075
  fail(ws, type, `Unknown kanban message type: ${type}`);
11908
12076
  return true;
@@ -13167,6 +13335,34 @@ async function handleSageRecover(ws, msg, memoryStore) {
13167
13335
  send(ws, { type: "memory.sage.recover", payload: { error: errMessage(err) } });
13168
13336
  }
13169
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
+ }
13170
13366
  async function handleSageCandidateResolve(ws, msg, memoryStore) {
13171
13367
  const Sage = getSageSurface(memoryStore);
13172
13368
  if (!Sage) {
@@ -13345,6 +13541,9 @@ async function handleMemoryRoute(ctx, ws, message) {
13345
13541
  case "memory.sage.recover":
13346
13542
  await handleSageRecover(ws, message, store);
13347
13543
  return true;
13544
+ case "memory.sage.listCandidates":
13545
+ await handleSageListCandidates(ws, message, store);
13546
+ return true;
13348
13547
  case "memory.sage.candidateResolve":
13349
13548
  await handleSageCandidateResolve(ws, message, store);
13350
13549
  return true;
@@ -13947,6 +14146,7 @@ import {
13947
14146
  finalizeTaskCompletion,
13948
14147
  getBoard as getBoard2,
13949
14148
  getKanbanQueueHealth as getKanbanQueueHealth2,
14149
+ kanbanQueueAnomalyCount as kanbanQueueAnomalyCount2,
13950
14150
  listBoards as listBoards3,
13951
14151
  reconcileKanbanBoard as reconcileKanbanBoard2,
13952
14152
  recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
@@ -14029,7 +14229,7 @@ function createKanbanSupervisor(deps2) {
14029
14229
  }) : null;
14030
14230
  if (recovered)
14031
14231
  health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
14032
- const anomalyCount = countAnomalies(health);
14232
+ const anomalyCount = kanbanQueueAnomalyCount2(health);
14033
14233
  const snapshot = {
14034
14234
  boardId: board.id,
14035
14235
  status: anomalyCount > 0 ? "attention" : "healthy",
@@ -14228,13 +14428,10 @@ function dispatchRoute(routing) {
14228
14428
  ...routing.fallbackModels?.length ? { fallbackModels: routing.fallbackModels } : {}
14229
14429
  };
14230
14430
  }
14231
- function countAnomalies(health) {
14232
- return health.staleAssignments.count + health.heartbeatDue.count + health.counts.failed + health.counts.blocked;
14233
- }
14234
14431
  function healthSummary(health) {
14235
14432
  return [
14236
14433
  `${health.counts.running} running`,
14237
- `${health.counts.ready} ready`,
14434
+ `${health.counts.startable} ready`,
14238
14435
  `${health.counts.review} review`,
14239
14436
  `${health.counts.blocked} blocked`,
14240
14437
  `${health.counts.failed} failed`,
@@ -14988,7 +15185,7 @@ async function handleProcessRoute(ws, msg, handlers) {
14988
15185
  import * as fs14 from "node:fs/promises";
14989
15186
  import * as path16 from "node:path";
14990
15187
  import { DefaultSessionStore } from "@wrongstack/core/storage";
14991
- import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
15188
+ import { activateProjectStateGuard, resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
14992
15189
  function createProjectHandlers(ctx) {
14993
15190
  const sendTo = (ws, message) => {
14994
15191
  if (ctx.sendMessage) ctx.sendMessage(ws, message);
@@ -15153,6 +15350,7 @@ function createProjectHandlers(ctx) {
15153
15350
  try {
15154
15351
  await ctx.onSessionSwapped?.(next.id, identityTarget);
15155
15352
  await ctx.onBeforeSessionTodosReplaced?.(next.id, paths.projectSessions);
15353
+ await activateProjectStateGuard(resolved);
15156
15354
  } catch (err) {
15157
15355
  try {
15158
15356
  await ctx.onBeforeSessionTodosReplaced?.(previous.id, previousPaths.projectSessions);
@@ -16107,6 +16305,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
16107
16305
  "memory.sage.get",
16108
16306
  "memory.sage.graph",
16109
16307
  "memory.sage.list",
16308
+ "memory.sage.listCandidates",
16110
16309
  "memory.sage.listPage",
16111
16310
  "memory.sage.recover",
16112
16311
  "memory.sage.remember",
@@ -16386,6 +16585,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
16386
16585
  "memory.sage.get",
16387
16586
  "memory.sage.graph",
16388
16587
  "memory.sage.list",
16588
+ "memory.sage.listCandidates",
16389
16589
  "memory.sage.listPage",
16390
16590
  "memory.sage.recover",
16391
16591
  "memory.sage.remember",
@@ -17046,6 +17246,7 @@ function createSessionHandlers(ctx) {
17046
17246
  ctx.abortActiveRun?.(current2.id);
17047
17247
  } catch {
17048
17248
  }
17249
+ await ctx.context.flushConversationJournal?.().catch(() => void 0);
17049
17250
  await finalizeSession(current2);
17050
17251
  }
17051
17252
  ctx.setSession(next);
@@ -17580,6 +17781,7 @@ import {
17580
17781
  clearProjectAgentConsolidated,
17581
17782
  clearProjectSkillAugmentation,
17582
17783
  createProjectAgent,
17784
+ DEFAULT_EAGER_SKILL_LIMIT,
17583
17785
  detectLearnedConflicts,
17584
17786
  evaluateAutoOptimize,
17585
17787
  FLEET_ROSTER,
@@ -17597,12 +17799,15 @@ import {
17597
17799
  loadProjectSkillAugmentation,
17598
17800
  loadSkillAffinity,
17599
17801
  optimizeProjectAgentLearning,
17802
+ rankRoleSkills,
17803
+ readQuarantinedDirectives,
17600
17804
  readRawLearnedEntries,
17601
17805
  resetProjectAgentIdentity,
17602
17806
  resolveAutoOptimizePolicy,
17603
17807
  resolveRoleSkillCandidates,
17604
17808
  saveProjectAgentConsolidated,
17605
17809
  saveProjectSkillAugmentation,
17810
+ scoreSkillAffinity,
17606
17811
  setSkillPinned,
17607
17812
  slugifyProjectAgentRole,
17608
17813
  updateProjectAgentConfig,
@@ -17909,9 +18114,7 @@ ${String(p.content ?? "")}`;
17909
18114
  // eligible right now, and why not when it does not. Surfacing the reason
17910
18115
  // is what keeps "nothing happened" from looking like a broken feature.
17911
18116
  case "agent-roster.auto-optimize-status": {
17912
- const policy = resolveAutoOptimizePolicy(
17913
- this.getAutoOptimizeSettings?.() ?? void 0
17914
- );
18117
+ const policy = resolveAutoOptimizePolicy(this.getAutoOptimizeSettings?.() ?? void 0);
17915
18118
  const roles = role ? [role] : listProjectAgentRoles(projectRoot);
17916
18119
  return {
17917
18120
  type,
@@ -17934,18 +18137,30 @@ ${String(p.content ?? "")}`;
17934
18137
  const candidates = resolveRoleSkillCandidates(role, projectRoot);
17935
18138
  const developed = listProjectSkillAugmentations(role, projectRoot);
17936
18139
  const affinity = loadSkillAffinity(role, projectRoot);
18140
+ const eager = new Set(rankRoleSkills(role, candidates, projectRoot));
17937
18141
  return {
17938
18142
  type,
17939
18143
  payload: {
17940
18144
  role,
18145
+ eagerLimit: DEFAULT_EAGER_SKILL_LIMIT,
17941
18146
  skills: candidates.map((skill) => ({
17942
18147
  skill,
17943
18148
  developed: developed.includes(skill),
17944
- affinity: affinity.entries[skill] ?? null
18149
+ affinity: affinity.entries[skill] ?? null,
18150
+ score: scoreSkillAffinity(affinity.entries[skill]) + (developed.includes(skill) ? 1 : 0),
18151
+ eager: eager.has(skill)
17945
18152
  }))
17946
18153
  }
17947
18154
  };
17948
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
+ }
17949
18164
  case "agent-roster.read-skill": {
17950
18165
  const skill = typeof p.skill === "string" ? p.skill : "";
17951
18166
  if (!role || !skill) return { type, payload: { error: "role and skill required" } };
@@ -19864,6 +20079,7 @@ function registerSetupEventsStatusWatcher(deps2) {
19864
20079
  const logWatcherMetricsEnabled = shouldLogWatcherStats();
19865
20080
  const logWatcherMetrics = () => logFileWatcherMetrics(watcherMetrics);
19866
20081
  const metricsInterval = logWatcherMetricsEnabled ? setInterval(logWatcherMetrics, 6e4) : void 0;
20082
+ metricsInterval?.unref?.();
19867
20083
  const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
19868
20084
  broadcast2(clients, { type: "client.status_update", payload: statusData });
19869
20085
  if (watcherMetrics) {
@@ -21083,14 +21299,7 @@ import {
21083
21299
  toErrorMessage as toErrorMessage9
21084
21300
  } from "@wrongstack/core/utils";
21085
21301
  import { makeLightSubagentFactory } from "@wrongstack/runtime";
21086
- import {
21087
- createSageContextMonitorMiddleware,
21088
- createSageToolCallMiddleware,
21089
- createSageTurnMiddleware,
21090
- getSageRetrieval,
21091
- getSageService,
21092
- InjectionTracker
21093
- } from "@wrongstack/sage";
21302
+ import { getSageService, setupSage } from "@wrongstack/sage";
21094
21303
 
21095
21304
  // src/server/discover-mailbox-bridge.ts
21096
21305
  import { spawn as spawn2 } from "node:child_process";
@@ -21893,6 +22102,7 @@ var WorktreeWebSocketHandler = class {
21893
22102
  this.broadcast(this.stateMessage());
21894
22103
  if (this.broadcastInterval) return;
21895
22104
  this.broadcastInterval = setInterval(() => this.broadcast(this.stateMessage()), 2e3);
22105
+ this.broadcastInterval.unref?.();
21896
22106
  }
21897
22107
  stopBroadcast() {
21898
22108
  this.broadcast(this.stateMessage());
@@ -21944,62 +22154,15 @@ async function createAgentServices(input) {
21944
22154
  const collabPause = collabPauseMiddleware(collabBus, { logger });
21945
22155
  pipelines.toolCall.prepend(collabPause);
21946
22156
  installDesignStudioMiddleware({ pipelines, ctx: context });
21947
- const memoryRetrieval = getSageRetrieval(memoryStore);
21948
- if (config.features.memory !== false && config.Sage?.enabled !== false && memoryRetrieval) {
21949
- const sageInjectionTracker = new InjectionTracker();
21950
- const getSageSessionId = () => input.sessionGetter().id;
21951
- if (config.Sage?.inject?.toolResults !== false) {
21952
- pipelines.toolCall.use(
21953
- createSageToolCallMiddleware({
21954
- memory: memoryRetrieval,
21955
- maxHintsPerTool: config.Sage?.inject?.maxHintsPerTool,
21956
- maxCharsPerTool: config.Sage?.inject?.maxCharsPerTool,
21957
- taskAware: config.Sage?.inject?.taskAware,
21958
- minScore: config.Sage?.inject?.minScore,
21959
- minImportance: config.Sage?.inject?.minImportance,
21960
- // Forward the explicit relation floor so an operator-configured
21961
- // `Sage.inject.relationFloor` is honored in WebUI sessions. Without
21962
- // this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
21963
- // is the CLI default but masks operator overrides.
21964
- relationFloor: config.Sage?.inject?.relationFloor,
21965
- repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
21966
- verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
21967
- triggers: config.Sage?.inject?.triggers,
21968
- // Resolve the live session so retrieval and cooldown are session-scoped
21969
- // (matching the CLI wiring in wiring/sage.ts). Without this, the
21970
- // middleware falls back to ctx.session.id for cooldown but passes
21971
- // undefined to retrieval, causing owned session-scoped memories to be
21972
- // silently excluded from tool-call injection.
21973
- getSessionId: getSageSessionId,
21974
- tracker: sageInjectionTracker,
21975
- events
21976
- })
21977
- );
21978
- }
21979
- if (config.Sage?.inject?.turnContext === true) {
21980
- pipelines.request.use(
21981
- createSageTurnMiddleware({
21982
- memory: memoryRetrieval,
21983
- maxMemories: config.Sage?.inject?.maxTurnMemories,
21984
- maxChars: config.Sage?.inject?.maxCharsPerTurn,
21985
- minScore: config.Sage?.inject?.minScore,
21986
- // CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
21987
- // value drives both runtimes instead of silently falling back to the
21988
- // 0.3 default. The undefined case keeps the middleware's own default.
21989
- metadataWeight: config.Sage?.retrieval?.metadataWeight,
21990
- getSessionId: getSageSessionId,
21991
- tracker: sageInjectionTracker
21992
- })
21993
- );
21994
- }
21995
- pipelines.request.use(
21996
- createSageContextMonitorMiddleware({
21997
- tracker: sageInjectionTracker,
21998
- events,
21999
- getSessionId: getSageSessionId
22000
- })
22001
- );
22002
- }
22157
+ const runSageSessionHygiene = setupSage({
22158
+ config,
22159
+ pipelines,
22160
+ memoryStore,
22161
+ logger,
22162
+ events,
22163
+ getSessionId: () => input.sessionGetter().id,
22164
+ projectRoot
22165
+ });
22003
22166
  const codebaseIndexing = setupWebUICodebaseIndexing({
22004
22167
  config,
22005
22168
  context,
@@ -22111,7 +22274,12 @@ async function createAgentServices(input) {
22111
22274
  confirmAwaiter: void 0,
22112
22275
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
22113
22276
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
22114
- 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
22115
22283
  });
22116
22284
  input.installToolBoundary?.(pipelines);
22117
22285
  const webuiLogger = container.resolve(TOKENS.Logger);
@@ -22131,6 +22299,7 @@ async function createAgentServices(input) {
22131
22299
  providers: providerRegistry,
22132
22300
  events,
22133
22301
  pipelines,
22302
+ refreshSystemPrompt: true,
22134
22303
  context,
22135
22304
  maxIterations: config.tools?.maxIterations ?? DEFAULT_TOOLS_CONFIG.maxIterations,
22136
22305
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
@@ -22398,6 +22567,7 @@ async function createAgentServices(input) {
22398
22567
  terminalHandler,
22399
22568
  collabHandler,
22400
22569
  disposeRealtimeHandlers,
22570
+ runSageSessionHygiene,
22401
22571
  updateAutoCompactionMaxContext
22402
22572
  };
22403
22573
  }
@@ -22877,15 +23047,9 @@ import {
22877
23047
  makeMailInboxTool,
22878
23048
  makeMailSendTool
22879
23049
  } from "@wrongstack/core/coordination";
22880
- import {
22881
- DefaultPromptLoader,
22882
- DefaultSkillLoader
22883
- } from "@wrongstack/core/execution";
22884
- import {
22885
- EventBus,
22886
- TOKENS as TOKENS2
22887
- } from "@wrongstack/core/kernel";
23050
+ import { DefaultPromptLoader, DefaultSkillLoader } from "@wrongstack/core/execution";
22888
23051
  import { DefaultTokenCounter } from "@wrongstack/core/infrastructure";
23052
+ import { EventBus, TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
22889
23053
  import { DefaultModelsRegistry, DefaultModeStore } from "@wrongstack/core/models";
22890
23054
  import { ProviderRegistry, ToolRegistry } from "@wrongstack/core/registry";
22891
23055
  import { SkillInstaller } from "@wrongstack/core/skills";
@@ -23543,6 +23707,7 @@ async function createPreContextServices(input) {
23543
23707
  modeId,
23544
23708
  modePrompt,
23545
23709
  modelCapabilities: () => modelCapabilitiesRef.current,
23710
+ tokenSavingMode: config.features.tokenSavingMode,
23546
23711
  instructionPaths: {
23547
23712
  globalDir: wpaths.globalInstructions,
23548
23713
  projectDir: wpaths.inProjectInstructions,
@@ -23563,7 +23728,8 @@ async function createPreContextServices(input) {
23563
23728
  const systemPrompt = await systemPromptBuilder.build({
23564
23729
  cwd: projectRoot,
23565
23730
  projectRoot,
23566
- tools: toolRegistry.list(),
23731
+ tools: toolRegistry.listForProvider(),
23732
+ catalogTools: toolRegistry.list(),
23567
23733
  provider: config.provider,
23568
23734
  model: config.model,
23569
23735
  onlineAgents
@@ -23581,6 +23747,7 @@ async function createPreContextServices(input) {
23581
23747
  projectRoot,
23582
23748
  model: config.model
23583
23749
  });
23750
+ context.meta["promptOnlineAgents"] = onlineAgents;
23584
23751
  const initialContextPolicy = resolveContextWindowPolicy3(config.context);
23585
23752
  context.meta["contextWindowMode"] = initialContextPolicy.id;
23586
23753
  context.meta["contextWindowPolicy"] = initialContextPolicy;
@@ -23653,6 +23820,7 @@ function createModeHandlers(context) {
23653
23820
  modeId: id,
23654
23821
  modePrompt,
23655
23822
  modelCapabilities: context.modelCapabilities,
23823
+ tokenSavingMode: config.features?.tokenSavingMode,
23656
23824
  instructionPaths: {
23657
23825
  globalDir: paths.globalInstructions,
23658
23826
  projectDir: paths.inProjectInstructions,
@@ -23662,7 +23830,8 @@ function createModeHandlers(context) {
23662
23830
  context.context.systemPrompt = await builder.build({
23663
23831
  cwd: context.projectRoot,
23664
23832
  projectRoot: context.projectRoot,
23665
- tools: context.toolRegistry.list(),
23833
+ tools: context.toolRegistry.listForProvider(),
23834
+ catalogTools: context.toolRegistry.list(),
23666
23835
  provider: config.provider,
23667
23836
  model: config.model
23668
23837
  });
@@ -24937,15 +25106,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
24937
25106
  eternalSubscription = null;
24938
25107
  }
24939
25108
  codebaseIndexing.dispose();
24940
- if (config.Sage?.enabled !== false && config.Sage?.hygiene?.autoAfterSession !== false) {
24941
- const candidate = memoryStore;
24942
- await candidate.hygiene?.({
24943
- retentionDays: config.Sage?.hygiene?.retentionDays,
24944
- archiveLowConfidenceAfterDays: config.Sage?.hygiene?.archiveLowConfidenceAfterDays
24945
- }).catch(
24946
- (err) => logger.warn(`sage session hygiene failed: ${toErrorMessage13(err)}`)
24947
- );
24948
- }
25109
+ await agentServices.runSageSessionHygiene().catch((err) => logger.warn(`sage session hygiene failed: ${toErrorMessage13(err)}`));
24949
25110
  await memoryStore.dispose().catch(
24950
25111
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
24951
25112
  );