@wrongstack/tools 0.286.0 → 0.289.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/README.md +2 -2
- package/dist/_edit-match.d.ts.map +1 -1
- package/dist/_fetch-guard.d.ts.map +1 -1
- package/dist/_redact-command.d.ts.map +1 -1
- package/dist/_spawn-stream.d.ts.map +1 -1
- package/dist/audit.js +61 -14
- package/dist/audit.js.map +3 -3
- package/dist/bash.d.ts +1 -1
- package/dist/bash.d.ts.map +1 -1
- package/dist/bash.js +97 -19
- package/dist/bash.js.map +3 -3
- package/dist/batch-tool-use.d.ts +1 -1
- package/dist/batch-tool-use.d.ts.map +1 -1
- package/dist/batch-tool-use.js +26 -8
- package/dist/batch-tool-use.js.map +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/types.d.ts +1 -1
- package/dist/browser/types.d.ts.map +1 -1
- package/dist/builtin.d.ts +12 -10
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +2107 -864
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/bm25.d.ts +1 -2
- package/dist/codebase-index/bm25.d.ts.map +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
- package/dist/codebase-index/go-parser.d.ts.map +1 -1
- package/dist/codebase-index/index-service.d.ts +11 -1
- package/dist/codebase-index/index-service.d.ts.map +1 -1
- package/dist/codebase-index/index.d.ts +2 -1
- package/dist/codebase-index/index.d.ts.map +1 -1
- package/dist/codebase-index/index.js +934 -283
- package/dist/codebase-index/index.js.map +4 -4
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/py-parser.d.ts.map +1 -1
- package/dist/codebase-index/rs-parser.d.ts.map +1 -1
- package/dist/codebase-index/schema.d.ts +43 -1
- package/dist/codebase-index/schema.d.ts.map +1 -1
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker-protocol.d.ts +1 -0
- package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +793 -241
- package/dist/codebase-index/worker.js.map +4 -4
- package/dist/codebase-index/writer.d.ts +27 -1
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/e2e.js +477 -0
- package/dist/e2e.js.map +7 -0
- package/dist/edit.d.ts.map +1 -1
- package/dist/edit.js +36 -12
- package/dist/edit.js.map +3 -3
- package/dist/exec.d.ts +1 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +60 -5
- package/dist/exec.js.map +3 -3
- package/dist/fetch.js +2 -2
- package/dist/fetch.js.map +2 -2
- package/dist/format.js +61 -14
- package/dist/format.js.map +3 -3
- package/dist/glob.js +2 -2
- package/dist/glob.js.map +1 -1
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +2 -2
- package/dist/grep.js.map +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2535 -865
- package/dist/index.js.map +4 -4
- package/dist/install.js +61 -14
- package/dist/install.js.map +3 -3
- package/dist/kanban.d.ts +16 -2
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +795 -532
- package/dist/kanban.js.map +4 -4
- package/dist/languages/detect.d.ts.map +1 -1
- package/dist/languages/index.js +61 -14
- package/dist/languages/index.js.map +3 -3
- package/dist/languages/types.d.ts +7 -0
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/lint.js +61 -14
- package/dist/lint.js.map +3 -3
- package/dist/next-steps.d.ts +6 -2
- package/dist/next-steps.d.ts.map +1 -1
- package/dist/next-steps.js +3 -3
- package/dist/next-steps.js.map +2 -2
- package/dist/outdated.js +61 -14
- package/dist/outdated.js.map +3 -3
- package/dist/pack.js +2104 -861
- package/dist/pack.js.map +4 -4
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +158 -4
- package/dist/plan.js.map +4 -4
- package/dist/process-registry.js +12 -5
- package/dist/process-registry.js.map +2 -2
- package/dist/search.js +2 -2
- package/dist/search.js.map +2 -2
- package/dist/session-kanban.d.ts +47 -0
- package/dist/session-kanban.d.ts.map +1 -0
- package/dist/session-kanban.js +626 -0
- package/dist/session-kanban.js.map +7 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +167 -4
- package/dist/task.js.map +4 -4
- package/dist/test.js +61 -14
- package/dist/test.js.map +3 -3
- package/dist/todo.d.ts.map +1 -1
- package/dist/todo.js +159 -7
- package/dist/todo.js.map +4 -4
- package/dist/tool-icon-map.d.ts.map +1 -1
- package/dist/tool-icons.d.ts.map +1 -1
- package/dist/tool-icons.js +7 -1
- package/dist/tool-icons.js.map +2 -2
- package/dist/tree.js +2 -2
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +61 -14
- package/dist/typecheck.js.map +3 -3
- package/package.json +11 -3
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
// src/session-kanban.ts
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
deserializeTaskGraph,
|
|
6
|
+
GlobalMailbox,
|
|
7
|
+
loadPlan,
|
|
8
|
+
loadTasks,
|
|
9
|
+
mutatePlan,
|
|
10
|
+
mutateTasks
|
|
11
|
+
} from "@wrongstack/core";
|
|
12
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
13
|
+
import {
|
|
14
|
+
createBoard,
|
|
15
|
+
getBoard,
|
|
16
|
+
getKanbanDir,
|
|
17
|
+
listBoards,
|
|
18
|
+
removeBoard,
|
|
19
|
+
syncBoardFromTaskGraph,
|
|
20
|
+
touchKanbanPresence,
|
|
21
|
+
updateBoard
|
|
22
|
+
} from "@wrongstack/kanban";
|
|
23
|
+
var SESSION_BOARD_TAG = "session-work";
|
|
24
|
+
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
25
|
+
var SESSION_KANBAN_COLUMNS = [
|
|
26
|
+
{ id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
|
|
27
|
+
{ id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
|
|
28
|
+
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
29
|
+
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
30
|
+
];
|
|
31
|
+
var boardQueue = /* @__PURE__ */ new Map();
|
|
32
|
+
var boardEnsures = /* @__PURE__ */ new Map();
|
|
33
|
+
var bindings = /* @__PURE__ */ new WeakMap();
|
|
34
|
+
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
35
|
+
var activeSessionBoards = /* @__PURE__ */ new Map();
|
|
36
|
+
function boardKey(projectRoot, sessionId) {
|
|
37
|
+
return `${projectRoot}\0${sessionId}`;
|
|
38
|
+
}
|
|
39
|
+
function sessionTag(sessionId) {
|
|
40
|
+
return `session:${sessionId}`;
|
|
41
|
+
}
|
|
42
|
+
function sessionBoardTitle(sessionId) {
|
|
43
|
+
const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
|
|
44
|
+
return `Session ${leaf.slice(0, 12)}`;
|
|
45
|
+
}
|
|
46
|
+
function sessionBoardTags(sessionId) {
|
|
47
|
+
return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
|
|
48
|
+
}
|
|
49
|
+
function sessionIdFromTags(tags) {
|
|
50
|
+
const tag = tags?.find((candidate) => candidate.startsWith("session:"));
|
|
51
|
+
return tag?.slice("session:".length) || null;
|
|
52
|
+
}
|
|
53
|
+
function isOwnedSessionBoard(tags) {
|
|
54
|
+
return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));
|
|
55
|
+
}
|
|
56
|
+
function retainActiveSessionBoard(projectRoot, sessionId) {
|
|
57
|
+
const key = boardKey(projectRoot, sessionId);
|
|
58
|
+
activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
function releaseActiveSessionBoard(projectRoot, sessionId) {
|
|
61
|
+
const key = boardKey(projectRoot, sessionId);
|
|
62
|
+
const remaining = (activeSessionBoards.get(key) ?? 0) - 1;
|
|
63
|
+
if (remaining > 0) activeSessionBoards.set(key, remaining);
|
|
64
|
+
else activeSessionBoards.delete(key);
|
|
65
|
+
}
|
|
66
|
+
function isSessionBoardActive(projectRoot, sessionId) {
|
|
67
|
+
return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;
|
|
68
|
+
}
|
|
69
|
+
function sameColumns(columns) {
|
|
70
|
+
return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
|
|
71
|
+
}
|
|
72
|
+
async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
73
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
74
|
+
const key = boardKey(projectRoot, sessionId);
|
|
75
|
+
const inFlight = boardEnsures.get(key);
|
|
76
|
+
if (inFlight) return inFlight;
|
|
77
|
+
const promise = (async () => {
|
|
78
|
+
const summary = (await listBoards(projectRoot)).find(
|
|
79
|
+
(board2) => board2.tags?.includes(sessionTag(sessionId))
|
|
80
|
+
);
|
|
81
|
+
let board = summary ? await getBoard(projectRoot, summary.id) : null;
|
|
82
|
+
if (!board) {
|
|
83
|
+
return createBoard(projectRoot, {
|
|
84
|
+
title: sessionBoardTitle(sessionId),
|
|
85
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
86
|
+
tags: sessionBoardTags(sessionId),
|
|
87
|
+
columns: SESSION_KANBAN_COLUMNS,
|
|
88
|
+
generatedBy: `session-kanban:${sessionId}`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
92
|
+
board = await updateBoard(projectRoot, board.id, {
|
|
93
|
+
title: sessionBoardTitle(sessionId),
|
|
94
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
95
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
96
|
+
columns: SESSION_KANBAN_COLUMNS
|
|
97
|
+
}) ?? board;
|
|
98
|
+
}
|
|
99
|
+
return board;
|
|
100
|
+
})();
|
|
101
|
+
boardEnsures.set(key, promise);
|
|
102
|
+
try {
|
|
103
|
+
return await promise;
|
|
104
|
+
} finally {
|
|
105
|
+
boardEnsures.delete(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function enqueueBoardWork(projectRoot, sessionId, work) {
|
|
109
|
+
const key = boardKey(projectRoot, sessionId);
|
|
110
|
+
const previous = boardQueue.get(key) ?? Promise.resolve();
|
|
111
|
+
const result = previous.catch(() => void 0).then(work);
|
|
112
|
+
const tail = result.then(
|
|
113
|
+
() => void 0,
|
|
114
|
+
() => void 0
|
|
115
|
+
);
|
|
116
|
+
boardQueue.set(key, tail);
|
|
117
|
+
void tail.then(() => {
|
|
118
|
+
if (boardQueue.get(key) === tail) boardQueue.delete(key);
|
|
119
|
+
});
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
async function removeEmptySessionBoard(projectRoot, boardId, sessionId) {
|
|
123
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
124
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return null;
|
|
125
|
+
const board = await getBoard(projectRoot, boardId);
|
|
126
|
+
if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;
|
|
127
|
+
if (sessionIdFromTags(board.tags) !== sessionId) return null;
|
|
128
|
+
return await removeBoard(projectRoot, board.id) ? board.id : null;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function cleanupSessionKanbanBoardIfEmpty(projectRoot, sessionId) {
|
|
132
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
|
|
133
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return [];
|
|
134
|
+
const candidates = (await listBoards(projectRoot)).filter(
|
|
135
|
+
(board) => board.taskCount === 0 && isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
|
|
136
|
+
);
|
|
137
|
+
const removed = await Promise.all(
|
|
138
|
+
candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId))
|
|
139
|
+
);
|
|
140
|
+
return removed.filter((boardId) => Boolean(boardId));
|
|
141
|
+
}
|
|
142
|
+
async function cleanupEmptySessionKanbanBoards(projectRoot, activeSessionId = "") {
|
|
143
|
+
if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === "0") return [];
|
|
144
|
+
const candidates = (await listBoards(projectRoot)).flatMap((board) => {
|
|
145
|
+
const ownerSessionId = sessionIdFromTags(board.tags);
|
|
146
|
+
return board.taskCount === 0 && isOwnedSessionBoard(board.tags) && ownerSessionId && ownerSessionId !== activeSessionId && !isSessionBoardActive(projectRoot, ownerSessionId) ? [{ boardId: board.id, sessionId: ownerSessionId }] : [];
|
|
147
|
+
});
|
|
148
|
+
const removed = await Promise.all(
|
|
149
|
+
candidates.map(
|
|
150
|
+
({ boardId, sessionId }) => removeEmptySessionBoard(projectRoot, boardId, sessionId)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
return removed.filter((boardId) => Boolean(boardId));
|
|
154
|
+
}
|
|
155
|
+
async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
156
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
157
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
158
|
+
const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
|
|
159
|
+
if (!board) return null;
|
|
160
|
+
const result = await syncBoardFromTaskGraph(
|
|
161
|
+
projectRoot,
|
|
162
|
+
board.id,
|
|
163
|
+
deserializeTaskGraph(graph),
|
|
164
|
+
{
|
|
165
|
+
sourceSystem,
|
|
166
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
167
|
+
archiveMissingTasks: true,
|
|
168
|
+
includeCompletedTasks: true
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
return result?.board ?? null;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function todoListToSerializedGraph(todos, sessionId) {
|
|
175
|
+
const nodes = todos.map((todo, index) => ({
|
|
176
|
+
id: todo.id,
|
|
177
|
+
title: todo.content,
|
|
178
|
+
description: todo.activeForm ?? "",
|
|
179
|
+
type: "chore",
|
|
180
|
+
priority: "medium",
|
|
181
|
+
status: todo.status,
|
|
182
|
+
createdAt: index,
|
|
183
|
+
updatedAt: index
|
|
184
|
+
}));
|
|
185
|
+
return {
|
|
186
|
+
id: `todo:${sessionId}`,
|
|
187
|
+
specId: `todo:${sessionId}`,
|
|
188
|
+
title: "Session todos",
|
|
189
|
+
nodes,
|
|
190
|
+
edges: [],
|
|
191
|
+
rootNodes: nodes.map((node) => node.id),
|
|
192
|
+
createdAt: 0,
|
|
193
|
+
updatedAt: 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
197
|
+
const ids = new Set(tasks.map((task) => task.id));
|
|
198
|
+
const nodes = tasks.map((task, index) => ({
|
|
199
|
+
id: task.id,
|
|
200
|
+
title: task.title,
|
|
201
|
+
description: task.description ?? "",
|
|
202
|
+
type: task.type,
|
|
203
|
+
priority: task.priority,
|
|
204
|
+
status: task.status,
|
|
205
|
+
...task.assignee ? { assignee: task.assignee } : {},
|
|
206
|
+
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
207
|
+
createdAt: index,
|
|
208
|
+
updatedAt: index
|
|
209
|
+
}));
|
|
210
|
+
const edges = tasks.flatMap(
|
|
211
|
+
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
212
|
+
id: `${dependency}->${task.id}`,
|
|
213
|
+
from: dependency,
|
|
214
|
+
to: task.id,
|
|
215
|
+
type: "depends_on"
|
|
216
|
+
}))
|
|
217
|
+
);
|
|
218
|
+
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
219
|
+
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
220
|
+
return {
|
|
221
|
+
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
222
|
+
id: `session:${sessionId}`,
|
|
223
|
+
specId: `session:${sessionId}`,
|
|
224
|
+
title: "Session tasks",
|
|
225
|
+
nodes,
|
|
226
|
+
edges,
|
|
227
|
+
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
228
|
+
createdAt: 0,
|
|
229
|
+
updatedAt: 0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
var PLAN_STATUS_TO_TASK = {
|
|
233
|
+
open: "pending",
|
|
234
|
+
in_progress: "in_progress",
|
|
235
|
+
done: "completed"
|
|
236
|
+
};
|
|
237
|
+
function planFileToSerializedGraph(items, sessionId) {
|
|
238
|
+
const nodes = items.map((item, index) => ({
|
|
239
|
+
id: item.id,
|
|
240
|
+
title: item.title,
|
|
241
|
+
description: item.details ?? "",
|
|
242
|
+
type: "chore",
|
|
243
|
+
priority: "medium",
|
|
244
|
+
status: PLAN_STATUS_TO_TASK[item.status],
|
|
245
|
+
createdAt: index,
|
|
246
|
+
updatedAt: index
|
|
247
|
+
}));
|
|
248
|
+
return {
|
|
249
|
+
id: `plan:${sessionId}`,
|
|
250
|
+
specId: `plan:${sessionId}`,
|
|
251
|
+
title: "Session plan",
|
|
252
|
+
nodes,
|
|
253
|
+
edges: [],
|
|
254
|
+
rootNodes: nodes.map((node) => node.id),
|
|
255
|
+
createdAt: 0,
|
|
256
|
+
updatedAt: 0
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
260
|
+
return projectGraph(
|
|
261
|
+
projectRoot,
|
|
262
|
+
sessionId,
|
|
263
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
264
|
+
"session-todo"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
268
|
+
return projectGraph(
|
|
269
|
+
projectRoot,
|
|
270
|
+
sessionId,
|
|
271
|
+
taskFileToSerializedGraph(tasks, sessionId),
|
|
272
|
+
"session-task"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
function projectSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
276
|
+
return projectGraph(
|
|
277
|
+
projectRoot,
|
|
278
|
+
sessionId,
|
|
279
|
+
planFileToSerializedGraph(items, sessionId),
|
|
280
|
+
"session-plan"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
function fireAndForget(work) {
|
|
284
|
+
void work.catch(() => {
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function broadcastTodoUpdate(context, todos) {
|
|
288
|
+
const sessionId = context.session?.id ?? "";
|
|
289
|
+
if (!context.agentId || !sessionId) return;
|
|
290
|
+
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
291
|
+
const mailbox = new GlobalMailbox(projectDir);
|
|
292
|
+
void mailbox.send({
|
|
293
|
+
from: context.agentId,
|
|
294
|
+
to: "*",
|
|
295
|
+
type: "status",
|
|
296
|
+
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
kind: "kanban.todos.updated",
|
|
299
|
+
sessionId,
|
|
300
|
+
revision: context.state.revision,
|
|
301
|
+
todos
|
|
302
|
+
}),
|
|
303
|
+
priority: "normal",
|
|
304
|
+
senderSessionId: sessionId,
|
|
305
|
+
ttlMs: 6 * 60 * 60 * 1e3
|
|
306
|
+
}).catch(() => {
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function notifyTodoUpdate(context, todos) {
|
|
310
|
+
const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
|
|
311
|
+
const text = `[KANBAN TODO UPDATE]
|
|
312
|
+
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
313
|
+
${summary}
|
|
314
|
+
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
315
|
+
const state = context.state;
|
|
316
|
+
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
317
|
+
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
318
|
+
}
|
|
319
|
+
if (typeof state.appendMessage === "function") {
|
|
320
|
+
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
324
|
+
fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
|
|
325
|
+
}
|
|
326
|
+
function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
327
|
+
fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));
|
|
328
|
+
}
|
|
329
|
+
function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
330
|
+
fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));
|
|
331
|
+
}
|
|
332
|
+
function attachSessionKanbanMirror(context) {
|
|
333
|
+
const existing = bindings.get(context);
|
|
334
|
+
if (existing) return existing;
|
|
335
|
+
const attachedProjectRoot = context.projectRoot ?? "";
|
|
336
|
+
let registeredSessionId = "";
|
|
337
|
+
const syncActiveSessionRegistration = () => {
|
|
338
|
+
if (!attachedProjectRoot) return;
|
|
339
|
+
const currentSessionId = context.session?.id ?? "";
|
|
340
|
+
if (currentSessionId === registeredSessionId) return;
|
|
341
|
+
if (registeredSessionId) {
|
|
342
|
+
releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
343
|
+
fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
|
|
344
|
+
}
|
|
345
|
+
registeredSessionId = currentSessionId;
|
|
346
|
+
if (registeredSessionId) {
|
|
347
|
+
retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
syncActiveSessionRegistration();
|
|
351
|
+
let watcher = null;
|
|
352
|
+
let watchedDir = "";
|
|
353
|
+
let timer = null;
|
|
354
|
+
let boardWatcher = null;
|
|
355
|
+
let watchedBoardId = "";
|
|
356
|
+
let boardTimer = null;
|
|
357
|
+
let presenceTimer = null;
|
|
358
|
+
const sessionId = () => context.session?.id ?? "";
|
|
359
|
+
const refreshFiles = async () => {
|
|
360
|
+
const id = sessionId();
|
|
361
|
+
if (!id) return;
|
|
362
|
+
const planPath = context.meta["plan.path"];
|
|
363
|
+
if (typeof planPath === "string" && planPath) {
|
|
364
|
+
const plan = await loadPlan(planPath);
|
|
365
|
+
if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
|
|
366
|
+
}
|
|
367
|
+
const taskPath = context.meta["task.path"];
|
|
368
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
369
|
+
const tasks = await loadTasks(taskPath);
|
|
370
|
+
if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const refreshBoard = async () => {
|
|
374
|
+
if (!watchedBoardId) return;
|
|
375
|
+
const board = await getBoard(context.projectRoot, watchedBoardId);
|
|
376
|
+
if (board) applySessionKanbanBoardToTodos(context, board);
|
|
377
|
+
};
|
|
378
|
+
const configureBoardWatcher = async () => {
|
|
379
|
+
const id = sessionId();
|
|
380
|
+
const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
|
|
381
|
+
if (!board || board.id === watchedBoardId) return;
|
|
382
|
+
boardWatcher?.close();
|
|
383
|
+
boardWatcher = null;
|
|
384
|
+
watchedBoardId = board.id;
|
|
385
|
+
try {
|
|
386
|
+
const boardFileName = `${board.id}.json`;
|
|
387
|
+
boardWatcher = watch(
|
|
388
|
+
getKanbanDir(context.projectRoot),
|
|
389
|
+
{ persistent: false },
|
|
390
|
+
(_event, filename) => {
|
|
391
|
+
if (filename?.toString() !== boardFileName) return;
|
|
392
|
+
if (boardTimer) clearTimeout(boardTimer);
|
|
393
|
+
boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
|
|
397
|
+
sessionId: id,
|
|
398
|
+
agentId: context.agentId,
|
|
399
|
+
agentName: context.agentName
|
|
400
|
+
});
|
|
401
|
+
fireAndForget(touchPresence());
|
|
402
|
+
if (presenceTimer) clearInterval(presenceTimer);
|
|
403
|
+
presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
|
|
404
|
+
presenceTimer.unref?.();
|
|
405
|
+
boardWatcher.on("error", () => {
|
|
406
|
+
boardWatcher?.close();
|
|
407
|
+
boardWatcher = null;
|
|
408
|
+
watchedBoardId = "";
|
|
409
|
+
});
|
|
410
|
+
} catch {
|
|
411
|
+
boardWatcher = null;
|
|
412
|
+
watchedBoardId = "";
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const configureWatcher = () => {
|
|
416
|
+
const planPath = context.meta["plan.path"];
|
|
417
|
+
const taskPath = context.meta["task.path"];
|
|
418
|
+
const candidate = typeof planPath === "string" && planPath ? dirname(planPath) : typeof taskPath === "string" && taskPath ? dirname(taskPath) : "";
|
|
419
|
+
if (!candidate || candidate === watchedDir) return;
|
|
420
|
+
watcher?.close();
|
|
421
|
+
watcher = null;
|
|
422
|
+
watchedDir = candidate;
|
|
423
|
+
try {
|
|
424
|
+
watcher = watch(candidate, { persistent: false }, (_event, filename) => {
|
|
425
|
+
const name = filename?.toString();
|
|
426
|
+
const currentPlanPath = context.meta["plan.path"];
|
|
427
|
+
const currentTaskPath = context.meta["task.path"];
|
|
428
|
+
const planName = typeof currentPlanPath === "string" ? basename(currentPlanPath) : "";
|
|
429
|
+
const taskName = typeof currentTaskPath === "string" ? basename(currentTaskPath) : "";
|
|
430
|
+
if (name && name !== planName && name !== taskName) return;
|
|
431
|
+
if (timer) clearTimeout(timer);
|
|
432
|
+
timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
|
|
433
|
+
});
|
|
434
|
+
watcher.on("error", () => watcher?.close());
|
|
435
|
+
} catch {
|
|
436
|
+
watcher = null;
|
|
437
|
+
watchedDir = "";
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const unsubscribe = context.state.onChange((change) => {
|
|
441
|
+
if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
|
|
442
|
+
mirrorSessionTodosToKanban(
|
|
443
|
+
context.projectRoot,
|
|
444
|
+
change.completedSnapshot ?? change.todos,
|
|
445
|
+
sessionId()
|
|
446
|
+
);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path")) {
|
|
450
|
+
syncActiveSessionRegistration();
|
|
451
|
+
configureWatcher();
|
|
452
|
+
fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
|
|
453
|
+
fireAndForget(configureBoardWatcher());
|
|
454
|
+
fireAndForget(refreshFiles());
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
configureWatcher();
|
|
458
|
+
fireAndForget(configureBoardWatcher());
|
|
459
|
+
const detach = () => {
|
|
460
|
+
unsubscribe();
|
|
461
|
+
if (timer) clearTimeout(timer);
|
|
462
|
+
if (boardTimer) clearTimeout(boardTimer);
|
|
463
|
+
if (presenceTimer) clearInterval(presenceTimer);
|
|
464
|
+
watcher?.close();
|
|
465
|
+
boardWatcher?.close();
|
|
466
|
+
bindings.delete(context);
|
|
467
|
+
if (attachedProjectRoot && registeredSessionId) {
|
|
468
|
+
releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
469
|
+
fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
|
|
470
|
+
registeredSessionId = "";
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
bindings.set(context, detach);
|
|
474
|
+
return detach;
|
|
475
|
+
}
|
|
476
|
+
async function hydrateSessionKanban(context) {
|
|
477
|
+
const id = context.session?.id ?? "";
|
|
478
|
+
if (!id) return null;
|
|
479
|
+
await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
|
|
480
|
+
let board = await ensureSessionKanbanBoard(context.projectRoot, id);
|
|
481
|
+
if (context.todos.length) {
|
|
482
|
+
board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
|
|
483
|
+
}
|
|
484
|
+
const planPath = context.meta["plan.path"];
|
|
485
|
+
if (typeof planPath === "string" && planPath) {
|
|
486
|
+
const plan = await loadPlan(planPath);
|
|
487
|
+
if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
|
|
488
|
+
}
|
|
489
|
+
const taskPath = context.meta["task.path"];
|
|
490
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
491
|
+
const tasks = await loadTasks(taskPath);
|
|
492
|
+
if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
|
|
493
|
+
}
|
|
494
|
+
return board;
|
|
495
|
+
}
|
|
496
|
+
function sourceStatus(task) {
|
|
497
|
+
if (task.status === "completed") return "completed";
|
|
498
|
+
if (task.status === "in_progress") return "in_progress";
|
|
499
|
+
if (task.status === "review") return "review";
|
|
500
|
+
if (task.status === "blocked") return "blocked";
|
|
501
|
+
if (task.status === "failed") return "failed";
|
|
502
|
+
return "pending";
|
|
503
|
+
}
|
|
504
|
+
function todoStatus(task) {
|
|
505
|
+
const status = sourceStatus(task);
|
|
506
|
+
if (status === "completed") return "completed";
|
|
507
|
+
if (status === "in_progress" || status === "review") return "in_progress";
|
|
508
|
+
return "pending";
|
|
509
|
+
}
|
|
510
|
+
function sessionTodoFromTask(task) {
|
|
511
|
+
return {
|
|
512
|
+
id: task.origin?.taskId ?? task.id,
|
|
513
|
+
content: task.title,
|
|
514
|
+
status: todoStatus(task),
|
|
515
|
+
...task.description ? { activeForm: task.description } : {}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function sameTodos(left, right) {
|
|
519
|
+
return left.length === right.length && left.every((todo, index) => {
|
|
520
|
+
const candidate = right[index];
|
|
521
|
+
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;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function applySessionKanbanBoardToTodos(context, board) {
|
|
525
|
+
const sessionId = context.session?.id ?? "";
|
|
526
|
+
if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
|
|
527
|
+
return [...context.todos];
|
|
528
|
+
}
|
|
529
|
+
const projectedTodos = board.tasks.filter(
|
|
530
|
+
(task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
|
|
531
|
+
).sort((left, right) => {
|
|
532
|
+
const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
|
|
533
|
+
const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
|
|
534
|
+
return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
|
|
535
|
+
}).map(sessionTodoFromTask);
|
|
536
|
+
const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
|
|
537
|
+
const effectiveTodos = allCompleted ? [] : projectedTodos;
|
|
538
|
+
if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
|
|
539
|
+
suppressedTodoMirrors.add(context);
|
|
540
|
+
try {
|
|
541
|
+
context.state.replaceTodos(projectedTodos);
|
|
542
|
+
} finally {
|
|
543
|
+
suppressedTodoMirrors.delete(context);
|
|
544
|
+
}
|
|
545
|
+
notifyTodoUpdate(context, context.todos);
|
|
546
|
+
broadcastTodoUpdate(context, context.todos);
|
|
547
|
+
return [...context.todos];
|
|
548
|
+
}
|
|
549
|
+
async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
550
|
+
const originId = task.origin?.taskId;
|
|
551
|
+
const graphId = task.origin?.graphId ?? "";
|
|
552
|
+
if (!originId) return { source: null };
|
|
553
|
+
if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
|
|
554
|
+
const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
|
|
555
|
+
(todo) => todo.id === originId ? {
|
|
556
|
+
...todo,
|
|
557
|
+
content: task.title,
|
|
558
|
+
status: sourceStatus(task) === "completed" ? "completed" : sourceStatus(task) === "in_progress" || sourceStatus(task) === "review" ? "in_progress" : "pending"
|
|
559
|
+
} : todo
|
|
560
|
+
);
|
|
561
|
+
suppressedTodoMirrors.add(context);
|
|
562
|
+
try {
|
|
563
|
+
context.state.replaceTodos(next);
|
|
564
|
+
} finally {
|
|
565
|
+
suppressedTodoMirrors.delete(context);
|
|
566
|
+
}
|
|
567
|
+
return { source: "todo", todos: [...context.todos] };
|
|
568
|
+
}
|
|
569
|
+
const id = context.session?.id ?? "";
|
|
570
|
+
if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
|
|
571
|
+
const planPath = context.meta["plan.path"];
|
|
572
|
+
if (typeof planPath !== "string" || !planPath) return { source: "plan" };
|
|
573
|
+
const plan = await mutatePlan(planPath, id, (file) => ({
|
|
574
|
+
...file,
|
|
575
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
576
|
+
items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
|
|
577
|
+
(item) => item.id === originId ? {
|
|
578
|
+
...item,
|
|
579
|
+
title: task.title,
|
|
580
|
+
details: task.description,
|
|
581
|
+
status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
|
|
582
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
583
|
+
} : item
|
|
584
|
+
)
|
|
585
|
+
}));
|
|
586
|
+
return { source: "plan", plan };
|
|
587
|
+
}
|
|
588
|
+
if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
|
|
589
|
+
const taskPath = context.meta["task.path"];
|
|
590
|
+
if (typeof taskPath !== "string" || !taskPath) return { source: "task" };
|
|
591
|
+
const tasks = await mutateTasks(taskPath, id, (file) => ({
|
|
592
|
+
...file,
|
|
593
|
+
tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
|
|
594
|
+
(item) => item.id === originId ? {
|
|
595
|
+
...item,
|
|
596
|
+
title: task.title,
|
|
597
|
+
description: task.description,
|
|
598
|
+
status: sourceStatus(task),
|
|
599
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
600
|
+
} : item
|
|
601
|
+
)
|
|
602
|
+
}));
|
|
603
|
+
return { source: "task", tasks };
|
|
604
|
+
}
|
|
605
|
+
return { source: null };
|
|
606
|
+
}
|
|
607
|
+
export {
|
|
608
|
+
SESSION_KANBAN_COLUMNS,
|
|
609
|
+
applySessionKanbanBoardToTodos,
|
|
610
|
+
applySessionKanbanTaskToSource,
|
|
611
|
+
attachSessionKanbanMirror,
|
|
612
|
+
cleanupEmptySessionKanbanBoards,
|
|
613
|
+
cleanupSessionKanbanBoardIfEmpty,
|
|
614
|
+
ensureSessionKanbanBoard,
|
|
615
|
+
hydrateSessionKanban,
|
|
616
|
+
mirrorSessionPlanToKanban,
|
|
617
|
+
mirrorSessionTasksToKanban,
|
|
618
|
+
mirrorSessionTodosToKanban,
|
|
619
|
+
planFileToSerializedGraph,
|
|
620
|
+
projectSessionPlanToKanban,
|
|
621
|
+
projectSessionTasksToKanban,
|
|
622
|
+
projectSessionTodosToKanban,
|
|
623
|
+
taskFileToSerializedGraph,
|
|
624
|
+
todoListToSerializedGraph
|
|
625
|
+
};
|
|
626
|
+
//# sourceMappingURL=session-kanban.js.map
|