@wrongstack/tools 0.302.2 → 0.305.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin.d.ts +6 -0
- package/dist/builtin.js +3213 -686
- package/dist/codebase-index/binary-frame.d.ts +43 -0
- package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +1 -0
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +1 -0
- package/dist/codebase-index/content-hash.d.ts +66 -0
- package/dist/codebase-index/index.js +1597 -153
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/parser-worker-pool.d.ts +63 -0
- package/dist/codebase-index/parser-worker-script.d.ts +42 -0
- package/dist/codebase-index/project-server-protocol.d.ts +2 -0
- package/dist/codebase-index/project-server.js +1483 -104
- package/dist/codebase-index/schema.d.ts +18 -0
- package/dist/codebase-index/tree-sitter/queries.d.ts +48 -0
- package/dist/codebase-index/tree-sitter/util.d.ts +31 -0
- package/dist/codebase-index/tree-sitter/visitor.d.ts +47 -0
- package/dist/codebase-index/tree-sitter-parser.d.ts +58 -0
- package/dist/codebase-index/vector-search.d.ts +62 -0
- package/dist/codebase-index/worker-protocol.d.ts +2 -0
- package/dist/codebase-index/worker.js +1452 -73
- package/dist/codebase-index/writer-bulk-insert.d.ts +5 -0
- package/dist/codebase-index/writer-graph-reader.d.ts +39 -0
- package/dist/codebase-index/writer-schema.d.ts +9 -2
- package/dist/codebase-index/writer.d.ts +36 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3258 -727
- package/dist/kanban-contract-actions.d.ts +7 -0
- package/dist/kanban-task-inputs.d.ts +16 -2
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +39 -2
- package/dist/kanban.js +580 -193
- package/dist/pack.js +3212 -686
- package/dist/plan.d.ts +4 -1
- package/dist/plan.js +2701 -18
- package/dist/read.js +1559 -103
- package/dist/session-kanban.d.ts +94 -1
- package/dist/session-kanban.js +308 -44
- package/dist/task.d.ts +5 -4
- package/dist/task.js +2825 -138
- package/dist/todo.d.ts +10 -1
- package/dist/todo.js +2474 -30
- package/dist/tool-tier.js +3212 -686
- package/package.json +8 -4
package/dist/task.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
// src/task.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "@wrongstack/core/utils";
|
|
6
|
-
import { mutateTasks as mutateTasks2 } from "@wrongstack/core/storage";
|
|
7
|
-
import {
|
|
8
|
-
addPlanItem,
|
|
9
|
-
mutatePlan as mutatePlan2,
|
|
10
|
-
formatPlan
|
|
11
|
-
} from "@wrongstack/core/storage";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3
|
+
import { addPlanItem, formatPlan, mutatePlan as mutatePlan2, mutateTasks as mutateTasks2 } from "@wrongstack/core/storage";
|
|
4
|
+
import { computeTaskItemProgress, formatTaskList } from "@wrongstack/core/utils";
|
|
13
5
|
|
|
14
6
|
// src/session-kanban.ts
|
|
15
7
|
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
@@ -20,12 +12,17 @@ import {
|
|
|
20
12
|
mutateTasks
|
|
21
13
|
} from "@wrongstack/core/storage";
|
|
22
14
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
23
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
15
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
24
16
|
import {
|
|
25
17
|
bridgeKanbanSupervisor,
|
|
18
|
+
compactSessionMirrorBoard,
|
|
26
19
|
createBoard,
|
|
20
|
+
DEFAULT_COLUMNS,
|
|
27
21
|
getBoard,
|
|
22
|
+
getDependencyReadinessIssues,
|
|
23
|
+
getKanbanOrchestrationSnapshot,
|
|
28
24
|
listBoards,
|
|
25
|
+
pruneSessionBoards,
|
|
29
26
|
removeBoard,
|
|
30
27
|
syncBoardFromTaskGraph,
|
|
31
28
|
touchKanbanPresence,
|
|
@@ -33,22 +30,48 @@ import {
|
|
|
33
30
|
} from "@wrongstack/kanban";
|
|
34
31
|
var SESSION_BOARD_TAG = "session-work";
|
|
35
32
|
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
36
|
-
var SESSION_KANBAN_COLUMNS =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
40
|
-
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
41
|
-
];
|
|
33
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
34
|
+
...column
|
|
35
|
+
}));
|
|
42
36
|
var boardQueue = /* @__PURE__ */ new Map();
|
|
43
37
|
var boardEnsures = /* @__PURE__ */ new Map();
|
|
44
38
|
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
45
39
|
var activeMirrors = /* @__PURE__ */ new Set();
|
|
40
|
+
var mirrorFailures = /* @__PURE__ */ new Map();
|
|
41
|
+
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
46
42
|
function boardKey(projectRoot, sessionId) {
|
|
47
43
|
return `${projectRoot}\0${sessionId}`;
|
|
48
44
|
}
|
|
49
45
|
function mirrorKey(projectRoot, sessionId, sourceSystem) {
|
|
50
46
|
return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
|
|
51
47
|
}
|
|
48
|
+
function completedReconciliationGraph(latest, candidates) {
|
|
49
|
+
const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
|
|
50
|
+
const carriedNodeIds = /* @__PURE__ */ new Set();
|
|
51
|
+
const completedNodes = candidates.flatMap(
|
|
52
|
+
(candidate) => candidate.nodes.filter((node) => {
|
|
53
|
+
if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
carriedNodeIds.add(node.id);
|
|
57
|
+
return true;
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
if (completedNodes.length === 0) return void 0;
|
|
61
|
+
const carriedRequirements = completedNodes.flatMap(
|
|
62
|
+
(node) => node.specRequirementId ? [node.specRequirementId] : []
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
...latest,
|
|
66
|
+
nodes: [...latest.nodes, ...completedNodes],
|
|
67
|
+
rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
|
|
68
|
+
...latest.requiredRequirementIds ? {
|
|
69
|
+
requiredRequirementIds: [
|
|
70
|
+
.../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
|
|
71
|
+
]
|
|
72
|
+
} : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
52
75
|
function sessionTag(sessionId) {
|
|
53
76
|
return `session:${sessionId}`;
|
|
54
77
|
}
|
|
@@ -112,108 +135,2633 @@ function enqueueBoardWork(projectRoot, sessionId, work) {
|
|
|
112
135
|
void tail.then(() => {
|
|
113
136
|
if (boardQueue.get(key) === tail) boardQueue.delete(key);
|
|
114
137
|
});
|
|
115
|
-
return result;
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
141
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
142
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
143
|
+
const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
|
|
144
|
+
if (!board) return null;
|
|
145
|
+
const result = await syncBoardFromTaskGraph(
|
|
146
|
+
projectRoot,
|
|
147
|
+
board.id,
|
|
148
|
+
deserializeTaskGraph(graph),
|
|
149
|
+
{
|
|
150
|
+
sourceSystem,
|
|
151
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
152
|
+
archiveMissingTasks: true,
|
|
153
|
+
includeCompletedTasks: true,
|
|
154
|
+
// The scope ledger stays declared and accurate, but it may not veto a
|
|
155
|
+
// projection. A session mirror reflects a tactical list that shrinks by
|
|
156
|
+
// design, and refusing the sync never protected the removed row — it
|
|
157
|
+
// froze the entire board, permanently, because the stored scope then
|
|
158
|
+
// outlived every later snapshot (`session-kanban.mirror-failed`).
|
|
159
|
+
// Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
|
|
160
|
+
// removed card on the board as `archived`, the reconciliation pass
|
|
161
|
+
// first walks vanished completed rows to Done, and the session journal
|
|
162
|
+
// remains the durable record.
|
|
163
|
+
allowRequirementScopeShrink: true
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
if (!result) return null;
|
|
167
|
+
const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
|
|
168
|
+
if (compacted?.removedTaskIds.length) {
|
|
169
|
+
return await getBoard(projectRoot, board.id) ?? result.board;
|
|
170
|
+
}
|
|
171
|
+
return result.board;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
175
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
|
|
176
|
+
const key = mirrorKey(projectRoot, sessionId, sourceSystem);
|
|
177
|
+
const previous = pendingMirrors.get(key);
|
|
178
|
+
const reconciliationGraph = previous ? completedReconciliationGraph(
|
|
179
|
+
graph,
|
|
180
|
+
[previous.reconciliationGraph, previous.graph].filter(
|
|
181
|
+
(candidate) => candidate !== void 0
|
|
182
|
+
)
|
|
183
|
+
) : void 0;
|
|
184
|
+
pendingMirrors.set(key, {
|
|
185
|
+
projectRoot,
|
|
186
|
+
sessionId,
|
|
187
|
+
graph,
|
|
188
|
+
...reconciliationGraph ? { reconciliationGraph } : {},
|
|
189
|
+
sourceSystem
|
|
190
|
+
});
|
|
191
|
+
if (activeMirrors.has(key)) return;
|
|
192
|
+
activeMirrors.add(key);
|
|
193
|
+
void (async () => {
|
|
194
|
+
try {
|
|
195
|
+
for (; ; ) {
|
|
196
|
+
const pending = pendingMirrors.get(key);
|
|
197
|
+
if (!pending) break;
|
|
198
|
+
pendingMirrors.delete(key);
|
|
199
|
+
try {
|
|
200
|
+
if (pending.reconciliationGraph) {
|
|
201
|
+
await projectGraph(
|
|
202
|
+
pending.projectRoot,
|
|
203
|
+
pending.sessionId,
|
|
204
|
+
pending.reconciliationGraph,
|
|
205
|
+
pending.sourceSystem
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
await projectGraph(
|
|
209
|
+
pending.projectRoot,
|
|
210
|
+
pending.sessionId,
|
|
211
|
+
pending.graph,
|
|
212
|
+
pending.sourceSystem
|
|
213
|
+
);
|
|
214
|
+
mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
|
|
218
|
+
message,
|
|
219
|
+
sourceSystem: pending.sourceSystem
|
|
220
|
+
});
|
|
221
|
+
console.warn(
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
level: "warn",
|
|
224
|
+
event: "session-kanban.mirror-failed",
|
|
225
|
+
sessionId: pending.sessionId,
|
|
226
|
+
sourceSystem: pending.sourceSystem,
|
|
227
|
+
message,
|
|
228
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} finally {
|
|
234
|
+
activeMirrors.delete(key);
|
|
235
|
+
const pending = pendingMirrors.get(key);
|
|
236
|
+
if (pending) {
|
|
237
|
+
pendingMirrors.delete(key);
|
|
238
|
+
queueLatestMirror(
|
|
239
|
+
pending.projectRoot,
|
|
240
|
+
pending.sessionId,
|
|
241
|
+
pending.graph,
|
|
242
|
+
pending.sourceSystem
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
}
|
|
248
|
+
function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
249
|
+
if (!projectRoot || !sessionId) return void 0;
|
|
250
|
+
const key = boardKey(projectRoot, sessionId);
|
|
251
|
+
const failure = mirrorFailures.get(key);
|
|
252
|
+
if (!failure) return void 0;
|
|
253
|
+
mirrorFailures.delete(key);
|
|
254
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
255
|
+
}
|
|
256
|
+
function todoListToSerializedGraph(todos, sessionId) {
|
|
257
|
+
const graphId = `todo:${sessionId}`;
|
|
258
|
+
const nodes = todos.map((todo, index) => ({
|
|
259
|
+
id: todo.id,
|
|
260
|
+
title: todo.content,
|
|
261
|
+
description: todo.activeForm ?? "",
|
|
262
|
+
type: "chore",
|
|
263
|
+
priority: "medium",
|
|
264
|
+
status: todo.status,
|
|
265
|
+
specRequirementId: `${graphId}:${todo.id}`,
|
|
266
|
+
createdAt: index,
|
|
267
|
+
updatedAt: index
|
|
268
|
+
}));
|
|
269
|
+
return {
|
|
270
|
+
id: graphId,
|
|
271
|
+
specId: graphId,
|
|
272
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
273
|
+
title: "Session todos",
|
|
274
|
+
nodes,
|
|
275
|
+
edges: [],
|
|
276
|
+
rootNodes: nodes.map((node) => node.id),
|
|
277
|
+
createdAt: 0,
|
|
278
|
+
updatedAt: 0
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
282
|
+
const graphId = `session:${sessionId}`;
|
|
283
|
+
const ids = new Set(tasks.map((task) => task.id));
|
|
284
|
+
const nodes = tasks.map((task, index) => ({
|
|
285
|
+
id: task.id,
|
|
286
|
+
title: task.title,
|
|
287
|
+
description: task.description ?? "",
|
|
288
|
+
type: task.type,
|
|
289
|
+
priority: task.priority,
|
|
290
|
+
status: task.status,
|
|
291
|
+
specRequirementId: `${graphId}:${task.id}`,
|
|
292
|
+
...task.assignee ? { assignee: task.assignee } : {},
|
|
293
|
+
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
294
|
+
createdAt: index,
|
|
295
|
+
updatedAt: index
|
|
296
|
+
}));
|
|
297
|
+
const edges = tasks.flatMap(
|
|
298
|
+
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
299
|
+
id: `${dependency}->${task.id}`,
|
|
300
|
+
from: dependency,
|
|
301
|
+
to: task.id,
|
|
302
|
+
type: "depends_on"
|
|
303
|
+
}))
|
|
304
|
+
);
|
|
305
|
+
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
306
|
+
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
307
|
+
return {
|
|
308
|
+
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
309
|
+
id: graphId,
|
|
310
|
+
specId: graphId,
|
|
311
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
312
|
+
title: "Session tasks",
|
|
313
|
+
nodes,
|
|
314
|
+
edges,
|
|
315
|
+
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
316
|
+
createdAt: 0,
|
|
317
|
+
updatedAt: 0
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function broadcastTodoUpdate(context, todos) {
|
|
321
|
+
const sessionId = context.session?.id ?? "";
|
|
322
|
+
if (!context.agentId || !sessionId) return;
|
|
323
|
+
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
324
|
+
for (const todo of todos) {
|
|
325
|
+
if (todo.status === "completed") statusCounts.completed++;
|
|
326
|
+
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
327
|
+
else statusCounts.pending++;
|
|
328
|
+
}
|
|
329
|
+
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
330
|
+
const mailbox = getSharedProjectMailbox(projectDir);
|
|
331
|
+
void mailbox.send({
|
|
332
|
+
from: context.agentId,
|
|
333
|
+
to: "*",
|
|
334
|
+
type: "status",
|
|
335
|
+
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
336
|
+
body: JSON.stringify({
|
|
337
|
+
kind: "kanban.todos.updated",
|
|
338
|
+
sessionId,
|
|
339
|
+
revision: context.state.revision,
|
|
340
|
+
todoCount: todos.length,
|
|
341
|
+
statusCounts
|
|
342
|
+
}),
|
|
343
|
+
priority: "normal",
|
|
344
|
+
senderSessionId: sessionId
|
|
345
|
+
}).catch(() => {
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function notifyTodoUpdate(context, todos) {
|
|
349
|
+
const summary = formatTodosForModel(todos);
|
|
350
|
+
const text = `[KANBAN TODO UPDATE]
|
|
351
|
+
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
352
|
+
${summary}
|
|
353
|
+
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.`;
|
|
354
|
+
const state = context.state;
|
|
355
|
+
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
356
|
+
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
357
|
+
}
|
|
358
|
+
if (typeof state.appendMessage === "function") {
|
|
359
|
+
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
363
|
+
queueLatestMirror(
|
|
364
|
+
projectRoot,
|
|
365
|
+
sessionId,
|
|
366
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
367
|
+
"session-todo"
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
371
|
+
queueLatestMirror(
|
|
372
|
+
projectRoot,
|
|
373
|
+
sessionId,
|
|
374
|
+
taskFileToSerializedGraph(tasks, sessionId),
|
|
375
|
+
"session-task"
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
function sourceStatus(task) {
|
|
379
|
+
if (task.status === "completed") return "completed";
|
|
380
|
+
if (task.status === "in_progress") return "in_progress";
|
|
381
|
+
if (task.status === "review") return "review";
|
|
382
|
+
if (task.status === "blocked") return "blocked";
|
|
383
|
+
if (task.status === "failed") return "failed";
|
|
384
|
+
return "pending";
|
|
385
|
+
}
|
|
386
|
+
function todoStatus(task) {
|
|
387
|
+
const status = sourceStatus(task);
|
|
388
|
+
if (status === "completed") return "completed";
|
|
389
|
+
if (status === "in_progress" || status === "review") return "in_progress";
|
|
390
|
+
return "pending";
|
|
391
|
+
}
|
|
392
|
+
function sessionTodoFromTask(task, board) {
|
|
393
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
394
|
+
return {
|
|
395
|
+
id: task.origin?.taskId ?? task.id,
|
|
396
|
+
content: task.title,
|
|
397
|
+
status: todoStatus(task),
|
|
398
|
+
...task.description ? { activeForm: task.description } : {},
|
|
399
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function managedTodoFromTask(task, board) {
|
|
403
|
+
return {
|
|
404
|
+
...sessionTodoFromTask(task, board),
|
|
405
|
+
kanbanBoardId: board.id,
|
|
406
|
+
kanbanTaskId: task.id
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function blockingTitles(board, task) {
|
|
410
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
411
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
412
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
413
|
+
return dependency.title;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
var PRIORITY_ORDER = {
|
|
417
|
+
critical: 0,
|
|
418
|
+
high: 1,
|
|
419
|
+
medium: 2,
|
|
420
|
+
low: 3
|
|
421
|
+
};
|
|
422
|
+
function orderTasksForTodos(board, tasks) {
|
|
423
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
424
|
+
const baseline = [...tasks].sort(
|
|
425
|
+
(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)
|
|
426
|
+
);
|
|
427
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
428
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
429
|
+
const emitted = [];
|
|
430
|
+
const done = /* @__PURE__ */ new Set();
|
|
431
|
+
while (remaining.size > 0) {
|
|
432
|
+
const ready = baseline.filter(
|
|
433
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
434
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
if (ready.length === 0) break;
|
|
438
|
+
for (const task of ready) {
|
|
439
|
+
remaining.delete(task.id);
|
|
440
|
+
done.add(task.id);
|
|
441
|
+
emitted.push(task);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
445
|
+
return emitted;
|
|
446
|
+
}
|
|
447
|
+
function sameTodos(left, right) {
|
|
448
|
+
return left.length === right.length && left.every((todo, index) => {
|
|
449
|
+
const candidate = right[index];
|
|
450
|
+
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && // Readiness is part of the projection: when a dependency completes,
|
|
451
|
+
// the rows are otherwise identical and the unblocking would never
|
|
452
|
+
// reach the model.
|
|
453
|
+
(candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function applyManagedKanbanBoardToTodos(context, board) {
|
|
457
|
+
const metaKanban = context.meta["kanban"];
|
|
458
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
459
|
+
const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
460
|
+
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
461
|
+
return [...context.todos];
|
|
462
|
+
}
|
|
463
|
+
const projectedTodos = orderTasksForTodos(
|
|
464
|
+
board,
|
|
465
|
+
board.tasks.filter(
|
|
466
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
467
|
+
)
|
|
468
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
469
|
+
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
470
|
+
suppressedTodoMirrors.add(context);
|
|
471
|
+
try {
|
|
472
|
+
context.state.replaceTodos(projectedTodos);
|
|
473
|
+
} finally {
|
|
474
|
+
suppressedTodoMirrors.delete(context);
|
|
475
|
+
}
|
|
476
|
+
notifyTodoUpdate(context, context.todos);
|
|
477
|
+
broadcastTodoUpdate(context, context.todos);
|
|
478
|
+
return [...context.todos];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/todo.ts
|
|
482
|
+
import {
|
|
483
|
+
loadPlan as loadPlan2,
|
|
484
|
+
loadTasks as loadTasks3,
|
|
485
|
+
savePlan,
|
|
486
|
+
saveTasks,
|
|
487
|
+
setPlanItemStatus
|
|
488
|
+
} from "@wrongstack/core/storage";
|
|
489
|
+
import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
490
|
+
|
|
491
|
+
// src/kanban.ts
|
|
492
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
493
|
+
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
494
|
+
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
495
|
+
import {
|
|
496
|
+
addTask,
|
|
497
|
+
adoptManagedLifecycle,
|
|
498
|
+
assignTask,
|
|
499
|
+
claimReadyTask,
|
|
500
|
+
copyTaskToBoard,
|
|
501
|
+
createBoard as createBoard2,
|
|
502
|
+
createBoardFromTaskGraph,
|
|
503
|
+
createBoardFromText,
|
|
504
|
+
duplicateBoard,
|
|
505
|
+
evaluateContractGraphReadiness,
|
|
506
|
+
exportBoardAsMarkdown,
|
|
507
|
+
exportBoardToTaskGraph,
|
|
508
|
+
finalizeTaskCompletion,
|
|
509
|
+
getBoard as getBoard3,
|
|
510
|
+
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
511
|
+
getKanbanQueueHealth,
|
|
512
|
+
getTask,
|
|
513
|
+
getTaskChain,
|
|
514
|
+
heartbeatTaskAssignment,
|
|
515
|
+
listBoards as listBoards2,
|
|
516
|
+
listKanbanEvents,
|
|
517
|
+
listReadyTasks,
|
|
518
|
+
mergeTasks,
|
|
519
|
+
moveTask,
|
|
520
|
+
parseLinesIntoTasks,
|
|
521
|
+
recoverStaleTaskAssignments,
|
|
522
|
+
releaseTaskClaim,
|
|
523
|
+
removeBoard as removeBoard2,
|
|
524
|
+
removeTask,
|
|
525
|
+
repairManagedTaskProjection,
|
|
526
|
+
resolveAutoAccept,
|
|
527
|
+
searchKanban,
|
|
528
|
+
setTaskChain,
|
|
529
|
+
stripLifecycleIssues,
|
|
530
|
+
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
531
|
+
transferTaskToBoard,
|
|
532
|
+
transitionTask,
|
|
533
|
+
updateBoard as updateBoard2,
|
|
534
|
+
updateTask as updateTask2,
|
|
535
|
+
updateTaskAssignment,
|
|
536
|
+
verifyTaskCompletion as verifyTaskCompletion2
|
|
537
|
+
} from "@wrongstack/kanban";
|
|
538
|
+
|
|
539
|
+
// src/kanban-board-inputs.ts
|
|
540
|
+
function agentSettableGate(enforcement) {
|
|
541
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
542
|
+
return { completionGate: { enforcement } };
|
|
543
|
+
}
|
|
544
|
+
function boardCreateInput(input, title) {
|
|
545
|
+
return {
|
|
546
|
+
title,
|
|
547
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
548
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
549
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
550
|
+
...input.atomicityMode !== void 0 ? {
|
|
551
|
+
atomicity: {
|
|
552
|
+
mode: input.atomicityMode,
|
|
553
|
+
decomposition: input.atomicityDecomposition ?? "propose"
|
|
554
|
+
}
|
|
555
|
+
} : {},
|
|
556
|
+
...agentSettableGate(input.gateEnforcement)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function boardUpdatePatch(input) {
|
|
560
|
+
return {
|
|
561
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
562
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
563
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
564
|
+
...input.atomicityMode !== void 0 ? {
|
|
565
|
+
atomicity: {
|
|
566
|
+
mode: input.atomicityMode,
|
|
567
|
+
decomposition: input.atomicityDecomposition ?? "propose"
|
|
568
|
+
}
|
|
569
|
+
} : {},
|
|
570
|
+
...agentSettableGate(input.gateEnforcement)
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function duplicateBoardOptions(input) {
|
|
574
|
+
return {
|
|
575
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
576
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
577
|
+
...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
|
|
578
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
579
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/kanban-contract-actions.ts
|
|
584
|
+
import {
|
|
585
|
+
addContractEdge,
|
|
586
|
+
configureContractGraph,
|
|
587
|
+
evaluateTaskContractGraph,
|
|
588
|
+
getContractGraph,
|
|
589
|
+
removeContractEdge,
|
|
590
|
+
removeContractNode,
|
|
591
|
+
upsertContractNode
|
|
592
|
+
} from "@wrongstack/kanban";
|
|
593
|
+
|
|
594
|
+
// src/kanban-tool-results.ts
|
|
595
|
+
function atomicityNudge(task) {
|
|
596
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
597
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
598
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
599
|
+
}
|
|
600
|
+
function readEnvGateEnforcement() {
|
|
601
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
602
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
603
|
+
}
|
|
604
|
+
function fail(message) {
|
|
605
|
+
return { ok: false, message };
|
|
606
|
+
}
|
|
607
|
+
function okBoard(board, message = "Board loaded.") {
|
|
608
|
+
return { ok: true, message, board };
|
|
609
|
+
}
|
|
610
|
+
function okTask(board, task, message) {
|
|
611
|
+
return { ok: true, message, board, task };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/kanban-contract-actions.ts
|
|
615
|
+
async function handleKanbanContractAction(projectRoot, input, actor) {
|
|
616
|
+
switch (input.action) {
|
|
617
|
+
case "get_contract_graph": {
|
|
618
|
+
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
619
|
+
const found = await getContractGraph(projectRoot, input.boardId);
|
|
620
|
+
if (!found) return fail("Board not found.");
|
|
621
|
+
const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
|
|
622
|
+
if (input.taskId && !evaluated) return fail("Task not found on this board.");
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
message: found.graph ? `Contract map: ${found.graph.nodes.length} node(s), ${found.graph.edges.length} edge(s), enforcement ${found.graph.enforcement}.` : "No contract map on this board yet. Call configure_contract_graph to start one.",
|
|
626
|
+
board: found.board,
|
|
627
|
+
contractGraph: found.graph,
|
|
628
|
+
...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
case "configure_contract_graph": {
|
|
632
|
+
if (!input.boardId) return fail("configure_contract_graph requires boardId.");
|
|
633
|
+
const enforcement = input.contractEnforcement ?? "advisory";
|
|
634
|
+
const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
|
|
635
|
+
return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
|
|
636
|
+
}
|
|
637
|
+
case "upsert_contract_node": {
|
|
638
|
+
if (!input.boardId || !input.taskId) {
|
|
639
|
+
return fail("upsert_contract_node requires boardId and taskId.");
|
|
640
|
+
}
|
|
641
|
+
if (!input.contractNodeKind || !input.contractNodeTitle) {
|
|
642
|
+
return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
|
|
643
|
+
}
|
|
644
|
+
const waiver = input.contractNodeState === "waived" ? {
|
|
645
|
+
actor: actor ?? "agent",
|
|
646
|
+
reason: input.contractWaiverReason ?? "",
|
|
647
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
648
|
+
} : void 0;
|
|
649
|
+
if (waiver && !waiver.reason.trim()) {
|
|
650
|
+
return fail("A waived contract node requires contractWaiverReason.");
|
|
651
|
+
}
|
|
652
|
+
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
653
|
+
taskId: input.taskId,
|
|
654
|
+
kind: input.contractNodeKind,
|
|
655
|
+
title: input.contractNodeTitle,
|
|
656
|
+
...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
|
|
657
|
+
...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
|
|
658
|
+
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
659
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
660
|
+
...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
|
|
661
|
+
...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
|
|
662
|
+
...waiver ? { waiver } : {},
|
|
663
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
664
|
+
});
|
|
665
|
+
if (!result) return fail("Board or task not found.");
|
|
666
|
+
return {
|
|
667
|
+
ok: true,
|
|
668
|
+
message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
|
|
669
|
+
board: result.board,
|
|
670
|
+
contractGraph: result.board.contractGraph ?? null
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
case "remove_contract_node": {
|
|
674
|
+
if (!input.boardId || !input.contractNodeId) {
|
|
675
|
+
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
676
|
+
}
|
|
677
|
+
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
678
|
+
return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
|
|
679
|
+
}
|
|
680
|
+
case "add_contract_edge": {
|
|
681
|
+
if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
|
|
682
|
+
return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
|
|
683
|
+
}
|
|
684
|
+
if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
|
|
685
|
+
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
686
|
+
from: input.contractEdgeFrom,
|
|
687
|
+
to: input.contractEdgeTo,
|
|
688
|
+
type: input.contractEdgeType,
|
|
689
|
+
...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
|
|
690
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
691
|
+
...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
|
|
692
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
693
|
+
});
|
|
694
|
+
if (!result) return fail("Board not found.");
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
|
|
698
|
+
board: result.board,
|
|
699
|
+
contractGraph: result.board.contractGraph ?? null
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
case "remove_contract_edge": {
|
|
703
|
+
if (!input.boardId || !input.contractEdgeId) {
|
|
704
|
+
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
705
|
+
}
|
|
706
|
+
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
707
|
+
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
708
|
+
}
|
|
709
|
+
default:
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/kanban-decomposition-actions.ts
|
|
715
|
+
import {
|
|
716
|
+
assessTaskAtomicity,
|
|
717
|
+
proposeTaskDecomposition,
|
|
718
|
+
updateTask,
|
|
719
|
+
verifyTaskCompletion
|
|
720
|
+
} from "@wrongstack/kanban";
|
|
721
|
+
|
|
722
|
+
// src/kanban-evidence-bridge.ts
|
|
723
|
+
import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
|
|
724
|
+
function kanbanEvidenceKey(boardId, taskId) {
|
|
725
|
+
return `kanban:${boardId}:${taskId}`;
|
|
726
|
+
}
|
|
727
|
+
function kanbanEvidencePointer(boardId, taskId) {
|
|
728
|
+
return `kanban://${boardId}/${taskId}#verificationReport`;
|
|
729
|
+
}
|
|
730
|
+
function recordKanbanVerificationEvidence(ctx, report) {
|
|
731
|
+
try {
|
|
732
|
+
const passed = report.checks.filter((check) => check.status === "passed").length;
|
|
733
|
+
const completedAt = Date.parse(report.completedAt);
|
|
734
|
+
recordCompletedWorkEvidence(ctx, {
|
|
735
|
+
key: kanbanEvidenceKey(report.boardId, report.taskId),
|
|
736
|
+
source: "verification",
|
|
737
|
+
summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
|
|
738
|
+
...Number.isFinite(completedAt) ? { completedAt } : {},
|
|
739
|
+
evidence: kanbanEvidencePointer(report.boardId, report.taskId)
|
|
740
|
+
});
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/kanban-decomposition-actions.ts
|
|
746
|
+
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
747
|
+
switch (input.action) {
|
|
748
|
+
case "assess_atomicity": {
|
|
749
|
+
if (!input.boardId || !input.taskId) {
|
|
750
|
+
return fail("assess_atomicity requires boardId and taskId.");
|
|
751
|
+
}
|
|
752
|
+
const result = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
|
|
753
|
+
assessedBy: "agent",
|
|
754
|
+
...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
|
|
755
|
+
});
|
|
756
|
+
if (!result) return fail("Task not found.");
|
|
757
|
+
const failing = result.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
|
|
758
|
+
const guidance = result.assessment.verdict === "needs_decomposition" ? ` This task should be split before dispatch \u2014 call propose_decomposition with 2+ subtasks (each with one verifiable success criterion). Reasons: ${failing.join(" | ")}` : result.assessment.verdict === "composite" ? " Container task: work happens in its children; it is verified via subtask aggregation." : "";
|
|
759
|
+
return okTask(
|
|
760
|
+
result.board,
|
|
761
|
+
result.task,
|
|
762
|
+
`Atomicity verdict: ${result.assessment.verdict} (score ${result.assessment.score}).${guidance}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
case "propose_decomposition": {
|
|
766
|
+
if (!input.boardId || !input.taskId || !input.subtasks?.length) {
|
|
767
|
+
return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
|
|
768
|
+
}
|
|
769
|
+
if (input.subtasks.length < 2) {
|
|
770
|
+
return fail("propose_decomposition requires at least two subtasks.");
|
|
771
|
+
}
|
|
772
|
+
const invalid = input.subtasks.find(
|
|
773
|
+
(subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
|
|
774
|
+
);
|
|
775
|
+
if (invalid) return fail("Every proposed subtask needs a non-blank title.");
|
|
776
|
+
const result = await proposeTaskDecomposition(
|
|
777
|
+
projectRoot,
|
|
778
|
+
input.boardId,
|
|
779
|
+
input.taskId,
|
|
780
|
+
{
|
|
781
|
+
subtasks: input.subtasks,
|
|
782
|
+
...input.note !== void 0 ? { rationale: input.note } : {},
|
|
783
|
+
...ctx.agentId !== void 0 ? { proposedBy: ctx.agentId } : {}
|
|
784
|
+
},
|
|
785
|
+
ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
|
|
786
|
+
);
|
|
787
|
+
if (!result) return fail("Task not found.");
|
|
788
|
+
const message = result.proposal.status === "applied" ? `Decomposition applied: ${result.proposal.appliedChildTaskIds?.length ?? 0} child tasks created (parent marked atomic).` : 'Decomposition proposal recorded \u2014 awaiting approval (board policy is "propose"). It can be approved from the WebUI or via update_task.';
|
|
789
|
+
return okTask(result.board, result.task, message);
|
|
790
|
+
}
|
|
791
|
+
case "verify_completion": {
|
|
792
|
+
if (!input.boardId || !input.taskId) {
|
|
793
|
+
return fail("verify_completion requires boardId and taskId.");
|
|
794
|
+
}
|
|
795
|
+
const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
|
|
796
|
+
const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
|
|
797
|
+
verificationReport: verResult.report,
|
|
798
|
+
successCriteria: verResult.task.successCriteria
|
|
799
|
+
});
|
|
800
|
+
if (!persistedBoard) {
|
|
801
|
+
return {
|
|
802
|
+
ok: false,
|
|
803
|
+
verdict: verResult.report.verdict,
|
|
804
|
+
message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
|
|
805
|
+
board: verResult.board
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
809
|
+
const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
|
|
810
|
+
const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
|
|
811
|
+
return {
|
|
812
|
+
ok: deterministicVerdicts.includes(
|
|
813
|
+
verResult.report.verdict
|
|
814
|
+
),
|
|
815
|
+
verdict: verResult.report.verdict,
|
|
816
|
+
message: verResult.report.markdownSummary,
|
|
817
|
+
board: persistedBoard,
|
|
818
|
+
task: freshTask ?? verResult.task
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
default:
|
|
822
|
+
return void 0;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/kanban-detail-actions.ts
|
|
827
|
+
import {
|
|
828
|
+
addCheckToTask,
|
|
829
|
+
addDependency,
|
|
830
|
+
addGoalMetricToTask,
|
|
831
|
+
addLinkToTask,
|
|
832
|
+
addNoteToTask,
|
|
833
|
+
getKanbanWorkbench,
|
|
834
|
+
removeCheckFromTask,
|
|
835
|
+
updateCheckOnTask,
|
|
836
|
+
updateGoalMetricOnTask
|
|
837
|
+
} from "@wrongstack/kanban";
|
|
838
|
+
|
|
839
|
+
// src/kanban-split-task-handler.ts
|
|
840
|
+
import { getBoard as getBoard2, splitTask } from "@wrongstack/kanban";
|
|
841
|
+
async function handleSplitTask(projectRoot, input, extraSplitOptions) {
|
|
842
|
+
const boardId = input.boardId;
|
|
843
|
+
const taskId = input.taskId;
|
|
844
|
+
const childTitles = input.childTitles;
|
|
845
|
+
if (!boardId || !taskId || !childTitles?.length) {
|
|
846
|
+
return fail("split requires boardId, taskId, and at least one childTitles.");
|
|
847
|
+
}
|
|
848
|
+
const {
|
|
849
|
+
targetColumnId,
|
|
850
|
+
inheritAssignment,
|
|
851
|
+
inheritLabels,
|
|
852
|
+
inheritSuccessCriteria,
|
|
853
|
+
inheritGoalMetrics,
|
|
854
|
+
inheritDependencies,
|
|
855
|
+
chainChildren,
|
|
856
|
+
rewireDependents
|
|
857
|
+
} = input;
|
|
858
|
+
const result = await splitTask(projectRoot, boardId, taskId, {
|
|
859
|
+
titles: childTitles,
|
|
860
|
+
...extraSplitOptions,
|
|
861
|
+
...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
|
|
862
|
+
...inheritAssignment !== void 0 ? { inheritAssignment } : {},
|
|
863
|
+
...inheritLabels !== void 0 ? { inheritLabels } : {},
|
|
864
|
+
...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
|
|
865
|
+
...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
|
|
866
|
+
...inheritDependencies !== void 0 ? { inheritDependencies } : {},
|
|
867
|
+
...chainChildren !== void 0 ? { chainChildren } : {},
|
|
868
|
+
...rewireDependents !== void 0 ? { rewireDependents } : {}
|
|
869
|
+
});
|
|
870
|
+
if (!result) return fail("Task not found.");
|
|
871
|
+
const freshParent = result.board.tasks?.find((t) => t.id === taskId);
|
|
872
|
+
if (!freshParent) {
|
|
873
|
+
return fail(
|
|
874
|
+
`Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
ok: true,
|
|
879
|
+
message: `${result.children.length} child task(s) created.`,
|
|
880
|
+
board: result.board,
|
|
881
|
+
task: freshParent,
|
|
882
|
+
children: result.children
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
async function requireBoard(projectRoot, boardId) {
|
|
886
|
+
return boardId ? getBoard2(projectRoot, boardId) : null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/kanban-detail-actions.ts
|
|
890
|
+
async function handleKanbanDetailAction(projectRoot, input) {
|
|
891
|
+
switch (input.action) {
|
|
892
|
+
case "workbench": {
|
|
893
|
+
const workbench = await getKanbanWorkbench(projectRoot, {
|
|
894
|
+
...input.limit !== void 0 ? { limitPerLane: input.limit, alertLimit: input.limit } : {}
|
|
895
|
+
});
|
|
896
|
+
return {
|
|
897
|
+
ok: true,
|
|
898
|
+
message: `${workbench.totals.now} now, ${workbench.totals.next} next, ${workbench.totals.blocked} blocked, ${workbench.totals.review} review; ${workbench.alertTotal} alert(s).`,
|
|
899
|
+
workbench
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
case "add_dependency": {
|
|
903
|
+
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
904
|
+
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
905
|
+
}
|
|
906
|
+
const board = await addDependency(
|
|
907
|
+
projectRoot,
|
|
908
|
+
input.boardId,
|
|
909
|
+
input.taskId,
|
|
910
|
+
input.dependencyTaskId
|
|
911
|
+
);
|
|
912
|
+
return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
|
|
913
|
+
}
|
|
914
|
+
case "add_goal_metric": {
|
|
915
|
+
if (!input.boardId || !input.taskId || !input.metricName) {
|
|
916
|
+
return fail("add_goal_metric requires boardId, taskId, and metricName.");
|
|
917
|
+
}
|
|
918
|
+
const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
|
|
919
|
+
name: input.metricName,
|
|
920
|
+
...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
|
|
921
|
+
...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
|
|
922
|
+
...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
|
|
923
|
+
...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
|
|
924
|
+
...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
|
|
925
|
+
});
|
|
926
|
+
return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
|
|
927
|
+
}
|
|
928
|
+
case "update_goal_metric": {
|
|
929
|
+
if (!input.boardId || !input.taskId || !input.metricId) {
|
|
930
|
+
return fail("update_goal_metric requires boardId, taskId, and metricId.");
|
|
931
|
+
}
|
|
932
|
+
const board = await updateGoalMetricOnTask(
|
|
933
|
+
projectRoot,
|
|
934
|
+
input.boardId,
|
|
935
|
+
input.taskId,
|
|
936
|
+
input.metricId,
|
|
937
|
+
{
|
|
938
|
+
...input.metricName !== void 0 ? { name: input.metricName } : {},
|
|
939
|
+
...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
|
|
940
|
+
...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
|
|
941
|
+
...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
|
|
942
|
+
...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
|
|
943
|
+
...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
|
|
944
|
+
}
|
|
945
|
+
);
|
|
946
|
+
return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
|
|
947
|
+
}
|
|
948
|
+
case "add_check": {
|
|
949
|
+
if (!input.boardId || !input.taskId || !input.checkDescription) {
|
|
950
|
+
return fail("add_check requires boardId, taskId, and checkDescription.");
|
|
951
|
+
}
|
|
952
|
+
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
953
|
+
description: input.checkDescription,
|
|
954
|
+
type: input.checkType ?? "manual",
|
|
955
|
+
status: input.checkStatus,
|
|
956
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
957
|
+
});
|
|
958
|
+
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
959
|
+
}
|
|
960
|
+
case "update_check": {
|
|
961
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
962
|
+
return fail("update_check requires boardId, taskId, and checkId.");
|
|
963
|
+
}
|
|
964
|
+
const board = await updateCheckOnTask(
|
|
965
|
+
projectRoot,
|
|
966
|
+
input.boardId,
|
|
967
|
+
input.taskId,
|
|
968
|
+
input.checkId,
|
|
969
|
+
{
|
|
970
|
+
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
971
|
+
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
|
|
972
|
+
// Promoting an existing manual criterion to an executable one is the
|
|
973
|
+
// common repair: the card was written before anyone knew the command.
|
|
974
|
+
...input.checkType !== void 0 ? { type: input.checkType } : {},
|
|
975
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
979
|
+
}
|
|
980
|
+
case "remove_check": {
|
|
981
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
982
|
+
return fail("remove_check requires boardId, taskId, and checkId.");
|
|
983
|
+
}
|
|
984
|
+
const board = await removeCheckFromTask(
|
|
985
|
+
projectRoot,
|
|
986
|
+
input.boardId,
|
|
987
|
+
input.taskId,
|
|
988
|
+
input.checkId
|
|
989
|
+
);
|
|
990
|
+
return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
|
|
991
|
+
}
|
|
992
|
+
case "add_note": {
|
|
993
|
+
if (!input.boardId || !input.taskId || !input.note)
|
|
994
|
+
return fail("add_note requires boardId, taskId, and note.");
|
|
995
|
+
const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
|
|
996
|
+
author: input.author ?? "agent",
|
|
997
|
+
content: input.note
|
|
998
|
+
});
|
|
999
|
+
return board ? okBoard(board, "Note added.") : fail("Task not found.");
|
|
1000
|
+
}
|
|
1001
|
+
case "add_link": {
|
|
1002
|
+
if (!input.boardId || !input.taskId || !input.url)
|
|
1003
|
+
return fail("add_link requires boardId, taskId, and url.");
|
|
1004
|
+
const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
|
|
1005
|
+
url: input.url,
|
|
1006
|
+
type: input.linkType ?? "url",
|
|
1007
|
+
...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
|
|
1008
|
+
});
|
|
1009
|
+
return board ? okBoard(board, "Link added.") : fail("Task not found.");
|
|
1010
|
+
}
|
|
1011
|
+
case "split_atomic": {
|
|
1012
|
+
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
1013
|
+
return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
|
|
1014
|
+
}
|
|
1015
|
+
return handleSplitTask(projectRoot, input, { atomic: true });
|
|
1016
|
+
}
|
|
1017
|
+
default:
|
|
1018
|
+
return void 0;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/kanban-presence.ts
|
|
1023
|
+
import { touchKanbanPresence as touchKanbanPresence2 } from "@wrongstack/kanban";
|
|
1024
|
+
function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
1025
|
+
return async (result) => {
|
|
1026
|
+
const boardId = result.board?.id ?? input.boardId;
|
|
1027
|
+
if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
|
|
1028
|
+
try {
|
|
1029
|
+
const board = await touchKanbanPresence2(projectRoot, boardId, {
|
|
1030
|
+
sessionId: ctx.session.id,
|
|
1031
|
+
agentId: ctx.agentId,
|
|
1032
|
+
agentName: ctx.agentName,
|
|
1033
|
+
taskId: input.taskId ?? result.task?.id,
|
|
1034
|
+
runTaskId: input.runTaskId
|
|
1035
|
+
});
|
|
1036
|
+
return board ? { ...result, board } : result;
|
|
1037
|
+
} catch {
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/kanban-task-inputs.ts
|
|
1044
|
+
import { randomUUID } from "node:crypto";
|
|
1045
|
+
import { clampSubagentCapabilities } from "@wrongstack/core/security";
|
|
1046
|
+
function taskInput(input) {
|
|
1047
|
+
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
1048
|
+
return {
|
|
1049
|
+
title: input.title ?? "",
|
|
1050
|
+
columnId: input.columnId,
|
|
1051
|
+
description: input.description,
|
|
1052
|
+
dueDate: input.dueDate,
|
|
1053
|
+
priority: input.priority,
|
|
1054
|
+
...input.taskType !== void 0 ? { type: input.taskType } : {},
|
|
1055
|
+
status: input.status,
|
|
1056
|
+
labels: input.labels,
|
|
1057
|
+
...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
|
|
1058
|
+
...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
|
|
1059
|
+
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
1060
|
+
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
1061
|
+
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
|
|
1062
|
+
...assignment ? { assignment } : {},
|
|
1063
|
+
...input.order !== void 0 ? { order: input.order } : {},
|
|
1064
|
+
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
1065
|
+
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
1066
|
+
// The system prompt has always told the model it may "set atomic: true"
|
|
1067
|
+
// when creating a composite parent. It could not: the field reached
|
|
1068
|
+
// neither the create input nor the patch, so the instruction described a
|
|
1069
|
+
// capability that did not exist and the attempt was silently dropped.
|
|
1070
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
1071
|
+
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
1072
|
+
...input.checkDescription !== void 0 ? {
|
|
1073
|
+
successCriteria: [
|
|
1074
|
+
{
|
|
1075
|
+
id: randomUUID(),
|
|
1076
|
+
description: input.checkDescription,
|
|
1077
|
+
// `manual` only as the fallback. Hard-coding it here meant every
|
|
1078
|
+
// agent-authored criterion was unverifiable by construction: the
|
|
1079
|
+
// deterministic plugins never matched, the registry passed the
|
|
1080
|
+
// hand-set status straight through, and "verified" collapsed into
|
|
1081
|
+
// "the author ticked its own box".
|
|
1082
|
+
type: input.checkType ?? "manual",
|
|
1083
|
+
status: input.checkStatus ?? "pending",
|
|
1084
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
1085
|
+
}
|
|
1086
|
+
]
|
|
1087
|
+
} : {},
|
|
1088
|
+
...input.metricName !== void 0 ? {
|
|
1089
|
+
goalMetrics: [
|
|
1090
|
+
{
|
|
1091
|
+
id: randomUUID(),
|
|
1092
|
+
name: input.metricName,
|
|
1093
|
+
status: input.metricStatus ?? "pending",
|
|
1094
|
+
...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
|
|
1095
|
+
...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
|
|
1096
|
+
...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
|
|
1097
|
+
...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
|
|
1098
|
+
}
|
|
1099
|
+
]
|
|
1100
|
+
} : {},
|
|
1101
|
+
...input.url !== void 0 ? {
|
|
1102
|
+
links: [
|
|
1103
|
+
{
|
|
1104
|
+
url: input.url,
|
|
1105
|
+
type: input.linkType ?? "url",
|
|
1106
|
+
...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
|
|
1107
|
+
}
|
|
1108
|
+
]
|
|
1109
|
+
} : {},
|
|
1110
|
+
...input.note !== void 0 ? {
|
|
1111
|
+
notes: [
|
|
1112
|
+
{
|
|
1113
|
+
id: randomUUID(),
|
|
1114
|
+
author: input.author ?? "agent",
|
|
1115
|
+
content: input.note,
|
|
1116
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1117
|
+
}
|
|
1118
|
+
]
|
|
1119
|
+
} : {},
|
|
1120
|
+
...[input.graphId, input.specId, input.specRequirementId].some((value) => value !== void 0) ? {
|
|
1121
|
+
origin: {
|
|
1122
|
+
system: input.sourceSystem ?? "kanban-tool",
|
|
1123
|
+
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
1124
|
+
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
1125
|
+
...input.specRequirementId !== void 0 ? { specRequirementId: input.specRequirementId } : {},
|
|
1126
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
|
|
1127
|
+
}
|
|
1128
|
+
} : {}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function mergedDependsOn(input) {
|
|
1132
|
+
if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
|
|
1133
|
+
return [
|
|
1134
|
+
...input.dependsOn ?? [],
|
|
1135
|
+
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
1136
|
+
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
1137
|
+
}
|
|
1138
|
+
function taskPatch(input) {
|
|
1139
|
+
return {
|
|
1140
|
+
title: input.title,
|
|
1141
|
+
description: input.description,
|
|
1142
|
+
dueDate: input.dueDate,
|
|
1143
|
+
columnId: input.columnId,
|
|
1144
|
+
order: input.order,
|
|
1145
|
+
priority: input.priority,
|
|
1146
|
+
...input.taskType !== void 0 ? { type: input.taskType } : {},
|
|
1147
|
+
status: input.status,
|
|
1148
|
+
labels: input.labels,
|
|
1149
|
+
assignedAgent: input.agentId,
|
|
1150
|
+
...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
|
|
1151
|
+
// `atomic` and `childTaskIds` are the composite-parent contract, and the
|
|
1152
|
+
// managed gate reads both: an `atomic` parent may not move forward without
|
|
1153
|
+
// children, and may not reach Done until every child is completed. The
|
|
1154
|
+
// manager has always accepted both on a patch; only this surface withheld
|
|
1155
|
+
// them, so `split_atomic` was a one-way door — delete the children and the
|
|
1156
|
+
// parent was stranded with no way to declare itself a leaf again.
|
|
1157
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
1158
|
+
...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
|
|
1159
|
+
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
1160
|
+
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function clampRequestedCapabilities(requested) {
|
|
1164
|
+
if (requested === void 0) return void 0;
|
|
1165
|
+
return clampSubagentCapabilities(requested).granted;
|
|
1166
|
+
}
|
|
1167
|
+
function assignmentInput(input) {
|
|
1168
|
+
return {
|
|
1169
|
+
agentId: input.agentId,
|
|
1170
|
+
name: input.name,
|
|
1171
|
+
role: input.role,
|
|
1172
|
+
provider: input.provider,
|
|
1173
|
+
model: input.model,
|
|
1174
|
+
fallbackProfile: input.fallbackProfile,
|
|
1175
|
+
fallbackModels: input.fallbackModels,
|
|
1176
|
+
tools: input.tools,
|
|
1177
|
+
allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
|
|
1178
|
+
assignee: input.assignee,
|
|
1179
|
+
leaseId: input.leaseId,
|
|
1180
|
+
claimedAt: input.claimedAt,
|
|
1181
|
+
heartbeatAt: input.heartbeatAt,
|
|
1182
|
+
leaseExpiresAt: input.leaseExpiresAt,
|
|
1183
|
+
attempt: input.attempt,
|
|
1184
|
+
maxAttempts: input.maxAttempts,
|
|
1185
|
+
costCeilingUsd: input.costCeilingUsd,
|
|
1186
|
+
retryPolicy: input.retryPolicy,
|
|
1187
|
+
lastFailureKind: input.lastFailureKind
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function hasAssignmentInput(input) {
|
|
1191
|
+
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;
|
|
1192
|
+
}
|
|
1193
|
+
function assignmentForTaskCreate(input) {
|
|
1194
|
+
return {
|
|
1195
|
+
status: input.assignmentStatus ?? "assigned",
|
|
1196
|
+
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
1197
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
1198
|
+
...input.role !== void 0 ? { role: input.role } : {},
|
|
1199
|
+
...input.provider !== void 0 ? { provider: input.provider } : {},
|
|
1200
|
+
...input.model !== void 0 ? { model: input.model } : {},
|
|
1201
|
+
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
1202
|
+
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
1203
|
+
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
1204
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
1205
|
+
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
1206
|
+
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
1207
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
1208
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
1209
|
+
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
1210
|
+
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {},
|
|
1211
|
+
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
1212
|
+
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
1213
|
+
...input.lastFailureKind !== void 0 ? { lastFailureKind: input.lastFailureKind } : {}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/kanban-tool-schema.ts
|
|
1218
|
+
var KANBAN_TOOL_DESCRIPTION = "Durable project task boards: create and move cards, record checks, notes, links and assignments. The board is a record of the work, not a permit for it \u2014 nothing here gates other tools. Managed boards additionally enforce ordered Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions; release_managed_lifecycle turns that off.";
|
|
1219
|
+
var KANBAN_TOOL_USAGE_HINT = 'Track substantial or multi-step work so it survives the session; a trivial edit or a question needs no card. Work stays on ONE board: call list_boards first and add_task to the board this project already uses. create_board is for a genuinely separate line of work, not for each new piece of it \u2014 a second board splits the same effort in two, and a board holding a single card is the usual sign. Common flow: list_boards or search_tasks to orient, add_task to record work, start_task when you begin, update_check with checkStatus "passed" to tick acceptance criteria (read their ids from get_task), then transition_task. On a managed board a refused transition names the field it wants \u2014 supply it and retry. When the acceptance criterion is something a machine can run, say so: set checkType ("command", "test", "file_exists", "file_matches", "git_diff", "metric") and put the command, pattern or path in checkNotes, then verify_completion executes it and the result is real evidence. Leave checkType off (or "manual") only for criteria that genuinely need a human eye \u2014 a manual check records your assertion, it does not test anything.';
|
|
1220
|
+
var KANBAN_INPUT_SCHEMA = {
|
|
1221
|
+
type: "object",
|
|
1222
|
+
properties: {
|
|
1223
|
+
action: {
|
|
1224
|
+
type: "string",
|
|
1225
|
+
enum: [
|
|
1226
|
+
"list_boards",
|
|
1227
|
+
"get_board",
|
|
1228
|
+
"create_board",
|
|
1229
|
+
"duplicate_board",
|
|
1230
|
+
"update_board",
|
|
1231
|
+
"adopt_managed_lifecycle",
|
|
1232
|
+
"release_managed_lifecycle",
|
|
1233
|
+
"delete_board",
|
|
1234
|
+
"generate_board",
|
|
1235
|
+
"export_markdown",
|
|
1236
|
+
"export_task_graph",
|
|
1237
|
+
"sync_task_graph",
|
|
1238
|
+
"create_from_graph",
|
|
1239
|
+
"import_session_tasks",
|
|
1240
|
+
"search_tasks",
|
|
1241
|
+
"ready_tasks",
|
|
1242
|
+
"snapshot",
|
|
1243
|
+
"workbench",
|
|
1244
|
+
"add_task",
|
|
1245
|
+
"split_task",
|
|
1246
|
+
"merge_tasks",
|
|
1247
|
+
"copy_task",
|
|
1248
|
+
"transfer_task",
|
|
1249
|
+
"get_task",
|
|
1250
|
+
"start_task",
|
|
1251
|
+
"update_task",
|
|
1252
|
+
"transition_task",
|
|
1253
|
+
"repair_managed_projection",
|
|
1254
|
+
"move_task",
|
|
1255
|
+
"delete_task",
|
|
1256
|
+
"set_chain",
|
|
1257
|
+
"get_chain",
|
|
1258
|
+
"claim_task",
|
|
1259
|
+
"release_task",
|
|
1260
|
+
"assign_task",
|
|
1261
|
+
"mark_assignment",
|
|
1262
|
+
"heartbeat_assignment",
|
|
1263
|
+
"recover_stale",
|
|
1264
|
+
"events",
|
|
1265
|
+
"queue_health",
|
|
1266
|
+
"add_dependency",
|
|
1267
|
+
"add_goal_metric",
|
|
1268
|
+
"update_goal_metric",
|
|
1269
|
+
"add_check",
|
|
1270
|
+
"update_check",
|
|
1271
|
+
"remove_check",
|
|
1272
|
+
"add_note",
|
|
1273
|
+
"add_link",
|
|
1274
|
+
"verify_completion",
|
|
1275
|
+
"split_atomic",
|
|
1276
|
+
"assess_atomicity",
|
|
1277
|
+
"propose_decomposition",
|
|
1278
|
+
"get_contract_graph",
|
|
1279
|
+
"configure_contract_graph",
|
|
1280
|
+
"upsert_contract_node",
|
|
1281
|
+
"remove_contract_node",
|
|
1282
|
+
"add_contract_edge",
|
|
1283
|
+
"remove_contract_edge"
|
|
1284
|
+
]
|
|
1285
|
+
},
|
|
1286
|
+
boardId: { type: "string" },
|
|
1287
|
+
taskId: { type: "string" },
|
|
1288
|
+
taskIds: { type: "array", items: { type: "string" } },
|
|
1289
|
+
chainId: { type: "string" },
|
|
1290
|
+
fromNodeId: { type: "string" },
|
|
1291
|
+
toNodeId: { type: "string" },
|
|
1292
|
+
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1293
|
+
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1294
|
+
columnId: { type: "string" },
|
|
1295
|
+
targetBoardId: { type: "string" },
|
|
1296
|
+
targetColumnId: { type: "string" },
|
|
1297
|
+
title: { type: "string" },
|
|
1298
|
+
description: { type: "string" },
|
|
1299
|
+
dueDate: { type: "string" },
|
|
1300
|
+
tags: { type: "array", items: { type: "string" } },
|
|
1301
|
+
labels: { type: "array", items: { type: "string" } },
|
|
1302
|
+
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
1303
|
+
taskType: {
|
|
1304
|
+
type: "string",
|
|
1305
|
+
enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
|
|
1306
|
+
},
|
|
1307
|
+
status: {
|
|
1308
|
+
type: "string",
|
|
1309
|
+
enum: [
|
|
1310
|
+
"pending",
|
|
1311
|
+
"ready",
|
|
1312
|
+
"in_progress",
|
|
1313
|
+
"blocked",
|
|
1314
|
+
"review",
|
|
1315
|
+
"completed",
|
|
1316
|
+
"failed",
|
|
1317
|
+
"archived"
|
|
1318
|
+
]
|
|
1319
|
+
},
|
|
1320
|
+
order: { type: "number" },
|
|
1321
|
+
query: { type: "string" },
|
|
1322
|
+
limit: { type: "number" },
|
|
1323
|
+
agentId: { type: "string" },
|
|
1324
|
+
name: { type: "string" },
|
|
1325
|
+
role: { type: "string" },
|
|
1326
|
+
provider: { type: "string" },
|
|
1327
|
+
model: { type: "string" },
|
|
1328
|
+
fallbackProfile: { type: "string" },
|
|
1329
|
+
fallbackModels: { type: "array", items: { type: "string" } },
|
|
1330
|
+
tools: { type: "array", items: { type: "string" } },
|
|
1331
|
+
allowedCapabilities: { type: "array", items: { type: "string" } },
|
|
1332
|
+
leaseId: { type: "string" },
|
|
1333
|
+
claimedAt: { type: "string" },
|
|
1334
|
+
heartbeatAt: { type: "string" },
|
|
1335
|
+
leaseExpiresAt: { type: "string" },
|
|
1336
|
+
attempt: { type: "number" },
|
|
1337
|
+
maxAttempts: { type: "number" },
|
|
1338
|
+
subagentId: { type: "string" },
|
|
1339
|
+
runTaskId: { type: "string" },
|
|
1340
|
+
lastResult: { type: "string" },
|
|
1341
|
+
error: { type: "string" },
|
|
1342
|
+
expectedLeaseId: { type: "string" },
|
|
1343
|
+
assignmentStatus: {
|
|
1344
|
+
type: "string",
|
|
1345
|
+
enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
|
|
1346
|
+
},
|
|
1347
|
+
lifecycleStage: {
|
|
1348
|
+
type: "string",
|
|
1349
|
+
enum: ["backlog", "todo", "running", "review", "done"]
|
|
1350
|
+
},
|
|
1351
|
+
transitionAction: { type: "string" },
|
|
1352
|
+
transitionComment: { type: "string" },
|
|
1353
|
+
attachmentUrl: { type: "string" },
|
|
1354
|
+
attachmentTitle: { type: "string" },
|
|
1355
|
+
attachmentType: {
|
|
1356
|
+
type: "string",
|
|
1357
|
+
enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
|
|
1358
|
+
},
|
|
1359
|
+
releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
|
|
1360
|
+
releaseReason: { type: "string" },
|
|
1361
|
+
clearAssignee: { type: "boolean" },
|
|
1362
|
+
recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
|
|
1363
|
+
recoveryNow: { type: "string" },
|
|
1364
|
+
recoveryPolicyFailOnCostCeiling: { type: "boolean" },
|
|
1365
|
+
recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
|
|
1366
|
+
recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
|
|
1367
|
+
recoveryPolicyRetryPolicyOverride: {
|
|
1368
|
+
type: "string",
|
|
1369
|
+
enum: ["off", "incremental", "exponential"]
|
|
1370
|
+
},
|
|
1371
|
+
assignee: { type: "string" },
|
|
1372
|
+
costCeilingUsd: { type: "number" },
|
|
1373
|
+
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
1374
|
+
lastFailureKind: { type: "string" },
|
|
1375
|
+
dependsOn: {
|
|
1376
|
+
type: "array",
|
|
1377
|
+
items: { type: "string" },
|
|
1378
|
+
description: "Task ids this card waits on. On update_task an explicit empty array clears them \u2014 use it when a dependency was recorded in error rather than completing work nobody wants."
|
|
1379
|
+
},
|
|
1380
|
+
atomic: {
|
|
1381
|
+
type: "boolean",
|
|
1382
|
+
description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
|
|
1383
|
+
},
|
|
1384
|
+
childTaskIds: {
|
|
1385
|
+
type: "array",
|
|
1386
|
+
items: { type: "string" },
|
|
1387
|
+
description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
|
|
1388
|
+
},
|
|
1389
|
+
estimatedHours: { type: "number" },
|
|
1390
|
+
actualHours: { type: "number" },
|
|
1391
|
+
taskGraph: { type: "object" },
|
|
1392
|
+
graphId: { type: "string" },
|
|
1393
|
+
specId: { type: "string" },
|
|
1394
|
+
specRequirementId: { type: "string" },
|
|
1395
|
+
sourceSystem: { type: "string" },
|
|
1396
|
+
phaseId: { type: "string" },
|
|
1397
|
+
preserveOriginTaskIds: { type: "boolean" },
|
|
1398
|
+
includeArchived: { type: "boolean" },
|
|
1399
|
+
archiveMissingTasks: { type: "boolean" },
|
|
1400
|
+
preserveManualDependencies: { type: "boolean" },
|
|
1401
|
+
dependencyTaskId: { type: "string" },
|
|
1402
|
+
enforceDependencies: { type: "boolean" },
|
|
1403
|
+
childTitles: { type: "array", items: { type: "string" } },
|
|
1404
|
+
inheritAssignment: { type: "boolean" },
|
|
1405
|
+
inheritLabels: { type: "boolean" },
|
|
1406
|
+
inheritSuccessCriteria: { type: "boolean" },
|
|
1407
|
+
inheritGoalMetrics: { type: "boolean" },
|
|
1408
|
+
inheritDependencies: { type: "boolean" },
|
|
1409
|
+
chainChildren: { type: "boolean" },
|
|
1410
|
+
rewireDependents: { type: "boolean" },
|
|
1411
|
+
closeSourceTasks: { type: "boolean" },
|
|
1412
|
+
metricId: { type: "string" },
|
|
1413
|
+
metricName: { type: "string" },
|
|
1414
|
+
metricTarget: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1415
|
+
metricCurrent: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1416
|
+
metricUnit: { type: "string" },
|
|
1417
|
+
metricStatus: { type: "string", enum: ["pending", "met", "missed", "waived"] },
|
|
1418
|
+
metricNotes: { type: "string" },
|
|
1419
|
+
checkId: { type: "string" },
|
|
1420
|
+
checkDescription: { type: "string" },
|
|
1421
|
+
checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
|
|
1422
|
+
checkType: {
|
|
1423
|
+
type: "string",
|
|
1424
|
+
// Only types a verifier can actually execute. `manual` is the default and
|
|
1425
|
+
// means a human or agent asserts the status by hand. The rest are run by
|
|
1426
|
+
// `verify_completion` against the default deterministic registry. Types
|
|
1427
|
+
// with no plugin in that registry (`auto`, `review`, `agent`, `council`)
|
|
1428
|
+
// are deliberately omitted: offering them would produce criteria that
|
|
1429
|
+
// silently report `skipped — no verifier plugin registered`.
|
|
1430
|
+
enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
|
|
1431
|
+
description: 'How this acceptance criterion is verified. Default "manual" (status set by hand). Any other value makes verify_completion execute it, so the criterion becomes real evidence rather than a self-assertion. Pair with checkNotes.'
|
|
1432
|
+
},
|
|
1433
|
+
checkNotes: {
|
|
1434
|
+
type: "string",
|
|
1435
|
+
description: 'The executable body for a non-manual checkType, read in preference to checkDescription. command/test: the shell command or test pattern. file_exists: the path. file_matches: JSON {"file","pattern","flags"}. git_diff: JSON {"expectedFiles","minChanges","maxChanges"}.'
|
|
1436
|
+
},
|
|
1437
|
+
// ── Contract map ───────────────────────────────────────────────────
|
|
1438
|
+
// The card contract: what this work targets, what it must not break, what
|
|
1439
|
+
// it risks, and what verifies it. Advisory by default — the readiness gate
|
|
1440
|
+
// deliberately does not require map structure, so a map is an operator
|
|
1441
|
+
// review aid, not work the model must complete before implementing.
|
|
1442
|
+
contractEnforcement: {
|
|
1443
|
+
type: "string",
|
|
1444
|
+
enum: ["off", "advisory", "strict"],
|
|
1445
|
+
description: "Board-level contract map enforcement. Default when first configured: advisory."
|
|
1446
|
+
},
|
|
1447
|
+
contractNodeId: { type: "string" },
|
|
1448
|
+
contractNodeKind: {
|
|
1449
|
+
type: "string",
|
|
1450
|
+
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
|
|
1451
|
+
description: "objective = what this card is for; guardrail = what must keep working; risk = what could go wrong; component/artifact = what it touches; verification = what settles it."
|
|
1452
|
+
},
|
|
1453
|
+
contractNodeTitle: { type: "string" },
|
|
1454
|
+
contractNodeDescription: { type: "string" },
|
|
1455
|
+
contractNodeState: {
|
|
1456
|
+
type: "string",
|
|
1457
|
+
enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
|
|
1458
|
+
},
|
|
1459
|
+
contractNodeEnforcement: {
|
|
1460
|
+
type: "string",
|
|
1461
|
+
enum: ["blocking", "advisory", "informational"]
|
|
1462
|
+
},
|
|
1463
|
+
/** Bind a node to an acceptance criterion or goal metric already on the task. */
|
|
1464
|
+
contractCheckId: { type: "string" },
|
|
1465
|
+
contractMetricId: { type: "string" },
|
|
1466
|
+
contractWaiverReason: {
|
|
1467
|
+
type: "string",
|
|
1468
|
+
description: 'Required, with an actor, when contractNodeState is "waived".'
|
|
1469
|
+
},
|
|
1470
|
+
contractEdgeId: { type: "string" },
|
|
1471
|
+
contractEdgeFrom: {
|
|
1472
|
+
type: "string",
|
|
1473
|
+
description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
|
|
1474
|
+
},
|
|
1475
|
+
contractEdgeTo: { type: "string" },
|
|
1476
|
+
contractEdgeType: {
|
|
1477
|
+
type: "string",
|
|
1478
|
+
enum: [
|
|
1479
|
+
"targets",
|
|
1480
|
+
"affects",
|
|
1481
|
+
"must_preserve",
|
|
1482
|
+
"exposes",
|
|
1483
|
+
"verified_by",
|
|
1484
|
+
"conflicts_with",
|
|
1485
|
+
"derived_from",
|
|
1486
|
+
"relates_to"
|
|
1487
|
+
]
|
|
1488
|
+
},
|
|
1489
|
+
contractEdgeRationale: { type: "string" },
|
|
1490
|
+
note: { type: "string" },
|
|
1491
|
+
author: { type: "string" },
|
|
1492
|
+
url: { type: "string" },
|
|
1493
|
+
linkTitle: { type: "string" },
|
|
1494
|
+
linkType: {
|
|
1495
|
+
type: "string",
|
|
1496
|
+
enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
|
|
1497
|
+
},
|
|
1498
|
+
context: { type: "string" },
|
|
1499
|
+
columns: { type: "array", items: { type: "string" } },
|
|
1500
|
+
generatedBy: { type: "string" },
|
|
1501
|
+
includeTasks: { type: "boolean" },
|
|
1502
|
+
includeCompletedTasks: { type: "boolean" },
|
|
1503
|
+
preserveAssignment: { type: "boolean" },
|
|
1504
|
+
preserveDependencies: { type: "boolean" },
|
|
1505
|
+
moveTasksToColumnId: { type: "string" },
|
|
1506
|
+
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
1507
|
+
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
1508
|
+
gateEnforcement: {
|
|
1509
|
+
type: "string",
|
|
1510
|
+
// WS-023: `'off'` is deliberately absent. The agent whose work this gate
|
|
1511
|
+
// checks must not be able to switch it off; it may only tighten. Turning
|
|
1512
|
+
// a gate off stays a human decision, made through board config.
|
|
1513
|
+
enum: ["strict", "soft"]
|
|
1514
|
+
},
|
|
1515
|
+
subtasks: {
|
|
1516
|
+
type: "array",
|
|
1517
|
+
minItems: 2,
|
|
1518
|
+
items: {
|
|
1519
|
+
type: "object",
|
|
1520
|
+
properties: {
|
|
1521
|
+
title: { type: "string" },
|
|
1522
|
+
description: { type: "string" },
|
|
1523
|
+
successCriteria: { type: "array", items: { type: "string" } },
|
|
1524
|
+
dependsOnIndex: { type: "array", items: { type: "number" } }
|
|
1525
|
+
},
|
|
1526
|
+
required: ["title"]
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
required: ["action"]
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// src/kanban.ts
|
|
1534
|
+
var kanbanTool = {
|
|
1535
|
+
name: "kanban",
|
|
1536
|
+
category: "Project",
|
|
1537
|
+
description: KANBAN_TOOL_DESCRIPTION,
|
|
1538
|
+
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
1539
|
+
permission: "confirm",
|
|
1540
|
+
mutating: true,
|
|
1541
|
+
capabilities: ["fs.write"],
|
|
1542
|
+
icon: "task",
|
|
1543
|
+
timeoutMs: 3e4,
|
|
1544
|
+
inputSchema: KANBAN_INPUT_SCHEMA,
|
|
1545
|
+
async execute(input, ctx) {
|
|
1546
|
+
const projectRoot = ctx.projectRoot;
|
|
1547
|
+
if (!projectRoot) return fail("No project root is available.");
|
|
1548
|
+
const withPresence = createKanbanPresenceWrapper(projectRoot, input, ctx);
|
|
1549
|
+
try {
|
|
1550
|
+
const result = await (async () => {
|
|
1551
|
+
const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
|
|
1552
|
+
if (decompositionResult !== void 0) return decompositionResult;
|
|
1553
|
+
switch (input.action) {
|
|
1554
|
+
case "list_boards": {
|
|
1555
|
+
const boards = await listBoards2(projectRoot);
|
|
1556
|
+
return { ok: true, message: `${boards.length} board(s).`, boards };
|
|
1557
|
+
}
|
|
1558
|
+
case "get_board": {
|
|
1559
|
+
const board = await requireBoard(projectRoot, input.boardId);
|
|
1560
|
+
return board ? okBoard(board) : fail("Board not found.");
|
|
1561
|
+
}
|
|
1562
|
+
case "create_board": {
|
|
1563
|
+
if (!input.title) return fail("create_board requires title.");
|
|
1564
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
1565
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
1566
|
+
);
|
|
1567
|
+
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
1568
|
+
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(
|
|
1569
|
+
", "
|
|
1570
|
+
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
1571
|
+
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
1572
|
+
}
|
|
1573
|
+
case "update_board": {
|
|
1574
|
+
if (!input.boardId) return fail("update_board requires boardId.");
|
|
1575
|
+
const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
|
|
1576
|
+
return board ? okBoard(board, "Board updated.") : fail("Board not found.");
|
|
1577
|
+
}
|
|
1578
|
+
case "adopt_managed_lifecycle": {
|
|
1579
|
+
if (!input.boardId || !input.author || !input.transitionComment) {
|
|
1580
|
+
return fail(
|
|
1581
|
+
"adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
if (input.columns?.length !== 5) {
|
|
1585
|
+
return fail(
|
|
1586
|
+
"adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const [backlog, todo, running, review, done] = input.columns;
|
|
1590
|
+
if (!backlog || !todo || !running || !review || !done) {
|
|
1591
|
+
return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
|
|
1592
|
+
}
|
|
1593
|
+
const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
|
|
1594
|
+
columns: { backlog, todo, running, review, done },
|
|
1595
|
+
actor: input.author,
|
|
1596
|
+
comment: input.transitionComment
|
|
1597
|
+
});
|
|
1598
|
+
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
1599
|
+
}
|
|
1600
|
+
// Adoption used to be a one-way door: the strict lifecycle carries
|
|
1601
|
+
// acceptance-criteria, verification-report, review-evidence and
|
|
1602
|
+
// one-stage-at-a-time gates, and nothing on the tool surface could
|
|
1603
|
+
// undo it, so a board adopted once kept its ceremony forever. The
|
|
1604
|
+
// gates are worth having where a fleet is supervised; they are not
|
|
1605
|
+
// worth being unable to leave. Cards and columns are untouched.
|
|
1606
|
+
case "release_managed_lifecycle": {
|
|
1607
|
+
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
1608
|
+
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
1609
|
+
return board ? okBoard(
|
|
1610
|
+
board,
|
|
1611
|
+
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
1612
|
+
) : fail("Board not found.");
|
|
1613
|
+
}
|
|
1614
|
+
case "duplicate_board": {
|
|
1615
|
+
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
1616
|
+
const board = await duplicateBoard(
|
|
1617
|
+
projectRoot,
|
|
1618
|
+
input.boardId,
|
|
1619
|
+
duplicateBoardOptions(input)
|
|
1620
|
+
);
|
|
1621
|
+
return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
|
|
1622
|
+
}
|
|
1623
|
+
case "delete_board": {
|
|
1624
|
+
if (!input.boardId) return fail("delete_board requires boardId.");
|
|
1625
|
+
const removed = await removeBoard2(projectRoot, input.boardId);
|
|
1626
|
+
return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
|
|
1627
|
+
}
|
|
1628
|
+
case "generate_board": {
|
|
1629
|
+
if (!input.description) return fail("generate_board requires description.");
|
|
1630
|
+
const boardInput = createBoardFromText({
|
|
1631
|
+
description: input.description,
|
|
1632
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1633
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
1634
|
+
});
|
|
1635
|
+
const board = await createBoard2(projectRoot, boardInput);
|
|
1636
|
+
for (const taskInput2 of parseLinesIntoTasks(
|
|
1637
|
+
input.description,
|
|
1638
|
+
board.columns[0]?.id ?? "backlog"
|
|
1639
|
+
)) {
|
|
1640
|
+
await addTask(projectRoot, board.id, taskInput2);
|
|
1641
|
+
}
|
|
1642
|
+
return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
|
|
1643
|
+
}
|
|
1644
|
+
case "export_markdown": {
|
|
1645
|
+
const board = await requireBoard(projectRoot, input.boardId);
|
|
1646
|
+
if (!board) return fail("Board not found.");
|
|
1647
|
+
return {
|
|
1648
|
+
ok: true,
|
|
1649
|
+
message: "Board exported.",
|
|
1650
|
+
board,
|
|
1651
|
+
markdown: exportBoardAsMarkdown(board)
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
case "export_task_graph": {
|
|
1655
|
+
if (!input.boardId) return fail("export_task_graph requires boardId.");
|
|
1656
|
+
const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
|
|
1657
|
+
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
1658
|
+
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
1659
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1660
|
+
...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
|
|
1661
|
+
...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
|
|
1662
|
+
});
|
|
1663
|
+
if (!exported) return fail("Board not found.");
|
|
1664
|
+
return {
|
|
1665
|
+
ok: true,
|
|
1666
|
+
message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
|
|
1667
|
+
board: exported.board,
|
|
1668
|
+
taskGraph: serializeTaskGraph(exported.graph)
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
case "sync_task_graph": {
|
|
1672
|
+
if (!input.boardId || !input.taskGraph) {
|
|
1673
|
+
return fail("sync_task_graph requires boardId and taskGraph.");
|
|
1674
|
+
}
|
|
1675
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
1676
|
+
const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
|
|
1677
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1678
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1679
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
1680
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
1681
|
+
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
1682
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
1683
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
1684
|
+
...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
|
|
1685
|
+
...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
|
|
1686
|
+
});
|
|
1687
|
+
return result2 ? {
|
|
1688
|
+
ok: true,
|
|
1689
|
+
message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
|
|
1690
|
+
board: result2.board
|
|
1691
|
+
} : fail("Board not found.");
|
|
1692
|
+
}
|
|
1693
|
+
case "create_from_graph": {
|
|
1694
|
+
if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
|
|
1695
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
1696
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
1697
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1698
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1699
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
1700
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
1701
|
+
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
1702
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
1703
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
|
|
1704
|
+
});
|
|
1705
|
+
return {
|
|
1706
|
+
ok: true,
|
|
1707
|
+
message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
|
|
1708
|
+
board
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
case "import_session_tasks": {
|
|
1712
|
+
const taskPath = ctx.meta?.["task.path"];
|
|
1713
|
+
if (!taskPath) return fail("No session task file for this session.");
|
|
1714
|
+
const file = await loadTasks2(taskPath);
|
|
1715
|
+
if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
|
|
1716
|
+
const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
|
|
1717
|
+
const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
|
|
1718
|
+
const tags = ["session", `session:${sessionId}`];
|
|
1719
|
+
const existing = (await listBoards2(projectRoot)).find(
|
|
1720
|
+
(b) => b.tags?.includes(`session:${sessionId}`)
|
|
1721
|
+
);
|
|
1722
|
+
if (existing) {
|
|
1723
|
+
const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
|
|
1724
|
+
sourceSystem: "session",
|
|
1725
|
+
tags,
|
|
1726
|
+
archiveMissingTasks: true,
|
|
1727
|
+
includeCompletedTasks: true
|
|
1728
|
+
});
|
|
1729
|
+
return result2 ? {
|
|
1730
|
+
ok: true,
|
|
1731
|
+
message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
|
|
1732
|
+
board: result2.board
|
|
1733
|
+
} : fail("Session board vanished mid-sync.");
|
|
1734
|
+
}
|
|
1735
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
1736
|
+
title: `Session tasks (${sessionId.slice(0, 8)})`,
|
|
1737
|
+
sourceSystem: "session",
|
|
1738
|
+
tags
|
|
1739
|
+
});
|
|
1740
|
+
return {
|
|
1741
|
+
ok: true,
|
|
1742
|
+
message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
|
|
1743
|
+
board
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
case "search_tasks": {
|
|
1747
|
+
const tasks = await searchKanban(projectRoot, {
|
|
1748
|
+
query: input.query,
|
|
1749
|
+
boardId: input.boardId,
|
|
1750
|
+
assignedAgent: input.agentId,
|
|
1751
|
+
status: input.status,
|
|
1752
|
+
priority: input.priority,
|
|
1753
|
+
label: input.labels?.[0],
|
|
1754
|
+
chainId: input.chainId
|
|
1755
|
+
});
|
|
1756
|
+
return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
|
|
1757
|
+
}
|
|
1758
|
+
case "ready_tasks": {
|
|
1759
|
+
const tasks = await listReadyTasks(projectRoot, {
|
|
1760
|
+
query: input.query,
|
|
1761
|
+
boardId: input.boardId,
|
|
1762
|
+
assignedAgent: input.agentId,
|
|
1763
|
+
priority: input.priority,
|
|
1764
|
+
label: input.labels?.[0],
|
|
1765
|
+
chainId: input.chainId,
|
|
1766
|
+
limit: input.limit
|
|
1767
|
+
});
|
|
1768
|
+
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1769
|
+
}
|
|
1770
|
+
case "snapshot": {
|
|
1771
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
1772
|
+
query: input.query,
|
|
1773
|
+
boardId: input.boardId,
|
|
1774
|
+
assignedAgent: input.agentId,
|
|
1775
|
+
status: input.status,
|
|
1776
|
+
priority: input.priority,
|
|
1777
|
+
label: input.labels?.[0],
|
|
1778
|
+
chainId: input.chainId
|
|
1779
|
+
});
|
|
1780
|
+
return {
|
|
1781
|
+
ok: true,
|
|
1782
|
+
message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
|
|
1783
|
+
snapshot
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
case "add_task": {
|
|
1787
|
+
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1788
|
+
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
1789
|
+
if (!result2) return fail("Board not found.");
|
|
1790
|
+
return okTask(result2.board, result2.task, `Task added.${atomicityNudge(result2.task)}`);
|
|
1791
|
+
}
|
|
1792
|
+
case "split_task": {
|
|
1793
|
+
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
1794
|
+
return fail("split_task requires boardId, taskId, and childTitles.");
|
|
1795
|
+
}
|
|
1796
|
+
return handleSplitTask(projectRoot, input, {});
|
|
1797
|
+
}
|
|
1798
|
+
case "merge_tasks": {
|
|
1799
|
+
if (!input.boardId || !input.taskIds?.length || !input.title) {
|
|
1800
|
+
return fail("merge_tasks requires boardId, taskIds, and title.");
|
|
1801
|
+
}
|
|
1802
|
+
const result2 = await mergeTasks(projectRoot, input.boardId, {
|
|
1803
|
+
taskIds: input.taskIds,
|
|
1804
|
+
title: input.title,
|
|
1805
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1806
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1807
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1808
|
+
...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
|
|
1809
|
+
});
|
|
1810
|
+
return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
|
|
1811
|
+
}
|
|
1812
|
+
case "copy_task": {
|
|
1813
|
+
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1814
|
+
return fail("copy_task requires boardId, taskId, and targetBoardId.");
|
|
1815
|
+
}
|
|
1816
|
+
const result2 = await copyTaskToBoard(
|
|
1817
|
+
projectRoot,
|
|
1818
|
+
input.boardId,
|
|
1819
|
+
input.taskId,
|
|
1820
|
+
input.targetBoardId,
|
|
1821
|
+
{
|
|
1822
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1823
|
+
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1824
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1825
|
+
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1826
|
+
}
|
|
1827
|
+
);
|
|
1828
|
+
return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
|
|
1829
|
+
}
|
|
1830
|
+
case "transfer_task": {
|
|
1831
|
+
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1832
|
+
return fail("transfer_task requires boardId, taskId, and targetBoardId.");
|
|
1833
|
+
}
|
|
1834
|
+
const result2 = await transferTaskToBoard(
|
|
1835
|
+
projectRoot,
|
|
1836
|
+
input.boardId,
|
|
1837
|
+
input.taskId,
|
|
1838
|
+
input.targetBoardId,
|
|
1839
|
+
{
|
|
1840
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1841
|
+
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1842
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1843
|
+
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1844
|
+
}
|
|
1845
|
+
);
|
|
1846
|
+
return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
|
|
1847
|
+
}
|
|
1848
|
+
case "get_task": {
|
|
1849
|
+
if (!input.boardId || !input.taskId)
|
|
1850
|
+
return fail("get_task requires boardId and taskId.");
|
|
1851
|
+
const task = await getTask(projectRoot, input.boardId, input.taskId);
|
|
1852
|
+
return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
|
|
1853
|
+
}
|
|
1854
|
+
case "start_task": {
|
|
1855
|
+
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1856
|
+
return fail("start_task requires boardId, taskId, author, and transitionComment.");
|
|
1857
|
+
}
|
|
1858
|
+
let board = await getBoard3(projectRoot, input.boardId);
|
|
1859
|
+
let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1860
|
+
if (!board || !task) return fail("Board or task not found.");
|
|
1861
|
+
const readiness = evaluateContractGraphReadiness(board, task.id);
|
|
1862
|
+
if (!readiness.ready) {
|
|
1863
|
+
return fail(
|
|
1864
|
+
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
if (board.lifecycle?.mode !== "managed") {
|
|
1868
|
+
const now = /* @__PURE__ */ new Date();
|
|
1869
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1870
|
+
status: "running",
|
|
1871
|
+
agentId: input.agentId ?? input.author,
|
|
1872
|
+
leaseId: input.leaseId ?? randomUUID2(),
|
|
1873
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1874
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1875
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1876
|
+
attempt: input.attempt ?? 1,
|
|
1877
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1878
|
+
});
|
|
1879
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1880
|
+
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
1881
|
+
status: "in_progress"
|
|
1882
|
+
});
|
|
1883
|
+
const current = started ?? assigned;
|
|
1884
|
+
const claimed = task;
|
|
1885
|
+
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
1886
|
+
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
1887
|
+
return okTask(
|
|
1888
|
+
current,
|
|
1889
|
+
currentTask,
|
|
1890
|
+
"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."
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
let stage = task.lifecycle?.currentStage;
|
|
1894
|
+
if (stage === "backlog") {
|
|
1895
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1896
|
+
to: "todo",
|
|
1897
|
+
actor: input.author,
|
|
1898
|
+
comment: input.transitionComment
|
|
1899
|
+
});
|
|
1900
|
+
if (!moved) return fail("Task could not enter Todo.");
|
|
1901
|
+
board = moved.board;
|
|
1902
|
+
task = moved.task;
|
|
1903
|
+
stage = task.lifecycle?.currentStage;
|
|
1904
|
+
}
|
|
1905
|
+
if (stage === "todo" || stage === "review") {
|
|
1906
|
+
const now = /* @__PURE__ */ new Date();
|
|
1907
|
+
const leaseId = input.leaseId ?? randomUUID2();
|
|
1908
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1909
|
+
status: "running",
|
|
1910
|
+
agentId: input.agentId ?? input.author,
|
|
1911
|
+
leaseId,
|
|
1912
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1913
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1914
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1915
|
+
attempt: input.attempt ?? 1,
|
|
1916
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1917
|
+
});
|
|
1918
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1919
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1920
|
+
to: "running",
|
|
1921
|
+
actor: input.author,
|
|
1922
|
+
comment: input.transitionComment
|
|
1923
|
+
});
|
|
1924
|
+
if (!moved) return fail("Task could not enter Running.");
|
|
1925
|
+
board = moved.board;
|
|
1926
|
+
task = moved.task;
|
|
1927
|
+
stage = task.lifecycle?.currentStage;
|
|
1928
|
+
}
|
|
1929
|
+
if (stage !== "running" || task.assignment?.status !== "running") {
|
|
1930
|
+
return fail(
|
|
1931
|
+
`start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
ctx.setCurrentKanbanTask(task.id, board.id);
|
|
1935
|
+
return okTask(
|
|
1936
|
+
board,
|
|
1937
|
+
task,
|
|
1938
|
+
"Task is active; runtime Kanban governance is now bound to this run."
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
case "update_task": {
|
|
1942
|
+
if (!input.boardId || !input.taskId)
|
|
1943
|
+
return fail("update_task requires boardId and taskId.");
|
|
1944
|
+
const board = await updateTask2(
|
|
1945
|
+
projectRoot,
|
|
1946
|
+
input.boardId,
|
|
1947
|
+
input.taskId,
|
|
1948
|
+
taskPatch(input)
|
|
1949
|
+
);
|
|
1950
|
+
return board ? okBoard(board, "Task updated.") : fail("Task not found.");
|
|
1951
|
+
}
|
|
1952
|
+
case "transition_task": {
|
|
1953
|
+
if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
|
|
1954
|
+
return fail(
|
|
1955
|
+
"transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
if (input.lifecycleStage === "done") {
|
|
1959
|
+
const boardBefore = await getBoard3(projectRoot, input.boardId);
|
|
1960
|
+
const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
|
|
1961
|
+
if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
|
|
1962
|
+
const preGate = await verifyTaskCompletion2(
|
|
1963
|
+
projectRoot,
|
|
1964
|
+
input.boardId,
|
|
1965
|
+
taskBefore.id,
|
|
1966
|
+
{
|
|
1967
|
+
persist: false
|
|
1968
|
+
}
|
|
1969
|
+
);
|
|
1970
|
+
await updateTask2(projectRoot, input.boardId, taskBefore.id, {
|
|
1971
|
+
verificationReport: preGate.report,
|
|
1972
|
+
successCriteria: preGate.task.successCriteria
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
|
|
1977
|
+
to: input.lifecycleStage,
|
|
1978
|
+
actor: input.author,
|
|
1979
|
+
comment: input.transitionComment,
|
|
1980
|
+
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
1981
|
+
...input.attachmentUrl !== void 0 ? {
|
|
1982
|
+
attachment: {
|
|
1983
|
+
url: input.attachmentUrl,
|
|
1984
|
+
type: input.attachmentType ?? "url",
|
|
1985
|
+
...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
|
|
1986
|
+
}
|
|
1987
|
+
} : {},
|
|
1988
|
+
patch: taskPatch(input)
|
|
1989
|
+
});
|
|
1990
|
+
if (result2 && input.lifecycleStage === "done" && result2.task.verificationReport) {
|
|
1991
|
+
recordKanbanVerificationEvidence(ctx, result2.task.verificationReport);
|
|
1992
|
+
}
|
|
1993
|
+
return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
|
|
1994
|
+
}
|
|
1995
|
+
case "repair_managed_projection": {
|
|
1996
|
+
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1997
|
+
return fail(
|
|
1998
|
+
"repair_managed_projection requires boardId, taskId, author, and transitionComment."
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
const result2 = await repairManagedTaskProjection(
|
|
2002
|
+
projectRoot,
|
|
2003
|
+
input.boardId,
|
|
2004
|
+
input.taskId,
|
|
2005
|
+
{
|
|
2006
|
+
actor: input.author,
|
|
2007
|
+
comment: input.transitionComment
|
|
2008
|
+
}
|
|
2009
|
+
);
|
|
2010
|
+
return result2 ? okTask(
|
|
2011
|
+
result2.board,
|
|
2012
|
+
result2.task,
|
|
2013
|
+
"Managed card projection repaired from lifecycle history."
|
|
2014
|
+
) : fail("Board or task not found.");
|
|
2015
|
+
}
|
|
2016
|
+
case "move_task": {
|
|
2017
|
+
if (!input.boardId || !input.taskId || !input.targetColumnId) {
|
|
2018
|
+
return fail("move_task requires boardId, taskId, and targetColumnId.");
|
|
2019
|
+
}
|
|
2020
|
+
const board = await moveTask(
|
|
2021
|
+
projectRoot,
|
|
2022
|
+
input.boardId,
|
|
2023
|
+
input.taskId,
|
|
2024
|
+
input.targetColumnId,
|
|
2025
|
+
input.order
|
|
2026
|
+
);
|
|
2027
|
+
return board ? okBoard(board, "Task moved.") : fail("Move failed.");
|
|
2028
|
+
}
|
|
2029
|
+
case "delete_task": {
|
|
2030
|
+
if (!input.boardId || !input.taskId)
|
|
2031
|
+
return fail("delete_task requires boardId and taskId.");
|
|
2032
|
+
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
2033
|
+
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
2034
|
+
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
2035
|
+
}
|
|
2036
|
+
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
2037
|
+
}
|
|
2038
|
+
case "set_chain": {
|
|
2039
|
+
if (!input.boardId || !input.taskIds?.length) {
|
|
2040
|
+
return fail("set_chain requires boardId and taskIds.");
|
|
2041
|
+
}
|
|
2042
|
+
const result2 = await setTaskChain(projectRoot, input.boardId, {
|
|
2043
|
+
taskIds: input.taskIds,
|
|
2044
|
+
...input.chainId !== void 0 ? { chainId: input.chainId } : {},
|
|
2045
|
+
...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
|
|
2046
|
+
});
|
|
2047
|
+
return result2 ? {
|
|
2048
|
+
ok: true,
|
|
2049
|
+
message: `Chain set: ${result2.chainId}`,
|
|
2050
|
+
board: result2.board,
|
|
2051
|
+
chain: result2.tasks
|
|
2052
|
+
} : fail("Board or task not found.");
|
|
2053
|
+
}
|
|
2054
|
+
case "get_chain": {
|
|
2055
|
+
if (!input.boardId || !(input.taskId || input.chainId)) {
|
|
2056
|
+
return fail("get_chain requires boardId and taskId or chainId.");
|
|
2057
|
+
}
|
|
2058
|
+
const result2 = await getTaskChain(
|
|
2059
|
+
projectRoot,
|
|
2060
|
+
input.boardId,
|
|
2061
|
+
input.taskId ?? input.chainId ?? ""
|
|
2062
|
+
);
|
|
2063
|
+
return result2 ? {
|
|
2064
|
+
ok: true,
|
|
2065
|
+
message: `Chain loaded: ${result2.chainId}`,
|
|
2066
|
+
board: result2.board,
|
|
2067
|
+
chain: result2.tasks
|
|
2068
|
+
} : fail("Chain not found.");
|
|
2069
|
+
}
|
|
2070
|
+
case "claim_task": {
|
|
2071
|
+
const result2 = await claimReadyTask(projectRoot, {
|
|
2072
|
+
...input.boardId !== void 0 ? { boardId: input.boardId } : {},
|
|
2073
|
+
...input.taskId !== void 0 ? { taskId: input.taskId } : {},
|
|
2074
|
+
...assignmentInput(input),
|
|
2075
|
+
status: input.assignmentStatus ?? "queued"
|
|
2076
|
+
});
|
|
2077
|
+
return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
|
|
2078
|
+
}
|
|
2079
|
+
case "release_task": {
|
|
2080
|
+
if (!input.boardId || !input.taskId) {
|
|
2081
|
+
return fail("release_task requires boardId and taskId.");
|
|
2082
|
+
}
|
|
2083
|
+
const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
|
|
2084
|
+
...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
|
|
2085
|
+
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
2086
|
+
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
|
|
2087
|
+
});
|
|
2088
|
+
return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
|
|
2089
|
+
}
|
|
2090
|
+
case "assign_task": {
|
|
2091
|
+
if (!input.boardId || !input.taskId)
|
|
2092
|
+
return fail("assign_task requires boardId and taskId.");
|
|
2093
|
+
const board = await assignTask(
|
|
2094
|
+
projectRoot,
|
|
2095
|
+
input.boardId,
|
|
2096
|
+
input.taskId,
|
|
2097
|
+
assignmentInput(input)
|
|
2098
|
+
);
|
|
2099
|
+
return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
|
|
2100
|
+
}
|
|
2101
|
+
case "mark_assignment": {
|
|
2102
|
+
if (!input.boardId || !input.taskId)
|
|
2103
|
+
return fail("mark_assignment requires boardId and taskId.");
|
|
2104
|
+
const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
|
|
2105
|
+
const board = await updateTaskAssignment(
|
|
2106
|
+
projectRoot,
|
|
2107
|
+
input.boardId,
|
|
2108
|
+
input.taskId,
|
|
2109
|
+
{
|
|
2110
|
+
...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
|
|
2111
|
+
...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
|
|
2112
|
+
...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
|
|
2113
|
+
...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
|
|
2114
|
+
...input.error !== void 0 ? { error: input.error } : {},
|
|
2115
|
+
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
2116
|
+
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
2117
|
+
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
2118
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
2119
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
2120
|
+
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
2121
|
+
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
|
|
2122
|
+
},
|
|
2123
|
+
// Ownership fence: when expectedLeaseId is supplied, the write is
|
|
2124
|
+
// applied only if the current assignment still holds this lease.
|
|
2125
|
+
// This prevents a recovered+reassigned stale worker's terminal
|
|
2126
|
+
// mark_assignment from overwriting the successor's state. The check
|
|
2127
|
+
// is atomic inside updateTaskAssignment's mutateBoard lock.
|
|
2128
|
+
input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
2129
|
+
);
|
|
2130
|
+
if (!board) return fail("Task not found.");
|
|
2131
|
+
if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
|
|
2132
|
+
const envGate = readEnvGateEnforcement();
|
|
2133
|
+
const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
|
|
2134
|
+
...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
|
|
2135
|
+
...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
|
|
2136
|
+
});
|
|
2137
|
+
if (finalized) {
|
|
2138
|
+
if (finalized.gate.report) {
|
|
2139
|
+
recordKanbanVerificationEvidence(ctx, finalized.gate.report);
|
|
2140
|
+
}
|
|
2141
|
+
const gateSummary = {
|
|
2142
|
+
enforcement: finalized.gate.enforcement,
|
|
2143
|
+
allowed: finalized.gate.allowed,
|
|
2144
|
+
verdict: finalized.gate.verdict,
|
|
2145
|
+
issues: finalized.gate.issues.map((issue) => issue.message)
|
|
2146
|
+
};
|
|
2147
|
+
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(" | ")}`;
|
|
2148
|
+
return {
|
|
2149
|
+
...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
|
|
2150
|
+
gate: gateSummary
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
} else if (board.lifecycle?.mode === "managed") {
|
|
2154
|
+
const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
|
|
2155
|
+
const stage = managedTask?.lifecycle?.currentStage;
|
|
2156
|
+
const actor = ctx.agentId ?? "kanban-agent";
|
|
2157
|
+
let transitionResult = null;
|
|
2158
|
+
const lifecycleWarnings = [];
|
|
2159
|
+
if (assignmentStatus === "running" && stage === "todo") {
|
|
2160
|
+
try {
|
|
2161
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
2162
|
+
to: "running",
|
|
2163
|
+
actor,
|
|
2164
|
+
comment: "Work started."
|
|
2165
|
+
});
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
lifecycleWarnings.push(
|
|
2168
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (assignmentStatus === "completed" && stage === "running") {
|
|
2173
|
+
const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
|
|
2174
|
+
try {
|
|
2175
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
2176
|
+
to: "review",
|
|
2177
|
+
actor,
|
|
2178
|
+
comment,
|
|
2179
|
+
attachment: {
|
|
2180
|
+
url: `kanban://task/${input.taskId}/result`,
|
|
2181
|
+
title: "Worker completion result",
|
|
2182
|
+
type: "file"
|
|
2183
|
+
},
|
|
2184
|
+
patch: {
|
|
2185
|
+
// Only patch non-description fields so the
|
|
2186
|
+
// original card description is preserved.
|
|
2187
|
+
...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
lifecycleWarnings.push(
|
|
2192
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
if (transitionResult) {
|
|
2196
|
+
const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
|
|
2197
|
+
if (hasCriteria) {
|
|
2198
|
+
try {
|
|
2199
|
+
const verResult = await verifyTaskCompletion2(
|
|
2200
|
+
projectRoot,
|
|
2201
|
+
board.id,
|
|
2202
|
+
input.taskId
|
|
2203
|
+
);
|
|
2204
|
+
if (verResult.report) {
|
|
2205
|
+
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
2206
|
+
}
|
|
2207
|
+
await updateTask2(projectRoot, board.id, input.taskId, {
|
|
2208
|
+
verificationReport: verResult.report,
|
|
2209
|
+
successCriteria: verResult.task.successCriteria
|
|
2210
|
+
});
|
|
2211
|
+
const verdict = verResult.report.verdict;
|
|
2212
|
+
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
2213
|
+
lifecycleWarnings.push(
|
|
2214
|
+
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
2215
|
+
);
|
|
2216
|
+
} else if (verdict === "passed") {
|
|
2217
|
+
try {
|
|
2218
|
+
const doneResult = await transitionTask(
|
|
2219
|
+
projectRoot,
|
|
2220
|
+
board.id,
|
|
2221
|
+
input.taskId,
|
|
2222
|
+
{
|
|
2223
|
+
to: "done",
|
|
2224
|
+
actor,
|
|
2225
|
+
action: "Automated acceptance after verification",
|
|
2226
|
+
comment: "Auto-accepted: verification passed.",
|
|
2227
|
+
attachment: {
|
|
2228
|
+
url: `kanban://task/${input.taskId}/verification`,
|
|
2229
|
+
title: "Auto-verification result",
|
|
2230
|
+
type: "file"
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
);
|
|
2234
|
+
transitionResult = doneResult;
|
|
2235
|
+
} catch (acceptErr) {
|
|
2236
|
+
lifecycleWarnings.push(
|
|
2237
|
+
`Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
} else {
|
|
2241
|
+
lifecycleWarnings.push(
|
|
2242
|
+
`Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
} catch (verifyErr) {
|
|
2246
|
+
lifecycleWarnings.push(
|
|
2247
|
+
`Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
} else {
|
|
2251
|
+
lifecycleWarnings.push(
|
|
2252
|
+
"No automatic success criteria \u2014 card left in Review for manual verification."
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
const responseBoard = transitionResult?.board ?? board;
|
|
2258
|
+
const responseTask = transitionResult?.task ?? managedTask;
|
|
2259
|
+
const msgParts = ["Assignment updated."];
|
|
2260
|
+
if (transitionResult) {
|
|
2261
|
+
msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
|
|
2262
|
+
}
|
|
2263
|
+
for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
|
|
2264
|
+
return okTask(responseBoard, responseTask, msgParts.join(" "));
|
|
2265
|
+
}
|
|
2266
|
+
return okBoard(board, "Assignment updated.");
|
|
2267
|
+
}
|
|
2268
|
+
case "heartbeat_assignment": {
|
|
2269
|
+
if (!input.boardId || !input.taskId) {
|
|
2270
|
+
return fail("heartbeat_assignment requires boardId and taskId.");
|
|
2271
|
+
}
|
|
2272
|
+
const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
|
|
2273
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
2274
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
2275
|
+
// Ownership fence: when expectedLeaseId is supplied, the renewal
|
|
2276
|
+
// is applied only if the current assignment still holds this lease.
|
|
2277
|
+
// This prevents a recovered+reassigned stale worker's heartbeat
|
|
2278
|
+
// from renewing the successor's lease. The check is atomic inside
|
|
2279
|
+
// heartbeatTaskAssignment's mutateBoard lock.
|
|
2280
|
+
...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
2281
|
+
});
|
|
2282
|
+
return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
|
|
2283
|
+
}
|
|
2284
|
+
case "recover_stale": {
|
|
2285
|
+
if (!input.boardId) return fail("recover_stale requires boardId.");
|
|
2286
|
+
const policyFields = [
|
|
2287
|
+
input.recoveryPolicyFailOnCostCeiling !== void 0,
|
|
2288
|
+
input.recoveryPolicyReleaseOnFailureKinds !== void 0,
|
|
2289
|
+
input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
|
|
2290
|
+
input.recoveryPolicyRetryPolicyOverride !== void 0
|
|
2291
|
+
].some(Boolean);
|
|
2292
|
+
const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
|
|
2293
|
+
...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
|
|
2294
|
+
...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
|
|
2295
|
+
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
2296
|
+
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
|
|
2297
|
+
...policyFields ? {
|
|
2298
|
+
policy: {
|
|
2299
|
+
...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
|
|
2300
|
+
...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
|
|
2301
|
+
releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
|
|
2302
|
+
} : {},
|
|
2303
|
+
...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
|
|
2304
|
+
releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
|
|
2305
|
+
} : {},
|
|
2306
|
+
...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
|
|
2307
|
+
retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
|
|
2308
|
+
} : {}
|
|
2309
|
+
}
|
|
2310
|
+
} : {}
|
|
2311
|
+
});
|
|
2312
|
+
return result2 ? {
|
|
2313
|
+
ok: true,
|
|
2314
|
+
message: `Recovered ${result2.tasks.length} stale assignment(s).`,
|
|
2315
|
+
board: result2.board,
|
|
2316
|
+
recoveredTasks: result2.tasks
|
|
2317
|
+
} : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
|
|
2318
|
+
}
|
|
2319
|
+
case "events": {
|
|
2320
|
+
if (!input.boardId) return fail("events requires boardId.");
|
|
2321
|
+
const eventList = await listKanbanEvents(projectRoot, input.boardId);
|
|
2322
|
+
return {
|
|
2323
|
+
ok: true,
|
|
2324
|
+
message: `${eventList.length} event(s).`,
|
|
2325
|
+
events: eventList
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
case "queue_health": {
|
|
2329
|
+
const health = await getKanbanQueueHealth(projectRoot, {
|
|
2330
|
+
...input.boardId !== void 0 ? { boardId: input.boardId } : {}
|
|
2331
|
+
});
|
|
2332
|
+
return {
|
|
2333
|
+
ok: true,
|
|
2334
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
2335
|
+
queueHealth: health
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
// Not every action is handled above. These are dispatched from here,
|
|
2339
|
+
// and the split has already cost real time: an agent that read this
|
|
2340
|
+
// file concluded `add_check` / `update_check` did not exist, wrote
|
|
2341
|
+
// that on a card, and spent a session trying to satisfy a gate it
|
|
2342
|
+
// already had the tool to clear. Keep this index in step with the
|
|
2343
|
+
// handlers.
|
|
2344
|
+
//
|
|
2345
|
+
// kanban-detail-actions.ts workbench · add_dependency ·
|
|
2346
|
+
// add_goal_metric · update_goal_metric · add_check ·
|
|
2347
|
+
// update_check · add_note · add_link · split_atomic
|
|
2348
|
+
// kanban-decomposition-actions.ts verify_completion ·
|
|
2349
|
+
// assess_atomicity · propose_decomposition
|
|
2350
|
+
// kanban-contract-actions.ts get_contract_graph ·
|
|
2351
|
+
// configure_contract_graph · upsert_contract_node ·
|
|
2352
|
+
// remove_contract_node · add_contract_edge · remove_contract_edge
|
|
2353
|
+
default:
|
|
2354
|
+
{
|
|
2355
|
+
const contractResult = await handleKanbanContractAction(
|
|
2356
|
+
projectRoot,
|
|
2357
|
+
input,
|
|
2358
|
+
input.author ?? input.agentId
|
|
2359
|
+
);
|
|
2360
|
+
if (contractResult !== void 0) return contractResult;
|
|
2361
|
+
}
|
|
2362
|
+
{
|
|
2363
|
+
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
2364
|
+
if (detailResult !== void 0) return detailResult;
|
|
2365
|
+
}
|
|
2366
|
+
return fail(`Unknown kanban action: ${input.action}`);
|
|
2367
|
+
}
|
|
2368
|
+
})();
|
|
2369
|
+
return withPresence(result);
|
|
2370
|
+
} catch (err) {
|
|
2371
|
+
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
|
|
2376
|
+
// src/todo.ts
|
|
2377
|
+
function normalizedTitle(value) {
|
|
2378
|
+
return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
|
|
2379
|
+
}
|
|
2380
|
+
function activeBoardId(items, ctx) {
|
|
2381
|
+
const metaKanban = ctx.meta?.["kanban"];
|
|
2382
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
2383
|
+
return ctx.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0) ?? items.find((item) => item.kanbanBoardId)?.kanbanBoardId ?? "";
|
|
2384
|
+
}
|
|
2385
|
+
function bindTodosToBoard(items, previous, board) {
|
|
2386
|
+
const previousById = new Map(previous.map((item) => [item.id, item]));
|
|
2387
|
+
const available = board.tasks.filter(
|
|
2388
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
2389
|
+
).sort(
|
|
2390
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.order - right.order
|
|
2391
|
+
);
|
|
2392
|
+
const used = /* @__PURE__ */ new Set();
|
|
2393
|
+
return items.map((item) => {
|
|
2394
|
+
const previousItem = previousById.get(item.id);
|
|
2395
|
+
const requestedTaskId = item.kanbanBoardId === board.id ? item.kanbanTaskId : previousItem?.kanbanBoardId === board.id ? previousItem.kanbanTaskId : void 0;
|
|
2396
|
+
const title = normalizedTitle(item.content);
|
|
2397
|
+
const candidates = [
|
|
2398
|
+
requestedTaskId ? board.tasks.find((task2) => task2.id === requestedTaskId) : void 0,
|
|
2399
|
+
board.tasks.find((task2) => task2.id === item.id),
|
|
2400
|
+
board.tasks.find((task2) => task2.origin?.taskId === item.id),
|
|
2401
|
+
available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
|
|
2402
|
+
];
|
|
2403
|
+
const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
|
|
2404
|
+
if (!task) {
|
|
2405
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
2406
|
+
return { ...rest };
|
|
2407
|
+
}
|
|
2408
|
+
used.add(task.id);
|
|
2409
|
+
const blockedBy = blockingTitles(board, task);
|
|
2410
|
+
return {
|
|
2411
|
+
...item,
|
|
2412
|
+
kanbanBoardId: board.id,
|
|
2413
|
+
kanbanTaskId: task.id,
|
|
2414
|
+
...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
|
|
2415
|
+
};
|
|
2416
|
+
});
|
|
116
2417
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const result = await syncBoardFromTaskGraph(
|
|
123
|
-
projectRoot,
|
|
124
|
-
board.id,
|
|
125
|
-
deserializeTaskGraph(graph),
|
|
126
|
-
{
|
|
127
|
-
sourceSystem,
|
|
128
|
-
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
129
|
-
archiveMissingTasks: true,
|
|
130
|
-
includeCompletedTasks: true
|
|
131
|
-
}
|
|
2418
|
+
function demoteBlockedInProgress(items, warnings) {
|
|
2419
|
+
return items.map((item) => {
|
|
2420
|
+
if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
|
|
2421
|
+
warnings.push(
|
|
2422
|
+
`"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
|
|
132
2423
|
);
|
|
133
|
-
return
|
|
2424
|
+
return { ...item, status: "pending" };
|
|
134
2425
|
});
|
|
135
2426
|
}
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (activeMirrors.has(key)) return;
|
|
141
|
-
activeMirrors.add(key);
|
|
142
|
-
void (async () => {
|
|
2427
|
+
async function createMissingManagedCards(items, board, ctx, warnings) {
|
|
2428
|
+
const created = /* @__PURE__ */ new Map();
|
|
2429
|
+
for (const item of items) {
|
|
2430
|
+
if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
|
|
143
2431
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
pending.sessionId,
|
|
152
|
-
pending.graph,
|
|
153
|
-
pending.sourceSystem
|
|
154
|
-
);
|
|
155
|
-
} catch {
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
} finally {
|
|
159
|
-
activeMirrors.delete(key);
|
|
160
|
-
const pending = pendingMirrors.get(key);
|
|
161
|
-
if (pending) {
|
|
162
|
-
pendingMirrors.delete(key);
|
|
163
|
-
queueLatestMirror(
|
|
164
|
-
pending.projectRoot,
|
|
165
|
-
pending.sessionId,
|
|
166
|
-
pending.graph,
|
|
167
|
-
pending.sourceSystem
|
|
168
|
-
);
|
|
2432
|
+
const result = await addTask2(ctx.projectRoot, board.id, {
|
|
2433
|
+
title: item.content,
|
|
2434
|
+
description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
|
|
2435
|
+
});
|
|
2436
|
+
if (!result) {
|
|
2437
|
+
warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
|
|
2438
|
+
continue;
|
|
169
2439
|
}
|
|
2440
|
+
created.set(item.id, result.task.id);
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
warnings.push(
|
|
2443
|
+
`Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
|
|
2444
|
+
);
|
|
170
2445
|
}
|
|
171
|
-
}
|
|
2446
|
+
}
|
|
2447
|
+
return created;
|
|
172
2448
|
}
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
updatedAt: index
|
|
186
|
-
}));
|
|
187
|
-
const edges = tasks.flatMap(
|
|
188
|
-
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
189
|
-
id: `${dependency}->${task.id}`,
|
|
190
|
-
from: dependency,
|
|
191
|
-
to: task.id,
|
|
192
|
-
type: "depends_on"
|
|
193
|
-
}))
|
|
194
|
-
);
|
|
195
|
-
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
196
|
-
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
197
|
-
return {
|
|
198
|
-
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
199
|
-
id: `session:${sessionId}`,
|
|
200
|
-
specId: `session:${sessionId}`,
|
|
201
|
-
title: "Session tasks",
|
|
202
|
-
nodes,
|
|
203
|
-
edges,
|
|
204
|
-
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
205
|
-
createdAt: 0,
|
|
206
|
-
updatedAt: 0
|
|
2449
|
+
async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
2450
|
+
let synced = 0;
|
|
2451
|
+
const warnings = [];
|
|
2452
|
+
const actor = ctx.agentId?.trim() || ctx.agentName?.trim() || "kanban-agent";
|
|
2453
|
+
const execute = async (input) => {
|
|
2454
|
+
const result = await kanbanTool.execute(input, ctx, { signal });
|
|
2455
|
+
if (!result.ok) warnings.push(result.message);
|
|
2456
|
+
else {
|
|
2457
|
+
synced++;
|
|
2458
|
+
if (result.message.includes("Warning:")) warnings.push(result.message);
|
|
2459
|
+
}
|
|
2460
|
+
return result;
|
|
207
2461
|
};
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
2462
|
+
for (const item of items) {
|
|
2463
|
+
if (item.status !== "pending" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2467
|
+
if (task?.lifecycle?.currentStage !== "running") continue;
|
|
2468
|
+
const released = await execute({
|
|
2469
|
+
action: "mark_assignment",
|
|
2470
|
+
boardId: board.id,
|
|
2471
|
+
taskId: task.id,
|
|
2472
|
+
assignmentStatus: "assigned",
|
|
2473
|
+
agentId: actor,
|
|
2474
|
+
lastResult: `Todo returned to queue: ${item.content}`
|
|
2475
|
+
});
|
|
2476
|
+
if (!released.ok) continue;
|
|
2477
|
+
await execute({
|
|
2478
|
+
action: "transition_task",
|
|
2479
|
+
boardId: board.id,
|
|
2480
|
+
taskId: task.id,
|
|
2481
|
+
lifecycleStage: "todo",
|
|
2482
|
+
author: actor,
|
|
2483
|
+
transitionComment: `Todo returned to queue: ${item.content}`
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
for (const item of items) {
|
|
2487
|
+
if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2491
|
+
if (task?.status !== "completed") continue;
|
|
2492
|
+
warnings.push(
|
|
2493
|
+
`"${item.content}" is already Done on the Kanban board and a completed card cannot be reopened; the row stays completed. Create a follow-up card for any remaining work.`
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
for (const item of items) {
|
|
2497
|
+
if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2501
|
+
if (!task || task.status === "completed") continue;
|
|
2502
|
+
await execute({
|
|
2503
|
+
action: "mark_assignment",
|
|
2504
|
+
boardId: board.id,
|
|
2505
|
+
taskId: task.id,
|
|
2506
|
+
assignmentStatus: "completed",
|
|
2507
|
+
agentId: actor,
|
|
2508
|
+
lastResult: `Todo completed: ${item.content}`
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
const attemptedParents = /* @__PURE__ */ new Set();
|
|
2512
|
+
let afterCompletions = await getBoard4(ctx.projectRoot, board.id);
|
|
2513
|
+
while (afterCompletions) {
|
|
2514
|
+
const parent = afterCompletions.tasks.find(
|
|
2515
|
+
(task) => task.atomic === true && task.status !== "completed" && Boolean(task.childTaskIds?.length) && !attemptedParents.has(task.id) && task.childTaskIds?.every(
|
|
2516
|
+
(childId) => afterCompletions?.tasks.find((candidate) => candidate.id === childId)?.status === "completed"
|
|
2517
|
+
)
|
|
2518
|
+
);
|
|
2519
|
+
if (!parent) break;
|
|
2520
|
+
attemptedParents.add(parent.id);
|
|
2521
|
+
const started = await execute({
|
|
2522
|
+
action: "start_task",
|
|
2523
|
+
boardId: board.id,
|
|
2524
|
+
taskId: parent.id,
|
|
2525
|
+
author: actor,
|
|
2526
|
+
agentId: actor,
|
|
2527
|
+
transitionComment: "All child tasks completed; validating composite parent."
|
|
2528
|
+
});
|
|
2529
|
+
if (started.ok) {
|
|
2530
|
+
await execute({
|
|
2531
|
+
action: "mark_assignment",
|
|
2532
|
+
boardId: board.id,
|
|
2533
|
+
taskId: parent.id,
|
|
2534
|
+
assignmentStatus: "completed",
|
|
2535
|
+
agentId: actor,
|
|
2536
|
+
lastResult: "All child tasks completed; composite result ready for verification."
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
afterCompletions = await getBoard4(ctx.projectRoot, board.id);
|
|
2540
|
+
}
|
|
2541
|
+
const completionPending = items.some(
|
|
2542
|
+
(item) => item.status === "completed" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId) && afterCompletions?.tasks.find((task) => task.id === item.kanbanTaskId)?.status !== "completed"
|
|
215
2543
|
);
|
|
2544
|
+
const active = items.find(
|
|
2545
|
+
(item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
|
|
2546
|
+
);
|
|
2547
|
+
const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
|
|
2548
|
+
if (active?.kanbanTaskId) {
|
|
2549
|
+
if (activeStage === "review" || activeStage === "done") {
|
|
2550
|
+
warnings.push(
|
|
2551
|
+
`"${active.content}" is in ${activeStage === "review" ? "Review" : "Done"} awaiting acceptance; not re-activating it from the todo list. ` + (activeStage === "review" ? "Call kanban start_task explicitly to reopen it as a repair." : "Done is terminal; reopen only by creating a follow-up card.")
|
|
2552
|
+
);
|
|
2553
|
+
} else {
|
|
2554
|
+
await execute({
|
|
2555
|
+
action: "start_task",
|
|
2556
|
+
boardId: board.id,
|
|
2557
|
+
taskId: active.kanbanTaskId,
|
|
2558
|
+
author: actor,
|
|
2559
|
+
agentId: actor,
|
|
2560
|
+
transitionComment: `Todo activated: ${active.content}`
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
if (active?.kanbanTaskId && activeStage === "review") {
|
|
2565
|
+
} else if (active?.kanbanTaskId && completionPending) {
|
|
2566
|
+
warnings.push(
|
|
2567
|
+
"A completed todo is still awaiting acceptance; the next independent Kanban task was started."
|
|
2568
|
+
);
|
|
2569
|
+
} else if (!active && ctx.currentKanbanBoardId === board.id) {
|
|
2570
|
+
ctx.setCurrentKanbanTask(void 0, board.id);
|
|
2571
|
+
}
|
|
2572
|
+
return { synced, warnings };
|
|
216
2573
|
}
|
|
2574
|
+
var todoTool = {
|
|
2575
|
+
name: "todo",
|
|
2576
|
+
category: "Session",
|
|
2577
|
+
description: "Manage the compact active-work list. With a managed Kanban binding, every row resolves to a real card and status changes advance that card; without Kanban it remains session-only state. Each call replaces ordering and supplied fields, but unfinished omitted rows are retained until completed.",
|
|
2578
|
+
usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- **Re-order items** to reflect current priorities. Omission is not cancellation: unfinished rows are retained until completed.\n- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
|
|
2579
|
+
permission: "confirm",
|
|
2580
|
+
mutating: true,
|
|
2581
|
+
timeoutMs: 3e4,
|
|
2582
|
+
capabilities: ["session.todo", "fs.write"],
|
|
2583
|
+
subjectKey: "todos",
|
|
2584
|
+
icon: "todo",
|
|
2585
|
+
inputSchema: {
|
|
2586
|
+
type: "object",
|
|
2587
|
+
properties: {
|
|
2588
|
+
todos: {
|
|
2589
|
+
type: "array",
|
|
2590
|
+
items: {
|
|
2591
|
+
type: "object",
|
|
2592
|
+
properties: {
|
|
2593
|
+
id: {
|
|
2594
|
+
type: "string",
|
|
2595
|
+
description: 'Unique identifier for the todo item (e.g. "1", "auth-flow").'
|
|
2596
|
+
},
|
|
2597
|
+
content: {
|
|
2598
|
+
type: "string",
|
|
2599
|
+
description: "Clear, actionable description of the task."
|
|
2600
|
+
},
|
|
2601
|
+
status: {
|
|
2602
|
+
type: "string",
|
|
2603
|
+
enum: ["pending", "in_progress", "completed"],
|
|
2604
|
+
description: 'Current status. Only one item should be "in_progress" at a time.'
|
|
2605
|
+
},
|
|
2606
|
+
activeForm: {
|
|
2607
|
+
type: "string",
|
|
2608
|
+
description: 'Optional present-tense form shown while the task is active (e.g. "Fixing auth bug").'
|
|
2609
|
+
},
|
|
2610
|
+
kanbanBoardId: {
|
|
2611
|
+
type: "string",
|
|
2612
|
+
description: "Kanban board that owns this UI row when Kanban is active."
|
|
2613
|
+
},
|
|
2614
|
+
kanbanTaskId: {
|
|
2615
|
+
type: "string",
|
|
2616
|
+
description: "Real Kanban task represented by this UI row."
|
|
2617
|
+
}
|
|
2618
|
+
},
|
|
2619
|
+
required: ["id", "content", "status"]
|
|
2620
|
+
},
|
|
2621
|
+
description: "The desired todo list. Supplied rows are replaced/reordered; unfinished omitted rows are retained."
|
|
2622
|
+
}
|
|
2623
|
+
},
|
|
2624
|
+
required: ["todos"]
|
|
2625
|
+
},
|
|
2626
|
+
async execute(input, ctx, call) {
|
|
2627
|
+
if (!Array.isArray(input?.todos)) {
|
|
2628
|
+
throw new Error("todo: todos must be an array");
|
|
2629
|
+
}
|
|
2630
|
+
const items = input.todos.filter((t) => Boolean(t?.id && t.content));
|
|
2631
|
+
const todoIdentity = (item) => item.kanbanBoardId && item.kanbanTaskId ? `kanban:${item.kanbanBoardId}:${item.kanbanTaskId}` : item.promotedFromTask ? `task:${item.promotedFromTask}` : item.promotedFromPlan ? `plan:${item.promotedFromPlan}` : `todo:${item.id}`;
|
|
2632
|
+
const requestedIdentities = new Set(items.map(todoIdentity));
|
|
2633
|
+
for (const previous of ctx.todos ?? []) {
|
|
2634
|
+
const identity = todoIdentity(previous);
|
|
2635
|
+
if (previous.status === "completed" || requestedIdentities.has(identity)) {
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
items.push({ ...previous });
|
|
2639
|
+
requestedIdentities.add(identity);
|
|
2640
|
+
}
|
|
2641
|
+
const inProgress = items.filter((t) => t.status === "in_progress");
|
|
2642
|
+
if (inProgress.length > 1) {
|
|
2643
|
+
let seenInProgress = false;
|
|
2644
|
+
for (const item of items) {
|
|
2645
|
+
if (item.status === "in_progress") {
|
|
2646
|
+
if (seenInProgress) item.status = "pending";
|
|
2647
|
+
seenInProgress = true;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
const boardId = activeBoardId(items, ctx);
|
|
2652
|
+
let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
|
|
2653
|
+
const managed = board?.lifecycle?.mode === "managed";
|
|
2654
|
+
let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
2655
|
+
const creationWarnings = [];
|
|
2656
|
+
if (managed && board) {
|
|
2657
|
+
const managedBoardId = board.id;
|
|
2658
|
+
const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
|
|
2659
|
+
if (created.size > 0) {
|
|
2660
|
+
boundItems = boundItems.map((item) => {
|
|
2661
|
+
const taskId = created.get(item.id);
|
|
2662
|
+
return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
|
|
2663
|
+
});
|
|
2664
|
+
board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
|
|
2665
|
+
boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
|
|
2666
|
+
}
|
|
2667
|
+
boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
|
|
2668
|
+
}
|
|
2669
|
+
ctx.state.replaceTodos(boundItems);
|
|
2670
|
+
const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
|
|
2671
|
+
kanbanSync.warnings.unshift(...creationWarnings);
|
|
2672
|
+
if (managed && board) {
|
|
2673
|
+
const unresolved = boundItems.filter(
|
|
2674
|
+
(item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
|
|
2675
|
+
);
|
|
2676
|
+
if (unresolved.length > 0) {
|
|
2677
|
+
kanbanSync.warnings.push(
|
|
2678
|
+
`${unresolved.length} Todo row(s) could not be bound to a Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
|
|
2683
|
+
if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
|
|
2684
|
+
let projectedBoard = board;
|
|
2685
|
+
if (managed && board) {
|
|
2686
|
+
const refreshed = await getBoard4(ctx.projectRoot, board.id);
|
|
2687
|
+
if (refreshed) {
|
|
2688
|
+
projectedBoard = refreshed;
|
|
2689
|
+
applyManagedKanbanBoardToTodos(ctx, refreshed);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
if (!managed) {
|
|
2693
|
+
mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
2694
|
+
}
|
|
2695
|
+
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
2696
|
+
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
2697
|
+
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
2698
|
+
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
2699
|
+
for (const item of items) {
|
|
2700
|
+
if (item.promotedFromPlan) {
|
|
2701
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
|
|
2702
|
+
item.promotedFromPlan
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
if (item.promotedFromTask) {
|
|
2706
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
|
|
2707
|
+
item.promotedFromTask
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
for (const planId of completedPlanIds) {
|
|
2712
|
+
if (pendingPlanIds.has(planId)) continue;
|
|
2713
|
+
const planPath = ctx.meta["plan.path"];
|
|
2714
|
+
if (typeof planPath !== "string" || !planPath) continue;
|
|
2715
|
+
try {
|
|
2716
|
+
const plan = await loadPlan2(planPath);
|
|
2717
|
+
if (plan) {
|
|
2718
|
+
const updated = setPlanItemStatus(plan, planId, "done");
|
|
2719
|
+
await savePlan(planPath, updated);
|
|
2720
|
+
}
|
|
2721
|
+
} catch {
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
for (const taskId of completedTaskIds) {
|
|
2725
|
+
if (pendingTaskIds.has(taskId)) continue;
|
|
2726
|
+
const taskPath = ctx.meta["task.path"];
|
|
2727
|
+
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
2728
|
+
try {
|
|
2729
|
+
const file = await loadTasks3(taskPath);
|
|
2730
|
+
if (file) {
|
|
2731
|
+
const task = file.tasks.find((t) => t.id === taskId);
|
|
2732
|
+
if (task && task.status !== "completed") {
|
|
2733
|
+
task.status = "completed";
|
|
2734
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2735
|
+
await saveTasks(taskPath, file);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
} catch {
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
return {
|
|
2742
|
+
count: items.length,
|
|
2743
|
+
in_progress: (ctx.todos ?? boundItems).filter((t) => t.status === "in_progress").length,
|
|
2744
|
+
...kanbanSync.synced > 0 ? { kanban_synced: kanbanSync.synced } : {},
|
|
2745
|
+
...kanbanSync.warnings.length > 0 ? { kanban_warnings: kanbanSync.warnings } : {},
|
|
2746
|
+
...projectedBoard?.lifecycle?.mode === "managed" ? {
|
|
2747
|
+
kanban_bindings: boundItems.flatMap((item) => {
|
|
2748
|
+
if (item.kanbanBoardId !== projectedBoard.id || !item.kanbanTaskId) return [];
|
|
2749
|
+
const task = projectedBoard.tasks.find(
|
|
2750
|
+
(candidate) => candidate.id === item.kanbanTaskId
|
|
2751
|
+
);
|
|
2752
|
+
return task ? [
|
|
2753
|
+
{
|
|
2754
|
+
todoId: item.id,
|
|
2755
|
+
boardId: projectedBoard.id,
|
|
2756
|
+
taskId: task.id,
|
|
2757
|
+
taskStatus: task.status
|
|
2758
|
+
}
|
|
2759
|
+
] : [];
|
|
2760
|
+
})
|
|
2761
|
+
} : {}
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
217
2765
|
|
|
218
2766
|
// src/task.ts
|
|
219
2767
|
function findTaskIndex(tasks, query) {
|
|
@@ -231,9 +2779,10 @@ var taskTool = {
|
|
|
231
2779
|
name: "task",
|
|
232
2780
|
category: "Session",
|
|
233
2781
|
description: 'Manage session-persistent structured work items with dependencies, types, and priorities. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. Tasks are written to disk and survive session resumes. By default they are isolated to this session; use `scope: "project"` to store tasks in a shared project-level file visible to all sessions.',
|
|
234
|
-
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014
|
|
2782
|
+
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 replace task details/order without omitting unfinished persisted tasks\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate\n- `scope`: "session" (default, isolated) or "project" (shared across sessions)',
|
|
235
2783
|
permission: "confirm",
|
|
236
2784
|
mutating: true,
|
|
2785
|
+
subjectKey: "action",
|
|
237
2786
|
capabilities: ["fs.write"],
|
|
238
2787
|
icon: "task",
|
|
239
2788
|
timeoutMs: 5e3,
|
|
@@ -253,9 +2802,15 @@ var taskTool = {
|
|
|
253
2802
|
id: { type: "string", description: 'Unique id (e.g. "t1", "auth-flow").' },
|
|
254
2803
|
title: { type: "string", description: "Short title." },
|
|
255
2804
|
description: { type: "string", description: "Optional details." },
|
|
256
|
-
type: {
|
|
2805
|
+
type: {
|
|
2806
|
+
type: "string",
|
|
2807
|
+
enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
|
|
2808
|
+
},
|
|
257
2809
|
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
258
|
-
status: {
|
|
2810
|
+
status: {
|
|
2811
|
+
type: "string",
|
|
2812
|
+
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"]
|
|
2813
|
+
},
|
|
259
2814
|
dependsOn: {
|
|
260
2815
|
type: "array",
|
|
261
2816
|
items: { type: "string" },
|
|
@@ -269,16 +2824,22 @@ var taskTool = {
|
|
|
269
2824
|
},
|
|
270
2825
|
required: ["id", "title", "type", "priority", "status"]
|
|
271
2826
|
},
|
|
272
|
-
description: "Complete task list.
|
|
2827
|
+
description: "Complete desired task list. Existing unfinished tasks may not be omitted; complete them first."
|
|
273
2828
|
},
|
|
274
2829
|
task: {
|
|
275
2830
|
type: "object",
|
|
276
2831
|
properties: {
|
|
277
2832
|
title: { type: "string" },
|
|
278
2833
|
description: { type: "string" },
|
|
279
|
-
type: {
|
|
2834
|
+
type: {
|
|
2835
|
+
type: "string",
|
|
2836
|
+
enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
|
|
2837
|
+
},
|
|
280
2838
|
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
281
|
-
status: {
|
|
2839
|
+
status: {
|
|
2840
|
+
type: "string",
|
|
2841
|
+
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"]
|
|
2842
|
+
},
|
|
282
2843
|
dependsOn: { type: "array", items: { type: "string" } },
|
|
283
2844
|
assignee: { type: "string" },
|
|
284
2845
|
estimateHours: { type: "number" },
|
|
@@ -287,7 +2848,10 @@ var taskTool = {
|
|
|
287
2848
|
required: ["title", "type", "priority"],
|
|
288
2849
|
description: "Single task to append (id/createdAt/updatedAt auto-generated)."
|
|
289
2850
|
},
|
|
290
|
-
id: {
|
|
2851
|
+
id: {
|
|
2852
|
+
type: "string",
|
|
2853
|
+
description: "Task id for action=status or target for action=promote."
|
|
2854
|
+
},
|
|
291
2855
|
status: {
|
|
292
2856
|
type: "string",
|
|
293
2857
|
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
|
|
@@ -315,14 +2879,23 @@ var taskTool = {
|
|
|
315
2879
|
let taskPath;
|
|
316
2880
|
if (input.scope === "project") {
|
|
317
2881
|
if (typeof sessionTaskPath === "string") {
|
|
318
|
-
const lastSep = Math.max(
|
|
2882
|
+
const lastSep = Math.max(
|
|
2883
|
+
sessionTaskPath.lastIndexOf("/"),
|
|
2884
|
+
sessionTaskPath.lastIndexOf("\\")
|
|
2885
|
+
);
|
|
319
2886
|
taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
320
2887
|
}
|
|
321
2888
|
} else {
|
|
322
2889
|
taskPath = sessionTaskPath;
|
|
323
2890
|
}
|
|
324
2891
|
if (typeof taskPath !== "string" || !taskPath) {
|
|
325
|
-
return {
|
|
2892
|
+
return {
|
|
2893
|
+
ok: false,
|
|
2894
|
+
message: "Task storage path not configured.",
|
|
2895
|
+
count: 0,
|
|
2896
|
+
completed: 0,
|
|
2897
|
+
inProgress: 0
|
|
2898
|
+
};
|
|
326
2899
|
}
|
|
327
2900
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
328
2901
|
let early = null;
|
|
@@ -338,17 +2911,27 @@ var taskTool = {
|
|
|
338
2911
|
break;
|
|
339
2912
|
case "replace": {
|
|
340
2913
|
if (!Array.isArray(input.tasks)) {
|
|
341
|
-
early = {
|
|
2914
|
+
early = {
|
|
2915
|
+
ok: false,
|
|
2916
|
+
message: "action=replace requires `tasks` array.",
|
|
2917
|
+
count: 0,
|
|
2918
|
+
completed: 0,
|
|
2919
|
+
inProgress: 0
|
|
2920
|
+
};
|
|
342
2921
|
return f;
|
|
343
2922
|
}
|
|
344
2923
|
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
345
2924
|
if (newIds.size !== input.tasks.length) {
|
|
346
2925
|
const seen = /* @__PURE__ */ new Set();
|
|
347
|
-
const dupes = [
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
2926
|
+
const dupes = [
|
|
2927
|
+
...new Set(
|
|
2928
|
+
input.tasks.map((t) => t.id).filter((id) => {
|
|
2929
|
+
if (seen.has(id)) return true;
|
|
2930
|
+
seen.add(id);
|
|
2931
|
+
return false;
|
|
2932
|
+
})
|
|
2933
|
+
)
|
|
2934
|
+
];
|
|
352
2935
|
early = {
|
|
353
2936
|
ok: false,
|
|
354
2937
|
message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
|
|
@@ -358,6 +2941,18 @@ var taskTool = {
|
|
|
358
2941
|
};
|
|
359
2942
|
return f;
|
|
360
2943
|
}
|
|
2944
|
+
const omittedUnfinished = f.tasks.filter(
|
|
2945
|
+
(task) => task.status !== "completed" && !newIds.has(task.id)
|
|
2946
|
+
);
|
|
2947
|
+
if (omittedUnfinished.length > 0) {
|
|
2948
|
+
early = {
|
|
2949
|
+
ok: false,
|
|
2950
|
+
message: `action=replace cannot omit unfinished tasks: ${omittedUnfinished.map((task) => task.id).join(", ")}. Complete them first.`,
|
|
2951
|
+
count: f.tasks.length,
|
|
2952
|
+
...computeTaskItemProgress(f.tasks)
|
|
2953
|
+
};
|
|
2954
|
+
return f;
|
|
2955
|
+
}
|
|
361
2956
|
for (const t of input.tasks) {
|
|
362
2957
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
363
2958
|
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
@@ -372,6 +2967,20 @@ var taskTool = {
|
|
|
372
2967
|
return f;
|
|
373
2968
|
}
|
|
374
2969
|
}
|
|
2970
|
+
if (t.status === "in_progress" || t.status === "completed") {
|
|
2971
|
+
const unmet = (t.dependsOn ?? []).filter(
|
|
2972
|
+
(dependencyId) => input.tasks?.find((candidate) => candidate.id === dependencyId)?.status !== "completed"
|
|
2973
|
+
);
|
|
2974
|
+
if (unmet.length > 0) {
|
|
2975
|
+
early = {
|
|
2976
|
+
ok: false,
|
|
2977
|
+
message: `dependency status validation failed: task "${t.id}" cannot be ${t.status} before completion of ${unmet.join(", ")}.`,
|
|
2978
|
+
count: f.tasks.length,
|
|
2979
|
+
...computeTaskItemProgress(f.tasks)
|
|
2980
|
+
};
|
|
2981
|
+
return f;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
375
2984
|
}
|
|
376
2985
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
377
2986
|
f.tasks = input.tasks.map((t) => ({
|
|
@@ -384,7 +2993,13 @@ var taskTool = {
|
|
|
384
2993
|
case "add": {
|
|
385
2994
|
const t = input.task;
|
|
386
2995
|
if (!t?.title) {
|
|
387
|
-
early = {
|
|
2996
|
+
early = {
|
|
2997
|
+
ok: false,
|
|
2998
|
+
message: "action=add requires `task` with at least `title`.",
|
|
2999
|
+
count: 0,
|
|
3000
|
+
completed: 0,
|
|
3001
|
+
inProgress: 0
|
|
3002
|
+
};
|
|
388
3003
|
return f;
|
|
389
3004
|
}
|
|
390
3005
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
@@ -403,7 +3018,7 @@ var taskTool = {
|
|
|
403
3018
|
}
|
|
404
3019
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
405
3020
|
const newTask = {
|
|
406
|
-
id: `task_${Date.now()}_${
|
|
3021
|
+
id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
|
|
407
3022
|
title: t.title,
|
|
408
3023
|
description: t.description,
|
|
409
3024
|
type: t.type || "feature",
|
|
@@ -421,14 +3036,40 @@ var taskTool = {
|
|
|
421
3036
|
}
|
|
422
3037
|
case "status": {
|
|
423
3038
|
if (!input.id || !input.status) {
|
|
424
|
-
early = {
|
|
3039
|
+
early = {
|
|
3040
|
+
ok: false,
|
|
3041
|
+
message: "action=status requires `id` and `status`.",
|
|
3042
|
+
count: 0,
|
|
3043
|
+
completed: 0,
|
|
3044
|
+
inProgress: 0
|
|
3045
|
+
};
|
|
425
3046
|
return f;
|
|
426
3047
|
}
|
|
427
3048
|
const task = f.tasks.find((t) => t.id === input.id);
|
|
428
3049
|
if (!task) {
|
|
429
|
-
early = {
|
|
3050
|
+
early = {
|
|
3051
|
+
ok: false,
|
|
3052
|
+
message: `Task "${input.id}" not found.`,
|
|
3053
|
+
count: 0,
|
|
3054
|
+
completed: 0,
|
|
3055
|
+
inProgress: 0
|
|
3056
|
+
};
|
|
430
3057
|
return f;
|
|
431
3058
|
}
|
|
3059
|
+
if (input.status === "in_progress" || input.status === "completed") {
|
|
3060
|
+
const unmet = (task.dependsOn ?? []).filter(
|
|
3061
|
+
(dependencyId) => f.tasks.find((candidate) => candidate.id === dependencyId)?.status !== "completed"
|
|
3062
|
+
);
|
|
3063
|
+
if (unmet.length > 0) {
|
|
3064
|
+
early = {
|
|
3065
|
+
ok: false,
|
|
3066
|
+
message: `Task "${task.id}" cannot be ${input.status} before dependencies complete: ${unmet.join(", ")}.`,
|
|
3067
|
+
count: f.tasks.length,
|
|
3068
|
+
...computeTaskItemProgress(f.tasks)
|
|
3069
|
+
};
|
|
3070
|
+
return f;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
432
3073
|
task.status = input.status;
|
|
433
3074
|
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
434
3075
|
break;
|
|
@@ -436,17 +3077,35 @@ var taskTool = {
|
|
|
436
3077
|
case "promote": {
|
|
437
3078
|
const target = input.target?.trim();
|
|
438
3079
|
if (!target) {
|
|
439
|
-
early = {
|
|
3080
|
+
early = {
|
|
3081
|
+
ok: false,
|
|
3082
|
+
message: "action=promote requires `target` (task id, index, or title substring).",
|
|
3083
|
+
count: 0,
|
|
3084
|
+
completed: 0,
|
|
3085
|
+
inProgress: 0
|
|
3086
|
+
};
|
|
440
3087
|
return f;
|
|
441
3088
|
}
|
|
442
3089
|
const idx = findTaskIndex(f.tasks, target);
|
|
443
3090
|
if (idx === -1) {
|
|
444
|
-
early = {
|
|
3091
|
+
early = {
|
|
3092
|
+
ok: false,
|
|
3093
|
+
message: `No task matched "${target}".`,
|
|
3094
|
+
count: 0,
|
|
3095
|
+
completed: 0,
|
|
3096
|
+
inProgress: 0
|
|
3097
|
+
};
|
|
445
3098
|
return f;
|
|
446
3099
|
}
|
|
447
3100
|
const match = f.tasks[idx];
|
|
448
3101
|
if (!match) {
|
|
449
|
-
early = {
|
|
3102
|
+
early = {
|
|
3103
|
+
ok: false,
|
|
3104
|
+
message: `No task matched "${target}".`,
|
|
3105
|
+
count: 0,
|
|
3106
|
+
completed: 0,
|
|
3107
|
+
inProgress: 0
|
|
3108
|
+
};
|
|
450
3109
|
return f;
|
|
451
3110
|
}
|
|
452
3111
|
if (match.status !== "completed" && match.status !== "failed") {
|
|
@@ -464,7 +3123,7 @@ var taskTool = {
|
|
|
464
3123
|
});
|
|
465
3124
|
if (match.description) {
|
|
466
3125
|
todos.push({
|
|
467
|
-
id: `todo_${ts}_${
|
|
3126
|
+
id: `todo_${ts}_${randomUUID3().slice(0, 6)}`,
|
|
468
3127
|
content: match.description.slice(0, 200),
|
|
469
3128
|
status: "pending",
|
|
470
3129
|
promotedFromTask: match.id
|
|
@@ -473,7 +3132,7 @@ var taskTool = {
|
|
|
473
3132
|
if (input.subtasks && input.subtasks.length > 0) {
|
|
474
3133
|
for (const st of input.subtasks) {
|
|
475
3134
|
todos.push({
|
|
476
|
-
id: `todo_${ts}_${
|
|
3135
|
+
id: `todo_${ts}_${randomUUID3().slice(0, 6)}`,
|
|
477
3136
|
content: st,
|
|
478
3137
|
status: "pending",
|
|
479
3138
|
promotedFromTask: match.id
|
|
@@ -488,17 +3147,35 @@ var taskTool = {
|
|
|
488
3147
|
case "planify": {
|
|
489
3148
|
const target = input.target?.trim();
|
|
490
3149
|
if (!target) {
|
|
491
|
-
early = {
|
|
3150
|
+
early = {
|
|
3151
|
+
ok: false,
|
|
3152
|
+
message: "action=planify requires `target` (task id, index, or title substring).",
|
|
3153
|
+
count: 0,
|
|
3154
|
+
completed: 0,
|
|
3155
|
+
inProgress: 0
|
|
3156
|
+
};
|
|
492
3157
|
return f;
|
|
493
3158
|
}
|
|
494
3159
|
const idx = findTaskIndex(f.tasks, target);
|
|
495
3160
|
if (idx === -1) {
|
|
496
|
-
early = {
|
|
3161
|
+
early = {
|
|
3162
|
+
ok: false,
|
|
3163
|
+
message: `No task matched "${target}".`,
|
|
3164
|
+
count: 0,
|
|
3165
|
+
completed: 0,
|
|
3166
|
+
inProgress: 0
|
|
3167
|
+
};
|
|
497
3168
|
return f;
|
|
498
3169
|
}
|
|
499
3170
|
const match = f.tasks[idx];
|
|
500
3171
|
if (!match) {
|
|
501
|
-
early = {
|
|
3172
|
+
early = {
|
|
3173
|
+
ok: false,
|
|
3174
|
+
message: `No task matched "${target}".`,
|
|
3175
|
+
count: 0,
|
|
3176
|
+
completed: 0,
|
|
3177
|
+
inProgress: 0
|
|
3178
|
+
};
|
|
502
3179
|
return f;
|
|
503
3180
|
}
|
|
504
3181
|
planifyMeta.title = match.title;
|
|
@@ -507,7 +3184,13 @@ var taskTool = {
|
|
|
507
3184
|
break;
|
|
508
3185
|
}
|
|
509
3186
|
default:
|
|
510
|
-
early = {
|
|
3187
|
+
early = {
|
|
3188
|
+
ok: false,
|
|
3189
|
+
message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`,
|
|
3190
|
+
count: 0,
|
|
3191
|
+
completed: 0,
|
|
3192
|
+
inProgress: 0
|
|
3193
|
+
};
|
|
511
3194
|
return f;
|
|
512
3195
|
}
|
|
513
3196
|
return f;
|
|
@@ -521,7 +3204,11 @@ var taskTool = {
|
|
|
521
3204
|
inProgress: 0
|
|
522
3205
|
};
|
|
523
3206
|
}
|
|
524
|
-
if (todosToReplace)
|
|
3207
|
+
if (todosToReplace) {
|
|
3208
|
+
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
3209
|
+
signal: AbortSignal.timeout(3e4)
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
525
3212
|
mirrorSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
|
|
526
3213
|
if (early) return early;
|
|
527
3214
|
if (didPlanify) {
|