@wrongstack/tools 0.313.0 → 0.313.1
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/audit.js +10 -4055
- package/dist/bash.js +9 -2308
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/builtin.js +69 -33949
- package/dist/chunk-2CTB3UK5.js +197 -0
- package/dist/chunk-2ME7ZWDD.js +1272 -0
- package/dist/chunk-2TFQDEPU.js +113 -0
- package/dist/chunk-2ZCJNKFE.js +315 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-3QMQBO5Z.js +89 -0
- package/dist/chunk-42AKKX7O.js +385 -0
- package/dist/chunk-42SKEETB.js +113 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-55KF77OK.js +1944 -0
- package/dist/chunk-6HLZK37X.js +359 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-B2CIOKWP.js +899 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DIJKHSUO.js +134 -0
- package/dist/chunk-DPL47QGS.js +3515 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FGOABE4Z.js +955 -0
- package/dist/chunk-FW65TP4G.js +520 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-H5MJUAA6.js +295 -0
- package/dist/chunk-H5OHH6F4.js +223 -0
- package/dist/chunk-HC5RM4WY.js +199 -0
- package/dist/chunk-HEQFMLZ7.js +190 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HXSBQ2SE.js +1723 -0
- package/dist/chunk-ILESNGNM.js +163 -0
- package/dist/chunk-INF5CGAO.js +137 -0
- package/dist/chunk-IWDFZ6XR.js +337 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-J4SRJKBW.js +428 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-KXNDHMUB.js +366 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-MAN26PZ5.js +260 -0
- package/dist/chunk-MKRQMLZA.js +474 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NHA6BEIH.js +101 -0
- package/dist/chunk-OA6YJIWL.js +4412 -0
- package/dist/chunk-OZC255IC.js +2830 -0
- package/dist/chunk-PUR6WU2B.js +154 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QHVW4AAM.js +892 -0
- package/dist/chunk-QNJ7AQ4B.js +1000 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RP2ZA47V.js +15 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-TVTCSBJF.js +122 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-URFKEIHM.js +1091 -0
- package/dist/chunk-UTV7YAW7.js +178 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-XMIFYKUT.js +1371 -0
- package/dist/chunk-YF2RSV34.js +54 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZIGAZFWE.js +522 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-6CA77WZ4.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35387
- package/dist/install.js +10 -4091
- package/dist/json-parser-7YMDJSBJ.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban.js +3 -2096
- package/dist/languages/index.js +23 -4664
- package/dist/legacy-bridge-7UKZT46G.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33897
- package/dist/parser-dispatch-VPOFDO2U.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/py-parser-GPM5VYRG.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-WVQ4KYCX.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -33984
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-YH4ZMWMH.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-GFAOKV53.js +157 -0
- package/package.json +5 -5
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
// src/session-kanban.ts
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
4
|
+
import { loadPlan, loadTasks } from "@wrongstack/core/storage";
|
|
5
|
+
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
6
|
+
import {
|
|
7
|
+
bridgeKanbanSupervisor,
|
|
8
|
+
compactSessionMirrorBoard,
|
|
9
|
+
createBoard,
|
|
10
|
+
DEFAULT_COLUMNS,
|
|
11
|
+
getBoard,
|
|
12
|
+
getKanbanOrchestrationSnapshot,
|
|
13
|
+
listBoards,
|
|
14
|
+
pruneSessionBoards,
|
|
15
|
+
removeBoard,
|
|
16
|
+
syncBoardFromTaskGraph,
|
|
17
|
+
touchKanbanPresence,
|
|
18
|
+
updateBoard
|
|
19
|
+
} from "@wrongstack/kanban";
|
|
20
|
+
|
|
21
|
+
// src/session-kanban-graph.ts
|
|
22
|
+
var PLAN_STATUS_TO_TASK = {
|
|
23
|
+
open: "pending",
|
|
24
|
+
in_progress: "in_progress",
|
|
25
|
+
done: "completed"
|
|
26
|
+
};
|
|
27
|
+
function todoListToSerializedGraph(todos, sessionId) {
|
|
28
|
+
const graphId = `todo:${sessionId}`;
|
|
29
|
+
const nodes = todos.map((todo, index) => ({
|
|
30
|
+
id: todo.id,
|
|
31
|
+
title: todo.content,
|
|
32
|
+
description: todo.activeForm ?? "",
|
|
33
|
+
type: "chore",
|
|
34
|
+
priority: "medium",
|
|
35
|
+
status: todo.status,
|
|
36
|
+
specRequirementId: `${graphId}:${todo.id}`,
|
|
37
|
+
createdAt: index,
|
|
38
|
+
updatedAt: index
|
|
39
|
+
}));
|
|
40
|
+
return {
|
|
41
|
+
id: graphId,
|
|
42
|
+
specId: graphId,
|
|
43
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
44
|
+
title: "Session todos",
|
|
45
|
+
nodes,
|
|
46
|
+
edges: [],
|
|
47
|
+
rootNodes: nodes.map((node) => node.id),
|
|
48
|
+
createdAt: 0,
|
|
49
|
+
updatedAt: 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
53
|
+
const graphId = `session:${sessionId}`;
|
|
54
|
+
const ids = new Set(tasks.map((task) => task.id));
|
|
55
|
+
const nodes = tasks.map((task, index) => ({
|
|
56
|
+
id: task.id,
|
|
57
|
+
title: task.title,
|
|
58
|
+
description: task.description ?? "",
|
|
59
|
+
type: task.type,
|
|
60
|
+
priority: task.priority,
|
|
61
|
+
status: task.status,
|
|
62
|
+
specRequirementId: `${graphId}:${task.id}`,
|
|
63
|
+
...task.assignee ? { assignee: task.assignee } : {},
|
|
64
|
+
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
65
|
+
createdAt: index,
|
|
66
|
+
updatedAt: index
|
|
67
|
+
}));
|
|
68
|
+
const edges = tasks.flatMap(
|
|
69
|
+
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
70
|
+
id: `${dependency}->${task.id}`,
|
|
71
|
+
from: dependency,
|
|
72
|
+
to: task.id,
|
|
73
|
+
type: "depends_on"
|
|
74
|
+
}))
|
|
75
|
+
);
|
|
76
|
+
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
77
|
+
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
78
|
+
return {
|
|
79
|
+
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
80
|
+
id: graphId,
|
|
81
|
+
specId: graphId,
|
|
82
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
83
|
+
title: "Session tasks",
|
|
84
|
+
nodes,
|
|
85
|
+
edges,
|
|
86
|
+
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
87
|
+
createdAt: 0,
|
|
88
|
+
updatedAt: 0
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function planFileToSerializedGraph(items, sessionId) {
|
|
92
|
+
const graphId = `plan:${sessionId}`;
|
|
93
|
+
const nodes = items.map((item, index) => ({
|
|
94
|
+
id: item.id,
|
|
95
|
+
title: item.title,
|
|
96
|
+
description: item.details ?? "",
|
|
97
|
+
type: "chore",
|
|
98
|
+
priority: "medium",
|
|
99
|
+
status: PLAN_STATUS_TO_TASK[item.status],
|
|
100
|
+
specRequirementId: `${graphId}:${item.id}`,
|
|
101
|
+
createdAt: index,
|
|
102
|
+
updatedAt: index
|
|
103
|
+
}));
|
|
104
|
+
return {
|
|
105
|
+
id: graphId,
|
|
106
|
+
specId: graphId,
|
|
107
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
108
|
+
title: "Session plan",
|
|
109
|
+
nodes,
|
|
110
|
+
edges: [],
|
|
111
|
+
rootNodes: nodes.map((node) => node.id),
|
|
112
|
+
createdAt: 0,
|
|
113
|
+
updatedAt: 0
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/session-kanban-sync.ts
|
|
118
|
+
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
119
|
+
import { mutatePlan, mutateTasks } from "@wrongstack/core/storage";
|
|
120
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
121
|
+
import {
|
|
122
|
+
getDependencyReadinessIssues
|
|
123
|
+
} from "@wrongstack/kanban";
|
|
124
|
+
function sourceStatus(task) {
|
|
125
|
+
if (task.status === "completed") return "completed";
|
|
126
|
+
if (task.status === "in_progress") return "in_progress";
|
|
127
|
+
if (task.status === "review") return "review";
|
|
128
|
+
if (task.status === "blocked") return "blocked";
|
|
129
|
+
if (task.status === "failed") return "failed";
|
|
130
|
+
return "pending";
|
|
131
|
+
}
|
|
132
|
+
function todoStatus(task) {
|
|
133
|
+
const status = sourceStatus(task);
|
|
134
|
+
if (status === "completed") return "completed";
|
|
135
|
+
if (status === "review" && task.assignment?.status === "completed") return "completed";
|
|
136
|
+
if (status === "in_progress" || status === "review") return "in_progress";
|
|
137
|
+
return "pending";
|
|
138
|
+
}
|
|
139
|
+
function sessionTodoFromTask(task, board) {
|
|
140
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
141
|
+
return {
|
|
142
|
+
id: task.origin?.taskId ?? task.id,
|
|
143
|
+
content: task.title,
|
|
144
|
+
status: todoStatus(task),
|
|
145
|
+
...task.description ? { activeForm: task.description } : {},
|
|
146
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function managedTodoFromTask(task, board) {
|
|
150
|
+
return {
|
|
151
|
+
...sessionTodoFromTask(task, board),
|
|
152
|
+
kanbanBoardId: board.id,
|
|
153
|
+
kanbanTaskId: task.id
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function blockingTitles(board, task) {
|
|
157
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
158
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
159
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
160
|
+
return dependency.title;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
var PRIORITY_ORDER = {
|
|
164
|
+
critical: 0,
|
|
165
|
+
high: 1,
|
|
166
|
+
medium: 2,
|
|
167
|
+
low: 3
|
|
168
|
+
};
|
|
169
|
+
function orderTasksForTodos(board, tasks) {
|
|
170
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
171
|
+
const baseline = [...tasks].sort(
|
|
172
|
+
(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)
|
|
173
|
+
);
|
|
174
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
175
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
176
|
+
const emitted = [];
|
|
177
|
+
const done = /* @__PURE__ */ new Set();
|
|
178
|
+
while (remaining.size > 0) {
|
|
179
|
+
const ready = baseline.filter(
|
|
180
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
181
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
if (ready.length === 0) break;
|
|
185
|
+
for (const task of ready) {
|
|
186
|
+
remaining.delete(task.id);
|
|
187
|
+
done.add(task.id);
|
|
188
|
+
emitted.push(task);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
192
|
+
return emitted;
|
|
193
|
+
}
|
|
194
|
+
function sameTodos(left, right) {
|
|
195
|
+
return left.length === right.length && left.every((todo, index) => {
|
|
196
|
+
const candidate = right[index];
|
|
197
|
+
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 && (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function broadcastTodoUpdate(context, todos) {
|
|
201
|
+
const sessionId = context.session?.id ?? "";
|
|
202
|
+
if (!context.agentId || !sessionId) return;
|
|
203
|
+
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
204
|
+
for (const todo of todos) {
|
|
205
|
+
if (todo.status === "completed") statusCounts.completed++;
|
|
206
|
+
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
207
|
+
else statusCounts.pending++;
|
|
208
|
+
}
|
|
209
|
+
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
210
|
+
const mailbox = getSharedProjectMailbox(projectDir);
|
|
211
|
+
void mailbox.send({
|
|
212
|
+
from: context.agentId,
|
|
213
|
+
to: `@session:${sessionId}`,
|
|
214
|
+
type: "status",
|
|
215
|
+
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
216
|
+
body: `Shared Kanban board synced this session's todo list: ${todos.length} item${todos.length === 1 ? "" : "s"} \u2014 ${statusCounts.completed} completed, ${statusCounts.inProgress} in progress, ${statusCounts.pending} pending.`,
|
|
217
|
+
priority: "normal",
|
|
218
|
+
senderSessionId: sessionId
|
|
219
|
+
}).catch(() => {
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function notifyTodoUpdate(context, todos) {
|
|
223
|
+
const summary = formatTodosForModel(todos);
|
|
224
|
+
const text = `[KANBAN TODO UPDATE]
|
|
225
|
+
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
226
|
+
${summary}
|
|
227
|
+
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.`;
|
|
228
|
+
const state = context.state;
|
|
229
|
+
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
230
|
+
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
231
|
+
}
|
|
232
|
+
if (typeof state.appendMessage === "function") {
|
|
233
|
+
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function todosNeedingSessionMirror(todos, activeBoardId) {
|
|
237
|
+
if (!activeBoardId) return todos;
|
|
238
|
+
return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId || !todo.kanbanTaskId);
|
|
239
|
+
}
|
|
240
|
+
function applySessionKanbanBoardToTodos(context, board, options) {
|
|
241
|
+
const sessionId = context.session?.id ?? "";
|
|
242
|
+
if (!sessionId || options.sessionIdFromTags(board.tags) !== sessionId || !options.isOwnedSessionBoard(board.tags)) {
|
|
243
|
+
return [...context.todos];
|
|
244
|
+
}
|
|
245
|
+
if (options.hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
|
|
246
|
+
const projectedTodos = orderTasksForTodos(
|
|
247
|
+
board,
|
|
248
|
+
board.tasks.filter(
|
|
249
|
+
(task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
|
|
250
|
+
)
|
|
251
|
+
).map((task) => sessionTodoFromTask(task, board));
|
|
252
|
+
const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
|
|
253
|
+
const effectiveTodos = allCompleted ? [] : projectedTodos;
|
|
254
|
+
if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
|
|
255
|
+
options.suppressedTodoMirrors.add(context);
|
|
256
|
+
try {
|
|
257
|
+
context.state.replaceTodos(projectedTodos);
|
|
258
|
+
} finally {
|
|
259
|
+
options.suppressedTodoMirrors.delete(context);
|
|
260
|
+
}
|
|
261
|
+
notifyTodoUpdate(context, context.todos);
|
|
262
|
+
broadcastTodoUpdate(context, context.todos);
|
|
263
|
+
return [...context.todos];
|
|
264
|
+
}
|
|
265
|
+
function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2, options = {}) {
|
|
266
|
+
const metaKanban = context.meta["kanban"];
|
|
267
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
268
|
+
const activeBoardId = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
269
|
+
if (!activeBoardId || board.id !== activeBoardId || board.lifecycle?.mode !== "managed") {
|
|
270
|
+
return [...context.todos];
|
|
271
|
+
}
|
|
272
|
+
const ownerSessionId = options.sessionOwnerFromTags?.(board.tags);
|
|
273
|
+
if (ownerSessionId && ownerSessionId !== (context.session?.id ?? "")) {
|
|
274
|
+
return [...context.todos];
|
|
275
|
+
}
|
|
276
|
+
const projectedTodos = orderTasksForTodos(
|
|
277
|
+
board,
|
|
278
|
+
board.tasks.filter(
|
|
279
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
280
|
+
)
|
|
281
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
282
|
+
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
283
|
+
suppressedTodoMirrors2.add(context);
|
|
284
|
+
try {
|
|
285
|
+
context.state.replaceTodos(projectedTodos);
|
|
286
|
+
} finally {
|
|
287
|
+
suppressedTodoMirrors2.delete(context);
|
|
288
|
+
}
|
|
289
|
+
notifyTodoUpdate(context, context.todos);
|
|
290
|
+
broadcastTodoUpdate(context, context.todos);
|
|
291
|
+
return [...context.todos];
|
|
292
|
+
}
|
|
293
|
+
async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
|
|
294
|
+
const originId = task.origin?.taskId;
|
|
295
|
+
const graphId = task.origin?.graphId ?? "";
|
|
296
|
+
const isTodoOrigin = task.origin?.system === "session-todo" || graphId.startsWith("todo:");
|
|
297
|
+
const targetTodo = context.todos?.find(
|
|
298
|
+
(todo) => originId !== void 0 && todo.id === originId || todo.kanbanTaskId === task.id || todo.id === task.id && !todo.promotedFromPlan && !todo.promotedFromTask
|
|
299
|
+
);
|
|
300
|
+
if (isTodoOrigin || targetTodo) {
|
|
301
|
+
const matchedId = targetTodo?.id ?? originId;
|
|
302
|
+
if (matchedId) {
|
|
303
|
+
const mappedStatus = todoStatus(task);
|
|
304
|
+
const next = options.remove ? context.todos.filter((todo) => todo.id !== matchedId) : context.todos.map(
|
|
305
|
+
(todo) => todo.id === matchedId ? {
|
|
306
|
+
...todo,
|
|
307
|
+
content: task.title,
|
|
308
|
+
status: mappedStatus,
|
|
309
|
+
kanbanTaskId: task.id
|
|
310
|
+
} : todo
|
|
311
|
+
);
|
|
312
|
+
suppressedTodoMirrors2.add(context);
|
|
313
|
+
try {
|
|
314
|
+
context.state.replaceTodos(next);
|
|
315
|
+
} finally {
|
|
316
|
+
suppressedTodoMirrors2.delete(context);
|
|
317
|
+
}
|
|
318
|
+
return { source: "todo", todos: [...context.todos] };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!originId) return { source: null };
|
|
322
|
+
const id = context.session?.id ?? "";
|
|
323
|
+
if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
|
|
324
|
+
const planPath = context.meta["plan.path"];
|
|
325
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"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."
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const plan = await mutatePlan(planPath, id, (file) => ({
|
|
331
|
+
...file,
|
|
332
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
333
|
+
items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
|
|
334
|
+
(item) => item.id === originId ? {
|
|
335
|
+
...item,
|
|
336
|
+
title: task.title,
|
|
337
|
+
details: task.description,
|
|
338
|
+
status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
|
|
339
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
340
|
+
} : item
|
|
341
|
+
)
|
|
342
|
+
}));
|
|
343
|
+
return { source: "plan", plan };
|
|
344
|
+
}
|
|
345
|
+
if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
|
|
346
|
+
const taskPath = context.meta["task.path"];
|
|
347
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"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."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const tasks = await mutateTasks(taskPath, id, (file) => ({
|
|
353
|
+
...file,
|
|
354
|
+
tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
|
|
355
|
+
(item) => item.id === originId ? {
|
|
356
|
+
...item,
|
|
357
|
+
title: task.title,
|
|
358
|
+
description: task.description,
|
|
359
|
+
status: sourceStatus(task),
|
|
360
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
361
|
+
} : item
|
|
362
|
+
)
|
|
363
|
+
}));
|
|
364
|
+
return { source: "task", tasks };
|
|
365
|
+
}
|
|
366
|
+
return { source: null };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/session-kanban.ts
|
|
370
|
+
var SESSION_BOARD_TAG = "session-work";
|
|
371
|
+
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
372
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
373
|
+
...column
|
|
374
|
+
}));
|
|
375
|
+
var boardQueue = /* @__PURE__ */ new Map();
|
|
376
|
+
var boardEnsures = /* @__PURE__ */ new Map();
|
|
377
|
+
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
378
|
+
var activeMirrors = /* @__PURE__ */ new Set();
|
|
379
|
+
var mirrorFailures = /* @__PURE__ */ new Map();
|
|
380
|
+
var bindings = /* @__PURE__ */ new WeakMap();
|
|
381
|
+
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
382
|
+
var activeSessionBoards = /* @__PURE__ */ new Map();
|
|
383
|
+
function boardKey(projectRoot, sessionId) {
|
|
384
|
+
return `${projectRoot}\0${sessionId}`;
|
|
385
|
+
}
|
|
386
|
+
function mirrorKey(projectRoot, sessionId, sourceSystem) {
|
|
387
|
+
return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
|
|
388
|
+
}
|
|
389
|
+
function completedReconciliationGraph(latest, candidates) {
|
|
390
|
+
const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
|
|
391
|
+
const carriedNodeIds = /* @__PURE__ */ new Set();
|
|
392
|
+
const completedNodes = candidates.flatMap(
|
|
393
|
+
(candidate) => candidate.nodes.filter((node) => {
|
|
394
|
+
if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
carriedNodeIds.add(node.id);
|
|
398
|
+
return true;
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
if (completedNodes.length === 0) return void 0;
|
|
402
|
+
const carriedRequirements = completedNodes.flatMap(
|
|
403
|
+
(node) => node.specRequirementId ? [node.specRequirementId] : []
|
|
404
|
+
);
|
|
405
|
+
return {
|
|
406
|
+
...latest,
|
|
407
|
+
nodes: [...latest.nodes, ...completedNodes],
|
|
408
|
+
rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
|
|
409
|
+
...latest.requiredRequirementIds ? {
|
|
410
|
+
requiredRequirementIds: [
|
|
411
|
+
.../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
|
|
412
|
+
]
|
|
413
|
+
} : {}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function sessionTag(sessionId) {
|
|
417
|
+
return `session:${sessionId}`;
|
|
418
|
+
}
|
|
419
|
+
function sessionBoardTitle(sessionId) {
|
|
420
|
+
const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
|
|
421
|
+
return `Session ${leaf.slice(0, 12)}`;
|
|
422
|
+
}
|
|
423
|
+
function sessionBoardTags(sessionId) {
|
|
424
|
+
return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
|
|
425
|
+
}
|
|
426
|
+
function sessionIdFromTags(tags) {
|
|
427
|
+
const tag = tags?.find((candidate) => candidate.startsWith("session:"));
|
|
428
|
+
return tag?.slice("session:".length) || null;
|
|
429
|
+
}
|
|
430
|
+
function isOwnedSessionBoard(tags) {
|
|
431
|
+
return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));
|
|
432
|
+
}
|
|
433
|
+
function retainActiveSessionBoard(projectRoot, sessionId) {
|
|
434
|
+
const key = boardKey(projectRoot, sessionId);
|
|
435
|
+
activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);
|
|
436
|
+
}
|
|
437
|
+
function releaseActiveSessionBoard(projectRoot, sessionId) {
|
|
438
|
+
const key = boardKey(projectRoot, sessionId);
|
|
439
|
+
const remaining = (activeSessionBoards.get(key) ?? 0) - 1;
|
|
440
|
+
if (remaining > 0) activeSessionBoards.set(key, remaining);
|
|
441
|
+
else activeSessionBoards.delete(key);
|
|
442
|
+
}
|
|
443
|
+
function isSessionBoardActive(projectRoot, sessionId) {
|
|
444
|
+
return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;
|
|
445
|
+
}
|
|
446
|
+
function sameColumns(columns) {
|
|
447
|
+
return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
|
|
448
|
+
}
|
|
449
|
+
async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
450
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
451
|
+
const key = boardKey(projectRoot, sessionId);
|
|
452
|
+
const inFlight = boardEnsures.get(key);
|
|
453
|
+
if (inFlight) return inFlight;
|
|
454
|
+
const promise = (async () => {
|
|
455
|
+
const summary = (await listBoards(projectRoot)).find(
|
|
456
|
+
(board2) => board2.tags?.includes(sessionTag(sessionId))
|
|
457
|
+
);
|
|
458
|
+
let board = summary ? await getBoard(projectRoot, summary.id) : null;
|
|
459
|
+
if (!board) {
|
|
460
|
+
return createBoard(projectRoot, {
|
|
461
|
+
title: sessionBoardTitle(sessionId),
|
|
462
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
463
|
+
tags: sessionBoardTags(sessionId),
|
|
464
|
+
columns: SESSION_KANBAN_COLUMNS,
|
|
465
|
+
generatedBy: `session-kanban:${sessionId}`,
|
|
466
|
+
kind: "session_mirror",
|
|
467
|
+
retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
471
|
+
board = await updateBoard(projectRoot, board.id, {
|
|
472
|
+
title: sessionBoardTitle(sessionId),
|
|
473
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
474
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
475
|
+
columns: SESSION_KANBAN_COLUMNS
|
|
476
|
+
}) ?? board;
|
|
477
|
+
}
|
|
478
|
+
return board;
|
|
479
|
+
})();
|
|
480
|
+
boardEnsures.set(key, promise);
|
|
481
|
+
try {
|
|
482
|
+
return await promise;
|
|
483
|
+
} finally {
|
|
484
|
+
boardEnsures.delete(key);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function enqueueBoardWork(projectRoot, sessionId, work) {
|
|
488
|
+
const key = boardKey(projectRoot, sessionId);
|
|
489
|
+
const previous = boardQueue.get(key) ?? Promise.resolve();
|
|
490
|
+
const result = previous.catch(() => void 0).then(work);
|
|
491
|
+
const tail = result.then(
|
|
492
|
+
() => void 0,
|
|
493
|
+
() => void 0
|
|
494
|
+
);
|
|
495
|
+
boardQueue.set(key, tail);
|
|
496
|
+
void tail.then(() => {
|
|
497
|
+
if (boardQueue.get(key) === tail) boardQueue.delete(key);
|
|
498
|
+
});
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
async function removeEmptySessionBoard(projectRoot, boardId, sessionId) {
|
|
502
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
503
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return null;
|
|
504
|
+
const board = await getBoard(projectRoot, boardId);
|
|
505
|
+
if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;
|
|
506
|
+
if (sessionIdFromTags(board.tags) !== sessionId) return null;
|
|
507
|
+
return await removeBoard(projectRoot, board.id) ? board.id : null;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async function removeOwnedSessionBoard(projectRoot, boardId, sessionId) {
|
|
511
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
512
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return null;
|
|
513
|
+
const board = await getBoard(projectRoot, boardId);
|
|
514
|
+
if (!board || !isOwnedSessionBoard(board.tags)) return null;
|
|
515
|
+
if (sessionIdFromTags(board.tags) !== sessionId) return null;
|
|
516
|
+
return await removeBoard(projectRoot, board.id) ? board.id : null;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
async function cleanupSessionKanbanBoard(projectRoot, sessionId) {
|
|
520
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
|
|
521
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return [];
|
|
522
|
+
const candidates = (await listBoards(projectRoot)).filter(
|
|
523
|
+
(board) => isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
|
|
524
|
+
);
|
|
525
|
+
const removed = await Promise.all(
|
|
526
|
+
candidates.map((board) => removeOwnedSessionBoard(projectRoot, board.id, sessionId))
|
|
527
|
+
);
|
|
528
|
+
return removed.filter((boardId) => Boolean(boardId));
|
|
529
|
+
}
|
|
530
|
+
async function cleanupSessionKanbanBoardIfEmpty(projectRoot, sessionId) {
|
|
531
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
|
|
532
|
+
if (isSessionBoardActive(projectRoot, sessionId)) return [];
|
|
533
|
+
const candidates = (await listBoards(projectRoot)).filter(
|
|
534
|
+
(board) => board.taskCount === 0 && isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
|
|
535
|
+
);
|
|
536
|
+
const removed = await Promise.all(
|
|
537
|
+
candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId))
|
|
538
|
+
);
|
|
539
|
+
return removed.filter((boardId) => Boolean(boardId));
|
|
540
|
+
}
|
|
541
|
+
async function cleanupEmptySessionKanbanBoards(projectRoot, activeSessionId = "") {
|
|
542
|
+
if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === "0") return [];
|
|
543
|
+
const candidates = (await listBoards(projectRoot)).flatMap((board) => {
|
|
544
|
+
const ownerSessionId = sessionIdFromTags(board.tags);
|
|
545
|
+
return board.taskCount === 0 && isOwnedSessionBoard(board.tags) && ownerSessionId && ownerSessionId !== activeSessionId && !isSessionBoardActive(projectRoot, ownerSessionId) ? [{ boardId: board.id, sessionId: ownerSessionId }] : [];
|
|
546
|
+
});
|
|
547
|
+
const removed = await Promise.all(
|
|
548
|
+
candidates.map(
|
|
549
|
+
({ boardId, sessionId }) => removeEmptySessionBoard(projectRoot, boardId, sessionId)
|
|
550
|
+
)
|
|
551
|
+
);
|
|
552
|
+
return removed.filter((boardId) => Boolean(boardId));
|
|
553
|
+
}
|
|
554
|
+
async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
555
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
556
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
557
|
+
const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
|
|
558
|
+
if (!board) return null;
|
|
559
|
+
const result = await syncBoardFromTaskGraph(
|
|
560
|
+
projectRoot,
|
|
561
|
+
board.id,
|
|
562
|
+
deserializeTaskGraph(graph),
|
|
563
|
+
{
|
|
564
|
+
sourceSystem,
|
|
565
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
566
|
+
archiveMissingTasks: true,
|
|
567
|
+
includeCompletedTasks: true,
|
|
568
|
+
allowRequirementScopeShrink: true
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
if (!result) return null;
|
|
572
|
+
const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
|
|
573
|
+
if (compacted?.removedTaskIds.length) {
|
|
574
|
+
return await getBoard(projectRoot, board.id) ?? result.board;
|
|
575
|
+
}
|
|
576
|
+
return result.board;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
580
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
|
|
581
|
+
const key = mirrorKey(projectRoot, sessionId, sourceSystem);
|
|
582
|
+
const previous = pendingMirrors.get(key);
|
|
583
|
+
const reconciliationGraph = previous ? completedReconciliationGraph(
|
|
584
|
+
graph,
|
|
585
|
+
[previous.reconciliationGraph, previous.graph].filter(
|
|
586
|
+
(candidate) => candidate !== void 0
|
|
587
|
+
)
|
|
588
|
+
) : void 0;
|
|
589
|
+
pendingMirrors.set(key, {
|
|
590
|
+
projectRoot,
|
|
591
|
+
sessionId,
|
|
592
|
+
graph,
|
|
593
|
+
...reconciliationGraph ? { reconciliationGraph } : {},
|
|
594
|
+
sourceSystem
|
|
595
|
+
});
|
|
596
|
+
if (activeMirrors.has(key)) return;
|
|
597
|
+
activeMirrors.add(key);
|
|
598
|
+
void (async () => {
|
|
599
|
+
try {
|
|
600
|
+
for (; ; ) {
|
|
601
|
+
const pending = pendingMirrors.get(key);
|
|
602
|
+
if (!pending) break;
|
|
603
|
+
pendingMirrors.delete(key);
|
|
604
|
+
try {
|
|
605
|
+
if (pending.reconciliationGraph) {
|
|
606
|
+
await projectGraph(
|
|
607
|
+
pending.projectRoot,
|
|
608
|
+
pending.sessionId,
|
|
609
|
+
pending.reconciliationGraph,
|
|
610
|
+
pending.sourceSystem
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
await projectGraph(
|
|
614
|
+
pending.projectRoot,
|
|
615
|
+
pending.sessionId,
|
|
616
|
+
pending.graph,
|
|
617
|
+
pending.sourceSystem
|
|
618
|
+
);
|
|
619
|
+
mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
|
|
620
|
+
} catch (error) {
|
|
621
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
622
|
+
mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
|
|
623
|
+
message,
|
|
624
|
+
sourceSystem: pending.sourceSystem
|
|
625
|
+
});
|
|
626
|
+
console.warn(
|
|
627
|
+
JSON.stringify({
|
|
628
|
+
level: "warn",
|
|
629
|
+
event: "session-kanban.mirror-failed",
|
|
630
|
+
sessionId: pending.sessionId,
|
|
631
|
+
sourceSystem: pending.sourceSystem,
|
|
632
|
+
message,
|
|
633
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} finally {
|
|
639
|
+
activeMirrors.delete(key);
|
|
640
|
+
const pending = pendingMirrors.get(key);
|
|
641
|
+
if (pending) {
|
|
642
|
+
pendingMirrors.delete(key);
|
|
643
|
+
queueLatestMirror(
|
|
644
|
+
pending.projectRoot,
|
|
645
|
+
pending.sessionId,
|
|
646
|
+
pending.graph,
|
|
647
|
+
pending.sourceSystem
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
652
|
+
}
|
|
653
|
+
function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
654
|
+
if (!projectRoot || !sessionId) return void 0;
|
|
655
|
+
const key = boardKey(projectRoot, sessionId);
|
|
656
|
+
const failure = mirrorFailures.get(key);
|
|
657
|
+
if (!failure) return void 0;
|
|
658
|
+
mirrorFailures.delete(key);
|
|
659
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
660
|
+
}
|
|
661
|
+
function hasInFlightTodoMirror(projectRoot, sessionId) {
|
|
662
|
+
if (!projectRoot || !sessionId) return false;
|
|
663
|
+
const key = mirrorKey(projectRoot, sessionId, "session-todo");
|
|
664
|
+
return pendingMirrors.has(key) || activeMirrors.has(key);
|
|
665
|
+
}
|
|
666
|
+
function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
667
|
+
return projectGraph(
|
|
668
|
+
projectRoot,
|
|
669
|
+
sessionId,
|
|
670
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
671
|
+
"session-todo"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
675
|
+
return projectGraph(
|
|
676
|
+
projectRoot,
|
|
677
|
+
sessionId,
|
|
678
|
+
taskFileToSerializedGraph(tasks, sessionId),
|
|
679
|
+
"session-task"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
function projectSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
683
|
+
return projectGraph(
|
|
684
|
+
projectRoot,
|
|
685
|
+
sessionId,
|
|
686
|
+
planFileToSerializedGraph(items, sessionId),
|
|
687
|
+
"session-plan"
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
function fireAndForget(context, work) {
|
|
691
|
+
void work.catch((err) => {
|
|
692
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
693
|
+
console.warn(
|
|
694
|
+
JSON.stringify({
|
|
695
|
+
level: "warn",
|
|
696
|
+
event: "session-kanban",
|
|
697
|
+
context,
|
|
698
|
+
message,
|
|
699
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
700
|
+
})
|
|
701
|
+
);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
705
|
+
queueLatestMirror(
|
|
706
|
+
projectRoot,
|
|
707
|
+
sessionId,
|
|
708
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
709
|
+
"session-todo"
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
713
|
+
queueLatestMirror(
|
|
714
|
+
projectRoot,
|
|
715
|
+
sessionId,
|
|
716
|
+
taskFileToSerializedGraph(tasks, sessionId),
|
|
717
|
+
"session-task"
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
721
|
+
queueLatestMirror(
|
|
722
|
+
projectRoot,
|
|
723
|
+
sessionId,
|
|
724
|
+
planFileToSerializedGraph(items, sessionId),
|
|
725
|
+
"session-plan"
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
function attachSessionKanbanMirror(context) {
|
|
729
|
+
const existing = bindings.get(context);
|
|
730
|
+
if (existing) return existing;
|
|
731
|
+
const attachedProjectRoot = context.projectRoot ?? "";
|
|
732
|
+
let registeredSessionId = "";
|
|
733
|
+
const syncActiveSessionRegistration = () => {
|
|
734
|
+
if (!attachedProjectRoot) return;
|
|
735
|
+
const currentSessionId = context.session?.id ?? "";
|
|
736
|
+
if (currentSessionId === registeredSessionId) return;
|
|
737
|
+
if (registeredSessionId) {
|
|
738
|
+
releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
739
|
+
fireAndForget(
|
|
740
|
+
"cleanup-board",
|
|
741
|
+
cleanupSessionKanbanBoard(attachedProjectRoot, registeredSessionId)
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
registeredSessionId = currentSessionId;
|
|
745
|
+
if (registeredSessionId) {
|
|
746
|
+
retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
syncActiveSessionRegistration();
|
|
750
|
+
let watcher = null;
|
|
751
|
+
let watchedDir = "";
|
|
752
|
+
let timer = null;
|
|
753
|
+
let unsubscribeBoardEvents = null;
|
|
754
|
+
let watchedBoardId = "";
|
|
755
|
+
let boardTimer = null;
|
|
756
|
+
let presenceTimer = null;
|
|
757
|
+
const sessionId = () => context.session?.id ?? "";
|
|
758
|
+
const activeManagedBoardId = () => {
|
|
759
|
+
const metaKanban = context.meta["kanban"];
|
|
760
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
761
|
+
return context.currentKanbanBoardId ?? (typeof metaBoardId === "string" && metaBoardId ? metaBoardId : "");
|
|
762
|
+
};
|
|
763
|
+
const refreshFiles = async () => {
|
|
764
|
+
const id = sessionId();
|
|
765
|
+
if (!id) return;
|
|
766
|
+
const planPath = context.meta["plan.path"];
|
|
767
|
+
if (typeof planPath === "string" && planPath) {
|
|
768
|
+
const plan = await loadPlan(planPath);
|
|
769
|
+
if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
|
|
770
|
+
}
|
|
771
|
+
const taskPath = context.meta["task.path"];
|
|
772
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
773
|
+
const tasks = await loadTasks(taskPath);
|
|
774
|
+
if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const refreshBoard = async () => {
|
|
778
|
+
if (!watchedBoardId) return;
|
|
779
|
+
const board = await getBoard(context.projectRoot, watchedBoardId);
|
|
780
|
+
if (!board) return;
|
|
781
|
+
if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
|
|
782
|
+
else applySessionKanbanBoardToTodos2(context, board);
|
|
783
|
+
};
|
|
784
|
+
const configureBoardWatcher = async () => {
|
|
785
|
+
const id = sessionId();
|
|
786
|
+
const managedBoardId = activeManagedBoardId();
|
|
787
|
+
const board = managedBoardId ? await getBoard(context.projectRoot, managedBoardId) : id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
|
|
788
|
+
if (!board) return;
|
|
789
|
+
if (board.id === watchedBoardId) {
|
|
790
|
+
await refreshBoard();
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
unsubscribeBoardEvents?.();
|
|
794
|
+
unsubscribeBoardEvents = null;
|
|
795
|
+
watchedBoardId = board.id;
|
|
796
|
+
try {
|
|
797
|
+
unsubscribeBoardEvents = bridgeKanbanSupervisor(
|
|
798
|
+
context.projectRoot,
|
|
799
|
+
(event) => {
|
|
800
|
+
const data = event.data;
|
|
801
|
+
if (data?.boardId !== board.id) return;
|
|
802
|
+
if (boardTimer) clearTimeout(boardTimer);
|
|
803
|
+
boardTimer = setTimeout(() => fireAndForget("refresh-board", refreshBoard()), 60);
|
|
804
|
+
},
|
|
805
|
+
{ autoReconnect: true, reconnectDelayMs: 1e3 }
|
|
806
|
+
);
|
|
807
|
+
const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
|
|
808
|
+
sessionId: id,
|
|
809
|
+
agentId: context.agentId,
|
|
810
|
+
agentName: context.agentName
|
|
811
|
+
});
|
|
812
|
+
fireAndForget("touch-presence", touchPresence());
|
|
813
|
+
if (presenceTimer) clearInterval(presenceTimer);
|
|
814
|
+
presenceTimer = setInterval(() => fireAndForget("touch-presence", touchPresence()), 6e4);
|
|
815
|
+
presenceTimer.unref?.();
|
|
816
|
+
if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
|
|
817
|
+
} catch {
|
|
818
|
+
unsubscribeBoardEvents = null;
|
|
819
|
+
watchedBoardId = "";
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
const configureWatcher = () => {
|
|
823
|
+
const planPath = context.meta["plan.path"];
|
|
824
|
+
const taskPath = context.meta["task.path"];
|
|
825
|
+
const candidate = typeof planPath === "string" && planPath ? dirname(planPath) : typeof taskPath === "string" && taskPath ? dirname(taskPath) : "";
|
|
826
|
+
if (!candidate || candidate === watchedDir) return;
|
|
827
|
+
watcher?.close();
|
|
828
|
+
watcher = null;
|
|
829
|
+
watchedDir = candidate;
|
|
830
|
+
try {
|
|
831
|
+
watcher = watch(candidate, { persistent: false }, (_event, filename) => {
|
|
832
|
+
const name = filename?.toString();
|
|
833
|
+
const currentPlanPath = context.meta["plan.path"];
|
|
834
|
+
const currentTaskPath = context.meta["task.path"];
|
|
835
|
+
const planName = typeof currentPlanPath === "string" ? basename(currentPlanPath) : "";
|
|
836
|
+
const taskName = typeof currentTaskPath === "string" ? basename(currentTaskPath) : "";
|
|
837
|
+
if (name && name !== planName && name !== taskName) return;
|
|
838
|
+
if (timer) clearTimeout(timer);
|
|
839
|
+
timer = setTimeout(() => fireAndForget("refresh-files", refreshFiles()), 60);
|
|
840
|
+
});
|
|
841
|
+
watcher.on("error", () => watcher?.close());
|
|
842
|
+
} catch {
|
|
843
|
+
watcher = null;
|
|
844
|
+
watchedDir = "";
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const unsubscribe = context.state.onChange((change) => {
|
|
848
|
+
if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
|
|
849
|
+
const snapshot = change.completedSnapshot ?? change.todos;
|
|
850
|
+
const unbound = todosNeedingSessionMirror(snapshot, activeManagedBoardId());
|
|
851
|
+
if (snapshot.length > 0 && unbound.length === 0) return;
|
|
852
|
+
mirrorSessionTodosToKanban(context.projectRoot, unbound, sessionId());
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path" || change.key === "kanban")) {
|
|
856
|
+
syncActiveSessionRegistration();
|
|
857
|
+
configureWatcher();
|
|
858
|
+
if (!activeManagedBoardId()) {
|
|
859
|
+
fireAndForget("ensure-board", ensureSessionKanbanBoard(context.projectRoot, sessionId()));
|
|
860
|
+
}
|
|
861
|
+
fireAndForget("configure-watcher", configureBoardWatcher());
|
|
862
|
+
fireAndForget("refresh-board", refreshBoard());
|
|
863
|
+
fireAndForget("refresh-files", refreshFiles());
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
configureWatcher();
|
|
867
|
+
fireAndForget("configure-watcher", configureBoardWatcher());
|
|
868
|
+
const detach = () => {
|
|
869
|
+
unsubscribe();
|
|
870
|
+
if (timer) clearTimeout(timer);
|
|
871
|
+
if (boardTimer) clearTimeout(boardTimer);
|
|
872
|
+
if (presenceTimer) clearInterval(presenceTimer);
|
|
873
|
+
watcher?.close();
|
|
874
|
+
unsubscribeBoardEvents?.();
|
|
875
|
+
bindings.delete(context);
|
|
876
|
+
if (attachedProjectRoot && registeredSessionId) {
|
|
877
|
+
releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
878
|
+
fireAndForget(
|
|
879
|
+
"cleanup-board",
|
|
880
|
+
cleanupSessionKanbanBoard(attachedProjectRoot, registeredSessionId)
|
|
881
|
+
);
|
|
882
|
+
registeredSessionId = "";
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
bindings.set(context, detach);
|
|
886
|
+
return detach;
|
|
887
|
+
}
|
|
888
|
+
async function rebindSessionKanbanTask(context) {
|
|
889
|
+
const sessionId = context.session?.id;
|
|
890
|
+
if (!sessionId || !context.projectRoot) return null;
|
|
891
|
+
if (context.currentKanbanTaskId) return null;
|
|
892
|
+
let best;
|
|
893
|
+
try {
|
|
894
|
+
const snapshot = await getKanbanOrchestrationSnapshot(context.projectRoot);
|
|
895
|
+
const nowMs = Date.now();
|
|
896
|
+
for (const result of snapshot.running) {
|
|
897
|
+
const assignment = result.task.assignment;
|
|
898
|
+
if (assignment?.status !== "running") continue;
|
|
899
|
+
const expiresAt = assignment.leaseExpiresAt ? Date.parse(assignment.leaseExpiresAt) : Number.NaN;
|
|
900
|
+
if (Number.isFinite(expiresAt) && expiresAt <= nowMs) continue;
|
|
901
|
+
const entry = result.board.presence?.find(
|
|
902
|
+
(candidate) => candidate.sessionId === sessionId && candidate.taskId === result.task.id
|
|
903
|
+
);
|
|
904
|
+
if (!entry) continue;
|
|
905
|
+
if (!best || entry.lastSeenAt > best.lastSeenAt) {
|
|
906
|
+
best = { boardId: result.board.id, taskId: result.task.id, lastSeenAt: entry.lastSeenAt };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
if (!best) return null;
|
|
913
|
+
context.setCurrentKanbanTask(best.taskId, best.boardId);
|
|
914
|
+
return { boardId: best.boardId, taskId: best.taskId };
|
|
915
|
+
}
|
|
916
|
+
var degradationReason;
|
|
917
|
+
function sessionKanbanDegradation() {
|
|
918
|
+
return degradationReason;
|
|
919
|
+
}
|
|
920
|
+
async function hydrateSessionKanban(context) {
|
|
921
|
+
const id = context.session?.id ?? "";
|
|
922
|
+
if (!id) return null;
|
|
923
|
+
try {
|
|
924
|
+
const board = await hydrateSessionKanbanBoard(context, id);
|
|
925
|
+
degradationReason = void 0;
|
|
926
|
+
return board;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
degradationReason = error instanceof Error ? error.message : String(error);
|
|
929
|
+
fireAndForget("hydrate", Promise.reject(error));
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function hydrateSessionKanbanBoard(context, id) {
|
|
934
|
+
await rebindSessionKanbanTask(context);
|
|
935
|
+
await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
|
|
936
|
+
if (context.projectRoot) {
|
|
937
|
+
fireAndForget("prune-session-boards", pruneSessionBoards(context.projectRoot));
|
|
938
|
+
}
|
|
939
|
+
let board = await ensureSessionKanbanBoard(context.projectRoot, id);
|
|
940
|
+
if (context.todos.length) {
|
|
941
|
+
board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
|
|
942
|
+
}
|
|
943
|
+
const planPath = context.meta["plan.path"];
|
|
944
|
+
if (typeof planPath === "string" && planPath) {
|
|
945
|
+
const plan = await loadPlan(planPath);
|
|
946
|
+
if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
|
|
947
|
+
}
|
|
948
|
+
const taskPath = context.meta["task.path"];
|
|
949
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
950
|
+
const tasks = await loadTasks(taskPath);
|
|
951
|
+
if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
|
|
952
|
+
}
|
|
953
|
+
return board;
|
|
954
|
+
}
|
|
955
|
+
function applySessionKanbanBoardToTodos2(context, board) {
|
|
956
|
+
return applySessionKanbanBoardToTodos(context, board, {
|
|
957
|
+
sessionIdFromTags,
|
|
958
|
+
isOwnedSessionBoard,
|
|
959
|
+
hasInFlightTodoMirror,
|
|
960
|
+
suppressedTodoMirrors
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
function applyManagedKanbanBoardToTodos2(context, board) {
|
|
964
|
+
return applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors, {
|
|
965
|
+
sessionOwnerFromTags: sessionIdFromTags
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
function applySessionKanbanTaskToSource2(context, task, options = {}) {
|
|
969
|
+
return applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors, options);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
export {
|
|
973
|
+
PLAN_STATUS_TO_TASK,
|
|
974
|
+
todoListToSerializedGraph,
|
|
975
|
+
taskFileToSerializedGraph,
|
|
976
|
+
planFileToSerializedGraph,
|
|
977
|
+
blockingTitles,
|
|
978
|
+
orderTasksForTodos,
|
|
979
|
+
todosNeedingSessionMirror,
|
|
980
|
+
SESSION_KANBAN_COLUMNS,
|
|
981
|
+
ensureSessionKanbanBoard,
|
|
982
|
+
cleanupSessionKanbanBoard,
|
|
983
|
+
cleanupSessionKanbanBoardIfEmpty,
|
|
984
|
+
cleanupEmptySessionKanbanBoards,
|
|
985
|
+
takeSessionMirrorFailure,
|
|
986
|
+
projectSessionTodosToKanban,
|
|
987
|
+
projectSessionTasksToKanban,
|
|
988
|
+
projectSessionPlanToKanban,
|
|
989
|
+
mirrorSessionTodosToKanban,
|
|
990
|
+
mirrorSessionTasksToKanban,
|
|
991
|
+
mirrorSessionPlanToKanban,
|
|
992
|
+
attachSessionKanbanMirror,
|
|
993
|
+
rebindSessionKanbanTask,
|
|
994
|
+
sessionKanbanDegradation,
|
|
995
|
+
hydrateSessionKanban,
|
|
996
|
+
applySessionKanbanBoardToTodos2 as applySessionKanbanBoardToTodos,
|
|
997
|
+
applyManagedKanbanBoardToTodos2 as applyManagedKanbanBoardToTodos,
|
|
998
|
+
applySessionKanbanTaskToSource2 as applySessionKanbanTaskToSource
|
|
999
|
+
};
|
|
1000
|
+
//# sourceMappingURL=chunk-QNJ7AQ4B.js.map
|