@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.
@@ -1421,7 +1421,13 @@ function indexDbVersion(projectRoot, indexDir) {
1421
1421
  try {
1422
1422
  const dir = resolveIndexDir(projectRoot, indexDir);
1423
1423
  const st = fs.statSync(path.join(dir, DB_FILE));
1424
- return `${st.mtimeMs}:${st.size}`;
1424
+ let wal = "";
1425
+ try {
1426
+ const walSt = fs.statSync(path.join(dir, `${DB_FILE}-wal`));
1427
+ wal = `:${walSt.mtimeMs}:${walSt.size}`;
1428
+ } catch {
1429
+ }
1430
+ return `${st.mtimeMs}:${st.size}${wal}`;
1425
1431
  } catch {
1426
1432
  return "missing";
1427
1433
  }
@@ -7603,12 +7609,34 @@ function handleTodosGet(ctx, ws) {
7603
7609
  payload: sessionPayload(ctx, { todos: [...ctx.context.todos] })
7604
7610
  });
7605
7611
  }
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: [] }) });
7612
+ async function commitTodos(ctx, todos) {
7613
+ if (ctx.mutateTodos) {
7614
+ const result = await ctx.mutateTodos(todos);
7615
+ return { todos: result.todos, warnings: result.warnings ?? [] };
7616
+ }
7617
+ ctx.replaceTodos?.(todos);
7618
+ return { todos: [...todos], warnings: [] };
7619
+ }
7620
+ function managedProjectionMessage() {
7621
+ return "Kanban-bound todos are task projections. Change or remove the task from Kanban.";
7622
+ }
7623
+ async function handleTodosClear(ctx, ws) {
7624
+ if (ctx.context.todos.some((todo) => todo.kanbanBoardId && todo.kanbanTaskId)) {
7625
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7626
+ return;
7627
+ }
7628
+ try {
7629
+ const result = await commitTodos(ctx, []);
7630
+ sendResult3(ctx, ws, true, "Todos cleared");
7631
+ ctx.broadcast({
7632
+ type: "todos.updated",
7633
+ payload: sessionPayload(ctx, { todos: result.todos })
7634
+ });
7635
+ } catch (error2) {
7636
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7637
+ }
7610
7638
  }
7611
- function handleTodosRemove(ctx, ws, payload) {
7639
+ async function handleTodosRemove(ctx, ws, payload) {
7612
7640
  if (!payload) {
7613
7641
  sendResult3(ctx, ws, false, "Missing id or index");
7614
7642
  return;
@@ -7625,12 +7653,27 @@ function handleTodosRemove(ctx, ws, payload) {
7625
7653
  sendResult3(ctx, ws, false, "Todo not found");
7626
7654
  return;
7627
7655
  }
7656
+ if (removed.kanbanBoardId && removed.kanbanTaskId) {
7657
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7658
+ return;
7659
+ }
7628
7660
  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 }) });
7661
+ try {
7662
+ const result = await commitTodos(ctx, next);
7663
+ sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
7664
+ ctx.broadcast({
7665
+ type: "todos.updated",
7666
+ payload: sessionPayload(ctx, { todos: result.todos })
7667
+ });
7668
+ } catch (error2) {
7669
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7670
+ }
7632
7671
  }
7633
- function handleTodoUpdate(ctx, ws, payload) {
7672
+ async function handleTodoUpdate(ctx, ws, payload) {
7673
+ 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") {
7674
+ sendResult3(ctx, ws, false, "Invalid todo update payload");
7675
+ return;
7676
+ }
7634
7677
  const index = ctx.context.todos.findIndex((todo) => todo.id === payload.id);
7635
7678
  const existing = ctx.context.todos[index];
7636
7679
  if (index === -1 || !existing) {
@@ -7643,9 +7686,25 @@ function handleTodoUpdate(ctx, ws, payload) {
7643
7686
  status: payload.status ?? existing.status,
7644
7687
  activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
7645
7688
  };
7646
- ctx.replaceTodos?.(next);
7647
- sendResult3(ctx, ws, true, `Todo "${existing.content}" updated`);
7648
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
7689
+ try {
7690
+ const result = await commitTodos(ctx, next);
7691
+ const projected = result.todos.find((todo) => todo.id === existing.id);
7692
+ const requestedStatus = payload.status ?? existing.status;
7693
+ const projectionRejected = Boolean(existing.kanbanBoardId && existing.kanbanTaskId) && projected?.status !== requestedStatus;
7694
+ const warning = result.warnings[0];
7695
+ sendResult3(
7696
+ ctx,
7697
+ ws,
7698
+ !projectionRejected,
7699
+ projectionRejected ? warning ?? `Kanban kept "${existing.content}" at ${projected?.status ?? "its current state"}.` : warning ? `Todo "${existing.content}" updated. ${warning}` : `Todo "${existing.content}" updated`
7700
+ );
7701
+ ctx.broadcast({
7702
+ type: "todos.updated",
7703
+ payload: sessionPayload(ctx, { todos: result.todos })
7704
+ });
7705
+ } catch (error2) {
7706
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7707
+ }
7649
7708
  }
7650
7709
  async function handleTasksGet(ctx, ws) {
7651
7710
  const taskPath = taskPathOf(ctx);
@@ -7673,14 +7732,32 @@ async function handleTaskUpdate(ctx, ws, payload) {
7673
7732
  return;
7674
7733
  }
7675
7734
  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}".`);
7735
+ let file;
7736
+ if (ctx.mutateTaskStatus) {
7737
+ const result = await ctx.mutateTaskStatus(payload.id, payload.status);
7738
+ if (!result.ok) {
7739
+ sendResult3(ctx, ws, false, result.message);
7740
+ return;
7741
+ }
7742
+ file = await loadTasks(taskPath);
7743
+ if (!file) throw new Error("Task mutation succeeded but its persisted snapshot is missing.");
7744
+ sendResult3(ctx, ws, true, result.message);
7745
+ } else {
7746
+ let matched = false;
7747
+ file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
7748
+ const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
7749
+ if (!task) return tasks;
7750
+ matched = true;
7751
+ task.status = payload.status;
7752
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7753
+ return tasks;
7754
+ });
7755
+ if (!matched) {
7756
+ sendResult3(ctx, ws, false, `Task "${payload.id}" not found.`);
7757
+ return;
7758
+ }
7759
+ sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
7760
+ }
7684
7761
  ctx.broadcast({
7685
7762
  type: "tasks.updated",
7686
7763
  payload: sessionPayload(ctx, { tasks: file.tasks })
@@ -7727,6 +7804,18 @@ async function handlePlanTemplateUse(ctx, ws, template) {
7727
7804
  return;
7728
7805
  }
7729
7806
  try {
7807
+ if (ctx.mutatePlan) {
7808
+ const result = await ctx.mutatePlan({ action: "template_use", template });
7809
+ if (!result.ok) {
7810
+ sendResult3(ctx, ws, false, result.message);
7811
+ return;
7812
+ }
7813
+ const plan2 = await loadPlan(planPath);
7814
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7815
+ sendResult3(ctx, ws, true, result.message);
7816
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7817
+ return;
7818
+ }
7730
7819
  const templateDefinition = getPlanTemplate(template);
7731
7820
  if (!templateDefinition) {
7732
7821
  sendResult3(ctx, ws, false, `Unknown template "${template}".`);
@@ -7755,6 +7844,22 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
7755
7844
  return;
7756
7845
  }
7757
7846
  try {
7847
+ if (ctx.mutatePlan) {
7848
+ const result = await ctx.mutatePlan({
7849
+ action: "status",
7850
+ target: payload.target,
7851
+ status: payload.status
7852
+ });
7853
+ if (!result.ok) {
7854
+ sendResult3(ctx, ws, false, result.message);
7855
+ return;
7856
+ }
7857
+ const plan2 = await loadPlan(planPath);
7858
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7859
+ sendResult3(ctx, ws, true, result.message);
7860
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7861
+ return;
7862
+ }
7758
7863
  let changed = false;
7759
7864
  const plan = await mutatePlan(planPath, currentSessionId(ctx), async (currentPlan) => {
7760
7865
  const before = currentPlan.updatedAt;
@@ -7778,13 +7883,17 @@ async function handleWorklistMessage(ctx, ws, message) {
7778
7883
  handleTodosGet(ctx, ws);
7779
7884
  return;
7780
7885
  case "todos.clear":
7781
- handleTodosClear(ctx, ws);
7886
+ await handleTodosClear(ctx, ws);
7782
7887
  return;
7783
7888
  case "todos.remove":
7784
- handleTodosRemove(ctx, ws, message.payload);
7889
+ await handleTodosRemove(
7890
+ ctx,
7891
+ ws,
7892
+ message.payload
7893
+ );
7785
7894
  return;
7786
7895
  case "todo.update":
7787
- handleTodoUpdate(
7896
+ await handleTodoUpdate(
7788
7897
  ctx,
7789
7898
  ws,
7790
7899
  message.payload
@@ -10745,9 +10854,9 @@ import {
10745
10854
  claimReadyTask,
10746
10855
  copyTaskToBoard,
10747
10856
  createBoard,
10857
+ createBoardFromText,
10748
10858
  duplicateBoard,
10749
10859
  exportBoardToTaskGraph,
10750
- createBoardFromText,
10751
10860
  getBoard,
10752
10861
  getKanbanOrchestrationSnapshot,
10753
10862
  getKanbanQueueHealth,
@@ -10935,8 +11044,35 @@ async function handleTaskVerification(ws, type, payload, ctx) {
10935
11044
  }
10936
11045
  }
10937
11046
 
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
+
10938
11073
  // src/server/kanban-task-routes.ts
10939
11074
  import {
11075
+ getKanbanWorkbench,
10940
11076
  getTask,
10941
11077
  listTaskActivity,
10942
11078
  recordTaskActivity,
@@ -10944,6 +11080,16 @@ import {
10944
11080
  } from "@wrongstack/kanban";
10945
11081
  async function handleKanbanTaskRoute(ws, type, payload, ctx) {
10946
11082
  switch (type) {
11083
+ case "kanban.workbench":
11084
+ ok(
11085
+ ws,
11086
+ type,
11087
+ await getKanbanWorkbench(ctx.projectRoot, {
11088
+ ...typeof payload?.limitPerLane === "number" ? { limitPerLane: payload.limitPerLane } : {},
11089
+ ...typeof payload?.alertLimit === "number" ? { alertLimit: payload.alertLimit } : {}
11090
+ })
11091
+ );
11092
+ return true;
10947
11093
  case "kanban.task.remove":
10948
11094
  await handleTaskRemove(ws, type, payload, ctx);
10949
11095
  return true;
@@ -11036,40 +11182,11 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
11036
11182
  outcome,
11037
11183
  ...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
11038
11184
  },
11039
- activityContext(
11040
- ctx,
11041
- payload?.actor ?? ctx.context?.agentId ?? "webui"
11042
- )
11185
+ activityContext(ctx, payload?.actor ?? ctx.context?.agentId ?? "webui")
11043
11186
  );
11044
11187
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
11045
11188
  }
11046
11189
 
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
11190
  // src/server/kanban-routes.ts
11074
11191
  async function handleKanbanRoute(ws, msg, ctx) {
11075
11192
  if (!msg.type.startsWith("kanban.")) return false;
@@ -11848,7 +11965,9 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
11848
11965
  projectRoot,
11849
11966
  async (event) => {
11850
11967
  const family = event.event?.split(".")[0];
11851
- if (family !== "board" && family !== "task" && family !== "column") return;
11968
+ if (family !== "board" && family !== "task" && family !== "column" && family !== "contract") {
11969
+ return;
11970
+ }
11852
11971
  const evData = event.data;
11853
11972
  const boardId = evData?.boardId;
11854
11973
  if (!boardId) return;
@@ -13160,14 +13279,17 @@ async function handleSageForFile(ws, msg, memoryStore) {
13160
13279
  send(ws, { type: "memory.sage.forFile", payload: { error: "filePath is required" } });
13161
13280
  return;
13162
13281
  }
13282
+ const includeSuperseded = typeof payload["showSuperseded"] === "boolean" ? payload["showSuperseded"] : typeof payload["includeSuperseded"] === "boolean" ? payload["includeSuperseded"] : void 0;
13283
+ const includeDeleted = payload["showDeleted"] === true || payload["includeDeleted"] === true;
13163
13284
  try {
13164
13285
  const response = await Sage.findMemoriesForFile(filePath, {
13165
13286
  ...typeof payload["lineStart"] === "number" ? { lineStart: payload["lineStart"] } : {},
13166
13287
  ...typeof payload["lineEnd"] === "number" ? { lineEnd: payload["lineEnd"] } : {},
13167
13288
  ...typeof payload["limit"] === "number" ? { limit: payload["limit"] } : {},
13168
- ...payload["includeDeleted"] === true ? { includeDeleted: true } : {}
13289
+ ...includeSuperseded !== void 0 ? { includeSuperseded } : {},
13290
+ ...includeDeleted ? { includeDeleted: true } : {}
13169
13291
  });
13170
- send(ws, { type: "memory.sage.forFile", payload: response });
13292
+ send(ws, { type: "memory.sage.forFile", payload: { response } });
13171
13293
  } catch (err) {
13172
13294
  send(ws, { type: "memory.sage.forFile", payload: { error: errMessage(err) } });
13173
13295
  }
@@ -13905,7 +14027,8 @@ function createKanbanSupervisor(deps2) {
13905
14027
  mode: config.recoveryMode ?? "auto",
13906
14028
  reason: "Kanban supervisor found an expired worker lease."
13907
14029
  }) : null;
13908
- if (recovered) health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
14030
+ if (recovered)
14031
+ health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
13909
14032
  const anomalyCount = countAnomalies(health);
13910
14033
  const snapshot = {
13911
14034
  boardId: board.id,
@@ -14000,7 +14123,9 @@ function createKanbanSupervisor(deps2) {
14000
14123
  } else {
14001
14124
  const summaries = await listBoards3(resolveProjectRoot(deps2));
14002
14125
  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));
14126
+ boards = (await Promise.all(
14127
+ summaries.map((summary) => getBoard2(resolveProjectRoot(deps2), summary.id))
14128
+ )).filter((board) => Boolean(board));
14004
14129
  }
14005
14130
  const results = [];
14006
14131
  for (const board of boards) results.push(await auditBoard(board));
@@ -17451,85 +17576,51 @@ function createSessionHandlers(ctx) {
17451
17576
  // src/server/agent-roster-handlers.ts
17452
17577
  import {
17453
17578
  applyProjectAgentConfig,
17454
- buildConsolidationInstruction,
17455
17579
  captureLearnedFromAgentOutputDetailed,
17456
17580
  clearProjectAgentConsolidated,
17581
+ clearProjectSkillAugmentation,
17457
17582
  createProjectAgent,
17458
17583
  detectLearnedConflicts,
17584
+ evaluateAutoOptimize,
17459
17585
  FLEET_ROSTER,
17460
17586
  getProjectAgentLearnStats,
17461
17587
  isConsolidated,
17462
17588
  listProjectAgentLearnedEntries,
17463
17589
  listProjectAgentRoles,
17590
+ listProjectSkillAugmentations,
17464
17591
  loadConsolidationMetadata,
17465
17592
  loadProjectAgentConfig,
17466
17593
  loadProjectAgentConsolidated,
17467
17594
  loadProjectAgentIdentity,
17468
17595
  loadProjectAgentLearned,
17469
17596
  loadProjectAgentProfile,
17597
+ loadProjectSkillAugmentation,
17598
+ loadSkillAffinity,
17599
+ optimizeProjectAgentLearning,
17600
+ readRawLearnedEntries,
17470
17601
  resetProjectAgentIdentity,
17602
+ resolveAutoOptimizePolicy,
17603
+ resolveRoleSkillCandidates,
17471
17604
  saveProjectAgentConsolidated,
17605
+ saveProjectSkillAugmentation,
17606
+ setSkillPinned,
17472
17607
  slugifyProjectAgentRole,
17473
17608
  updateProjectAgentConfig,
17474
17609
  updateProjectAgentIdentity,
17475
17610
  updateProjectAgentLearned,
17476
17611
  updateProjectAgentLearningPolicy
17477
17612
  } from "@wrongstack/core/coordination";
17478
- import { isTextBlock } from "@wrongstack/core/types";
17479
- var CONSOLIDATION_MAX_TOKENS = 8e3;
17480
- var CONSOLIDATION_TIMEOUT_MS = 12e4;
17481
17613
  var AgentRosterWSHandler = class {
17482
17614
  getProjectRoot;
17483
17615
  getLlm;
17484
17616
  broadcast;
17617
+ getAutoOptimizeSettings;
17485
17618
  constructor(opts) {
17486
17619
  this.getProjectRoot = typeof opts.projectRoot === "function" ? opts.projectRoot : () => opts.projectRoot;
17487
17620
  this.getLlm = opts.getLlm ?? (() => void 0);
17488
17621
  this.broadcast = opts.broadcast ?? (() => {
17489
17622
  });
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
- }
17623
+ this.getAutoOptimizeSettings = opts.getAutoOptimizeSettings;
17533
17624
  }
17534
17625
  /** Handle an incoming client message. Returns a response payload. */
17535
17626
  async handleMessage(_ws, type, payload) {
@@ -17734,70 +17825,47 @@ ${String(p.content ?? "")}`;
17734
17825
  const conflicts = detectLearnedConflicts(projectRoot);
17735
17826
  return { type, payload: { conflicts } };
17736
17827
  }
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": {
17828
+ // ── Optimize: distil captures into skill addenda + a consolidated doc,
17829
+ // then archive and reset the raw buffer. Shared implementation with the
17830
+ // CLI (`optimizeProjectAgentLearning`) so both surfaces persist the same
17831
+ // artifacts instead of the CLI producing markdown nobody saved.
17832
+ case "agent-roster.consolidate":
17833
+ case "agent-roster.optimize": {
17744
17834
  if (!role) return { type, payload: { error: "role required" } };
17745
- const { instruction, rawEntries, hasExistingConsolidation } = buildConsolidationInstruction(role, projectRoot);
17746
- if (rawEntries.length === 0) {
17835
+ const hasExistingConsolidation = isConsolidated(role, projectRoot);
17836
+ const pending = readRawLearnedEntries(role, projectRoot);
17837
+ if (pending.length === 0) {
17747
17838
  return {
17748
17839
  type: "agent-roster.consolidate",
17749
17840
  payload: {
17750
17841
  role,
17751
17842
  consolidated: false,
17752
17843
  rawEntryCount: 0,
17844
+ skills: [],
17753
17845
  hasExistingConsolidation,
17754
17846
  currentStats: getProjectAgentLearnStats(role, projectRoot)
17755
17847
  }
17756
17848
  };
17757
17849
  }
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
- }
17850
+ const llm = this.getLlm();
17851
+ const result = await optimizeProjectAgentLearning(role, projectRoot, {
17852
+ ...llm ? { llm } : {},
17853
+ trigger: "manual"
17854
+ });
17855
+ const currentStats = getProjectAgentLearnStats(role, projectRoot);
17856
+ const metadata = loadConsolidationMetadata(role, projectRoot);
17857
+ const basePayload = {
17858
+ role,
17859
+ rawEntryCount: result.rawEntryCount,
17860
+ skills: result.skills,
17861
+ hasExistingConsolidation,
17862
+ currentStats
17863
+ };
17864
+ if (result.status === "optimized") {
17797
17865
  try {
17798
17866
  this.broadcast({
17799
17867
  type: "agent-roster.updated",
17800
- payload: { role, reason: "consolidated", currentStats: stats, metadata }
17868
+ payload: { role, reason: "consolidated", currentStats, metadata }
17801
17869
  });
17802
17870
  } catch (e) {
17803
17871
  console.warn(
@@ -17813,44 +17881,101 @@ ${String(p.content ?? "")}`;
17813
17881
  return {
17814
17882
  type: "agent-roster.consolidate",
17815
17883
  payload: {
17816
- role,
17884
+ ...basePayload,
17817
17885
  consolidated: true,
17818
- rawEntryCount: rawEntries.length,
17819
- content: synth.content,
17820
- model: synth.model,
17821
- currentStats: stats,
17886
+ content: result.content,
17887
+ model: result.model,
17888
+ pruned: result.pruned,
17822
17889
  metadata
17823
17890
  }
17824
17891
  };
17825
17892
  }
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
17893
  return {
17841
17894
  type: "agent-roster.consolidate",
17842
17895
  payload: {
17843
- role,
17896
+ ...basePayload,
17844
17897
  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.`
17898
+ ...result.status === "empty-synthesis" ? { emptySynthesis: true, model: result.model } : {},
17899
+ ...result.status === "failed" ? { error: result.error } : {},
17900
+ ...result.status === "no-llm" ? {
17901
+ instruction: result.instruction,
17902
+ 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.`
17903
+ } : {}
17851
17904
  }
17852
17905
  };
17853
17906
  }
17907
+ // ── Automatic-optimization status ─────────────────────────────────
17908
+ // Read-only: says whether the background scheduler considers each role
17909
+ // eligible right now, and why not when it does not. Surfacing the reason
17910
+ // is what keeps "nothing happened" from looking like a broken feature.
17911
+ case "agent-roster.auto-optimize-status": {
17912
+ const policy = resolveAutoOptimizePolicy(
17913
+ this.getAutoOptimizeSettings?.() ?? void 0
17914
+ );
17915
+ const roles = role ? [role] : listProjectAgentRoles(projectRoot);
17916
+ return {
17917
+ type,
17918
+ payload: {
17919
+ policy,
17920
+ roles: roles.map((current2) => {
17921
+ try {
17922
+ const decision = evaluateAutoOptimize(current2, projectRoot, policy);
17923
+ return { role: current2, ...decision };
17924
+ } catch {
17925
+ return { role: current2, eligible: false, reason: "disabled" };
17926
+ }
17927
+ })
17928
+ }
17929
+ };
17930
+ }
17931
+ // ── Skill layer: what this project has developed for each role skill ──
17932
+ case "agent-roster.skills": {
17933
+ if (!role) return { type, payload: { error: "role required" } };
17934
+ const candidates = resolveRoleSkillCandidates(role, projectRoot);
17935
+ const developed = listProjectSkillAugmentations(role, projectRoot);
17936
+ const affinity = loadSkillAffinity(role, projectRoot);
17937
+ return {
17938
+ type,
17939
+ payload: {
17940
+ role,
17941
+ skills: candidates.map((skill) => ({
17942
+ skill,
17943
+ developed: developed.includes(skill),
17944
+ affinity: affinity.entries[skill] ?? null
17945
+ }))
17946
+ }
17947
+ };
17948
+ }
17949
+ case "agent-roster.read-skill": {
17950
+ const skill = typeof p.skill === "string" ? p.skill : "";
17951
+ if (!role || !skill) return { type, payload: { error: "role and skill required" } };
17952
+ return {
17953
+ type,
17954
+ payload: { role, skill, content: loadProjectSkillAugmentation(role, skill, projectRoot) }
17955
+ };
17956
+ }
17957
+ case "agent-roster.save-skill": {
17958
+ const skill = typeof p.skill === "string" ? p.skill : "";
17959
+ if (!role || !skill || typeof p.content !== "string") {
17960
+ return { type, payload: { error: "role, skill and content required" } };
17961
+ }
17962
+ const savedPath = saveProjectSkillAugmentation(role, skill, p.content, projectRoot);
17963
+ return { type, payload: { role, skill, path: savedPath, success: true } };
17964
+ }
17965
+ case "agent-roster.clear-skill": {
17966
+ const skill = typeof p.skill === "string" ? p.skill : "";
17967
+ if (!role) return { type, payload: { error: "role required" } };
17968
+ clearProjectSkillAugmentation(role, skill || void 0, projectRoot);
17969
+ return { type, payload: { role, skill: skill || null, success: true } };
17970
+ }
17971
+ case "agent-roster.pin-skill": {
17972
+ const skill = typeof p.skill === "string" ? p.skill : "";
17973
+ if (!role || !skill || typeof p.pinned !== "boolean") {
17974
+ return { type, payload: { error: "role, skill and boolean pinned required" } };
17975
+ }
17976
+ const affinity = setSkillPinned(role, skill, p.pinned, projectRoot);
17977
+ return { type, payload: { role, skill, pinned: p.pinned, affinity, success: true } };
17978
+ }
17854
17979
  // ── Save consolidated document ────────────────────────────────────
17855
17980
  case "agent-roster.save-consolidated": {
17856
17981
  if (!role || typeof p.content !== "string") {
@@ -22429,6 +22554,7 @@ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES)
22429
22554
 
22430
22555
  // src/server/message-dispatcher.ts
22431
22556
  import path23 from "node:path";
22557
+ import { planTool, taskTool, todoTool } from "@wrongstack/tools";
22432
22558
  function createMessageDispatcher(opts) {
22433
22559
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
22434
22560
  function makeWorklistContext() {
@@ -22440,7 +22566,20 @@ function createMessageDispatcher(opts) {
22440
22566
  },
22441
22567
  send: (w, m) => send(w, m),
22442
22568
  broadcast: (m) => broadcast(state.getClients(), m),
22443
- replaceTodos: (todos) => deps2.context.state.replaceTodos(todos)
22569
+ replaceTodos: (todos) => deps2.context.state.replaceTodos(todos),
22570
+ mutateTodos: async (todos) => {
22571
+ const result = await todoTool.execute({ todos }, deps2.context, {
22572
+ signal: AbortSignal.timeout(3e4)
22573
+ });
22574
+ return {
22575
+ todos: [...deps2.context.todos],
22576
+ ...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
22577
+ };
22578
+ },
22579
+ mutateTaskStatus: async (id, status) => taskTool.execute({ action: "status", id, status }, deps2.context, {
22580
+ signal: AbortSignal.timeout(3e4)
22581
+ }),
22582
+ mutatePlan: async (operation) => planTool.execute(operation, deps2.context, { signal: AbortSignal.timeout(3e4) })
22444
22583
  };
22445
22584
  }
22446
22585
  function makeSkillsContext() {
@@ -22652,6 +22791,7 @@ function createMessageDispatcher(opts) {
22652
22791
  agentRoster: {
22653
22792
  rosterHandler: new AgentRosterWSHandler({
22654
22793
  projectRoot: state.getProjectRoot,
22794
+ getAutoOptimizeSettings: () => state.getConfig().fleet?.learning?.autoOptimize,
22655
22795
  getLlm: () => {
22656
22796
  const ctx = deps2.agent.ctx;
22657
22797
  return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
@@ -23862,11 +24002,11 @@ function buildRoutes(state, deps2, cb) {
23862
24002
  }
23863
24003
 
23864
24004
  // src/server/server-runtime.ts
23865
- import * as path28 from "node:path";
23866
24005
  import { createRequire as createRequire4 } from "node:module";
24006
+ import * as path28 from "node:path";
23867
24007
  import { fileURLToPath } from "node:url";
23868
- import { WebSocketServer } from "ws";
23869
24008
  import { toErrorMessage as toErrorMessage12 } from "@wrongstack/core/utils";
24009
+ import { WebSocketServer } from "ws";
23870
24010
  async function resolvePorts(opts) {
23871
24011
  const surface = opts.surface ?? "webui";
23872
24012
  const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };