@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/plan.js
CHANGED
|
@@ -14,22 +14,14 @@ import {
|
|
|
14
14
|
import { formatTaskList } from "@wrongstack/core/utils";
|
|
15
15
|
|
|
16
16
|
// src/session-kanban.ts
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
loadPlan,
|
|
20
|
-
loadTasks,
|
|
21
|
-
mutatePlan,
|
|
22
|
-
mutateTasks
|
|
23
|
-
} from "@wrongstack/core/storage";
|
|
17
|
+
import { loadPlan, loadTasks } from "@wrongstack/core/storage";
|
|
24
18
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
25
|
-
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
26
19
|
import {
|
|
27
20
|
bridgeKanbanSupervisor,
|
|
28
21
|
compactSessionMirrorBoard,
|
|
29
22
|
createBoard,
|
|
30
23
|
DEFAULT_COLUMNS,
|
|
31
24
|
getBoard,
|
|
32
|
-
getDependencyReadinessIssues,
|
|
33
25
|
getKanbanOrchestrationSnapshot,
|
|
34
26
|
listBoards,
|
|
35
27
|
pruneSessionBoards,
|
|
@@ -38,6 +30,254 @@ import {
|
|
|
38
30
|
touchKanbanPresence,
|
|
39
31
|
updateBoard
|
|
40
32
|
} from "@wrongstack/kanban";
|
|
33
|
+
|
|
34
|
+
// src/session-kanban-graph.ts
|
|
35
|
+
var PLAN_STATUS_TO_TASK = {
|
|
36
|
+
open: "pending",
|
|
37
|
+
in_progress: "in_progress",
|
|
38
|
+
done: "completed"
|
|
39
|
+
};
|
|
40
|
+
function todoListToSerializedGraph(todos, sessionId) {
|
|
41
|
+
const graphId = `todo:${sessionId}`;
|
|
42
|
+
const nodes = todos.map((todo, index) => ({
|
|
43
|
+
id: todo.id,
|
|
44
|
+
title: todo.content,
|
|
45
|
+
description: todo.activeForm ?? "",
|
|
46
|
+
type: "chore",
|
|
47
|
+
priority: "medium",
|
|
48
|
+
status: todo.status,
|
|
49
|
+
specRequirementId: `${graphId}:${todo.id}`,
|
|
50
|
+
createdAt: index,
|
|
51
|
+
updatedAt: index
|
|
52
|
+
}));
|
|
53
|
+
return {
|
|
54
|
+
id: graphId,
|
|
55
|
+
specId: graphId,
|
|
56
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
57
|
+
title: "Session todos",
|
|
58
|
+
nodes,
|
|
59
|
+
edges: [],
|
|
60
|
+
rootNodes: nodes.map((node) => node.id),
|
|
61
|
+
createdAt: 0,
|
|
62
|
+
updatedAt: 0
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
66
|
+
const graphId = `session:${sessionId}`;
|
|
67
|
+
const ids = new Set(tasks.map((task) => task.id));
|
|
68
|
+
const nodes = tasks.map((task, index) => ({
|
|
69
|
+
id: task.id,
|
|
70
|
+
title: task.title,
|
|
71
|
+
description: task.description ?? "",
|
|
72
|
+
type: task.type,
|
|
73
|
+
priority: task.priority,
|
|
74
|
+
status: task.status,
|
|
75
|
+
specRequirementId: `${graphId}:${task.id}`,
|
|
76
|
+
...task.assignee ? { assignee: task.assignee } : {},
|
|
77
|
+
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
78
|
+
createdAt: index,
|
|
79
|
+
updatedAt: index
|
|
80
|
+
}));
|
|
81
|
+
const edges = tasks.flatMap(
|
|
82
|
+
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
83
|
+
id: `${dependency}->${task.id}`,
|
|
84
|
+
from: dependency,
|
|
85
|
+
to: task.id,
|
|
86
|
+
type: "depends_on"
|
|
87
|
+
}))
|
|
88
|
+
);
|
|
89
|
+
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
90
|
+
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
91
|
+
return {
|
|
92
|
+
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
93
|
+
id: graphId,
|
|
94
|
+
specId: graphId,
|
|
95
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
96
|
+
title: "Session tasks",
|
|
97
|
+
nodes,
|
|
98
|
+
edges,
|
|
99
|
+
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
100
|
+
createdAt: 0,
|
|
101
|
+
updatedAt: 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function planFileToSerializedGraph(items, sessionId) {
|
|
105
|
+
const graphId = `plan:${sessionId}`;
|
|
106
|
+
const nodes = items.map((item, index) => ({
|
|
107
|
+
id: item.id,
|
|
108
|
+
title: item.title,
|
|
109
|
+
description: item.details ?? "",
|
|
110
|
+
type: "chore",
|
|
111
|
+
priority: "medium",
|
|
112
|
+
status: PLAN_STATUS_TO_TASK[item.status],
|
|
113
|
+
specRequirementId: `${graphId}:${item.id}`,
|
|
114
|
+
createdAt: index,
|
|
115
|
+
updatedAt: index
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
id: graphId,
|
|
119
|
+
specId: graphId,
|
|
120
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
121
|
+
title: "Session plan",
|
|
122
|
+
nodes,
|
|
123
|
+
edges: [],
|
|
124
|
+
rootNodes: nodes.map((node) => node.id),
|
|
125
|
+
createdAt: 0,
|
|
126
|
+
updatedAt: 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/session-kanban-sync.ts
|
|
131
|
+
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
132
|
+
import { mutatePlan, mutateTasks } from "@wrongstack/core/storage";
|
|
133
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
134
|
+
import {
|
|
135
|
+
getDependencyReadinessIssues
|
|
136
|
+
} from "@wrongstack/kanban";
|
|
137
|
+
function sourceStatus(task) {
|
|
138
|
+
if (task.status === "completed") return "completed";
|
|
139
|
+
if (task.status === "in_progress") return "in_progress";
|
|
140
|
+
if (task.status === "review") return "review";
|
|
141
|
+
if (task.status === "blocked") return "blocked";
|
|
142
|
+
if (task.status === "failed") return "failed";
|
|
143
|
+
return "pending";
|
|
144
|
+
}
|
|
145
|
+
function todoStatus(task) {
|
|
146
|
+
const status = sourceStatus(task);
|
|
147
|
+
if (status === "completed") return "completed";
|
|
148
|
+
if (status === "review" && task.assignment?.status === "completed") return "completed";
|
|
149
|
+
if (status === "in_progress" || status === "review") return "in_progress";
|
|
150
|
+
return "pending";
|
|
151
|
+
}
|
|
152
|
+
function sessionTodoFromTask(task, board) {
|
|
153
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
154
|
+
return {
|
|
155
|
+
id: task.origin?.taskId ?? task.id,
|
|
156
|
+
content: task.title,
|
|
157
|
+
status: todoStatus(task),
|
|
158
|
+
...task.description ? { activeForm: task.description } : {},
|
|
159
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function managedTodoFromTask(task, board) {
|
|
163
|
+
return {
|
|
164
|
+
...sessionTodoFromTask(task, board),
|
|
165
|
+
kanbanBoardId: board.id,
|
|
166
|
+
kanbanTaskId: task.id
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function blockingTitles(board, task) {
|
|
170
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
171
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
172
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
173
|
+
return dependency.title;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
var PRIORITY_ORDER = {
|
|
177
|
+
critical: 0,
|
|
178
|
+
high: 1,
|
|
179
|
+
medium: 2,
|
|
180
|
+
low: 3
|
|
181
|
+
};
|
|
182
|
+
function orderTasksForTodos(board, tasks) {
|
|
183
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
184
|
+
const baseline = [...tasks].sort(
|
|
185
|
+
(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)
|
|
186
|
+
);
|
|
187
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
188
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
189
|
+
const emitted = [];
|
|
190
|
+
const done = /* @__PURE__ */ new Set();
|
|
191
|
+
while (remaining.size > 0) {
|
|
192
|
+
const ready = baseline.filter(
|
|
193
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
194
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
if (ready.length === 0) break;
|
|
198
|
+
for (const task of ready) {
|
|
199
|
+
remaining.delete(task.id);
|
|
200
|
+
done.add(task.id);
|
|
201
|
+
emitted.push(task);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
205
|
+
return emitted;
|
|
206
|
+
}
|
|
207
|
+
function sameTodos(left, right) {
|
|
208
|
+
return left.length === right.length && left.every((todo, index) => {
|
|
209
|
+
const candidate = right[index];
|
|
210
|
+
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");
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function broadcastTodoUpdate(context, todos) {
|
|
214
|
+
const sessionId = context.session?.id ?? "";
|
|
215
|
+
if (!context.agentId || !sessionId) return;
|
|
216
|
+
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
217
|
+
for (const todo of todos) {
|
|
218
|
+
if (todo.status === "completed") statusCounts.completed++;
|
|
219
|
+
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
220
|
+
else statusCounts.pending++;
|
|
221
|
+
}
|
|
222
|
+
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
223
|
+
const mailbox = getSharedProjectMailbox(projectDir);
|
|
224
|
+
void mailbox.send({
|
|
225
|
+
from: context.agentId,
|
|
226
|
+
to: "*",
|
|
227
|
+
type: "status",
|
|
228
|
+
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
kind: "kanban.todos.updated",
|
|
231
|
+
sessionId,
|
|
232
|
+
revision: context.state.revision,
|
|
233
|
+
todoCount: todos.length,
|
|
234
|
+
statusCounts
|
|
235
|
+
}),
|
|
236
|
+
priority: "normal",
|
|
237
|
+
senderSessionId: sessionId
|
|
238
|
+
}).catch(() => {
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function notifyTodoUpdate(context, todos) {
|
|
242
|
+
const summary = formatTodosForModel(todos);
|
|
243
|
+
const text = `[KANBAN TODO UPDATE]
|
|
244
|
+
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
245
|
+
${summary}
|
|
246
|
+
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.`;
|
|
247
|
+
const state = context.state;
|
|
248
|
+
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
249
|
+
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
250
|
+
}
|
|
251
|
+
if (typeof state.appendMessage === "function") {
|
|
252
|
+
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2) {
|
|
256
|
+
const metaKanban = context.meta["kanban"];
|
|
257
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
258
|
+
const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
259
|
+
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
260
|
+
return [...context.todos];
|
|
261
|
+
}
|
|
262
|
+
const projectedTodos = orderTasksForTodos(
|
|
263
|
+
board,
|
|
264
|
+
board.tasks.filter(
|
|
265
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
266
|
+
)
|
|
267
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
268
|
+
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
269
|
+
suppressedTodoMirrors2.add(context);
|
|
270
|
+
try {
|
|
271
|
+
context.state.replaceTodos(projectedTodos);
|
|
272
|
+
} finally {
|
|
273
|
+
suppressedTodoMirrors2.delete(context);
|
|
274
|
+
}
|
|
275
|
+
notifyTodoUpdate(context, context.todos);
|
|
276
|
+
broadcastTodoUpdate(context, context.todos);
|
|
277
|
+
return [...context.todos];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/session-kanban.ts
|
|
41
281
|
var SESSION_BOARD_TAG = "session-work";
|
|
42
282
|
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
43
283
|
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
@@ -161,15 +401,6 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
161
401
|
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
162
402
|
archiveMissingTasks: true,
|
|
163
403
|
includeCompletedTasks: true,
|
|
164
|
-
// The scope ledger stays declared and accurate, but it may not veto a
|
|
165
|
-
// projection. A session mirror reflects a tactical list that shrinks by
|
|
166
|
-
// design, and refusing the sync never protected the removed row — it
|
|
167
|
-
// froze the entire board, permanently, because the stored scope then
|
|
168
|
-
// outlived every later snapshot (`session-kanban.mirror-failed`).
|
|
169
|
-
// Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
|
|
170
|
-
// removed card on the board as `archived`, the reconciliation pass
|
|
171
|
-
// first walks vanished completed rows to Done, and the session journal
|
|
172
|
-
// remains the durable record.
|
|
173
404
|
allowRequirementScopeShrink: true
|
|
174
405
|
}
|
|
175
406
|
);
|
|
@@ -263,260 +494,24 @@ function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
|
263
494
|
mirrorFailures.delete(key);
|
|
264
495
|
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
265
496
|
}
|
|
266
|
-
function todoListToSerializedGraph(todos, sessionId) {
|
|
267
|
-
const graphId = `todo:${sessionId}`;
|
|
268
|
-
const nodes = todos.map((todo, index) => ({
|
|
269
|
-
id: todo.id,
|
|
270
|
-
title: todo.content,
|
|
271
|
-
description: todo.activeForm ?? "",
|
|
272
|
-
type: "chore",
|
|
273
|
-
priority: "medium",
|
|
274
|
-
status: todo.status,
|
|
275
|
-
specRequirementId: `${graphId}:${todo.id}`,
|
|
276
|
-
createdAt: index,
|
|
277
|
-
updatedAt: index
|
|
278
|
-
}));
|
|
279
|
-
return {
|
|
280
|
-
id: graphId,
|
|
281
|
-
specId: graphId,
|
|
282
|
-
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
283
|
-
title: "Session todos",
|
|
284
|
-
nodes,
|
|
285
|
-
edges: [],
|
|
286
|
-
rootNodes: nodes.map((node) => node.id),
|
|
287
|
-
createdAt: 0,
|
|
288
|
-
updatedAt: 0
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
292
|
-
const graphId = `session:${sessionId}`;
|
|
293
|
-
const ids = new Set(tasks.map((task) => task.id));
|
|
294
|
-
const nodes = tasks.map((task, index) => ({
|
|
295
|
-
id: task.id,
|
|
296
|
-
title: task.title,
|
|
297
|
-
description: task.description ?? "",
|
|
298
|
-
type: task.type,
|
|
299
|
-
priority: task.priority,
|
|
300
|
-
status: task.status,
|
|
301
|
-
specRequirementId: `${graphId}:${task.id}`,
|
|
302
|
-
...task.assignee ? { assignee: task.assignee } : {},
|
|
303
|
-
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
304
|
-
createdAt: index,
|
|
305
|
-
updatedAt: index
|
|
306
|
-
}));
|
|
307
|
-
const edges = tasks.flatMap(
|
|
308
|
-
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
309
|
-
id: `${dependency}->${task.id}`,
|
|
310
|
-
from: dependency,
|
|
311
|
-
to: task.id,
|
|
312
|
-
type: "depends_on"
|
|
313
|
-
}))
|
|
314
|
-
);
|
|
315
|
-
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
316
|
-
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
317
|
-
return {
|
|
318
|
-
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
319
|
-
id: graphId,
|
|
320
|
-
specId: graphId,
|
|
321
|
-
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
322
|
-
title: "Session tasks",
|
|
323
|
-
nodes,
|
|
324
|
-
edges,
|
|
325
|
-
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
326
|
-
createdAt: 0,
|
|
327
|
-
updatedAt: 0
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
var PLAN_STATUS_TO_TASK = {
|
|
331
|
-
open: "pending",
|
|
332
|
-
in_progress: "in_progress",
|
|
333
|
-
done: "completed"
|
|
334
|
-
};
|
|
335
|
-
function planFileToSerializedGraph(items, sessionId) {
|
|
336
|
-
const graphId = `plan:${sessionId}`;
|
|
337
|
-
const nodes = items.map((item, index) => ({
|
|
338
|
-
id: item.id,
|
|
339
|
-
title: item.title,
|
|
340
|
-
description: item.details ?? "",
|
|
341
|
-
type: "chore",
|
|
342
|
-
priority: "medium",
|
|
343
|
-
status: PLAN_STATUS_TO_TASK[item.status],
|
|
344
|
-
specRequirementId: `${graphId}:${item.id}`,
|
|
345
|
-
createdAt: index,
|
|
346
|
-
updatedAt: index
|
|
347
|
-
}));
|
|
348
|
-
return {
|
|
349
|
-
id: graphId,
|
|
350
|
-
specId: graphId,
|
|
351
|
-
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
352
|
-
title: "Session plan",
|
|
353
|
-
nodes,
|
|
354
|
-
edges: [],
|
|
355
|
-
rootNodes: nodes.map((node) => node.id),
|
|
356
|
-
createdAt: 0,
|
|
357
|
-
updatedAt: 0
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
497
|
function projectSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
361
498
|
return projectGraph(
|
|
362
499
|
projectRoot,
|
|
363
500
|
sessionId,
|
|
364
501
|
planFileToSerializedGraph(items, sessionId),
|
|
365
|
-
"session-plan"
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
function broadcastTodoUpdate(context, todos) {
|
|
369
|
-
const sessionId = context.session?.id ?? "";
|
|
370
|
-
if (!context.agentId || !sessionId) return;
|
|
371
|
-
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
372
|
-
for (const todo of todos) {
|
|
373
|
-
if (todo.status === "completed") statusCounts.completed++;
|
|
374
|
-
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
375
|
-
else statusCounts.pending++;
|
|
376
|
-
}
|
|
377
|
-
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
378
|
-
const mailbox = getSharedProjectMailbox(projectDir);
|
|
379
|
-
void mailbox.send({
|
|
380
|
-
from: context.agentId,
|
|
381
|
-
to: "*",
|
|
382
|
-
type: "status",
|
|
383
|
-
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
384
|
-
body: JSON.stringify({
|
|
385
|
-
kind: "kanban.todos.updated",
|
|
386
|
-
sessionId,
|
|
387
|
-
revision: context.state.revision,
|
|
388
|
-
todoCount: todos.length,
|
|
389
|
-
statusCounts
|
|
390
|
-
}),
|
|
391
|
-
priority: "normal",
|
|
392
|
-
senderSessionId: sessionId
|
|
393
|
-
}).catch(() => {
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
function notifyTodoUpdate(context, todos) {
|
|
397
|
-
const summary = formatTodosForModel(todos);
|
|
398
|
-
const text = `[KANBAN TODO UPDATE]
|
|
399
|
-
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
400
|
-
${summary}
|
|
401
|
-
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.`;
|
|
402
|
-
const state = context.state;
|
|
403
|
-
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
404
|
-
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
405
|
-
}
|
|
406
|
-
if (typeof state.appendMessage === "function") {
|
|
407
|
-
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
411
|
-
queueLatestMirror(
|
|
412
|
-
projectRoot,
|
|
413
|
-
sessionId,
|
|
414
|
-
todoListToSerializedGraph(todos, sessionId),
|
|
415
|
-
"session-todo"
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
function sourceStatus(task) {
|
|
419
|
-
if (task.status === "completed") return "completed";
|
|
420
|
-
if (task.status === "in_progress") return "in_progress";
|
|
421
|
-
if (task.status === "review") return "review";
|
|
422
|
-
if (task.status === "blocked") return "blocked";
|
|
423
|
-
if (task.status === "failed") return "failed";
|
|
424
|
-
return "pending";
|
|
425
|
-
}
|
|
426
|
-
function todoStatus(task) {
|
|
427
|
-
const status = sourceStatus(task);
|
|
428
|
-
if (status === "completed") return "completed";
|
|
429
|
-
if (status === "review" && task.assignment?.status === "completed") return "completed";
|
|
430
|
-
if (status === "in_progress" || status === "review") return "in_progress";
|
|
431
|
-
return "pending";
|
|
432
|
-
}
|
|
433
|
-
function sessionTodoFromTask(task, board) {
|
|
434
|
-
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
435
|
-
return {
|
|
436
|
-
id: task.origin?.taskId ?? task.id,
|
|
437
|
-
content: task.title,
|
|
438
|
-
status: todoStatus(task),
|
|
439
|
-
...task.description ? { activeForm: task.description } : {},
|
|
440
|
-
...blockedBy.length ? { blockedBy } : {}
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
function managedTodoFromTask(task, board) {
|
|
444
|
-
return {
|
|
445
|
-
...sessionTodoFromTask(task, board),
|
|
446
|
-
kanbanBoardId: board.id,
|
|
447
|
-
kanbanTaskId: task.id
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
function blockingTitles(board, task) {
|
|
451
|
-
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
452
|
-
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
453
|
-
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
454
|
-
return dependency.title;
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
var PRIORITY_ORDER = {
|
|
458
|
-
critical: 0,
|
|
459
|
-
high: 1,
|
|
460
|
-
medium: 2,
|
|
461
|
-
low: 3
|
|
462
|
-
};
|
|
463
|
-
function orderTasksForTodos(board, tasks) {
|
|
464
|
-
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
465
|
-
const baseline = [...tasks].sort(
|
|
466
|
-
(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)
|
|
467
|
-
);
|
|
468
|
-
const included = new Set(baseline.map((task) => task.id));
|
|
469
|
-
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
470
|
-
const emitted = [];
|
|
471
|
-
const done = /* @__PURE__ */ new Set();
|
|
472
|
-
while (remaining.size > 0) {
|
|
473
|
-
const ready = baseline.filter(
|
|
474
|
-
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
475
|
-
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
476
|
-
)
|
|
477
|
-
);
|
|
478
|
-
if (ready.length === 0) break;
|
|
479
|
-
for (const task of ready) {
|
|
480
|
-
remaining.delete(task.id);
|
|
481
|
-
done.add(task.id);
|
|
482
|
-
emitted.push(task);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
486
|
-
return emitted;
|
|
502
|
+
"session-plan"
|
|
503
|
+
);
|
|
487
504
|
}
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
});
|
|
505
|
+
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
506
|
+
queueLatestMirror(
|
|
507
|
+
projectRoot,
|
|
508
|
+
sessionId,
|
|
509
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
510
|
+
"session-todo"
|
|
511
|
+
);
|
|
496
512
|
}
|
|
497
|
-
function
|
|
498
|
-
|
|
499
|
-
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
500
|
-
const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
501
|
-
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
502
|
-
return [...context.todos];
|
|
503
|
-
}
|
|
504
|
-
const projectedTodos = orderTasksForTodos(
|
|
505
|
-
board,
|
|
506
|
-
board.tasks.filter(
|
|
507
|
-
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
508
|
-
)
|
|
509
|
-
).map((task) => managedTodoFromTask(task, board));
|
|
510
|
-
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
511
|
-
suppressedTodoMirrors.add(context);
|
|
512
|
-
try {
|
|
513
|
-
context.state.replaceTodos(projectedTodos);
|
|
514
|
-
} finally {
|
|
515
|
-
suppressedTodoMirrors.delete(context);
|
|
516
|
-
}
|
|
517
|
-
notifyTodoUpdate(context, context.todos);
|
|
518
|
-
broadcastTodoUpdate(context, context.todos);
|
|
519
|
-
return [...context.todos];
|
|
513
|
+
function applyManagedKanbanBoardToTodos2(context, board) {
|
|
514
|
+
return applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors);
|
|
520
515
|
}
|
|
521
516
|
|
|
522
517
|
// src/todo.ts
|
|
@@ -527,54 +522,34 @@ import {
|
|
|
527
522
|
saveTasks,
|
|
528
523
|
setPlanItemStatus
|
|
529
524
|
} from "@wrongstack/core/storage";
|
|
530
|
-
import { addTask as
|
|
525
|
+
import { addTask as addTask3, getBoard as getBoard5 } from "@wrongstack/kanban";
|
|
531
526
|
|
|
532
527
|
// src/kanban.ts
|
|
533
|
-
import {
|
|
528
|
+
import { stripLifecycleIssues as stripLifecycleIssues2 } from "@wrongstack/kanban";
|
|
529
|
+
|
|
530
|
+
// src/kanban-board-actions.ts
|
|
534
531
|
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
535
532
|
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
536
533
|
import {
|
|
537
534
|
addTask,
|
|
538
535
|
adoptManagedLifecycle,
|
|
539
|
-
assignTask,
|
|
540
|
-
claimReadyTask,
|
|
541
|
-
copyTaskToBoard,
|
|
542
536
|
createBoard as createBoard2,
|
|
543
537
|
createBoardFromTaskGraph,
|
|
544
538
|
createBoardFromText,
|
|
545
539
|
duplicateBoard,
|
|
546
|
-
evaluateContractGraphReadiness,
|
|
547
540
|
exportBoardAsMarkdown,
|
|
548
541
|
exportBoardToTaskGraph,
|
|
549
|
-
finalizeTaskCompletion,
|
|
550
542
|
getBoard as getBoard3,
|
|
551
543
|
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
552
544
|
getKanbanQueueHealth,
|
|
553
|
-
getTask,
|
|
554
|
-
getTaskChain,
|
|
555
|
-
heartbeatTaskAssignment,
|
|
556
545
|
listBoards as listBoards2,
|
|
557
546
|
listKanbanEvents,
|
|
558
547
|
listReadyTasks,
|
|
559
|
-
mergeTasks,
|
|
560
|
-
moveTask,
|
|
561
548
|
parseLinesIntoTasks,
|
|
562
|
-
recoverStaleTaskAssignments,
|
|
563
|
-
releaseTaskClaim,
|
|
564
549
|
removeBoard as removeBoard2,
|
|
565
|
-
removeTask,
|
|
566
|
-
repairManagedTaskProjection,
|
|
567
|
-
resolveAutoAccept,
|
|
568
550
|
searchKanban,
|
|
569
|
-
setTaskChain,
|
|
570
|
-
stripLifecycleIssues,
|
|
571
551
|
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
572
|
-
|
|
573
|
-
transitionTask,
|
|
574
|
-
updateBoard as updateBoard2,
|
|
575
|
-
updateTask as updateTask2,
|
|
576
|
-
updateTaskAssignment,
|
|
577
|
-
verifyTaskCompletion as verifyTaskCompletion2
|
|
552
|
+
updateBoard as updateBoard2
|
|
578
553
|
} from "@wrongstack/kanban";
|
|
579
554
|
|
|
580
555
|
// src/kanban-board-inputs.ts
|
|
@@ -621,16 +596,8 @@ function duplicateBoardOptions(input) {
|
|
|
621
596
|
};
|
|
622
597
|
}
|
|
623
598
|
|
|
624
|
-
// src/kanban-
|
|
625
|
-
import {
|
|
626
|
-
addContractEdge,
|
|
627
|
-
configureContractGraph,
|
|
628
|
-
evaluateTaskContractGraph,
|
|
629
|
-
getContractGraph,
|
|
630
|
-
removeContractEdge,
|
|
631
|
-
removeContractNode,
|
|
632
|
-
upsertContractNode
|
|
633
|
-
} from "@wrongstack/kanban";
|
|
599
|
+
// src/kanban-split-task-handler.ts
|
|
600
|
+
import { getBoard as getBoard2, splitTask } from "@wrongstack/kanban";
|
|
634
601
|
|
|
635
602
|
// src/kanban-tool-results.ts
|
|
636
603
|
function atomicityNudge(task) {
|
|
@@ -652,7 +619,314 @@ function okTask(board, task, message) {
|
|
|
652
619
|
return { ok: true, message, board, task };
|
|
653
620
|
}
|
|
654
621
|
|
|
622
|
+
// src/kanban-split-task-handler.ts
|
|
623
|
+
async function handleSplitTask(projectRoot, input, extraSplitOptions) {
|
|
624
|
+
const boardId = input.boardId;
|
|
625
|
+
const taskId = input.taskId;
|
|
626
|
+
const childTitles = input.childTitles;
|
|
627
|
+
if (!boardId || !taskId || !childTitles?.length) {
|
|
628
|
+
return fail("split requires boardId, taskId, and at least one childTitles.");
|
|
629
|
+
}
|
|
630
|
+
const {
|
|
631
|
+
targetColumnId,
|
|
632
|
+
inheritAssignment,
|
|
633
|
+
inheritLabels,
|
|
634
|
+
inheritSuccessCriteria,
|
|
635
|
+
inheritGoalMetrics,
|
|
636
|
+
inheritDependencies,
|
|
637
|
+
chainChildren,
|
|
638
|
+
rewireDependents
|
|
639
|
+
} = input;
|
|
640
|
+
const result = await splitTask(projectRoot, boardId, taskId, {
|
|
641
|
+
titles: childTitles,
|
|
642
|
+
...extraSplitOptions,
|
|
643
|
+
...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
|
|
644
|
+
...inheritAssignment !== void 0 ? { inheritAssignment } : {},
|
|
645
|
+
...inheritLabels !== void 0 ? { inheritLabels } : {},
|
|
646
|
+
...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
|
|
647
|
+
...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
|
|
648
|
+
...inheritDependencies !== void 0 ? { inheritDependencies } : {},
|
|
649
|
+
...chainChildren !== void 0 ? { chainChildren } : {},
|
|
650
|
+
...rewireDependents !== void 0 ? { rewireDependents } : {}
|
|
651
|
+
});
|
|
652
|
+
if (!result) return fail("Task not found.");
|
|
653
|
+
const freshParent = result.board.tasks?.find((t) => t.id === taskId);
|
|
654
|
+
if (!freshParent) {
|
|
655
|
+
return fail(
|
|
656
|
+
`Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
ok: true,
|
|
661
|
+
message: `${result.children.length} child task(s) created.`,
|
|
662
|
+
board: result.board,
|
|
663
|
+
task: freshParent,
|
|
664
|
+
children: result.children
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
async function requireBoard(projectRoot, boardId) {
|
|
668
|
+
return boardId ? getBoard2(projectRoot, boardId) : null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/kanban-board-actions.ts
|
|
672
|
+
async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
673
|
+
switch (input.action) {
|
|
674
|
+
case "list_boards": {
|
|
675
|
+
const boards = await listBoards2(projectRoot);
|
|
676
|
+
return { ok: true, message: `${boards.length} board(s).`, boards };
|
|
677
|
+
}
|
|
678
|
+
case "get_board": {
|
|
679
|
+
const board = await requireBoard(projectRoot, input.boardId);
|
|
680
|
+
return board ? okBoard(board) : fail("Board not found.");
|
|
681
|
+
}
|
|
682
|
+
case "create_board": {
|
|
683
|
+
if (!input.title) return fail("create_board requires title.");
|
|
684
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
685
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
686
|
+
);
|
|
687
|
+
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
688
|
+
const note = existing.length ? ` ${existing.length} other project board(s) already exist: ${existing.slice(0, 3).map((candidate) => `"${candidate.title}" (${candidate.taskCount} task(s))`).join(
|
|
689
|
+
", "
|
|
690
|
+
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
691
|
+
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
692
|
+
}
|
|
693
|
+
case "update_board": {
|
|
694
|
+
if (!input.boardId) return fail("update_board requires boardId.");
|
|
695
|
+
const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
|
|
696
|
+
return board ? okBoard(board, "Board updated.") : fail("Board not found.");
|
|
697
|
+
}
|
|
698
|
+
case "adopt_managed_lifecycle": {
|
|
699
|
+
if (!input.boardId || !input.author || !input.transitionComment) {
|
|
700
|
+
return fail(
|
|
701
|
+
"adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (input.columns?.length !== 5) {
|
|
705
|
+
return fail(
|
|
706
|
+
"adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const [backlog, todo, running, review, done] = input.columns;
|
|
710
|
+
if (!backlog || !todo || !running || !review || !done) {
|
|
711
|
+
return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
|
|
712
|
+
}
|
|
713
|
+
const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
|
|
714
|
+
columns: { backlog, todo, running, review, done },
|
|
715
|
+
actor: input.author,
|
|
716
|
+
comment: input.transitionComment
|
|
717
|
+
});
|
|
718
|
+
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
719
|
+
}
|
|
720
|
+
case "release_managed_lifecycle": {
|
|
721
|
+
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
722
|
+
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
723
|
+
return board ? okBoard(
|
|
724
|
+
board,
|
|
725
|
+
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
726
|
+
) : fail("Board not found.");
|
|
727
|
+
}
|
|
728
|
+
case "duplicate_board": {
|
|
729
|
+
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
730
|
+
const board = await duplicateBoard(projectRoot, input.boardId, duplicateBoardOptions(input));
|
|
731
|
+
return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
|
|
732
|
+
}
|
|
733
|
+
case "delete_board": {
|
|
734
|
+
if (!input.boardId) return fail("delete_board requires boardId.");
|
|
735
|
+
const removed = await removeBoard2(projectRoot, input.boardId);
|
|
736
|
+
return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
|
|
737
|
+
}
|
|
738
|
+
case "generate_board": {
|
|
739
|
+
if (!input.description) return fail("generate_board requires description.");
|
|
740
|
+
const boardInput = createBoardFromText({
|
|
741
|
+
description: input.description,
|
|
742
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
743
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
744
|
+
});
|
|
745
|
+
const board = await createBoard2(projectRoot, boardInput);
|
|
746
|
+
for (const tInput of parseLinesIntoTasks(
|
|
747
|
+
input.description,
|
|
748
|
+
board.columns[0]?.id ?? "backlog"
|
|
749
|
+
)) {
|
|
750
|
+
await addTask(projectRoot, board.id, tInput);
|
|
751
|
+
}
|
|
752
|
+
return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
|
|
753
|
+
}
|
|
754
|
+
case "export_markdown": {
|
|
755
|
+
const board = await requireBoard(projectRoot, input.boardId);
|
|
756
|
+
if (!board) return fail("Board not found.");
|
|
757
|
+
return {
|
|
758
|
+
ok: true,
|
|
759
|
+
message: "Board exported.",
|
|
760
|
+
board,
|
|
761
|
+
markdown: exportBoardAsMarkdown(board)
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
case "export_task_graph": {
|
|
765
|
+
if (!input.boardId) return fail("export_task_graph requires boardId.");
|
|
766
|
+
const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
|
|
767
|
+
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
768
|
+
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
769
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
770
|
+
...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
|
|
771
|
+
...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
|
|
772
|
+
});
|
|
773
|
+
if (!exported) return fail("Board not found.");
|
|
774
|
+
return {
|
|
775
|
+
ok: true,
|
|
776
|
+
message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
|
|
777
|
+
board: exported.board,
|
|
778
|
+
taskGraph: serializeTaskGraph(exported.graph)
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
case "sync_task_graph": {
|
|
782
|
+
if (!input.boardId || !input.taskGraph) {
|
|
783
|
+
return fail("sync_task_graph requires boardId and taskGraph.");
|
|
784
|
+
}
|
|
785
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
786
|
+
const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
|
|
787
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
788
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
789
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
790
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
791
|
+
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
792
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
793
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
794
|
+
...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
|
|
795
|
+
...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
|
|
796
|
+
});
|
|
797
|
+
return result ? {
|
|
798
|
+
ok: true,
|
|
799
|
+
message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
|
|
800
|
+
board: result.board
|
|
801
|
+
} : fail("Board not found.");
|
|
802
|
+
}
|
|
803
|
+
case "create_from_graph": {
|
|
804
|
+
if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
|
|
805
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
806
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
807
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
808
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
809
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
810
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
811
|
+
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
812
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
813
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
|
|
814
|
+
});
|
|
815
|
+
return {
|
|
816
|
+
ok: true,
|
|
817
|
+
message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
|
|
818
|
+
board
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
case "import_session_tasks": {
|
|
822
|
+
const taskPath = ctx.meta?.["task.path"];
|
|
823
|
+
if (!taskPath) return fail("No session task file for this session.");
|
|
824
|
+
const file = await loadTasks2(taskPath);
|
|
825
|
+
if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
|
|
826
|
+
const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
|
|
827
|
+
const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
|
|
828
|
+
const tags = ["session", `session:${sessionId}`];
|
|
829
|
+
const existing = (await listBoards2(projectRoot)).find(
|
|
830
|
+
(b) => b.tags?.includes(`session:${sessionId}`)
|
|
831
|
+
);
|
|
832
|
+
if (existing) {
|
|
833
|
+
const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
|
|
834
|
+
sourceSystem: "session",
|
|
835
|
+
tags,
|
|
836
|
+
archiveMissingTasks: true,
|
|
837
|
+
includeCompletedTasks: true
|
|
838
|
+
});
|
|
839
|
+
return result ? {
|
|
840
|
+
ok: true,
|
|
841
|
+
message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
|
|
842
|
+
board: result.board
|
|
843
|
+
} : fail("Session board vanished mid-sync.");
|
|
844
|
+
}
|
|
845
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
846
|
+
title: `Session tasks (${sessionId.slice(0, 8)})`,
|
|
847
|
+
sourceSystem: "session",
|
|
848
|
+
tags
|
|
849
|
+
});
|
|
850
|
+
return {
|
|
851
|
+
ok: true,
|
|
852
|
+
message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
|
|
853
|
+
board
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
case "search_tasks": {
|
|
857
|
+
const tasks = await searchKanban(projectRoot, {
|
|
858
|
+
query: input.query,
|
|
859
|
+
boardId: input.boardId,
|
|
860
|
+
assignedAgent: input.agentId,
|
|
861
|
+
status: input.status,
|
|
862
|
+
priority: input.priority,
|
|
863
|
+
label: input.labels?.[0],
|
|
864
|
+
chainId: input.chainId
|
|
865
|
+
});
|
|
866
|
+
return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
|
|
867
|
+
}
|
|
868
|
+
case "ready_tasks": {
|
|
869
|
+
const tasks = await listReadyTasks(projectRoot, {
|
|
870
|
+
query: input.query,
|
|
871
|
+
boardId: input.boardId,
|
|
872
|
+
assignedAgent: input.agentId,
|
|
873
|
+
priority: input.priority,
|
|
874
|
+
label: input.labels?.[0],
|
|
875
|
+
chainId: input.chainId,
|
|
876
|
+
limit: input.limit
|
|
877
|
+
});
|
|
878
|
+
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
879
|
+
}
|
|
880
|
+
case "snapshot": {
|
|
881
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
882
|
+
query: input.query,
|
|
883
|
+
boardId: input.boardId,
|
|
884
|
+
assignedAgent: input.agentId,
|
|
885
|
+
status: input.status,
|
|
886
|
+
priority: input.priority,
|
|
887
|
+
label: input.labels?.[0],
|
|
888
|
+
chainId: input.chainId
|
|
889
|
+
});
|
|
890
|
+
return {
|
|
891
|
+
ok: true,
|
|
892
|
+
message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
|
|
893
|
+
snapshot
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
case "events": {
|
|
897
|
+
if (!input.boardId) return fail("events requires boardId.");
|
|
898
|
+
const eventList = await listKanbanEvents(projectRoot, input.boardId);
|
|
899
|
+
return {
|
|
900
|
+
ok: true,
|
|
901
|
+
message: `${eventList.length} event(s).`,
|
|
902
|
+
events: eventList
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
case "queue_health": {
|
|
906
|
+
const health = await getKanbanQueueHealth(projectRoot, {
|
|
907
|
+
...input.boardId !== void 0 ? { boardId: input.boardId } : {}
|
|
908
|
+
});
|
|
909
|
+
return {
|
|
910
|
+
ok: true,
|
|
911
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
912
|
+
queueHealth: health
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
default:
|
|
916
|
+
return void 0;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
655
920
|
// src/kanban-contract-actions.ts
|
|
921
|
+
import {
|
|
922
|
+
addContractEdge,
|
|
923
|
+
configureContractGraph,
|
|
924
|
+
evaluateTaskContractGraph,
|
|
925
|
+
getContractGraph,
|
|
926
|
+
removeContractEdge,
|
|
927
|
+
removeContractNode,
|
|
928
|
+
upsertContractNode
|
|
929
|
+
} from "@wrongstack/kanban";
|
|
656
930
|
async function handleKanbanContractAction(projectRoot, input, actor) {
|
|
657
931
|
switch (input.action) {
|
|
658
932
|
case "get_contract_graph": {
|
|
@@ -855,79 +1129,27 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
855
1129
|
),
|
|
856
1130
|
verdict: verResult.report.verdict,
|
|
857
1131
|
message: verResult.report.markdownSummary,
|
|
858
|
-
board: persistedBoard,
|
|
859
|
-
task: freshTask ?? verResult.task
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
default:
|
|
863
|
-
return void 0;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// src/kanban-detail-actions.ts
|
|
868
|
-
import {
|
|
869
|
-
addCheckToTask,
|
|
870
|
-
addDependency,
|
|
871
|
-
addGoalMetricToTask,
|
|
872
|
-
addLinkToTask,
|
|
873
|
-
addNoteToTask,
|
|
874
|
-
getKanbanWorkbench,
|
|
875
|
-
removeCheckFromTask,
|
|
876
|
-
updateCheckOnTask,
|
|
877
|
-
updateGoalMetricOnTask
|
|
878
|
-
} from "@wrongstack/kanban";
|
|
879
|
-
|
|
880
|
-
// src/kanban-split-task-handler.ts
|
|
881
|
-
import { getBoard as getBoard2, splitTask } from "@wrongstack/kanban";
|
|
882
|
-
async function handleSplitTask(projectRoot, input, extraSplitOptions) {
|
|
883
|
-
const boardId = input.boardId;
|
|
884
|
-
const taskId = input.taskId;
|
|
885
|
-
const childTitles = input.childTitles;
|
|
886
|
-
if (!boardId || !taskId || !childTitles?.length) {
|
|
887
|
-
return fail("split requires boardId, taskId, and at least one childTitles.");
|
|
888
|
-
}
|
|
889
|
-
const {
|
|
890
|
-
targetColumnId,
|
|
891
|
-
inheritAssignment,
|
|
892
|
-
inheritLabels,
|
|
893
|
-
inheritSuccessCriteria,
|
|
894
|
-
inheritGoalMetrics,
|
|
895
|
-
inheritDependencies,
|
|
896
|
-
chainChildren,
|
|
897
|
-
rewireDependents
|
|
898
|
-
} = input;
|
|
899
|
-
const result = await splitTask(projectRoot, boardId, taskId, {
|
|
900
|
-
titles: childTitles,
|
|
901
|
-
...extraSplitOptions,
|
|
902
|
-
...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
|
|
903
|
-
...inheritAssignment !== void 0 ? { inheritAssignment } : {},
|
|
904
|
-
...inheritLabels !== void 0 ? { inheritLabels } : {},
|
|
905
|
-
...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
|
|
906
|
-
...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
|
|
907
|
-
...inheritDependencies !== void 0 ? { inheritDependencies } : {},
|
|
908
|
-
...chainChildren !== void 0 ? { chainChildren } : {},
|
|
909
|
-
...rewireDependents !== void 0 ? { rewireDependents } : {}
|
|
910
|
-
});
|
|
911
|
-
if (!result) return fail("Task not found.");
|
|
912
|
-
const freshParent = result.board.tasks?.find((t) => t.id === taskId);
|
|
913
|
-
if (!freshParent) {
|
|
914
|
-
return fail(
|
|
915
|
-
`Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
|
|
916
|
-
);
|
|
1132
|
+
board: persistedBoard,
|
|
1133
|
+
task: freshTask ?? verResult.task
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
default:
|
|
1137
|
+
return void 0;
|
|
917
1138
|
}
|
|
918
|
-
return {
|
|
919
|
-
ok: true,
|
|
920
|
-
message: `${result.children.length} child task(s) created.`,
|
|
921
|
-
board: result.board,
|
|
922
|
-
task: freshParent,
|
|
923
|
-
children: result.children
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
async function requireBoard(projectRoot, boardId) {
|
|
927
|
-
return boardId ? getBoard2(projectRoot, boardId) : null;
|
|
928
1139
|
}
|
|
929
1140
|
|
|
930
1141
|
// src/kanban-detail-actions.ts
|
|
1142
|
+
import {
|
|
1143
|
+
addCheckToTask,
|
|
1144
|
+
addDependency,
|
|
1145
|
+
addGoalMetricToTask,
|
|
1146
|
+
addLinkToTask,
|
|
1147
|
+
addNoteToTask,
|
|
1148
|
+
getKanbanWorkbench,
|
|
1149
|
+
removeCheckFromTask,
|
|
1150
|
+
updateCheckOnTask,
|
|
1151
|
+
updateGoalMetricOnTask
|
|
1152
|
+
} from "@wrongstack/kanban";
|
|
931
1153
|
async function handleKanbanDetailAction(projectRoot, input) {
|
|
932
1154
|
switch (input.action) {
|
|
933
1155
|
case "workbench": {
|
|
@@ -1060,26 +1282,34 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
1060
1282
|
}
|
|
1061
1283
|
}
|
|
1062
1284
|
|
|
1063
|
-
// src/kanban-
|
|
1064
|
-
import {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1285
|
+
// src/kanban-lifecycle-actions.ts
|
|
1286
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1287
|
+
import {
|
|
1288
|
+
addTask as addTask2,
|
|
1289
|
+
assignTask,
|
|
1290
|
+
claimReadyTask,
|
|
1291
|
+
copyTaskToBoard,
|
|
1292
|
+
evaluateContractGraphReadiness,
|
|
1293
|
+
finalizeTaskCompletion,
|
|
1294
|
+
getBoard as getBoard4,
|
|
1295
|
+
getTask,
|
|
1296
|
+
getTaskChain,
|
|
1297
|
+
heartbeatTaskAssignment,
|
|
1298
|
+
mergeTasks,
|
|
1299
|
+
moveTask,
|
|
1300
|
+
recoverStaleTaskAssignments,
|
|
1301
|
+
releaseTaskClaim,
|
|
1302
|
+
removeTask,
|
|
1303
|
+
repairManagedTaskProjection,
|
|
1304
|
+
resolveAutoAccept,
|
|
1305
|
+
setTaskChain,
|
|
1306
|
+
stripLifecycleIssues,
|
|
1307
|
+
transferTaskToBoard,
|
|
1308
|
+
transitionTask,
|
|
1309
|
+
updateTask as updateTask2,
|
|
1310
|
+
updateTaskAssignment,
|
|
1311
|
+
verifyTaskCompletion as verifyTaskCompletion2
|
|
1312
|
+
} from "@wrongstack/kanban";
|
|
1083
1313
|
|
|
1084
1314
|
// src/kanban-task-inputs.ts
|
|
1085
1315
|
import { randomUUID } from "node:crypto";
|
|
@@ -1228,31 +1458,597 @@ function assignmentInput(input) {
|
|
|
1228
1458
|
lastFailureKind: input.lastFailureKind
|
|
1229
1459
|
};
|
|
1230
1460
|
}
|
|
1231
|
-
function hasAssignmentInput(input) {
|
|
1232
|
-
return input.agentId !== void 0 || input.name !== void 0 || input.role !== void 0 || input.provider !== void 0 || input.model !== void 0 || input.fallbackProfile !== void 0 || input.fallbackModels !== void 0 || input.tools !== void 0 || input.allowedCapabilities !== void 0 || input.assignee !== void 0 || input.leaseId !== void 0 || input.claimedAt !== void 0 || input.heartbeatAt !== void 0 || input.leaseExpiresAt !== void 0 || input.attempt !== void 0 || input.maxAttempts !== void 0 || input.costCeilingUsd !== void 0 || input.retryPolicy !== void 0 || input.lastFailureKind !== void 0 || input.assignmentStatus !== void 0;
|
|
1233
|
-
}
|
|
1234
|
-
function assignmentForTaskCreate(input) {
|
|
1235
|
-
return {
|
|
1236
|
-
status: input.assignmentStatus ?? "assigned",
|
|
1237
|
-
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
1238
|
-
...input.name !== void 0 ? { name: input.name } : {},
|
|
1239
|
-
...input.role !== void 0 ? { role: input.role } : {},
|
|
1240
|
-
...input.provider !== void 0 ? { provider: input.provider } : {},
|
|
1241
|
-
...input.model !== void 0 ? { model: input.model } : {},
|
|
1242
|
-
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
1243
|
-
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
1244
|
-
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
1245
|
-
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
1246
|
-
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
1247
|
-
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
1248
|
-
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
1249
|
-
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
1250
|
-
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
1251
|
-
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {},
|
|
1252
|
-
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
1253
|
-
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
1254
|
-
...input.lastFailureKind !== void 0 ? { lastFailureKind: input.lastFailureKind } : {}
|
|
1255
|
-
};
|
|
1461
|
+
function hasAssignmentInput(input) {
|
|
1462
|
+
return input.agentId !== void 0 || input.name !== void 0 || input.role !== void 0 || input.provider !== void 0 || input.model !== void 0 || input.fallbackProfile !== void 0 || input.fallbackModels !== void 0 || input.tools !== void 0 || input.allowedCapabilities !== void 0 || input.assignee !== void 0 || input.leaseId !== void 0 || input.claimedAt !== void 0 || input.heartbeatAt !== void 0 || input.leaseExpiresAt !== void 0 || input.attempt !== void 0 || input.maxAttempts !== void 0 || input.costCeilingUsd !== void 0 || input.retryPolicy !== void 0 || input.lastFailureKind !== void 0 || input.assignmentStatus !== void 0;
|
|
1463
|
+
}
|
|
1464
|
+
function assignmentForTaskCreate(input) {
|
|
1465
|
+
return {
|
|
1466
|
+
status: input.assignmentStatus ?? "assigned",
|
|
1467
|
+
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
1468
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
1469
|
+
...input.role !== void 0 ? { role: input.role } : {},
|
|
1470
|
+
...input.provider !== void 0 ? { provider: input.provider } : {},
|
|
1471
|
+
...input.model !== void 0 ? { model: input.model } : {},
|
|
1472
|
+
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
1473
|
+
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
1474
|
+
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
1475
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
1476
|
+
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
1477
|
+
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
1478
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
1479
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
1480
|
+
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
1481
|
+
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {},
|
|
1482
|
+
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
1483
|
+
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
1484
|
+
...input.lastFailureKind !== void 0 ? { lastFailureKind: input.lastFailureKind } : {}
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// src/kanban-lifecycle-actions.ts
|
|
1489
|
+
async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
1490
|
+
switch (input.action) {
|
|
1491
|
+
case "add_task": {
|
|
1492
|
+
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1493
|
+
const result = await addTask2(projectRoot, input.boardId, taskInput(input));
|
|
1494
|
+
if (!result) return fail("Board not found.");
|
|
1495
|
+
return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
|
|
1496
|
+
}
|
|
1497
|
+
case "split_task": {
|
|
1498
|
+
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
1499
|
+
return fail("split_task requires boardId, taskId, and childTitles.");
|
|
1500
|
+
}
|
|
1501
|
+
return handleSplitTask(projectRoot, input, {});
|
|
1502
|
+
}
|
|
1503
|
+
case "merge_tasks": {
|
|
1504
|
+
if (!input.boardId || !input.taskIds?.length || !input.title) {
|
|
1505
|
+
return fail("merge_tasks requires boardId, taskIds, and title.");
|
|
1506
|
+
}
|
|
1507
|
+
const result = await mergeTasks(projectRoot, input.boardId, {
|
|
1508
|
+
taskIds: input.taskIds,
|
|
1509
|
+
title: input.title,
|
|
1510
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1511
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1512
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1513
|
+
...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
|
|
1514
|
+
});
|
|
1515
|
+
return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
|
|
1516
|
+
}
|
|
1517
|
+
case "copy_task": {
|
|
1518
|
+
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1519
|
+
return fail("copy_task requires boardId, taskId, and targetBoardId.");
|
|
1520
|
+
}
|
|
1521
|
+
const result = await copyTaskToBoard(
|
|
1522
|
+
projectRoot,
|
|
1523
|
+
input.boardId,
|
|
1524
|
+
input.taskId,
|
|
1525
|
+
input.targetBoardId,
|
|
1526
|
+
{
|
|
1527
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1528
|
+
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1529
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1530
|
+
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1531
|
+
}
|
|
1532
|
+
);
|
|
1533
|
+
return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
|
|
1534
|
+
}
|
|
1535
|
+
case "transfer_task": {
|
|
1536
|
+
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1537
|
+
return fail("transfer_task requires boardId, taskId, and targetBoardId.");
|
|
1538
|
+
}
|
|
1539
|
+
const result = await transferTaskToBoard(
|
|
1540
|
+
projectRoot,
|
|
1541
|
+
input.boardId,
|
|
1542
|
+
input.taskId,
|
|
1543
|
+
input.targetBoardId,
|
|
1544
|
+
{
|
|
1545
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1546
|
+
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1547
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1548
|
+
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1549
|
+
}
|
|
1550
|
+
);
|
|
1551
|
+
return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
|
|
1552
|
+
}
|
|
1553
|
+
case "get_task": {
|
|
1554
|
+
if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
|
|
1555
|
+
const task = await getTask(projectRoot, input.boardId, input.taskId);
|
|
1556
|
+
return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
|
|
1557
|
+
}
|
|
1558
|
+
case "start_task": {
|
|
1559
|
+
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1560
|
+
return fail("start_task requires boardId, taskId, author, and transitionComment.");
|
|
1561
|
+
}
|
|
1562
|
+
let board = await getBoard4(projectRoot, input.boardId);
|
|
1563
|
+
let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1564
|
+
if (!board || !task) return fail("Board or task not found.");
|
|
1565
|
+
const readiness = evaluateContractGraphReadiness(board, task.id);
|
|
1566
|
+
if (!readiness.ready) {
|
|
1567
|
+
return fail(
|
|
1568
|
+
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (board.lifecycle?.mode !== "managed") {
|
|
1572
|
+
const now = /* @__PURE__ */ new Date();
|
|
1573
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1574
|
+
status: "running",
|
|
1575
|
+
agentId: input.agentId ?? input.author,
|
|
1576
|
+
leaseId: input.leaseId ?? randomUUID2(),
|
|
1577
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1578
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1579
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1580
|
+
attempt: input.attempt ?? 1,
|
|
1581
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1582
|
+
});
|
|
1583
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1584
|
+
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
1585
|
+
status: "in_progress"
|
|
1586
|
+
});
|
|
1587
|
+
const current = started ?? assigned;
|
|
1588
|
+
const claimed = task;
|
|
1589
|
+
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
1590
|
+
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
1591
|
+
return okTask(
|
|
1592
|
+
current,
|
|
1593
|
+
currentTask,
|
|
1594
|
+
"Task is active and bound to this run for attribution. This board is not in managed lifecycle mode, so runtime Kanban governance was not bound to it."
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
let stage = task.lifecycle?.currentStage;
|
|
1598
|
+
if (stage === "backlog") {
|
|
1599
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1600
|
+
to: "todo",
|
|
1601
|
+
actor: input.author,
|
|
1602
|
+
comment: input.transitionComment
|
|
1603
|
+
});
|
|
1604
|
+
if (!moved) return fail("Task could not enter Todo.");
|
|
1605
|
+
board = moved.board;
|
|
1606
|
+
task = moved.task;
|
|
1607
|
+
stage = task.lifecycle?.currentStage;
|
|
1608
|
+
}
|
|
1609
|
+
if (stage === "todo" || stage === "review") {
|
|
1610
|
+
const now = /* @__PURE__ */ new Date();
|
|
1611
|
+
const leaseId = input.leaseId ?? randomUUID2();
|
|
1612
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1613
|
+
status: "running",
|
|
1614
|
+
agentId: input.agentId ?? input.author,
|
|
1615
|
+
leaseId,
|
|
1616
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1617
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1618
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1619
|
+
attempt: input.attempt ?? 1,
|
|
1620
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1621
|
+
});
|
|
1622
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1623
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1624
|
+
to: "running",
|
|
1625
|
+
actor: input.author,
|
|
1626
|
+
comment: input.transitionComment
|
|
1627
|
+
});
|
|
1628
|
+
if (!moved) return fail("Task could not enter Running.");
|
|
1629
|
+
board = moved.board;
|
|
1630
|
+
task = moved.task;
|
|
1631
|
+
stage = task.lifecycle?.currentStage;
|
|
1632
|
+
}
|
|
1633
|
+
if (stage !== "running" || task.assignment?.status !== "running") {
|
|
1634
|
+
return fail(
|
|
1635
|
+
`start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
ctx.setCurrentKanbanTask(task.id, board.id);
|
|
1639
|
+
return okTask(
|
|
1640
|
+
board,
|
|
1641
|
+
task,
|
|
1642
|
+
"Task is active; runtime Kanban governance is now bound to this run."
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
case "update_task": {
|
|
1646
|
+
if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
|
|
1647
|
+
const board = await updateTask2(projectRoot, input.boardId, input.taskId, taskPatch(input));
|
|
1648
|
+
return board ? okBoard(board, "Task updated.") : fail("Task not found.");
|
|
1649
|
+
}
|
|
1650
|
+
case "transition_task": {
|
|
1651
|
+
if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
|
|
1652
|
+
return fail(
|
|
1653
|
+
"transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
if (input.lifecycleStage === "done") {
|
|
1657
|
+
const boardBefore = await getBoard4(projectRoot, input.boardId);
|
|
1658
|
+
const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
|
|
1659
|
+
if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
|
|
1660
|
+
const preGate = await verifyTaskCompletion2(projectRoot, input.boardId, taskBefore.id, {
|
|
1661
|
+
persist: false
|
|
1662
|
+
});
|
|
1663
|
+
await updateTask2(projectRoot, input.boardId, taskBefore.id, {
|
|
1664
|
+
verificationReport: preGate.report,
|
|
1665
|
+
successCriteria: preGate.task.successCriteria
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
const result = await transitionTask(projectRoot, input.boardId, input.taskId, {
|
|
1670
|
+
to: input.lifecycleStage,
|
|
1671
|
+
actor: input.author,
|
|
1672
|
+
comment: input.transitionComment,
|
|
1673
|
+
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
1674
|
+
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
1675
|
+
...input.attachmentUrl !== void 0 ? {
|
|
1676
|
+
attachment: {
|
|
1677
|
+
url: input.attachmentUrl,
|
|
1678
|
+
type: input.attachmentType ?? "url",
|
|
1679
|
+
...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
|
|
1680
|
+
}
|
|
1681
|
+
} : {},
|
|
1682
|
+
patch: taskPatch(input)
|
|
1683
|
+
});
|
|
1684
|
+
if (result && input.lifecycleStage === "done" && result.task.verificationReport) {
|
|
1685
|
+
recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
|
|
1686
|
+
}
|
|
1687
|
+
return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
|
|
1688
|
+
}
|
|
1689
|
+
case "repair_managed_projection": {
|
|
1690
|
+
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1691
|
+
return fail(
|
|
1692
|
+
"repair_managed_projection requires boardId, taskId, author, and transitionComment."
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
const result = await repairManagedTaskProjection(projectRoot, input.boardId, input.taskId, {
|
|
1696
|
+
actor: input.author,
|
|
1697
|
+
comment: input.transitionComment
|
|
1698
|
+
});
|
|
1699
|
+
return result ? okTask(
|
|
1700
|
+
result.board,
|
|
1701
|
+
result.task,
|
|
1702
|
+
"Managed card projection repaired from lifecycle history."
|
|
1703
|
+
) : fail("Board or task not found.");
|
|
1704
|
+
}
|
|
1705
|
+
case "move_task": {
|
|
1706
|
+
if (!input.boardId || !input.taskId || !input.targetColumnId) {
|
|
1707
|
+
return fail("move_task requires boardId, taskId, and targetColumnId.");
|
|
1708
|
+
}
|
|
1709
|
+
const board = await moveTask(
|
|
1710
|
+
projectRoot,
|
|
1711
|
+
input.boardId,
|
|
1712
|
+
input.taskId,
|
|
1713
|
+
input.targetColumnId,
|
|
1714
|
+
input.order
|
|
1715
|
+
);
|
|
1716
|
+
return board ? okBoard(board, "Task moved.") : fail("Move failed.");
|
|
1717
|
+
}
|
|
1718
|
+
case "delete_task": {
|
|
1719
|
+
if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
|
|
1720
|
+
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
1721
|
+
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
1722
|
+
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
1723
|
+
}
|
|
1724
|
+
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1725
|
+
}
|
|
1726
|
+
case "set_chain": {
|
|
1727
|
+
if (!input.boardId || !input.taskIds?.length) {
|
|
1728
|
+
return fail("set_chain requires boardId and taskIds.");
|
|
1729
|
+
}
|
|
1730
|
+
const result = await setTaskChain(projectRoot, input.boardId, {
|
|
1731
|
+
taskIds: input.taskIds,
|
|
1732
|
+
...input.chainId !== void 0 ? { chainId: input.chainId } : {},
|
|
1733
|
+
...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
|
|
1734
|
+
});
|
|
1735
|
+
return result ? {
|
|
1736
|
+
ok: true,
|
|
1737
|
+
message: `Chain set: ${result.chainId}`,
|
|
1738
|
+
board: result.board,
|
|
1739
|
+
chain: result.tasks
|
|
1740
|
+
} : fail("Board or task not found.");
|
|
1741
|
+
}
|
|
1742
|
+
case "get_chain": {
|
|
1743
|
+
if (!input.boardId || !(input.taskId || input.chainId)) {
|
|
1744
|
+
return fail("get_chain requires boardId and taskId or chainId.");
|
|
1745
|
+
}
|
|
1746
|
+
const result = await getTaskChain(
|
|
1747
|
+
projectRoot,
|
|
1748
|
+
input.boardId,
|
|
1749
|
+
input.taskId ?? input.chainId ?? ""
|
|
1750
|
+
);
|
|
1751
|
+
return result ? {
|
|
1752
|
+
ok: true,
|
|
1753
|
+
message: `Chain loaded: ${result.chainId}`,
|
|
1754
|
+
board: result.board,
|
|
1755
|
+
chain: result.tasks
|
|
1756
|
+
} : fail("Chain not found.");
|
|
1757
|
+
}
|
|
1758
|
+
case "claim_task": {
|
|
1759
|
+
const result = await claimReadyTask(projectRoot, {
|
|
1760
|
+
...input.boardId !== void 0 ? { boardId: input.boardId } : {},
|
|
1761
|
+
...input.taskId !== void 0 ? { taskId: input.taskId } : {},
|
|
1762
|
+
...assignmentInput(input),
|
|
1763
|
+
status: input.assignmentStatus ?? "queued"
|
|
1764
|
+
});
|
|
1765
|
+
return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
|
|
1766
|
+
}
|
|
1767
|
+
case "release_task": {
|
|
1768
|
+
if (!input.boardId || !input.taskId) {
|
|
1769
|
+
return fail("release_task requires boardId and taskId.");
|
|
1770
|
+
}
|
|
1771
|
+
const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
|
|
1772
|
+
...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
|
|
1773
|
+
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
1774
|
+
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
|
|
1775
|
+
});
|
|
1776
|
+
return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
|
|
1777
|
+
}
|
|
1778
|
+
case "assign_task": {
|
|
1779
|
+
if (!input.boardId || !input.taskId) return fail("assign_task requires boardId and taskId.");
|
|
1780
|
+
const board = await assignTask(
|
|
1781
|
+
projectRoot,
|
|
1782
|
+
input.boardId,
|
|
1783
|
+
input.taskId,
|
|
1784
|
+
assignmentInput(input)
|
|
1785
|
+
);
|
|
1786
|
+
return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
|
|
1787
|
+
}
|
|
1788
|
+
case "mark_assignment": {
|
|
1789
|
+
if (!input.boardId || !input.taskId)
|
|
1790
|
+
return fail("mark_assignment requires boardId and taskId.");
|
|
1791
|
+
const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
|
|
1792
|
+
const board = await updateTaskAssignment(
|
|
1793
|
+
projectRoot,
|
|
1794
|
+
input.boardId,
|
|
1795
|
+
input.taskId,
|
|
1796
|
+
{
|
|
1797
|
+
...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
|
|
1798
|
+
...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
|
|
1799
|
+
...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
|
|
1800
|
+
...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
|
|
1801
|
+
...input.error !== void 0 ? { error: input.error } : {},
|
|
1802
|
+
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
1803
|
+
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
1804
|
+
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
1805
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
1806
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
1807
|
+
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
1808
|
+
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
|
|
1809
|
+
},
|
|
1810
|
+
input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
1811
|
+
);
|
|
1812
|
+
if (!board) return fail("Task not found.");
|
|
1813
|
+
if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
|
|
1814
|
+
const envGate = readEnvGateEnforcement();
|
|
1815
|
+
const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
|
|
1816
|
+
...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
|
|
1817
|
+
...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
|
|
1818
|
+
});
|
|
1819
|
+
if (finalized) {
|
|
1820
|
+
if (finalized.gate.report) {
|
|
1821
|
+
recordKanbanVerificationEvidence(ctx, finalized.gate.report);
|
|
1822
|
+
}
|
|
1823
|
+
const gateSummary = {
|
|
1824
|
+
enforcement: finalized.gate.enforcement,
|
|
1825
|
+
allowed: finalized.gate.allowed,
|
|
1826
|
+
verdict: finalized.gate.verdict,
|
|
1827
|
+
issues: finalized.gate.issues.map((issue) => issue.message)
|
|
1828
|
+
};
|
|
1829
|
+
const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "passed" : "passed"}; task completed.` : finalized.gate.enforcement === "strict" ? `Completion gate BLOCKED (verdict: ${finalized.gate.verdict}); task parked in review. Issues: ${gateSummary.issues.join(" | ")}` : `Completion gate failed softly (verdict: ${finalized.gate.verdict}); task completed with warnings. Issues: ${gateSummary.issues.join(" | ")}`;
|
|
1830
|
+
return {
|
|
1831
|
+
...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
|
|
1832
|
+
gate: gateSummary
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
} else if (board.lifecycle?.mode === "managed") {
|
|
1836
|
+
const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1837
|
+
const stage = managedTask?.lifecycle?.currentStage;
|
|
1838
|
+
const actor = ctx.agentId ?? "kanban-agent";
|
|
1839
|
+
let transitionResult = null;
|
|
1840
|
+
const lifecycleWarnings = [];
|
|
1841
|
+
if (assignmentStatus === "running" && stage === "todo") {
|
|
1842
|
+
try {
|
|
1843
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
1844
|
+
to: "running",
|
|
1845
|
+
actor,
|
|
1846
|
+
comment: "Work started."
|
|
1847
|
+
});
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
lifecycleWarnings.push(
|
|
1850
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (assignmentStatus === "completed" && stage === "running") {
|
|
1855
|
+
const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
|
|
1856
|
+
try {
|
|
1857
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
1858
|
+
to: "review",
|
|
1859
|
+
actor,
|
|
1860
|
+
comment,
|
|
1861
|
+
attachment: {
|
|
1862
|
+
url: `kanban://task/${input.taskId}/result`,
|
|
1863
|
+
title: "Worker completion result",
|
|
1864
|
+
type: "file"
|
|
1865
|
+
},
|
|
1866
|
+
patch: {
|
|
1867
|
+
...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
lifecycleWarnings.push(
|
|
1872
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
if (transitionResult) {
|
|
1876
|
+
const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
|
|
1877
|
+
if (hasCriteria) {
|
|
1878
|
+
try {
|
|
1879
|
+
const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId);
|
|
1880
|
+
if (verResult.report) {
|
|
1881
|
+
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
1882
|
+
}
|
|
1883
|
+
await updateTask2(projectRoot, board.id, input.taskId, {
|
|
1884
|
+
verificationReport: verResult.report,
|
|
1885
|
+
successCriteria: verResult.task.successCriteria
|
|
1886
|
+
});
|
|
1887
|
+
const verdict = verResult.report.verdict;
|
|
1888
|
+
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
1889
|
+
lifecycleWarnings.push(
|
|
1890
|
+
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
1891
|
+
);
|
|
1892
|
+
} else if (verdict === "passed") {
|
|
1893
|
+
try {
|
|
1894
|
+
const doneResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
1895
|
+
to: "done",
|
|
1896
|
+
actor,
|
|
1897
|
+
action: "Automated acceptance after verification",
|
|
1898
|
+
comment: "Auto-accepted: verification passed.",
|
|
1899
|
+
attachment: {
|
|
1900
|
+
url: `kanban://task/${input.taskId}/verification`,
|
|
1901
|
+
title: "Auto-verification result",
|
|
1902
|
+
type: "file"
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
transitionResult = doneResult;
|
|
1906
|
+
} catch (acceptErr) {
|
|
1907
|
+
lifecycleWarnings.push(
|
|
1908
|
+
`Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
} else {
|
|
1912
|
+
lifecycleWarnings.push(
|
|
1913
|
+
`Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
} catch (verifyErr) {
|
|
1917
|
+
lifecycleWarnings.push(
|
|
1918
|
+
`Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
} else {
|
|
1922
|
+
lifecycleWarnings.push(
|
|
1923
|
+
"No automatic success criteria \u2014 card left in Review for manual verification."
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const responseBoard = transitionResult?.board ?? board;
|
|
1929
|
+
const responseTask = transitionResult?.task ?? managedTask;
|
|
1930
|
+
const msgParts = ["Assignment updated."];
|
|
1931
|
+
if (transitionResult) {
|
|
1932
|
+
msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
|
|
1933
|
+
}
|
|
1934
|
+
for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
|
|
1935
|
+
return okTask(responseBoard, responseTask, msgParts.join(" "));
|
|
1936
|
+
}
|
|
1937
|
+
return okBoard(board, "Assignment updated.");
|
|
1938
|
+
}
|
|
1939
|
+
case "heartbeat_assignment": {
|
|
1940
|
+
if (!input.boardId || !input.taskId) {
|
|
1941
|
+
return fail("heartbeat_assignment requires boardId and taskId.");
|
|
1942
|
+
}
|
|
1943
|
+
const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
|
|
1944
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
1945
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
1946
|
+
...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
1947
|
+
});
|
|
1948
|
+
return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
|
|
1949
|
+
}
|
|
1950
|
+
case "recover_stale": {
|
|
1951
|
+
if (!input.boardId) return fail("recover_stale requires boardId.");
|
|
1952
|
+
const policyFields = [
|
|
1953
|
+
input.recoveryPolicyFailOnCostCeiling !== void 0,
|
|
1954
|
+
input.recoveryPolicyReleaseOnFailureKinds !== void 0,
|
|
1955
|
+
input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
|
|
1956
|
+
input.recoveryPolicyRetryPolicyOverride !== void 0
|
|
1957
|
+
].some(Boolean);
|
|
1958
|
+
const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
|
|
1959
|
+
...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
|
|
1960
|
+
...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
|
|
1961
|
+
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
1962
|
+
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
|
|
1963
|
+
...policyFields ? {
|
|
1964
|
+
policy: {
|
|
1965
|
+
...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
|
|
1966
|
+
...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
|
|
1967
|
+
releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
|
|
1968
|
+
} : {},
|
|
1969
|
+
...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
|
|
1970
|
+
releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
|
|
1971
|
+
} : {},
|
|
1972
|
+
...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
|
|
1973
|
+
retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
|
|
1974
|
+
} : {}
|
|
1975
|
+
}
|
|
1976
|
+
} : {}
|
|
1977
|
+
});
|
|
1978
|
+
return result ? {
|
|
1979
|
+
ok: true,
|
|
1980
|
+
message: `Recovered ${result.tasks.length} stale assignment(s).`,
|
|
1981
|
+
board: result.board,
|
|
1982
|
+
recoveredTasks: result.tasks
|
|
1983
|
+
} : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
|
|
1984
|
+
}
|
|
1985
|
+
default:
|
|
1986
|
+
return void 0;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// src/kanban-presence.ts
|
|
1991
|
+
import { touchKanbanPresence as touchKanbanPresence2 } from "@wrongstack/kanban";
|
|
1992
|
+
function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
1993
|
+
return async (result) => {
|
|
1994
|
+
const boardId = result.board?.id ?? input.boardId;
|
|
1995
|
+
if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
|
|
1996
|
+
try {
|
|
1997
|
+
const board = await touchKanbanPresence2(projectRoot, boardId, {
|
|
1998
|
+
sessionId: ctx.session.id,
|
|
1999
|
+
agentId: ctx.agentId,
|
|
2000
|
+
agentName: ctx.agentName,
|
|
2001
|
+
taskId: input.taskId ?? result.task?.id,
|
|
2002
|
+
runTaskId: input.runTaskId
|
|
2003
|
+
});
|
|
2004
|
+
return board ? { ...result, board } : result;
|
|
2005
|
+
} catch {
|
|
2006
|
+
return result;
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// src/kanban-serializer.ts
|
|
2012
|
+
var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
|
|
2013
|
+
var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
2014
|
+
"get_board",
|
|
2015
|
+
"export_markdown",
|
|
2016
|
+
"export_task_graph"
|
|
2017
|
+
]);
|
|
2018
|
+
function serializeKanbanOutput(output, input) {
|
|
2019
|
+
const action = input && typeof input === "object" ? input.action : void 0;
|
|
2020
|
+
const board = output.board;
|
|
2021
|
+
if (board) {
|
|
2022
|
+
const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
|
|
2023
|
+
let boardBytes = 0;
|
|
2024
|
+
if (!keepFull) {
|
|
2025
|
+
try {
|
|
2026
|
+
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
2027
|
+
} catch {
|
|
2028
|
+
boardBytes = 0;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
2032
|
+
const columns = {};
|
|
2033
|
+
for (const column of board.columns) {
|
|
2034
|
+
columns[column.title || column.id] = board.tasks.filter(
|
|
2035
|
+
(task) => task.columnId === column.id
|
|
2036
|
+
).length;
|
|
2037
|
+
}
|
|
2038
|
+
const compact = {
|
|
2039
|
+
...output,
|
|
2040
|
+
board: {
|
|
2041
|
+
id: board.id,
|
|
2042
|
+
title: board.title,
|
|
2043
|
+
columns,
|
|
2044
|
+
totalTasks: board.tasks.length,
|
|
2045
|
+
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
return JSON.stringify(compact, null, 2);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
return JSON.stringify(output, null, 2);
|
|
1256
2052
|
}
|
|
1257
2053
|
|
|
1258
2054
|
// src/kanban-tool-schema.ts
|
|
@@ -1590,8 +2386,6 @@ var kanbanTool = {
|
|
|
1590
2386
|
description: KANBAN_TOOL_DESCRIPTION,
|
|
1591
2387
|
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
1592
2388
|
permission: "confirm",
|
|
1593
|
-
// WS-046: gives permission decisions something to key on.
|
|
1594
|
-
// The action performed; kanban has no single file or path subject.
|
|
1595
2389
|
subjectKey: "action",
|
|
1596
2390
|
mutating: true,
|
|
1597
2391
|
capabilities: ["fs.write"],
|
|
@@ -1606,873 +2400,29 @@ var kanbanTool = {
|
|
|
1606
2400
|
const result = await (async () => {
|
|
1607
2401
|
const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
|
|
1608
2402
|
if (decompositionResult !== void 0) return decompositionResult;
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
);
|
|
1623
|
-
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
1624
|
-
const note = existing.length ? ` ${existing.length} other project board(s) already exist: ${existing.slice(0, 3).map((candidate) => `"${candidate.title}" (${candidate.taskCount} task(s))`).join(
|
|
1625
|
-
", "
|
|
1626
|
-
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
1627
|
-
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
1628
|
-
}
|
|
1629
|
-
case "update_board": {
|
|
1630
|
-
if (!input.boardId) return fail("update_board requires boardId.");
|
|
1631
|
-
const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
|
|
1632
|
-
return board ? okBoard(board, "Board updated.") : fail("Board not found.");
|
|
1633
|
-
}
|
|
1634
|
-
case "adopt_managed_lifecycle": {
|
|
1635
|
-
if (!input.boardId || !input.author || !input.transitionComment) {
|
|
1636
|
-
return fail(
|
|
1637
|
-
"adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
|
|
1638
|
-
);
|
|
1639
|
-
}
|
|
1640
|
-
if (input.columns?.length !== 5) {
|
|
1641
|
-
return fail(
|
|
1642
|
-
"adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
|
|
1643
|
-
);
|
|
1644
|
-
}
|
|
1645
|
-
const [backlog, todo, running, review, done] = input.columns;
|
|
1646
|
-
if (!backlog || !todo || !running || !review || !done) {
|
|
1647
|
-
return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
|
|
1648
|
-
}
|
|
1649
|
-
const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
|
|
1650
|
-
columns: { backlog, todo, running, review, done },
|
|
1651
|
-
actor: input.author,
|
|
1652
|
-
comment: input.transitionComment
|
|
1653
|
-
});
|
|
1654
|
-
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
1655
|
-
}
|
|
1656
|
-
// Adoption used to be a one-way door: the strict lifecycle carries
|
|
1657
|
-
// acceptance-criteria, verification-report, review-evidence and
|
|
1658
|
-
// one-stage-at-a-time gates, and nothing on the tool surface could
|
|
1659
|
-
// undo it, so a board adopted once kept its ceremony forever. The
|
|
1660
|
-
// gates are worth having where a fleet is supervised; they are not
|
|
1661
|
-
// worth being unable to leave. Cards and columns are untouched.
|
|
1662
|
-
case "release_managed_lifecycle": {
|
|
1663
|
-
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
1664
|
-
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
1665
|
-
return board ? okBoard(
|
|
1666
|
-
board,
|
|
1667
|
-
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
1668
|
-
) : fail("Board not found.");
|
|
1669
|
-
}
|
|
1670
|
-
case "duplicate_board": {
|
|
1671
|
-
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
1672
|
-
const board = await duplicateBoard(
|
|
1673
|
-
projectRoot,
|
|
1674
|
-
input.boardId,
|
|
1675
|
-
duplicateBoardOptions(input)
|
|
1676
|
-
);
|
|
1677
|
-
return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
|
|
1678
|
-
}
|
|
1679
|
-
case "delete_board": {
|
|
1680
|
-
if (!input.boardId) return fail("delete_board requires boardId.");
|
|
1681
|
-
const removed = await removeBoard2(projectRoot, input.boardId);
|
|
1682
|
-
return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
|
|
1683
|
-
}
|
|
1684
|
-
case "generate_board": {
|
|
1685
|
-
if (!input.description) return fail("generate_board requires description.");
|
|
1686
|
-
const boardInput = createBoardFromText({
|
|
1687
|
-
description: input.description,
|
|
1688
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1689
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
1690
|
-
});
|
|
1691
|
-
const board = await createBoard2(projectRoot, boardInput);
|
|
1692
|
-
for (const taskInput2 of parseLinesIntoTasks(
|
|
1693
|
-
input.description,
|
|
1694
|
-
board.columns[0]?.id ?? "backlog"
|
|
1695
|
-
)) {
|
|
1696
|
-
await addTask(projectRoot, board.id, taskInput2);
|
|
1697
|
-
}
|
|
1698
|
-
return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
|
|
1699
|
-
}
|
|
1700
|
-
case "export_markdown": {
|
|
1701
|
-
const board = await requireBoard(projectRoot, input.boardId);
|
|
1702
|
-
if (!board) return fail("Board not found.");
|
|
1703
|
-
return {
|
|
1704
|
-
ok: true,
|
|
1705
|
-
message: "Board exported.",
|
|
1706
|
-
board,
|
|
1707
|
-
markdown: exportBoardAsMarkdown(board)
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
case "export_task_graph": {
|
|
1711
|
-
if (!input.boardId) return fail("export_task_graph requires boardId.");
|
|
1712
|
-
const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
|
|
1713
|
-
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
1714
|
-
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
1715
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1716
|
-
...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
|
|
1717
|
-
...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
|
|
1718
|
-
});
|
|
1719
|
-
if (!exported) return fail("Board not found.");
|
|
1720
|
-
return {
|
|
1721
|
-
ok: true,
|
|
1722
|
-
message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
|
|
1723
|
-
board: exported.board,
|
|
1724
|
-
taskGraph: serializeTaskGraph(exported.graph)
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
case "sync_task_graph": {
|
|
1728
|
-
if (!input.boardId || !input.taskGraph) {
|
|
1729
|
-
return fail("sync_task_graph requires boardId and taskGraph.");
|
|
1730
|
-
}
|
|
1731
|
-
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
1732
|
-
const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
|
|
1733
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1734
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1735
|
-
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
1736
|
-
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
1737
|
-
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
1738
|
-
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
1739
|
-
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
1740
|
-
...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
|
|
1741
|
-
...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
|
|
1742
|
-
});
|
|
1743
|
-
return result2 ? {
|
|
1744
|
-
ok: true,
|
|
1745
|
-
message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
|
|
1746
|
-
board: result2.board
|
|
1747
|
-
} : fail("Board not found.");
|
|
1748
|
-
}
|
|
1749
|
-
case "create_from_graph": {
|
|
1750
|
-
if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
|
|
1751
|
-
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
1752
|
-
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
1753
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1754
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1755
|
-
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
1756
|
-
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
1757
|
-
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
1758
|
-
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
1759
|
-
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
|
|
1760
|
-
});
|
|
1761
|
-
return {
|
|
1762
|
-
ok: true,
|
|
1763
|
-
message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
|
|
1764
|
-
board
|
|
1765
|
-
};
|
|
1766
|
-
}
|
|
1767
|
-
case "import_session_tasks": {
|
|
1768
|
-
const taskPath = ctx.meta?.["task.path"];
|
|
1769
|
-
if (!taskPath) return fail("No session task file for this session.");
|
|
1770
|
-
const file = await loadTasks2(taskPath);
|
|
1771
|
-
if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
|
|
1772
|
-
const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
|
|
1773
|
-
const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
|
|
1774
|
-
const tags = ["session", `session:${sessionId}`];
|
|
1775
|
-
const existing = (await listBoards2(projectRoot)).find(
|
|
1776
|
-
(b) => b.tags?.includes(`session:${sessionId}`)
|
|
1777
|
-
);
|
|
1778
|
-
if (existing) {
|
|
1779
|
-
const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
|
|
1780
|
-
sourceSystem: "session",
|
|
1781
|
-
tags,
|
|
1782
|
-
archiveMissingTasks: true,
|
|
1783
|
-
includeCompletedTasks: true
|
|
1784
|
-
});
|
|
1785
|
-
return result2 ? {
|
|
1786
|
-
ok: true,
|
|
1787
|
-
message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
|
|
1788
|
-
board: result2.board
|
|
1789
|
-
} : fail("Session board vanished mid-sync.");
|
|
1790
|
-
}
|
|
1791
|
-
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
1792
|
-
title: `Session tasks (${sessionId.slice(0, 8)})`,
|
|
1793
|
-
sourceSystem: "session",
|
|
1794
|
-
tags
|
|
1795
|
-
});
|
|
1796
|
-
return {
|
|
1797
|
-
ok: true,
|
|
1798
|
-
message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
|
|
1799
|
-
board
|
|
1800
|
-
};
|
|
1801
|
-
}
|
|
1802
|
-
case "search_tasks": {
|
|
1803
|
-
const tasks = await searchKanban(projectRoot, {
|
|
1804
|
-
query: input.query,
|
|
1805
|
-
boardId: input.boardId,
|
|
1806
|
-
assignedAgent: input.agentId,
|
|
1807
|
-
status: input.status,
|
|
1808
|
-
priority: input.priority,
|
|
1809
|
-
label: input.labels?.[0],
|
|
1810
|
-
chainId: input.chainId
|
|
1811
|
-
});
|
|
1812
|
-
return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
|
|
1813
|
-
}
|
|
1814
|
-
case "ready_tasks": {
|
|
1815
|
-
const tasks = await listReadyTasks(projectRoot, {
|
|
1816
|
-
query: input.query,
|
|
1817
|
-
boardId: input.boardId,
|
|
1818
|
-
assignedAgent: input.agentId,
|
|
1819
|
-
priority: input.priority,
|
|
1820
|
-
label: input.labels?.[0],
|
|
1821
|
-
chainId: input.chainId,
|
|
1822
|
-
limit: input.limit
|
|
1823
|
-
});
|
|
1824
|
-
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1825
|
-
}
|
|
1826
|
-
case "snapshot": {
|
|
1827
|
-
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
1828
|
-
query: input.query,
|
|
1829
|
-
boardId: input.boardId,
|
|
1830
|
-
assignedAgent: input.agentId,
|
|
1831
|
-
status: input.status,
|
|
1832
|
-
priority: input.priority,
|
|
1833
|
-
label: input.labels?.[0],
|
|
1834
|
-
chainId: input.chainId
|
|
1835
|
-
});
|
|
1836
|
-
return {
|
|
1837
|
-
ok: true,
|
|
1838
|
-
message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
|
|
1839
|
-
snapshot
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
case "add_task": {
|
|
1843
|
-
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1844
|
-
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
1845
|
-
if (!result2) return fail("Board not found.");
|
|
1846
|
-
return okTask(result2.board, result2.task, `Task added.${atomicityNudge(result2.task)}`);
|
|
1847
|
-
}
|
|
1848
|
-
case "split_task": {
|
|
1849
|
-
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
1850
|
-
return fail("split_task requires boardId, taskId, and childTitles.");
|
|
1851
|
-
}
|
|
1852
|
-
return handleSplitTask(projectRoot, input, {});
|
|
1853
|
-
}
|
|
1854
|
-
case "merge_tasks": {
|
|
1855
|
-
if (!input.boardId || !input.taskIds?.length || !input.title) {
|
|
1856
|
-
return fail("merge_tasks requires boardId, taskIds, and title.");
|
|
1857
|
-
}
|
|
1858
|
-
const result2 = await mergeTasks(projectRoot, input.boardId, {
|
|
1859
|
-
taskIds: input.taskIds,
|
|
1860
|
-
title: input.title,
|
|
1861
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1862
|
-
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1863
|
-
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1864
|
-
...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
|
|
1865
|
-
});
|
|
1866
|
-
return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
|
|
1867
|
-
}
|
|
1868
|
-
case "copy_task": {
|
|
1869
|
-
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1870
|
-
return fail("copy_task requires boardId, taskId, and targetBoardId.");
|
|
1871
|
-
}
|
|
1872
|
-
const result2 = await copyTaskToBoard(
|
|
1873
|
-
projectRoot,
|
|
1874
|
-
input.boardId,
|
|
1875
|
-
input.taskId,
|
|
1876
|
-
input.targetBoardId,
|
|
1877
|
-
{
|
|
1878
|
-
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1879
|
-
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1880
|
-
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1881
|
-
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1882
|
-
}
|
|
1883
|
-
);
|
|
1884
|
-
return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
|
|
1885
|
-
}
|
|
1886
|
-
case "transfer_task": {
|
|
1887
|
-
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1888
|
-
return fail("transfer_task requires boardId, taskId, and targetBoardId.");
|
|
1889
|
-
}
|
|
1890
|
-
const result2 = await transferTaskToBoard(
|
|
1891
|
-
projectRoot,
|
|
1892
|
-
input.boardId,
|
|
1893
|
-
input.taskId,
|
|
1894
|
-
input.targetBoardId,
|
|
1895
|
-
{
|
|
1896
|
-
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1897
|
-
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1898
|
-
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1899
|
-
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1900
|
-
}
|
|
1901
|
-
);
|
|
1902
|
-
return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
|
|
1903
|
-
}
|
|
1904
|
-
case "get_task": {
|
|
1905
|
-
if (!input.boardId || !input.taskId)
|
|
1906
|
-
return fail("get_task requires boardId and taskId.");
|
|
1907
|
-
const task = await getTask(projectRoot, input.boardId, input.taskId);
|
|
1908
|
-
return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
|
|
1909
|
-
}
|
|
1910
|
-
case "start_task": {
|
|
1911
|
-
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1912
|
-
return fail("start_task requires boardId, taskId, author, and transitionComment.");
|
|
1913
|
-
}
|
|
1914
|
-
let board = await getBoard3(projectRoot, input.boardId);
|
|
1915
|
-
let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1916
|
-
if (!board || !task) return fail("Board or task not found.");
|
|
1917
|
-
const readiness = evaluateContractGraphReadiness(board, task.id);
|
|
1918
|
-
if (!readiness.ready) {
|
|
1919
|
-
return fail(
|
|
1920
|
-
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
1923
|
-
if (board.lifecycle?.mode !== "managed") {
|
|
1924
|
-
const now = /* @__PURE__ */ new Date();
|
|
1925
|
-
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1926
|
-
status: "running",
|
|
1927
|
-
agentId: input.agentId ?? input.author,
|
|
1928
|
-
leaseId: input.leaseId ?? randomUUID2(),
|
|
1929
|
-
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1930
|
-
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1931
|
-
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1932
|
-
attempt: input.attempt ?? 1,
|
|
1933
|
-
maxAttempts: input.maxAttempts ?? 3
|
|
1934
|
-
});
|
|
1935
|
-
if (!assigned) return fail("Task assignment could not be started.");
|
|
1936
|
-
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
1937
|
-
status: "in_progress"
|
|
1938
|
-
});
|
|
1939
|
-
const current = started ?? assigned;
|
|
1940
|
-
const claimed = task;
|
|
1941
|
-
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
1942
|
-
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
1943
|
-
return okTask(
|
|
1944
|
-
current,
|
|
1945
|
-
currentTask,
|
|
1946
|
-
"Task is active and bound to this run for attribution. This board is not in managed lifecycle mode, so runtime Kanban governance was not bound to it."
|
|
1947
|
-
);
|
|
1948
|
-
}
|
|
1949
|
-
let stage = task.lifecycle?.currentStage;
|
|
1950
|
-
if (stage === "backlog") {
|
|
1951
|
-
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1952
|
-
to: "todo",
|
|
1953
|
-
actor: input.author,
|
|
1954
|
-
comment: input.transitionComment
|
|
1955
|
-
});
|
|
1956
|
-
if (!moved) return fail("Task could not enter Todo.");
|
|
1957
|
-
board = moved.board;
|
|
1958
|
-
task = moved.task;
|
|
1959
|
-
stage = task.lifecycle?.currentStage;
|
|
1960
|
-
}
|
|
1961
|
-
if (stage === "todo" || stage === "review") {
|
|
1962
|
-
const now = /* @__PURE__ */ new Date();
|
|
1963
|
-
const leaseId = input.leaseId ?? randomUUID2();
|
|
1964
|
-
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1965
|
-
status: "running",
|
|
1966
|
-
agentId: input.agentId ?? input.author,
|
|
1967
|
-
leaseId,
|
|
1968
|
-
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1969
|
-
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1970
|
-
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1971
|
-
attempt: input.attempt ?? 1,
|
|
1972
|
-
maxAttempts: input.maxAttempts ?? 3
|
|
1973
|
-
});
|
|
1974
|
-
if (!assigned) return fail("Task assignment could not be started.");
|
|
1975
|
-
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1976
|
-
to: "running",
|
|
1977
|
-
actor: input.author,
|
|
1978
|
-
comment: input.transitionComment
|
|
1979
|
-
});
|
|
1980
|
-
if (!moved) return fail("Task could not enter Running.");
|
|
1981
|
-
board = moved.board;
|
|
1982
|
-
task = moved.task;
|
|
1983
|
-
stage = task.lifecycle?.currentStage;
|
|
1984
|
-
}
|
|
1985
|
-
if (stage !== "running" || task.assignment?.status !== "running") {
|
|
1986
|
-
return fail(
|
|
1987
|
-
`start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
|
|
1988
|
-
);
|
|
1989
|
-
}
|
|
1990
|
-
ctx.setCurrentKanbanTask(task.id, board.id);
|
|
1991
|
-
return okTask(
|
|
1992
|
-
board,
|
|
1993
|
-
task,
|
|
1994
|
-
"Task is active; runtime Kanban governance is now bound to this run."
|
|
1995
|
-
);
|
|
1996
|
-
}
|
|
1997
|
-
case "update_task": {
|
|
1998
|
-
if (!input.boardId || !input.taskId)
|
|
1999
|
-
return fail("update_task requires boardId and taskId.");
|
|
2000
|
-
const board = await updateTask2(
|
|
2001
|
-
projectRoot,
|
|
2002
|
-
input.boardId,
|
|
2003
|
-
input.taskId,
|
|
2004
|
-
taskPatch(input)
|
|
2005
|
-
);
|
|
2006
|
-
return board ? okBoard(board, "Task updated.") : fail("Task not found.");
|
|
2007
|
-
}
|
|
2008
|
-
case "transition_task": {
|
|
2009
|
-
if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
|
|
2010
|
-
return fail(
|
|
2011
|
-
"transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
|
|
2012
|
-
);
|
|
2013
|
-
}
|
|
2014
|
-
if (input.lifecycleStage === "done") {
|
|
2015
|
-
const boardBefore = await getBoard3(projectRoot, input.boardId);
|
|
2016
|
-
const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
|
|
2017
|
-
if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
|
|
2018
|
-
const preGate = await verifyTaskCompletion2(
|
|
2019
|
-
projectRoot,
|
|
2020
|
-
input.boardId,
|
|
2021
|
-
taskBefore.id,
|
|
2022
|
-
{
|
|
2023
|
-
persist: false
|
|
2024
|
-
}
|
|
2025
|
-
);
|
|
2026
|
-
await updateTask2(projectRoot, input.boardId, taskBefore.id, {
|
|
2027
|
-
verificationReport: preGate.report,
|
|
2028
|
-
successCriteria: preGate.task.successCriteria
|
|
2029
|
-
});
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
|
|
2033
|
-
to: input.lifecycleStage,
|
|
2034
|
-
actor: input.author,
|
|
2035
|
-
comment: input.transitionComment,
|
|
2036
|
-
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
2037
|
-
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
2038
|
-
...input.attachmentUrl !== void 0 ? {
|
|
2039
|
-
attachment: {
|
|
2040
|
-
url: input.attachmentUrl,
|
|
2041
|
-
type: input.attachmentType ?? "url",
|
|
2042
|
-
...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
|
|
2043
|
-
}
|
|
2044
|
-
} : {},
|
|
2045
|
-
patch: taskPatch(input)
|
|
2046
|
-
});
|
|
2047
|
-
if (result2 && input.lifecycleStage === "done" && result2.task.verificationReport) {
|
|
2048
|
-
recordKanbanVerificationEvidence(ctx, result2.task.verificationReport);
|
|
2049
|
-
}
|
|
2050
|
-
return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
|
|
2051
|
-
}
|
|
2052
|
-
case "repair_managed_projection": {
|
|
2053
|
-
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
2054
|
-
return fail(
|
|
2055
|
-
"repair_managed_projection requires boardId, taskId, author, and transitionComment."
|
|
2056
|
-
);
|
|
2057
|
-
}
|
|
2058
|
-
const result2 = await repairManagedTaskProjection(
|
|
2059
|
-
projectRoot,
|
|
2060
|
-
input.boardId,
|
|
2061
|
-
input.taskId,
|
|
2062
|
-
{
|
|
2063
|
-
actor: input.author,
|
|
2064
|
-
comment: input.transitionComment
|
|
2065
|
-
}
|
|
2066
|
-
);
|
|
2067
|
-
return result2 ? okTask(
|
|
2068
|
-
result2.board,
|
|
2069
|
-
result2.task,
|
|
2070
|
-
"Managed card projection repaired from lifecycle history."
|
|
2071
|
-
) : fail("Board or task not found.");
|
|
2072
|
-
}
|
|
2073
|
-
case "move_task": {
|
|
2074
|
-
if (!input.boardId || !input.taskId || !input.targetColumnId) {
|
|
2075
|
-
return fail("move_task requires boardId, taskId, and targetColumnId.");
|
|
2076
|
-
}
|
|
2077
|
-
const board = await moveTask(
|
|
2078
|
-
projectRoot,
|
|
2079
|
-
input.boardId,
|
|
2080
|
-
input.taskId,
|
|
2081
|
-
input.targetColumnId,
|
|
2082
|
-
input.order
|
|
2083
|
-
);
|
|
2084
|
-
return board ? okBoard(board, "Task moved.") : fail("Move failed.");
|
|
2085
|
-
}
|
|
2086
|
-
case "delete_task": {
|
|
2087
|
-
if (!input.boardId || !input.taskId)
|
|
2088
|
-
return fail("delete_task requires boardId and taskId.");
|
|
2089
|
-
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
2090
|
-
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
2091
|
-
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
2092
|
-
}
|
|
2093
|
-
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
2094
|
-
}
|
|
2095
|
-
case "set_chain": {
|
|
2096
|
-
if (!input.boardId || !input.taskIds?.length) {
|
|
2097
|
-
return fail("set_chain requires boardId and taskIds.");
|
|
2098
|
-
}
|
|
2099
|
-
const result2 = await setTaskChain(projectRoot, input.boardId, {
|
|
2100
|
-
taskIds: input.taskIds,
|
|
2101
|
-
...input.chainId !== void 0 ? { chainId: input.chainId } : {},
|
|
2102
|
-
...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
|
|
2103
|
-
});
|
|
2104
|
-
return result2 ? {
|
|
2105
|
-
ok: true,
|
|
2106
|
-
message: `Chain set: ${result2.chainId}`,
|
|
2107
|
-
board: result2.board,
|
|
2108
|
-
chain: result2.tasks
|
|
2109
|
-
} : fail("Board or task not found.");
|
|
2110
|
-
}
|
|
2111
|
-
case "get_chain": {
|
|
2112
|
-
if (!input.boardId || !(input.taskId || input.chainId)) {
|
|
2113
|
-
return fail("get_chain requires boardId and taskId or chainId.");
|
|
2114
|
-
}
|
|
2115
|
-
const result2 = await getTaskChain(
|
|
2116
|
-
projectRoot,
|
|
2117
|
-
input.boardId,
|
|
2118
|
-
input.taskId ?? input.chainId ?? ""
|
|
2119
|
-
);
|
|
2120
|
-
return result2 ? {
|
|
2121
|
-
ok: true,
|
|
2122
|
-
message: `Chain loaded: ${result2.chainId}`,
|
|
2123
|
-
board: result2.board,
|
|
2124
|
-
chain: result2.tasks
|
|
2125
|
-
} : fail("Chain not found.");
|
|
2126
|
-
}
|
|
2127
|
-
case "claim_task": {
|
|
2128
|
-
const result2 = await claimReadyTask(projectRoot, {
|
|
2129
|
-
...input.boardId !== void 0 ? { boardId: input.boardId } : {},
|
|
2130
|
-
...input.taskId !== void 0 ? { taskId: input.taskId } : {},
|
|
2131
|
-
...assignmentInput(input),
|
|
2132
|
-
status: input.assignmentStatus ?? "queued"
|
|
2133
|
-
});
|
|
2134
|
-
return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
|
|
2135
|
-
}
|
|
2136
|
-
case "release_task": {
|
|
2137
|
-
if (!input.boardId || !input.taskId) {
|
|
2138
|
-
return fail("release_task requires boardId and taskId.");
|
|
2139
|
-
}
|
|
2140
|
-
const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
|
|
2141
|
-
...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
|
|
2142
|
-
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
2143
|
-
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
|
|
2144
|
-
});
|
|
2145
|
-
return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
|
|
2146
|
-
}
|
|
2147
|
-
case "assign_task": {
|
|
2148
|
-
if (!input.boardId || !input.taskId)
|
|
2149
|
-
return fail("assign_task requires boardId and taskId.");
|
|
2150
|
-
const board = await assignTask(
|
|
2151
|
-
projectRoot,
|
|
2152
|
-
input.boardId,
|
|
2153
|
-
input.taskId,
|
|
2154
|
-
assignmentInput(input)
|
|
2155
|
-
);
|
|
2156
|
-
return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
|
|
2157
|
-
}
|
|
2158
|
-
case "mark_assignment": {
|
|
2159
|
-
if (!input.boardId || !input.taskId)
|
|
2160
|
-
return fail("mark_assignment requires boardId and taskId.");
|
|
2161
|
-
const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
|
|
2162
|
-
const board = await updateTaskAssignment(
|
|
2163
|
-
projectRoot,
|
|
2164
|
-
input.boardId,
|
|
2165
|
-
input.taskId,
|
|
2166
|
-
{
|
|
2167
|
-
...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
|
|
2168
|
-
...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
|
|
2169
|
-
...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
|
|
2170
|
-
...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
|
|
2171
|
-
...input.error !== void 0 ? { error: input.error } : {},
|
|
2172
|
-
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
2173
|
-
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
2174
|
-
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
2175
|
-
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
2176
|
-
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
2177
|
-
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
2178
|
-
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
|
|
2179
|
-
},
|
|
2180
|
-
// Ownership fence: when expectedLeaseId is supplied, the write is
|
|
2181
|
-
// applied only if the current assignment still holds this lease.
|
|
2182
|
-
// This prevents a recovered+reassigned stale worker's terminal
|
|
2183
|
-
// mark_assignment from overwriting the successor's state. The check
|
|
2184
|
-
// is atomic inside updateTaskAssignment's mutateBoard lock.
|
|
2185
|
-
input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
2186
|
-
);
|
|
2187
|
-
if (!board) return fail("Task not found.");
|
|
2188
|
-
if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
|
|
2189
|
-
const envGate = readEnvGateEnforcement();
|
|
2190
|
-
const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
|
|
2191
|
-
...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
|
|
2192
|
-
...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
|
|
2193
|
-
});
|
|
2194
|
-
if (finalized) {
|
|
2195
|
-
if (finalized.gate.report) {
|
|
2196
|
-
recordKanbanVerificationEvidence(ctx, finalized.gate.report);
|
|
2197
|
-
}
|
|
2198
|
-
const gateSummary = {
|
|
2199
|
-
enforcement: finalized.gate.enforcement,
|
|
2200
|
-
allowed: finalized.gate.allowed,
|
|
2201
|
-
verdict: finalized.gate.verdict,
|
|
2202
|
-
issues: finalized.gate.issues.map((issue) => issue.message)
|
|
2203
|
-
};
|
|
2204
|
-
const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "skipped" : "passed"}; task completed.` : finalized.gate.enforcement === "strict" ? `Completion gate BLOCKED (verdict: ${finalized.gate.verdict}); task parked in review. Issues: ${gateSummary.issues.join(" | ")}` : `Completion gate failed softly (verdict: ${finalized.gate.verdict}); task completed with warnings. Issues: ${gateSummary.issues.join(" | ")}`;
|
|
2205
|
-
return {
|
|
2206
|
-
...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
|
|
2207
|
-
gate: gateSummary
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
} else if (board.lifecycle?.mode === "managed") {
|
|
2211
|
-
const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
|
|
2212
|
-
const stage = managedTask?.lifecycle?.currentStage;
|
|
2213
|
-
const actor = ctx.agentId ?? "kanban-agent";
|
|
2214
|
-
let transitionResult = null;
|
|
2215
|
-
const lifecycleWarnings = [];
|
|
2216
|
-
if (assignmentStatus === "running" && stage === "todo") {
|
|
2217
|
-
try {
|
|
2218
|
-
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
2219
|
-
to: "running",
|
|
2220
|
-
actor,
|
|
2221
|
-
comment: "Work started."
|
|
2222
|
-
});
|
|
2223
|
-
} catch (err) {
|
|
2224
|
-
lifecycleWarnings.push(
|
|
2225
|
-
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
2226
|
-
);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
if (assignmentStatus === "completed" && stage === "running") {
|
|
2230
|
-
const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
|
|
2231
|
-
try {
|
|
2232
|
-
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
2233
|
-
to: "review",
|
|
2234
|
-
actor,
|
|
2235
|
-
comment,
|
|
2236
|
-
attachment: {
|
|
2237
|
-
url: `kanban://task/${input.taskId}/result`,
|
|
2238
|
-
title: "Worker completion result",
|
|
2239
|
-
type: "file"
|
|
2240
|
-
},
|
|
2241
|
-
patch: {
|
|
2242
|
-
// Only patch non-description fields so the
|
|
2243
|
-
// original card description is preserved.
|
|
2244
|
-
...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
|
|
2245
|
-
}
|
|
2246
|
-
});
|
|
2247
|
-
} catch (err) {
|
|
2248
|
-
lifecycleWarnings.push(
|
|
2249
|
-
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
2250
|
-
);
|
|
2251
|
-
}
|
|
2252
|
-
if (transitionResult) {
|
|
2253
|
-
const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
|
|
2254
|
-
if (hasCriteria) {
|
|
2255
|
-
try {
|
|
2256
|
-
const verResult = await verifyTaskCompletion2(
|
|
2257
|
-
projectRoot,
|
|
2258
|
-
board.id,
|
|
2259
|
-
input.taskId
|
|
2260
|
-
);
|
|
2261
|
-
if (verResult.report) {
|
|
2262
|
-
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
2263
|
-
}
|
|
2264
|
-
await updateTask2(projectRoot, board.id, input.taskId, {
|
|
2265
|
-
verificationReport: verResult.report,
|
|
2266
|
-
successCriteria: verResult.task.successCriteria
|
|
2267
|
-
});
|
|
2268
|
-
const verdict = verResult.report.verdict;
|
|
2269
|
-
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
2270
|
-
lifecycleWarnings.push(
|
|
2271
|
-
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
2272
|
-
);
|
|
2273
|
-
} else if (verdict === "passed") {
|
|
2274
|
-
try {
|
|
2275
|
-
const doneResult = await transitionTask(
|
|
2276
|
-
projectRoot,
|
|
2277
|
-
board.id,
|
|
2278
|
-
input.taskId,
|
|
2279
|
-
{
|
|
2280
|
-
to: "done",
|
|
2281
|
-
actor,
|
|
2282
|
-
action: "Automated acceptance after verification",
|
|
2283
|
-
comment: "Auto-accepted: verification passed.",
|
|
2284
|
-
attachment: {
|
|
2285
|
-
url: `kanban://task/${input.taskId}/verification`,
|
|
2286
|
-
title: "Auto-verification result",
|
|
2287
|
-
type: "file"
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
);
|
|
2291
|
-
transitionResult = doneResult;
|
|
2292
|
-
} catch (acceptErr) {
|
|
2293
|
-
lifecycleWarnings.push(
|
|
2294
|
-
`Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
|
|
2295
|
-
);
|
|
2296
|
-
}
|
|
2297
|
-
} else {
|
|
2298
|
-
lifecycleWarnings.push(
|
|
2299
|
-
`Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
|
|
2300
|
-
);
|
|
2301
|
-
}
|
|
2302
|
-
} catch (verifyErr) {
|
|
2303
|
-
lifecycleWarnings.push(
|
|
2304
|
-
`Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
|
|
2305
|
-
);
|
|
2306
|
-
}
|
|
2307
|
-
} else {
|
|
2308
|
-
lifecycleWarnings.push(
|
|
2309
|
-
"No automatic success criteria \u2014 card left in Review for manual verification."
|
|
2310
|
-
);
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
const responseBoard = transitionResult?.board ?? board;
|
|
2315
|
-
const responseTask = transitionResult?.task ?? managedTask;
|
|
2316
|
-
const msgParts = ["Assignment updated."];
|
|
2317
|
-
if (transitionResult) {
|
|
2318
|
-
msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
|
|
2319
|
-
}
|
|
2320
|
-
for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
|
|
2321
|
-
return okTask(responseBoard, responseTask, msgParts.join(" "));
|
|
2322
|
-
}
|
|
2323
|
-
return okBoard(board, "Assignment updated.");
|
|
2324
|
-
}
|
|
2325
|
-
case "heartbeat_assignment": {
|
|
2326
|
-
if (!input.boardId || !input.taskId) {
|
|
2327
|
-
return fail("heartbeat_assignment requires boardId and taskId.");
|
|
2328
|
-
}
|
|
2329
|
-
const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
|
|
2330
|
-
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
2331
|
-
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
2332
|
-
// Ownership fence: when expectedLeaseId is supplied, the renewal
|
|
2333
|
-
// is applied only if the current assignment still holds this lease.
|
|
2334
|
-
// This prevents a recovered+reassigned stale worker's heartbeat
|
|
2335
|
-
// from renewing the successor's lease. The check is atomic inside
|
|
2336
|
-
// heartbeatTaskAssignment's mutateBoard lock.
|
|
2337
|
-
...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
2338
|
-
});
|
|
2339
|
-
return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
|
|
2340
|
-
}
|
|
2341
|
-
case "recover_stale": {
|
|
2342
|
-
if (!input.boardId) return fail("recover_stale requires boardId.");
|
|
2343
|
-
const policyFields = [
|
|
2344
|
-
input.recoveryPolicyFailOnCostCeiling !== void 0,
|
|
2345
|
-
input.recoveryPolicyReleaseOnFailureKinds !== void 0,
|
|
2346
|
-
input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
|
|
2347
|
-
input.recoveryPolicyRetryPolicyOverride !== void 0
|
|
2348
|
-
].some(Boolean);
|
|
2349
|
-
const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
|
|
2350
|
-
...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
|
|
2351
|
-
...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
|
|
2352
|
-
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
2353
|
-
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
|
|
2354
|
-
...policyFields ? {
|
|
2355
|
-
policy: {
|
|
2356
|
-
...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
|
|
2357
|
-
...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
|
|
2358
|
-
releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
|
|
2359
|
-
} : {},
|
|
2360
|
-
...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
|
|
2361
|
-
releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
|
|
2362
|
-
} : {},
|
|
2363
|
-
...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
|
|
2364
|
-
retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
|
|
2365
|
-
} : {}
|
|
2366
|
-
}
|
|
2367
|
-
} : {}
|
|
2368
|
-
});
|
|
2369
|
-
return result2 ? {
|
|
2370
|
-
ok: true,
|
|
2371
|
-
message: `Recovered ${result2.tasks.length} stale assignment(s).`,
|
|
2372
|
-
board: result2.board,
|
|
2373
|
-
recoveredTasks: result2.tasks
|
|
2374
|
-
} : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
|
|
2375
|
-
}
|
|
2376
|
-
case "events": {
|
|
2377
|
-
if (!input.boardId) return fail("events requires boardId.");
|
|
2378
|
-
const eventList = await listKanbanEvents(projectRoot, input.boardId);
|
|
2379
|
-
return {
|
|
2380
|
-
ok: true,
|
|
2381
|
-
message: `${eventList.length} event(s).`,
|
|
2382
|
-
events: eventList
|
|
2383
|
-
};
|
|
2384
|
-
}
|
|
2385
|
-
case "queue_health": {
|
|
2386
|
-
const health = await getKanbanQueueHealth(projectRoot, {
|
|
2387
|
-
...input.boardId !== void 0 ? { boardId: input.boardId } : {}
|
|
2388
|
-
});
|
|
2389
|
-
return {
|
|
2390
|
-
ok: true,
|
|
2391
|
-
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
2392
|
-
queueHealth: health
|
|
2393
|
-
};
|
|
2394
|
-
}
|
|
2395
|
-
// Not every action is handled above. These are dispatched from here,
|
|
2396
|
-
// and the split has already cost real time: an agent that read this
|
|
2397
|
-
// file concluded `add_check` / `update_check` did not exist, wrote
|
|
2398
|
-
// that on a card, and spent a session trying to satisfy a gate it
|
|
2399
|
-
// already had the tool to clear. Keep this index in step with the
|
|
2400
|
-
// handlers.
|
|
2401
|
-
//
|
|
2402
|
-
// kanban-detail-actions.ts workbench · add_dependency ·
|
|
2403
|
-
// add_goal_metric · update_goal_metric · add_check ·
|
|
2404
|
-
// update_check · add_note · add_link · split_atomic
|
|
2405
|
-
// kanban-decomposition-actions.ts verify_completion ·
|
|
2406
|
-
// assess_atomicity · propose_decomposition
|
|
2407
|
-
// kanban-contract-actions.ts get_contract_graph ·
|
|
2408
|
-
// configure_contract_graph · upsert_contract_node ·
|
|
2409
|
-
// remove_contract_node · add_contract_edge · remove_contract_edge
|
|
2410
|
-
default:
|
|
2411
|
-
{
|
|
2412
|
-
const contractResult = await handleKanbanContractAction(
|
|
2413
|
-
projectRoot,
|
|
2414
|
-
input,
|
|
2415
|
-
input.author ?? input.agentId
|
|
2416
|
-
);
|
|
2417
|
-
if (contractResult !== void 0) return contractResult;
|
|
2418
|
-
}
|
|
2419
|
-
{
|
|
2420
|
-
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
2421
|
-
if (detailResult !== void 0) return detailResult;
|
|
2422
|
-
}
|
|
2423
|
-
return fail(`Unknown kanban action: ${input.action}`);
|
|
2424
|
-
}
|
|
2403
|
+
const boardResult = await handleKanbanBoardAction(projectRoot, input, ctx);
|
|
2404
|
+
if (boardResult !== void 0) return boardResult;
|
|
2405
|
+
const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, input, ctx);
|
|
2406
|
+
if (lifecycleResult !== void 0) return lifecycleResult;
|
|
2407
|
+
const contractResult = await handleKanbanContractAction(
|
|
2408
|
+
projectRoot,
|
|
2409
|
+
input,
|
|
2410
|
+
input.author ?? input.agentId
|
|
2411
|
+
);
|
|
2412
|
+
if (contractResult !== void 0) return contractResult;
|
|
2413
|
+
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
2414
|
+
if (detailResult !== void 0) return detailResult;
|
|
2415
|
+
return fail(`Unknown kanban action: ${input.action}`);
|
|
2425
2416
|
})();
|
|
2426
2417
|
return withPresence(result);
|
|
2427
2418
|
} catch (err) {
|
|
2428
|
-
return fail(
|
|
2419
|
+
return fail(stripLifecycleIssues2(err instanceof Error ? err.message : String(err)));
|
|
2429
2420
|
}
|
|
2430
2421
|
},
|
|
2431
2422
|
serialize(output, input) {
|
|
2432
2423
|
return serializeKanbanOutput(output, input);
|
|
2433
2424
|
}
|
|
2434
2425
|
};
|
|
2435
|
-
var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
|
|
2436
|
-
var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
2437
|
-
"get_board",
|
|
2438
|
-
"export_markdown",
|
|
2439
|
-
"export_task_graph"
|
|
2440
|
-
]);
|
|
2441
|
-
function serializeKanbanOutput(output, input) {
|
|
2442
|
-
const action = input && typeof input === "object" ? input.action : void 0;
|
|
2443
|
-
const board = output.board;
|
|
2444
|
-
if (board) {
|
|
2445
|
-
const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
|
|
2446
|
-
let boardBytes = 0;
|
|
2447
|
-
if (!keepFull) {
|
|
2448
|
-
try {
|
|
2449
|
-
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
2450
|
-
} catch {
|
|
2451
|
-
boardBytes = 0;
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
2455
|
-
const columns = {};
|
|
2456
|
-
for (const column of board.columns) {
|
|
2457
|
-
columns[column.title || column.id] = board.tasks.filter(
|
|
2458
|
-
(task) => task.columnId === column.id
|
|
2459
|
-
).length;
|
|
2460
|
-
}
|
|
2461
|
-
const compact = {
|
|
2462
|
-
...output,
|
|
2463
|
-
board: {
|
|
2464
|
-
id: board.id,
|
|
2465
|
-
title: board.title,
|
|
2466
|
-
columns,
|
|
2467
|
-
totalTasks: board.tasks.length,
|
|
2468
|
-
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
2469
|
-
}
|
|
2470
|
-
};
|
|
2471
|
-
return JSON.stringify(compact, null, 2);
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
return JSON.stringify(output, null, 2);
|
|
2475
|
-
}
|
|
2476
2426
|
|
|
2477
2427
|
// src/todo.ts
|
|
2478
2428
|
function normalizedTitle(value) {
|
|
@@ -2530,7 +2480,7 @@ async function createMissingManagedCards(items, board, ctx, warnings) {
|
|
|
2530
2480
|
for (const item of items) {
|
|
2531
2481
|
if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
|
|
2532
2482
|
try {
|
|
2533
|
-
const result = await
|
|
2483
|
+
const result = await addTask3(ctx.projectRoot, board.id, {
|
|
2534
2484
|
title: item.content,
|
|
2535
2485
|
description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
|
|
2536
2486
|
});
|
|
@@ -2624,7 +2574,7 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2624
2574
|
});
|
|
2625
2575
|
}
|
|
2626
2576
|
const attemptedParents = /* @__PURE__ */ new Set();
|
|
2627
|
-
let afterCompletions = await
|
|
2577
|
+
let afterCompletions = await getBoard5(ctx.projectRoot, board.id);
|
|
2628
2578
|
while (afterCompletions) {
|
|
2629
2579
|
const parent = afterCompletions.tasks.find(
|
|
2630
2580
|
(task) => task.atomic === true && task.status !== "completed" && Boolean(task.childTaskIds?.length) && !attemptedParents.has(task.id) && task.childTaskIds?.every(
|
|
@@ -2651,7 +2601,7 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2651
2601
|
lastResult: "All child tasks completed; composite result ready for verification."
|
|
2652
2602
|
});
|
|
2653
2603
|
}
|
|
2654
|
-
afterCompletions = await
|
|
2604
|
+
afterCompletions = await getBoard5(ctx.projectRoot, board.id);
|
|
2655
2605
|
}
|
|
2656
2606
|
const completionPending = items.some(
|
|
2657
2607
|
(item) => item.status === "completed" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId) && afterCompletions?.tasks.find((task) => task.id === item.kanbanTaskId)?.status !== "completed"
|
|
@@ -2764,7 +2714,7 @@ var todoTool = {
|
|
|
2764
2714
|
}
|
|
2765
2715
|
}
|
|
2766
2716
|
const boardId = activeBoardId(items, ctx);
|
|
2767
|
-
let board = boardId ? await
|
|
2717
|
+
let board = boardId ? await getBoard5(ctx.projectRoot, boardId) : null;
|
|
2768
2718
|
const managed = board?.lifecycle?.mode === "managed";
|
|
2769
2719
|
let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
2770
2720
|
const creationWarnings = [];
|
|
@@ -2776,7 +2726,7 @@ var todoTool = {
|
|
|
2776
2726
|
const taskId = created.get(item.id);
|
|
2777
2727
|
return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
|
|
2778
2728
|
});
|
|
2779
|
-
board = await
|
|
2729
|
+
board = await getBoard5(ctx.projectRoot, managedBoardId) ?? board;
|
|
2780
2730
|
boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
|
|
2781
2731
|
}
|
|
2782
2732
|
boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
|
|
@@ -2798,10 +2748,10 @@ var todoTool = {
|
|
|
2798
2748
|
if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
|
|
2799
2749
|
let projectedBoard = board;
|
|
2800
2750
|
if (managed && board) {
|
|
2801
|
-
const refreshed = await
|
|
2751
|
+
const refreshed = await getBoard5(ctx.projectRoot, board.id);
|
|
2802
2752
|
if (refreshed) {
|
|
2803
2753
|
projectedBoard = refreshed;
|
|
2804
|
-
|
|
2754
|
+
applyManagedKanbanBoardToTodos2(ctx, refreshed);
|
|
2805
2755
|
}
|
|
2806
2756
|
}
|
|
2807
2757
|
if (!managed) {
|