@wrongstack/tools 0.302.2 → 0.305.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin.d.ts +6 -0
- package/dist/builtin.js +3213 -686
- package/dist/codebase-index/binary-frame.d.ts +43 -0
- package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +1 -0
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +1 -0
- package/dist/codebase-index/content-hash.d.ts +66 -0
- package/dist/codebase-index/index.js +1597 -153
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/parser-worker-pool.d.ts +63 -0
- package/dist/codebase-index/parser-worker-script.d.ts +42 -0
- package/dist/codebase-index/project-server-protocol.d.ts +2 -0
- package/dist/codebase-index/project-server.js +1483 -104
- package/dist/codebase-index/schema.d.ts +18 -0
- package/dist/codebase-index/tree-sitter/queries.d.ts +48 -0
- package/dist/codebase-index/tree-sitter/util.d.ts +31 -0
- package/dist/codebase-index/tree-sitter/visitor.d.ts +47 -0
- package/dist/codebase-index/tree-sitter-parser.d.ts +58 -0
- package/dist/codebase-index/vector-search.d.ts +62 -0
- package/dist/codebase-index/worker-protocol.d.ts +2 -0
- package/dist/codebase-index/worker.js +1452 -73
- package/dist/codebase-index/writer-bulk-insert.d.ts +5 -0
- package/dist/codebase-index/writer-graph-reader.d.ts +39 -0
- package/dist/codebase-index/writer-schema.d.ts +9 -2
- package/dist/codebase-index/writer.d.ts +36 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3258 -727
- package/dist/kanban-contract-actions.d.ts +7 -0
- package/dist/kanban-task-inputs.d.ts +16 -2
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +39 -2
- package/dist/kanban.js +580 -193
- package/dist/pack.js +3212 -686
- package/dist/plan.d.ts +4 -1
- package/dist/plan.js +2701 -18
- package/dist/read.js +1559 -103
- package/dist/session-kanban.d.ts +94 -1
- package/dist/session-kanban.js +308 -44
- package/dist/task.d.ts +5 -4
- package/dist/task.js +2825 -138
- package/dist/todo.d.ts +10 -1
- package/dist/todo.js +2474 -30
- package/dist/tool-tier.js +3212 -686
- package/package.json +8 -4
package/dist/kanban.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/kanban.ts
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3
3
|
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
4
|
+
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
4
5
|
import {
|
|
5
|
-
addColumn,
|
|
6
6
|
addTask,
|
|
7
7
|
adoptManagedLifecycle,
|
|
8
8
|
assignTask,
|
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
copyTaskToBoard,
|
|
11
11
|
createBoard as createBoard2,
|
|
12
12
|
createBoardFromTaskGraph,
|
|
13
|
+
createBoardFromText,
|
|
13
14
|
duplicateBoard,
|
|
15
|
+
evaluateContractGraphReadiness,
|
|
14
16
|
exportBoardAsMarkdown,
|
|
15
17
|
exportBoardToTaskGraph,
|
|
16
|
-
|
|
18
|
+
finalizeTaskCompletion,
|
|
17
19
|
getBoard as getBoard3,
|
|
18
|
-
getKanbanOrchestrationSnapshot,
|
|
20
|
+
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
19
21
|
getKanbanQueueHealth,
|
|
20
22
|
getTask,
|
|
21
23
|
getTaskChain,
|
|
@@ -27,53 +29,76 @@ import {
|
|
|
27
29
|
moveTask,
|
|
28
30
|
parseLinesIntoTasks,
|
|
29
31
|
recoverStaleTaskAssignments,
|
|
30
|
-
repairManagedTaskProjection,
|
|
31
32
|
releaseTaskClaim,
|
|
32
33
|
removeBoard as removeBoard2,
|
|
33
|
-
removeColumn,
|
|
34
34
|
removeTask,
|
|
35
|
+
repairManagedTaskProjection,
|
|
36
|
+
resolveAutoAccept,
|
|
35
37
|
searchKanban,
|
|
36
38
|
setTaskChain,
|
|
39
|
+
stripLifecycleIssues,
|
|
37
40
|
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
38
|
-
transitionTask,
|
|
39
41
|
transferTaskToBoard,
|
|
42
|
+
transitionTask,
|
|
40
43
|
updateBoard as updateBoard2,
|
|
41
|
-
updateColumn,
|
|
42
44
|
updateTask as updateTask2,
|
|
43
45
|
updateTaskAssignment,
|
|
44
|
-
verifyTaskCompletion as verifyTaskCompletion2
|
|
45
|
-
finalizeTaskCompletion
|
|
46
|
+
verifyTaskCompletion as verifyTaskCompletion2
|
|
46
47
|
} from "@wrongstack/kanban";
|
|
47
48
|
|
|
48
|
-
// src/kanban-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return
|
|
49
|
+
// src/kanban-board-inputs.ts
|
|
50
|
+
function agentSettableGate(enforcement) {
|
|
51
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
52
|
+
return { completionGate: { enforcement } };
|
|
52
53
|
}
|
|
53
|
-
function
|
|
54
|
-
return
|
|
54
|
+
function boardCreateInput(input, title) {
|
|
55
|
+
return {
|
|
56
|
+
title,
|
|
57
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
58
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
59
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
60
|
+
...input.atomicityMode !== void 0 ? {
|
|
61
|
+
atomicity: {
|
|
62
|
+
mode: input.atomicityMode,
|
|
63
|
+
decomposition: input.atomicityDecomposition ?? "propose"
|
|
64
|
+
}
|
|
65
|
+
} : {},
|
|
66
|
+
...agentSettableGate(input.gateEnforcement)
|
|
67
|
+
};
|
|
55
68
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
+
function boardUpdatePatch(input) {
|
|
70
|
+
return {
|
|
71
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
72
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
73
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
74
|
+
...input.atomicityMode !== void 0 ? {
|
|
75
|
+
atomicity: {
|
|
76
|
+
mode: input.atomicityMode,
|
|
77
|
+
decomposition: input.atomicityDecomposition ?? "propose"
|
|
78
|
+
}
|
|
79
|
+
} : {},
|
|
80
|
+
...agentSettableGate(input.gateEnforcement)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function duplicateBoardOptions(input) {
|
|
84
|
+
return {
|
|
85
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
86
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
87
|
+
...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
|
|
88
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
89
|
+
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
|
|
90
|
+
};
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
// src/kanban-
|
|
93
|
+
// src/kanban-contract-actions.ts
|
|
72
94
|
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
addContractEdge,
|
|
96
|
+
configureContractGraph,
|
|
97
|
+
evaluateTaskContractGraph,
|
|
98
|
+
getContractGraph,
|
|
99
|
+
removeContractEdge,
|
|
100
|
+
removeContractNode,
|
|
101
|
+
upsertContractNode
|
|
77
102
|
} from "@wrongstack/kanban";
|
|
78
103
|
|
|
79
104
|
// src/kanban-tool-results.ts
|
|
@@ -96,6 +121,137 @@ function okTask(board, task, message) {
|
|
|
96
121
|
return { ok: true, message, board, task };
|
|
97
122
|
}
|
|
98
123
|
|
|
124
|
+
// src/kanban-contract-actions.ts
|
|
125
|
+
async function handleKanbanContractAction(projectRoot, input, actor) {
|
|
126
|
+
switch (input.action) {
|
|
127
|
+
case "get_contract_graph": {
|
|
128
|
+
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
129
|
+
const found = await getContractGraph(projectRoot, input.boardId);
|
|
130
|
+
if (!found) return fail("Board not found.");
|
|
131
|
+
const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
|
|
132
|
+
if (input.taskId && !evaluated) return fail("Task not found on this board.");
|
|
133
|
+
return {
|
|
134
|
+
ok: true,
|
|
135
|
+
message: found.graph ? `Contract map: ${found.graph.nodes.length} node(s), ${found.graph.edges.length} edge(s), enforcement ${found.graph.enforcement}.` : "No contract map on this board yet. Call configure_contract_graph to start one.",
|
|
136
|
+
board: found.board,
|
|
137
|
+
contractGraph: found.graph,
|
|
138
|
+
...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
case "configure_contract_graph": {
|
|
142
|
+
if (!input.boardId) return fail("configure_contract_graph requires boardId.");
|
|
143
|
+
const enforcement = input.contractEnforcement ?? "advisory";
|
|
144
|
+
const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
|
|
145
|
+
return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
|
|
146
|
+
}
|
|
147
|
+
case "upsert_contract_node": {
|
|
148
|
+
if (!input.boardId || !input.taskId) {
|
|
149
|
+
return fail("upsert_contract_node requires boardId and taskId.");
|
|
150
|
+
}
|
|
151
|
+
if (!input.contractNodeKind || !input.contractNodeTitle) {
|
|
152
|
+
return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
|
|
153
|
+
}
|
|
154
|
+
const waiver = input.contractNodeState === "waived" ? {
|
|
155
|
+
actor: actor ?? "agent",
|
|
156
|
+
reason: input.contractWaiverReason ?? "",
|
|
157
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
158
|
+
} : void 0;
|
|
159
|
+
if (waiver && !waiver.reason.trim()) {
|
|
160
|
+
return fail("A waived contract node requires contractWaiverReason.");
|
|
161
|
+
}
|
|
162
|
+
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
163
|
+
taskId: input.taskId,
|
|
164
|
+
kind: input.contractNodeKind,
|
|
165
|
+
title: input.contractNodeTitle,
|
|
166
|
+
...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
|
|
167
|
+
...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
|
|
168
|
+
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
169
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
170
|
+
...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
|
|
171
|
+
...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
|
|
172
|
+
...waiver ? { waiver } : {},
|
|
173
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
174
|
+
});
|
|
175
|
+
if (!result) return fail("Board or task not found.");
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
|
|
179
|
+
board: result.board,
|
|
180
|
+
contractGraph: result.board.contractGraph ?? null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
case "remove_contract_node": {
|
|
184
|
+
if (!input.boardId || !input.contractNodeId) {
|
|
185
|
+
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
186
|
+
}
|
|
187
|
+
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
188
|
+
return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
|
|
189
|
+
}
|
|
190
|
+
case "add_contract_edge": {
|
|
191
|
+
if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
|
|
192
|
+
return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
|
|
193
|
+
}
|
|
194
|
+
if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
|
|
195
|
+
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
196
|
+
from: input.contractEdgeFrom,
|
|
197
|
+
to: input.contractEdgeTo,
|
|
198
|
+
type: input.contractEdgeType,
|
|
199
|
+
...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
|
|
200
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
201
|
+
...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
|
|
202
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
203
|
+
});
|
|
204
|
+
if (!result) return fail("Board not found.");
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
|
|
208
|
+
board: result.board,
|
|
209
|
+
contractGraph: result.board.contractGraph ?? null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case "remove_contract_edge": {
|
|
213
|
+
if (!input.boardId || !input.contractEdgeId) {
|
|
214
|
+
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
215
|
+
}
|
|
216
|
+
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
217
|
+
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
218
|
+
}
|
|
219
|
+
default:
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/kanban-decomposition-actions.ts
|
|
225
|
+
import {
|
|
226
|
+
assessTaskAtomicity,
|
|
227
|
+
proposeTaskDecomposition,
|
|
228
|
+
updateTask,
|
|
229
|
+
verifyTaskCompletion
|
|
230
|
+
} from "@wrongstack/kanban";
|
|
231
|
+
|
|
232
|
+
// src/kanban-evidence-bridge.ts
|
|
233
|
+
import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
|
|
234
|
+
function kanbanEvidenceKey(boardId, taskId) {
|
|
235
|
+
return `kanban:${boardId}:${taskId}`;
|
|
236
|
+
}
|
|
237
|
+
function kanbanEvidencePointer(boardId, taskId) {
|
|
238
|
+
return `kanban://${boardId}/${taskId}#verificationReport`;
|
|
239
|
+
}
|
|
240
|
+
function recordKanbanVerificationEvidence(ctx, report) {
|
|
241
|
+
try {
|
|
242
|
+
const passed = report.checks.filter((check) => check.status === "passed").length;
|
|
243
|
+
const completedAt = Date.parse(report.completedAt);
|
|
244
|
+
recordCompletedWorkEvidence(ctx, {
|
|
245
|
+
key: kanbanEvidenceKey(report.boardId, report.taskId),
|
|
246
|
+
source: "verification",
|
|
247
|
+
summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
|
|
248
|
+
...Number.isFinite(completedAt) ? { completedAt } : {},
|
|
249
|
+
evidence: kanbanEvidencePointer(report.boardId, report.taskId)
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
99
255
|
// src/kanban-decomposition-actions.ts
|
|
100
256
|
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
101
257
|
switch (input.action) {
|
|
@@ -184,6 +340,8 @@ import {
|
|
|
184
340
|
addGoalMetricToTask,
|
|
185
341
|
addLinkToTask,
|
|
186
342
|
addNoteToTask,
|
|
343
|
+
getKanbanWorkbench,
|
|
344
|
+
removeCheckFromTask,
|
|
187
345
|
updateCheckOnTask,
|
|
188
346
|
updateGoalMetricOnTask
|
|
189
347
|
} from "@wrongstack/kanban";
|
|
@@ -241,6 +399,16 @@ async function requireBoard(projectRoot, boardId) {
|
|
|
241
399
|
// src/kanban-detail-actions.ts
|
|
242
400
|
async function handleKanbanDetailAction(projectRoot, input) {
|
|
243
401
|
switch (input.action) {
|
|
402
|
+
case "workbench": {
|
|
403
|
+
const workbench = await getKanbanWorkbench(projectRoot, {
|
|
404
|
+
...input.limit !== void 0 ? { limitPerLane: input.limit, alertLimit: input.limit } : {}
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
message: `${workbench.totals.now} now, ${workbench.totals.next} next, ${workbench.totals.blocked} blocked, ${workbench.totals.review} review; ${workbench.alertTotal} alert(s).`,
|
|
409
|
+
workbench
|
|
410
|
+
};
|
|
411
|
+
}
|
|
244
412
|
case "add_dependency": {
|
|
245
413
|
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
246
414
|
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
@@ -293,8 +461,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
293
461
|
}
|
|
294
462
|
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
295
463
|
description: input.checkDescription,
|
|
296
|
-
type: "manual",
|
|
297
|
-
status: input.checkStatus
|
|
464
|
+
type: input.checkType ?? "manual",
|
|
465
|
+
status: input.checkStatus,
|
|
466
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
298
467
|
});
|
|
299
468
|
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
300
469
|
}
|
|
@@ -309,11 +478,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
309
478
|
input.checkId,
|
|
310
479
|
{
|
|
311
480
|
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
312
|
-
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
|
|
481
|
+
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
|
|
482
|
+
// Promoting an existing manual criterion to an executable one is the
|
|
483
|
+
// common repair: the card was written before anyone knew the command.
|
|
484
|
+
...input.checkType !== void 0 ? { type: input.checkType } : {},
|
|
485
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
313
486
|
}
|
|
314
487
|
);
|
|
315
488
|
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
316
489
|
}
|
|
490
|
+
case "remove_check": {
|
|
491
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
492
|
+
return fail("remove_check requires boardId, taskId, and checkId.");
|
|
493
|
+
}
|
|
494
|
+
const board = await removeCheckFromTask(
|
|
495
|
+
projectRoot,
|
|
496
|
+
input.boardId,
|
|
497
|
+
input.taskId,
|
|
498
|
+
input.checkId
|
|
499
|
+
);
|
|
500
|
+
return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
|
|
501
|
+
}
|
|
317
502
|
case "add_note": {
|
|
318
503
|
if (!input.boardId || !input.taskId || !input.note)
|
|
319
504
|
return fail("add_note requires boardId, taskId, and note.");
|
|
@@ -344,47 +529,24 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
344
529
|
}
|
|
345
530
|
}
|
|
346
531
|
|
|
347
|
-
// src/kanban-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
function boardUpdatePatch(input) {
|
|
368
|
-
return {
|
|
369
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
370
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
371
|
-
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
372
|
-
...input.atomicityMode !== void 0 ? {
|
|
373
|
-
atomicity: {
|
|
374
|
-
mode: input.atomicityMode,
|
|
375
|
-
decomposition: input.atomicityDecomposition ?? "propose"
|
|
376
|
-
}
|
|
377
|
-
} : {},
|
|
378
|
-
...agentSettableGate(input.gateEnforcement)
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
function duplicateBoardOptions(input) {
|
|
382
|
-
return {
|
|
383
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
384
|
-
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
385
|
-
...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
|
|
386
|
-
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
|
|
387
|
-
...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
|
|
532
|
+
// src/kanban-presence.ts
|
|
533
|
+
import { touchKanbanPresence } from "@wrongstack/kanban";
|
|
534
|
+
function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
535
|
+
return async (result) => {
|
|
536
|
+
const boardId = result.board?.id ?? input.boardId;
|
|
537
|
+
if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
|
|
538
|
+
try {
|
|
539
|
+
const board = await touchKanbanPresence(projectRoot, boardId, {
|
|
540
|
+
sessionId: ctx.session.id,
|
|
541
|
+
agentId: ctx.agentId,
|
|
542
|
+
agentName: ctx.agentName,
|
|
543
|
+
taskId: input.taskId ?? result.task?.id,
|
|
544
|
+
runTaskId: input.runTaskId
|
|
545
|
+
});
|
|
546
|
+
return board ? { ...result, board } : result;
|
|
547
|
+
} catch {
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
388
550
|
};
|
|
389
551
|
}
|
|
390
552
|
|
|
@@ -411,14 +573,25 @@ function taskInput(input) {
|
|
|
411
573
|
...input.order !== void 0 ? { order: input.order } : {},
|
|
412
574
|
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
413
575
|
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
576
|
+
// The system prompt has always told the model it may "set atomic: true"
|
|
577
|
+
// when creating a composite parent. It could not: the field reached
|
|
578
|
+
// neither the create input nor the patch, so the instruction described a
|
|
579
|
+
// capability that did not exist and the attempt was silently dropped.
|
|
580
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
414
581
|
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
415
582
|
...input.checkDescription !== void 0 ? {
|
|
416
583
|
successCriteria: [
|
|
417
584
|
{
|
|
418
585
|
id: randomUUID(),
|
|
419
586
|
description: input.checkDescription,
|
|
420
|
-
|
|
421
|
-
|
|
587
|
+
// `manual` only as the fallback. Hard-coding it here meant every
|
|
588
|
+
// agent-authored criterion was unverifiable by construction: the
|
|
589
|
+
// deterministic plugins never matched, the registry passed the
|
|
590
|
+
// hand-set status straight through, and "verified" collapsed into
|
|
591
|
+
// "the author ticked its own box".
|
|
592
|
+
type: input.checkType ?? "manual",
|
|
593
|
+
status: input.checkStatus ?? "pending",
|
|
594
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
422
595
|
}
|
|
423
596
|
]
|
|
424
597
|
} : {},
|
|
@@ -454,22 +627,23 @@ function taskInput(input) {
|
|
|
454
627
|
}
|
|
455
628
|
]
|
|
456
629
|
} : {},
|
|
457
|
-
...input.graphId !== void 0 ? {
|
|
630
|
+
...[input.graphId, input.specId, input.specRequirementId].some((value) => value !== void 0) ? {
|
|
458
631
|
origin: {
|
|
459
632
|
system: input.sourceSystem ?? "kanban-tool",
|
|
460
633
|
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
461
634
|
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
635
|
+
...input.specRequirementId !== void 0 ? { specRequirementId: input.specRequirementId } : {},
|
|
462
636
|
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
|
|
463
637
|
}
|
|
464
638
|
} : {}
|
|
465
639
|
};
|
|
466
640
|
}
|
|
467
641
|
function mergedDependsOn(input) {
|
|
468
|
-
|
|
642
|
+
if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
|
|
643
|
+
return [
|
|
469
644
|
...input.dependsOn ?? [],
|
|
470
645
|
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
471
646
|
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
472
|
-
return ids.length > 0 ? ids : void 0;
|
|
473
647
|
}
|
|
474
648
|
function taskPatch(input) {
|
|
475
649
|
return {
|
|
@@ -483,7 +657,15 @@ function taskPatch(input) {
|
|
|
483
657
|
status: input.status,
|
|
484
658
|
labels: input.labels,
|
|
485
659
|
assignedAgent: input.agentId,
|
|
486
|
-
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
660
|
+
...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
|
|
661
|
+
// `atomic` and `childTaskIds` are the composite-parent contract, and the
|
|
662
|
+
// managed gate reads both: an `atomic` parent may not move forward without
|
|
663
|
+
// children, and may not reach Done until every child is completed. The
|
|
664
|
+
// manager has always accepted both on a patch; only this surface withheld
|
|
665
|
+
// them, so `split_atomic` was a one-way door — delete the children and the
|
|
666
|
+
// parent was stranded with no way to declare itself a leaf again.
|
|
667
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
668
|
+
...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
|
|
487
669
|
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
488
670
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
489
671
|
};
|
|
@@ -543,8 +725,8 @@ function assignmentForTaskCreate(input) {
|
|
|
543
725
|
}
|
|
544
726
|
|
|
545
727
|
// src/kanban-tool-schema.ts
|
|
546
|
-
var KANBAN_TOOL_DESCRIPTION = "
|
|
547
|
-
var KANBAN_TOOL_USAGE_HINT =
|
|
728
|
+
var KANBAN_TOOL_DESCRIPTION = "Durable project task boards: create and move cards, record checks, notes, links and assignments. The board is a record of the work, not a permit for it \u2014 nothing here gates other tools. Managed boards additionally enforce ordered Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions; release_managed_lifecycle turns that off.";
|
|
729
|
+
var KANBAN_TOOL_USAGE_HINT = 'Track substantial or multi-step work so it survives the session; a trivial edit or a question needs no card. Work stays on ONE board: call list_boards first and add_task to the board this project already uses. create_board is for a genuinely separate line of work, not for each new piece of it \u2014 a second board splits the same effort in two, and a board holding a single card is the usual sign. Common flow: list_boards or search_tasks to orient, add_task to record work, start_task when you begin, update_check with checkStatus "passed" to tick acceptance criteria (read their ids from get_task), then transition_task. On a managed board a refused transition names the field it wants \u2014 supply it and retry. When the acceptance criterion is something a machine can run, say so: set checkType ("command", "test", "file_exists", "file_matches", "git_diff", "metric") and put the command, pattern or path in checkNotes, then verify_completion executes it and the result is real evidence. Leave checkType off (or "manual") only for criteria that genuinely need a human eye \u2014 a manual check records your assertion, it does not test anything.';
|
|
548
730
|
var KANBAN_INPUT_SCHEMA = {
|
|
549
731
|
type: "object",
|
|
550
732
|
properties: {
|
|
@@ -557,6 +739,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
557
739
|
"duplicate_board",
|
|
558
740
|
"update_board",
|
|
559
741
|
"adopt_managed_lifecycle",
|
|
742
|
+
"release_managed_lifecycle",
|
|
560
743
|
"delete_board",
|
|
561
744
|
"generate_board",
|
|
562
745
|
"export_markdown",
|
|
@@ -567,15 +750,14 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
567
750
|
"search_tasks",
|
|
568
751
|
"ready_tasks",
|
|
569
752
|
"snapshot",
|
|
570
|
-
"
|
|
571
|
-
"update_column",
|
|
572
|
-
"delete_column",
|
|
753
|
+
"workbench",
|
|
573
754
|
"add_task",
|
|
574
755
|
"split_task",
|
|
575
756
|
"merge_tasks",
|
|
576
757
|
"copy_task",
|
|
577
758
|
"transfer_task",
|
|
578
759
|
"get_task",
|
|
760
|
+
"start_task",
|
|
579
761
|
"update_task",
|
|
580
762
|
"transition_task",
|
|
581
763
|
"repair_managed_projection",
|
|
@@ -596,18 +778,29 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
596
778
|
"update_goal_metric",
|
|
597
779
|
"add_check",
|
|
598
780
|
"update_check",
|
|
781
|
+
"remove_check",
|
|
599
782
|
"add_note",
|
|
600
783
|
"add_link",
|
|
601
784
|
"verify_completion",
|
|
602
785
|
"split_atomic",
|
|
603
786
|
"assess_atomicity",
|
|
604
|
-
"propose_decomposition"
|
|
787
|
+
"propose_decomposition",
|
|
788
|
+
"get_contract_graph",
|
|
789
|
+
"configure_contract_graph",
|
|
790
|
+
"upsert_contract_node",
|
|
791
|
+
"remove_contract_node",
|
|
792
|
+
"add_contract_edge",
|
|
793
|
+
"remove_contract_edge"
|
|
605
794
|
]
|
|
606
795
|
},
|
|
607
796
|
boardId: { type: "string" },
|
|
608
797
|
taskId: { type: "string" },
|
|
609
798
|
taskIds: { type: "array", items: { type: "string" } },
|
|
610
799
|
chainId: { type: "string" },
|
|
800
|
+
fromNodeId: { type: "string" },
|
|
801
|
+
toNodeId: { type: "string" },
|
|
802
|
+
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
803
|
+
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
611
804
|
columnId: { type: "string" },
|
|
612
805
|
targetBoardId: { type: "string" },
|
|
613
806
|
targetColumnId: { type: "string" },
|
|
@@ -689,12 +882,26 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
689
882
|
costCeilingUsd: { type: "number" },
|
|
690
883
|
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
691
884
|
lastFailureKind: { type: "string" },
|
|
692
|
-
dependsOn: {
|
|
885
|
+
dependsOn: {
|
|
886
|
+
type: "array",
|
|
887
|
+
items: { type: "string" },
|
|
888
|
+
description: "Task ids this card waits on. On update_task an explicit empty array clears them \u2014 use it when a dependency was recorded in error rather than completing work nobody wants."
|
|
889
|
+
},
|
|
890
|
+
atomic: {
|
|
891
|
+
type: "boolean",
|
|
892
|
+
description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
|
|
893
|
+
},
|
|
894
|
+
childTaskIds: {
|
|
895
|
+
type: "array",
|
|
896
|
+
items: { type: "string" },
|
|
897
|
+
description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
|
|
898
|
+
},
|
|
693
899
|
estimatedHours: { type: "number" },
|
|
694
900
|
actualHours: { type: "number" },
|
|
695
901
|
taskGraph: { type: "object" },
|
|
696
902
|
graphId: { type: "string" },
|
|
697
903
|
specId: { type: "string" },
|
|
904
|
+
specRequirementId: { type: "string" },
|
|
698
905
|
sourceSystem: { type: "string" },
|
|
699
906
|
phaseId: { type: "string" },
|
|
700
907
|
preserveOriginTaskIds: { type: "boolean" },
|
|
@@ -722,6 +929,74 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
722
929
|
checkId: { type: "string" },
|
|
723
930
|
checkDescription: { type: "string" },
|
|
724
931
|
checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
|
|
932
|
+
checkType: {
|
|
933
|
+
type: "string",
|
|
934
|
+
// Only types a verifier can actually execute. `manual` is the default and
|
|
935
|
+
// means a human or agent asserts the status by hand. The rest are run by
|
|
936
|
+
// `verify_completion` against the default deterministic registry. Types
|
|
937
|
+
// with no plugin in that registry (`auto`, `review`, `agent`, `council`)
|
|
938
|
+
// are deliberately omitted: offering them would produce criteria that
|
|
939
|
+
// silently report `skipped — no verifier plugin registered`.
|
|
940
|
+
enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
|
|
941
|
+
description: 'How this acceptance criterion is verified. Default "manual" (status set by hand). Any other value makes verify_completion execute it, so the criterion becomes real evidence rather than a self-assertion. Pair with checkNotes.'
|
|
942
|
+
},
|
|
943
|
+
checkNotes: {
|
|
944
|
+
type: "string",
|
|
945
|
+
description: 'The executable body for a non-manual checkType, read in preference to checkDescription. command/test: the shell command or test pattern. file_exists: the path. file_matches: JSON {"file","pattern","flags"}. git_diff: JSON {"expectedFiles","minChanges","maxChanges"}.'
|
|
946
|
+
},
|
|
947
|
+
// ── Contract map ───────────────────────────────────────────────────
|
|
948
|
+
// The card contract: what this work targets, what it must not break, what
|
|
949
|
+
// it risks, and what verifies it. Advisory by default — the readiness gate
|
|
950
|
+
// deliberately does not require map structure, so a map is an operator
|
|
951
|
+
// review aid, not work the model must complete before implementing.
|
|
952
|
+
contractEnforcement: {
|
|
953
|
+
type: "string",
|
|
954
|
+
enum: ["off", "advisory", "strict"],
|
|
955
|
+
description: "Board-level contract map enforcement. Default when first configured: advisory."
|
|
956
|
+
},
|
|
957
|
+
contractNodeId: { type: "string" },
|
|
958
|
+
contractNodeKind: {
|
|
959
|
+
type: "string",
|
|
960
|
+
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
|
|
961
|
+
description: "objective = what this card is for; guardrail = what must keep working; risk = what could go wrong; component/artifact = what it touches; verification = what settles it."
|
|
962
|
+
},
|
|
963
|
+
contractNodeTitle: { type: "string" },
|
|
964
|
+
contractNodeDescription: { type: "string" },
|
|
965
|
+
contractNodeState: {
|
|
966
|
+
type: "string",
|
|
967
|
+
enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
|
|
968
|
+
},
|
|
969
|
+
contractNodeEnforcement: {
|
|
970
|
+
type: "string",
|
|
971
|
+
enum: ["blocking", "advisory", "informational"]
|
|
972
|
+
},
|
|
973
|
+
/** Bind a node to an acceptance criterion or goal metric already on the task. */
|
|
974
|
+
contractCheckId: { type: "string" },
|
|
975
|
+
contractMetricId: { type: "string" },
|
|
976
|
+
contractWaiverReason: {
|
|
977
|
+
type: "string",
|
|
978
|
+
description: 'Required, with an actor, when contractNodeState is "waived".'
|
|
979
|
+
},
|
|
980
|
+
contractEdgeId: { type: "string" },
|
|
981
|
+
contractEdgeFrom: {
|
|
982
|
+
type: "string",
|
|
983
|
+
description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
|
|
984
|
+
},
|
|
985
|
+
contractEdgeTo: { type: "string" },
|
|
986
|
+
contractEdgeType: {
|
|
987
|
+
type: "string",
|
|
988
|
+
enum: [
|
|
989
|
+
"targets",
|
|
990
|
+
"affects",
|
|
991
|
+
"must_preserve",
|
|
992
|
+
"exposes",
|
|
993
|
+
"verified_by",
|
|
994
|
+
"conflicts_with",
|
|
995
|
+
"derived_from",
|
|
996
|
+
"relates_to"
|
|
997
|
+
]
|
|
998
|
+
},
|
|
999
|
+
contractEdgeRationale: { type: "string" },
|
|
725
1000
|
note: { type: "string" },
|
|
726
1001
|
author: { type: "string" },
|
|
727
1002
|
url: { type: "string" },
|
|
@@ -765,27 +1040,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
765
1040
|
required: ["action"]
|
|
766
1041
|
};
|
|
767
1042
|
|
|
768
|
-
// src/kanban-presence.ts
|
|
769
|
-
import { touchKanbanPresence } from "@wrongstack/kanban";
|
|
770
|
-
function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
771
|
-
return async (result) => {
|
|
772
|
-
const boardId = result.board?.id ?? input.boardId;
|
|
773
|
-
if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
|
|
774
|
-
try {
|
|
775
|
-
const board = await touchKanbanPresence(projectRoot, boardId, {
|
|
776
|
-
sessionId: ctx.session.id,
|
|
777
|
-
agentId: ctx.agentId,
|
|
778
|
-
agentName: ctx.agentName,
|
|
779
|
-
taskId: input.taskId ?? result.task?.id,
|
|
780
|
-
runTaskId: input.runTaskId
|
|
781
|
-
});
|
|
782
|
-
return board ? { ...result, board } : result;
|
|
783
|
-
} catch {
|
|
784
|
-
return result;
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
|
|
789
1043
|
// src/session-kanban.ts
|
|
790
1044
|
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
791
1045
|
import {
|
|
@@ -795,18 +1049,27 @@ import {
|
|
|
795
1049
|
mutateTasks
|
|
796
1050
|
} from "@wrongstack/core/storage";
|
|
797
1051
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
798
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
1052
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
799
1053
|
import {
|
|
800
1054
|
bridgeKanbanSupervisor,
|
|
1055
|
+
compactSessionMirrorBoard,
|
|
801
1056
|
createBoard,
|
|
1057
|
+
DEFAULT_COLUMNS,
|
|
802
1058
|
getBoard as getBoard2,
|
|
1059
|
+
getDependencyReadinessIssues,
|
|
1060
|
+
getKanbanOrchestrationSnapshot,
|
|
803
1061
|
listBoards,
|
|
1062
|
+
pruneSessionBoards,
|
|
804
1063
|
removeBoard,
|
|
805
1064
|
syncBoardFromTaskGraph,
|
|
806
1065
|
touchKanbanPresence as touchKanbanPresence2,
|
|
807
1066
|
updateBoard
|
|
808
1067
|
} from "@wrongstack/kanban";
|
|
1068
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
1069
|
+
...column
|
|
1070
|
+
}));
|
|
809
1071
|
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
1072
|
+
const graphId = `session:${sessionId}`;
|
|
810
1073
|
const ids = new Set(tasks.map((task) => task.id));
|
|
811
1074
|
const nodes = tasks.map((task, index) => ({
|
|
812
1075
|
id: task.id,
|
|
@@ -815,6 +1078,7 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
815
1078
|
type: task.type,
|
|
816
1079
|
priority: task.priority,
|
|
817
1080
|
status: task.status,
|
|
1081
|
+
specRequirementId: `${graphId}:${task.id}`,
|
|
818
1082
|
...task.assignee ? { assignee: task.assignee } : {},
|
|
819
1083
|
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
820
1084
|
createdAt: index,
|
|
@@ -832,8 +1096,9 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
832
1096
|
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
833
1097
|
return {
|
|
834
1098
|
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
835
|
-
id:
|
|
836
|
-
specId:
|
|
1099
|
+
id: graphId,
|
|
1100
|
+
specId: graphId,
|
|
1101
|
+
requiredRequirementIds: nodes.map((node) => node.specRequirementId),
|
|
837
1102
|
title: "Session tasks",
|
|
838
1103
|
nodes,
|
|
839
1104
|
edges,
|
|
@@ -874,8 +1139,14 @@ var kanbanTool = {
|
|
|
874
1139
|
}
|
|
875
1140
|
case "create_board": {
|
|
876
1141
|
if (!input.title) return fail("create_board requires title.");
|
|
1142
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
1143
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
1144
|
+
);
|
|
877
1145
|
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
878
|
-
|
|
1146
|
+
const note = existing.length ? ` ${existing.length} other project board(s) already exist: ${existing.slice(0, 3).map((candidate) => `"${candidate.title}" (${candidate.taskCount} task(s))`).join(
|
|
1147
|
+
", "
|
|
1148
|
+
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
1149
|
+
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
879
1150
|
}
|
|
880
1151
|
case "update_board": {
|
|
881
1152
|
if (!input.boardId) return fail("update_board requires boardId.");
|
|
@@ -904,9 +1175,27 @@ var kanbanTool = {
|
|
|
904
1175
|
});
|
|
905
1176
|
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
906
1177
|
}
|
|
1178
|
+
// Adoption used to be a one-way door: the strict lifecycle carries
|
|
1179
|
+
// acceptance-criteria, verification-report, review-evidence and
|
|
1180
|
+
// one-stage-at-a-time gates, and nothing on the tool surface could
|
|
1181
|
+
// undo it, so a board adopted once kept its ceremony forever. The
|
|
1182
|
+
// gates are worth having where a fleet is supervised; they are not
|
|
1183
|
+
// worth being unable to leave. Cards and columns are untouched.
|
|
1184
|
+
case "release_managed_lifecycle": {
|
|
1185
|
+
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
1186
|
+
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
1187
|
+
return board ? okBoard(
|
|
1188
|
+
board,
|
|
1189
|
+
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
1190
|
+
) : fail("Board not found.");
|
|
1191
|
+
}
|
|
907
1192
|
case "duplicate_board": {
|
|
908
1193
|
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
909
|
-
const board = await duplicateBoard(
|
|
1194
|
+
const board = await duplicateBoard(
|
|
1195
|
+
projectRoot,
|
|
1196
|
+
input.boardId,
|
|
1197
|
+
duplicateBoardOptions(input)
|
|
1198
|
+
);
|
|
910
1199
|
return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
|
|
911
1200
|
}
|
|
912
1201
|
case "delete_board": {
|
|
@@ -919,8 +1208,7 @@ var kanbanTool = {
|
|
|
919
1208
|
const boardInput = createBoardFromText({
|
|
920
1209
|
description: input.description,
|
|
921
1210
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
922
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
923
|
-
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
1211
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
924
1212
|
});
|
|
925
1213
|
const board = await createBoard2(projectRoot, boardInput);
|
|
926
1214
|
for (const taskInput2 of parseLinesIntoTasks(
|
|
@@ -1058,7 +1346,7 @@ var kanbanTool = {
|
|
|
1058
1346
|
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1059
1347
|
}
|
|
1060
1348
|
case "snapshot": {
|
|
1061
|
-
const snapshot = await
|
|
1349
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
1062
1350
|
query: input.query,
|
|
1063
1351
|
boardId: input.boardId,
|
|
1064
1352
|
assignedAgent: input.agentId,
|
|
@@ -1073,41 +1361,11 @@ var kanbanTool = {
|
|
|
1073
1361
|
snapshot
|
|
1074
1362
|
};
|
|
1075
1363
|
}
|
|
1076
|
-
case "add_column": {
|
|
1077
|
-
if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
|
|
1078
|
-
const result2 = await addColumn(projectRoot, input.boardId, {
|
|
1079
|
-
title: input.title,
|
|
1080
|
-
...input.description !== void 0 ? { description: input.description } : {}
|
|
1081
|
-
});
|
|
1082
|
-
return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
|
|
1083
|
-
}
|
|
1084
|
-
case "update_column": {
|
|
1085
|
-
if (!input.boardId || !input.columnId)
|
|
1086
|
-
return fail("update_column requires boardId and columnId.");
|
|
1087
|
-
const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
|
|
1088
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1089
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1090
|
-
...input.order !== void 0 ? { order: input.order } : {}
|
|
1091
|
-
});
|
|
1092
|
-
return board ? okBoard(board, "Column updated.") : fail("Column not found.");
|
|
1093
|
-
}
|
|
1094
|
-
case "delete_column": {
|
|
1095
|
-
if (!input.boardId || !input.columnId)
|
|
1096
|
-
return fail("delete_column requires boardId and columnId.");
|
|
1097
|
-
const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
|
|
1098
|
-
moveTasksToColumnId: input.moveTasksToColumnId
|
|
1099
|
-
});
|
|
1100
|
-
return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
|
|
1101
|
-
}
|
|
1102
1364
|
case "add_task": {
|
|
1103
1365
|
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1104
1366
|
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
1105
1367
|
if (!result2) return fail("Board not found.");
|
|
1106
|
-
return okTask(
|
|
1107
|
-
result2.board,
|
|
1108
|
-
result2.task,
|
|
1109
|
-
`Task added.${atomicityNudge(result2.task)}`
|
|
1110
|
-
);
|
|
1368
|
+
return okTask(result2.board, result2.task, `Task added.${atomicityNudge(result2.task)}`);
|
|
1111
1369
|
}
|
|
1112
1370
|
case "split_task": {
|
|
1113
1371
|
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
@@ -1166,10 +1424,98 @@ var kanbanTool = {
|
|
|
1166
1424
|
return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
|
|
1167
1425
|
}
|
|
1168
1426
|
case "get_task": {
|
|
1169
|
-
if (!input.boardId || !input.taskId)
|
|
1427
|
+
if (!input.boardId || !input.taskId)
|
|
1428
|
+
return fail("get_task requires boardId and taskId.");
|
|
1170
1429
|
const task = await getTask(projectRoot, input.boardId, input.taskId);
|
|
1171
1430
|
return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
|
|
1172
1431
|
}
|
|
1432
|
+
case "start_task": {
|
|
1433
|
+
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1434
|
+
return fail("start_task requires boardId, taskId, author, and transitionComment.");
|
|
1435
|
+
}
|
|
1436
|
+
let board = await getBoard3(projectRoot, input.boardId);
|
|
1437
|
+
let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1438
|
+
if (!board || !task) return fail("Board or task not found.");
|
|
1439
|
+
const readiness = evaluateContractGraphReadiness(board, task.id);
|
|
1440
|
+
if (!readiness.ready) {
|
|
1441
|
+
return fail(
|
|
1442
|
+
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
if (board.lifecycle?.mode !== "managed") {
|
|
1446
|
+
const now = /* @__PURE__ */ new Date();
|
|
1447
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1448
|
+
status: "running",
|
|
1449
|
+
agentId: input.agentId ?? input.author,
|
|
1450
|
+
leaseId: input.leaseId ?? randomUUID2(),
|
|
1451
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1452
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1453
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1454
|
+
attempt: input.attempt ?? 1,
|
|
1455
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1456
|
+
});
|
|
1457
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1458
|
+
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
1459
|
+
status: "in_progress"
|
|
1460
|
+
});
|
|
1461
|
+
const current = started ?? assigned;
|
|
1462
|
+
const claimed = task;
|
|
1463
|
+
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
1464
|
+
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
1465
|
+
return okTask(
|
|
1466
|
+
current,
|
|
1467
|
+
currentTask,
|
|
1468
|
+
"Task is active and bound to this run for attribution. This board is not in managed lifecycle mode, so runtime Kanban governance was not bound to it."
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
let stage = task.lifecycle?.currentStage;
|
|
1472
|
+
if (stage === "backlog") {
|
|
1473
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1474
|
+
to: "todo",
|
|
1475
|
+
actor: input.author,
|
|
1476
|
+
comment: input.transitionComment
|
|
1477
|
+
});
|
|
1478
|
+
if (!moved) return fail("Task could not enter Todo.");
|
|
1479
|
+
board = moved.board;
|
|
1480
|
+
task = moved.task;
|
|
1481
|
+
stage = task.lifecycle?.currentStage;
|
|
1482
|
+
}
|
|
1483
|
+
if (stage === "todo" || stage === "review") {
|
|
1484
|
+
const now = /* @__PURE__ */ new Date();
|
|
1485
|
+
const leaseId = input.leaseId ?? randomUUID2();
|
|
1486
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1487
|
+
status: "running",
|
|
1488
|
+
agentId: input.agentId ?? input.author,
|
|
1489
|
+
leaseId,
|
|
1490
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1491
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1492
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1493
|
+
attempt: input.attempt ?? 1,
|
|
1494
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1495
|
+
});
|
|
1496
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1497
|
+
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1498
|
+
to: "running",
|
|
1499
|
+
actor: input.author,
|
|
1500
|
+
comment: input.transitionComment
|
|
1501
|
+
});
|
|
1502
|
+
if (!moved) return fail("Task could not enter Running.");
|
|
1503
|
+
board = moved.board;
|
|
1504
|
+
task = moved.task;
|
|
1505
|
+
stage = task.lifecycle?.currentStage;
|
|
1506
|
+
}
|
|
1507
|
+
if (stage !== "running" || task.assignment?.status !== "running") {
|
|
1508
|
+
return fail(
|
|
1509
|
+
`start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
ctx.setCurrentKanbanTask(task.id, board.id);
|
|
1513
|
+
return okTask(
|
|
1514
|
+
board,
|
|
1515
|
+
task,
|
|
1516
|
+
"Task is active; runtime Kanban governance is now bound to this run."
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1173
1519
|
case "update_task": {
|
|
1174
1520
|
if (!input.boardId || !input.taskId)
|
|
1175
1521
|
return fail("update_task requires boardId and taskId.");
|
|
@@ -1191,9 +1537,14 @@ var kanbanTool = {
|
|
|
1191
1537
|
const boardBefore = await getBoard3(projectRoot, input.boardId);
|
|
1192
1538
|
const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
|
|
1193
1539
|
if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
|
|
1194
|
-
const preGate = await verifyTaskCompletion2(
|
|
1195
|
-
|
|
1196
|
-
|
|
1540
|
+
const preGate = await verifyTaskCompletion2(
|
|
1541
|
+
projectRoot,
|
|
1542
|
+
input.boardId,
|
|
1543
|
+
taskBefore.id,
|
|
1544
|
+
{
|
|
1545
|
+
persist: false
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1197
1548
|
await updateTask2(projectRoot, input.boardId, taskBefore.id, {
|
|
1198
1549
|
verificationReport: preGate.report,
|
|
1199
1550
|
successCriteria: preGate.task.successCriteria
|
|
@@ -1225,11 +1576,20 @@ var kanbanTool = {
|
|
|
1225
1576
|
"repair_managed_projection requires boardId, taskId, author, and transitionComment."
|
|
1226
1577
|
);
|
|
1227
1578
|
}
|
|
1228
|
-
const result2 = await repairManagedTaskProjection(
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1579
|
+
const result2 = await repairManagedTaskProjection(
|
|
1580
|
+
projectRoot,
|
|
1581
|
+
input.boardId,
|
|
1582
|
+
input.taskId,
|
|
1583
|
+
{
|
|
1584
|
+
actor: input.author,
|
|
1585
|
+
comment: input.transitionComment
|
|
1586
|
+
}
|
|
1587
|
+
);
|
|
1588
|
+
return result2 ? okTask(
|
|
1589
|
+
result2.board,
|
|
1590
|
+
result2.task,
|
|
1591
|
+
"Managed card projection repaired from lifecycle history."
|
|
1592
|
+
) : fail("Board or task not found.");
|
|
1233
1593
|
}
|
|
1234
1594
|
case "move_task": {
|
|
1235
1595
|
if (!input.boardId || !input.taskId || !input.targetColumnId) {
|
|
@@ -1248,6 +1608,9 @@ var kanbanTool = {
|
|
|
1248
1608
|
if (!input.boardId || !input.taskId)
|
|
1249
1609
|
return fail("delete_task requires boardId and taskId.");
|
|
1250
1610
|
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
1611
|
+
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
1612
|
+
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
1613
|
+
}
|
|
1251
1614
|
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1252
1615
|
}
|
|
1253
1616
|
case "set_chain": {
|
|
@@ -1366,53 +1729,45 @@ var kanbanTool = {
|
|
|
1366
1729
|
};
|
|
1367
1730
|
}
|
|
1368
1731
|
} else if (board.lifecycle?.mode === "managed") {
|
|
1369
|
-
const managedTask = board.tasks.find(
|
|
1370
|
-
(candidate) => candidate.id === input.taskId
|
|
1371
|
-
);
|
|
1732
|
+
const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
|
|
1372
1733
|
const stage = managedTask?.lifecycle?.currentStage;
|
|
1373
1734
|
const actor = ctx.agentId ?? "kanban-agent";
|
|
1374
1735
|
let transitionResult = null;
|
|
1375
1736
|
const lifecycleWarnings = [];
|
|
1376
1737
|
if (assignmentStatus === "running" && stage === "todo") {
|
|
1377
1738
|
try {
|
|
1378
|
-
transitionResult = await transitionTask(
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
);
|
|
1739
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
1740
|
+
to: "running",
|
|
1741
|
+
actor,
|
|
1742
|
+
comment: "Work started."
|
|
1743
|
+
});
|
|
1384
1744
|
} catch (err) {
|
|
1385
1745
|
lifecycleWarnings.push(
|
|
1386
|
-
`Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
|
|
1746
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1387
1747
|
);
|
|
1388
1748
|
}
|
|
1389
1749
|
}
|
|
1390
1750
|
if (assignmentStatus === "completed" && stage === "running") {
|
|
1391
1751
|
const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
|
|
1392
1752
|
try {
|
|
1393
|
-
transitionResult = await transitionTask(
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
{
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
}
|
|
1406
|
-
patch: {
|
|
1407
|
-
// Only patch non-description fields so the
|
|
1408
|
-
// original card description is preserved.
|
|
1409
|
-
...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
|
|
1410
|
-
}
|
|
1753
|
+
transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
|
|
1754
|
+
to: "review",
|
|
1755
|
+
actor,
|
|
1756
|
+
comment,
|
|
1757
|
+
attachment: {
|
|
1758
|
+
url: `kanban://task/${input.taskId}/result`,
|
|
1759
|
+
title: "Worker completion result",
|
|
1760
|
+
type: "file"
|
|
1761
|
+
},
|
|
1762
|
+
patch: {
|
|
1763
|
+
// Only patch non-description fields so the
|
|
1764
|
+
// original card description is preserved.
|
|
1765
|
+
...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
|
|
1411
1766
|
}
|
|
1412
|
-
);
|
|
1767
|
+
});
|
|
1413
1768
|
} catch (err) {
|
|
1414
1769
|
lifecycleWarnings.push(
|
|
1415
|
-
`Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1770
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1416
1771
|
);
|
|
1417
1772
|
}
|
|
1418
1773
|
if (transitionResult) {
|
|
@@ -1427,8 +1782,16 @@ var kanbanTool = {
|
|
|
1427
1782
|
if (verResult.report) {
|
|
1428
1783
|
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
1429
1784
|
}
|
|
1785
|
+
await updateTask2(projectRoot, board.id, input.taskId, {
|
|
1786
|
+
verificationReport: verResult.report,
|
|
1787
|
+
successCriteria: verResult.task.successCriteria
|
|
1788
|
+
});
|
|
1430
1789
|
const verdict = verResult.report.verdict;
|
|
1431
|
-
if (verdict === "passed") {
|
|
1790
|
+
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
1791
|
+
lifecycleWarnings.push(
|
|
1792
|
+
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
1793
|
+
);
|
|
1794
|
+
} else if (verdict === "passed") {
|
|
1432
1795
|
try {
|
|
1433
1796
|
const doneResult = await transitionTask(
|
|
1434
1797
|
projectRoot,
|
|
@@ -1437,6 +1800,7 @@ var kanbanTool = {
|
|
|
1437
1800
|
{
|
|
1438
1801
|
to: "done",
|
|
1439
1802
|
actor,
|
|
1803
|
+
action: "Automated acceptance after verification",
|
|
1440
1804
|
comment: "Auto-accepted: verification passed.",
|
|
1441
1805
|
attachment: {
|
|
1442
1806
|
url: `kanban://task/${input.taskId}/verification`,
|
|
@@ -1545,11 +1909,34 @@ var kanbanTool = {
|
|
|
1545
1909
|
});
|
|
1546
1910
|
return {
|
|
1547
1911
|
ok: true,
|
|
1548
|
-
message: `Counts:
|
|
1912
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
1549
1913
|
queueHealth: health
|
|
1550
1914
|
};
|
|
1551
1915
|
}
|
|
1916
|
+
// Not every action is handled above. These are dispatched from here,
|
|
1917
|
+
// and the split has already cost real time: an agent that read this
|
|
1918
|
+
// file concluded `add_check` / `update_check` did not exist, wrote
|
|
1919
|
+
// that on a card, and spent a session trying to satisfy a gate it
|
|
1920
|
+
// already had the tool to clear. Keep this index in step with the
|
|
1921
|
+
// handlers.
|
|
1922
|
+
//
|
|
1923
|
+
// kanban-detail-actions.ts workbench · add_dependency ·
|
|
1924
|
+
// add_goal_metric · update_goal_metric · add_check ·
|
|
1925
|
+
// update_check · add_note · add_link · split_atomic
|
|
1926
|
+
// kanban-decomposition-actions.ts verify_completion ·
|
|
1927
|
+
// assess_atomicity · propose_decomposition
|
|
1928
|
+
// kanban-contract-actions.ts get_contract_graph ·
|
|
1929
|
+
// configure_contract_graph · upsert_contract_node ·
|
|
1930
|
+
// remove_contract_node · add_contract_edge · remove_contract_edge
|
|
1552
1931
|
default:
|
|
1932
|
+
{
|
|
1933
|
+
const contractResult = await handleKanbanContractAction(
|
|
1934
|
+
projectRoot,
|
|
1935
|
+
input,
|
|
1936
|
+
input.author ?? input.agentId
|
|
1937
|
+
);
|
|
1938
|
+
if (contractResult !== void 0) return contractResult;
|
|
1939
|
+
}
|
|
1553
1940
|
{
|
|
1554
1941
|
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
1555
1942
|
if (detailResult !== void 0) return detailResult;
|
|
@@ -1559,7 +1946,7 @@ var kanbanTool = {
|
|
|
1559
1946
|
})();
|
|
1560
1947
|
return withPresence(result);
|
|
1561
1948
|
} catch (err) {
|
|
1562
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
1949
|
+
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
1563
1950
|
}
|
|
1564
1951
|
}
|
|
1565
1952
|
};
|