@wrongstack/webui-server 0.302.2 → 0.303.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.
package/dist/index.js CHANGED
@@ -1464,7 +1464,13 @@ function indexDbVersion(projectRoot, indexDir) {
1464
1464
  try {
1465
1465
  const dir = resolveIndexDir(projectRoot, indexDir);
1466
1466
  const st = fs.statSync(path.join(dir, DB_FILE));
1467
- return `${st.mtimeMs}:${st.size}`;
1467
+ let wal = "";
1468
+ try {
1469
+ const walSt = fs.statSync(path.join(dir, `${DB_FILE}-wal`));
1470
+ wal = `:${walSt.mtimeMs}:${walSt.size}`;
1471
+ } catch {
1472
+ }
1473
+ return `${st.mtimeMs}:${st.size}${wal}`;
1468
1474
  } catch {
1469
1475
  return "missing";
1470
1476
  }
@@ -7689,12 +7695,34 @@ function handleTodosGet(ctx, ws) {
7689
7695
  payload: sessionPayload(ctx, { todos: [...ctx.context.todos] })
7690
7696
  });
7691
7697
  }
7692
- function handleTodosClear(ctx, ws) {
7693
- ctx.replaceTodos?.([]);
7694
- sendResult3(ctx, ws, true, "Todos cleared");
7695
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: [] }) });
7698
+ async function commitTodos(ctx, todos) {
7699
+ if (ctx.mutateTodos) {
7700
+ const result = await ctx.mutateTodos(todos);
7701
+ return { todos: result.todos, warnings: result.warnings ?? [] };
7702
+ }
7703
+ ctx.replaceTodos?.(todos);
7704
+ return { todos: [...todos], warnings: [] };
7696
7705
  }
7697
- function handleTodosRemove(ctx, ws, payload) {
7706
+ function managedProjectionMessage() {
7707
+ return "Kanban-bound todos are task projections. Change or remove the task from Kanban.";
7708
+ }
7709
+ async function handleTodosClear(ctx, ws) {
7710
+ if (ctx.context.todos.some((todo) => todo.kanbanBoardId && todo.kanbanTaskId)) {
7711
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7712
+ return;
7713
+ }
7714
+ try {
7715
+ const result = await commitTodos(ctx, []);
7716
+ sendResult3(ctx, ws, true, "Todos cleared");
7717
+ ctx.broadcast({
7718
+ type: "todos.updated",
7719
+ payload: sessionPayload(ctx, { todos: result.todos })
7720
+ });
7721
+ } catch (error2) {
7722
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7723
+ }
7724
+ }
7725
+ async function handleTodosRemove(ctx, ws, payload) {
7698
7726
  if (!payload) {
7699
7727
  sendResult3(ctx, ws, false, "Missing id or index");
7700
7728
  return;
@@ -7711,12 +7739,27 @@ function handleTodosRemove(ctx, ws, payload) {
7711
7739
  sendResult3(ctx, ws, false, "Todo not found");
7712
7740
  return;
7713
7741
  }
7742
+ if (removed.kanbanBoardId && removed.kanbanTaskId) {
7743
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7744
+ return;
7745
+ }
7714
7746
  const next = [...todos.slice(0, targetIndex), ...todos.slice(targetIndex + 1)];
7715
- ctx.replaceTodos?.(next);
7716
- sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
7717
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
7747
+ try {
7748
+ const result = await commitTodos(ctx, next);
7749
+ sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
7750
+ ctx.broadcast({
7751
+ type: "todos.updated",
7752
+ payload: sessionPayload(ctx, { todos: result.todos })
7753
+ });
7754
+ } catch (error2) {
7755
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7756
+ }
7718
7757
  }
7719
- function handleTodoUpdate(ctx, ws, payload) {
7758
+ async function handleTodoUpdate(ctx, ws, payload) {
7759
+ 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") {
7760
+ sendResult3(ctx, ws, false, "Invalid todo update payload");
7761
+ return;
7762
+ }
7720
7763
  const index = ctx.context.todos.findIndex((todo) => todo.id === payload.id);
7721
7764
  const existing = ctx.context.todos[index];
7722
7765
  if (index === -1 || !existing) {
@@ -7729,9 +7772,25 @@ function handleTodoUpdate(ctx, ws, payload) {
7729
7772
  status: payload.status ?? existing.status,
7730
7773
  activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
7731
7774
  };
7732
- ctx.replaceTodos?.(next);
7733
- sendResult3(ctx, ws, true, `Todo "${existing.content}" updated`);
7734
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
7775
+ try {
7776
+ const result = await commitTodos(ctx, next);
7777
+ const projected = result.todos.find((todo) => todo.id === existing.id);
7778
+ const requestedStatus = payload.status ?? existing.status;
7779
+ const projectionRejected = Boolean(existing.kanbanBoardId && existing.kanbanTaskId) && projected?.status !== requestedStatus;
7780
+ const warning = result.warnings[0];
7781
+ sendResult3(
7782
+ ctx,
7783
+ ws,
7784
+ !projectionRejected,
7785
+ projectionRejected ? warning ?? `Kanban kept "${existing.content}" at ${projected?.status ?? "its current state"}.` : warning ? `Todo "${existing.content}" updated. ${warning}` : `Todo "${existing.content}" updated`
7786
+ );
7787
+ ctx.broadcast({
7788
+ type: "todos.updated",
7789
+ payload: sessionPayload(ctx, { todos: result.todos })
7790
+ });
7791
+ } catch (error2) {
7792
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7793
+ }
7735
7794
  }
7736
7795
  async function handleTasksGet(ctx, ws) {
7737
7796
  const taskPath = taskPathOf(ctx);
@@ -7759,14 +7818,32 @@ async function handleTaskUpdate(ctx, ws, payload) {
7759
7818
  return;
7760
7819
  }
7761
7820
  try {
7762
- const file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
7763
- const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
7764
- if (!task) return tasks;
7765
- task.status = payload.status;
7766
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7767
- return tasks;
7768
- });
7769
- sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
7821
+ let file;
7822
+ if (ctx.mutateTaskStatus) {
7823
+ const result = await ctx.mutateTaskStatus(payload.id, payload.status);
7824
+ if (!result.ok) {
7825
+ sendResult3(ctx, ws, false, result.message);
7826
+ return;
7827
+ }
7828
+ file = await loadTasks(taskPath);
7829
+ if (!file) throw new Error("Task mutation succeeded but its persisted snapshot is missing.");
7830
+ sendResult3(ctx, ws, true, result.message);
7831
+ } else {
7832
+ let matched = false;
7833
+ file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
7834
+ const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
7835
+ if (!task) return tasks;
7836
+ matched = true;
7837
+ task.status = payload.status;
7838
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7839
+ return tasks;
7840
+ });
7841
+ if (!matched) {
7842
+ sendResult3(ctx, ws, false, `Task "${payload.id}" not found.`);
7843
+ return;
7844
+ }
7845
+ sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
7846
+ }
7770
7847
  ctx.broadcast({
7771
7848
  type: "tasks.updated",
7772
7849
  payload: sessionPayload(ctx, { tasks: file.tasks })
@@ -7813,6 +7890,18 @@ async function handlePlanTemplateUse(ctx, ws, template) {
7813
7890
  return;
7814
7891
  }
7815
7892
  try {
7893
+ if (ctx.mutatePlan) {
7894
+ const result = await ctx.mutatePlan({ action: "template_use", template });
7895
+ if (!result.ok) {
7896
+ sendResult3(ctx, ws, false, result.message);
7897
+ return;
7898
+ }
7899
+ const plan2 = await loadPlan(planPath);
7900
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7901
+ sendResult3(ctx, ws, true, result.message);
7902
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7903
+ return;
7904
+ }
7816
7905
  const templateDefinition = getPlanTemplate(template);
7817
7906
  if (!templateDefinition) {
7818
7907
  sendResult3(ctx, ws, false, `Unknown template "${template}".`);
@@ -7841,6 +7930,22 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
7841
7930
  return;
7842
7931
  }
7843
7932
  try {
7933
+ if (ctx.mutatePlan) {
7934
+ const result = await ctx.mutatePlan({
7935
+ action: "status",
7936
+ target: payload.target,
7937
+ status: payload.status
7938
+ });
7939
+ if (!result.ok) {
7940
+ sendResult3(ctx, ws, false, result.message);
7941
+ return;
7942
+ }
7943
+ const plan2 = await loadPlan(planPath);
7944
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7945
+ sendResult3(ctx, ws, true, result.message);
7946
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7947
+ return;
7948
+ }
7844
7949
  let changed = false;
7845
7950
  const plan = await mutatePlan(planPath, currentSessionId(ctx), async (currentPlan) => {
7846
7951
  const before = currentPlan.updatedAt;
@@ -7864,13 +7969,17 @@ async function handleWorklistMessage(ctx, ws, message) {
7864
7969
  handleTodosGet(ctx, ws);
7865
7970
  return;
7866
7971
  case "todos.clear":
7867
- handleTodosClear(ctx, ws);
7972
+ await handleTodosClear(ctx, ws);
7868
7973
  return;
7869
7974
  case "todos.remove":
7870
- handleTodosRemove(ctx, ws, message.payload);
7975
+ await handleTodosRemove(
7976
+ ctx,
7977
+ ws,
7978
+ message.payload
7979
+ );
7871
7980
  return;
7872
7981
  case "todo.update":
7873
- handleTodoUpdate(
7982
+ await handleTodoUpdate(
7874
7983
  ctx,
7875
7984
  ws,
7876
7985
  message.payload
@@ -10928,9 +11037,9 @@ import {
10928
11037
  claimReadyTask,
10929
11038
  copyTaskToBoard,
10930
11039
  createBoard,
11040
+ createBoardFromText,
10931
11041
  duplicateBoard,
10932
11042
  exportBoardToTaskGraph,
10933
- createBoardFromText,
10934
11043
  getBoard,
10935
11044
  getKanbanOrchestrationSnapshot,
10936
11045
  getKanbanQueueHealth,
@@ -11118,8 +11227,35 @@ async function handleTaskVerification(ws, type, payload, ctx) {
11118
11227
  }
11119
11228
  }
11120
11229
 
11230
+ // src/server/kanban-route-pagination.ts
11231
+ function paginateKanbanBoards(boards, input) {
11232
+ const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
11233
+ const activeSessionIds = new Set(input.activeSessionIds ?? []);
11234
+ const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
11235
+ const sorted = [...boards].sort((left, right) => {
11236
+ const activityOrder = Number(isActive(right)) - Number(isActive(left));
11237
+ return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
11238
+ });
11239
+ const activeTotal = sorted.filter(isActive).length;
11240
+ const total = sorted.length;
11241
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
11242
+ const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
11243
+ const page = Math.min(totalPages, Math.max(1, requestedPage));
11244
+ const start = (page - 1) * pageSize;
11245
+ return {
11246
+ items: sorted.slice(start, start + pageSize),
11247
+ total,
11248
+ page,
11249
+ pageSize,
11250
+ totalPages,
11251
+ activeTotal,
11252
+ orphanedTotal: total - activeTotal
11253
+ };
11254
+ }
11255
+
11121
11256
  // src/server/kanban-task-routes.ts
11122
11257
  import {
11258
+ getKanbanWorkbench,
11123
11259
  getTask,
11124
11260
  listTaskActivity,
11125
11261
  recordTaskActivity,
@@ -11127,6 +11263,16 @@ import {
11127
11263
  } from "@wrongstack/kanban";
11128
11264
  async function handleKanbanTaskRoute(ws, type, payload, ctx) {
11129
11265
  switch (type) {
11266
+ case "kanban.workbench":
11267
+ ok(
11268
+ ws,
11269
+ type,
11270
+ await getKanbanWorkbench(ctx.projectRoot, {
11271
+ ...typeof payload?.limitPerLane === "number" ? { limitPerLane: payload.limitPerLane } : {},
11272
+ ...typeof payload?.alertLimit === "number" ? { alertLimit: payload.alertLimit } : {}
11273
+ })
11274
+ );
11275
+ return true;
11130
11276
  case "kanban.task.remove":
11131
11277
  await handleTaskRemove(ws, type, payload, ctx);
11132
11278
  return true;
@@ -11219,40 +11365,11 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
11219
11365
  outcome,
11220
11366
  ...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
11221
11367
  },
11222
- activityContext(
11223
- ctx,
11224
- payload?.actor ?? ctx.context?.agentId ?? "webui"
11225
- )
11368
+ activityContext(ctx, payload?.actor ?? ctx.context?.agentId ?? "webui")
11226
11369
  );
11227
11370
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
11228
11371
  }
11229
11372
 
11230
- // src/server/kanban-route-pagination.ts
11231
- function paginateKanbanBoards(boards, input) {
11232
- const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
11233
- const activeSessionIds = new Set(input.activeSessionIds ?? []);
11234
- const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
11235
- const sorted = [...boards].sort((left, right) => {
11236
- const activityOrder = Number(isActive(right)) - Number(isActive(left));
11237
- return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
11238
- });
11239
- const activeTotal = sorted.filter(isActive).length;
11240
- const total = sorted.length;
11241
- const totalPages = Math.max(1, Math.ceil(total / pageSize));
11242
- const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
11243
- const page = Math.min(totalPages, Math.max(1, requestedPage));
11244
- const start = (page - 1) * pageSize;
11245
- return {
11246
- items: sorted.slice(start, start + pageSize),
11247
- total,
11248
- page,
11249
- pageSize,
11250
- totalPages,
11251
- activeTotal,
11252
- orphanedTotal: total - activeTotal
11253
- };
11254
- }
11255
-
11256
11373
  // src/server/kanban-route-protocol.ts
11257
11374
  var KANBAN_CLIENT_MESSAGE_TYPES = [
11258
11375
  "kanban.capabilities",
@@ -11297,7 +11414,8 @@ var KANBAN_CLIENT_MESSAGE_TYPES = [
11297
11414
  "kanban.task.verify",
11298
11415
  "kanban.taskgraph.export",
11299
11416
  "kanban.taskgraph.sync",
11300
- "kanban.update"
11417
+ "kanban.update",
11418
+ "kanban.workbench"
11301
11419
  ];
11302
11420
 
11303
11421
  // src/server/kanban-routes.ts
@@ -12078,7 +12196,9 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
12078
12196
  projectRoot,
12079
12197
  async (event) => {
12080
12198
  const family = event.event?.split(".")[0];
12081
- if (family !== "board" && family !== "task" && family !== "column") return;
12199
+ if (family !== "board" && family !== "task" && family !== "column" && family !== "contract") {
12200
+ return;
12201
+ }
12082
12202
  const evData = event.data;
12083
12203
  const boardId = evData?.boardId;
12084
12204
  if (!boardId) return;
@@ -13395,14 +13515,17 @@ async function handleSageForFile(ws, msg, memoryStore) {
13395
13515
  send(ws, { type: "memory.sage.forFile", payload: { error: "filePath is required" } });
13396
13516
  return;
13397
13517
  }
13518
+ const includeSuperseded = typeof payload["showSuperseded"] === "boolean" ? payload["showSuperseded"] : typeof payload["includeSuperseded"] === "boolean" ? payload["includeSuperseded"] : void 0;
13519
+ const includeDeleted = payload["showDeleted"] === true || payload["includeDeleted"] === true;
13398
13520
  try {
13399
13521
  const response = await Sage.findMemoriesForFile(filePath, {
13400
13522
  ...typeof payload["lineStart"] === "number" ? { lineStart: payload["lineStart"] } : {},
13401
13523
  ...typeof payload["lineEnd"] === "number" ? { lineEnd: payload["lineEnd"] } : {},
13402
13524
  ...typeof payload["limit"] === "number" ? { limit: payload["limit"] } : {},
13403
- ...payload["includeDeleted"] === true ? { includeDeleted: true } : {}
13525
+ ...includeSuperseded !== void 0 ? { includeSuperseded } : {},
13526
+ ...includeDeleted ? { includeDeleted: true } : {}
13404
13527
  });
13405
- send(ws, { type: "memory.sage.forFile", payload: response });
13528
+ send(ws, { type: "memory.sage.forFile", payload: { response } });
13406
13529
  } catch (err) {
13407
13530
  send(ws, { type: "memory.sage.forFile", payload: { error: errMessage(err) } });
13408
13531
  }
@@ -15104,7 +15227,8 @@ function createKanbanSupervisor(deps2) {
15104
15227
  mode: config.recoveryMode ?? "auto",
15105
15228
  reason: "Kanban supervisor found an expired worker lease."
15106
15229
  }) : null;
15107
- if (recovered) health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
15230
+ if (recovered)
15231
+ health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
15108
15232
  const anomalyCount = countAnomalies(health);
15109
15233
  const snapshot = {
15110
15234
  boardId: board.id,
@@ -15199,7 +15323,9 @@ function createKanbanSupervisor(deps2) {
15199
15323
  } else {
15200
15324
  const summaries = await listBoards4(resolveProjectRoot(deps2));
15201
15325
  pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
15202
- boards = (await Promise.all(summaries.map((summary) => getBoard3(resolveProjectRoot(deps2), summary.id)))).filter((board) => Boolean(board));
15326
+ boards = (await Promise.all(
15327
+ summaries.map((summary) => getBoard3(resolveProjectRoot(deps2), summary.id))
15328
+ )).filter((board) => Boolean(board));
15203
15329
  }
15204
15330
  const results = [];
15205
15331
  for (const board of boards) results.push(await auditBoard(board));
@@ -19071,89 +19197,56 @@ function createEmbeddedProjectRoutes(ctx) {
19071
19197
 
19072
19198
  // src/server/embedded-message-router.ts
19073
19199
  import { makeProviderFromConfig as makeProviderFromConfig2 } from "@wrongstack/providers";
19200
+ import { planTool, taskTool, todoTool } from "@wrongstack/tools";
19074
19201
 
19075
19202
  // src/server/agent-roster-handlers.ts
19076
19203
  import {
19077
19204
  applyProjectAgentConfig,
19078
- buildConsolidationInstruction,
19079
19205
  captureLearnedFromAgentOutputDetailed,
19080
19206
  clearProjectAgentConsolidated,
19207
+ clearProjectSkillAugmentation,
19081
19208
  createProjectAgent,
19082
19209
  detectLearnedConflicts,
19210
+ evaluateAutoOptimize,
19083
19211
  FLEET_ROSTER,
19084
19212
  getProjectAgentLearnStats,
19085
19213
  isConsolidated,
19086
19214
  listProjectAgentLearnedEntries,
19087
19215
  listProjectAgentRoles,
19216
+ listProjectSkillAugmentations,
19088
19217
  loadConsolidationMetadata,
19089
19218
  loadProjectAgentConfig,
19090
19219
  loadProjectAgentConsolidated,
19091
19220
  loadProjectAgentIdentity,
19092
19221
  loadProjectAgentLearned,
19093
19222
  loadProjectAgentProfile,
19223
+ loadProjectSkillAugmentation,
19224
+ loadSkillAffinity,
19225
+ optimizeProjectAgentLearning,
19226
+ readRawLearnedEntries,
19094
19227
  resetProjectAgentIdentity,
19228
+ resolveAutoOptimizePolicy,
19229
+ resolveRoleSkillCandidates,
19095
19230
  saveProjectAgentConsolidated,
19231
+ saveProjectSkillAugmentation,
19232
+ setSkillPinned,
19096
19233
  slugifyProjectAgentRole,
19097
19234
  updateProjectAgentConfig,
19098
19235
  updateProjectAgentIdentity,
19099
19236
  updateProjectAgentLearned,
19100
19237
  updateProjectAgentLearningPolicy
19101
19238
  } from "@wrongstack/core/coordination";
19102
- import { isTextBlock } from "@wrongstack/core/types";
19103
- var CONSOLIDATION_MAX_TOKENS = 8e3;
19104
- var CONSOLIDATION_TIMEOUT_MS = 12e4;
19105
19239
  var AgentRosterWSHandler = class {
19106
19240
  getProjectRoot;
19107
19241
  getLlm;
19108
19242
  broadcast;
19243
+ getAutoOptimizeSettings;
19109
19244
  constructor(opts) {
19110
19245
  this.getProjectRoot = typeof opts.projectRoot === "function" ? opts.projectRoot : () => opts.projectRoot;
19111
19246
  this.getLlm = opts.getLlm ?? (() => void 0);
19112
19247
  this.broadcast = opts.broadcast ?? (() => {
19113
19248
  });
19114
- }
19115
- /**
19116
- * Run the consolidation LLM synthesis headlessly and return the cleaned
19117
- * document text. Returns undefined when no LLM is available so the caller
19118
- * can fall back to the instruction-only path.
19119
- */
19120
- async synthesizeConsolidation(instruction) {
19121
- const llm = this.getLlm();
19122
- if (!llm) return void 0;
19123
- const req = {
19124
- model: llm.model,
19125
- system: [
19126
- {
19127
- type: "text",
19128
- 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."
19129
- }
19130
- ],
19131
- messages: [{ role: "user", content: instruction }],
19132
- maxTokens: CONSOLIDATION_MAX_TOKENS
19133
- };
19134
- const timer = new AbortController();
19135
- let timedOut = false;
19136
- const to = setTimeout(() => {
19137
- timedOut = true;
19138
- timer.abort(new Error("consolidation timeout"));
19139
- }, CONSOLIDATION_TIMEOUT_MS);
19140
- to.unref();
19141
- try {
19142
- const res = await llm.provider.complete(req, { signal: timer.signal });
19143
- const text2 = res.content.filter(isTextBlock).map((block) => block.text).join("\n").trim();
19144
- const wholeDocFence = /^```(?:markdown|md)?[^\n]*\n([\s\S]*?)\n?```\s*$/i;
19145
- const wrapped = wholeDocFence.exec(text2);
19146
- const inner = wrapped?.[1];
19147
- const unfenced = inner !== void 0 ? inner.trim() : text2;
19148
- return { content: unfenced, model: llm.model };
19149
- } catch (err) {
19150
- if (timedOut) {
19151
- throw new Error(`consolidation timed out after ${CONSOLIDATION_TIMEOUT_MS}ms`);
19152
- }
19153
- throw err;
19154
- } finally {
19155
- clearTimeout(to);
19156
- }
19249
+ this.getAutoOptimizeSettings = opts.getAutoOptimizeSettings;
19157
19250
  }
19158
19251
  /** Handle an incoming client message. Returns a response payload. */
19159
19252
  async handleMessage(_ws, type, payload) {
@@ -19358,70 +19451,47 @@ ${String(p.content ?? "")}`;
19358
19451
  const conflicts = detectLearnedConflicts(projectRoot);
19359
19452
  return { type, payload: { conflicts } };
19360
19453
  }
19361
- // ── Consolidate learned entries (headless LLM synthesis) ──────────
19362
- // Runs the whole optimization on the server: read raw entries
19363
- // synthesize with the active model write consolidated.md +
19364
- // consolidation.json. No chat round-trip. When no LLM is available the
19365
- // handler degrades to returning the instruction so a caller (e.g. the
19366
- // chat agent) can still perform the consolidation manually.
19367
- case "agent-roster.consolidate": {
19454
+ // ── Optimize: distil captures into skill addenda + a consolidated doc,
19455
+ // then archive and reset the raw buffer. Shared implementation with the
19456
+ // CLI (`optimizeProjectAgentLearning`) so both surfaces persist the same
19457
+ // artifacts instead of the CLI producing markdown nobody saved.
19458
+ case "agent-roster.consolidate":
19459
+ case "agent-roster.optimize": {
19368
19460
  if (!role) return { type, payload: { error: "role required" } };
19369
- const { instruction, rawEntries, hasExistingConsolidation } = buildConsolidationInstruction(role, projectRoot);
19370
- if (rawEntries.length === 0) {
19461
+ const hasExistingConsolidation = isConsolidated(role, projectRoot);
19462
+ const pending = readRawLearnedEntries(role, projectRoot);
19463
+ if (pending.length === 0) {
19371
19464
  return {
19372
19465
  type: "agent-roster.consolidate",
19373
19466
  payload: {
19374
19467
  role,
19375
19468
  consolidated: false,
19376
19469
  rawEntryCount: 0,
19470
+ skills: [],
19377
19471
  hasExistingConsolidation,
19378
19472
  currentStats: getProjectAgentLearnStats(role, projectRoot)
19379
19473
  }
19380
19474
  };
19381
19475
  }
19382
- let synth;
19383
- try {
19384
- synth = await this.synthesizeConsolidation(instruction);
19385
- } catch (err) {
19386
- return {
19387
- type: "agent-roster.consolidate",
19388
- payload: {
19389
- role,
19390
- consolidated: false,
19391
- rawEntryCount: rawEntries.length,
19392
- hasExistingConsolidation,
19393
- currentStats: getProjectAgentLearnStats(role, projectRoot),
19394
- error: err instanceof Error ? err.message : "consolidation failed"
19395
- }
19396
- };
19397
- }
19398
- if (synth && synth.content.length > 0) {
19399
- let stats;
19400
- let metadata;
19401
- try {
19402
- saveProjectAgentConsolidated(role, synth.content, projectRoot, {
19403
- trigger: "manual",
19404
- model: synth.model
19405
- });
19406
- stats = getProjectAgentLearnStats(role, projectRoot);
19407
- metadata = loadConsolidationMetadata(role, projectRoot);
19408
- } catch (err) {
19409
- return {
19410
- type: "agent-roster.consolidate",
19411
- payload: {
19412
- role,
19413
- consolidated: false,
19414
- rawEntryCount: rawEntries.length,
19415
- hasExistingConsolidation,
19416
- currentStats: getProjectAgentLearnStats(role, projectRoot),
19417
- error: err instanceof Error ? err.message : "failed to persist consolidation"
19418
- }
19419
- };
19420
- }
19476
+ const llm = this.getLlm();
19477
+ const result = await optimizeProjectAgentLearning(role, projectRoot, {
19478
+ ...llm ? { llm } : {},
19479
+ trigger: "manual"
19480
+ });
19481
+ const currentStats = getProjectAgentLearnStats(role, projectRoot);
19482
+ const metadata = loadConsolidationMetadata(role, projectRoot);
19483
+ const basePayload = {
19484
+ role,
19485
+ rawEntryCount: result.rawEntryCount,
19486
+ skills: result.skills,
19487
+ hasExistingConsolidation,
19488
+ currentStats
19489
+ };
19490
+ if (result.status === "optimized") {
19421
19491
  try {
19422
19492
  this.broadcast({
19423
19493
  type: "agent-roster.updated",
19424
- payload: { role, reason: "consolidated", currentStats: stats, metadata }
19494
+ payload: { role, reason: "consolidated", currentStats, metadata }
19425
19495
  });
19426
19496
  } catch (e) {
19427
19497
  console.warn(
@@ -19437,44 +19507,101 @@ ${String(p.content ?? "")}`;
19437
19507
  return {
19438
19508
  type: "agent-roster.consolidate",
19439
19509
  payload: {
19440
- role,
19510
+ ...basePayload,
19441
19511
  consolidated: true,
19442
- rawEntryCount: rawEntries.length,
19443
- content: synth.content,
19444
- model: synth.model,
19445
- currentStats: stats,
19512
+ content: result.content,
19513
+ model: result.model,
19514
+ pruned: result.pruned,
19446
19515
  metadata
19447
19516
  }
19448
19517
  };
19449
19518
  }
19450
- if (synth) {
19451
- return {
19452
- type: "agent-roster.consolidate",
19453
- payload: {
19454
- role,
19455
- consolidated: false,
19456
- emptySynthesis: true,
19457
- model: synth.model,
19458
- rawEntryCount: rawEntries.length,
19459
- hasExistingConsolidation,
19460
- currentStats: getProjectAgentLearnStats(role, projectRoot)
19461
- }
19462
- };
19463
- }
19464
19519
  return {
19465
19520
  type: "agent-roster.consolidate",
19466
19521
  payload: {
19467
- role,
19522
+ ...basePayload,
19468
19523
  consolidated: false,
19469
- instruction,
19470
- rawEntryCount: rawEntries.length,
19471
- hasExistingConsolidation,
19472
- currentStats: getProjectAgentLearnStats(role, projectRoot),
19473
- // Instruction for the leader agent to execute the consolidation
19474
- 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.`
19524
+ ...result.status === "empty-synthesis" ? { emptySynthesis: true, model: result.model } : {},
19525
+ ...result.status === "failed" ? { error: result.error } : {},
19526
+ ...result.status === "no-llm" ? {
19527
+ instruction: result.instruction,
19528
+ 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.`
19529
+ } : {}
19475
19530
  }
19476
19531
  };
19477
19532
  }
19533
+ // ── Automatic-optimization status ─────────────────────────────────
19534
+ // Read-only: says whether the background scheduler considers each role
19535
+ // eligible right now, and why not when it does not. Surfacing the reason
19536
+ // is what keeps "nothing happened" from looking like a broken feature.
19537
+ case "agent-roster.auto-optimize-status": {
19538
+ const policy = resolveAutoOptimizePolicy(
19539
+ this.getAutoOptimizeSettings?.() ?? void 0
19540
+ );
19541
+ const roles = role ? [role] : listProjectAgentRoles(projectRoot);
19542
+ return {
19543
+ type,
19544
+ payload: {
19545
+ policy,
19546
+ roles: roles.map((current2) => {
19547
+ try {
19548
+ const decision = evaluateAutoOptimize(current2, projectRoot, policy);
19549
+ return { role: current2, ...decision };
19550
+ } catch {
19551
+ return { role: current2, eligible: false, reason: "disabled" };
19552
+ }
19553
+ })
19554
+ }
19555
+ };
19556
+ }
19557
+ // ── Skill layer: what this project has developed for each role skill ──
19558
+ case "agent-roster.skills": {
19559
+ if (!role) return { type, payload: { error: "role required" } };
19560
+ const candidates = resolveRoleSkillCandidates(role, projectRoot);
19561
+ const developed = listProjectSkillAugmentations(role, projectRoot);
19562
+ const affinity = loadSkillAffinity(role, projectRoot);
19563
+ return {
19564
+ type,
19565
+ payload: {
19566
+ role,
19567
+ skills: candidates.map((skill) => ({
19568
+ skill,
19569
+ developed: developed.includes(skill),
19570
+ affinity: affinity.entries[skill] ?? null
19571
+ }))
19572
+ }
19573
+ };
19574
+ }
19575
+ case "agent-roster.read-skill": {
19576
+ const skill = typeof p.skill === "string" ? p.skill : "";
19577
+ if (!role || !skill) return { type, payload: { error: "role and skill required" } };
19578
+ return {
19579
+ type,
19580
+ payload: { role, skill, content: loadProjectSkillAugmentation(role, skill, projectRoot) }
19581
+ };
19582
+ }
19583
+ case "agent-roster.save-skill": {
19584
+ const skill = typeof p.skill === "string" ? p.skill : "";
19585
+ if (!role || !skill || typeof p.content !== "string") {
19586
+ return { type, payload: { error: "role, skill and content required" } };
19587
+ }
19588
+ const savedPath = saveProjectSkillAugmentation(role, skill, p.content, projectRoot);
19589
+ return { type, payload: { role, skill, path: savedPath, success: true } };
19590
+ }
19591
+ case "agent-roster.clear-skill": {
19592
+ const skill = typeof p.skill === "string" ? p.skill : "";
19593
+ if (!role) return { type, payload: { error: "role required" } };
19594
+ clearProjectSkillAugmentation(role, skill || void 0, projectRoot);
19595
+ return { type, payload: { role, skill: skill || null, success: true } };
19596
+ }
19597
+ case "agent-roster.pin-skill": {
19598
+ const skill = typeof p.skill === "string" ? p.skill : "";
19599
+ if (!role || !skill || typeof p.pinned !== "boolean") {
19600
+ return { type, payload: { error: "role, skill and boolean pinned required" } };
19601
+ }
19602
+ const affinity = setSkillPinned(role, skill, p.pinned, projectRoot);
19603
+ return { type, payload: { role, skill, pinned: p.pinned, affinity, success: true } };
19604
+ }
19478
19605
  // ── Save consolidated document ────────────────────────────────────
19479
19606
  case "agent-roster.save-consolidated": {
19480
19607
  if (!role || typeof p.content !== "string") {
@@ -20447,7 +20574,20 @@ function createEmbeddedMessageRouter(deps2) {
20447
20574
  },
20448
20575
  send: send2,
20449
20576
  broadcast: deps2.providerCtx.broadcast,
20450
- replaceTodos: (todos) => opts.agent.ctx.state.replaceTodos(todos)
20577
+ replaceTodos: (todos) => opts.agent.ctx.state.replaceTodos(todos),
20578
+ mutateTodos: async (todos) => {
20579
+ const result = await todoTool.execute({ todos }, opts.agent.ctx, {
20580
+ signal: AbortSignal.timeout(3e4)
20581
+ });
20582
+ return {
20583
+ todos: [...opts.agent.ctx.todos],
20584
+ ...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
20585
+ };
20586
+ },
20587
+ mutateTaskStatus: async (id, status) => taskTool.execute({ action: "status", id, status }, opts.agent.ctx, {
20588
+ signal: AbortSignal.timeout(3e4)
20589
+ }),
20590
+ mutatePlan: async (operation) => planTool.execute(operation, opts.agent.ctx, { signal: AbortSignal.timeout(3e4) })
20451
20591
  })
20452
20592
  });
20453
20593
  const processRoutes = {
@@ -20531,6 +20671,7 @@ function createEmbeddedMessageRouter(deps2) {
20531
20671
  agentRoster: {
20532
20672
  rosterHandler: new AgentRosterWSHandler({
20533
20673
  projectRoot,
20674
+ getAutoOptimizeSettings: () => deps2.agentConfigCtx.getConfig?.()?.fleet?.learning?.autoOptimize,
20534
20675
  getLlm: () => {
20535
20676
  const ctx = opts.agent.ctx;
20536
20677
  return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
@@ -24526,6 +24667,7 @@ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES)
24526
24667
 
24527
24668
  // src/server/message-dispatcher.ts
24528
24669
  import path28 from "node:path";
24670
+ import { planTool as planTool2, taskTool as taskTool2, todoTool as todoTool2 } from "@wrongstack/tools";
24529
24671
  function createMessageDispatcher(opts) {
24530
24672
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
24531
24673
  function makeWorklistContext() {
@@ -24537,7 +24679,20 @@ function createMessageDispatcher(opts) {
24537
24679
  },
24538
24680
  send: (w, m) => send(w, m),
24539
24681
  broadcast: (m) => broadcast(state.getClients(), m),
24540
- replaceTodos: (todos) => deps2.context.state.replaceTodos(todos)
24682
+ replaceTodos: (todos) => deps2.context.state.replaceTodos(todos),
24683
+ mutateTodos: async (todos) => {
24684
+ const result = await todoTool2.execute({ todos }, deps2.context, {
24685
+ signal: AbortSignal.timeout(3e4)
24686
+ });
24687
+ return {
24688
+ todos: [...deps2.context.todos],
24689
+ ...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
24690
+ };
24691
+ },
24692
+ mutateTaskStatus: async (id, status) => taskTool2.execute({ action: "status", id, status }, deps2.context, {
24693
+ signal: AbortSignal.timeout(3e4)
24694
+ }),
24695
+ mutatePlan: async (operation) => planTool2.execute(operation, deps2.context, { signal: AbortSignal.timeout(3e4) })
24541
24696
  };
24542
24697
  }
24543
24698
  function makeSkillsContext() {
@@ -24749,6 +24904,7 @@ function createMessageDispatcher(opts) {
24749
24904
  agentRoster: {
24750
24905
  rosterHandler: new AgentRosterWSHandler({
24751
24906
  projectRoot: state.getProjectRoot,
24907
+ getAutoOptimizeSettings: () => state.getConfig().fleet?.learning?.autoOptimize,
24752
24908
  getLlm: () => {
24753
24909
  const ctx = deps2.agent.ctx;
24754
24910
  return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
@@ -25959,11 +26115,11 @@ function buildRoutes(state, deps2, cb) {
25959
26115
  }
25960
26116
 
25961
26117
  // src/server/server-runtime.ts
25962
- import * as path33 from "node:path";
25963
26118
  import { createRequire as createRequire4 } from "node:module";
26119
+ import * as path33 from "node:path";
25964
26120
  import { fileURLToPath } from "node:url";
25965
- import { WebSocketServer } from "ws";
25966
26121
  import { toErrorMessage as toErrorMessage13 } from "@wrongstack/core/utils";
26122
+ import { WebSocketServer } from "ws";
25967
26123
  async function resolvePorts(opts) {
25968
26124
  const surface = opts.surface ?? "webui";
25969
26125
  const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };