@wrongstack/webui-server 0.302.0 → 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.
@@ -12,22 +12,41 @@ export interface WorklistContext {
12
12
  send: (ws: WebSocket, msg: WSServerMessage) => void;
13
13
  broadcast: (msg: WSServerMessage) => void;
14
14
  replaceTodos?: ((todos: TodoItem[]) => void) | undefined;
15
+ mutateTodos?: ((todos: TodoItem[]) => Promise<{
16
+ todos: TodoItem[];
17
+ warnings?: string[] | undefined;
18
+ }>) | undefined;
19
+ mutateTaskStatus?: ((id: string, status: 'pending' | 'in_progress' | 'blocked' | 'failed' | 'review' | 'completed') => Promise<{
20
+ ok: boolean;
21
+ message: string;
22
+ }>) | undefined;
23
+ mutatePlan?: ((operation: {
24
+ action: 'template_use';
25
+ template: string;
26
+ } | {
27
+ action: 'status';
28
+ target: string;
29
+ status: 'open' | 'in_progress' | 'done';
30
+ }) => Promise<{
31
+ ok: boolean;
32
+ message: string;
33
+ }>) | undefined;
15
34
  }
16
35
  export interface WorklistMessage {
17
36
  type: string;
18
37
  payload?: unknown;
19
38
  }
20
39
  export declare function handleTodosGet(ctx: WorklistContext, ws: WebSocket): void;
21
- export declare function handleTodosClear(ctx: WorklistContext, ws: WebSocket): void;
40
+ export declare function handleTodosClear(ctx: WorklistContext, ws: WebSocket): Promise<void>;
22
41
  export declare function handleTodosRemove(ctx: WorklistContext, ws: WebSocket, payload: {
23
42
  id?: string | undefined;
24
43
  index?: number | undefined;
25
- } | undefined): void;
44
+ } | undefined): Promise<void>;
26
45
  export declare function handleTodoUpdate(ctx: WorklistContext, ws: WebSocket, payload: {
27
- id: string;
46
+ id?: string | undefined;
28
47
  status?: TodoItem['status'] | undefined;
29
48
  activeForm?: string | undefined;
30
- }): void;
49
+ } | undefined): Promise<void>;
31
50
  export declare function handleTasksGet(ctx: WorklistContext, ws: WebSocket): Promise<void>;
32
51
  export declare function handleTaskUpdate(ctx: WorklistContext, ws: WebSocket, payload: {
33
52
  id: string;
@@ -55,12 +55,34 @@ function handleTodosGet(ctx, ws) {
55
55
  payload: sessionPayload(ctx, { todos: [...ctx.context.todos] })
56
56
  });
57
57
  }
58
- function handleTodosClear(ctx, ws) {
59
- ctx.replaceTodos?.([]);
60
- sendResult(ctx, ws, true, "Todos cleared");
61
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: [] }) });
58
+ async function commitTodos(ctx, todos) {
59
+ if (ctx.mutateTodos) {
60
+ const result = await ctx.mutateTodos(todos);
61
+ return { todos: result.todos, warnings: result.warnings ?? [] };
62
+ }
63
+ ctx.replaceTodos?.(todos);
64
+ return { todos: [...todos], warnings: [] };
65
+ }
66
+ function managedProjectionMessage() {
67
+ return "Kanban-bound todos are task projections. Change or remove the task from Kanban.";
62
68
  }
63
- function handleTodosRemove(ctx, ws, payload) {
69
+ async function handleTodosClear(ctx, ws) {
70
+ if (ctx.context.todos.some((todo) => todo.kanbanBoardId && todo.kanbanTaskId)) {
71
+ sendResult(ctx, ws, false, managedProjectionMessage());
72
+ return;
73
+ }
74
+ try {
75
+ const result = await commitTodos(ctx, []);
76
+ sendResult(ctx, ws, true, "Todos cleared");
77
+ ctx.broadcast({
78
+ type: "todos.updated",
79
+ payload: sessionPayload(ctx, { todos: result.todos })
80
+ });
81
+ } catch (error) {
82
+ sendResult(ctx, ws, false, error instanceof Error ? error.message : String(error));
83
+ }
84
+ }
85
+ async function handleTodosRemove(ctx, ws, payload) {
64
86
  if (!payload) {
65
87
  sendResult(ctx, ws, false, "Missing id or index");
66
88
  return;
@@ -77,12 +99,27 @@ function handleTodosRemove(ctx, ws, payload) {
77
99
  sendResult(ctx, ws, false, "Todo not found");
78
100
  return;
79
101
  }
102
+ if (removed.kanbanBoardId && removed.kanbanTaskId) {
103
+ sendResult(ctx, ws, false, managedProjectionMessage());
104
+ return;
105
+ }
80
106
  const next = [...todos.slice(0, targetIndex), ...todos.slice(targetIndex + 1)];
81
- ctx.replaceTodos?.(next);
82
- sendResult(ctx, ws, true, `Removed: ${removed.content}`);
83
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
107
+ try {
108
+ const result = await commitTodos(ctx, next);
109
+ sendResult(ctx, ws, true, `Removed: ${removed.content}`);
110
+ ctx.broadcast({
111
+ type: "todos.updated",
112
+ payload: sessionPayload(ctx, { todos: result.todos })
113
+ });
114
+ } catch (error) {
115
+ sendResult(ctx, ws, false, error instanceof Error ? error.message : String(error));
116
+ }
84
117
  }
85
- function handleTodoUpdate(ctx, ws, payload) {
118
+ async function handleTodoUpdate(ctx, ws, payload) {
119
+ 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") {
120
+ sendResult(ctx, ws, false, "Invalid todo update payload");
121
+ return;
122
+ }
86
123
  const index = ctx.context.todos.findIndex((todo) => todo.id === payload.id);
87
124
  const existing = ctx.context.todos[index];
88
125
  if (index === -1 || !existing) {
@@ -95,9 +132,25 @@ function handleTodoUpdate(ctx, ws, payload) {
95
132
  status: payload.status ?? existing.status,
96
133
  activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
97
134
  };
98
- ctx.replaceTodos?.(next);
99
- sendResult(ctx, ws, true, `Todo "${existing.content}" updated`);
100
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
135
+ try {
136
+ const result = await commitTodos(ctx, next);
137
+ const projected = result.todos.find((todo) => todo.id === existing.id);
138
+ const requestedStatus = payload.status ?? existing.status;
139
+ const projectionRejected = Boolean(existing.kanbanBoardId && existing.kanbanTaskId) && projected?.status !== requestedStatus;
140
+ const warning = result.warnings[0];
141
+ sendResult(
142
+ ctx,
143
+ ws,
144
+ !projectionRejected,
145
+ projectionRejected ? warning ?? `Kanban kept "${existing.content}" at ${projected?.status ?? "its current state"}.` : warning ? `Todo "${existing.content}" updated. ${warning}` : `Todo "${existing.content}" updated`
146
+ );
147
+ ctx.broadcast({
148
+ type: "todos.updated",
149
+ payload: sessionPayload(ctx, { todos: result.todos })
150
+ });
151
+ } catch (error) {
152
+ sendResult(ctx, ws, false, error instanceof Error ? error.message : String(error));
153
+ }
101
154
  }
102
155
  async function handleTasksGet(ctx, ws) {
103
156
  const taskPath = taskPathOf(ctx);
@@ -125,14 +178,32 @@ async function handleTaskUpdate(ctx, ws, payload) {
125
178
  return;
126
179
  }
127
180
  try {
128
- const file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
129
- const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
130
- if (!task) return tasks;
131
- task.status = payload.status;
132
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
133
- return tasks;
134
- });
135
- sendResult(ctx, ws, true, `Task status updated to "${payload.status}".`);
181
+ let file;
182
+ if (ctx.mutateTaskStatus) {
183
+ const result = await ctx.mutateTaskStatus(payload.id, payload.status);
184
+ if (!result.ok) {
185
+ sendResult(ctx, ws, false, result.message);
186
+ return;
187
+ }
188
+ file = await loadTasks(taskPath);
189
+ if (!file) throw new Error("Task mutation succeeded but its persisted snapshot is missing.");
190
+ sendResult(ctx, ws, true, result.message);
191
+ } else {
192
+ let matched = false;
193
+ file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
194
+ const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
195
+ if (!task) return tasks;
196
+ matched = true;
197
+ task.status = payload.status;
198
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
199
+ return tasks;
200
+ });
201
+ if (!matched) {
202
+ sendResult(ctx, ws, false, `Task "${payload.id}" not found.`);
203
+ return;
204
+ }
205
+ sendResult(ctx, ws, true, `Task status updated to "${payload.status}".`);
206
+ }
136
207
  ctx.broadcast({
137
208
  type: "tasks.updated",
138
209
  payload: sessionPayload(ctx, { tasks: file.tasks })
@@ -179,6 +250,18 @@ async function handlePlanTemplateUse(ctx, ws, template) {
179
250
  return;
180
251
  }
181
252
  try {
253
+ if (ctx.mutatePlan) {
254
+ const result = await ctx.mutatePlan({ action: "template_use", template });
255
+ if (!result.ok) {
256
+ sendResult(ctx, ws, false, result.message);
257
+ return;
258
+ }
259
+ const plan2 = await loadPlan(planPath);
260
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
261
+ sendResult(ctx, ws, true, result.message);
262
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
263
+ return;
264
+ }
182
265
  const templateDefinition = getPlanTemplate(template);
183
266
  if (!templateDefinition) {
184
267
  sendResult(ctx, ws, false, `Unknown template "${template}".`);
@@ -207,6 +290,22 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
207
290
  return;
208
291
  }
209
292
  try {
293
+ if (ctx.mutatePlan) {
294
+ const result = await ctx.mutatePlan({
295
+ action: "status",
296
+ target: payload.target,
297
+ status: payload.status
298
+ });
299
+ if (!result.ok) {
300
+ sendResult(ctx, ws, false, result.message);
301
+ return;
302
+ }
303
+ const plan2 = await loadPlan(planPath);
304
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
305
+ sendResult(ctx, ws, true, result.message);
306
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
307
+ return;
308
+ }
210
309
  let changed = false;
211
310
  const plan = await mutatePlan(planPath, currentSessionId(ctx), async (currentPlan) => {
212
311
  const before = currentPlan.updatedAt;
@@ -230,13 +329,17 @@ async function handleWorklistMessage(ctx, ws, message) {
230
329
  handleTodosGet(ctx, ws);
231
330
  return;
232
331
  case "todos.clear":
233
- handleTodosClear(ctx, ws);
332
+ await handleTodosClear(ctx, ws);
234
333
  return;
235
334
  case "todos.remove":
236
- handleTodosRemove(ctx, ws, message.payload);
335
+ await handleTodosRemove(
336
+ ctx,
337
+ ws,
338
+ message.payload
339
+ );
237
340
  return;
238
341
  case "todo.update":
239
- handleTodoUpdate(
342
+ await handleTodoUpdate(
240
343
  ctx,
241
344
  ws,
242
345
  message.payload
@@ -1,2 +1,2 @@
1
- export declare const KANBAN_CLIENT_MESSAGE_TYPES: readonly ['kanban.capabilities', 'kanban.column.add', 'kanban.column.remove', 'kanban.create', 'kanban.decomposition.approve', 'kanban.decomposition.reject', 'kanban.delete', 'kanban.duplicate', 'kanban.generate', 'kanban.get', 'kanban.health', 'kanban.list', 'kanban.snapshot', 'kanban.supervisor.audit', 'kanban.supervisor.status', 'kanban.task.activity', 'kanban.task.activity.add', 'kanban.task.add', 'kanban.task.assign', 'kanban.task.chain', 'kanban.task.chain.get', 'kanban.task.check.add', 'kanban.task.check.update', 'kanban.task.claim', 'kanban.task.copy', 'kanban.task.dispatch', 'kanban.task.get', 'kanban.task.merge', 'kanban.task.metric.add', 'kanban.task.metric.update', 'kanban.task.move', 'kanban.task.note.add', 'kanban.task.ready', 'kanban.task.release', 'kanban.task.remove', 'kanban.task.split', 'kanban.task.transfer', 'kanban.task.transition', 'kanban.task.update', 'kanban.task.verify', 'kanban.taskgraph.export', 'kanban.taskgraph.sync', 'kanban.update'];
1
+ export declare const KANBAN_CLIENT_MESSAGE_TYPES: readonly ['kanban.capabilities', 'kanban.column.add', 'kanban.column.remove', 'kanban.create', 'kanban.decomposition.approve', 'kanban.decomposition.reject', 'kanban.delete', 'kanban.duplicate', 'kanban.generate', 'kanban.get', 'kanban.health', 'kanban.list', 'kanban.snapshot', 'kanban.supervisor.audit', 'kanban.supervisor.status', 'kanban.task.activity', 'kanban.task.activity.add', 'kanban.task.add', 'kanban.task.assign', 'kanban.task.chain', 'kanban.task.chain.get', 'kanban.task.check.add', 'kanban.task.check.update', 'kanban.task.claim', 'kanban.task.copy', 'kanban.task.dispatch', 'kanban.task.get', 'kanban.task.merge', 'kanban.task.metric.add', 'kanban.task.metric.update', 'kanban.task.move', 'kanban.task.note.add', 'kanban.task.ready', 'kanban.task.release', 'kanban.task.remove', 'kanban.task.split', 'kanban.task.transfer', 'kanban.task.transition', 'kanban.task.update', 'kanban.task.verify', 'kanban.taskgraph.export', 'kanban.taskgraph.sync', 'kanban.update', 'kanban.workbench'];
2
2
  //# sourceMappingURL=kanban-route-protocol.d.ts.map
@@ -1,14 +1,22 @@
1
1
  import type { Context } from '@wrongstack/core/agent';
2
2
  import type { WebSocket } from 'ws';
3
- import type { WSClientMessage, WSServerMessage } from './types.js';
4
3
  import { type KanbanTaskDispatcher } from './kanban-dispatch.js';
4
+ import type { KanbanSupervisor } from './kanban-supervisor.js';
5
+ import type { WSClientMessage, WSServerMessage } from './types.js';
6
+ export { type KanbanBoardPage, paginateKanbanBoards } from './kanban-route-pagination.js';
5
7
  export { KANBAN_CLIENT_MESSAGE_TYPES } from './kanban-route-protocol.js';
6
- export { paginateKanbanBoards, type KanbanBoardPage } from './kanban-route-pagination.js';
7
8
  export interface KanbanRouteContext {
8
9
  projectRoot: string;
9
10
  context?: Context | undefined;
10
11
  broadcast?: ((msg: WSServerMessage) => void) | undefined;
11
12
  dispatchTask?: KanbanTaskDispatcher | undefined;
13
+ /**
14
+ * Background supervisor (if enabled). When present, the `kanban.supervisor.audit`
15
+ * and `kanban.supervisor.status` routes delegate to its in-process audit cycle
16
+ * instead of running a second reconcile/health/recover chain — preventing the
17
+ * double-audit that happens when both paths refresh the same board concurrently.
18
+ */
19
+ supervisor?: KanbanSupervisor | undefined;
12
20
  }
13
21
  export declare function handleKanbanRoute(ws: WebSocket, msg: WSClientMessage, ctx: KanbanRouteContext): Promise<boolean>;
14
22
  //# sourceMappingURL=kanban-routes.d.ts.map
@@ -25,7 +25,7 @@ export interface KanbanSupervisorDispatchOptions {
25
25
  }) => void | Promise<void>) | undefined;
26
26
  }
27
27
  export interface KanbanSupervisorDeps {
28
- projectRoot: string;
28
+ projectRoot: string | (() => string);
29
29
  broadcast: (message: {
30
30
  type: string;
31
31
  payload: unknown;
@@ -38,6 +38,13 @@ export interface LifecycleResources {
38
38
  * logged, never thrown — cleanup must not block a clean shutdown.
39
39
  */
40
40
  onShutdown?: (() => Promise<void> | void) | undefined;
41
+ /**
42
+ * Fires **before** any HTTP/WS servers close. Use this for cleanup that must
43
+ * finish while the network is still up — e.g. telling a Kanban supervisor
44
+ * to flush its periodic tick so no in-flight `kanban.*` broadcast races a
45
+ * `WebSocketServer.close()`.
46
+ */
47
+ onPreShutdown?: (() => Promise<void> | void) | undefined;
41
48
  /** Output sink. Defaults to `console.log`. */
42
49
  log?: ((msg: string) => void) | undefined;
43
50
  /** Process exit. Defaults to `process.exit`. Injectable for tests. */
@@ -29,6 +29,9 @@ import type { ConnectedClient, WSClientMessage } from './types.js';
29
29
  interface RunLockControl {
30
30
  get(): AbortController | null;
31
31
  set(ctrl: AbortController | null): void;
32
+ /** Session ID that owns the active run, or null when idle. */
33
+ getSession(): string | null;
34
+ setSession(id: string | null): void;
32
35
  }
33
36
  interface MessageDispatcherOptions {
34
37
  state: WebuiMutableState;
@@ -47,6 +50,13 @@ interface MessageDispatcherOptions {
47
50
  runLock: RunLockControl;
48
51
  /** Pending permission confirmations — tool.confirm_result resolves one. */
49
52
  pendingConfirms: Map<string, PendingConfirm>;
53
+ /**
54
+ * Caller-supplied register hook. The dispatcher calls this **once** during
55
+ * construction to hand its disposer to the caller. The caller is
56
+ * responsible for invoking the registered disposer during its own shutdown
57
+ * — the dispatcher itself never invokes the disposer.
58
+ */
59
+ onDispose?: ((disposer: () => void) => void) | undefined;
50
60
  }
51
61
  /**
52
62
  * Build the inbound message dispatcher. Mirrors the `handleMessage` closure
@@ -1,6 +1,18 @@
1
+ /**
2
+ * Server lifecycle helpers for the standalone WebUI server.
3
+ *
4
+ * Phase 1e of the god-module split: the port resolution, WS auth/server
5
+ * creation, event arming, session-start payload builder, HTTP server
6
+ * startup, and graceful shutdown registration all moved here from
7
+ * `start-webui.ts` so the orchestrator reads as connect-the-dots.
8
+ *
9
+ * Each function is a pure construction step — no behaviour change. The
10
+ * WS/HTTP/shutdown wiring that used to be inline (~370 lines) now lives
11
+ * behind four focused entry points.
12
+ */
1
13
  import type { Config, ModelsRegistry } from '@wrongstack/core/types';
2
- import { WebSocketServer, type WebSocket } from 'ws';
3
- import { setupEvents, type FileWatcherMetrics } from './setup-events.js';
14
+ import { type WebSocket, WebSocketServer } from 'ws';
15
+ import { type FileWatcherMetrics, setupEvents } from './setup-events.js';
4
16
  import type { ConnectedClient } from './types.js';
5
17
  interface ResolvedPorts {
6
18
  wsHost: string;
@@ -110,8 +122,15 @@ interface ShutdownDeps {
110
122
  flushSession: () => Promise<void>;
111
123
  clients: () => IterableIterator<WebSocket>;
112
124
  servers: Array<import('node:http').Server | WebSocketServer>;
125
+ /**
126
+ * Fires **before** any HTTP/WS servers close. Use this for cleanup that must
127
+ * finish while the network is still up — e.g. telling a Kanban supervisor
128
+ * to flush its periodic tick so no in-flight `kanban.*` broadcast races a
129
+ * `WebSocketServer.close()`.
130
+ */
131
+ onPreShutdown?: () => Promise<void> | void;
113
132
  onShutdown: () => Promise<void> | void;
114
133
  }
115
- export declare function registerShutdown(deps: ShutdownDeps): void;
134
+ export declare function registerShutdown(deps: ShutdownDeps): () => void;
116
135
  export {};
117
136
  //# sourceMappingURL=server-runtime.d.ts.map
@@ -80,8 +80,9 @@ export interface SessionHandlersContext {
80
80
  * swapped. Without this, a run started in the previous session keeps
81
81
  * streaming/tool-calling in the background after session.new/resume.
82
82
  */
83
- abortActiveRun?: (() => void) | undefined;
84
- isRunActive?: (() => boolean) | undefined;
83
+ /** When sessionId is provided, abort only that session's run; otherwise abort all. */
84
+ abortActiveRun?: ((sessionId?: string) => void) | undefined;
85
+ isRunActive?: ((sessionId?: string) => boolean) | undefined;
85
86
  sessionStartPayload: (overrides?: Record<string, unknown>) => Promise<SessionStartPayload>;
86
87
  }
87
88
  export declare function createSessionHandlers(ctx: SessionHandlersContext): SessionRouteHandlers;
@@ -1,5 +1,5 @@
1
1
  import type { EventBus } from '@wrongstack/core/kernel';
2
- import { AgentStatusTracker, type SessionRegistry } from '@wrongstack/core/storage';
2
+ import { AgentStatusTracker, getSessionRegistry } from '@wrongstack/core/storage';
3
3
  import type { Config, Logger } from '@wrongstack/core/types';
4
4
  export interface StandaloneSessionIdentityPaths {
5
5
  globalRoot: string;
@@ -13,10 +13,17 @@ export interface StandaloneSessionIdentityOptions {
13
13
  paths: StandaloneSessionIdentityPaths;
14
14
  workingDir: string;
15
15
  initialSessionId: string;
16
- sessionRegistry?: SessionRegistry | undefined;
16
+ sessionRegistry?: SessionIdentityRegistry | undefined;
17
17
  /** Test/embedding escape hatch; production defaults to enabled. */
18
18
  enableHqTelemetry?: boolean | undefined;
19
19
  }
20
+ interface SessionIdentityRegistry {
21
+ register(entry: Parameters<ReturnType<typeof getSessionRegistry>['register']>[0]): Promise<void>;
22
+ updateAgents(agents: Parameters<ReturnType<typeof getSessionRegistry>['updateAgents']>[0]): Promise<void>;
23
+ markClosing(): Promise<void>;
24
+ unregister(): Promise<void>;
25
+ reserveResume?: ReturnType<typeof getSessionRegistry>['reserveResume'] | undefined;
26
+ }
20
27
  export interface SessionIdentityTarget {
21
28
  projectSlug: string;
22
29
  projectRoot: string;
@@ -37,4 +44,5 @@ export interface StandaloneSessionIdentityLifecycle {
37
44
  * external surface is fail-soft, while identity selection itself stays live.
38
45
  */
39
46
  export declare function createStandaloneSessionIdentityLifecycle(opts: StandaloneSessionIdentityOptions): Promise<StandaloneSessionIdentityLifecycle>;
47
+ export {};
40
48
  //# sourceMappingURL=standalone-session-identity.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/webui-server",
3
- "version": "0.302.0",
3
+ "version": "0.303.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack WebUI HTTP/WebSocket server module — extracted from @wrongstack/webui in PR #243/244 to remove the CLI -> @wrongstack/webui/server cross-package edge (audit §3.1.1). Pure backend: HTTP routes, WebSocket handlers, MCP tool wrappers, HTML serving. The web frontend lives in @wrongstack/webui; this package is the standalone server it can run on.",
6
6
  "keywords": [
@@ -40,16 +40,16 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "ws": "^8.21.1",
43
- "@wrongstack/core": "0.302.0",
44
- "@wrongstack/mcp": "0.302.0",
45
- "@wrongstack/techstack": "0.302.0",
46
- "@wrongstack/kanban": "0.302.0",
47
- "@wrongstack/runtime": "0.302.0",
48
- "@wrongstack/sage": "0.302.0",
49
- "@wrongstack/providers": "0.302.0",
50
- "@wrongstack/requirement-intake": "0.302.0",
51
- "@wrongstack/tools": "0.302.0",
52
- "@wrongstack/sdd": "0.302.0"
43
+ "@wrongstack/core": "0.303.0",
44
+ "@wrongstack/kanban": "0.303.0",
45
+ "@wrongstack/requirement-intake": "0.303.0",
46
+ "@wrongstack/runtime": "0.303.0",
47
+ "@wrongstack/mcp": "0.303.0",
48
+ "@wrongstack/sage": "0.303.0",
49
+ "@wrongstack/providers": "0.303.0",
50
+ "@wrongstack/sdd": "0.303.0",
51
+ "@wrongstack/techstack": "0.303.0",
52
+ "@wrongstack/tools": "0.303.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^26.1.2",