@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 +343 -187
- package/dist/server/agent-roster-handlers.d.ts +7 -6
- package/dist/server/codemap-cache.d.ts +13 -1
- package/dist/server/entry.js +325 -185
- package/dist/server/handlers/worklist-handlers.d.ts +23 -4
- package/dist/server/handlers.js +126 -23
- package/dist/server/kanban-route-protocol.d.ts +1 -1
- package/dist/server/kanban-routes.d.ts +2 -2
- package/dist/server/server-runtime.d.ts +14 -2
- package/package.json +11 -11
package/dist/server/entry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
7607
|
-
ctx.
|
|
7608
|
-
|
|
7609
|
-
|
|
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
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
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
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
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
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
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(
|
|
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")
|
|
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
|
-
...
|
|
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)
|
|
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(
|
|
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
|
-
// ──
|
|
17738
|
-
//
|
|
17739
|
-
//
|
|
17740
|
-
//
|
|
17741
|
-
|
|
17742
|
-
|
|
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
|
|
17746
|
-
|
|
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
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
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
|
|
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
|
-
|
|
17884
|
+
...basePayload,
|
|
17817
17885
|
consolidated: true,
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
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
|
-
|
|
17896
|
+
...basePayload,
|
|
17844
17897
|
consolidated: false,
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
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 };
|