@wrongstack/tools 0.302.2 → 0.305.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/builtin.d.ts +6 -0
- package/dist/builtin.js +3213 -686
- package/dist/codebase-index/binary-frame.d.ts +43 -0
- package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +1 -0
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +1 -0
- package/dist/codebase-index/content-hash.d.ts +66 -0
- package/dist/codebase-index/index.js +1597 -153
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/parser-worker-pool.d.ts +63 -0
- package/dist/codebase-index/parser-worker-script.d.ts +42 -0
- package/dist/codebase-index/project-server-protocol.d.ts +2 -0
- package/dist/codebase-index/project-server.js +1483 -104
- package/dist/codebase-index/schema.d.ts +18 -0
- package/dist/codebase-index/tree-sitter/queries.d.ts +48 -0
- package/dist/codebase-index/tree-sitter/util.d.ts +31 -0
- package/dist/codebase-index/tree-sitter/visitor.d.ts +47 -0
- package/dist/codebase-index/tree-sitter-parser.d.ts +58 -0
- package/dist/codebase-index/vector-search.d.ts +62 -0
- package/dist/codebase-index/worker-protocol.d.ts +2 -0
- package/dist/codebase-index/worker.js +1452 -73
- package/dist/codebase-index/writer-bulk-insert.d.ts +5 -0
- package/dist/codebase-index/writer-graph-reader.d.ts +39 -0
- package/dist/codebase-index/writer-schema.d.ts +9 -2
- package/dist/codebase-index/writer.d.ts +36 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3258 -727
- package/dist/kanban-contract-actions.d.ts +7 -0
- package/dist/kanban-task-inputs.d.ts +16 -2
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +39 -2
- package/dist/kanban.js +580 -193
- package/dist/pack.js +3212 -686
- package/dist/plan.d.ts +4 -1
- package/dist/plan.js +2701 -18
- package/dist/read.js +1559 -103
- package/dist/session-kanban.d.ts +94 -1
- package/dist/session-kanban.js +308 -44
- package/dist/task.d.ts +5 -4
- package/dist/task.js +2825 -138
- package/dist/todo.d.ts +10 -1
- package/dist/todo.js +2474 -30
- package/dist/tool-tier.js +3212 -686
- package/package.json +8 -4
package/dist/session-kanban.js
CHANGED
|
@@ -9,12 +9,17 @@ import {
|
|
|
9
9
|
mutateTasks
|
|
10
10
|
} from "@wrongstack/core/storage";
|
|
11
11
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
12
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
12
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
13
13
|
import {
|
|
14
14
|
bridgeKanbanSupervisor,
|
|
15
|
+
compactSessionMirrorBoard,
|
|
15
16
|
createBoard,
|
|
17
|
+
DEFAULT_COLUMNS,
|
|
16
18
|
getBoard,
|
|
19
|
+
getDependencyReadinessIssues,
|
|
20
|
+
getKanbanOrchestrationSnapshot,
|
|
17
21
|
listBoards,
|
|
22
|
+
pruneSessionBoards,
|
|
18
23
|
removeBoard,
|
|
19
24
|
syncBoardFromTaskGraph,
|
|
20
25
|
touchKanbanPresence,
|
|
@@ -22,16 +27,14 @@ import {
|
|
|
22
27
|
} from "@wrongstack/kanban";
|
|
23
28
|
var SESSION_BOARD_TAG = "session-work";
|
|
24
29
|
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
25
|
-
var SESSION_KANBAN_COLUMNS =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
29
|
-
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
30
|
-
];
|
|
30
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
31
|
+
...column
|
|
32
|
+
}));
|
|
31
33
|
var boardQueue = /* @__PURE__ */ new Map();
|
|
32
34
|
var boardEnsures = /* @__PURE__ */ new Map();
|
|
33
35
|
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
34
36
|
var activeMirrors = /* @__PURE__ */ new Set();
|
|
37
|
+
var mirrorFailures = /* @__PURE__ */ new Map();
|
|
35
38
|
var bindings = /* @__PURE__ */ new WeakMap();
|
|
36
39
|
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
37
40
|
var activeSessionBoards = /* @__PURE__ */ new Map();
|
|
@@ -41,6 +44,33 @@ function boardKey(projectRoot, sessionId) {
|
|
|
41
44
|
function mirrorKey(projectRoot, sessionId, sourceSystem) {
|
|
42
45
|
return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
|
|
43
46
|
}
|
|
47
|
+
function completedReconciliationGraph(latest, candidates) {
|
|
48
|
+
const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
|
|
49
|
+
const carriedNodeIds = /* @__PURE__ */ new Set();
|
|
50
|
+
const completedNodes = candidates.flatMap(
|
|
51
|
+
(candidate) => candidate.nodes.filter((node) => {
|
|
52
|
+
if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
carriedNodeIds.add(node.id);
|
|
56
|
+
return true;
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
if (completedNodes.length === 0) return void 0;
|
|
60
|
+
const carriedRequirements = completedNodes.flatMap(
|
|
61
|
+
(node) => node.specRequirementId ? [node.specRequirementId] : []
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
...latest,
|
|
65
|
+
nodes: [...latest.nodes, ...completedNodes],
|
|
66
|
+
rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
|
|
67
|
+
...latest.requiredRequirementIds ? {
|
|
68
|
+
requiredRequirementIds: [
|
|
69
|
+
.../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
|
|
70
|
+
]
|
|
71
|
+
} : {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
44
74
|
function sessionTag(sessionId) {
|
|
45
75
|
return `session:${sessionId}`;
|
|
46
76
|
}
|
|
@@ -135,6 +165,26 @@ async function removeEmptySessionBoard(projectRoot, boardId, sessionId) {
|
|
|
135
165
|
return await removeBoard(projectRoot, board.id) ? board.id : null;
|
|
136
166
|
});
|
|
137
167
|
}
|
|
168
|
+
async function removeOwnedSessionBoard(projectRoot, boardId, sessionId) {
|
|
169
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
170
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return null;
|
|
171
|
+
const board = await getBoard(projectRoot, boardId);
|
|
172
|
+
if (!board || !isOwnedSessionBoard(board.tags)) return null;
|
|
173
|
+
if (sessionIdFromTags(board.tags) !== sessionId) return null;
|
|
174
|
+
return await removeBoard(projectRoot, board.id) ? board.id : null;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function cleanupSessionKanbanBoard(projectRoot, sessionId) {
|
|
178
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
|
|
179
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return [];
|
|
180
|
+
const candidates = (await listBoards(projectRoot)).filter(
|
|
181
|
+
(board) => isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
|
|
182
|
+
);
|
|
183
|
+
const removed = await Promise.all(
|
|
184
|
+
candidates.map((board) => removeOwnedSessionBoard(projectRoot, board.id, sessionId))
|
|
185
|
+
);
|
|
186
|
+
return removed.filter((boardId) => Boolean(boardId));
|
|
187
|
+
}
|
|
138
188
|
async function cleanupSessionKanbanBoardIfEmpty(projectRoot, sessionId) {
|
|
139
189
|
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
|
|
140
190
|
if (isSessionBoardActive(projectRoot, sessionId)) return [];
|
|
@@ -172,16 +222,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
172
222
|
sourceSystem,
|
|
173
223
|
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
174
224
|
archiveMissingTasks: true,
|
|
175
|
-
includeCompletedTasks: true
|
|
225
|
+
includeCompletedTasks: true,
|
|
226
|
+
// The scope ledger stays declared and accurate, but it may not veto a
|
|
227
|
+
// projection. A session mirror reflects a tactical list that shrinks by
|
|
228
|
+
// design, and refusing the sync never protected the removed row — it
|
|
229
|
+
// froze the entire board, permanently, because the stored scope then
|
|
230
|
+
// outlived every later snapshot (`session-kanban.mirror-failed`).
|
|
231
|
+
// Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
|
|
232
|
+
// removed card on the board as `archived`, the reconciliation pass
|
|
233
|
+
// first walks vanished completed rows to Done, and the session journal
|
|
234
|
+
// remains the durable record.
|
|
235
|
+
allowRequirementScopeShrink: true
|
|
176
236
|
}
|
|
177
237
|
);
|
|
178
|
-
|
|
238
|
+
if (!result) return null;
|
|
239
|
+
const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
|
|
240
|
+
if (compacted?.removedTaskIds.length) {
|
|
241
|
+
return await getBoard(projectRoot, board.id) ?? result.board;
|
|
242
|
+
}
|
|
243
|
+
return result.board;
|
|
179
244
|
});
|
|
180
245
|
}
|
|
181
246
|
function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
182
247
|
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
|
|
183
248
|
const key = mirrorKey(projectRoot, sessionId, sourceSystem);
|
|
184
|
-
pendingMirrors.
|
|
249
|
+
const previous = pendingMirrors.get(key);
|
|
250
|
+
const reconciliationGraph = previous ? completedReconciliationGraph(
|
|
251
|
+
graph,
|
|
252
|
+
[previous.reconciliationGraph, previous.graph].filter(
|
|
253
|
+
(candidate) => candidate !== void 0
|
|
254
|
+
)
|
|
255
|
+
) : void 0;
|
|
256
|
+
pendingMirrors.set(key, {
|
|
257
|
+
projectRoot,
|
|
258
|
+
sessionId,
|
|
259
|
+
graph,
|
|
260
|
+
...reconciliationGraph ? { reconciliationGraph } : {},
|
|
261
|
+
sourceSystem
|
|
262
|
+
});
|
|
185
263
|
if (activeMirrors.has(key)) return;
|
|
186
264
|
activeMirrors.add(key);
|
|
187
265
|
void (async () => {
|
|
@@ -191,13 +269,37 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
191
269
|
if (!pending) break;
|
|
192
270
|
pendingMirrors.delete(key);
|
|
193
271
|
try {
|
|
272
|
+
if (pending.reconciliationGraph) {
|
|
273
|
+
await projectGraph(
|
|
274
|
+
pending.projectRoot,
|
|
275
|
+
pending.sessionId,
|
|
276
|
+
pending.reconciliationGraph,
|
|
277
|
+
pending.sourceSystem
|
|
278
|
+
);
|
|
279
|
+
}
|
|
194
280
|
await projectGraph(
|
|
195
281
|
pending.projectRoot,
|
|
196
282
|
pending.sessionId,
|
|
197
283
|
pending.graph,
|
|
198
284
|
pending.sourceSystem
|
|
199
285
|
);
|
|
200
|
-
|
|
286
|
+
mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
289
|
+
mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
|
|
290
|
+
message,
|
|
291
|
+
sourceSystem: pending.sourceSystem
|
|
292
|
+
});
|
|
293
|
+
console.warn(
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
level: "warn",
|
|
296
|
+
event: "session-kanban.mirror-failed",
|
|
297
|
+
sessionId: pending.sessionId,
|
|
298
|
+
sourceSystem: pending.sourceSystem,
|
|
299
|
+
message,
|
|
300
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
301
|
+
})
|
|
302
|
+
);
|
|
201
303
|
}
|
|
202
304
|
}
|
|
203
305
|
} finally {
|
|
@@ -215,7 +317,21 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
215
317
|
}
|
|
216
318
|
})();
|
|
217
319
|
}
|
|
320
|
+
function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
321
|
+
if (!projectRoot || !sessionId) return void 0;
|
|
322
|
+
const key = boardKey(projectRoot, sessionId);
|
|
323
|
+
const failure = mirrorFailures.get(key);
|
|
324
|
+
if (!failure) return void 0;
|
|
325
|
+
mirrorFailures.delete(key);
|
|
326
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
327
|
+
}
|
|
328
|
+
function hasInFlightTodoMirror(projectRoot, sessionId) {
|
|
329
|
+
if (!projectRoot || !sessionId) return false;
|
|
330
|
+
const key = mirrorKey(projectRoot, sessionId, "session-todo");
|
|
331
|
+
return pendingMirrors.has(key) || activeMirrors.has(key);
|
|
332
|
+
}
|
|
218
333
|
function todoListToSerializedGraph(todos, sessionId) {
|
|
334
|
+
const graphId = `todo:${sessionId}`;
|
|
219
335
|
const nodes = todos.map((todo, index) => ({
|
|
220
336
|
id: todo.id,
|
|
221
337
|
title: todo.content,
|
|
@@ -223,12 +339,14 @@ function todoListToSerializedGraph(todos, sessionId) {
|
|
|
223
339
|
type: "chore",
|
|
224
340
|
priority: "medium",
|
|
225
341
|
status: todo.status,
|
|
342
|
+
specRequirementId: `${graphId}:${todo.id}`,
|
|
226
343
|
createdAt: index,
|
|
227
344
|
updatedAt: index
|
|
228
345
|
}));
|
|
229
346
|
return {
|
|
230
|
-
id:
|
|
231
|
-
specId:
|
|
347
|
+
id: graphId,
|
|
348
|
+
specId: graphId,
|
|
349
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
232
350
|
title: "Session todos",
|
|
233
351
|
nodes,
|
|
234
352
|
edges: [],
|
|
@@ -238,6 +356,7 @@ function todoListToSerializedGraph(todos, sessionId) {
|
|
|
238
356
|
};
|
|
239
357
|
}
|
|
240
358
|
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
359
|
+
const graphId = `session:${sessionId}`;
|
|
241
360
|
const ids = new Set(tasks.map((task) => task.id));
|
|
242
361
|
const nodes = tasks.map((task, index) => ({
|
|
243
362
|
id: task.id,
|
|
@@ -246,6 +365,7 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
246
365
|
type: task.type,
|
|
247
366
|
priority: task.priority,
|
|
248
367
|
status: task.status,
|
|
368
|
+
specRequirementId: `${graphId}:${task.id}`,
|
|
249
369
|
...task.assignee ? { assignee: task.assignee } : {},
|
|
250
370
|
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
251
371
|
createdAt: index,
|
|
@@ -263,8 +383,9 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
263
383
|
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
264
384
|
return {
|
|
265
385
|
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
266
|
-
id:
|
|
267
|
-
specId:
|
|
386
|
+
id: graphId,
|
|
387
|
+
specId: graphId,
|
|
388
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
268
389
|
title: "Session tasks",
|
|
269
390
|
nodes,
|
|
270
391
|
edges,
|
|
@@ -279,6 +400,7 @@ var PLAN_STATUS_TO_TASK = {
|
|
|
279
400
|
done: "completed"
|
|
280
401
|
};
|
|
281
402
|
function planFileToSerializedGraph(items, sessionId) {
|
|
403
|
+
const graphId = `plan:${sessionId}`;
|
|
282
404
|
const nodes = items.map((item, index) => ({
|
|
283
405
|
id: item.id,
|
|
284
406
|
title: item.title,
|
|
@@ -286,12 +408,14 @@ function planFileToSerializedGraph(items, sessionId) {
|
|
|
286
408
|
type: "chore",
|
|
287
409
|
priority: "medium",
|
|
288
410
|
status: PLAN_STATUS_TO_TASK[item.status],
|
|
411
|
+
specRequirementId: `${graphId}:${item.id}`,
|
|
289
412
|
createdAt: index,
|
|
290
413
|
updatedAt: index
|
|
291
414
|
}));
|
|
292
415
|
return {
|
|
293
|
-
id:
|
|
294
|
-
specId:
|
|
416
|
+
id: graphId,
|
|
417
|
+
specId: graphId,
|
|
418
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
295
419
|
title: "Session plan",
|
|
296
420
|
nodes,
|
|
297
421
|
edges: [],
|
|
@@ -367,11 +491,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
367
491
|
});
|
|
368
492
|
}
|
|
369
493
|
function notifyTodoUpdate(context, todos) {
|
|
370
|
-
const summary = todos
|
|
494
|
+
const summary = formatTodosForModel(todos);
|
|
371
495
|
const text = `[KANBAN TODO UPDATE]
|
|
372
496
|
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
373
497
|
${summary}
|
|
374
|
-
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
498
|
+
Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
|
|
375
499
|
const state = context.state;
|
|
376
500
|
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
377
501
|
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
@@ -380,6 +504,10 @@ Reassess your current plan before continuing; do not rely on the initial todo sn
|
|
|
380
504
|
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
381
505
|
}
|
|
382
506
|
}
|
|
507
|
+
function todosNeedingSessionMirror(todos, activeBoardId) {
|
|
508
|
+
if (!activeBoardId) return todos;
|
|
509
|
+
return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId || !todo.kanbanTaskId);
|
|
510
|
+
}
|
|
383
511
|
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
384
512
|
queueLatestMirror(
|
|
385
513
|
projectRoot,
|
|
@@ -417,7 +545,7 @@ function attachSessionKanbanMirror(context) {
|
|
|
417
545
|
releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
418
546
|
fireAndForget(
|
|
419
547
|
"cleanup-board",
|
|
420
|
-
|
|
548
|
+
cleanupSessionKanbanBoard(attachedProjectRoot, registeredSessionId)
|
|
421
549
|
);
|
|
422
550
|
}
|
|
423
551
|
registeredSessionId = currentSessionId;
|
|
@@ -434,6 +562,11 @@ function attachSessionKanbanMirror(context) {
|
|
|
434
562
|
let boardTimer = null;
|
|
435
563
|
let presenceTimer = null;
|
|
436
564
|
const sessionId = () => context.session?.id ?? "";
|
|
565
|
+
const activeManagedBoardId = () => {
|
|
566
|
+
const metaKanban = context.meta["kanban"];
|
|
567
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
568
|
+
return context.currentKanbanBoardId ?? (typeof metaBoardId === "string" && metaBoardId ? metaBoardId : "");
|
|
569
|
+
};
|
|
437
570
|
const refreshFiles = async () => {
|
|
438
571
|
const id = sessionId();
|
|
439
572
|
if (!id) return;
|
|
@@ -451,12 +584,19 @@ function attachSessionKanbanMirror(context) {
|
|
|
451
584
|
const refreshBoard = async () => {
|
|
452
585
|
if (!watchedBoardId) return;
|
|
453
586
|
const board = await getBoard(context.projectRoot, watchedBoardId);
|
|
454
|
-
if (board)
|
|
587
|
+
if (!board) return;
|
|
588
|
+
if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos(context, board);
|
|
589
|
+
else applySessionKanbanBoardToTodos(context, board);
|
|
455
590
|
};
|
|
456
591
|
const configureBoardWatcher = async () => {
|
|
457
592
|
const id = sessionId();
|
|
458
|
-
const
|
|
459
|
-
|
|
593
|
+
const managedBoardId = activeManagedBoardId();
|
|
594
|
+
const board = managedBoardId ? await getBoard(context.projectRoot, managedBoardId) : id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
|
|
595
|
+
if (!board) return;
|
|
596
|
+
if (board.id === watchedBoardId) {
|
|
597
|
+
await refreshBoard();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
460
600
|
unsubscribeBoardEvents?.();
|
|
461
601
|
unsubscribeBoardEvents = null;
|
|
462
602
|
watchedBoardId = board.id;
|
|
@@ -480,6 +620,7 @@ function attachSessionKanbanMirror(context) {
|
|
|
480
620
|
if (presenceTimer) clearInterval(presenceTimer);
|
|
481
621
|
presenceTimer = setInterval(() => fireAndForget("touch-presence", touchPresence()), 6e4);
|
|
482
622
|
presenceTimer.unref?.();
|
|
623
|
+
if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos(context, board);
|
|
483
624
|
} catch {
|
|
484
625
|
unsubscribeBoardEvents = null;
|
|
485
626
|
watchedBoardId = "";
|
|
@@ -512,18 +653,20 @@ function attachSessionKanbanMirror(context) {
|
|
|
512
653
|
};
|
|
513
654
|
const unsubscribe = context.state.onChange((change) => {
|
|
514
655
|
if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
);
|
|
656
|
+
const snapshot = change.completedSnapshot ?? change.todos;
|
|
657
|
+
const unbound = todosNeedingSessionMirror(snapshot, activeManagedBoardId());
|
|
658
|
+
if (snapshot.length > 0 && unbound.length === 0) return;
|
|
659
|
+
mirrorSessionTodosToKanban(context.projectRoot, unbound, sessionId());
|
|
520
660
|
return;
|
|
521
661
|
}
|
|
522
|
-
if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path")) {
|
|
662
|
+
if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path" || change.key === "kanban")) {
|
|
523
663
|
syncActiveSessionRegistration();
|
|
524
664
|
configureWatcher();
|
|
525
|
-
|
|
665
|
+
if (!activeManagedBoardId()) {
|
|
666
|
+
fireAndForget("ensure-board", ensureSessionKanbanBoard(context.projectRoot, sessionId()));
|
|
667
|
+
}
|
|
526
668
|
fireAndForget("configure-watcher", configureBoardWatcher());
|
|
669
|
+
fireAndForget("refresh-board", refreshBoard());
|
|
527
670
|
fireAndForget("refresh-files", refreshFiles());
|
|
528
671
|
}
|
|
529
672
|
});
|
|
@@ -541,7 +684,7 @@ function attachSessionKanbanMirror(context) {
|
|
|
541
684
|
releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
542
685
|
fireAndForget(
|
|
543
686
|
"cleanup-board",
|
|
544
|
-
|
|
687
|
+
cleanupSessionKanbanBoard(attachedProjectRoot, registeredSessionId)
|
|
545
688
|
);
|
|
546
689
|
registeredSessionId = "";
|
|
547
690
|
}
|
|
@@ -549,10 +692,42 @@ function attachSessionKanbanMirror(context) {
|
|
|
549
692
|
bindings.set(context, detach);
|
|
550
693
|
return detach;
|
|
551
694
|
}
|
|
695
|
+
async function rebindSessionKanbanTask(context) {
|
|
696
|
+
const sessionId = context.session?.id;
|
|
697
|
+
if (!sessionId || !context.projectRoot) return null;
|
|
698
|
+
if (context.currentKanbanTaskId) return null;
|
|
699
|
+
let best;
|
|
700
|
+
try {
|
|
701
|
+
const snapshot = await getKanbanOrchestrationSnapshot(context.projectRoot);
|
|
702
|
+
const nowMs = Date.now();
|
|
703
|
+
for (const result of snapshot.running) {
|
|
704
|
+
const assignment = result.task.assignment;
|
|
705
|
+
if (assignment?.status !== "running") continue;
|
|
706
|
+
const expiresAt = assignment.leaseExpiresAt ? Date.parse(assignment.leaseExpiresAt) : Number.NaN;
|
|
707
|
+
if (Number.isFinite(expiresAt) && expiresAt <= nowMs) continue;
|
|
708
|
+
const entry = result.board.presence?.find(
|
|
709
|
+
(candidate) => candidate.sessionId === sessionId && candidate.taskId === result.task.id
|
|
710
|
+
);
|
|
711
|
+
if (!entry) continue;
|
|
712
|
+
if (!best || entry.lastSeenAt > best.lastSeenAt) {
|
|
713
|
+
best = { boardId: result.board.id, taskId: result.task.id, lastSeenAt: entry.lastSeenAt };
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
if (!best) return null;
|
|
720
|
+
context.setCurrentKanbanTask(best.taskId, best.boardId);
|
|
721
|
+
return { boardId: best.boardId, taskId: best.taskId };
|
|
722
|
+
}
|
|
552
723
|
async function hydrateSessionKanban(context) {
|
|
553
724
|
const id = context.session?.id ?? "";
|
|
554
725
|
if (!id) return null;
|
|
726
|
+
await rebindSessionKanbanTask(context);
|
|
555
727
|
await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
|
|
728
|
+
if (context.projectRoot) {
|
|
729
|
+
fireAndForget("prune-session-boards", pruneSessionBoards(context.projectRoot));
|
|
730
|
+
}
|
|
556
731
|
let board = await ensureSessionKanbanBoard(context.projectRoot, id);
|
|
557
732
|
if (context.todos.length) {
|
|
558
733
|
board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
|
|
@@ -583,18 +758,68 @@ function todoStatus(task) {
|
|
|
583
758
|
if (status === "in_progress" || status === "review") return "in_progress";
|
|
584
759
|
return "pending";
|
|
585
760
|
}
|
|
586
|
-
function sessionTodoFromTask(task) {
|
|
761
|
+
function sessionTodoFromTask(task, board) {
|
|
762
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
587
763
|
return {
|
|
588
764
|
id: task.origin?.taskId ?? task.id,
|
|
589
765
|
content: task.title,
|
|
590
766
|
status: todoStatus(task),
|
|
591
|
-
...task.description ? { activeForm: task.description } : {}
|
|
767
|
+
...task.description ? { activeForm: task.description } : {},
|
|
768
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function managedTodoFromTask(task, board) {
|
|
772
|
+
return {
|
|
773
|
+
...sessionTodoFromTask(task, board),
|
|
774
|
+
kanbanBoardId: board.id,
|
|
775
|
+
kanbanTaskId: task.id
|
|
592
776
|
};
|
|
593
777
|
}
|
|
778
|
+
function blockingTitles(board, task) {
|
|
779
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
780
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
781
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
782
|
+
return dependency.title;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
var PRIORITY_ORDER = {
|
|
786
|
+
critical: 0,
|
|
787
|
+
high: 1,
|
|
788
|
+
medium: 2,
|
|
789
|
+
low: 3
|
|
790
|
+
};
|
|
791
|
+
function orderTasksForTodos(board, tasks) {
|
|
792
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
793
|
+
const baseline = [...tasks].sort(
|
|
794
|
+
(left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
|
|
795
|
+
);
|
|
796
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
797
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
798
|
+
const emitted = [];
|
|
799
|
+
const done = /* @__PURE__ */ new Set();
|
|
800
|
+
while (remaining.size > 0) {
|
|
801
|
+
const ready = baseline.filter(
|
|
802
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
803
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
804
|
+
)
|
|
805
|
+
);
|
|
806
|
+
if (ready.length === 0) break;
|
|
807
|
+
for (const task of ready) {
|
|
808
|
+
remaining.delete(task.id);
|
|
809
|
+
done.add(task.id);
|
|
810
|
+
emitted.push(task);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
814
|
+
return emitted;
|
|
815
|
+
}
|
|
594
816
|
function sameTodos(left, right) {
|
|
595
817
|
return left.length === right.length && left.every((todo, index) => {
|
|
596
818
|
const candidate = right[index];
|
|
597
|
-
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask
|
|
819
|
+
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && // Readiness is part of the projection: when a dependency completes,
|
|
820
|
+
// the rows are otherwise identical and the unblocking would never
|
|
821
|
+
// reach the model.
|
|
822
|
+
(candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
598
823
|
});
|
|
599
824
|
}
|
|
600
825
|
function applySessionKanbanBoardToTodos(context, board) {
|
|
@@ -602,13 +827,13 @@ function applySessionKanbanBoardToTodos(context, board) {
|
|
|
602
827
|
if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
|
|
603
828
|
return [...context.todos];
|
|
604
829
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
830
|
+
if (hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
|
|
831
|
+
const projectedTodos = orderTasksForTodos(
|
|
832
|
+
board,
|
|
833
|
+
board.tasks.filter(
|
|
834
|
+
(task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
|
|
835
|
+
)
|
|
836
|
+
).map((task) => sessionTodoFromTask(task, board));
|
|
612
837
|
const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
|
|
613
838
|
const effectiveTodos = allCompleted ? [] : projectedTodos;
|
|
614
839
|
if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
|
|
@@ -622,6 +847,30 @@ function applySessionKanbanBoardToTodos(context, board) {
|
|
|
622
847
|
broadcastTodoUpdate(context, context.todos);
|
|
623
848
|
return [...context.todos];
|
|
624
849
|
}
|
|
850
|
+
function applyManagedKanbanBoardToTodos(context, board) {
|
|
851
|
+
const metaKanban = context.meta["kanban"];
|
|
852
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
853
|
+
const activeBoardId = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
854
|
+
if (!activeBoardId || board.id !== activeBoardId || board.lifecycle?.mode !== "managed") {
|
|
855
|
+
return [...context.todos];
|
|
856
|
+
}
|
|
857
|
+
const projectedTodos = orderTasksForTodos(
|
|
858
|
+
board,
|
|
859
|
+
board.tasks.filter(
|
|
860
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
861
|
+
)
|
|
862
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
863
|
+
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
864
|
+
suppressedTodoMirrors.add(context);
|
|
865
|
+
try {
|
|
866
|
+
context.state.replaceTodos(projectedTodos);
|
|
867
|
+
} finally {
|
|
868
|
+
suppressedTodoMirrors.delete(context);
|
|
869
|
+
}
|
|
870
|
+
notifyTodoUpdate(context, context.todos);
|
|
871
|
+
broadcastTodoUpdate(context, context.todos);
|
|
872
|
+
return [...context.todos];
|
|
873
|
+
}
|
|
625
874
|
async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
626
875
|
const originId = task.origin?.taskId;
|
|
627
876
|
const graphId = task.origin?.graphId ?? "";
|
|
@@ -645,7 +894,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
|
645
894
|
const id = context.session?.id ?? "";
|
|
646
895
|
if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
|
|
647
896
|
const planPath = context.meta["plan.path"];
|
|
648
|
-
if (typeof planPath !== "string" || !planPath)
|
|
897
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
"Cannot reflect this Kanban edit back to its plan source: the session has no plan.path configured. The board mutation already succeeded; the plan file is now out of sync."
|
|
900
|
+
);
|
|
901
|
+
}
|
|
649
902
|
const plan = await mutatePlan(planPath, id, (file) => ({
|
|
650
903
|
...file,
|
|
651
904
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -663,7 +916,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
|
663
916
|
}
|
|
664
917
|
if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
|
|
665
918
|
const taskPath = context.meta["task.path"];
|
|
666
|
-
if (typeof taskPath !== "string" || !taskPath)
|
|
919
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
"Cannot reflect this Kanban edit back to its task source: the session has no task.path configured. The board mutation already succeeded; the task file is now out of sync."
|
|
922
|
+
);
|
|
923
|
+
}
|
|
667
924
|
const tasks = await mutateTasks(taskPath, id, (file) => ({
|
|
668
925
|
...file,
|
|
669
926
|
tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
|
|
@@ -682,21 +939,28 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
|
682
939
|
}
|
|
683
940
|
export {
|
|
684
941
|
SESSION_KANBAN_COLUMNS,
|
|
942
|
+
applyManagedKanbanBoardToTodos,
|
|
685
943
|
applySessionKanbanBoardToTodos,
|
|
686
944
|
applySessionKanbanTaskToSource,
|
|
687
945
|
attachSessionKanbanMirror,
|
|
946
|
+
blockingTitles,
|
|
688
947
|
cleanupEmptySessionKanbanBoards,
|
|
948
|
+
cleanupSessionKanbanBoard,
|
|
689
949
|
cleanupSessionKanbanBoardIfEmpty,
|
|
690
950
|
ensureSessionKanbanBoard,
|
|
691
951
|
hydrateSessionKanban,
|
|
692
952
|
mirrorSessionPlanToKanban,
|
|
693
953
|
mirrorSessionTasksToKanban,
|
|
694
954
|
mirrorSessionTodosToKanban,
|
|
955
|
+
orderTasksForTodos,
|
|
695
956
|
planFileToSerializedGraph,
|
|
696
957
|
projectSessionPlanToKanban,
|
|
697
958
|
projectSessionTasksToKanban,
|
|
698
959
|
projectSessionTodosToKanban,
|
|
960
|
+
rebindSessionKanbanTask,
|
|
961
|
+
takeSessionMirrorFailure,
|
|
699
962
|
taskFileToSerializedGraph,
|
|
700
|
-
todoListToSerializedGraph
|
|
963
|
+
todoListToSerializedGraph,
|
|
964
|
+
todosNeedingSessionMirror
|
|
701
965
|
};
|
|
702
966
|
//# sourceMappingURL=session-kanban.js.map
|
package/dist/task.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type { TaskStatus } from '@wrongstack/core/types';
|
|
1
|
+
import type { TaskStatus, Tool } from '@wrongstack/core/types';
|
|
2
2
|
import { type TaskItem } from '@wrongstack/core/utils';
|
|
3
|
-
|
|
3
|
+
type TaskReplacementItem = Omit<TaskItem, 'createdAt' | 'updatedAt'> & Partial<Pick<TaskItem, 'createdAt' | 'updatedAt'>>;
|
|
4
|
+
type TaskAdditionItem = Omit<TaskItem, 'id' | 'createdAt' | 'updatedAt' | 'status'> & Partial<Pick<TaskItem, 'status'>>;
|
|
4
5
|
interface TaskInput {
|
|
5
6
|
/** Replace: set new task list. Add: append a task. Status: update task status. Promote: convert a task to todo items. */
|
|
6
7
|
action: 'replace' | 'add' | 'status' | 'show' | 'promote' | 'planify';
|
|
7
8
|
/** Full task list for action=replace. */
|
|
8
|
-
tasks?:
|
|
9
|
+
tasks?: TaskReplacementItem[] | undefined;
|
|
9
10
|
/** Single task for action=add. id, createdAt, updatedAt are auto-generated. */
|
|
10
|
-
task?:
|
|
11
|
+
task?: TaskAdditionItem | undefined;
|
|
11
12
|
/** Task id for action=status or target for action=promote. */
|
|
12
13
|
id?: string | undefined;
|
|
13
14
|
/** New status for action=status. */
|