@wrongstack/tools 0.302.0 → 0.303.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.js +2799 -609
- 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-outgoing-calls-tool.d.ts +1 -0
- package/dist/codebase-index/content-hash.d.ts +66 -0
- package/dist/codebase-index/index.js +1569 -148
- 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 +1455 -99
- package/dist/codebase-index/schema.d.ts +7 -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 +1424 -68
- 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 +1 -0
- package/dist/index.js +2785 -634
- package/dist/kanban-task-inputs.d.ts +1 -0
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +18 -2
- package/dist/kanban.js +392 -126
- package/dist/pack.js +2799 -609
- package/dist/plan.d.ts +4 -1
- package/dist/plan.js +2380 -9
- package/dist/read.js +1531 -98
- package/dist/session-kanban.d.ts +8 -0
- package/dist/session-kanban.js +111 -17
- package/dist/task.d.ts +5 -4
- package/dist/task.js +2418 -43
- package/dist/todo.d.ts +10 -1
- package/dist/todo.js +2152 -20
- package/dist/tool-tier.js +2799 -609
- 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";
|
|
@@ -43,6 +35,7 @@ var boardQueue = /* @__PURE__ */ new Map();
|
|
|
43
35
|
var boardEnsures = /* @__PURE__ */ new Map();
|
|
44
36
|
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
45
37
|
var activeMirrors = /* @__PURE__ */ new Set();
|
|
38
|
+
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
46
39
|
function boardKey(projectRoot, sessionId) {
|
|
47
40
|
return `${projectRoot}\0${sessionId}`;
|
|
48
41
|
}
|
|
@@ -152,7 +145,17 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
152
145
|
pending.graph,
|
|
153
146
|
pending.sourceSystem
|
|
154
147
|
);
|
|
155
|
-
} catch {
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
level: "warn",
|
|
152
|
+
event: "session-kanban.mirror-failed",
|
|
153
|
+
sessionId: pending.sessionId,
|
|
154
|
+
sourceSystem: pending.sourceSystem,
|
|
155
|
+
message: error instanceof Error ? error.message : String(error),
|
|
156
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
157
|
+
})
|
|
158
|
+
);
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
} finally {
|
|
@@ -170,7 +173,33 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
170
173
|
}
|
|
171
174
|
})();
|
|
172
175
|
}
|
|
176
|
+
function todoListToSerializedGraph(todos, sessionId) {
|
|
177
|
+
const graphId = `todo:${sessionId}`;
|
|
178
|
+
const nodes = todos.map((todo, index) => ({
|
|
179
|
+
id: todo.id,
|
|
180
|
+
title: todo.content,
|
|
181
|
+
description: todo.activeForm ?? "",
|
|
182
|
+
type: "chore",
|
|
183
|
+
priority: "medium",
|
|
184
|
+
status: todo.status,
|
|
185
|
+
specRequirementId: `${graphId}:${todo.id}`,
|
|
186
|
+
createdAt: index,
|
|
187
|
+
updatedAt: index
|
|
188
|
+
}));
|
|
189
|
+
return {
|
|
190
|
+
id: graphId,
|
|
191
|
+
specId: graphId,
|
|
192
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
193
|
+
title: "Session todos",
|
|
194
|
+
nodes,
|
|
195
|
+
edges: [],
|
|
196
|
+
rootNodes: nodes.map((node) => node.id),
|
|
197
|
+
createdAt: 0,
|
|
198
|
+
updatedAt: 0
|
|
199
|
+
};
|
|
200
|
+
}
|
|
173
201
|
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
202
|
+
const graphId = `session:${sessionId}`;
|
|
174
203
|
const ids = new Set(tasks.map((task) => task.id));
|
|
175
204
|
const nodes = tasks.map((task, index) => ({
|
|
176
205
|
id: task.id,
|
|
@@ -179,6 +208,7 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
179
208
|
type: task.type,
|
|
180
209
|
priority: task.priority,
|
|
181
210
|
status: task.status,
|
|
211
|
+
specRequirementId: `${graphId}:${task.id}`,
|
|
182
212
|
...task.assignee ? { assignee: task.assignee } : {},
|
|
183
213
|
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
184
214
|
createdAt: index,
|
|
@@ -196,8 +226,9 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
196
226
|
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
197
227
|
return {
|
|
198
228
|
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
199
|
-
id:
|
|
200
|
-
specId:
|
|
229
|
+
id: graphId,
|
|
230
|
+
specId: graphId,
|
|
231
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
201
232
|
title: "Session tasks",
|
|
202
233
|
nodes,
|
|
203
234
|
edges,
|
|
@@ -206,6 +237,56 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
206
237
|
updatedAt: 0
|
|
207
238
|
};
|
|
208
239
|
}
|
|
240
|
+
function broadcastTodoUpdate(context, todos) {
|
|
241
|
+
const sessionId = context.session?.id ?? "";
|
|
242
|
+
if (!context.agentId || !sessionId) return;
|
|
243
|
+
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
244
|
+
for (const todo of todos) {
|
|
245
|
+
if (todo.status === "completed") statusCounts.completed++;
|
|
246
|
+
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
247
|
+
else statusCounts.pending++;
|
|
248
|
+
}
|
|
249
|
+
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
250
|
+
const mailbox = getSharedProjectMailbox(projectDir);
|
|
251
|
+
void mailbox.send({
|
|
252
|
+
from: context.agentId,
|
|
253
|
+
to: "*",
|
|
254
|
+
type: "status",
|
|
255
|
+
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
kind: "kanban.todos.updated",
|
|
258
|
+
sessionId,
|
|
259
|
+
revision: context.state.revision,
|
|
260
|
+
todoCount: todos.length,
|
|
261
|
+
statusCounts
|
|
262
|
+
}),
|
|
263
|
+
priority: "normal",
|
|
264
|
+
senderSessionId: sessionId
|
|
265
|
+
}).catch(() => {
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function notifyTodoUpdate(context, todos) {
|
|
269
|
+
const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
|
|
270
|
+
const text = `[KANBAN TODO UPDATE]
|
|
271
|
+
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
272
|
+
${summary}
|
|
273
|
+
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
274
|
+
const state = context.state;
|
|
275
|
+
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
276
|
+
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
277
|
+
}
|
|
278
|
+
if (typeof state.appendMessage === "function") {
|
|
279
|
+
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
283
|
+
queueLatestMirror(
|
|
284
|
+
projectRoot,
|
|
285
|
+
sessionId,
|
|
286
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
287
|
+
"session-todo"
|
|
288
|
+
);
|
|
289
|
+
}
|
|
209
290
|
function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
210
291
|
queueLatestMirror(
|
|
211
292
|
projectRoot,
|
|
@@ -214,6 +295,2161 @@ function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
|
214
295
|
"session-task"
|
|
215
296
|
);
|
|
216
297
|
}
|
|
298
|
+
function sourceStatus(task) {
|
|
299
|
+
if (task.status === "completed") return "completed";
|
|
300
|
+
if (task.status === "in_progress") return "in_progress";
|
|
301
|
+
if (task.status === "review") return "review";
|
|
302
|
+
if (task.status === "blocked") return "blocked";
|
|
303
|
+
if (task.status === "failed") return "failed";
|
|
304
|
+
return "pending";
|
|
305
|
+
}
|
|
306
|
+
function todoStatus(task) {
|
|
307
|
+
const status = sourceStatus(task);
|
|
308
|
+
if (status === "completed") return "completed";
|
|
309
|
+
if (status === "in_progress" || status === "review") return "in_progress";
|
|
310
|
+
return "pending";
|
|
311
|
+
}
|
|
312
|
+
function sessionTodoFromTask(task, boardId) {
|
|
313
|
+
return {
|
|
314
|
+
id: task.origin?.taskId ?? task.id,
|
|
315
|
+
content: task.title,
|
|
316
|
+
status: todoStatus(task),
|
|
317
|
+
kanbanBoardId: boardId,
|
|
318
|
+
kanbanTaskId: task.id,
|
|
319
|
+
...task.description ? { activeForm: task.description } : {}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function managedTodoFromTask(task, boardId) {
|
|
323
|
+
return {
|
|
324
|
+
...sessionTodoFromTask(task, boardId),
|
|
325
|
+
status: task.status === "completed" ? "completed" : task.status === "in_progress" ? "in_progress" : "pending"
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function sameTodos(left, right) {
|
|
329
|
+
return left.length === right.length && left.every((todo, index) => {
|
|
330
|
+
const candidate = right[index];
|
|
331
|
+
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;
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function applyManagedKanbanBoardToTodos(context, board) {
|
|
335
|
+
const metaKanban = context.meta["kanban"];
|
|
336
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
337
|
+
const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
|
|
338
|
+
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
339
|
+
return [...context.todos];
|
|
340
|
+
}
|
|
341
|
+
const projectedTodos = board.tasks.filter(
|
|
342
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
343
|
+
).sort(
|
|
344
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.order - right.order
|
|
345
|
+
).map((task) => managedTodoFromTask(task, board.id));
|
|
346
|
+
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
347
|
+
suppressedTodoMirrors.add(context);
|
|
348
|
+
try {
|
|
349
|
+
context.state.replaceTodos(projectedTodos);
|
|
350
|
+
} finally {
|
|
351
|
+
suppressedTodoMirrors.delete(context);
|
|
352
|
+
}
|
|
353
|
+
notifyTodoUpdate(context, context.todos);
|
|
354
|
+
broadcastTodoUpdate(context, context.todos);
|
|
355
|
+
return [...context.todos];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/todo.ts
|
|
359
|
+
import {
|
|
360
|
+
loadPlan as loadPlan2,
|
|
361
|
+
loadTasks as loadTasks3,
|
|
362
|
+
savePlan,
|
|
363
|
+
saveTasks,
|
|
364
|
+
setPlanItemStatus
|
|
365
|
+
} from "@wrongstack/core/storage";
|
|
366
|
+
import { getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
367
|
+
|
|
368
|
+
// src/kanban.ts
|
|
369
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
370
|
+
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
371
|
+
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
372
|
+
import {
|
|
373
|
+
addColumn,
|
|
374
|
+
addTask,
|
|
375
|
+
adoptManagedLifecycle,
|
|
376
|
+
assignTask,
|
|
377
|
+
claimReadyTask,
|
|
378
|
+
copyTaskToBoard,
|
|
379
|
+
createBoard as createBoard2,
|
|
380
|
+
createBoardFromTaskGraph,
|
|
381
|
+
createBoardFromText,
|
|
382
|
+
duplicateBoard,
|
|
383
|
+
evaluateContractGraphReadiness,
|
|
384
|
+
exportBoardAsMarkdown,
|
|
385
|
+
exportBoardToTaskGraph,
|
|
386
|
+
finalizeTaskCompletion,
|
|
387
|
+
getBoard as getBoard3,
|
|
388
|
+
getKanbanOrchestrationSnapshot,
|
|
389
|
+
getKanbanQueueHealth,
|
|
390
|
+
getTask,
|
|
391
|
+
getTaskChain,
|
|
392
|
+
heartbeatTaskAssignment,
|
|
393
|
+
listBoards as listBoards2,
|
|
394
|
+
listKanbanEvents,
|
|
395
|
+
listReadyTasks,
|
|
396
|
+
mergeTasks,
|
|
397
|
+
moveTask,
|
|
398
|
+
parseLinesIntoTasks,
|
|
399
|
+
recoverStaleTaskAssignments,
|
|
400
|
+
releaseTaskClaim,
|
|
401
|
+
removeBoard as removeBoard2,
|
|
402
|
+
removeColumn,
|
|
403
|
+
removeTask,
|
|
404
|
+
repairManagedTaskProjection,
|
|
405
|
+
searchKanban,
|
|
406
|
+
setTaskChain,
|
|
407
|
+
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
408
|
+
transferTaskToBoard,
|
|
409
|
+
transitionTask,
|
|
410
|
+
updateBoard as updateBoard2,
|
|
411
|
+
updateColumn,
|
|
412
|
+
updateTask as updateTask2,
|
|
413
|
+
updateTaskAssignment,
|
|
414
|
+
verifyTaskCompletion as verifyTaskCompletion2
|
|
415
|
+
} from "@wrongstack/kanban";
|
|
416
|
+
|
|
417
|
+
// src/kanban-board-inputs.ts
|
|
418
|
+
function agentSettableGate(enforcement) {
|
|
419
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
420
|
+
return { completionGate: { enforcement } };
|
|
421
|
+
}
|
|
422
|
+
function boardCreateInput(input, title) {
|
|
423
|
+
return {
|
|
424
|
+
title,
|
|
425
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
426
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
427
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
428
|
+
...input.atomicityMode !== void 0 ? {
|
|
429
|
+
atomicity: {
|
|
430
|
+
mode: input.atomicityMode,
|
|
431
|
+
decomposition: input.atomicityDecomposition ?? "propose"
|
|
432
|
+
}
|
|
433
|
+
} : {},
|
|
434
|
+
...agentSettableGate(input.gateEnforcement)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function boardUpdatePatch(input) {
|
|
438
|
+
return {
|
|
439
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
440
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
441
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
442
|
+
...input.atomicityMode !== void 0 ? {
|
|
443
|
+
atomicity: {
|
|
444
|
+
mode: input.atomicityMode,
|
|
445
|
+
decomposition: input.atomicityDecomposition ?? "propose"
|
|
446
|
+
}
|
|
447
|
+
} : {},
|
|
448
|
+
...agentSettableGate(input.gateEnforcement)
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function duplicateBoardOptions(input) {
|
|
452
|
+
return {
|
|
453
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
454
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
455
|
+
...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
|
|
456
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
457
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/kanban-decomposition-actions.ts
|
|
462
|
+
import {
|
|
463
|
+
assessTaskAtomicity,
|
|
464
|
+
proposeTaskDecomposition,
|
|
465
|
+
updateTask,
|
|
466
|
+
verifyTaskCompletion
|
|
467
|
+
} from "@wrongstack/kanban";
|
|
468
|
+
|
|
469
|
+
// src/kanban-evidence-bridge.ts
|
|
470
|
+
import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
|
|
471
|
+
function kanbanEvidenceKey(boardId, taskId) {
|
|
472
|
+
return `kanban:${boardId}:${taskId}`;
|
|
473
|
+
}
|
|
474
|
+
function kanbanEvidencePointer(boardId, taskId) {
|
|
475
|
+
return `kanban://${boardId}/${taskId}#verificationReport`;
|
|
476
|
+
}
|
|
477
|
+
function recordKanbanVerificationEvidence(ctx, report) {
|
|
478
|
+
try {
|
|
479
|
+
const passed = report.checks.filter((check) => check.status === "passed").length;
|
|
480
|
+
const completedAt = Date.parse(report.completedAt);
|
|
481
|
+
recordCompletedWorkEvidence(ctx, {
|
|
482
|
+
key: kanbanEvidenceKey(report.boardId, report.taskId),
|
|
483
|
+
source: "verification",
|
|
484
|
+
summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
|
|
485
|
+
...Number.isFinite(completedAt) ? { completedAt } : {},
|
|
486
|
+
evidence: kanbanEvidencePointer(report.boardId, report.taskId)
|
|
487
|
+
});
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/kanban-tool-results.ts
|
|
493
|
+
function atomicityNudge(task) {
|
|
494
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
495
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
496
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
497
|
+
}
|
|
498
|
+
function readEnvGateEnforcement() {
|
|
499
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
500
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
501
|
+
}
|
|
502
|
+
function fail(message) {
|
|
503
|
+
return { ok: false, message };
|
|
504
|
+
}
|
|
505
|
+
function okBoard(board, message = "Board loaded.") {
|
|
506
|
+
return { ok: true, message, board };
|
|
507
|
+
}
|
|
508
|
+
function okTask(board, task, message) {
|
|
509
|
+
return { ok: true, message, board, task };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/kanban-decomposition-actions.ts
|
|
513
|
+
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
514
|
+
switch (input.action) {
|
|
515
|
+
case "assess_atomicity": {
|
|
516
|
+
if (!input.boardId || !input.taskId) {
|
|
517
|
+
return fail("assess_atomicity requires boardId and taskId.");
|
|
518
|
+
}
|
|
519
|
+
const result = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
|
|
520
|
+
assessedBy: "agent",
|
|
521
|
+
...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
|
|
522
|
+
});
|
|
523
|
+
if (!result) return fail("Task not found.");
|
|
524
|
+
const failing = result.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
|
|
525
|
+
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." : "";
|
|
526
|
+
return okTask(
|
|
527
|
+
result.board,
|
|
528
|
+
result.task,
|
|
529
|
+
`Atomicity verdict: ${result.assessment.verdict} (score ${result.assessment.score}).${guidance}`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
case "propose_decomposition": {
|
|
533
|
+
if (!input.boardId || !input.taskId || !input.subtasks?.length) {
|
|
534
|
+
return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
|
|
535
|
+
}
|
|
536
|
+
if (input.subtasks.length < 2) {
|
|
537
|
+
return fail("propose_decomposition requires at least two subtasks.");
|
|
538
|
+
}
|
|
539
|
+
const invalid = input.subtasks.find(
|
|
540
|
+
(subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
|
|
541
|
+
);
|
|
542
|
+
if (invalid) return fail("Every proposed subtask needs a non-blank title.");
|
|
543
|
+
const result = await proposeTaskDecomposition(
|
|
544
|
+
projectRoot,
|
|
545
|
+
input.boardId,
|
|
546
|
+
input.taskId,
|
|
547
|
+
{
|
|
548
|
+
subtasks: input.subtasks,
|
|
549
|
+
...input.note !== void 0 ? { rationale: input.note } : {},
|
|
550
|
+
...ctx.agentId !== void 0 ? { proposedBy: ctx.agentId } : {}
|
|
551
|
+
},
|
|
552
|
+
ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
|
|
553
|
+
);
|
|
554
|
+
if (!result) return fail("Task not found.");
|
|
555
|
+
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.';
|
|
556
|
+
return okTask(result.board, result.task, message);
|
|
557
|
+
}
|
|
558
|
+
case "verify_completion": {
|
|
559
|
+
if (!input.boardId || !input.taskId) {
|
|
560
|
+
return fail("verify_completion requires boardId and taskId.");
|
|
561
|
+
}
|
|
562
|
+
const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
|
|
563
|
+
const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
|
|
564
|
+
verificationReport: verResult.report,
|
|
565
|
+
successCriteria: verResult.task.successCriteria
|
|
566
|
+
});
|
|
567
|
+
if (!persistedBoard) {
|
|
568
|
+
return {
|
|
569
|
+
ok: false,
|
|
570
|
+
verdict: verResult.report.verdict,
|
|
571
|
+
message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
|
|
572
|
+
board: verResult.board
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
576
|
+
const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
|
|
577
|
+
const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
|
|
578
|
+
return {
|
|
579
|
+
ok: deterministicVerdicts.includes(
|
|
580
|
+
verResult.report.verdict
|
|
581
|
+
),
|
|
582
|
+
verdict: verResult.report.verdict,
|
|
583
|
+
message: verResult.report.markdownSummary,
|
|
584
|
+
board: persistedBoard,
|
|
585
|
+
task: freshTask ?? verResult.task
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
default:
|
|
589
|
+
return void 0;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/kanban-detail-actions.ts
|
|
594
|
+
import {
|
|
595
|
+
addCheckToTask,
|
|
596
|
+
addContractEdge,
|
|
597
|
+
addDependency,
|
|
598
|
+
addGoalMetricToTask,
|
|
599
|
+
addLinkToTask,
|
|
600
|
+
addNoteToTask,
|
|
601
|
+
configureContractGraph,
|
|
602
|
+
evaluateTaskContractGraph,
|
|
603
|
+
getContractGraph,
|
|
604
|
+
getKanbanWorkbench,
|
|
605
|
+
removeContractEdge,
|
|
606
|
+
removeContractNode,
|
|
607
|
+
updateCheckOnTask,
|
|
608
|
+
updateGoalMetricOnTask,
|
|
609
|
+
upsertContractNode
|
|
610
|
+
} from "@wrongstack/kanban";
|
|
611
|
+
|
|
612
|
+
// src/kanban-split-task-handler.ts
|
|
613
|
+
import { getBoard as getBoard2, splitTask } from "@wrongstack/kanban";
|
|
614
|
+
async function handleSplitTask(projectRoot, input, extraSplitOptions) {
|
|
615
|
+
const boardId = input.boardId;
|
|
616
|
+
const taskId = input.taskId;
|
|
617
|
+
const childTitles = input.childTitles;
|
|
618
|
+
if (!boardId || !taskId || !childTitles?.length) {
|
|
619
|
+
return fail("split requires boardId, taskId, and at least one childTitles.");
|
|
620
|
+
}
|
|
621
|
+
const {
|
|
622
|
+
targetColumnId,
|
|
623
|
+
inheritAssignment,
|
|
624
|
+
inheritLabels,
|
|
625
|
+
inheritSuccessCriteria,
|
|
626
|
+
inheritGoalMetrics,
|
|
627
|
+
inheritDependencies,
|
|
628
|
+
chainChildren,
|
|
629
|
+
rewireDependents
|
|
630
|
+
} = input;
|
|
631
|
+
const result = await splitTask(projectRoot, boardId, taskId, {
|
|
632
|
+
titles: childTitles,
|
|
633
|
+
...extraSplitOptions,
|
|
634
|
+
...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
|
|
635
|
+
...inheritAssignment !== void 0 ? { inheritAssignment } : {},
|
|
636
|
+
...inheritLabels !== void 0 ? { inheritLabels } : {},
|
|
637
|
+
...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
|
|
638
|
+
...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
|
|
639
|
+
...inheritDependencies !== void 0 ? { inheritDependencies } : {},
|
|
640
|
+
...chainChildren !== void 0 ? { chainChildren } : {},
|
|
641
|
+
...rewireDependents !== void 0 ? { rewireDependents } : {}
|
|
642
|
+
});
|
|
643
|
+
if (!result) return fail("Task not found.");
|
|
644
|
+
const freshParent = result.board.tasks?.find((t) => t.id === taskId);
|
|
645
|
+
if (!freshParent) {
|
|
646
|
+
return fail(
|
|
647
|
+
`Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
ok: true,
|
|
652
|
+
message: `${result.children.length} child task(s) created.`,
|
|
653
|
+
board: result.board,
|
|
654
|
+
task: freshParent,
|
|
655
|
+
children: result.children
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async function requireBoard(projectRoot, boardId) {
|
|
659
|
+
return boardId ? getBoard2(projectRoot, boardId) : null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/kanban-detail-actions.ts
|
|
663
|
+
async function handleKanbanDetailAction(projectRoot, input) {
|
|
664
|
+
switch (input.action) {
|
|
665
|
+
case "workbench": {
|
|
666
|
+
const workbench = await getKanbanWorkbench(projectRoot, {
|
|
667
|
+
...input.limit !== void 0 ? { limitPerLane: input.limit, alertLimit: input.limit } : {}
|
|
668
|
+
});
|
|
669
|
+
return {
|
|
670
|
+
ok: true,
|
|
671
|
+
message: `${workbench.totals.now} now, ${workbench.totals.next} next, ${workbench.totals.blocked} blocked, ${workbench.totals.review} review; ${workbench.alertTotal} alert(s).`,
|
|
672
|
+
workbench
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
case "get_contract_graph": {
|
|
676
|
+
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
677
|
+
const result = await getContractGraph(projectRoot, input.boardId);
|
|
678
|
+
return result ? {
|
|
679
|
+
ok: true,
|
|
680
|
+
message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
|
|
681
|
+
board: result.board,
|
|
682
|
+
...result.graph ? { contractGraph: result.graph } : {}
|
|
683
|
+
} : fail("Board not found.");
|
|
684
|
+
}
|
|
685
|
+
case "configure_contract_graph": {
|
|
686
|
+
if (!input.boardId || !input.contractGraphEnforcement) {
|
|
687
|
+
return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
|
|
688
|
+
}
|
|
689
|
+
const current = await getContractGraph(projectRoot, input.boardId);
|
|
690
|
+
if (!current) return fail("Board not found.");
|
|
691
|
+
if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
|
|
692
|
+
return fail(
|
|
693
|
+
"Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
|
|
697
|
+
return fail("An autonomous agent may not loosen a strict contract graph.");
|
|
698
|
+
}
|
|
699
|
+
const board = await configureContractGraph(
|
|
700
|
+
projectRoot,
|
|
701
|
+
input.boardId,
|
|
702
|
+
input.contractGraphEnforcement
|
|
703
|
+
);
|
|
704
|
+
return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
|
|
705
|
+
}
|
|
706
|
+
case "upsert_contract_node": {
|
|
707
|
+
if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
|
|
708
|
+
return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
|
|
709
|
+
}
|
|
710
|
+
if (input.contractNodeState === "waived") {
|
|
711
|
+
return fail(
|
|
712
|
+
"The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
if (input.contractNodeId) {
|
|
716
|
+
const current = await getContractGraph(projectRoot, input.boardId);
|
|
717
|
+
const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
|
|
718
|
+
if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
|
|
719
|
+
return fail(
|
|
720
|
+
"The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
725
|
+
...input.contractNodeId ? { id: input.contractNodeId } : {},
|
|
726
|
+
taskId: input.taskId,
|
|
727
|
+
kind: input.contractNodeKind,
|
|
728
|
+
title: input.title,
|
|
729
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
730
|
+
...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
|
|
731
|
+
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
732
|
+
...input.checkId !== void 0 ? { checkId: input.checkId } : {},
|
|
733
|
+
...input.metricId !== void 0 ? { metricId: input.metricId } : {},
|
|
734
|
+
...input.baseline !== void 0 ? { baseline: input.baseline } : {},
|
|
735
|
+
...input.threshold !== void 0 ? { threshold: input.threshold } : {},
|
|
736
|
+
...input.author !== void 0 ? { createdBy: input.author } : {}
|
|
737
|
+
});
|
|
738
|
+
return result ? {
|
|
739
|
+
...okBoard(result.board, "Contract node saved."),
|
|
740
|
+
contractGraph: result.board.contractGraph
|
|
741
|
+
} : fail("Task not found.");
|
|
742
|
+
}
|
|
743
|
+
case "link_contract_nodes": {
|
|
744
|
+
if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
|
|
745
|
+
return fail(
|
|
746
|
+
"link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
750
|
+
from: input.fromNodeId,
|
|
751
|
+
to: input.toNodeId,
|
|
752
|
+
type: input.contractEdgeType,
|
|
753
|
+
...input.contractEdgeId ? { id: input.contractEdgeId } : {},
|
|
754
|
+
...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
|
|
755
|
+
...input.contractRationale ? { rationale: input.contractRationale } : {},
|
|
756
|
+
...input.author ? { createdBy: input.author } : {}
|
|
757
|
+
});
|
|
758
|
+
return result ? {
|
|
759
|
+
...okBoard(result.board, "Contract edge added."),
|
|
760
|
+
contractGraph: result.board.contractGraph
|
|
761
|
+
} : fail("Board not found.");
|
|
762
|
+
}
|
|
763
|
+
case "remove_contract_node": {
|
|
764
|
+
if (!input.boardId || !input.contractNodeId) {
|
|
765
|
+
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
766
|
+
}
|
|
767
|
+
const current = await getContractGraph(projectRoot, input.boardId);
|
|
768
|
+
const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
|
|
769
|
+
if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
|
|
770
|
+
return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
|
|
771
|
+
}
|
|
772
|
+
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
773
|
+
return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
|
|
774
|
+
}
|
|
775
|
+
case "remove_contract_edge": {
|
|
776
|
+
if (!input.boardId || !input.contractEdgeId) {
|
|
777
|
+
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
778
|
+
}
|
|
779
|
+
const current = await getContractGraph(projectRoot, input.boardId);
|
|
780
|
+
const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
|
|
781
|
+
if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
|
|
782
|
+
return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
|
|
783
|
+
}
|
|
784
|
+
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
785
|
+
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
786
|
+
}
|
|
787
|
+
case "evaluate_contract_graph": {
|
|
788
|
+
if (!input.boardId || !input.taskId) {
|
|
789
|
+
return fail("evaluate_contract_graph requires boardId and taskId.");
|
|
790
|
+
}
|
|
791
|
+
const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
|
|
792
|
+
return result ? {
|
|
793
|
+
ok: result.evaluation.allowed,
|
|
794
|
+
message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
|
|
795
|
+
board: result.board,
|
|
796
|
+
contractGraph: result.board.contractGraph,
|
|
797
|
+
contractEvaluation: result.evaluation
|
|
798
|
+
} : fail("Task not found.");
|
|
799
|
+
}
|
|
800
|
+
case "add_dependency": {
|
|
801
|
+
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
802
|
+
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
803
|
+
}
|
|
804
|
+
const board = await addDependency(
|
|
805
|
+
projectRoot,
|
|
806
|
+
input.boardId,
|
|
807
|
+
input.taskId,
|
|
808
|
+
input.dependencyTaskId
|
|
809
|
+
);
|
|
810
|
+
return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
|
|
811
|
+
}
|
|
812
|
+
case "add_goal_metric": {
|
|
813
|
+
if (!input.boardId || !input.taskId || !input.metricName) {
|
|
814
|
+
return fail("add_goal_metric requires boardId, taskId, and metricName.");
|
|
815
|
+
}
|
|
816
|
+
const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
|
|
817
|
+
name: input.metricName,
|
|
818
|
+
...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
|
|
819
|
+
...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
|
|
820
|
+
...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
|
|
821
|
+
...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
|
|
822
|
+
...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
|
|
823
|
+
});
|
|
824
|
+
return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
|
|
825
|
+
}
|
|
826
|
+
case "update_goal_metric": {
|
|
827
|
+
if (!input.boardId || !input.taskId || !input.metricId) {
|
|
828
|
+
return fail("update_goal_metric requires boardId, taskId, and metricId.");
|
|
829
|
+
}
|
|
830
|
+
const board = await updateGoalMetricOnTask(
|
|
831
|
+
projectRoot,
|
|
832
|
+
input.boardId,
|
|
833
|
+
input.taskId,
|
|
834
|
+
input.metricId,
|
|
835
|
+
{
|
|
836
|
+
...input.metricName !== void 0 ? { name: input.metricName } : {},
|
|
837
|
+
...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
|
|
838
|
+
...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
|
|
839
|
+
...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
|
|
840
|
+
...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
|
|
841
|
+
...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
|
|
845
|
+
}
|
|
846
|
+
case "add_check": {
|
|
847
|
+
if (!input.boardId || !input.taskId || !input.checkDescription) {
|
|
848
|
+
return fail("add_check requires boardId, taskId, and checkDescription.");
|
|
849
|
+
}
|
|
850
|
+
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
851
|
+
description: input.checkDescription,
|
|
852
|
+
type: "manual",
|
|
853
|
+
status: input.checkStatus
|
|
854
|
+
});
|
|
855
|
+
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
856
|
+
}
|
|
857
|
+
case "update_check": {
|
|
858
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
859
|
+
return fail("update_check requires boardId, taskId, and checkId.");
|
|
860
|
+
}
|
|
861
|
+
const board = await updateCheckOnTask(
|
|
862
|
+
projectRoot,
|
|
863
|
+
input.boardId,
|
|
864
|
+
input.taskId,
|
|
865
|
+
input.checkId,
|
|
866
|
+
{
|
|
867
|
+
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
868
|
+
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
872
|
+
}
|
|
873
|
+
case "add_note": {
|
|
874
|
+
if (!input.boardId || !input.taskId || !input.note)
|
|
875
|
+
return fail("add_note requires boardId, taskId, and note.");
|
|
876
|
+
const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
|
|
877
|
+
author: input.author ?? "agent",
|
|
878
|
+
content: input.note
|
|
879
|
+
});
|
|
880
|
+
return board ? okBoard(board, "Note added.") : fail("Task not found.");
|
|
881
|
+
}
|
|
882
|
+
case "add_link": {
|
|
883
|
+
if (!input.boardId || !input.taskId || !input.url)
|
|
884
|
+
return fail("add_link requires boardId, taskId, and url.");
|
|
885
|
+
const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
|
|
886
|
+
url: input.url,
|
|
887
|
+
type: input.linkType ?? "url",
|
|
888
|
+
...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
|
|
889
|
+
});
|
|
890
|
+
return board ? okBoard(board, "Link added.") : fail("Task not found.");
|
|
891
|
+
}
|
|
892
|
+
case "split_atomic": {
|
|
893
|
+
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
894
|
+
return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
|
|
895
|
+
}
|
|
896
|
+
return handleSplitTask(projectRoot, input, { atomic: true });
|
|
897
|
+
}
|
|
898
|
+
default:
|
|
899
|
+
return void 0;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/kanban-presence.ts
|
|
904
|
+
import { touchKanbanPresence as touchKanbanPresence2 } from "@wrongstack/kanban";
|
|
905
|
+
function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
906
|
+
return async (result) => {
|
|
907
|
+
const boardId = result.board?.id ?? input.boardId;
|
|
908
|
+
if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
|
|
909
|
+
try {
|
|
910
|
+
const board = await touchKanbanPresence2(projectRoot, boardId, {
|
|
911
|
+
sessionId: ctx.session.id,
|
|
912
|
+
agentId: ctx.agentId,
|
|
913
|
+
agentName: ctx.agentName,
|
|
914
|
+
taskId: input.taskId ?? result.task?.id,
|
|
915
|
+
runTaskId: input.runTaskId
|
|
916
|
+
});
|
|
917
|
+
return board ? { ...result, board } : result;
|
|
918
|
+
} catch {
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/kanban-task-inputs.ts
|
|
925
|
+
import { randomUUID } from "node:crypto";
|
|
926
|
+
import { clampSubagentCapabilities } from "@wrongstack/core/security";
|
|
927
|
+
function taskInput(input) {
|
|
928
|
+
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
929
|
+
return {
|
|
930
|
+
title: input.title ?? "",
|
|
931
|
+
columnId: input.columnId,
|
|
932
|
+
description: input.description,
|
|
933
|
+
dueDate: input.dueDate,
|
|
934
|
+
priority: input.priority,
|
|
935
|
+
...input.taskType !== void 0 ? { type: input.taskType } : {},
|
|
936
|
+
status: input.status,
|
|
937
|
+
labels: input.labels,
|
|
938
|
+
...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
|
|
939
|
+
...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
|
|
940
|
+
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
941
|
+
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
942
|
+
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
|
|
943
|
+
...assignment ? { assignment } : {},
|
|
944
|
+
...input.order !== void 0 ? { order: input.order } : {},
|
|
945
|
+
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
946
|
+
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
947
|
+
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
948
|
+
...input.checkDescription !== void 0 ? {
|
|
949
|
+
successCriteria: [
|
|
950
|
+
{
|
|
951
|
+
id: randomUUID(),
|
|
952
|
+
description: input.checkDescription,
|
|
953
|
+
type: "manual",
|
|
954
|
+
status: input.checkStatus ?? "pending"
|
|
955
|
+
}
|
|
956
|
+
]
|
|
957
|
+
} : {},
|
|
958
|
+
...input.metricName !== void 0 ? {
|
|
959
|
+
goalMetrics: [
|
|
960
|
+
{
|
|
961
|
+
id: randomUUID(),
|
|
962
|
+
name: input.metricName,
|
|
963
|
+
status: input.metricStatus ?? "pending",
|
|
964
|
+
...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
|
|
965
|
+
...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
|
|
966
|
+
...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
|
|
967
|
+
...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
|
|
968
|
+
}
|
|
969
|
+
]
|
|
970
|
+
} : {},
|
|
971
|
+
...input.url !== void 0 ? {
|
|
972
|
+
links: [
|
|
973
|
+
{
|
|
974
|
+
url: input.url,
|
|
975
|
+
type: input.linkType ?? "url",
|
|
976
|
+
...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
|
|
977
|
+
}
|
|
978
|
+
]
|
|
979
|
+
} : {},
|
|
980
|
+
...input.note !== void 0 ? {
|
|
981
|
+
notes: [
|
|
982
|
+
{
|
|
983
|
+
id: randomUUID(),
|
|
984
|
+
author: input.author ?? "agent",
|
|
985
|
+
content: input.note,
|
|
986
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
987
|
+
}
|
|
988
|
+
]
|
|
989
|
+
} : {},
|
|
990
|
+
...[input.graphId, input.specId, input.specRequirementId].some((value) => value !== void 0) ? {
|
|
991
|
+
origin: {
|
|
992
|
+
system: input.sourceSystem ?? "kanban-tool",
|
|
993
|
+
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
994
|
+
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
995
|
+
...input.specRequirementId !== void 0 ? { specRequirementId: input.specRequirementId } : {},
|
|
996
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
|
|
997
|
+
}
|
|
998
|
+
} : {}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function mergedDependsOn(input) {
|
|
1002
|
+
const ids = [
|
|
1003
|
+
...input.dependsOn ?? [],
|
|
1004
|
+
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
1005
|
+
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
1006
|
+
return ids.length > 0 ? ids : void 0;
|
|
1007
|
+
}
|
|
1008
|
+
function taskPatch(input) {
|
|
1009
|
+
return {
|
|
1010
|
+
title: input.title,
|
|
1011
|
+
description: input.description,
|
|
1012
|
+
dueDate: input.dueDate,
|
|
1013
|
+
columnId: input.columnId,
|
|
1014
|
+
order: input.order,
|
|
1015
|
+
priority: input.priority,
|
|
1016
|
+
...input.taskType !== void 0 ? { type: input.taskType } : {},
|
|
1017
|
+
status: input.status,
|
|
1018
|
+
labels: input.labels,
|
|
1019
|
+
assignedAgent: input.agentId,
|
|
1020
|
+
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
1021
|
+
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
1022
|
+
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function clampRequestedCapabilities(requested) {
|
|
1026
|
+
if (requested === void 0) return void 0;
|
|
1027
|
+
return clampSubagentCapabilities(requested).granted;
|
|
1028
|
+
}
|
|
1029
|
+
function assignmentInput(input) {
|
|
1030
|
+
return {
|
|
1031
|
+
agentId: input.agentId,
|
|
1032
|
+
name: input.name,
|
|
1033
|
+
role: input.role,
|
|
1034
|
+
provider: input.provider,
|
|
1035
|
+
model: input.model,
|
|
1036
|
+
fallbackProfile: input.fallbackProfile,
|
|
1037
|
+
fallbackModels: input.fallbackModels,
|
|
1038
|
+
tools: input.tools,
|
|
1039
|
+
allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
|
|
1040
|
+
assignee: input.assignee,
|
|
1041
|
+
leaseId: input.leaseId,
|
|
1042
|
+
claimedAt: input.claimedAt,
|
|
1043
|
+
heartbeatAt: input.heartbeatAt,
|
|
1044
|
+
leaseExpiresAt: input.leaseExpiresAt,
|
|
1045
|
+
attempt: input.attempt,
|
|
1046
|
+
maxAttempts: input.maxAttempts,
|
|
1047
|
+
costCeilingUsd: input.costCeilingUsd,
|
|
1048
|
+
retryPolicy: input.retryPolicy,
|
|
1049
|
+
lastFailureKind: input.lastFailureKind
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function hasAssignmentInput(input) {
|
|
1053
|
+
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;
|
|
1054
|
+
}
|
|
1055
|
+
function assignmentForTaskCreate(input) {
|
|
1056
|
+
return {
|
|
1057
|
+
status: input.assignmentStatus ?? "assigned",
|
|
1058
|
+
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
1059
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
1060
|
+
...input.role !== void 0 ? { role: input.role } : {},
|
|
1061
|
+
...input.provider !== void 0 ? { provider: input.provider } : {},
|
|
1062
|
+
...input.model !== void 0 ? { model: input.model } : {},
|
|
1063
|
+
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
1064
|
+
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
1065
|
+
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
1066
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
1067
|
+
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
1068
|
+
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
1069
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
1070
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
1071
|
+
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
1072
|
+
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {},
|
|
1073
|
+
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
1074
|
+
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
1075
|
+
...input.lastFailureKind !== void 0 ? { lastFailureKind: input.lastFailureKind } : {}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/kanban-tool-schema.ts
|
|
1080
|
+
var KANBAN_TOOL_DESCRIPTION = "Manage and audit project-scoped multi-kanban boards through the shared IPC Kanban server and its SQLite store. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Contract Map actions are optional advisory metadata unless an operator explicitly enabled strict enforcement. Use verify_completion to validate executable success criteria before Done.";
|
|
1081
|
+
var KANBAN_TOOL_USAGE_HINT = "Use this for durable project kanban state. Read workbench when orienting across boards; it returns bounded Now, Next, Blocked, Review lanes and operational alerts. Before coding mutation, create a fully detailed card on a managed board with executable acceptance criteria, then call start_task. The runtime blocks product mutations until start_task binds a ready Running card. Do not create, inspect, repair, or enable a Contract Map during ordinary work; every map mode stays off the execution and completion path. Worker completion enters Review; Done requires passed acceptance criteria and review evidence. Surface existing strict-map findings for operator review without stopping work to repair them.";
|
|
1082
|
+
var KANBAN_INPUT_SCHEMA = {
|
|
1083
|
+
type: "object",
|
|
1084
|
+
properties: {
|
|
1085
|
+
action: {
|
|
1086
|
+
type: "string",
|
|
1087
|
+
enum: [
|
|
1088
|
+
"list_boards",
|
|
1089
|
+
"get_board",
|
|
1090
|
+
"create_board",
|
|
1091
|
+
"duplicate_board",
|
|
1092
|
+
"update_board",
|
|
1093
|
+
"adopt_managed_lifecycle",
|
|
1094
|
+
"delete_board",
|
|
1095
|
+
"generate_board",
|
|
1096
|
+
"export_markdown",
|
|
1097
|
+
"export_task_graph",
|
|
1098
|
+
"sync_task_graph",
|
|
1099
|
+
"create_from_graph",
|
|
1100
|
+
"import_session_tasks",
|
|
1101
|
+
"search_tasks",
|
|
1102
|
+
"ready_tasks",
|
|
1103
|
+
"snapshot",
|
|
1104
|
+
"workbench",
|
|
1105
|
+
"add_column",
|
|
1106
|
+
"update_column",
|
|
1107
|
+
"delete_column",
|
|
1108
|
+
"add_task",
|
|
1109
|
+
"split_task",
|
|
1110
|
+
"merge_tasks",
|
|
1111
|
+
"copy_task",
|
|
1112
|
+
"transfer_task",
|
|
1113
|
+
"get_task",
|
|
1114
|
+
"start_task",
|
|
1115
|
+
"update_task",
|
|
1116
|
+
"transition_task",
|
|
1117
|
+
"repair_managed_projection",
|
|
1118
|
+
"move_task",
|
|
1119
|
+
"delete_task",
|
|
1120
|
+
"set_chain",
|
|
1121
|
+
"get_chain",
|
|
1122
|
+
"get_contract_graph",
|
|
1123
|
+
"configure_contract_graph",
|
|
1124
|
+
"upsert_contract_node",
|
|
1125
|
+
"link_contract_nodes",
|
|
1126
|
+
"remove_contract_node",
|
|
1127
|
+
"remove_contract_edge",
|
|
1128
|
+
"evaluate_contract_graph",
|
|
1129
|
+
"claim_task",
|
|
1130
|
+
"release_task",
|
|
1131
|
+
"assign_task",
|
|
1132
|
+
"mark_assignment",
|
|
1133
|
+
"heartbeat_assignment",
|
|
1134
|
+
"recover_stale",
|
|
1135
|
+
"events",
|
|
1136
|
+
"queue_health",
|
|
1137
|
+
"add_dependency",
|
|
1138
|
+
"add_goal_metric",
|
|
1139
|
+
"update_goal_metric",
|
|
1140
|
+
"add_check",
|
|
1141
|
+
"update_check",
|
|
1142
|
+
"add_note",
|
|
1143
|
+
"add_link",
|
|
1144
|
+
"verify_completion",
|
|
1145
|
+
"split_atomic",
|
|
1146
|
+
"assess_atomicity",
|
|
1147
|
+
"propose_decomposition"
|
|
1148
|
+
]
|
|
1149
|
+
},
|
|
1150
|
+
boardId: { type: "string" },
|
|
1151
|
+
taskId: { type: "string" },
|
|
1152
|
+
taskIds: { type: "array", items: { type: "string" } },
|
|
1153
|
+
chainId: { type: "string" },
|
|
1154
|
+
contractNodeId: { type: "string" },
|
|
1155
|
+
contractNodeKind: {
|
|
1156
|
+
type: "string",
|
|
1157
|
+
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
|
|
1158
|
+
},
|
|
1159
|
+
contractNodeState: {
|
|
1160
|
+
type: "string",
|
|
1161
|
+
enum: ["unknown", "active", "satisfied", "violated", "resolved"]
|
|
1162
|
+
},
|
|
1163
|
+
contractEnforcement: {
|
|
1164
|
+
type: "string",
|
|
1165
|
+
enum: ["blocking", "advisory", "informational"]
|
|
1166
|
+
},
|
|
1167
|
+
contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
|
|
1168
|
+
contractEdgeId: { type: "string" },
|
|
1169
|
+
contractEdgeType: {
|
|
1170
|
+
type: "string",
|
|
1171
|
+
enum: [
|
|
1172
|
+
"targets",
|
|
1173
|
+
"affects",
|
|
1174
|
+
"must_preserve",
|
|
1175
|
+
"exposes",
|
|
1176
|
+
"verified_by",
|
|
1177
|
+
"conflicts_with",
|
|
1178
|
+
"derived_from",
|
|
1179
|
+
"relates_to"
|
|
1180
|
+
]
|
|
1181
|
+
},
|
|
1182
|
+
fromNodeId: { type: "string" },
|
|
1183
|
+
toNodeId: { type: "string" },
|
|
1184
|
+
contractRationale: { type: "string" },
|
|
1185
|
+
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1186
|
+
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1187
|
+
columnId: { type: "string" },
|
|
1188
|
+
targetBoardId: { type: "string" },
|
|
1189
|
+
targetColumnId: { type: "string" },
|
|
1190
|
+
title: { type: "string" },
|
|
1191
|
+
description: { type: "string" },
|
|
1192
|
+
dueDate: { type: "string" },
|
|
1193
|
+
tags: { type: "array", items: { type: "string" } },
|
|
1194
|
+
labels: { type: "array", items: { type: "string" } },
|
|
1195
|
+
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
1196
|
+
taskType: {
|
|
1197
|
+
type: "string",
|
|
1198
|
+
enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
|
|
1199
|
+
},
|
|
1200
|
+
status: {
|
|
1201
|
+
type: "string",
|
|
1202
|
+
enum: [
|
|
1203
|
+
"pending",
|
|
1204
|
+
"ready",
|
|
1205
|
+
"in_progress",
|
|
1206
|
+
"blocked",
|
|
1207
|
+
"review",
|
|
1208
|
+
"completed",
|
|
1209
|
+
"failed",
|
|
1210
|
+
"archived"
|
|
1211
|
+
]
|
|
1212
|
+
},
|
|
1213
|
+
order: { type: "number" },
|
|
1214
|
+
query: { type: "string" },
|
|
1215
|
+
limit: { type: "number" },
|
|
1216
|
+
agentId: { type: "string" },
|
|
1217
|
+
name: { type: "string" },
|
|
1218
|
+
role: { type: "string" },
|
|
1219
|
+
provider: { type: "string" },
|
|
1220
|
+
model: { type: "string" },
|
|
1221
|
+
fallbackProfile: { type: "string" },
|
|
1222
|
+
fallbackModels: { type: "array", items: { type: "string" } },
|
|
1223
|
+
tools: { type: "array", items: { type: "string" } },
|
|
1224
|
+
allowedCapabilities: { type: "array", items: { type: "string" } },
|
|
1225
|
+
leaseId: { type: "string" },
|
|
1226
|
+
claimedAt: { type: "string" },
|
|
1227
|
+
heartbeatAt: { type: "string" },
|
|
1228
|
+
leaseExpiresAt: { type: "string" },
|
|
1229
|
+
attempt: { type: "number" },
|
|
1230
|
+
maxAttempts: { type: "number" },
|
|
1231
|
+
subagentId: { type: "string" },
|
|
1232
|
+
runTaskId: { type: "string" },
|
|
1233
|
+
lastResult: { type: "string" },
|
|
1234
|
+
error: { type: "string" },
|
|
1235
|
+
expectedLeaseId: { type: "string" },
|
|
1236
|
+
assignmentStatus: {
|
|
1237
|
+
type: "string",
|
|
1238
|
+
enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
|
|
1239
|
+
},
|
|
1240
|
+
lifecycleStage: {
|
|
1241
|
+
type: "string",
|
|
1242
|
+
enum: ["backlog", "todo", "running", "review", "done"]
|
|
1243
|
+
},
|
|
1244
|
+
transitionAction: { type: "string" },
|
|
1245
|
+
transitionComment: { type: "string" },
|
|
1246
|
+
attachmentUrl: { type: "string" },
|
|
1247
|
+
attachmentTitle: { type: "string" },
|
|
1248
|
+
attachmentType: {
|
|
1249
|
+
type: "string",
|
|
1250
|
+
enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
|
|
1251
|
+
},
|
|
1252
|
+
releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
|
|
1253
|
+
releaseReason: { type: "string" },
|
|
1254
|
+
clearAssignee: { type: "boolean" },
|
|
1255
|
+
recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
|
|
1256
|
+
recoveryNow: { type: "string" },
|
|
1257
|
+
recoveryPolicyFailOnCostCeiling: { type: "boolean" },
|
|
1258
|
+
recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
|
|
1259
|
+
recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
|
|
1260
|
+
recoveryPolicyRetryPolicyOverride: {
|
|
1261
|
+
type: "string",
|
|
1262
|
+
enum: ["off", "incremental", "exponential"]
|
|
1263
|
+
},
|
|
1264
|
+
assignee: { type: "string" },
|
|
1265
|
+
costCeilingUsd: { type: "number" },
|
|
1266
|
+
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
1267
|
+
lastFailureKind: { type: "string" },
|
|
1268
|
+
dependsOn: { type: "array", items: { type: "string" } },
|
|
1269
|
+
estimatedHours: { type: "number" },
|
|
1270
|
+
actualHours: { type: "number" },
|
|
1271
|
+
taskGraph: { type: "object" },
|
|
1272
|
+
graphId: { type: "string" },
|
|
1273
|
+
specId: { type: "string" },
|
|
1274
|
+
specRequirementId: { type: "string" },
|
|
1275
|
+
sourceSystem: { type: "string" },
|
|
1276
|
+
phaseId: { type: "string" },
|
|
1277
|
+
preserveOriginTaskIds: { type: "boolean" },
|
|
1278
|
+
includeArchived: { type: "boolean" },
|
|
1279
|
+
archiveMissingTasks: { type: "boolean" },
|
|
1280
|
+
preserveManualDependencies: { type: "boolean" },
|
|
1281
|
+
dependencyTaskId: { type: "string" },
|
|
1282
|
+
enforceDependencies: { type: "boolean" },
|
|
1283
|
+
childTitles: { type: "array", items: { type: "string" } },
|
|
1284
|
+
inheritAssignment: { type: "boolean" },
|
|
1285
|
+
inheritLabels: { type: "boolean" },
|
|
1286
|
+
inheritSuccessCriteria: { type: "boolean" },
|
|
1287
|
+
inheritGoalMetrics: { type: "boolean" },
|
|
1288
|
+
inheritDependencies: { type: "boolean" },
|
|
1289
|
+
chainChildren: { type: "boolean" },
|
|
1290
|
+
rewireDependents: { type: "boolean" },
|
|
1291
|
+
closeSourceTasks: { type: "boolean" },
|
|
1292
|
+
metricId: { type: "string" },
|
|
1293
|
+
metricName: { type: "string" },
|
|
1294
|
+
metricTarget: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1295
|
+
metricCurrent: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1296
|
+
metricUnit: { type: "string" },
|
|
1297
|
+
metricStatus: { type: "string", enum: ["pending", "met", "missed", "waived"] },
|
|
1298
|
+
metricNotes: { type: "string" },
|
|
1299
|
+
checkId: { type: "string" },
|
|
1300
|
+
checkDescription: { type: "string" },
|
|
1301
|
+
checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
|
|
1302
|
+
note: { type: "string" },
|
|
1303
|
+
author: { type: "string" },
|
|
1304
|
+
url: { type: "string" },
|
|
1305
|
+
linkTitle: { type: "string" },
|
|
1306
|
+
linkType: {
|
|
1307
|
+
type: "string",
|
|
1308
|
+
enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
|
|
1309
|
+
},
|
|
1310
|
+
context: { type: "string" },
|
|
1311
|
+
columns: { type: "array", items: { type: "string" } },
|
|
1312
|
+
generatedBy: { type: "string" },
|
|
1313
|
+
includeTasks: { type: "boolean" },
|
|
1314
|
+
includeCompletedTasks: { type: "boolean" },
|
|
1315
|
+
preserveAssignment: { type: "boolean" },
|
|
1316
|
+
preserveDependencies: { type: "boolean" },
|
|
1317
|
+
moveTasksToColumnId: { type: "string" },
|
|
1318
|
+
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
1319
|
+
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
1320
|
+
gateEnforcement: {
|
|
1321
|
+
type: "string",
|
|
1322
|
+
// WS-023: `'off'` is deliberately absent. The agent whose work this gate
|
|
1323
|
+
// checks must not be able to switch it off; it may only tighten. Turning
|
|
1324
|
+
// a gate off stays a human decision, made through board config.
|
|
1325
|
+
enum: ["strict", "soft"]
|
|
1326
|
+
},
|
|
1327
|
+
subtasks: {
|
|
1328
|
+
type: "array",
|
|
1329
|
+
minItems: 2,
|
|
1330
|
+
items: {
|
|
1331
|
+
type: "object",
|
|
1332
|
+
properties: {
|
|
1333
|
+
title: { type: "string" },
|
|
1334
|
+
description: { type: "string" },
|
|
1335
|
+
successCriteria: { type: "array", items: { type: "string" } },
|
|
1336
|
+
dependsOnIndex: { type: "array", items: { type: "number" } }
|
|
1337
|
+
},
|
|
1338
|
+
required: ["title"]
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
required: ["action"]
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
// src/kanban.ts
|
|
1346
|
+
var kanbanTool = {
|
|
1347
|
+
name: "kanban",
|
|
1348
|
+
category: "Project",
|
|
1349
|
+
description: KANBAN_TOOL_DESCRIPTION,
|
|
1350
|
+
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
1351
|
+
permission: "confirm",
|
|
1352
|
+
mutating: true,
|
|
1353
|
+
capabilities: ["fs.write"],
|
|
1354
|
+
icon: "task",
|
|
1355
|
+
timeoutMs: 3e4,
|
|
1356
|
+
inputSchema: KANBAN_INPUT_SCHEMA,
|
|
1357
|
+
async execute(input, ctx) {
|
|
1358
|
+
const projectRoot = ctx.projectRoot;
|
|
1359
|
+
if (!projectRoot) return fail("No project root is available.");
|
|
1360
|
+
const withPresence = createKanbanPresenceWrapper(projectRoot, input, ctx);
|
|
1361
|
+
try {
|
|
1362
|
+
const result = await (async () => {
|
|
1363
|
+
const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
|
|
1364
|
+
if (decompositionResult !== void 0) return decompositionResult;
|
|
1365
|
+
switch (input.action) {
|
|
1366
|
+
case "list_boards": {
|
|
1367
|
+
const boards = await listBoards2(projectRoot);
|
|
1368
|
+
return { ok: true, message: `${boards.length} board(s).`, boards };
|
|
1369
|
+
}
|
|
1370
|
+
case "get_board": {
|
|
1371
|
+
const board = await requireBoard(projectRoot, input.boardId);
|
|
1372
|
+
return board ? okBoard(board) : fail("Board not found.");
|
|
1373
|
+
}
|
|
1374
|
+
case "create_board": {
|
|
1375
|
+
if (!input.title) return fail("create_board requires title.");
|
|
1376
|
+
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
1377
|
+
return { ok: true, message: `Board created: ${board.title}`, board };
|
|
1378
|
+
}
|
|
1379
|
+
case "update_board": {
|
|
1380
|
+
if (!input.boardId) return fail("update_board requires boardId.");
|
|
1381
|
+
const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
|
|
1382
|
+
return board ? okBoard(board, "Board updated.") : fail("Board not found.");
|
|
1383
|
+
}
|
|
1384
|
+
case "adopt_managed_lifecycle": {
|
|
1385
|
+
if (!input.boardId || !input.author || !input.transitionComment) {
|
|
1386
|
+
return fail(
|
|
1387
|
+
"adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
if (input.columns?.length !== 5) {
|
|
1391
|
+
return fail(
|
|
1392
|
+
"adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
const [backlog, todo, running, review, done] = input.columns;
|
|
1396
|
+
if (!backlog || !todo || !running || !review || !done) {
|
|
1397
|
+
return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
|
|
1398
|
+
}
|
|
1399
|
+
const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
|
|
1400
|
+
columns: { backlog, todo, running, review, done },
|
|
1401
|
+
actor: input.author,
|
|
1402
|
+
comment: input.transitionComment
|
|
1403
|
+
});
|
|
1404
|
+
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
1405
|
+
}
|
|
1406
|
+
case "duplicate_board": {
|
|
1407
|
+
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
1408
|
+
const board = await duplicateBoard(
|
|
1409
|
+
projectRoot,
|
|
1410
|
+
input.boardId,
|
|
1411
|
+
duplicateBoardOptions(input)
|
|
1412
|
+
);
|
|
1413
|
+
return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
|
|
1414
|
+
}
|
|
1415
|
+
case "delete_board": {
|
|
1416
|
+
if (!input.boardId) return fail("delete_board requires boardId.");
|
|
1417
|
+
const removed = await removeBoard2(projectRoot, input.boardId);
|
|
1418
|
+
return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
|
|
1419
|
+
}
|
|
1420
|
+
case "generate_board": {
|
|
1421
|
+
if (!input.description) return fail("generate_board requires description.");
|
|
1422
|
+
const boardInput = createBoardFromText({
|
|
1423
|
+
description: input.description,
|
|
1424
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1425
|
+
...input.context !== void 0 ? { context: input.context } : {},
|
|
1426
|
+
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
1427
|
+
});
|
|
1428
|
+
const board = await createBoard2(projectRoot, boardInput);
|
|
1429
|
+
for (const taskInput2 of parseLinesIntoTasks(
|
|
1430
|
+
input.description,
|
|
1431
|
+
board.columns[0]?.id ?? "backlog"
|
|
1432
|
+
)) {
|
|
1433
|
+
await addTask(projectRoot, board.id, taskInput2);
|
|
1434
|
+
}
|
|
1435
|
+
return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
|
|
1436
|
+
}
|
|
1437
|
+
case "export_markdown": {
|
|
1438
|
+
const board = await requireBoard(projectRoot, input.boardId);
|
|
1439
|
+
if (!board) return fail("Board not found.");
|
|
1440
|
+
return {
|
|
1441
|
+
ok: true,
|
|
1442
|
+
message: "Board exported.",
|
|
1443
|
+
board,
|
|
1444
|
+
markdown: exportBoardAsMarkdown(board)
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
case "export_task_graph": {
|
|
1448
|
+
if (!input.boardId) return fail("export_task_graph requires boardId.");
|
|
1449
|
+
const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
|
|
1450
|
+
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
1451
|
+
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
1452
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1453
|
+
...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
|
|
1454
|
+
...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
|
|
1455
|
+
});
|
|
1456
|
+
if (!exported) return fail("Board not found.");
|
|
1457
|
+
return {
|
|
1458
|
+
ok: true,
|
|
1459
|
+
message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
|
|
1460
|
+
board: exported.board,
|
|
1461
|
+
taskGraph: serializeTaskGraph(exported.graph)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
case "sync_task_graph": {
|
|
1465
|
+
if (!input.boardId || !input.taskGraph) {
|
|
1466
|
+
return fail("sync_task_graph requires boardId and taskGraph.");
|
|
1467
|
+
}
|
|
1468
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
1469
|
+
const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
|
|
1470
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1471
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1472
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
1473
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
1474
|
+
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
1475
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
1476
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
1477
|
+
...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
|
|
1478
|
+
...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
|
|
1479
|
+
});
|
|
1480
|
+
return result2 ? {
|
|
1481
|
+
ok: true,
|
|
1482
|
+
message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
|
|
1483
|
+
board: result2.board
|
|
1484
|
+
} : fail("Board not found.");
|
|
1485
|
+
}
|
|
1486
|
+
case "create_from_graph": {
|
|
1487
|
+
if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
|
|
1488
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
1489
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
1490
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1491
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1492
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
1493
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
1494
|
+
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
1495
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
1496
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
|
|
1497
|
+
});
|
|
1498
|
+
return {
|
|
1499
|
+
ok: true,
|
|
1500
|
+
message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
|
|
1501
|
+
board
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
case "import_session_tasks": {
|
|
1505
|
+
const taskPath = ctx.meta?.["task.path"];
|
|
1506
|
+
if (!taskPath) return fail("No session task file for this session.");
|
|
1507
|
+
const file = await loadTasks2(taskPath);
|
|
1508
|
+
if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
|
|
1509
|
+
const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
|
|
1510
|
+
const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
|
|
1511
|
+
const tags = ["session", `session:${sessionId}`];
|
|
1512
|
+
const existing = (await listBoards2(projectRoot)).find(
|
|
1513
|
+
(b) => b.tags?.includes(`session:${sessionId}`)
|
|
1514
|
+
);
|
|
1515
|
+
if (existing) {
|
|
1516
|
+
const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
|
|
1517
|
+
sourceSystem: "session",
|
|
1518
|
+
tags,
|
|
1519
|
+
archiveMissingTasks: true,
|
|
1520
|
+
includeCompletedTasks: true
|
|
1521
|
+
});
|
|
1522
|
+
return result2 ? {
|
|
1523
|
+
ok: true,
|
|
1524
|
+
message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
|
|
1525
|
+
board: result2.board
|
|
1526
|
+
} : fail("Session board vanished mid-sync.");
|
|
1527
|
+
}
|
|
1528
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
1529
|
+
title: `Session tasks (${sessionId.slice(0, 8)})`,
|
|
1530
|
+
sourceSystem: "session",
|
|
1531
|
+
tags
|
|
1532
|
+
});
|
|
1533
|
+
return {
|
|
1534
|
+
ok: true,
|
|
1535
|
+
message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
|
|
1536
|
+
board
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
case "search_tasks": {
|
|
1540
|
+
const tasks = await searchKanban(projectRoot, {
|
|
1541
|
+
query: input.query,
|
|
1542
|
+
boardId: input.boardId,
|
|
1543
|
+
assignedAgent: input.agentId,
|
|
1544
|
+
status: input.status,
|
|
1545
|
+
priority: input.priority,
|
|
1546
|
+
label: input.labels?.[0],
|
|
1547
|
+
chainId: input.chainId
|
|
1548
|
+
});
|
|
1549
|
+
return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
|
|
1550
|
+
}
|
|
1551
|
+
case "ready_tasks": {
|
|
1552
|
+
const tasks = await listReadyTasks(projectRoot, {
|
|
1553
|
+
query: input.query,
|
|
1554
|
+
boardId: input.boardId,
|
|
1555
|
+
assignedAgent: input.agentId,
|
|
1556
|
+
priority: input.priority,
|
|
1557
|
+
label: input.labels?.[0],
|
|
1558
|
+
chainId: input.chainId,
|
|
1559
|
+
limit: input.limit
|
|
1560
|
+
});
|
|
1561
|
+
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1562
|
+
}
|
|
1563
|
+
case "snapshot": {
|
|
1564
|
+
const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
|
|
1565
|
+
query: input.query,
|
|
1566
|
+
boardId: input.boardId,
|
|
1567
|
+
assignedAgent: input.agentId,
|
|
1568
|
+
status: input.status,
|
|
1569
|
+
priority: input.priority,
|
|
1570
|
+
label: input.labels?.[0],
|
|
1571
|
+
chainId: input.chainId
|
|
1572
|
+
});
|
|
1573
|
+
return {
|
|
1574
|
+
ok: true,
|
|
1575
|
+
message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
|
|
1576
|
+
snapshot
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
case "add_column": {
|
|
1580
|
+
if (!input.boardId || !input.title)
|
|
1581
|
+
return fail("add_column requires boardId and title.");
|
|
1582
|
+
const result2 = await addColumn(projectRoot, input.boardId, {
|
|
1583
|
+
title: input.title,
|
|
1584
|
+
...input.description !== void 0 ? { description: input.description } : {}
|
|
1585
|
+
});
|
|
1586
|
+
return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
|
|
1587
|
+
}
|
|
1588
|
+
case "update_column": {
|
|
1589
|
+
if (!input.boardId || !input.columnId)
|
|
1590
|
+
return fail("update_column requires boardId and columnId.");
|
|
1591
|
+
const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
|
|
1592
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1593
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1594
|
+
...input.order !== void 0 ? { order: input.order } : {}
|
|
1595
|
+
});
|
|
1596
|
+
return board ? okBoard(board, "Column updated.") : fail("Column not found.");
|
|
1597
|
+
}
|
|
1598
|
+
case "delete_column": {
|
|
1599
|
+
if (!input.boardId || !input.columnId)
|
|
1600
|
+
return fail("delete_column requires boardId and columnId.");
|
|
1601
|
+
const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
|
|
1602
|
+
moveTasksToColumnId: input.moveTasksToColumnId
|
|
1603
|
+
});
|
|
1604
|
+
return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
|
|
1605
|
+
}
|
|
1606
|
+
case "add_task": {
|
|
1607
|
+
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1608
|
+
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
1609
|
+
if (!result2) return fail("Board not found.");
|
|
1610
|
+
return okTask(result2.board, result2.task, `Task added.${atomicityNudge(result2.task)}`);
|
|
1611
|
+
}
|
|
1612
|
+
case "split_task": {
|
|
1613
|
+
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
1614
|
+
return fail("split_task requires boardId, taskId, and childTitles.");
|
|
1615
|
+
}
|
|
1616
|
+
return handleSplitTask(projectRoot, input, {});
|
|
1617
|
+
}
|
|
1618
|
+
case "merge_tasks": {
|
|
1619
|
+
if (!input.boardId || !input.taskIds?.length || !input.title) {
|
|
1620
|
+
return fail("merge_tasks requires boardId, taskIds, and title.");
|
|
1621
|
+
}
|
|
1622
|
+
const result2 = await mergeTasks(projectRoot, input.boardId, {
|
|
1623
|
+
taskIds: input.taskIds,
|
|
1624
|
+
title: input.title,
|
|
1625
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
1626
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1627
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1628
|
+
...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
|
|
1629
|
+
});
|
|
1630
|
+
return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
|
|
1631
|
+
}
|
|
1632
|
+
case "copy_task": {
|
|
1633
|
+
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1634
|
+
return fail("copy_task requires boardId, taskId, and targetBoardId.");
|
|
1635
|
+
}
|
|
1636
|
+
const result2 = await copyTaskToBoard(
|
|
1637
|
+
projectRoot,
|
|
1638
|
+
input.boardId,
|
|
1639
|
+
input.taskId,
|
|
1640
|
+
input.targetBoardId,
|
|
1641
|
+
{
|
|
1642
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1643
|
+
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1644
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1645
|
+
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1646
|
+
}
|
|
1647
|
+
);
|
|
1648
|
+
return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
|
|
1649
|
+
}
|
|
1650
|
+
case "transfer_task": {
|
|
1651
|
+
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1652
|
+
return fail("transfer_task requires boardId, taskId, and targetBoardId.");
|
|
1653
|
+
}
|
|
1654
|
+
const result2 = await transferTaskToBoard(
|
|
1655
|
+
projectRoot,
|
|
1656
|
+
input.boardId,
|
|
1657
|
+
input.taskId,
|
|
1658
|
+
input.targetBoardId,
|
|
1659
|
+
{
|
|
1660
|
+
...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
|
|
1661
|
+
...input.order !== void 0 ? { targetOrder: input.order } : {},
|
|
1662
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
|
|
1663
|
+
...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
|
|
1664
|
+
}
|
|
1665
|
+
);
|
|
1666
|
+
return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
|
|
1667
|
+
}
|
|
1668
|
+
case "get_task": {
|
|
1669
|
+
if (!input.boardId || !input.taskId)
|
|
1670
|
+
return fail("get_task requires boardId and taskId.");
|
|
1671
|
+
const task = await getTask(projectRoot, input.boardId, input.taskId);
|
|
1672
|
+
return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
|
|
1673
|
+
}
|
|
1674
|
+
case "start_task": {
|
|
1675
|
+
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1676
|
+
return fail("start_task requires boardId, taskId, author, and transitionComment.");
|
|
1677
|
+
}
|
|
1678
|
+
let board = await getBoard3(projectRoot, input.boardId);
|
|
1679
|
+
let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1680
|
+
if (!board || !task) return fail("Board or task not found.");
|
|
1681
|
+
const readiness = evaluateContractGraphReadiness(board, task.id);
|
|
1682
|
+
if (!readiness.ready) {
|
|
1683
|
+
return fail(
|
|
1684
|
+
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
let stage = task.lifecycle?.currentStage;
|
|
1688
|
+
if (stage === "backlog") {
|
|
1689
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1690
|
+
to: "todo",
|
|
1691
|
+
actor: input.author,
|
|
1692
|
+
comment: input.transitionComment
|
|
1693
|
+
});
|
|
1694
|
+
if (!moved) return fail("Task could not enter Todo.");
|
|
1695
|
+
board = moved.board;
|
|
1696
|
+
task = moved.task;
|
|
1697
|
+
stage = task.lifecycle?.currentStage;
|
|
1698
|
+
}
|
|
1699
|
+
if (stage === "todo" || stage === "review") {
|
|
1700
|
+
const now = /* @__PURE__ */ new Date();
|
|
1701
|
+
const leaseId = input.leaseId ?? randomUUID2();
|
|
1702
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1703
|
+
status: "running",
|
|
1704
|
+
agentId: input.agentId ?? input.author,
|
|
1705
|
+
leaseId,
|
|
1706
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1707
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1708
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1709
|
+
attempt: input.attempt ?? 1,
|
|
1710
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1711
|
+
});
|
|
1712
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1713
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1714
|
+
to: "running",
|
|
1715
|
+
actor: input.author,
|
|
1716
|
+
comment: input.transitionComment
|
|
1717
|
+
});
|
|
1718
|
+
if (!moved) return fail("Task could not enter Running.");
|
|
1719
|
+
board = moved.board;
|
|
1720
|
+
task = moved.task;
|
|
1721
|
+
stage = task.lifecycle?.currentStage;
|
|
1722
|
+
}
|
|
1723
|
+
if (stage !== "running" || task.assignment?.status !== "running") {
|
|
1724
|
+
return fail(
|
|
1725
|
+
`start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
ctx.setCurrentKanbanTask(task.id, board.id);
|
|
1729
|
+
return okTask(
|
|
1730
|
+
board,
|
|
1731
|
+
task,
|
|
1732
|
+
"Task is active; runtime Kanban governance is now bound to this run."
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
case "update_task": {
|
|
1736
|
+
if (!input.boardId || !input.taskId)
|
|
1737
|
+
return fail("update_task requires boardId and taskId.");
|
|
1738
|
+
const board = await updateTask2(
|
|
1739
|
+
projectRoot,
|
|
1740
|
+
input.boardId,
|
|
1741
|
+
input.taskId,
|
|
1742
|
+
taskPatch(input)
|
|
1743
|
+
);
|
|
1744
|
+
return board ? okBoard(board, "Task updated.") : fail("Task not found.");
|
|
1745
|
+
}
|
|
1746
|
+
case "transition_task": {
|
|
1747
|
+
if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
|
|
1748
|
+
return fail(
|
|
1749
|
+
"transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
if (input.lifecycleStage === "done") {
|
|
1753
|
+
const boardBefore = await getBoard3(projectRoot, input.boardId);
|
|
1754
|
+
const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
|
|
1755
|
+
if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
|
|
1756
|
+
const preGate = await verifyTaskCompletion2(
|
|
1757
|
+
projectRoot,
|
|
1758
|
+
input.boardId,
|
|
1759
|
+
taskBefore.id,
|
|
1760
|
+
{
|
|
1761
|
+
persist: false
|
|
1762
|
+
}
|
|
1763
|
+
);
|
|
1764
|
+
await updateTask2(projectRoot, input.boardId, taskBefore.id, {
|
|
1765
|
+
verificationReport: preGate.report,
|
|
1766
|
+
successCriteria: preGate.task.successCriteria
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
|
|
1771
|
+
to: input.lifecycleStage,
|
|
1772
|
+
actor: input.author,
|
|
1773
|
+
comment: input.transitionComment,
|
|
1774
|
+
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
1775
|
+
...input.attachmentUrl !== void 0 ? {
|
|
1776
|
+
attachment: {
|
|
1777
|
+
url: input.attachmentUrl,
|
|
1778
|
+
type: input.attachmentType ?? "url",
|
|
1779
|
+
...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
|
|
1780
|
+
}
|
|
1781
|
+
} : {},
|
|
1782
|
+
patch: taskPatch(input)
|
|
1783
|
+
});
|
|
1784
|
+
if (result2 && input.lifecycleStage === "done" && result2.task.verificationReport) {
|
|
1785
|
+
recordKanbanVerificationEvidence(ctx, result2.task.verificationReport);
|
|
1786
|
+
}
|
|
1787
|
+
return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
|
|
1788
|
+
}
|
|
1789
|
+
case "repair_managed_projection": {
|
|
1790
|
+
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1791
|
+
return fail(
|
|
1792
|
+
"repair_managed_projection requires boardId, taskId, author, and transitionComment."
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
const result2 = await repairManagedTaskProjection(
|
|
1796
|
+
projectRoot,
|
|
1797
|
+
input.boardId,
|
|
1798
|
+
input.taskId,
|
|
1799
|
+
{
|
|
1800
|
+
actor: input.author,
|
|
1801
|
+
comment: input.transitionComment
|
|
1802
|
+
}
|
|
1803
|
+
);
|
|
1804
|
+
return result2 ? okTask(
|
|
1805
|
+
result2.board,
|
|
1806
|
+
result2.task,
|
|
1807
|
+
"Managed card projection repaired from lifecycle history."
|
|
1808
|
+
) : fail("Board or task not found.");
|
|
1809
|
+
}
|
|
1810
|
+
case "move_task": {
|
|
1811
|
+
if (!input.boardId || !input.taskId || !input.targetColumnId) {
|
|
1812
|
+
return fail("move_task requires boardId, taskId, and targetColumnId.");
|
|
1813
|
+
}
|
|
1814
|
+
const board = await moveTask(
|
|
1815
|
+
projectRoot,
|
|
1816
|
+
input.boardId,
|
|
1817
|
+
input.taskId,
|
|
1818
|
+
input.targetColumnId,
|
|
1819
|
+
input.order
|
|
1820
|
+
);
|
|
1821
|
+
return board ? okBoard(board, "Task moved.") : fail("Move failed.");
|
|
1822
|
+
}
|
|
1823
|
+
case "delete_task": {
|
|
1824
|
+
if (!input.boardId || !input.taskId)
|
|
1825
|
+
return fail("delete_task requires boardId and taskId.");
|
|
1826
|
+
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
1827
|
+
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1828
|
+
}
|
|
1829
|
+
case "set_chain": {
|
|
1830
|
+
if (!input.boardId || !input.taskIds?.length) {
|
|
1831
|
+
return fail("set_chain requires boardId and taskIds.");
|
|
1832
|
+
}
|
|
1833
|
+
const result2 = await setTaskChain(projectRoot, input.boardId, {
|
|
1834
|
+
taskIds: input.taskIds,
|
|
1835
|
+
...input.chainId !== void 0 ? { chainId: input.chainId } : {},
|
|
1836
|
+
...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
|
|
1837
|
+
});
|
|
1838
|
+
return result2 ? {
|
|
1839
|
+
ok: true,
|
|
1840
|
+
message: `Chain set: ${result2.chainId}`,
|
|
1841
|
+
board: result2.board,
|
|
1842
|
+
chain: result2.tasks
|
|
1843
|
+
} : fail("Board or task not found.");
|
|
1844
|
+
}
|
|
1845
|
+
case "get_chain": {
|
|
1846
|
+
if (!input.boardId || !(input.taskId || input.chainId)) {
|
|
1847
|
+
return fail("get_chain requires boardId and taskId or chainId.");
|
|
1848
|
+
}
|
|
1849
|
+
const result2 = await getTaskChain(
|
|
1850
|
+
projectRoot,
|
|
1851
|
+
input.boardId,
|
|
1852
|
+
input.taskId ?? input.chainId ?? ""
|
|
1853
|
+
);
|
|
1854
|
+
return result2 ? {
|
|
1855
|
+
ok: true,
|
|
1856
|
+
message: `Chain loaded: ${result2.chainId}`,
|
|
1857
|
+
board: result2.board,
|
|
1858
|
+
chain: result2.tasks
|
|
1859
|
+
} : fail("Chain not found.");
|
|
1860
|
+
}
|
|
1861
|
+
case "claim_task": {
|
|
1862
|
+
const result2 = await claimReadyTask(projectRoot, {
|
|
1863
|
+
...input.boardId !== void 0 ? { boardId: input.boardId } : {},
|
|
1864
|
+
...input.taskId !== void 0 ? { taskId: input.taskId } : {},
|
|
1865
|
+
...assignmentInput(input),
|
|
1866
|
+
status: input.assignmentStatus ?? "queued"
|
|
1867
|
+
});
|
|
1868
|
+
return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
|
|
1869
|
+
}
|
|
1870
|
+
case "release_task": {
|
|
1871
|
+
if (!input.boardId || !input.taskId) {
|
|
1872
|
+
return fail("release_task requires boardId and taskId.");
|
|
1873
|
+
}
|
|
1874
|
+
const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
|
|
1875
|
+
...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
|
|
1876
|
+
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
1877
|
+
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
|
|
1878
|
+
});
|
|
1879
|
+
return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
|
|
1880
|
+
}
|
|
1881
|
+
case "assign_task": {
|
|
1882
|
+
if (!input.boardId || !input.taskId)
|
|
1883
|
+
return fail("assign_task requires boardId and taskId.");
|
|
1884
|
+
const board = await assignTask(
|
|
1885
|
+
projectRoot,
|
|
1886
|
+
input.boardId,
|
|
1887
|
+
input.taskId,
|
|
1888
|
+
assignmentInput(input)
|
|
1889
|
+
);
|
|
1890
|
+
return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
|
|
1891
|
+
}
|
|
1892
|
+
case "mark_assignment": {
|
|
1893
|
+
if (!input.boardId || !input.taskId)
|
|
1894
|
+
return fail("mark_assignment requires boardId and taskId.");
|
|
1895
|
+
const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
|
|
1896
|
+
const board = await updateTaskAssignment(
|
|
1897
|
+
projectRoot,
|
|
1898
|
+
input.boardId,
|
|
1899
|
+
input.taskId,
|
|
1900
|
+
{
|
|
1901
|
+
...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
|
|
1902
|
+
...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
|
|
1903
|
+
...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
|
|
1904
|
+
...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
|
|
1905
|
+
...input.error !== void 0 ? { error: input.error } : {},
|
|
1906
|
+
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
1907
|
+
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
1908
|
+
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
1909
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
1910
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
1911
|
+
...input.attempt !== void 0 ? { attempt: input.attempt } : {},
|
|
1912
|
+
...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
|
|
1913
|
+
},
|
|
1914
|
+
// Ownership fence: when expectedLeaseId is supplied, the write is
|
|
1915
|
+
// applied only if the current assignment still holds this lease.
|
|
1916
|
+
// This prevents a recovered+reassigned stale worker's terminal
|
|
1917
|
+
// mark_assignment from overwriting the successor's state. The check
|
|
1918
|
+
// is atomic inside updateTaskAssignment's mutateBoard lock.
|
|
1919
|
+
input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
1920
|
+
);
|
|
1921
|
+
if (!board) return fail("Task not found.");
|
|
1922
|
+
if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
|
|
1923
|
+
const envGate = readEnvGateEnforcement();
|
|
1924
|
+
const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
|
|
1925
|
+
...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
|
|
1926
|
+
...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
|
|
1927
|
+
});
|
|
1928
|
+
if (finalized) {
|
|
1929
|
+
if (finalized.gate.report) {
|
|
1930
|
+
recordKanbanVerificationEvidence(ctx, finalized.gate.report);
|
|
1931
|
+
}
|
|
1932
|
+
const gateSummary = {
|
|
1933
|
+
enforcement: finalized.gate.enforcement,
|
|
1934
|
+
allowed: finalized.gate.allowed,
|
|
1935
|
+
verdict: finalized.gate.verdict,
|
|
1936
|
+
issues: finalized.gate.issues.map((issue) => issue.message)
|
|
1937
|
+
};
|
|
1938
|
+
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(" | ")}`;
|
|
1939
|
+
return {
|
|
1940
|
+
...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
|
|
1941
|
+
gate: gateSummary
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
} else if (board.lifecycle?.mode === "managed") {
|
|
1945
|
+
const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1946
|
+
const stage = managedTask?.lifecycle?.currentStage;
|
|
1947
|
+
const actor = ctx.agentId ?? "kanban-agent";
|
|
1948
|
+
let transitionResult = null;
|
|
1949
|
+
const lifecycleWarnings = [];
|
|
1950
|
+
if (assignmentStatus === "running" && stage === "todo") {
|
|
1951
|
+
try {
|
|
1952
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
1953
|
+
to: "running",
|
|
1954
|
+
actor,
|
|
1955
|
+
comment: "Work started."
|
|
1956
|
+
});
|
|
1957
|
+
} catch (err) {
|
|
1958
|
+
lifecycleWarnings.push(
|
|
1959
|
+
`Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (assignmentStatus === "completed" && stage === "running") {
|
|
1964
|
+
const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
|
|
1965
|
+
try {
|
|
1966
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
1967
|
+
to: "review",
|
|
1968
|
+
actor,
|
|
1969
|
+
comment,
|
|
1970
|
+
attachment: {
|
|
1971
|
+
url: `kanban://task/${input.taskId}/result`,
|
|
1972
|
+
title: "Worker completion result",
|
|
1973
|
+
type: "file"
|
|
1974
|
+
},
|
|
1975
|
+
patch: {
|
|
1976
|
+
// Only patch non-description fields so the
|
|
1977
|
+
// original card description is preserved.
|
|
1978
|
+
...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
lifecycleWarnings.push(
|
|
1983
|
+
`Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
if (transitionResult) {
|
|
1987
|
+
const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
|
|
1988
|
+
if (hasCriteria) {
|
|
1989
|
+
try {
|
|
1990
|
+
const verResult = await verifyTaskCompletion2(
|
|
1991
|
+
projectRoot,
|
|
1992
|
+
board.id,
|
|
1993
|
+
input.taskId
|
|
1994
|
+
);
|
|
1995
|
+
if (verResult.report) {
|
|
1996
|
+
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
1997
|
+
}
|
|
1998
|
+
await updateTask2(projectRoot, board.id, input.taskId, {
|
|
1999
|
+
verificationReport: verResult.report,
|
|
2000
|
+
successCriteria: verResult.task.successCriteria
|
|
2001
|
+
});
|
|
2002
|
+
const verdict = verResult.report.verdict;
|
|
2003
|
+
if (verdict === "passed") {
|
|
2004
|
+
try {
|
|
2005
|
+
const doneResult = await transitionTask(
|
|
2006
|
+
projectRoot,
|
|
2007
|
+
board.id,
|
|
2008
|
+
input.taskId,
|
|
2009
|
+
{
|
|
2010
|
+
to: "done",
|
|
2011
|
+
actor,
|
|
2012
|
+
action: "Automated acceptance after verification",
|
|
2013
|
+
comment: "Auto-accepted: verification passed.",
|
|
2014
|
+
attachment: {
|
|
2015
|
+
url: `kanban://task/${input.taskId}/verification`,
|
|
2016
|
+
title: "Auto-verification result",
|
|
2017
|
+
type: "file"
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
);
|
|
2021
|
+
transitionResult = doneResult;
|
|
2022
|
+
} catch (acceptErr) {
|
|
2023
|
+
lifecycleWarnings.push(
|
|
2024
|
+
`Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
} else {
|
|
2028
|
+
lifecycleWarnings.push(
|
|
2029
|
+
`Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
} catch (verifyErr) {
|
|
2033
|
+
lifecycleWarnings.push(
|
|
2034
|
+
`Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
} else {
|
|
2038
|
+
lifecycleWarnings.push(
|
|
2039
|
+
"No automatic success criteria \u2014 card left in Review for manual verification."
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const responseBoard = transitionResult?.board ?? board;
|
|
2045
|
+
const responseTask = transitionResult?.task ?? managedTask;
|
|
2046
|
+
const msgParts = ["Assignment updated."];
|
|
2047
|
+
if (transitionResult) {
|
|
2048
|
+
msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
|
|
2049
|
+
}
|
|
2050
|
+
for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
|
|
2051
|
+
return okTask(responseBoard, responseTask, msgParts.join(" "));
|
|
2052
|
+
}
|
|
2053
|
+
return okBoard(board, "Assignment updated.");
|
|
2054
|
+
}
|
|
2055
|
+
case "heartbeat_assignment": {
|
|
2056
|
+
if (!input.boardId || !input.taskId) {
|
|
2057
|
+
return fail("heartbeat_assignment requires boardId and taskId.");
|
|
2058
|
+
}
|
|
2059
|
+
const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
|
|
2060
|
+
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
2061
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
2062
|
+
// Ownership fence: when expectedLeaseId is supplied, the renewal
|
|
2063
|
+
// is applied only if the current assignment still holds this lease.
|
|
2064
|
+
// This prevents a recovered+reassigned stale worker's heartbeat
|
|
2065
|
+
// from renewing the successor's lease. The check is atomic inside
|
|
2066
|
+
// heartbeatTaskAssignment's mutateBoard lock.
|
|
2067
|
+
...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
2068
|
+
});
|
|
2069
|
+
return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
|
|
2070
|
+
}
|
|
2071
|
+
case "recover_stale": {
|
|
2072
|
+
if (!input.boardId) return fail("recover_stale requires boardId.");
|
|
2073
|
+
const policyFields = [
|
|
2074
|
+
input.recoveryPolicyFailOnCostCeiling !== void 0,
|
|
2075
|
+
input.recoveryPolicyReleaseOnFailureKinds !== void 0,
|
|
2076
|
+
input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
|
|
2077
|
+
input.recoveryPolicyRetryPolicyOverride !== void 0
|
|
2078
|
+
].some(Boolean);
|
|
2079
|
+
const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
|
|
2080
|
+
...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
|
|
2081
|
+
...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
|
|
2082
|
+
...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
|
|
2083
|
+
...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
|
|
2084
|
+
...policyFields ? {
|
|
2085
|
+
policy: {
|
|
2086
|
+
...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
|
|
2087
|
+
...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
|
|
2088
|
+
releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
|
|
2089
|
+
} : {},
|
|
2090
|
+
...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
|
|
2091
|
+
releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
|
|
2092
|
+
} : {},
|
|
2093
|
+
...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
|
|
2094
|
+
retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
|
|
2095
|
+
} : {}
|
|
2096
|
+
}
|
|
2097
|
+
} : {}
|
|
2098
|
+
});
|
|
2099
|
+
return result2 ? {
|
|
2100
|
+
ok: true,
|
|
2101
|
+
message: `Recovered ${result2.tasks.length} stale assignment(s).`,
|
|
2102
|
+
board: result2.board,
|
|
2103
|
+
recoveredTasks: result2.tasks
|
|
2104
|
+
} : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
|
|
2105
|
+
}
|
|
2106
|
+
case "events": {
|
|
2107
|
+
if (!input.boardId) return fail("events requires boardId.");
|
|
2108
|
+
const eventList = await listKanbanEvents(projectRoot, input.boardId);
|
|
2109
|
+
return {
|
|
2110
|
+
ok: true,
|
|
2111
|
+
message: `${eventList.length} event(s).`,
|
|
2112
|
+
events: eventList
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
case "queue_health": {
|
|
2116
|
+
const health = await getKanbanQueueHealth(projectRoot, {
|
|
2117
|
+
...input.boardId !== void 0 ? { boardId: input.boardId } : {}
|
|
2118
|
+
});
|
|
2119
|
+
return {
|
|
2120
|
+
ok: true,
|
|
2121
|
+
message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
2122
|
+
queueHealth: health
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
default:
|
|
2126
|
+
{
|
|
2127
|
+
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
2128
|
+
if (detailResult !== void 0) return detailResult;
|
|
2129
|
+
}
|
|
2130
|
+
return fail(`Unknown kanban action: ${input.action}`);
|
|
2131
|
+
}
|
|
2132
|
+
})();
|
|
2133
|
+
return withPresence(result);
|
|
2134
|
+
} catch (err) {
|
|
2135
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2140
|
+
// src/todo.ts
|
|
2141
|
+
function normalizedTitle(value) {
|
|
2142
|
+
return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
|
|
2143
|
+
}
|
|
2144
|
+
function activeBoardId(items, ctx) {
|
|
2145
|
+
const metaKanban = ctx.meta?.["kanban"];
|
|
2146
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
2147
|
+
return ctx.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0) ?? items.find((item) => item.kanbanBoardId)?.kanbanBoardId ?? "";
|
|
2148
|
+
}
|
|
2149
|
+
function bindTodosToBoard(items, previous, board) {
|
|
2150
|
+
const previousById = new Map(previous.map((item) => [item.id, item]));
|
|
2151
|
+
const available = board.tasks.filter(
|
|
2152
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
2153
|
+
).sort(
|
|
2154
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.order - right.order
|
|
2155
|
+
);
|
|
2156
|
+
const used = /* @__PURE__ */ new Set();
|
|
2157
|
+
return items.map((item) => {
|
|
2158
|
+
const previousItem = previousById.get(item.id);
|
|
2159
|
+
const requestedTaskId = item.kanbanBoardId === board.id ? item.kanbanTaskId : previousItem?.kanbanBoardId === board.id ? previousItem.kanbanTaskId : void 0;
|
|
2160
|
+
const title = normalizedTitle(item.content);
|
|
2161
|
+
const candidates = [
|
|
2162
|
+
requestedTaskId ? board.tasks.find((task2) => task2.id === requestedTaskId) : void 0,
|
|
2163
|
+
board.tasks.find((task2) => task2.id === item.id),
|
|
2164
|
+
board.tasks.find((task2) => task2.origin?.taskId === item.id),
|
|
2165
|
+
available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
|
|
2166
|
+
];
|
|
2167
|
+
const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
|
|
2168
|
+
if (!task) return { ...item };
|
|
2169
|
+
used.add(task.id);
|
|
2170
|
+
return { ...item, kanbanBoardId: board.id, kanbanTaskId: task.id };
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
2174
|
+
let synced = 0;
|
|
2175
|
+
const warnings = [];
|
|
2176
|
+
const actor = ctx.agentId?.trim() || ctx.agentName?.trim() || "kanban-agent";
|
|
2177
|
+
const execute = async (input) => {
|
|
2178
|
+
const result = await kanbanTool.execute(input, ctx, { signal });
|
|
2179
|
+
if (!result.ok) warnings.push(result.message);
|
|
2180
|
+
else {
|
|
2181
|
+
synced++;
|
|
2182
|
+
if (result.message.includes("Warning:")) warnings.push(result.message);
|
|
2183
|
+
}
|
|
2184
|
+
return result;
|
|
2185
|
+
};
|
|
2186
|
+
for (const item of items) {
|
|
2187
|
+
if (item.status !== "pending" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2191
|
+
if (task?.lifecycle?.currentStage !== "running") continue;
|
|
2192
|
+
const released = await execute({
|
|
2193
|
+
action: "mark_assignment",
|
|
2194
|
+
boardId: board.id,
|
|
2195
|
+
taskId: task.id,
|
|
2196
|
+
assignmentStatus: "assigned",
|
|
2197
|
+
agentId: actor,
|
|
2198
|
+
lastResult: `Todo returned to queue: ${item.content}`
|
|
2199
|
+
});
|
|
2200
|
+
if (!released.ok) continue;
|
|
2201
|
+
await execute({
|
|
2202
|
+
action: "transition_task",
|
|
2203
|
+
boardId: board.id,
|
|
2204
|
+
taskId: task.id,
|
|
2205
|
+
lifecycleStage: "todo",
|
|
2206
|
+
author: actor,
|
|
2207
|
+
transitionComment: `Todo returned to queue: ${item.content}`
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
for (const item of items) {
|
|
2211
|
+
if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2215
|
+
if (!task || task.status === "completed") continue;
|
|
2216
|
+
await execute({
|
|
2217
|
+
action: "mark_assignment",
|
|
2218
|
+
boardId: board.id,
|
|
2219
|
+
taskId: task.id,
|
|
2220
|
+
assignmentStatus: "completed",
|
|
2221
|
+
agentId: actor,
|
|
2222
|
+
lastResult: `Todo completed: ${item.content}`
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
const attemptedParents = /* @__PURE__ */ new Set();
|
|
2226
|
+
let afterCompletions = await getBoard4(ctx.projectRoot, board.id);
|
|
2227
|
+
while (afterCompletions) {
|
|
2228
|
+
const parent = afterCompletions.tasks.find(
|
|
2229
|
+
(task) => task.atomic === true && task.status !== "completed" && Boolean(task.childTaskIds?.length) && !attemptedParents.has(task.id) && task.childTaskIds?.every(
|
|
2230
|
+
(childId) => afterCompletions?.tasks.find((candidate) => candidate.id === childId)?.status === "completed"
|
|
2231
|
+
)
|
|
2232
|
+
);
|
|
2233
|
+
if (!parent) break;
|
|
2234
|
+
attemptedParents.add(parent.id);
|
|
2235
|
+
const started = await execute({
|
|
2236
|
+
action: "start_task",
|
|
2237
|
+
boardId: board.id,
|
|
2238
|
+
taskId: parent.id,
|
|
2239
|
+
author: actor,
|
|
2240
|
+
agentId: actor,
|
|
2241
|
+
transitionComment: "All child tasks completed; validating composite parent."
|
|
2242
|
+
});
|
|
2243
|
+
if (started.ok) {
|
|
2244
|
+
await execute({
|
|
2245
|
+
action: "mark_assignment",
|
|
2246
|
+
boardId: board.id,
|
|
2247
|
+
taskId: parent.id,
|
|
2248
|
+
assignmentStatus: "completed",
|
|
2249
|
+
agentId: actor,
|
|
2250
|
+
lastResult: "All child tasks completed; composite result ready for verification."
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
afterCompletions = await getBoard4(ctx.projectRoot, board.id);
|
|
2254
|
+
}
|
|
2255
|
+
const completionPending = items.some(
|
|
2256
|
+
(item) => item.status === "completed" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId) && afterCompletions?.tasks.find((task) => task.id === item.kanbanTaskId)?.status !== "completed"
|
|
2257
|
+
);
|
|
2258
|
+
const active = items.find(
|
|
2259
|
+
(item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
|
|
2260
|
+
);
|
|
2261
|
+
if (active?.kanbanTaskId) {
|
|
2262
|
+
await execute({
|
|
2263
|
+
action: "start_task",
|
|
2264
|
+
boardId: board.id,
|
|
2265
|
+
taskId: active.kanbanTaskId,
|
|
2266
|
+
author: actor,
|
|
2267
|
+
agentId: actor,
|
|
2268
|
+
transitionComment: `Todo activated: ${active.content}`
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
if (active?.kanbanTaskId && completionPending) {
|
|
2272
|
+
warnings.push(
|
|
2273
|
+
"A completed todo is still awaiting acceptance; the next independent Kanban task was started."
|
|
2274
|
+
);
|
|
2275
|
+
} else if (!active && ctx.currentKanbanBoardId === board.id) {
|
|
2276
|
+
ctx.setCurrentKanbanTask(void 0, board.id);
|
|
2277
|
+
}
|
|
2278
|
+
return { synced, warnings };
|
|
2279
|
+
}
|
|
2280
|
+
var todoTool = {
|
|
2281
|
+
name: "todo",
|
|
2282
|
+
category: "Session",
|
|
2283
|
+
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.",
|
|
2284
|
+
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.",
|
|
2285
|
+
permission: "confirm",
|
|
2286
|
+
mutating: true,
|
|
2287
|
+
timeoutMs: 3e4,
|
|
2288
|
+
capabilities: ["session.todo", "fs.write"],
|
|
2289
|
+
subjectKey: "todos",
|
|
2290
|
+
icon: "todo",
|
|
2291
|
+
inputSchema: {
|
|
2292
|
+
type: "object",
|
|
2293
|
+
properties: {
|
|
2294
|
+
todos: {
|
|
2295
|
+
type: "array",
|
|
2296
|
+
items: {
|
|
2297
|
+
type: "object",
|
|
2298
|
+
properties: {
|
|
2299
|
+
id: {
|
|
2300
|
+
type: "string",
|
|
2301
|
+
description: 'Unique identifier for the todo item (e.g. "1", "auth-flow").'
|
|
2302
|
+
},
|
|
2303
|
+
content: {
|
|
2304
|
+
type: "string",
|
|
2305
|
+
description: "Clear, actionable description of the task."
|
|
2306
|
+
},
|
|
2307
|
+
status: {
|
|
2308
|
+
type: "string",
|
|
2309
|
+
enum: ["pending", "in_progress", "completed"],
|
|
2310
|
+
description: 'Current status. Only one item should be "in_progress" at a time.'
|
|
2311
|
+
},
|
|
2312
|
+
activeForm: {
|
|
2313
|
+
type: "string",
|
|
2314
|
+
description: 'Optional present-tense form shown while the task is active (e.g. "Fixing auth bug").'
|
|
2315
|
+
},
|
|
2316
|
+
kanbanBoardId: {
|
|
2317
|
+
type: "string",
|
|
2318
|
+
description: "Kanban board that owns this UI row when Kanban is active."
|
|
2319
|
+
},
|
|
2320
|
+
kanbanTaskId: {
|
|
2321
|
+
type: "string",
|
|
2322
|
+
description: "Real Kanban task represented by this UI row."
|
|
2323
|
+
}
|
|
2324
|
+
},
|
|
2325
|
+
required: ["id", "content", "status"]
|
|
2326
|
+
},
|
|
2327
|
+
description: "The desired todo list. Supplied rows are replaced/reordered; unfinished omitted rows are retained."
|
|
2328
|
+
}
|
|
2329
|
+
},
|
|
2330
|
+
required: ["todos"]
|
|
2331
|
+
},
|
|
2332
|
+
async execute(input, ctx, call) {
|
|
2333
|
+
if (!Array.isArray(input?.todos)) {
|
|
2334
|
+
throw new Error("todo: todos must be an array");
|
|
2335
|
+
}
|
|
2336
|
+
const items = input.todos.filter((t) => Boolean(t?.id && t.content));
|
|
2337
|
+
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}`;
|
|
2338
|
+
const requestedIdentities = new Set(items.map(todoIdentity));
|
|
2339
|
+
for (const previous of ctx.todos ?? []) {
|
|
2340
|
+
const identity = todoIdentity(previous);
|
|
2341
|
+
if (previous.status === "completed" || requestedIdentities.has(identity)) {
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
items.push({ ...previous });
|
|
2345
|
+
requestedIdentities.add(identity);
|
|
2346
|
+
}
|
|
2347
|
+
const inProgress = items.filter((t) => t.status === "in_progress");
|
|
2348
|
+
if (inProgress.length > 1) {
|
|
2349
|
+
let seenInProgress = false;
|
|
2350
|
+
for (const item of items) {
|
|
2351
|
+
if (item.status === "in_progress") {
|
|
2352
|
+
if (seenInProgress) item.status = "pending";
|
|
2353
|
+
seenInProgress = true;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const boardId = activeBoardId(items, ctx);
|
|
2358
|
+
const board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
|
|
2359
|
+
const boundItems = board?.lifecycle?.mode === "managed" ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
2360
|
+
ctx.state.replaceTodos(boundItems);
|
|
2361
|
+
const kanbanSync = board?.lifecycle?.mode === "managed" ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
|
|
2362
|
+
if (board?.lifecycle?.mode === "managed") {
|
|
2363
|
+
const unresolved = boundItems.filter(
|
|
2364
|
+
(item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
|
|
2365
|
+
);
|
|
2366
|
+
if (unresolved.length > 0) {
|
|
2367
|
+
kanbanSync.warnings.push(
|
|
2368
|
+
`${unresolved.length} Todo row(s) did not match a real Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
let projectedBoard = board;
|
|
2373
|
+
if (board?.lifecycle?.mode === "managed") {
|
|
2374
|
+
const refreshed = await getBoard4(ctx.projectRoot, board.id);
|
|
2375
|
+
if (refreshed) {
|
|
2376
|
+
projectedBoard = refreshed;
|
|
2377
|
+
applyManagedKanbanBoardToTodos(ctx, refreshed);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
if (board?.lifecycle?.mode !== "managed") {
|
|
2381
|
+
mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
2382
|
+
}
|
|
2383
|
+
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
2384
|
+
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
2385
|
+
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
2386
|
+
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
2387
|
+
for (const item of items) {
|
|
2388
|
+
if (item.promotedFromPlan) {
|
|
2389
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
|
|
2390
|
+
item.promotedFromPlan
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
if (item.promotedFromTask) {
|
|
2394
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
|
|
2395
|
+
item.promotedFromTask
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
for (const planId of completedPlanIds) {
|
|
2400
|
+
if (pendingPlanIds.has(planId)) continue;
|
|
2401
|
+
const planPath = ctx.meta["plan.path"];
|
|
2402
|
+
if (typeof planPath !== "string" || !planPath) continue;
|
|
2403
|
+
try {
|
|
2404
|
+
const plan = await loadPlan2(planPath);
|
|
2405
|
+
if (plan) {
|
|
2406
|
+
const updated = setPlanItemStatus(plan, planId, "done");
|
|
2407
|
+
await savePlan(planPath, updated);
|
|
2408
|
+
}
|
|
2409
|
+
} catch {
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
for (const taskId of completedTaskIds) {
|
|
2413
|
+
if (pendingTaskIds.has(taskId)) continue;
|
|
2414
|
+
const taskPath = ctx.meta["task.path"];
|
|
2415
|
+
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
2416
|
+
try {
|
|
2417
|
+
const file = await loadTasks3(taskPath);
|
|
2418
|
+
if (file) {
|
|
2419
|
+
const task = file.tasks.find((t) => t.id === taskId);
|
|
2420
|
+
if (task && task.status !== "completed") {
|
|
2421
|
+
task.status = "completed";
|
|
2422
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2423
|
+
await saveTasks(taskPath, file);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
} catch {
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return {
|
|
2430
|
+
count: items.length,
|
|
2431
|
+
in_progress: (ctx.todos ?? boundItems).filter((t) => t.status === "in_progress").length,
|
|
2432
|
+
...kanbanSync.synced > 0 ? { kanban_synced: kanbanSync.synced } : {},
|
|
2433
|
+
...kanbanSync.warnings.length > 0 ? { kanban_warnings: kanbanSync.warnings } : {},
|
|
2434
|
+
...projectedBoard?.lifecycle?.mode === "managed" ? {
|
|
2435
|
+
kanban_bindings: boundItems.flatMap((item) => {
|
|
2436
|
+
if (item.kanbanBoardId !== projectedBoard.id || !item.kanbanTaskId) return [];
|
|
2437
|
+
const task = projectedBoard.tasks.find(
|
|
2438
|
+
(candidate) => candidate.id === item.kanbanTaskId
|
|
2439
|
+
);
|
|
2440
|
+
return task ? [
|
|
2441
|
+
{
|
|
2442
|
+
todoId: item.id,
|
|
2443
|
+
boardId: projectedBoard.id,
|
|
2444
|
+
taskId: task.id,
|
|
2445
|
+
taskStatus: task.status
|
|
2446
|
+
}
|
|
2447
|
+
] : [];
|
|
2448
|
+
})
|
|
2449
|
+
} : {}
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
217
2453
|
|
|
218
2454
|
// src/task.ts
|
|
219
2455
|
function findTaskIndex(tasks, query) {
|
|
@@ -231,9 +2467,10 @@ var taskTool = {
|
|
|
231
2467
|
name: "task",
|
|
232
2468
|
category: "Session",
|
|
233
2469
|
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
|
|
2470
|
+
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
2471
|
permission: "confirm",
|
|
236
2472
|
mutating: true,
|
|
2473
|
+
subjectKey: "action",
|
|
237
2474
|
capabilities: ["fs.write"],
|
|
238
2475
|
icon: "task",
|
|
239
2476
|
timeoutMs: 5e3,
|
|
@@ -253,9 +2490,15 @@ var taskTool = {
|
|
|
253
2490
|
id: { type: "string", description: 'Unique id (e.g. "t1", "auth-flow").' },
|
|
254
2491
|
title: { type: "string", description: "Short title." },
|
|
255
2492
|
description: { type: "string", description: "Optional details." },
|
|
256
|
-
type: {
|
|
2493
|
+
type: {
|
|
2494
|
+
type: "string",
|
|
2495
|
+
enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
|
|
2496
|
+
},
|
|
257
2497
|
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
258
|
-
status: {
|
|
2498
|
+
status: {
|
|
2499
|
+
type: "string",
|
|
2500
|
+
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"]
|
|
2501
|
+
},
|
|
259
2502
|
dependsOn: {
|
|
260
2503
|
type: "array",
|
|
261
2504
|
items: { type: "string" },
|
|
@@ -269,16 +2512,22 @@ var taskTool = {
|
|
|
269
2512
|
},
|
|
270
2513
|
required: ["id", "title", "type", "priority", "status"]
|
|
271
2514
|
},
|
|
272
|
-
description: "Complete task list.
|
|
2515
|
+
description: "Complete desired task list. Existing unfinished tasks may not be omitted; complete them first."
|
|
273
2516
|
},
|
|
274
2517
|
task: {
|
|
275
2518
|
type: "object",
|
|
276
2519
|
properties: {
|
|
277
2520
|
title: { type: "string" },
|
|
278
2521
|
description: { type: "string" },
|
|
279
|
-
type: {
|
|
2522
|
+
type: {
|
|
2523
|
+
type: "string",
|
|
2524
|
+
enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
|
|
2525
|
+
},
|
|
280
2526
|
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
281
|
-
status: {
|
|
2527
|
+
status: {
|
|
2528
|
+
type: "string",
|
|
2529
|
+
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"]
|
|
2530
|
+
},
|
|
282
2531
|
dependsOn: { type: "array", items: { type: "string" } },
|
|
283
2532
|
assignee: { type: "string" },
|
|
284
2533
|
estimateHours: { type: "number" },
|
|
@@ -287,7 +2536,10 @@ var taskTool = {
|
|
|
287
2536
|
required: ["title", "type", "priority"],
|
|
288
2537
|
description: "Single task to append (id/createdAt/updatedAt auto-generated)."
|
|
289
2538
|
},
|
|
290
|
-
id: {
|
|
2539
|
+
id: {
|
|
2540
|
+
type: "string",
|
|
2541
|
+
description: "Task id for action=status or target for action=promote."
|
|
2542
|
+
},
|
|
291
2543
|
status: {
|
|
292
2544
|
type: "string",
|
|
293
2545
|
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
|
|
@@ -315,14 +2567,23 @@ var taskTool = {
|
|
|
315
2567
|
let taskPath;
|
|
316
2568
|
if (input.scope === "project") {
|
|
317
2569
|
if (typeof sessionTaskPath === "string") {
|
|
318
|
-
const lastSep = Math.max(
|
|
2570
|
+
const lastSep = Math.max(
|
|
2571
|
+
sessionTaskPath.lastIndexOf("/"),
|
|
2572
|
+
sessionTaskPath.lastIndexOf("\\")
|
|
2573
|
+
);
|
|
319
2574
|
taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
320
2575
|
}
|
|
321
2576
|
} else {
|
|
322
2577
|
taskPath = sessionTaskPath;
|
|
323
2578
|
}
|
|
324
2579
|
if (typeof taskPath !== "string" || !taskPath) {
|
|
325
|
-
return {
|
|
2580
|
+
return {
|
|
2581
|
+
ok: false,
|
|
2582
|
+
message: "Task storage path not configured.",
|
|
2583
|
+
count: 0,
|
|
2584
|
+
completed: 0,
|
|
2585
|
+
inProgress: 0
|
|
2586
|
+
};
|
|
326
2587
|
}
|
|
327
2588
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
328
2589
|
let early = null;
|
|
@@ -338,17 +2599,27 @@ var taskTool = {
|
|
|
338
2599
|
break;
|
|
339
2600
|
case "replace": {
|
|
340
2601
|
if (!Array.isArray(input.tasks)) {
|
|
341
|
-
early = {
|
|
2602
|
+
early = {
|
|
2603
|
+
ok: false,
|
|
2604
|
+
message: "action=replace requires `tasks` array.",
|
|
2605
|
+
count: 0,
|
|
2606
|
+
completed: 0,
|
|
2607
|
+
inProgress: 0
|
|
2608
|
+
};
|
|
342
2609
|
return f;
|
|
343
2610
|
}
|
|
344
2611
|
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
345
2612
|
if (newIds.size !== input.tasks.length) {
|
|
346
2613
|
const seen = /* @__PURE__ */ new Set();
|
|
347
|
-
const dupes = [
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
2614
|
+
const dupes = [
|
|
2615
|
+
...new Set(
|
|
2616
|
+
input.tasks.map((t) => t.id).filter((id) => {
|
|
2617
|
+
if (seen.has(id)) return true;
|
|
2618
|
+
seen.add(id);
|
|
2619
|
+
return false;
|
|
2620
|
+
})
|
|
2621
|
+
)
|
|
2622
|
+
];
|
|
352
2623
|
early = {
|
|
353
2624
|
ok: false,
|
|
354
2625
|
message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
|
|
@@ -358,6 +2629,18 @@ var taskTool = {
|
|
|
358
2629
|
};
|
|
359
2630
|
return f;
|
|
360
2631
|
}
|
|
2632
|
+
const omittedUnfinished = f.tasks.filter(
|
|
2633
|
+
(task) => task.status !== "completed" && !newIds.has(task.id)
|
|
2634
|
+
);
|
|
2635
|
+
if (omittedUnfinished.length > 0) {
|
|
2636
|
+
early = {
|
|
2637
|
+
ok: false,
|
|
2638
|
+
message: `action=replace cannot omit unfinished tasks: ${omittedUnfinished.map((task) => task.id).join(", ")}. Complete them first.`,
|
|
2639
|
+
count: f.tasks.length,
|
|
2640
|
+
...computeTaskItemProgress(f.tasks)
|
|
2641
|
+
};
|
|
2642
|
+
return f;
|
|
2643
|
+
}
|
|
361
2644
|
for (const t of input.tasks) {
|
|
362
2645
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
363
2646
|
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
@@ -372,6 +2655,20 @@ var taskTool = {
|
|
|
372
2655
|
return f;
|
|
373
2656
|
}
|
|
374
2657
|
}
|
|
2658
|
+
if (t.status === "in_progress" || t.status === "completed") {
|
|
2659
|
+
const unmet = (t.dependsOn ?? []).filter(
|
|
2660
|
+
(dependencyId) => input.tasks?.find((candidate) => candidate.id === dependencyId)?.status !== "completed"
|
|
2661
|
+
);
|
|
2662
|
+
if (unmet.length > 0) {
|
|
2663
|
+
early = {
|
|
2664
|
+
ok: false,
|
|
2665
|
+
message: `dependency status validation failed: task "${t.id}" cannot be ${t.status} before completion of ${unmet.join(", ")}.`,
|
|
2666
|
+
count: f.tasks.length,
|
|
2667
|
+
...computeTaskItemProgress(f.tasks)
|
|
2668
|
+
};
|
|
2669
|
+
return f;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
375
2672
|
}
|
|
376
2673
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
377
2674
|
f.tasks = input.tasks.map((t) => ({
|
|
@@ -384,7 +2681,13 @@ var taskTool = {
|
|
|
384
2681
|
case "add": {
|
|
385
2682
|
const t = input.task;
|
|
386
2683
|
if (!t?.title) {
|
|
387
|
-
early = {
|
|
2684
|
+
early = {
|
|
2685
|
+
ok: false,
|
|
2686
|
+
message: "action=add requires `task` with at least `title`.",
|
|
2687
|
+
count: 0,
|
|
2688
|
+
completed: 0,
|
|
2689
|
+
inProgress: 0
|
|
2690
|
+
};
|
|
388
2691
|
return f;
|
|
389
2692
|
}
|
|
390
2693
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
@@ -403,7 +2706,7 @@ var taskTool = {
|
|
|
403
2706
|
}
|
|
404
2707
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
405
2708
|
const newTask = {
|
|
406
|
-
id: `task_${Date.now()}_${
|
|
2709
|
+
id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
|
|
407
2710
|
title: t.title,
|
|
408
2711
|
description: t.description,
|
|
409
2712
|
type: t.type || "feature",
|
|
@@ -421,14 +2724,40 @@ var taskTool = {
|
|
|
421
2724
|
}
|
|
422
2725
|
case "status": {
|
|
423
2726
|
if (!input.id || !input.status) {
|
|
424
|
-
early = {
|
|
2727
|
+
early = {
|
|
2728
|
+
ok: false,
|
|
2729
|
+
message: "action=status requires `id` and `status`.",
|
|
2730
|
+
count: 0,
|
|
2731
|
+
completed: 0,
|
|
2732
|
+
inProgress: 0
|
|
2733
|
+
};
|
|
425
2734
|
return f;
|
|
426
2735
|
}
|
|
427
2736
|
const task = f.tasks.find((t) => t.id === input.id);
|
|
428
2737
|
if (!task) {
|
|
429
|
-
early = {
|
|
2738
|
+
early = {
|
|
2739
|
+
ok: false,
|
|
2740
|
+
message: `Task "${input.id}" not found.`,
|
|
2741
|
+
count: 0,
|
|
2742
|
+
completed: 0,
|
|
2743
|
+
inProgress: 0
|
|
2744
|
+
};
|
|
430
2745
|
return f;
|
|
431
2746
|
}
|
|
2747
|
+
if (input.status === "in_progress" || input.status === "completed") {
|
|
2748
|
+
const unmet = (task.dependsOn ?? []).filter(
|
|
2749
|
+
(dependencyId) => f.tasks.find((candidate) => candidate.id === dependencyId)?.status !== "completed"
|
|
2750
|
+
);
|
|
2751
|
+
if (unmet.length > 0) {
|
|
2752
|
+
early = {
|
|
2753
|
+
ok: false,
|
|
2754
|
+
message: `Task "${task.id}" cannot be ${input.status} before dependencies complete: ${unmet.join(", ")}.`,
|
|
2755
|
+
count: f.tasks.length,
|
|
2756
|
+
...computeTaskItemProgress(f.tasks)
|
|
2757
|
+
};
|
|
2758
|
+
return f;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
432
2761
|
task.status = input.status;
|
|
433
2762
|
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
434
2763
|
break;
|
|
@@ -436,17 +2765,35 @@ var taskTool = {
|
|
|
436
2765
|
case "promote": {
|
|
437
2766
|
const target = input.target?.trim();
|
|
438
2767
|
if (!target) {
|
|
439
|
-
early = {
|
|
2768
|
+
early = {
|
|
2769
|
+
ok: false,
|
|
2770
|
+
message: "action=promote requires `target` (task id, index, or title substring).",
|
|
2771
|
+
count: 0,
|
|
2772
|
+
completed: 0,
|
|
2773
|
+
inProgress: 0
|
|
2774
|
+
};
|
|
440
2775
|
return f;
|
|
441
2776
|
}
|
|
442
2777
|
const idx = findTaskIndex(f.tasks, target);
|
|
443
2778
|
if (idx === -1) {
|
|
444
|
-
early = {
|
|
2779
|
+
early = {
|
|
2780
|
+
ok: false,
|
|
2781
|
+
message: `No task matched "${target}".`,
|
|
2782
|
+
count: 0,
|
|
2783
|
+
completed: 0,
|
|
2784
|
+
inProgress: 0
|
|
2785
|
+
};
|
|
445
2786
|
return f;
|
|
446
2787
|
}
|
|
447
2788
|
const match = f.tasks[idx];
|
|
448
2789
|
if (!match) {
|
|
449
|
-
early = {
|
|
2790
|
+
early = {
|
|
2791
|
+
ok: false,
|
|
2792
|
+
message: `No task matched "${target}".`,
|
|
2793
|
+
count: 0,
|
|
2794
|
+
completed: 0,
|
|
2795
|
+
inProgress: 0
|
|
2796
|
+
};
|
|
450
2797
|
return f;
|
|
451
2798
|
}
|
|
452
2799
|
if (match.status !== "completed" && match.status !== "failed") {
|
|
@@ -464,7 +2811,7 @@ var taskTool = {
|
|
|
464
2811
|
});
|
|
465
2812
|
if (match.description) {
|
|
466
2813
|
todos.push({
|
|
467
|
-
id: `todo_${ts}_${
|
|
2814
|
+
id: `todo_${ts}_${randomUUID3().slice(0, 6)}`,
|
|
468
2815
|
content: match.description.slice(0, 200),
|
|
469
2816
|
status: "pending",
|
|
470
2817
|
promotedFromTask: match.id
|
|
@@ -473,7 +2820,7 @@ var taskTool = {
|
|
|
473
2820
|
if (input.subtasks && input.subtasks.length > 0) {
|
|
474
2821
|
for (const st of input.subtasks) {
|
|
475
2822
|
todos.push({
|
|
476
|
-
id: `todo_${ts}_${
|
|
2823
|
+
id: `todo_${ts}_${randomUUID3().slice(0, 6)}`,
|
|
477
2824
|
content: st,
|
|
478
2825
|
status: "pending",
|
|
479
2826
|
promotedFromTask: match.id
|
|
@@ -488,17 +2835,35 @@ var taskTool = {
|
|
|
488
2835
|
case "planify": {
|
|
489
2836
|
const target = input.target?.trim();
|
|
490
2837
|
if (!target) {
|
|
491
|
-
early = {
|
|
2838
|
+
early = {
|
|
2839
|
+
ok: false,
|
|
2840
|
+
message: "action=planify requires `target` (task id, index, or title substring).",
|
|
2841
|
+
count: 0,
|
|
2842
|
+
completed: 0,
|
|
2843
|
+
inProgress: 0
|
|
2844
|
+
};
|
|
492
2845
|
return f;
|
|
493
2846
|
}
|
|
494
2847
|
const idx = findTaskIndex(f.tasks, target);
|
|
495
2848
|
if (idx === -1) {
|
|
496
|
-
early = {
|
|
2849
|
+
early = {
|
|
2850
|
+
ok: false,
|
|
2851
|
+
message: `No task matched "${target}".`,
|
|
2852
|
+
count: 0,
|
|
2853
|
+
completed: 0,
|
|
2854
|
+
inProgress: 0
|
|
2855
|
+
};
|
|
497
2856
|
return f;
|
|
498
2857
|
}
|
|
499
2858
|
const match = f.tasks[idx];
|
|
500
2859
|
if (!match) {
|
|
501
|
-
early = {
|
|
2860
|
+
early = {
|
|
2861
|
+
ok: false,
|
|
2862
|
+
message: `No task matched "${target}".`,
|
|
2863
|
+
count: 0,
|
|
2864
|
+
completed: 0,
|
|
2865
|
+
inProgress: 0
|
|
2866
|
+
};
|
|
502
2867
|
return f;
|
|
503
2868
|
}
|
|
504
2869
|
planifyMeta.title = match.title;
|
|
@@ -507,7 +2872,13 @@ var taskTool = {
|
|
|
507
2872
|
break;
|
|
508
2873
|
}
|
|
509
2874
|
default:
|
|
510
|
-
early = {
|
|
2875
|
+
early = {
|
|
2876
|
+
ok: false,
|
|
2877
|
+
message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`,
|
|
2878
|
+
count: 0,
|
|
2879
|
+
completed: 0,
|
|
2880
|
+
inProgress: 0
|
|
2881
|
+
};
|
|
511
2882
|
return f;
|
|
512
2883
|
}
|
|
513
2884
|
return f;
|
|
@@ -521,7 +2892,11 @@ var taskTool = {
|
|
|
521
2892
|
inProgress: 0
|
|
522
2893
|
};
|
|
523
2894
|
}
|
|
524
|
-
if (todosToReplace)
|
|
2895
|
+
if (todosToReplace) {
|
|
2896
|
+
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
2897
|
+
signal: AbortSignal.timeout(3e4)
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
525
2900
|
mirrorSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
|
|
526
2901
|
if (early) return early;
|
|
527
2902
|
if (didPlanify) {
|