@wrongstack/tools 0.306.3 → 0.307.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/audit.js +50 -9
- package/dist/auto-proceed-loop-guard.js +8 -2
- package/dist/bash.js +57 -22
- package/dist/builtin.d.ts +6 -6
- package/dist/builtin.js +11315 -9317
- package/dist/clarify.d.ts +32 -0
- package/dist/codebase-index/ast-invariant-engine.d.ts +102 -0
- package/dist/codebase-index/ast-symbol-mutator.d.ts +28 -0
- package/dist/codebase-index/background-indexer.d.ts +9 -0
- package/dist/codebase-index/codebase-ast-replace-tool.d.ts +31 -0
- package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +40 -0
- package/dist/codebase-index/codebase-invariant-check-tool.d.ts +23 -0
- package/dist/codebase-index/codebase-repo-map-tool.d.ts +22 -0
- package/dist/codebase-index/codebase-skeleton-tool.d.ts +36 -0
- package/dist/codebase-index/codebase-targeted-test-tool.d.ts +30 -0
- package/dist/codebase-index/index.d.ts +11 -1
- package/dist/codebase-index/index.js +4141 -1479
- package/dist/codebase-index/project-server-client-state.d.ts +79 -0
- package/dist/codebase-index/project-server-client.d.ts +3 -71
- package/dist/codebase-index/project-server-protocol.d.ts +1 -0
- package/dist/codebase-index/project-server.js +751 -909
- package/dist/codebase-index/repo-map.d.ts +20 -0
- package/dist/codebase-index/skeleton-extractor.d.ts +64 -0
- package/dist/codebase-index/tree-sitter-parser.d.ts +11 -0
- package/dist/codebase-index/worker.js +723 -891
- package/dist/codebase-index/writer-mutations.d.ts +26 -0
- package/dist/codebase-index/writer-refs.d.ts +50 -0
- package/dist/codebase-index/writer-search.d.ts +30 -0
- package/dist/codebase-index/writer.d.ts +2 -320
- package/dist/diff.js +3 -2
- package/dist/document.js +37 -2
- package/dist/e2e.js +3 -2
- package/dist/edit.js +8558 -348
- package/dist/exec.js +76 -22
- package/dist/fetch.js +11 -9
- package/dist/format.js +50 -9
- package/dist/glob.js +8 -3
- package/dist/grep.js +22 -10
- package/dist/index.d.ts +3 -1
- package/dist/index.js +11411 -9440
- package/dist/install.js +50 -9
- package/dist/json.js +91 -17
- package/dist/kanban-board-actions.d.ts +4 -0
- package/dist/kanban-lifecycle-actions.d.ts +4 -0
- package/dist/kanban-serializer.d.ts +21 -0
- package/dist/kanban.js +985 -1034
- package/dist/languages/index.js +17 -10
- package/dist/lint.js +50 -9
- package/dist/logs.js +17 -6
- package/dist/next-steps.d.ts +8 -0
- package/dist/next-steps.js +18 -0
- package/dist/outdated.js +18 -11
- package/dist/pack.js +11308 -9317
- package/dist/patch.js +8301 -77
- package/dist/plan.js +1231 -1281
- package/dist/process-registry.js +14 -8
- package/dist/ps-slash.js +65 -30
- package/dist/read.js +754 -912
- package/dist/replace.js +8456 -195
- package/dist/scaffold.js +36 -1
- package/dist/search.js +21 -15
- package/dist/security-ast-scan-tool.d.ts +43 -0
- package/dist/session-kanban-graph.d.ts +9 -0
- package/dist/session-kanban-sync.d.ts +31 -0
- package/dist/session-kanban.d.ts +5 -141
- package/dist/session-kanban.js +367 -360
- package/dist/task.js +1157 -1207
- package/dist/test.js +50 -9
- package/dist/todo.js +2187 -2237
- package/dist/tool-diff.js +6 -1
- package/dist/tool-summary.js +4 -2
- package/dist/tool-tier.js +11315 -9317
- package/dist/tree.js +36 -1
- package/dist/typecheck.js +51 -10
- package/dist/write.js +8374 -152
- package/package.json +7 -7
package/dist/session-kanban.js
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
// src/session-kanban.ts
|
|
2
2
|
import { watch } from "node:fs";
|
|
3
3
|
import { basename, dirname } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
loadPlan,
|
|
7
|
-
loadTasks,
|
|
8
|
-
mutatePlan,
|
|
9
|
-
mutateTasks
|
|
10
|
-
} from "@wrongstack/core/storage";
|
|
4
|
+
import { loadPlan, loadTasks } from "@wrongstack/core/storage";
|
|
11
5
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
12
|
-
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
13
6
|
import {
|
|
14
7
|
bridgeKanbanSupervisor,
|
|
15
8
|
compactSessionMirrorBoard,
|
|
16
9
|
createBoard,
|
|
17
10
|
DEFAULT_COLUMNS,
|
|
18
11
|
getBoard,
|
|
19
|
-
getDependencyReadinessIssues,
|
|
20
12
|
getKanbanOrchestrationSnapshot,
|
|
21
13
|
listBoards,
|
|
22
14
|
pruneSessionBoards,
|
|
@@ -25,6 +17,350 @@ import {
|
|
|
25
17
|
touchKanbanPresence,
|
|
26
18
|
updateBoard
|
|
27
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: "*",
|
|
214
|
+
type: "status",
|
|
215
|
+
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
kind: "kanban.todos.updated",
|
|
218
|
+
sessionId,
|
|
219
|
+
revision: context.state.revision,
|
|
220
|
+
todoCount: todos.length,
|
|
221
|
+
statusCounts
|
|
222
|
+
}),
|
|
223
|
+
priority: "normal",
|
|
224
|
+
senderSessionId: sessionId
|
|
225
|
+
}).catch(() => {
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function notifyTodoUpdate(context, todos) {
|
|
229
|
+
const summary = formatTodosForModel(todos);
|
|
230
|
+
const text = `[KANBAN TODO UPDATE]
|
|
231
|
+
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
232
|
+
${summary}
|
|
233
|
+
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.`;
|
|
234
|
+
const state = context.state;
|
|
235
|
+
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
236
|
+
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
237
|
+
}
|
|
238
|
+
if (typeof state.appendMessage === "function") {
|
|
239
|
+
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function todosNeedingSessionMirror(todos, activeBoardId) {
|
|
243
|
+
if (!activeBoardId) return todos;
|
|
244
|
+
return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId || !todo.kanbanTaskId);
|
|
245
|
+
}
|
|
246
|
+
function applySessionKanbanBoardToTodos(context, board, options) {
|
|
247
|
+
const sessionId = context.session?.id ?? "";
|
|
248
|
+
if (!sessionId || options.sessionIdFromTags(board.tags) !== sessionId || !options.isOwnedSessionBoard(board.tags)) {
|
|
249
|
+
return [...context.todos];
|
|
250
|
+
}
|
|
251
|
+
if (options.hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
|
|
252
|
+
const projectedTodos = orderTasksForTodos(
|
|
253
|
+
board,
|
|
254
|
+
board.tasks.filter(
|
|
255
|
+
(task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
|
|
256
|
+
)
|
|
257
|
+
).map((task) => sessionTodoFromTask(task, board));
|
|
258
|
+
const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
|
|
259
|
+
const effectiveTodos = allCompleted ? [] : projectedTodos;
|
|
260
|
+
if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
|
|
261
|
+
options.suppressedTodoMirrors.add(context);
|
|
262
|
+
try {
|
|
263
|
+
context.state.replaceTodos(projectedTodos);
|
|
264
|
+
} finally {
|
|
265
|
+
options.suppressedTodoMirrors.delete(context);
|
|
266
|
+
}
|
|
267
|
+
notifyTodoUpdate(context, context.todos);
|
|
268
|
+
broadcastTodoUpdate(context, context.todos);
|
|
269
|
+
return [...context.todos];
|
|
270
|
+
}
|
|
271
|
+
function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2) {
|
|
272
|
+
const metaKanban = context.meta["kanban"];
|
|
273
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
274
|
+
const activeBoardId = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
275
|
+
if (!activeBoardId || board.id !== activeBoardId || board.lifecycle?.mode !== "managed") {
|
|
276
|
+
return [...context.todos];
|
|
277
|
+
}
|
|
278
|
+
const projectedTodos = orderTasksForTodos(
|
|
279
|
+
board,
|
|
280
|
+
board.tasks.filter(
|
|
281
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
282
|
+
)
|
|
283
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
284
|
+
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
285
|
+
suppressedTodoMirrors2.add(context);
|
|
286
|
+
try {
|
|
287
|
+
context.state.replaceTodos(projectedTodos);
|
|
288
|
+
} finally {
|
|
289
|
+
suppressedTodoMirrors2.delete(context);
|
|
290
|
+
}
|
|
291
|
+
notifyTodoUpdate(context, context.todos);
|
|
292
|
+
broadcastTodoUpdate(context, context.todos);
|
|
293
|
+
return [...context.todos];
|
|
294
|
+
}
|
|
295
|
+
async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
|
|
296
|
+
const originId = task.origin?.taskId;
|
|
297
|
+
const graphId = task.origin?.graphId ?? "";
|
|
298
|
+
if (!originId) return { source: null };
|
|
299
|
+
if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
|
|
300
|
+
const mappedStatus = todoStatus(task);
|
|
301
|
+
const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
|
|
302
|
+
(todo) => todo.id === originId ? {
|
|
303
|
+
...todo,
|
|
304
|
+
content: task.title,
|
|
305
|
+
status: mappedStatus
|
|
306
|
+
} : todo
|
|
307
|
+
);
|
|
308
|
+
suppressedTodoMirrors2.add(context);
|
|
309
|
+
try {
|
|
310
|
+
context.state.replaceTodos(next);
|
|
311
|
+
} finally {
|
|
312
|
+
suppressedTodoMirrors2.delete(context);
|
|
313
|
+
}
|
|
314
|
+
return { source: "todo", todos: [...context.todos] };
|
|
315
|
+
}
|
|
316
|
+
const id = context.session?.id ?? "";
|
|
317
|
+
if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
|
|
318
|
+
const planPath = context.meta["plan.path"];
|
|
319
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
"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."
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const plan = await mutatePlan(planPath, id, (file) => ({
|
|
325
|
+
...file,
|
|
326
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
327
|
+
items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
|
|
328
|
+
(item) => item.id === originId ? {
|
|
329
|
+
...item,
|
|
330
|
+
title: task.title,
|
|
331
|
+
details: task.description,
|
|
332
|
+
status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
|
|
333
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
334
|
+
} : item
|
|
335
|
+
)
|
|
336
|
+
}));
|
|
337
|
+
return { source: "plan", plan };
|
|
338
|
+
}
|
|
339
|
+
if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
|
|
340
|
+
const taskPath = context.meta["task.path"];
|
|
341
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"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."
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const tasks = await mutateTasks(taskPath, id, (file) => ({
|
|
347
|
+
...file,
|
|
348
|
+
tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
|
|
349
|
+
(item) => item.id === originId ? {
|
|
350
|
+
...item,
|
|
351
|
+
title: task.title,
|
|
352
|
+
description: task.description,
|
|
353
|
+
status: sourceStatus(task),
|
|
354
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
355
|
+
} : item
|
|
356
|
+
)
|
|
357
|
+
}));
|
|
358
|
+
return { source: "task", tasks };
|
|
359
|
+
}
|
|
360
|
+
return { source: null };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/session-kanban.ts
|
|
28
364
|
var SESSION_BOARD_TAG = "session-work";
|
|
29
365
|
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
30
366
|
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
@@ -223,15 +559,6 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
223
559
|
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
224
560
|
archiveMissingTasks: true,
|
|
225
561
|
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
562
|
allowRequirementScopeShrink: true
|
|
236
563
|
}
|
|
237
564
|
);
|
|
@@ -323,106 +650,12 @@ function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
|
323
650
|
const failure = mirrorFailures.get(key);
|
|
324
651
|
if (!failure) return void 0;
|
|
325
652
|
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
|
-
}
|
|
333
|
-
function todoListToSerializedGraph(todos, sessionId) {
|
|
334
|
-
const graphId = `todo:${sessionId}`;
|
|
335
|
-
const nodes = todos.map((todo, index) => ({
|
|
336
|
-
id: todo.id,
|
|
337
|
-
title: todo.content,
|
|
338
|
-
description: todo.activeForm ?? "",
|
|
339
|
-
type: "chore",
|
|
340
|
-
priority: "medium",
|
|
341
|
-
status: todo.status,
|
|
342
|
-
specRequirementId: `${graphId}:${todo.id}`,
|
|
343
|
-
createdAt: index,
|
|
344
|
-
updatedAt: index
|
|
345
|
-
}));
|
|
346
|
-
return {
|
|
347
|
-
id: graphId,
|
|
348
|
-
specId: graphId,
|
|
349
|
-
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
350
|
-
title: "Session todos",
|
|
351
|
-
nodes,
|
|
352
|
-
edges: [],
|
|
353
|
-
rootNodes: nodes.map((node) => node.id),
|
|
354
|
-
createdAt: 0,
|
|
355
|
-
updatedAt: 0
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
359
|
-
const graphId = `session:${sessionId}`;
|
|
360
|
-
const ids = new Set(tasks.map((task) => task.id));
|
|
361
|
-
const nodes = tasks.map((task, index) => ({
|
|
362
|
-
id: task.id,
|
|
363
|
-
title: task.title,
|
|
364
|
-
description: task.description ?? "",
|
|
365
|
-
type: task.type,
|
|
366
|
-
priority: task.priority,
|
|
367
|
-
status: task.status,
|
|
368
|
-
specRequirementId: `${graphId}:${task.id}`,
|
|
369
|
-
...task.assignee ? { assignee: task.assignee } : {},
|
|
370
|
-
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
371
|
-
createdAt: index,
|
|
372
|
-
updatedAt: index
|
|
373
|
-
}));
|
|
374
|
-
const edges = tasks.flatMap(
|
|
375
|
-
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
376
|
-
id: `${dependency}->${task.id}`,
|
|
377
|
-
from: dependency,
|
|
378
|
-
to: task.id,
|
|
379
|
-
type: "depends_on"
|
|
380
|
-
}))
|
|
381
|
-
);
|
|
382
|
-
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
383
|
-
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
384
|
-
return {
|
|
385
|
-
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
386
|
-
id: graphId,
|
|
387
|
-
specId: graphId,
|
|
388
|
-
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
389
|
-
title: "Session tasks",
|
|
390
|
-
nodes,
|
|
391
|
-
edges,
|
|
392
|
-
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
393
|
-
createdAt: 0,
|
|
394
|
-
updatedAt: 0
|
|
395
|
-
};
|
|
653
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
396
654
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
};
|
|
402
|
-
function planFileToSerializedGraph(items, sessionId) {
|
|
403
|
-
const graphId = `plan:${sessionId}`;
|
|
404
|
-
const nodes = items.map((item, index) => ({
|
|
405
|
-
id: item.id,
|
|
406
|
-
title: item.title,
|
|
407
|
-
description: item.details ?? "",
|
|
408
|
-
type: "chore",
|
|
409
|
-
priority: "medium",
|
|
410
|
-
status: PLAN_STATUS_TO_TASK[item.status],
|
|
411
|
-
specRequirementId: `${graphId}:${item.id}`,
|
|
412
|
-
createdAt: index,
|
|
413
|
-
updatedAt: index
|
|
414
|
-
}));
|
|
415
|
-
return {
|
|
416
|
-
id: graphId,
|
|
417
|
-
specId: graphId,
|
|
418
|
-
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
419
|
-
title: "Session plan",
|
|
420
|
-
nodes,
|
|
421
|
-
edges: [],
|
|
422
|
-
rootNodes: nodes.map((node) => node.id),
|
|
423
|
-
createdAt: 0,
|
|
424
|
-
updatedAt: 0
|
|
425
|
-
};
|
|
655
|
+
function hasInFlightTodoMirror(projectRoot, sessionId) {
|
|
656
|
+
if (!projectRoot || !sessionId) return false;
|
|
657
|
+
const key = mirrorKey(projectRoot, sessionId, "session-todo");
|
|
658
|
+
return pendingMirrors.has(key) || activeMirrors.has(key);
|
|
426
659
|
}
|
|
427
660
|
function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
428
661
|
return projectGraph(
|
|
@@ -462,52 +695,6 @@ function fireAndForget(context, work) {
|
|
|
462
695
|
);
|
|
463
696
|
});
|
|
464
697
|
}
|
|
465
|
-
function broadcastTodoUpdate(context, todos) {
|
|
466
|
-
const sessionId = context.session?.id ?? "";
|
|
467
|
-
if (!context.agentId || !sessionId) return;
|
|
468
|
-
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
469
|
-
for (const todo of todos) {
|
|
470
|
-
if (todo.status === "completed") statusCounts.completed++;
|
|
471
|
-
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
472
|
-
else statusCounts.pending++;
|
|
473
|
-
}
|
|
474
|
-
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
475
|
-
const mailbox = getSharedProjectMailbox(projectDir);
|
|
476
|
-
void mailbox.send({
|
|
477
|
-
from: context.agentId,
|
|
478
|
-
to: "*",
|
|
479
|
-
type: "status",
|
|
480
|
-
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
481
|
-
body: JSON.stringify({
|
|
482
|
-
kind: "kanban.todos.updated",
|
|
483
|
-
sessionId,
|
|
484
|
-
revision: context.state.revision,
|
|
485
|
-
todoCount: todos.length,
|
|
486
|
-
statusCounts
|
|
487
|
-
}),
|
|
488
|
-
priority: "normal",
|
|
489
|
-
senderSessionId: sessionId
|
|
490
|
-
}).catch(() => {
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
function notifyTodoUpdate(context, todos) {
|
|
494
|
-
const summary = formatTodosForModel(todos);
|
|
495
|
-
const text = `[KANBAN TODO UPDATE]
|
|
496
|
-
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
497
|
-
${summary}
|
|
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.`;
|
|
499
|
-
const state = context.state;
|
|
500
|
-
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
501
|
-
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
502
|
-
}
|
|
503
|
-
if (typeof state.appendMessage === "function") {
|
|
504
|
-
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
function todosNeedingSessionMirror(todos, activeBoardId) {
|
|
508
|
-
if (!activeBoardId) return todos;
|
|
509
|
-
return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId || !todo.kanbanTaskId);
|
|
510
|
-
}
|
|
511
698
|
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
512
699
|
queueLatestMirror(
|
|
513
700
|
projectRoot,
|
|
@@ -585,8 +772,8 @@ function attachSessionKanbanMirror(context) {
|
|
|
585
772
|
if (!watchedBoardId) return;
|
|
586
773
|
const board = await getBoard(context.projectRoot, watchedBoardId);
|
|
587
774
|
if (!board) return;
|
|
588
|
-
if (board.lifecycle?.mode === "managed")
|
|
589
|
-
else
|
|
775
|
+
if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
|
|
776
|
+
else applySessionKanbanBoardToTodos2(context, board);
|
|
590
777
|
};
|
|
591
778
|
const configureBoardWatcher = async () => {
|
|
592
779
|
const id = sessionId();
|
|
@@ -620,7 +807,7 @@ function attachSessionKanbanMirror(context) {
|
|
|
620
807
|
if (presenceTimer) clearInterval(presenceTimer);
|
|
621
808
|
presenceTimer = setInterval(() => fireAndForget("touch-presence", touchPresence()), 6e4);
|
|
622
809
|
presenceTimer.unref?.();
|
|
623
|
-
if (board.lifecycle?.mode === "managed")
|
|
810
|
+
if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
|
|
624
811
|
} catch {
|
|
625
812
|
unsubscribeBoardEvents = null;
|
|
626
813
|
watchedBoardId = "";
|
|
@@ -759,206 +946,26 @@ async function hydrateSessionKanbanBoard(context, id) {
|
|
|
759
946
|
}
|
|
760
947
|
return board;
|
|
761
948
|
}
|
|
762
|
-
function
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return "pending";
|
|
769
|
-
}
|
|
770
|
-
function todoStatus(task) {
|
|
771
|
-
const status = sourceStatus(task);
|
|
772
|
-
if (status === "completed") return "completed";
|
|
773
|
-
if (status === "review" && task.assignment?.status === "completed") return "completed";
|
|
774
|
-
if (status === "in_progress" || status === "review") return "in_progress";
|
|
775
|
-
return "pending";
|
|
776
|
-
}
|
|
777
|
-
function sessionTodoFromTask(task, board) {
|
|
778
|
-
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
779
|
-
return {
|
|
780
|
-
id: task.origin?.taskId ?? task.id,
|
|
781
|
-
content: task.title,
|
|
782
|
-
status: todoStatus(task),
|
|
783
|
-
...task.description ? { activeForm: task.description } : {},
|
|
784
|
-
...blockedBy.length ? { blockedBy } : {}
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
function managedTodoFromTask(task, board) {
|
|
788
|
-
return {
|
|
789
|
-
...sessionTodoFromTask(task, board),
|
|
790
|
-
kanbanBoardId: board.id,
|
|
791
|
-
kanbanTaskId: task.id
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
function blockingTitles(board, task) {
|
|
795
|
-
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
796
|
-
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
797
|
-
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
798
|
-
return dependency.title;
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
var PRIORITY_ORDER = {
|
|
802
|
-
critical: 0,
|
|
803
|
-
high: 1,
|
|
804
|
-
medium: 2,
|
|
805
|
-
low: 3
|
|
806
|
-
};
|
|
807
|
-
function orderTasksForTodos(board, tasks) {
|
|
808
|
-
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
809
|
-
const baseline = [...tasks].sort(
|
|
810
|
-
(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)
|
|
811
|
-
);
|
|
812
|
-
const included = new Set(baseline.map((task) => task.id));
|
|
813
|
-
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
814
|
-
const emitted = [];
|
|
815
|
-
const done = /* @__PURE__ */ new Set();
|
|
816
|
-
while (remaining.size > 0) {
|
|
817
|
-
const ready = baseline.filter(
|
|
818
|
-
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
819
|
-
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
820
|
-
)
|
|
821
|
-
);
|
|
822
|
-
if (ready.length === 0) break;
|
|
823
|
-
for (const task of ready) {
|
|
824
|
-
remaining.delete(task.id);
|
|
825
|
-
done.add(task.id);
|
|
826
|
-
emitted.push(task);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
830
|
-
return emitted;
|
|
831
|
-
}
|
|
832
|
-
function sameTodos(left, right) {
|
|
833
|
-
return left.length === right.length && left.every((todo, index) => {
|
|
834
|
-
const candidate = right[index];
|
|
835
|
-
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,
|
|
836
|
-
// the rows are otherwise identical and the unblocking would never
|
|
837
|
-
// reach the model.
|
|
838
|
-
(candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
949
|
+
function applySessionKanbanBoardToTodos2(context, board) {
|
|
950
|
+
return applySessionKanbanBoardToTodos(context, board, {
|
|
951
|
+
sessionIdFromTags,
|
|
952
|
+
isOwnedSessionBoard,
|
|
953
|
+
hasInFlightTodoMirror,
|
|
954
|
+
suppressedTodoMirrors
|
|
839
955
|
});
|
|
840
956
|
}
|
|
841
|
-
function
|
|
842
|
-
|
|
843
|
-
if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
|
|
844
|
-
return [...context.todos];
|
|
845
|
-
}
|
|
846
|
-
if (hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
|
|
847
|
-
const projectedTodos = orderTasksForTodos(
|
|
848
|
-
board,
|
|
849
|
-
board.tasks.filter(
|
|
850
|
-
(task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
|
|
851
|
-
)
|
|
852
|
-
).map((task) => sessionTodoFromTask(task, board));
|
|
853
|
-
const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
|
|
854
|
-
const effectiveTodos = allCompleted ? [] : projectedTodos;
|
|
855
|
-
if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
|
|
856
|
-
suppressedTodoMirrors.add(context);
|
|
857
|
-
try {
|
|
858
|
-
context.state.replaceTodos(projectedTodos);
|
|
859
|
-
} finally {
|
|
860
|
-
suppressedTodoMirrors.delete(context);
|
|
861
|
-
}
|
|
862
|
-
notifyTodoUpdate(context, context.todos);
|
|
863
|
-
broadcastTodoUpdate(context, context.todos);
|
|
864
|
-
return [...context.todos];
|
|
865
|
-
}
|
|
866
|
-
function applyManagedKanbanBoardToTodos(context, board) {
|
|
867
|
-
const metaKanban = context.meta["kanban"];
|
|
868
|
-
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
869
|
-
const activeBoardId = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
870
|
-
if (!activeBoardId || board.id !== activeBoardId || board.lifecycle?.mode !== "managed") {
|
|
871
|
-
return [...context.todos];
|
|
872
|
-
}
|
|
873
|
-
const projectedTodos = orderTasksForTodos(
|
|
874
|
-
board,
|
|
875
|
-
board.tasks.filter(
|
|
876
|
-
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
877
|
-
)
|
|
878
|
-
).map((task) => managedTodoFromTask(task, board));
|
|
879
|
-
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
880
|
-
suppressedTodoMirrors.add(context);
|
|
881
|
-
try {
|
|
882
|
-
context.state.replaceTodos(projectedTodos);
|
|
883
|
-
} finally {
|
|
884
|
-
suppressedTodoMirrors.delete(context);
|
|
885
|
-
}
|
|
886
|
-
notifyTodoUpdate(context, context.todos);
|
|
887
|
-
broadcastTodoUpdate(context, context.todos);
|
|
888
|
-
return [...context.todos];
|
|
957
|
+
function applyManagedKanbanBoardToTodos2(context, board) {
|
|
958
|
+
return applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors);
|
|
889
959
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const graphId = task.origin?.graphId ?? "";
|
|
893
|
-
if (!originId) return { source: null };
|
|
894
|
-
if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
|
|
895
|
-
const mappedStatus = todoStatus(task);
|
|
896
|
-
const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
|
|
897
|
-
(todo) => todo.id === originId ? {
|
|
898
|
-
...todo,
|
|
899
|
-
content: task.title,
|
|
900
|
-
status: mappedStatus
|
|
901
|
-
} : todo
|
|
902
|
-
);
|
|
903
|
-
suppressedTodoMirrors.add(context);
|
|
904
|
-
try {
|
|
905
|
-
context.state.replaceTodos(next);
|
|
906
|
-
} finally {
|
|
907
|
-
suppressedTodoMirrors.delete(context);
|
|
908
|
-
}
|
|
909
|
-
return { source: "todo", todos: [...context.todos] };
|
|
910
|
-
}
|
|
911
|
-
const id = context.session?.id ?? "";
|
|
912
|
-
if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
|
|
913
|
-
const planPath = context.meta["plan.path"];
|
|
914
|
-
if (typeof planPath !== "string" || !planPath) {
|
|
915
|
-
throw new Error(
|
|
916
|
-
"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."
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
const plan = await mutatePlan(planPath, id, (file) => ({
|
|
920
|
-
...file,
|
|
921
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
922
|
-
items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
|
|
923
|
-
(item) => item.id === originId ? {
|
|
924
|
-
...item,
|
|
925
|
-
title: task.title,
|
|
926
|
-
details: task.description,
|
|
927
|
-
status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
|
|
928
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
929
|
-
} : item
|
|
930
|
-
)
|
|
931
|
-
}));
|
|
932
|
-
return { source: "plan", plan };
|
|
933
|
-
}
|
|
934
|
-
if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
|
|
935
|
-
const taskPath = context.meta["task.path"];
|
|
936
|
-
if (typeof taskPath !== "string" || !taskPath) {
|
|
937
|
-
throw new Error(
|
|
938
|
-
"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."
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
const tasks = await mutateTasks(taskPath, id, (file) => ({
|
|
942
|
-
...file,
|
|
943
|
-
tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
|
|
944
|
-
(item) => item.id === originId ? {
|
|
945
|
-
...item,
|
|
946
|
-
title: task.title,
|
|
947
|
-
description: task.description,
|
|
948
|
-
status: sourceStatus(task),
|
|
949
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
950
|
-
} : item
|
|
951
|
-
)
|
|
952
|
-
}));
|
|
953
|
-
return { source: "task", tasks };
|
|
954
|
-
}
|
|
955
|
-
return { source: null };
|
|
960
|
+
function applySessionKanbanTaskToSource2(context, task, options = {}) {
|
|
961
|
+
return applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors, options);
|
|
956
962
|
}
|
|
957
963
|
export {
|
|
964
|
+
PLAN_STATUS_TO_TASK,
|
|
958
965
|
SESSION_KANBAN_COLUMNS,
|
|
959
|
-
applyManagedKanbanBoardToTodos,
|
|
960
|
-
applySessionKanbanBoardToTodos,
|
|
961
|
-
applySessionKanbanTaskToSource,
|
|
966
|
+
applyManagedKanbanBoardToTodos2 as applyManagedKanbanBoardToTodos,
|
|
967
|
+
applySessionKanbanBoardToTodos2 as applySessionKanbanBoardToTodos,
|
|
968
|
+
applySessionKanbanTaskToSource2 as applySessionKanbanTaskToSource,
|
|
962
969
|
attachSessionKanbanMirror,
|
|
963
970
|
blockingTitles,
|
|
964
971
|
cleanupEmptySessionKanbanBoards,
|