@wrongstack/tools 0.303.0 → 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 +715 -378
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/index.js +112 -89
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/project-server.js +112 -89
- package/dist/codebase-index/schema.d.ts +11 -0
- package/dist/codebase-index/worker.js +112 -89
- package/dist/index.d.ts +3 -3
- package/dist/index.js +777 -397
- package/dist/kanban-contract-actions.d.ts +7 -0
- package/dist/kanban-task-inputs.d.ts +15 -2
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +32 -11
- package/dist/kanban.js +366 -245
- package/dist/pack.js +714 -378
- package/dist/plan.js +601 -289
- package/dist/read.js +112 -89
- package/dist/session-kanban.d.ts +86 -1
- package/dist/session-kanban.js +216 -46
- package/dist/task.js +601 -289
- package/dist/todo.js +601 -289
- package/dist/tool-tier.js +714 -378
- package/package.json +3 -3
package/dist/kanban.js
CHANGED
|
@@ -3,7 +3,6 @@ import { randomUUID as randomUUID2 } from "node:crypto";
|
|
|
3
3
|
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
4
4
|
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
5
5
|
import {
|
|
6
|
-
addColumn,
|
|
7
6
|
addTask,
|
|
8
7
|
adoptManagedLifecycle,
|
|
9
8
|
assignTask,
|
|
@@ -18,7 +17,7 @@ import {
|
|
|
18
17
|
exportBoardToTaskGraph,
|
|
19
18
|
finalizeTaskCompletion,
|
|
20
19
|
getBoard as getBoard3,
|
|
21
|
-
getKanbanOrchestrationSnapshot,
|
|
20
|
+
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
22
21
|
getKanbanQueueHealth,
|
|
23
22
|
getTask,
|
|
24
23
|
getTaskChain,
|
|
@@ -32,16 +31,16 @@ import {
|
|
|
32
31
|
recoverStaleTaskAssignments,
|
|
33
32
|
releaseTaskClaim,
|
|
34
33
|
removeBoard as removeBoard2,
|
|
35
|
-
removeColumn,
|
|
36
34
|
removeTask,
|
|
37
35
|
repairManagedTaskProjection,
|
|
36
|
+
resolveAutoAccept,
|
|
38
37
|
searchKanban,
|
|
39
38
|
setTaskChain,
|
|
39
|
+
stripLifecycleIssues,
|
|
40
40
|
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
41
41
|
transferTaskToBoard,
|
|
42
42
|
transitionTask,
|
|
43
43
|
updateBoard as updateBoard2,
|
|
44
|
-
updateColumn,
|
|
45
44
|
updateTask as updateTask2,
|
|
46
45
|
updateTaskAssignment,
|
|
47
46
|
verifyTaskCompletion as verifyTaskCompletion2
|
|
@@ -91,6 +90,137 @@ function duplicateBoardOptions(input) {
|
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
|
|
93
|
+
// src/kanban-contract-actions.ts
|
|
94
|
+
import {
|
|
95
|
+
addContractEdge,
|
|
96
|
+
configureContractGraph,
|
|
97
|
+
evaluateTaskContractGraph,
|
|
98
|
+
getContractGraph,
|
|
99
|
+
removeContractEdge,
|
|
100
|
+
removeContractNode,
|
|
101
|
+
upsertContractNode
|
|
102
|
+
} from "@wrongstack/kanban";
|
|
103
|
+
|
|
104
|
+
// src/kanban-tool-results.ts
|
|
105
|
+
function atomicityNudge(task) {
|
|
106
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
107
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
108
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
109
|
+
}
|
|
110
|
+
function readEnvGateEnforcement() {
|
|
111
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
112
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
113
|
+
}
|
|
114
|
+
function fail(message) {
|
|
115
|
+
return { ok: false, message };
|
|
116
|
+
}
|
|
117
|
+
function okBoard(board, message = "Board loaded.") {
|
|
118
|
+
return { ok: true, message, board };
|
|
119
|
+
}
|
|
120
|
+
function okTask(board, task, message) {
|
|
121
|
+
return { ok: true, message, board, task };
|
|
122
|
+
}
|
|
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
|
+
|
|
94
224
|
// src/kanban-decomposition-actions.ts
|
|
95
225
|
import {
|
|
96
226
|
assessTaskAtomicity,
|
|
@@ -122,26 +252,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
|
|
|
122
252
|
}
|
|
123
253
|
}
|
|
124
254
|
|
|
125
|
-
// src/kanban-tool-results.ts
|
|
126
|
-
function atomicityNudge(task) {
|
|
127
|
-
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
128
|
-
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
129
|
-
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
130
|
-
}
|
|
131
|
-
function readEnvGateEnforcement() {
|
|
132
|
-
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
133
|
-
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
134
|
-
}
|
|
135
|
-
function fail(message) {
|
|
136
|
-
return { ok: false, message };
|
|
137
|
-
}
|
|
138
|
-
function okBoard(board, message = "Board loaded.") {
|
|
139
|
-
return { ok: true, message, board };
|
|
140
|
-
}
|
|
141
|
-
function okTask(board, task, message) {
|
|
142
|
-
return { ok: true, message, board, task };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
255
|
// src/kanban-decomposition-actions.ts
|
|
146
256
|
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
147
257
|
switch (input.action) {
|
|
@@ -226,20 +336,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
226
336
|
// src/kanban-detail-actions.ts
|
|
227
337
|
import {
|
|
228
338
|
addCheckToTask,
|
|
229
|
-
addContractEdge,
|
|
230
339
|
addDependency,
|
|
231
340
|
addGoalMetricToTask,
|
|
232
341
|
addLinkToTask,
|
|
233
342
|
addNoteToTask,
|
|
234
|
-
configureContractGraph,
|
|
235
|
-
evaluateTaskContractGraph,
|
|
236
|
-
getContractGraph,
|
|
237
343
|
getKanbanWorkbench,
|
|
238
|
-
|
|
239
|
-
removeContractNode,
|
|
344
|
+
removeCheckFromTask,
|
|
240
345
|
updateCheckOnTask,
|
|
241
|
-
updateGoalMetricOnTask
|
|
242
|
-
upsertContractNode
|
|
346
|
+
updateGoalMetricOnTask
|
|
243
347
|
} from "@wrongstack/kanban";
|
|
244
348
|
|
|
245
349
|
// src/kanban-split-task-handler.ts
|
|
@@ -305,131 +409,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
305
409
|
workbench
|
|
306
410
|
};
|
|
307
411
|
}
|
|
308
|
-
case "get_contract_graph": {
|
|
309
|
-
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
310
|
-
const result = await getContractGraph(projectRoot, input.boardId);
|
|
311
|
-
return result ? {
|
|
312
|
-
ok: true,
|
|
313
|
-
message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
|
|
314
|
-
board: result.board,
|
|
315
|
-
...result.graph ? { contractGraph: result.graph } : {}
|
|
316
|
-
} : fail("Board not found.");
|
|
317
|
-
}
|
|
318
|
-
case "configure_contract_graph": {
|
|
319
|
-
if (!input.boardId || !input.contractGraphEnforcement) {
|
|
320
|
-
return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
|
|
321
|
-
}
|
|
322
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
323
|
-
if (!current) return fail("Board not found.");
|
|
324
|
-
if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
|
|
325
|
-
return fail(
|
|
326
|
-
"Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
|
|
330
|
-
return fail("An autonomous agent may not loosen a strict contract graph.");
|
|
331
|
-
}
|
|
332
|
-
const board = await configureContractGraph(
|
|
333
|
-
projectRoot,
|
|
334
|
-
input.boardId,
|
|
335
|
-
input.contractGraphEnforcement
|
|
336
|
-
);
|
|
337
|
-
return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
|
|
338
|
-
}
|
|
339
|
-
case "upsert_contract_node": {
|
|
340
|
-
if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
|
|
341
|
-
return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
|
|
342
|
-
}
|
|
343
|
-
if (input.contractNodeState === "waived") {
|
|
344
|
-
return fail(
|
|
345
|
-
"The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
if (input.contractNodeId) {
|
|
349
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
350
|
-
const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
|
|
351
|
-
if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
|
|
352
|
-
return fail(
|
|
353
|
-
"The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
358
|
-
...input.contractNodeId ? { id: input.contractNodeId } : {},
|
|
359
|
-
taskId: input.taskId,
|
|
360
|
-
kind: input.contractNodeKind,
|
|
361
|
-
title: input.title,
|
|
362
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
363
|
-
...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
|
|
364
|
-
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
365
|
-
...input.checkId !== void 0 ? { checkId: input.checkId } : {},
|
|
366
|
-
...input.metricId !== void 0 ? { metricId: input.metricId } : {},
|
|
367
|
-
...input.baseline !== void 0 ? { baseline: input.baseline } : {},
|
|
368
|
-
...input.threshold !== void 0 ? { threshold: input.threshold } : {},
|
|
369
|
-
...input.author !== void 0 ? { createdBy: input.author } : {}
|
|
370
|
-
});
|
|
371
|
-
return result ? {
|
|
372
|
-
...okBoard(result.board, "Contract node saved."),
|
|
373
|
-
contractGraph: result.board.contractGraph
|
|
374
|
-
} : fail("Task not found.");
|
|
375
|
-
}
|
|
376
|
-
case "link_contract_nodes": {
|
|
377
|
-
if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
|
|
378
|
-
return fail(
|
|
379
|
-
"link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
383
|
-
from: input.fromNodeId,
|
|
384
|
-
to: input.toNodeId,
|
|
385
|
-
type: input.contractEdgeType,
|
|
386
|
-
...input.contractEdgeId ? { id: input.contractEdgeId } : {},
|
|
387
|
-
...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
|
|
388
|
-
...input.contractRationale ? { rationale: input.contractRationale } : {},
|
|
389
|
-
...input.author ? { createdBy: input.author } : {}
|
|
390
|
-
});
|
|
391
|
-
return result ? {
|
|
392
|
-
...okBoard(result.board, "Contract edge added."),
|
|
393
|
-
contractGraph: result.board.contractGraph
|
|
394
|
-
} : fail("Board not found.");
|
|
395
|
-
}
|
|
396
|
-
case "remove_contract_node": {
|
|
397
|
-
if (!input.boardId || !input.contractNodeId) {
|
|
398
|
-
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
399
|
-
}
|
|
400
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
401
|
-
const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
|
|
402
|
-
if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
|
|
403
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
|
|
404
|
-
}
|
|
405
|
-
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
406
|
-
return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
|
|
407
|
-
}
|
|
408
|
-
case "remove_contract_edge": {
|
|
409
|
-
if (!input.boardId || !input.contractEdgeId) {
|
|
410
|
-
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
411
|
-
}
|
|
412
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
413
|
-
const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
|
|
414
|
-
if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
|
|
415
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
|
|
416
|
-
}
|
|
417
|
-
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
418
|
-
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
419
|
-
}
|
|
420
|
-
case "evaluate_contract_graph": {
|
|
421
|
-
if (!input.boardId || !input.taskId) {
|
|
422
|
-
return fail("evaluate_contract_graph requires boardId and taskId.");
|
|
423
|
-
}
|
|
424
|
-
const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
|
|
425
|
-
return result ? {
|
|
426
|
-
ok: result.evaluation.allowed,
|
|
427
|
-
message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
|
|
428
|
-
board: result.board,
|
|
429
|
-
contractGraph: result.board.contractGraph,
|
|
430
|
-
contractEvaluation: result.evaluation
|
|
431
|
-
} : fail("Task not found.");
|
|
432
|
-
}
|
|
433
412
|
case "add_dependency": {
|
|
434
413
|
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
435
414
|
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
@@ -482,8 +461,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
482
461
|
}
|
|
483
462
|
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
484
463
|
description: input.checkDescription,
|
|
485
|
-
type: "manual",
|
|
486
|
-
status: input.checkStatus
|
|
464
|
+
type: input.checkType ?? "manual",
|
|
465
|
+
status: input.checkStatus,
|
|
466
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
487
467
|
});
|
|
488
468
|
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
489
469
|
}
|
|
@@ -498,11 +478,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
498
478
|
input.checkId,
|
|
499
479
|
{
|
|
500
480
|
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
501
|
-
...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 } : {}
|
|
502
486
|
}
|
|
503
487
|
);
|
|
504
488
|
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
505
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
|
+
}
|
|
506
502
|
case "add_note": {
|
|
507
503
|
if (!input.boardId || !input.taskId || !input.note)
|
|
508
504
|
return fail("add_note requires boardId, taskId, and note.");
|
|
@@ -577,14 +573,25 @@ function taskInput(input) {
|
|
|
577
573
|
...input.order !== void 0 ? { order: input.order } : {},
|
|
578
574
|
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
579
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 } : {},
|
|
580
581
|
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
581
582
|
...input.checkDescription !== void 0 ? {
|
|
582
583
|
successCriteria: [
|
|
583
584
|
{
|
|
584
585
|
id: randomUUID(),
|
|
585
586
|
description: input.checkDescription,
|
|
586
|
-
|
|
587
|
-
|
|
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 } : {}
|
|
588
595
|
}
|
|
589
596
|
]
|
|
590
597
|
} : {},
|
|
@@ -632,11 +639,11 @@ function taskInput(input) {
|
|
|
632
639
|
};
|
|
633
640
|
}
|
|
634
641
|
function mergedDependsOn(input) {
|
|
635
|
-
|
|
642
|
+
if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
|
|
643
|
+
return [
|
|
636
644
|
...input.dependsOn ?? [],
|
|
637
645
|
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
638
646
|
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
639
|
-
return ids.length > 0 ? ids : void 0;
|
|
640
647
|
}
|
|
641
648
|
function taskPatch(input) {
|
|
642
649
|
return {
|
|
@@ -650,7 +657,15 @@ function taskPatch(input) {
|
|
|
650
657
|
status: input.status,
|
|
651
658
|
labels: input.labels,
|
|
652
659
|
assignedAgent: input.agentId,
|
|
653
|
-
...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 } : {},
|
|
654
669
|
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
655
670
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
656
671
|
};
|
|
@@ -710,8 +725,8 @@ function assignmentForTaskCreate(input) {
|
|
|
710
725
|
}
|
|
711
726
|
|
|
712
727
|
// src/kanban-tool-schema.ts
|
|
713
|
-
var KANBAN_TOOL_DESCRIPTION = "
|
|
714
|
-
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.';
|
|
715
730
|
var KANBAN_INPUT_SCHEMA = {
|
|
716
731
|
type: "object",
|
|
717
732
|
properties: {
|
|
@@ -724,6 +739,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
724
739
|
"duplicate_board",
|
|
725
740
|
"update_board",
|
|
726
741
|
"adopt_managed_lifecycle",
|
|
742
|
+
"release_managed_lifecycle",
|
|
727
743
|
"delete_board",
|
|
728
744
|
"generate_board",
|
|
729
745
|
"export_markdown",
|
|
@@ -735,9 +751,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
735
751
|
"ready_tasks",
|
|
736
752
|
"snapshot",
|
|
737
753
|
"workbench",
|
|
738
|
-
"add_column",
|
|
739
|
-
"update_column",
|
|
740
|
-
"delete_column",
|
|
741
754
|
"add_task",
|
|
742
755
|
"split_task",
|
|
743
756
|
"merge_tasks",
|
|
@@ -752,13 +765,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
752
765
|
"delete_task",
|
|
753
766
|
"set_chain",
|
|
754
767
|
"get_chain",
|
|
755
|
-
"get_contract_graph",
|
|
756
|
-
"configure_contract_graph",
|
|
757
|
-
"upsert_contract_node",
|
|
758
|
-
"link_contract_nodes",
|
|
759
|
-
"remove_contract_node",
|
|
760
|
-
"remove_contract_edge",
|
|
761
|
-
"evaluate_contract_graph",
|
|
762
768
|
"claim_task",
|
|
763
769
|
"release_task",
|
|
764
770
|
"assign_task",
|
|
@@ -772,49 +778,27 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
772
778
|
"update_goal_metric",
|
|
773
779
|
"add_check",
|
|
774
780
|
"update_check",
|
|
781
|
+
"remove_check",
|
|
775
782
|
"add_note",
|
|
776
783
|
"add_link",
|
|
777
784
|
"verify_completion",
|
|
778
785
|
"split_atomic",
|
|
779
786
|
"assess_atomicity",
|
|
780
|
-
"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"
|
|
781
794
|
]
|
|
782
795
|
},
|
|
783
796
|
boardId: { type: "string" },
|
|
784
797
|
taskId: { type: "string" },
|
|
785
798
|
taskIds: { type: "array", items: { type: "string" } },
|
|
786
799
|
chainId: { type: "string" },
|
|
787
|
-
contractNodeId: { type: "string" },
|
|
788
|
-
contractNodeKind: {
|
|
789
|
-
type: "string",
|
|
790
|
-
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
|
|
791
|
-
},
|
|
792
|
-
contractNodeState: {
|
|
793
|
-
type: "string",
|
|
794
|
-
enum: ["unknown", "active", "satisfied", "violated", "resolved"]
|
|
795
|
-
},
|
|
796
|
-
contractEnforcement: {
|
|
797
|
-
type: "string",
|
|
798
|
-
enum: ["blocking", "advisory", "informational"]
|
|
799
|
-
},
|
|
800
|
-
contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
|
|
801
|
-
contractEdgeId: { type: "string" },
|
|
802
|
-
contractEdgeType: {
|
|
803
|
-
type: "string",
|
|
804
|
-
enum: [
|
|
805
|
-
"targets",
|
|
806
|
-
"affects",
|
|
807
|
-
"must_preserve",
|
|
808
|
-
"exposes",
|
|
809
|
-
"verified_by",
|
|
810
|
-
"conflicts_with",
|
|
811
|
-
"derived_from",
|
|
812
|
-
"relates_to"
|
|
813
|
-
]
|
|
814
|
-
},
|
|
815
800
|
fromNodeId: { type: "string" },
|
|
816
801
|
toNodeId: { type: "string" },
|
|
817
|
-
contractRationale: { type: "string" },
|
|
818
802
|
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
819
803
|
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
820
804
|
columnId: { type: "string" },
|
|
@@ -898,7 +882,20 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
898
882
|
costCeilingUsd: { type: "number" },
|
|
899
883
|
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
900
884
|
lastFailureKind: { type: "string" },
|
|
901
|
-
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
|
+
},
|
|
902
899
|
estimatedHours: { type: "number" },
|
|
903
900
|
actualHours: { type: "number" },
|
|
904
901
|
taskGraph: { type: "object" },
|
|
@@ -932,6 +929,74 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
932
929
|
checkId: { type: "string" },
|
|
933
930
|
checkDescription: { type: "string" },
|
|
934
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" },
|
|
935
1000
|
note: { type: "string" },
|
|
936
1001
|
author: { type: "string" },
|
|
937
1002
|
url: { type: "string" },
|
|
@@ -984,17 +1049,25 @@ import {
|
|
|
984
1049
|
mutateTasks
|
|
985
1050
|
} from "@wrongstack/core/storage";
|
|
986
1051
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
987
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
1052
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
988
1053
|
import {
|
|
989
1054
|
bridgeKanbanSupervisor,
|
|
1055
|
+
compactSessionMirrorBoard,
|
|
990
1056
|
createBoard,
|
|
1057
|
+
DEFAULT_COLUMNS,
|
|
991
1058
|
getBoard as getBoard2,
|
|
1059
|
+
getDependencyReadinessIssues,
|
|
1060
|
+
getKanbanOrchestrationSnapshot,
|
|
992
1061
|
listBoards,
|
|
1062
|
+
pruneSessionBoards,
|
|
993
1063
|
removeBoard,
|
|
994
1064
|
syncBoardFromTaskGraph,
|
|
995
1065
|
touchKanbanPresence as touchKanbanPresence2,
|
|
996
1066
|
updateBoard
|
|
997
1067
|
} from "@wrongstack/kanban";
|
|
1068
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
1069
|
+
...column
|
|
1070
|
+
}));
|
|
998
1071
|
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
999
1072
|
const graphId = `session:${sessionId}`;
|
|
1000
1073
|
const ids = new Set(tasks.map((task) => task.id));
|
|
@@ -1066,8 +1139,14 @@ var kanbanTool = {
|
|
|
1066
1139
|
}
|
|
1067
1140
|
case "create_board": {
|
|
1068
1141
|
if (!input.title) return fail("create_board requires title.");
|
|
1142
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
1143
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
1144
|
+
);
|
|
1069
1145
|
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
1070
|
-
|
|
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 };
|
|
1071
1150
|
}
|
|
1072
1151
|
case "update_board": {
|
|
1073
1152
|
if (!input.boardId) return fail("update_board requires boardId.");
|
|
@@ -1096,6 +1175,20 @@ var kanbanTool = {
|
|
|
1096
1175
|
});
|
|
1097
1176
|
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
1098
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
|
+
}
|
|
1099
1192
|
case "duplicate_board": {
|
|
1100
1193
|
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
1101
1194
|
const board = await duplicateBoard(
|
|
@@ -1115,8 +1208,7 @@ var kanbanTool = {
|
|
|
1115
1208
|
const boardInput = createBoardFromText({
|
|
1116
1209
|
description: input.description,
|
|
1117
1210
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
1118
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
1119
|
-
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
1211
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
1120
1212
|
});
|
|
1121
1213
|
const board = await createBoard2(projectRoot, boardInput);
|
|
1122
1214
|
for (const taskInput2 of parseLinesIntoTasks(
|
|
@@ -1254,7 +1346,7 @@ var kanbanTool = {
|
|
|
1254
1346
|
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1255
1347
|
}
|
|
1256
1348
|
case "snapshot": {
|
|
1257
|
-
const snapshot = await
|
|
1349
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
1258
1350
|
query: input.query,
|
|
1259
1351
|
boardId: input.boardId,
|
|
1260
1352
|
assignedAgent: input.agentId,
|
|
@@ -1269,33 +1361,6 @@ var kanbanTool = {
|
|
|
1269
1361
|
snapshot
|
|
1270
1362
|
};
|
|
1271
1363
|
}
|
|
1272
|
-
case "add_column": {
|
|
1273
|
-
if (!input.boardId || !input.title)
|
|
1274
|
-
return fail("add_column requires boardId and title.");
|
|
1275
|
-
const result2 = await addColumn(projectRoot, input.boardId, {
|
|
1276
|
-
title: input.title,
|
|
1277
|
-
...input.description !== void 0 ? { description: input.description } : {}
|
|
1278
|
-
});
|
|
1279
|
-
return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
|
|
1280
|
-
}
|
|
1281
|
-
case "update_column": {
|
|
1282
|
-
if (!input.boardId || !input.columnId)
|
|
1283
|
-
return fail("update_column requires boardId and columnId.");
|
|
1284
|
-
const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
|
|
1285
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1286
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1287
|
-
...input.order !== void 0 ? { order: input.order } : {}
|
|
1288
|
-
});
|
|
1289
|
-
return board ? okBoard(board, "Column updated.") : fail("Column not found.");
|
|
1290
|
-
}
|
|
1291
|
-
case "delete_column": {
|
|
1292
|
-
if (!input.boardId || !input.columnId)
|
|
1293
|
-
return fail("delete_column requires boardId and columnId.");
|
|
1294
|
-
const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
|
|
1295
|
-
moveTasksToColumnId: input.moveTasksToColumnId
|
|
1296
|
-
});
|
|
1297
|
-
return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
|
|
1298
|
-
}
|
|
1299
1364
|
case "add_task": {
|
|
1300
1365
|
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1301
1366
|
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
@@ -1377,6 +1442,32 @@ var kanbanTool = {
|
|
|
1377
1442
|
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1378
1443
|
);
|
|
1379
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
|
+
}
|
|
1380
1471
|
let stage = task.lifecycle?.currentStage;
|
|
1381
1472
|
if (stage === "backlog") {
|
|
1382
1473
|
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
@@ -1517,6 +1608,9 @@ var kanbanTool = {
|
|
|
1517
1608
|
if (!input.boardId || !input.taskId)
|
|
1518
1609
|
return fail("delete_task requires boardId and taskId.");
|
|
1519
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
|
+
}
|
|
1520
1614
|
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1521
1615
|
}
|
|
1522
1616
|
case "set_chain": {
|
|
@@ -1649,7 +1743,7 @@ var kanbanTool = {
|
|
|
1649
1743
|
});
|
|
1650
1744
|
} catch (err) {
|
|
1651
1745
|
lifecycleWarnings.push(
|
|
1652
|
-
`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))}`
|
|
1653
1747
|
);
|
|
1654
1748
|
}
|
|
1655
1749
|
}
|
|
@@ -1673,7 +1767,7 @@ var kanbanTool = {
|
|
|
1673
1767
|
});
|
|
1674
1768
|
} catch (err) {
|
|
1675
1769
|
lifecycleWarnings.push(
|
|
1676
|
-
`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))}`
|
|
1677
1771
|
);
|
|
1678
1772
|
}
|
|
1679
1773
|
if (transitionResult) {
|
|
@@ -1693,7 +1787,11 @@ var kanbanTool = {
|
|
|
1693
1787
|
successCriteria: verResult.task.successCriteria
|
|
1694
1788
|
});
|
|
1695
1789
|
const verdict = verResult.report.verdict;
|
|
1696
|
-
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") {
|
|
1697
1795
|
try {
|
|
1698
1796
|
const doneResult = await transitionTask(
|
|
1699
1797
|
projectRoot,
|
|
@@ -1811,11 +1909,34 @@ var kanbanTool = {
|
|
|
1811
1909
|
});
|
|
1812
1910
|
return {
|
|
1813
1911
|
ok: true,
|
|
1814
|
-
message: `Counts:
|
|
1912
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
1815
1913
|
queueHealth: health
|
|
1816
1914
|
};
|
|
1817
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
|
|
1818
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
|
+
}
|
|
1819
1940
|
{
|
|
1820
1941
|
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
1821
1942
|
if (detailResult !== void 0) return detailResult;
|
|
@@ -1825,7 +1946,7 @@ var kanbanTool = {
|
|
|
1825
1946
|
})();
|
|
1826
1947
|
return withPresence(result);
|
|
1827
1948
|
} catch (err) {
|
|
1828
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
1949
|
+
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
1829
1950
|
}
|
|
1830
1951
|
}
|
|
1831
1952
|
};
|