@wrongstack/tools 0.303.0 → 0.305.1
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 +717 -381
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/index.d.ts +1 -0
- package/dist/codebase-index/index.js +114 -90
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
- package/dist/codebase-index/project-server.js +131 -109
- 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 +793 -402
- 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 +716 -381
- package/dist/plan.js +601 -289
- package/dist/read.js +112 -90
- package/dist/session-kanban.d.ts +111 -1
- package/dist/session-kanban.js +232 -46
- package/dist/task.js +601 -289
- package/dist/todo.js +601 -289
- package/dist/tool-tier.js +716 -381
- package/package.json +4 -3
package/dist/todo.js
CHANGED
|
@@ -6,14 +6,13 @@ import {
|
|
|
6
6
|
saveTasks,
|
|
7
7
|
setPlanItemStatus
|
|
8
8
|
} from "@wrongstack/core/storage";
|
|
9
|
-
import { getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
9
|
+
import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
10
10
|
|
|
11
11
|
// src/kanban.ts
|
|
12
12
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
13
13
|
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
14
14
|
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
15
15
|
import {
|
|
16
|
-
addColumn,
|
|
17
16
|
addTask,
|
|
18
17
|
adoptManagedLifecycle,
|
|
19
18
|
assignTask,
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
exportBoardToTaskGraph,
|
|
29
28
|
finalizeTaskCompletion,
|
|
30
29
|
getBoard as getBoard3,
|
|
31
|
-
getKanbanOrchestrationSnapshot,
|
|
30
|
+
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
32
31
|
getKanbanQueueHealth,
|
|
33
32
|
getTask,
|
|
34
33
|
getTaskChain,
|
|
@@ -42,16 +41,16 @@ import {
|
|
|
42
41
|
recoverStaleTaskAssignments,
|
|
43
42
|
releaseTaskClaim,
|
|
44
43
|
removeBoard as removeBoard2,
|
|
45
|
-
removeColumn,
|
|
46
44
|
removeTask,
|
|
47
45
|
repairManagedTaskProjection,
|
|
46
|
+
resolveAutoAccept,
|
|
48
47
|
searchKanban,
|
|
49
48
|
setTaskChain,
|
|
49
|
+
stripLifecycleIssues,
|
|
50
50
|
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
51
51
|
transferTaskToBoard,
|
|
52
52
|
transitionTask,
|
|
53
53
|
updateBoard as updateBoard2,
|
|
54
|
-
updateColumn,
|
|
55
54
|
updateTask as updateTask2,
|
|
56
55
|
updateTaskAssignment,
|
|
57
56
|
verifyTaskCompletion as verifyTaskCompletion2
|
|
@@ -101,6 +100,137 @@ function duplicateBoardOptions(input) {
|
|
|
101
100
|
};
|
|
102
101
|
}
|
|
103
102
|
|
|
103
|
+
// src/kanban-contract-actions.ts
|
|
104
|
+
import {
|
|
105
|
+
addContractEdge,
|
|
106
|
+
configureContractGraph,
|
|
107
|
+
evaluateTaskContractGraph,
|
|
108
|
+
getContractGraph,
|
|
109
|
+
removeContractEdge,
|
|
110
|
+
removeContractNode,
|
|
111
|
+
upsertContractNode
|
|
112
|
+
} from "@wrongstack/kanban";
|
|
113
|
+
|
|
114
|
+
// src/kanban-tool-results.ts
|
|
115
|
+
function atomicityNudge(task) {
|
|
116
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
117
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
118
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
119
|
+
}
|
|
120
|
+
function readEnvGateEnforcement() {
|
|
121
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
122
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
123
|
+
}
|
|
124
|
+
function fail(message) {
|
|
125
|
+
return { ok: false, message };
|
|
126
|
+
}
|
|
127
|
+
function okBoard(board, message = "Board loaded.") {
|
|
128
|
+
return { ok: true, message, board };
|
|
129
|
+
}
|
|
130
|
+
function okTask(board, task, message) {
|
|
131
|
+
return { ok: true, message, board, task };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/kanban-contract-actions.ts
|
|
135
|
+
async function handleKanbanContractAction(projectRoot, input, actor) {
|
|
136
|
+
switch (input.action) {
|
|
137
|
+
case "get_contract_graph": {
|
|
138
|
+
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
139
|
+
const found = await getContractGraph(projectRoot, input.boardId);
|
|
140
|
+
if (!found) return fail("Board not found.");
|
|
141
|
+
const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
|
|
142
|
+
if (input.taskId && !evaluated) return fail("Task not found on this board.");
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
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.",
|
|
146
|
+
board: found.board,
|
|
147
|
+
contractGraph: found.graph,
|
|
148
|
+
...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
case "configure_contract_graph": {
|
|
152
|
+
if (!input.boardId) return fail("configure_contract_graph requires boardId.");
|
|
153
|
+
const enforcement = input.contractEnforcement ?? "advisory";
|
|
154
|
+
const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
|
|
155
|
+
return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
|
|
156
|
+
}
|
|
157
|
+
case "upsert_contract_node": {
|
|
158
|
+
if (!input.boardId || !input.taskId) {
|
|
159
|
+
return fail("upsert_contract_node requires boardId and taskId.");
|
|
160
|
+
}
|
|
161
|
+
if (!input.contractNodeKind || !input.contractNodeTitle) {
|
|
162
|
+
return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
|
|
163
|
+
}
|
|
164
|
+
const waiver = input.contractNodeState === "waived" ? {
|
|
165
|
+
actor: actor ?? "agent",
|
|
166
|
+
reason: input.contractWaiverReason ?? "",
|
|
167
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
168
|
+
} : void 0;
|
|
169
|
+
if (waiver && !waiver.reason.trim()) {
|
|
170
|
+
return fail("A waived contract node requires contractWaiverReason.");
|
|
171
|
+
}
|
|
172
|
+
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
173
|
+
taskId: input.taskId,
|
|
174
|
+
kind: input.contractNodeKind,
|
|
175
|
+
title: input.contractNodeTitle,
|
|
176
|
+
...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
|
|
177
|
+
...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
|
|
178
|
+
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
179
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
180
|
+
...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
|
|
181
|
+
...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
|
|
182
|
+
...waiver ? { waiver } : {},
|
|
183
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
184
|
+
});
|
|
185
|
+
if (!result) return fail("Board or task not found.");
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
|
|
189
|
+
board: result.board,
|
|
190
|
+
contractGraph: result.board.contractGraph ?? null
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
case "remove_contract_node": {
|
|
194
|
+
if (!input.boardId || !input.contractNodeId) {
|
|
195
|
+
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
196
|
+
}
|
|
197
|
+
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
198
|
+
return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
|
|
199
|
+
}
|
|
200
|
+
case "add_contract_edge": {
|
|
201
|
+
if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
|
|
202
|
+
return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
|
|
203
|
+
}
|
|
204
|
+
if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
|
|
205
|
+
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
206
|
+
from: input.contractEdgeFrom,
|
|
207
|
+
to: input.contractEdgeTo,
|
|
208
|
+
type: input.contractEdgeType,
|
|
209
|
+
...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
|
|
210
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
211
|
+
...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
|
|
212
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
213
|
+
});
|
|
214
|
+
if (!result) return fail("Board not found.");
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
|
|
218
|
+
board: result.board,
|
|
219
|
+
contractGraph: result.board.contractGraph ?? null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
case "remove_contract_edge": {
|
|
223
|
+
if (!input.boardId || !input.contractEdgeId) {
|
|
224
|
+
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
225
|
+
}
|
|
226
|
+
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
227
|
+
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
228
|
+
}
|
|
229
|
+
default:
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
104
234
|
// src/kanban-decomposition-actions.ts
|
|
105
235
|
import {
|
|
106
236
|
assessTaskAtomicity,
|
|
@@ -132,26 +262,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
|
|
|
132
262
|
}
|
|
133
263
|
}
|
|
134
264
|
|
|
135
|
-
// src/kanban-tool-results.ts
|
|
136
|
-
function atomicityNudge(task) {
|
|
137
|
-
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
138
|
-
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
139
|
-
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
140
|
-
}
|
|
141
|
-
function readEnvGateEnforcement() {
|
|
142
|
-
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
143
|
-
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
144
|
-
}
|
|
145
|
-
function fail(message) {
|
|
146
|
-
return { ok: false, message };
|
|
147
|
-
}
|
|
148
|
-
function okBoard(board, message = "Board loaded.") {
|
|
149
|
-
return { ok: true, message, board };
|
|
150
|
-
}
|
|
151
|
-
function okTask(board, task, message) {
|
|
152
|
-
return { ok: true, message, board, task };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
265
|
// src/kanban-decomposition-actions.ts
|
|
156
266
|
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
157
267
|
switch (input.action) {
|
|
@@ -236,20 +346,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
236
346
|
// src/kanban-detail-actions.ts
|
|
237
347
|
import {
|
|
238
348
|
addCheckToTask,
|
|
239
|
-
addContractEdge,
|
|
240
349
|
addDependency,
|
|
241
350
|
addGoalMetricToTask,
|
|
242
351
|
addLinkToTask,
|
|
243
352
|
addNoteToTask,
|
|
244
|
-
configureContractGraph,
|
|
245
|
-
evaluateTaskContractGraph,
|
|
246
|
-
getContractGraph,
|
|
247
353
|
getKanbanWorkbench,
|
|
248
|
-
|
|
249
|
-
removeContractNode,
|
|
354
|
+
removeCheckFromTask,
|
|
250
355
|
updateCheckOnTask,
|
|
251
|
-
updateGoalMetricOnTask
|
|
252
|
-
upsertContractNode
|
|
356
|
+
updateGoalMetricOnTask
|
|
253
357
|
} from "@wrongstack/kanban";
|
|
254
358
|
|
|
255
359
|
// src/kanban-split-task-handler.ts
|
|
@@ -315,131 +419,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
315
419
|
workbench
|
|
316
420
|
};
|
|
317
421
|
}
|
|
318
|
-
case "get_contract_graph": {
|
|
319
|
-
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
320
|
-
const result = await getContractGraph(projectRoot, input.boardId);
|
|
321
|
-
return result ? {
|
|
322
|
-
ok: true,
|
|
323
|
-
message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
|
|
324
|
-
board: result.board,
|
|
325
|
-
...result.graph ? { contractGraph: result.graph } : {}
|
|
326
|
-
} : fail("Board not found.");
|
|
327
|
-
}
|
|
328
|
-
case "configure_contract_graph": {
|
|
329
|
-
if (!input.boardId || !input.contractGraphEnforcement) {
|
|
330
|
-
return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
|
|
331
|
-
}
|
|
332
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
333
|
-
if (!current) return fail("Board not found.");
|
|
334
|
-
if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
|
|
335
|
-
return fail(
|
|
336
|
-
"Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
|
|
340
|
-
return fail("An autonomous agent may not loosen a strict contract graph.");
|
|
341
|
-
}
|
|
342
|
-
const board = await configureContractGraph(
|
|
343
|
-
projectRoot,
|
|
344
|
-
input.boardId,
|
|
345
|
-
input.contractGraphEnforcement
|
|
346
|
-
);
|
|
347
|
-
return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
|
|
348
|
-
}
|
|
349
|
-
case "upsert_contract_node": {
|
|
350
|
-
if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
|
|
351
|
-
return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
|
|
352
|
-
}
|
|
353
|
-
if (input.contractNodeState === "waived") {
|
|
354
|
-
return fail(
|
|
355
|
-
"The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
if (input.contractNodeId) {
|
|
359
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
360
|
-
const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
|
|
361
|
-
if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
|
|
362
|
-
return fail(
|
|
363
|
-
"The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
368
|
-
...input.contractNodeId ? { id: input.contractNodeId } : {},
|
|
369
|
-
taskId: input.taskId,
|
|
370
|
-
kind: input.contractNodeKind,
|
|
371
|
-
title: input.title,
|
|
372
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
373
|
-
...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
|
|
374
|
-
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
375
|
-
...input.checkId !== void 0 ? { checkId: input.checkId } : {},
|
|
376
|
-
...input.metricId !== void 0 ? { metricId: input.metricId } : {},
|
|
377
|
-
...input.baseline !== void 0 ? { baseline: input.baseline } : {},
|
|
378
|
-
...input.threshold !== void 0 ? { threshold: input.threshold } : {},
|
|
379
|
-
...input.author !== void 0 ? { createdBy: input.author } : {}
|
|
380
|
-
});
|
|
381
|
-
return result ? {
|
|
382
|
-
...okBoard(result.board, "Contract node saved."),
|
|
383
|
-
contractGraph: result.board.contractGraph
|
|
384
|
-
} : fail("Task not found.");
|
|
385
|
-
}
|
|
386
|
-
case "link_contract_nodes": {
|
|
387
|
-
if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
|
|
388
|
-
return fail(
|
|
389
|
-
"link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
393
|
-
from: input.fromNodeId,
|
|
394
|
-
to: input.toNodeId,
|
|
395
|
-
type: input.contractEdgeType,
|
|
396
|
-
...input.contractEdgeId ? { id: input.contractEdgeId } : {},
|
|
397
|
-
...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
|
|
398
|
-
...input.contractRationale ? { rationale: input.contractRationale } : {},
|
|
399
|
-
...input.author ? { createdBy: input.author } : {}
|
|
400
|
-
});
|
|
401
|
-
return result ? {
|
|
402
|
-
...okBoard(result.board, "Contract edge added."),
|
|
403
|
-
contractGraph: result.board.contractGraph
|
|
404
|
-
} : fail("Board not found.");
|
|
405
|
-
}
|
|
406
|
-
case "remove_contract_node": {
|
|
407
|
-
if (!input.boardId || !input.contractNodeId) {
|
|
408
|
-
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
409
|
-
}
|
|
410
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
411
|
-
const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
|
|
412
|
-
if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
|
|
413
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
|
|
414
|
-
}
|
|
415
|
-
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
416
|
-
return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
|
|
417
|
-
}
|
|
418
|
-
case "remove_contract_edge": {
|
|
419
|
-
if (!input.boardId || !input.contractEdgeId) {
|
|
420
|
-
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
421
|
-
}
|
|
422
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
423
|
-
const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
|
|
424
|
-
if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
|
|
425
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
|
|
426
|
-
}
|
|
427
|
-
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
428
|
-
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
429
|
-
}
|
|
430
|
-
case "evaluate_contract_graph": {
|
|
431
|
-
if (!input.boardId || !input.taskId) {
|
|
432
|
-
return fail("evaluate_contract_graph requires boardId and taskId.");
|
|
433
|
-
}
|
|
434
|
-
const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
|
|
435
|
-
return result ? {
|
|
436
|
-
ok: result.evaluation.allowed,
|
|
437
|
-
message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
|
|
438
|
-
board: result.board,
|
|
439
|
-
contractGraph: result.board.contractGraph,
|
|
440
|
-
contractEvaluation: result.evaluation
|
|
441
|
-
} : fail("Task not found.");
|
|
442
|
-
}
|
|
443
422
|
case "add_dependency": {
|
|
444
423
|
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
445
424
|
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
@@ -492,8 +471,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
492
471
|
}
|
|
493
472
|
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
494
473
|
description: input.checkDescription,
|
|
495
|
-
type: "manual",
|
|
496
|
-
status: input.checkStatus
|
|
474
|
+
type: input.checkType ?? "manual",
|
|
475
|
+
status: input.checkStatus,
|
|
476
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
497
477
|
});
|
|
498
478
|
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
499
479
|
}
|
|
@@ -508,11 +488,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
508
488
|
input.checkId,
|
|
509
489
|
{
|
|
510
490
|
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
511
|
-
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
|
|
491
|
+
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
|
|
492
|
+
// Promoting an existing manual criterion to an executable one is the
|
|
493
|
+
// common repair: the card was written before anyone knew the command.
|
|
494
|
+
...input.checkType !== void 0 ? { type: input.checkType } : {},
|
|
495
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
512
496
|
}
|
|
513
497
|
);
|
|
514
498
|
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
515
499
|
}
|
|
500
|
+
case "remove_check": {
|
|
501
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
502
|
+
return fail("remove_check requires boardId, taskId, and checkId.");
|
|
503
|
+
}
|
|
504
|
+
const board = await removeCheckFromTask(
|
|
505
|
+
projectRoot,
|
|
506
|
+
input.boardId,
|
|
507
|
+
input.taskId,
|
|
508
|
+
input.checkId
|
|
509
|
+
);
|
|
510
|
+
return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
|
|
511
|
+
}
|
|
516
512
|
case "add_note": {
|
|
517
513
|
if (!input.boardId || !input.taskId || !input.note)
|
|
518
514
|
return fail("add_note requires boardId, taskId, and note.");
|
|
@@ -587,14 +583,25 @@ function taskInput(input) {
|
|
|
587
583
|
...input.order !== void 0 ? { order: input.order } : {},
|
|
588
584
|
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
589
585
|
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
586
|
+
// The system prompt has always told the model it may "set atomic: true"
|
|
587
|
+
// when creating a composite parent. It could not: the field reached
|
|
588
|
+
// neither the create input nor the patch, so the instruction described a
|
|
589
|
+
// capability that did not exist and the attempt was silently dropped.
|
|
590
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
590
591
|
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
591
592
|
...input.checkDescription !== void 0 ? {
|
|
592
593
|
successCriteria: [
|
|
593
594
|
{
|
|
594
595
|
id: randomUUID(),
|
|
595
596
|
description: input.checkDescription,
|
|
596
|
-
|
|
597
|
-
|
|
597
|
+
// `manual` only as the fallback. Hard-coding it here meant every
|
|
598
|
+
// agent-authored criterion was unverifiable by construction: the
|
|
599
|
+
// deterministic plugins never matched, the registry passed the
|
|
600
|
+
// hand-set status straight through, and "verified" collapsed into
|
|
601
|
+
// "the author ticked its own box".
|
|
602
|
+
type: input.checkType ?? "manual",
|
|
603
|
+
status: input.checkStatus ?? "pending",
|
|
604
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
598
605
|
}
|
|
599
606
|
]
|
|
600
607
|
} : {},
|
|
@@ -642,11 +649,11 @@ function taskInput(input) {
|
|
|
642
649
|
};
|
|
643
650
|
}
|
|
644
651
|
function mergedDependsOn(input) {
|
|
645
|
-
|
|
652
|
+
if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
|
|
653
|
+
return [
|
|
646
654
|
...input.dependsOn ?? [],
|
|
647
655
|
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
648
656
|
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
649
|
-
return ids.length > 0 ? ids : void 0;
|
|
650
657
|
}
|
|
651
658
|
function taskPatch(input) {
|
|
652
659
|
return {
|
|
@@ -660,7 +667,15 @@ function taskPatch(input) {
|
|
|
660
667
|
status: input.status,
|
|
661
668
|
labels: input.labels,
|
|
662
669
|
assignedAgent: input.agentId,
|
|
663
|
-
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
670
|
+
...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
|
|
671
|
+
// `atomic` and `childTaskIds` are the composite-parent contract, and the
|
|
672
|
+
// managed gate reads both: an `atomic` parent may not move forward without
|
|
673
|
+
// children, and may not reach Done until every child is completed. The
|
|
674
|
+
// manager has always accepted both on a patch; only this surface withheld
|
|
675
|
+
// them, so `split_atomic` was a one-way door — delete the children and the
|
|
676
|
+
// parent was stranded with no way to declare itself a leaf again.
|
|
677
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
678
|
+
...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
|
|
664
679
|
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
665
680
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
666
681
|
};
|
|
@@ -720,8 +735,8 @@ function assignmentForTaskCreate(input) {
|
|
|
720
735
|
}
|
|
721
736
|
|
|
722
737
|
// src/kanban-tool-schema.ts
|
|
723
|
-
var KANBAN_TOOL_DESCRIPTION = "
|
|
724
|
-
var KANBAN_TOOL_USAGE_HINT =
|
|
738
|
+
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.";
|
|
739
|
+
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.';
|
|
725
740
|
var KANBAN_INPUT_SCHEMA = {
|
|
726
741
|
type: "object",
|
|
727
742
|
properties: {
|
|
@@ -734,6 +749,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
734
749
|
"duplicate_board",
|
|
735
750
|
"update_board",
|
|
736
751
|
"adopt_managed_lifecycle",
|
|
752
|
+
"release_managed_lifecycle",
|
|
737
753
|
"delete_board",
|
|
738
754
|
"generate_board",
|
|
739
755
|
"export_markdown",
|
|
@@ -745,9 +761,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
745
761
|
"ready_tasks",
|
|
746
762
|
"snapshot",
|
|
747
763
|
"workbench",
|
|
748
|
-
"add_column",
|
|
749
|
-
"update_column",
|
|
750
|
-
"delete_column",
|
|
751
764
|
"add_task",
|
|
752
765
|
"split_task",
|
|
753
766
|
"merge_tasks",
|
|
@@ -762,13 +775,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
762
775
|
"delete_task",
|
|
763
776
|
"set_chain",
|
|
764
777
|
"get_chain",
|
|
765
|
-
"get_contract_graph",
|
|
766
|
-
"configure_contract_graph",
|
|
767
|
-
"upsert_contract_node",
|
|
768
|
-
"link_contract_nodes",
|
|
769
|
-
"remove_contract_node",
|
|
770
|
-
"remove_contract_edge",
|
|
771
|
-
"evaluate_contract_graph",
|
|
772
778
|
"claim_task",
|
|
773
779
|
"release_task",
|
|
774
780
|
"assign_task",
|
|
@@ -782,49 +788,27 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
782
788
|
"update_goal_metric",
|
|
783
789
|
"add_check",
|
|
784
790
|
"update_check",
|
|
791
|
+
"remove_check",
|
|
785
792
|
"add_note",
|
|
786
793
|
"add_link",
|
|
787
794
|
"verify_completion",
|
|
788
795
|
"split_atomic",
|
|
789
796
|
"assess_atomicity",
|
|
790
|
-
"propose_decomposition"
|
|
797
|
+
"propose_decomposition",
|
|
798
|
+
"get_contract_graph",
|
|
799
|
+
"configure_contract_graph",
|
|
800
|
+
"upsert_contract_node",
|
|
801
|
+
"remove_contract_node",
|
|
802
|
+
"add_contract_edge",
|
|
803
|
+
"remove_contract_edge"
|
|
791
804
|
]
|
|
792
805
|
},
|
|
793
806
|
boardId: { type: "string" },
|
|
794
807
|
taskId: { type: "string" },
|
|
795
808
|
taskIds: { type: "array", items: { type: "string" } },
|
|
796
809
|
chainId: { type: "string" },
|
|
797
|
-
contractNodeId: { type: "string" },
|
|
798
|
-
contractNodeKind: {
|
|
799
|
-
type: "string",
|
|
800
|
-
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
|
|
801
|
-
},
|
|
802
|
-
contractNodeState: {
|
|
803
|
-
type: "string",
|
|
804
|
-
enum: ["unknown", "active", "satisfied", "violated", "resolved"]
|
|
805
|
-
},
|
|
806
|
-
contractEnforcement: {
|
|
807
|
-
type: "string",
|
|
808
|
-
enum: ["blocking", "advisory", "informational"]
|
|
809
|
-
},
|
|
810
|
-
contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
|
|
811
|
-
contractEdgeId: { type: "string" },
|
|
812
|
-
contractEdgeType: {
|
|
813
|
-
type: "string",
|
|
814
|
-
enum: [
|
|
815
|
-
"targets",
|
|
816
|
-
"affects",
|
|
817
|
-
"must_preserve",
|
|
818
|
-
"exposes",
|
|
819
|
-
"verified_by",
|
|
820
|
-
"conflicts_with",
|
|
821
|
-
"derived_from",
|
|
822
|
-
"relates_to"
|
|
823
|
-
]
|
|
824
|
-
},
|
|
825
810
|
fromNodeId: { type: "string" },
|
|
826
811
|
toNodeId: { type: "string" },
|
|
827
|
-
contractRationale: { type: "string" },
|
|
828
812
|
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
829
813
|
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
830
814
|
columnId: { type: "string" },
|
|
@@ -908,7 +892,20 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
908
892
|
costCeilingUsd: { type: "number" },
|
|
909
893
|
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
910
894
|
lastFailureKind: { type: "string" },
|
|
911
|
-
dependsOn: {
|
|
895
|
+
dependsOn: {
|
|
896
|
+
type: "array",
|
|
897
|
+
items: { type: "string" },
|
|
898
|
+
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."
|
|
899
|
+
},
|
|
900
|
+
atomic: {
|
|
901
|
+
type: "boolean",
|
|
902
|
+
description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
|
|
903
|
+
},
|
|
904
|
+
childTaskIds: {
|
|
905
|
+
type: "array",
|
|
906
|
+
items: { type: "string" },
|
|
907
|
+
description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
|
|
908
|
+
},
|
|
912
909
|
estimatedHours: { type: "number" },
|
|
913
910
|
actualHours: { type: "number" },
|
|
914
911
|
taskGraph: { type: "object" },
|
|
@@ -942,6 +939,74 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
942
939
|
checkId: { type: "string" },
|
|
943
940
|
checkDescription: { type: "string" },
|
|
944
941
|
checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
|
|
942
|
+
checkType: {
|
|
943
|
+
type: "string",
|
|
944
|
+
// Only types a verifier can actually execute. `manual` is the default and
|
|
945
|
+
// means a human or agent asserts the status by hand. The rest are run by
|
|
946
|
+
// `verify_completion` against the default deterministic registry. Types
|
|
947
|
+
// with no plugin in that registry (`auto`, `review`, `agent`, `council`)
|
|
948
|
+
// are deliberately omitted: offering them would produce criteria that
|
|
949
|
+
// silently report `skipped — no verifier plugin registered`.
|
|
950
|
+
enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
|
|
951
|
+
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.'
|
|
952
|
+
},
|
|
953
|
+
checkNotes: {
|
|
954
|
+
type: "string",
|
|
955
|
+
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"}.'
|
|
956
|
+
},
|
|
957
|
+
// ── Contract map ───────────────────────────────────────────────────
|
|
958
|
+
// The card contract: what this work targets, what it must not break, what
|
|
959
|
+
// it risks, and what verifies it. Advisory by default — the readiness gate
|
|
960
|
+
// deliberately does not require map structure, so a map is an operator
|
|
961
|
+
// review aid, not work the model must complete before implementing.
|
|
962
|
+
contractEnforcement: {
|
|
963
|
+
type: "string",
|
|
964
|
+
enum: ["off", "advisory", "strict"],
|
|
965
|
+
description: "Board-level contract map enforcement. Default when first configured: advisory."
|
|
966
|
+
},
|
|
967
|
+
contractNodeId: { type: "string" },
|
|
968
|
+
contractNodeKind: {
|
|
969
|
+
type: "string",
|
|
970
|
+
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
|
|
971
|
+
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."
|
|
972
|
+
},
|
|
973
|
+
contractNodeTitle: { type: "string" },
|
|
974
|
+
contractNodeDescription: { type: "string" },
|
|
975
|
+
contractNodeState: {
|
|
976
|
+
type: "string",
|
|
977
|
+
enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
|
|
978
|
+
},
|
|
979
|
+
contractNodeEnforcement: {
|
|
980
|
+
type: "string",
|
|
981
|
+
enum: ["blocking", "advisory", "informational"]
|
|
982
|
+
},
|
|
983
|
+
/** Bind a node to an acceptance criterion or goal metric already on the task. */
|
|
984
|
+
contractCheckId: { type: "string" },
|
|
985
|
+
contractMetricId: { type: "string" },
|
|
986
|
+
contractWaiverReason: {
|
|
987
|
+
type: "string",
|
|
988
|
+
description: 'Required, with an actor, when contractNodeState is "waived".'
|
|
989
|
+
},
|
|
990
|
+
contractEdgeId: { type: "string" },
|
|
991
|
+
contractEdgeFrom: {
|
|
992
|
+
type: "string",
|
|
993
|
+
description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
|
|
994
|
+
},
|
|
995
|
+
contractEdgeTo: { type: "string" },
|
|
996
|
+
contractEdgeType: {
|
|
997
|
+
type: "string",
|
|
998
|
+
enum: [
|
|
999
|
+
"targets",
|
|
1000
|
+
"affects",
|
|
1001
|
+
"must_preserve",
|
|
1002
|
+
"exposes",
|
|
1003
|
+
"verified_by",
|
|
1004
|
+
"conflicts_with",
|
|
1005
|
+
"derived_from",
|
|
1006
|
+
"relates_to"
|
|
1007
|
+
]
|
|
1008
|
+
},
|
|
1009
|
+
contractEdgeRationale: { type: "string" },
|
|
945
1010
|
note: { type: "string" },
|
|
946
1011
|
author: { type: "string" },
|
|
947
1012
|
url: { type: "string" },
|
|
@@ -994,12 +1059,17 @@ import {
|
|
|
994
1059
|
mutateTasks
|
|
995
1060
|
} from "@wrongstack/core/storage";
|
|
996
1061
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
997
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
1062
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
998
1063
|
import {
|
|
999
1064
|
bridgeKanbanSupervisor,
|
|
1065
|
+
compactSessionMirrorBoard,
|
|
1000
1066
|
createBoard,
|
|
1067
|
+
DEFAULT_COLUMNS,
|
|
1001
1068
|
getBoard as getBoard2,
|
|
1069
|
+
getDependencyReadinessIssues,
|
|
1070
|
+
getKanbanOrchestrationSnapshot,
|
|
1002
1071
|
listBoards,
|
|
1072
|
+
pruneSessionBoards,
|
|
1003
1073
|
removeBoard,
|
|
1004
1074
|
syncBoardFromTaskGraph,
|
|
1005
1075
|
touchKanbanPresence as touchKanbanPresence2,
|
|
@@ -1007,16 +1077,14 @@ import {
|
|
|
1007
1077
|
} from "@wrongstack/kanban";
|
|
1008
1078
|
var SESSION_BOARD_TAG = "session-work";
|
|
1009
1079
|
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
1010
|
-
var SESSION_KANBAN_COLUMNS =
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
1014
|
-
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
1015
|
-
];
|
|
1080
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
1081
|
+
...column
|
|
1082
|
+
}));
|
|
1016
1083
|
var boardQueue = /* @__PURE__ */ new Map();
|
|
1017
1084
|
var boardEnsures = /* @__PURE__ */ new Map();
|
|
1018
1085
|
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
1019
1086
|
var activeMirrors = /* @__PURE__ */ new Set();
|
|
1087
|
+
var mirrorFailures = /* @__PURE__ */ new Map();
|
|
1020
1088
|
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
1021
1089
|
function boardKey(projectRoot, sessionId) {
|
|
1022
1090
|
return `${projectRoot}\0${sessionId}`;
|
|
@@ -1024,6 +1092,33 @@ function boardKey(projectRoot, sessionId) {
|
|
|
1024
1092
|
function mirrorKey(projectRoot, sessionId, sourceSystem) {
|
|
1025
1093
|
return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
|
|
1026
1094
|
}
|
|
1095
|
+
function completedReconciliationGraph(latest, candidates) {
|
|
1096
|
+
const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
|
|
1097
|
+
const carriedNodeIds = /* @__PURE__ */ new Set();
|
|
1098
|
+
const completedNodes = candidates.flatMap(
|
|
1099
|
+
(candidate) => candidate.nodes.filter((node) => {
|
|
1100
|
+
if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
carriedNodeIds.add(node.id);
|
|
1104
|
+
return true;
|
|
1105
|
+
})
|
|
1106
|
+
);
|
|
1107
|
+
if (completedNodes.length === 0) return void 0;
|
|
1108
|
+
const carriedRequirements = completedNodes.flatMap(
|
|
1109
|
+
(node) => node.specRequirementId ? [node.specRequirementId] : []
|
|
1110
|
+
);
|
|
1111
|
+
return {
|
|
1112
|
+
...latest,
|
|
1113
|
+
nodes: [...latest.nodes, ...completedNodes],
|
|
1114
|
+
rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
|
|
1115
|
+
...latest.requiredRequirementIds ? {
|
|
1116
|
+
requiredRequirementIds: [
|
|
1117
|
+
.../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
|
|
1118
|
+
]
|
|
1119
|
+
} : {}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1027
1122
|
function sessionTag(sessionId) {
|
|
1028
1123
|
return `session:${sessionId}`;
|
|
1029
1124
|
}
|
|
@@ -1102,16 +1197,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
1102
1197
|
sourceSystem,
|
|
1103
1198
|
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
1104
1199
|
archiveMissingTasks: true,
|
|
1105
|
-
includeCompletedTasks: true
|
|
1200
|
+
includeCompletedTasks: true,
|
|
1201
|
+
// The scope ledger stays declared and accurate, but it may not veto a
|
|
1202
|
+
// projection. A session mirror reflects a tactical list that shrinks by
|
|
1203
|
+
// design, and refusing the sync never protected the removed row — it
|
|
1204
|
+
// froze the entire board, permanently, because the stored scope then
|
|
1205
|
+
// outlived every later snapshot (`session-kanban.mirror-failed`).
|
|
1206
|
+
// Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
|
|
1207
|
+
// removed card on the board as `archived`, the reconciliation pass
|
|
1208
|
+
// first walks vanished completed rows to Done, and the session journal
|
|
1209
|
+
// remains the durable record.
|
|
1210
|
+
allowRequirementScopeShrink: true
|
|
1106
1211
|
}
|
|
1107
1212
|
);
|
|
1108
|
-
|
|
1213
|
+
if (!result) return null;
|
|
1214
|
+
const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
|
|
1215
|
+
if (compacted?.removedTaskIds.length) {
|
|
1216
|
+
return await getBoard2(projectRoot, board.id) ?? result.board;
|
|
1217
|
+
}
|
|
1218
|
+
return result.board;
|
|
1109
1219
|
});
|
|
1110
1220
|
}
|
|
1111
1221
|
function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
1112
1222
|
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
|
|
1113
1223
|
const key = mirrorKey(projectRoot, sessionId, sourceSystem);
|
|
1114
|
-
pendingMirrors.
|
|
1224
|
+
const previous = pendingMirrors.get(key);
|
|
1225
|
+
const reconciliationGraph = previous ? completedReconciliationGraph(
|
|
1226
|
+
graph,
|
|
1227
|
+
[previous.reconciliationGraph, previous.graph].filter(
|
|
1228
|
+
(candidate) => candidate !== void 0
|
|
1229
|
+
)
|
|
1230
|
+
) : void 0;
|
|
1231
|
+
pendingMirrors.set(key, {
|
|
1232
|
+
projectRoot,
|
|
1233
|
+
sessionId,
|
|
1234
|
+
graph,
|
|
1235
|
+
...reconciliationGraph ? { reconciliationGraph } : {},
|
|
1236
|
+
sourceSystem
|
|
1237
|
+
});
|
|
1115
1238
|
if (activeMirrors.has(key)) return;
|
|
1116
1239
|
activeMirrors.add(key);
|
|
1117
1240
|
void (async () => {
|
|
@@ -1121,20 +1244,34 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
1121
1244
|
if (!pending) break;
|
|
1122
1245
|
pendingMirrors.delete(key);
|
|
1123
1246
|
try {
|
|
1247
|
+
if (pending.reconciliationGraph) {
|
|
1248
|
+
await projectGraph(
|
|
1249
|
+
pending.projectRoot,
|
|
1250
|
+
pending.sessionId,
|
|
1251
|
+
pending.reconciliationGraph,
|
|
1252
|
+
pending.sourceSystem
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1124
1255
|
await projectGraph(
|
|
1125
1256
|
pending.projectRoot,
|
|
1126
1257
|
pending.sessionId,
|
|
1127
1258
|
pending.graph,
|
|
1128
1259
|
pending.sourceSystem
|
|
1129
1260
|
);
|
|
1261
|
+
mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
|
|
1130
1262
|
} catch (error) {
|
|
1263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1264
|
+
mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
|
|
1265
|
+
message,
|
|
1266
|
+
sourceSystem: pending.sourceSystem
|
|
1267
|
+
});
|
|
1131
1268
|
console.warn(
|
|
1132
1269
|
JSON.stringify({
|
|
1133
1270
|
level: "warn",
|
|
1134
1271
|
event: "session-kanban.mirror-failed",
|
|
1135
1272
|
sessionId: pending.sessionId,
|
|
1136
1273
|
sourceSystem: pending.sourceSystem,
|
|
1137
|
-
message
|
|
1274
|
+
message,
|
|
1138
1275
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1139
1276
|
})
|
|
1140
1277
|
);
|
|
@@ -1155,6 +1292,14 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
1155
1292
|
}
|
|
1156
1293
|
})();
|
|
1157
1294
|
}
|
|
1295
|
+
function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
1296
|
+
if (!projectRoot || !sessionId) return void 0;
|
|
1297
|
+
const key = boardKey(projectRoot, sessionId);
|
|
1298
|
+
const failure = mirrorFailures.get(key);
|
|
1299
|
+
if (!failure) return void 0;
|
|
1300
|
+
mirrorFailures.delete(key);
|
|
1301
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
1302
|
+
}
|
|
1158
1303
|
function todoListToSerializedGraph(todos, sessionId) {
|
|
1159
1304
|
const graphId = `todo:${sessionId}`;
|
|
1160
1305
|
const nodes = todos.map((todo, index) => ({
|
|
@@ -1248,11 +1393,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
1248
1393
|
});
|
|
1249
1394
|
}
|
|
1250
1395
|
function notifyTodoUpdate(context, todos) {
|
|
1251
|
-
const summary = todos
|
|
1396
|
+
const summary = formatTodosForModel(todos);
|
|
1252
1397
|
const text = `[KANBAN TODO UPDATE]
|
|
1253
1398
|
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
1254
1399
|
${summary}
|
|
1255
|
-
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
1400
|
+
Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
|
|
1256
1401
|
const state = context.state;
|
|
1257
1402
|
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
1258
1403
|
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
@@ -1283,26 +1428,68 @@ function todoStatus(task) {
|
|
|
1283
1428
|
if (status === "in_progress" || status === "review") return "in_progress";
|
|
1284
1429
|
return "pending";
|
|
1285
1430
|
}
|
|
1286
|
-
function sessionTodoFromTask(task,
|
|
1431
|
+
function sessionTodoFromTask(task, board) {
|
|
1432
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
1287
1433
|
return {
|
|
1288
1434
|
id: task.origin?.taskId ?? task.id,
|
|
1289
1435
|
content: task.title,
|
|
1290
1436
|
status: todoStatus(task),
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
...task.description ? { activeForm: task.description } : {}
|
|
1437
|
+
...task.description ? { activeForm: task.description } : {},
|
|
1438
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
1294
1439
|
};
|
|
1295
1440
|
}
|
|
1296
|
-
function managedTodoFromTask(task,
|
|
1441
|
+
function managedTodoFromTask(task, board) {
|
|
1297
1442
|
return {
|
|
1298
|
-
...sessionTodoFromTask(task,
|
|
1299
|
-
|
|
1443
|
+
...sessionTodoFromTask(task, board),
|
|
1444
|
+
kanbanBoardId: board.id,
|
|
1445
|
+
kanbanTaskId: task.id
|
|
1300
1446
|
};
|
|
1301
1447
|
}
|
|
1448
|
+
function blockingTitles(board, task) {
|
|
1449
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
1450
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
1451
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
1452
|
+
return dependency.title;
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
var PRIORITY_ORDER = {
|
|
1456
|
+
critical: 0,
|
|
1457
|
+
high: 1,
|
|
1458
|
+
medium: 2,
|
|
1459
|
+
low: 3
|
|
1460
|
+
};
|
|
1461
|
+
function orderTasksForTodos(board, tasks) {
|
|
1462
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
1463
|
+
const baseline = [...tasks].sort(
|
|
1464
|
+
(left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
|
|
1465
|
+
);
|
|
1466
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
1467
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
1468
|
+
const emitted = [];
|
|
1469
|
+
const done = /* @__PURE__ */ new Set();
|
|
1470
|
+
while (remaining.size > 0) {
|
|
1471
|
+
const ready = baseline.filter(
|
|
1472
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
1473
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
1474
|
+
)
|
|
1475
|
+
);
|
|
1476
|
+
if (ready.length === 0) break;
|
|
1477
|
+
for (const task of ready) {
|
|
1478
|
+
remaining.delete(task.id);
|
|
1479
|
+
done.add(task.id);
|
|
1480
|
+
emitted.push(task);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
1484
|
+
return emitted;
|
|
1485
|
+
}
|
|
1302
1486
|
function sameTodos(left, right) {
|
|
1303
1487
|
return left.length === right.length && left.every((todo, index) => {
|
|
1304
1488
|
const candidate = right[index];
|
|
1305
|
-
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId
|
|
1489
|
+
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && // Readiness is part of the projection: when a dependency completes,
|
|
1490
|
+
// the rows are otherwise identical and the unblocking would never
|
|
1491
|
+
// reach the model.
|
|
1492
|
+
(candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
1306
1493
|
});
|
|
1307
1494
|
}
|
|
1308
1495
|
function applyManagedKanbanBoardToTodos(context, board) {
|
|
@@ -1312,11 +1499,12 @@ function applyManagedKanbanBoardToTodos(context, board) {
|
|
|
1312
1499
|
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
1313
1500
|
return [...context.todos];
|
|
1314
1501
|
}
|
|
1315
|
-
const projectedTodos =
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1502
|
+
const projectedTodos = orderTasksForTodos(
|
|
1503
|
+
board,
|
|
1504
|
+
board.tasks.filter(
|
|
1505
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
1506
|
+
)
|
|
1507
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
1320
1508
|
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
1321
1509
|
suppressedTodoMirrors.add(context);
|
|
1322
1510
|
try {
|
|
@@ -1360,8 +1548,14 @@ var kanbanTool = {
|
|
|
1360
1548
|
}
|
|
1361
1549
|
case "create_board": {
|
|
1362
1550
|
if (!input.title) return fail("create_board requires title.");
|
|
1551
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
1552
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
1553
|
+
);
|
|
1363
1554
|
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
1364
|
-
|
|
1555
|
+
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(
|
|
1556
|
+
", "
|
|
1557
|
+
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
1558
|
+
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
1365
1559
|
}
|
|
1366
1560
|
case "update_board": {
|
|
1367
1561
|
if (!input.boardId) return fail("update_board requires boardId.");
|
|
@@ -1390,6 +1584,20 @@ var kanbanTool = {
|
|
|
1390
1584
|
});
|
|
1391
1585
|
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
1392
1586
|
}
|
|
1587
|
+
// Adoption used to be a one-way door: the strict lifecycle carries
|
|
1588
|
+
// acceptance-criteria, verification-report, review-evidence and
|
|
1589
|
+
// one-stage-at-a-time gates, and nothing on the tool surface could
|
|
1590
|
+
// undo it, so a board adopted once kept its ceremony forever. The
|
|
1591
|
+
// gates are worth having where a fleet is supervised; they are not
|
|
1592
|
+
// worth being unable to leave. Cards and columns are untouched.
|
|
1593
|
+
case "release_managed_lifecycle": {
|
|
1594
|
+
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
1595
|
+
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
1596
|
+
return board ? okBoard(
|
|
1597
|
+
board,
|
|
1598
|
+
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
1599
|
+
) : fail("Board not found.");
|
|
1600
|
+
}
|
|
1393
1601
|
case "duplicate_board": {
|
|
1394
1602
|
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
1395
1603
|
const board = await duplicateBoard(
|
|
@@ -1409,8 +1617,7 @@ var kanbanTool = {
|
|
|
1409
1617
|
const boardInput = createBoardFromText({
|
|
1410
1618
|
description: input.description,
|
|
1411
1619
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
1412
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
1413
|
-
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
1620
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
1414
1621
|
});
|
|
1415
1622
|
const board = await createBoard2(projectRoot, boardInput);
|
|
1416
1623
|
for (const taskInput2 of parseLinesIntoTasks(
|
|
@@ -1548,7 +1755,7 @@ var kanbanTool = {
|
|
|
1548
1755
|
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1549
1756
|
}
|
|
1550
1757
|
case "snapshot": {
|
|
1551
|
-
const snapshot = await
|
|
1758
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
1552
1759
|
query: input.query,
|
|
1553
1760
|
boardId: input.boardId,
|
|
1554
1761
|
assignedAgent: input.agentId,
|
|
@@ -1563,33 +1770,6 @@ var kanbanTool = {
|
|
|
1563
1770
|
snapshot
|
|
1564
1771
|
};
|
|
1565
1772
|
}
|
|
1566
|
-
case "add_column": {
|
|
1567
|
-
if (!input.boardId || !input.title)
|
|
1568
|
-
return fail("add_column requires boardId and title.");
|
|
1569
|
-
const result2 = await addColumn(projectRoot, input.boardId, {
|
|
1570
|
-
title: input.title,
|
|
1571
|
-
...input.description !== void 0 ? { description: input.description } : {}
|
|
1572
|
-
});
|
|
1573
|
-
return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
|
|
1574
|
-
}
|
|
1575
|
-
case "update_column": {
|
|
1576
|
-
if (!input.boardId || !input.columnId)
|
|
1577
|
-
return fail("update_column requires boardId and columnId.");
|
|
1578
|
-
const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
|
|
1579
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1580
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1581
|
-
...input.order !== void 0 ? { order: input.order } : {}
|
|
1582
|
-
});
|
|
1583
|
-
return board ? okBoard(board, "Column updated.") : fail("Column not found.");
|
|
1584
|
-
}
|
|
1585
|
-
case "delete_column": {
|
|
1586
|
-
if (!input.boardId || !input.columnId)
|
|
1587
|
-
return fail("delete_column requires boardId and columnId.");
|
|
1588
|
-
const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
|
|
1589
|
-
moveTasksToColumnId: input.moveTasksToColumnId
|
|
1590
|
-
});
|
|
1591
|
-
return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
|
|
1592
|
-
}
|
|
1593
1773
|
case "add_task": {
|
|
1594
1774
|
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1595
1775
|
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
@@ -1671,6 +1851,32 @@ var kanbanTool = {
|
|
|
1671
1851
|
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1672
1852
|
);
|
|
1673
1853
|
}
|
|
1854
|
+
if (board.lifecycle?.mode !== "managed") {
|
|
1855
|
+
const now = /* @__PURE__ */ new Date();
|
|
1856
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1857
|
+
status: "running",
|
|
1858
|
+
agentId: input.agentId ?? input.author,
|
|
1859
|
+
leaseId: input.leaseId ?? randomUUID2(),
|
|
1860
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1861
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1862
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1863
|
+
attempt: input.attempt ?? 1,
|
|
1864
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1865
|
+
});
|
|
1866
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1867
|
+
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
1868
|
+
status: "in_progress"
|
|
1869
|
+
});
|
|
1870
|
+
const current = started ?? assigned;
|
|
1871
|
+
const claimed = task;
|
|
1872
|
+
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
1873
|
+
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
1874
|
+
return okTask(
|
|
1875
|
+
current,
|
|
1876
|
+
currentTask,
|
|
1877
|
+
"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."
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1674
1880
|
let stage = task.lifecycle?.currentStage;
|
|
1675
1881
|
if (stage === "backlog") {
|
|
1676
1882
|
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
@@ -1811,6 +2017,9 @@ var kanbanTool = {
|
|
|
1811
2017
|
if (!input.boardId || !input.taskId)
|
|
1812
2018
|
return fail("delete_task requires boardId and taskId.");
|
|
1813
2019
|
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
2020
|
+
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
2021
|
+
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
2022
|
+
}
|
|
1814
2023
|
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1815
2024
|
}
|
|
1816
2025
|
case "set_chain": {
|
|
@@ -1943,7 +2152,7 @@ var kanbanTool = {
|
|
|
1943
2152
|
});
|
|
1944
2153
|
} catch (err) {
|
|
1945
2154
|
lifecycleWarnings.push(
|
|
1946
|
-
`Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
|
|
2155
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1947
2156
|
);
|
|
1948
2157
|
}
|
|
1949
2158
|
}
|
|
@@ -1967,7 +2176,7 @@ var kanbanTool = {
|
|
|
1967
2176
|
});
|
|
1968
2177
|
} catch (err) {
|
|
1969
2178
|
lifecycleWarnings.push(
|
|
1970
|
-
`Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2179
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1971
2180
|
);
|
|
1972
2181
|
}
|
|
1973
2182
|
if (transitionResult) {
|
|
@@ -1987,7 +2196,11 @@ var kanbanTool = {
|
|
|
1987
2196
|
successCriteria: verResult.task.successCriteria
|
|
1988
2197
|
});
|
|
1989
2198
|
const verdict = verResult.report.verdict;
|
|
1990
|
-
if (verdict === "passed") {
|
|
2199
|
+
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
2200
|
+
lifecycleWarnings.push(
|
|
2201
|
+
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
2202
|
+
);
|
|
2203
|
+
} else if (verdict === "passed") {
|
|
1991
2204
|
try {
|
|
1992
2205
|
const doneResult = await transitionTask(
|
|
1993
2206
|
projectRoot,
|
|
@@ -2105,11 +2318,34 @@ var kanbanTool = {
|
|
|
2105
2318
|
});
|
|
2106
2319
|
return {
|
|
2107
2320
|
ok: true,
|
|
2108
|
-
message: `Counts:
|
|
2321
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
2109
2322
|
queueHealth: health
|
|
2110
2323
|
};
|
|
2111
2324
|
}
|
|
2325
|
+
// Not every action is handled above. These are dispatched from here,
|
|
2326
|
+
// and the split has already cost real time: an agent that read this
|
|
2327
|
+
// file concluded `add_check` / `update_check` did not exist, wrote
|
|
2328
|
+
// that on a card, and spent a session trying to satisfy a gate it
|
|
2329
|
+
// already had the tool to clear. Keep this index in step with the
|
|
2330
|
+
// handlers.
|
|
2331
|
+
//
|
|
2332
|
+
// kanban-detail-actions.ts workbench · add_dependency ·
|
|
2333
|
+
// add_goal_metric · update_goal_metric · add_check ·
|
|
2334
|
+
// update_check · add_note · add_link · split_atomic
|
|
2335
|
+
// kanban-decomposition-actions.ts verify_completion ·
|
|
2336
|
+
// assess_atomicity · propose_decomposition
|
|
2337
|
+
// kanban-contract-actions.ts get_contract_graph ·
|
|
2338
|
+
// configure_contract_graph · upsert_contract_node ·
|
|
2339
|
+
// remove_contract_node · add_contract_edge · remove_contract_edge
|
|
2112
2340
|
default:
|
|
2341
|
+
{
|
|
2342
|
+
const contractResult = await handleKanbanContractAction(
|
|
2343
|
+
projectRoot,
|
|
2344
|
+
input,
|
|
2345
|
+
input.author ?? input.agentId
|
|
2346
|
+
);
|
|
2347
|
+
if (contractResult !== void 0) return contractResult;
|
|
2348
|
+
}
|
|
2113
2349
|
{
|
|
2114
2350
|
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
2115
2351
|
if (detailResult !== void 0) return detailResult;
|
|
@@ -2119,7 +2355,7 @@ var kanbanTool = {
|
|
|
2119
2355
|
})();
|
|
2120
2356
|
return withPresence(result);
|
|
2121
2357
|
} catch (err) {
|
|
2122
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
2358
|
+
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
2123
2359
|
}
|
|
2124
2360
|
}
|
|
2125
2361
|
};
|
|
@@ -2152,11 +2388,51 @@ function bindTodosToBoard(items, previous, board) {
|
|
|
2152
2388
|
available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
|
|
2153
2389
|
];
|
|
2154
2390
|
const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
|
|
2155
|
-
if (!task)
|
|
2391
|
+
if (!task) {
|
|
2392
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
2393
|
+
return { ...rest };
|
|
2394
|
+
}
|
|
2156
2395
|
used.add(task.id);
|
|
2157
|
-
|
|
2396
|
+
const blockedBy = blockingTitles(board, task);
|
|
2397
|
+
return {
|
|
2398
|
+
...item,
|
|
2399
|
+
kanbanBoardId: board.id,
|
|
2400
|
+
kanbanTaskId: task.id,
|
|
2401
|
+
...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
|
|
2402
|
+
};
|
|
2158
2403
|
});
|
|
2159
2404
|
}
|
|
2405
|
+
function demoteBlockedInProgress(items, warnings) {
|
|
2406
|
+
return items.map((item) => {
|
|
2407
|
+
if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
|
|
2408
|
+
warnings.push(
|
|
2409
|
+
`"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
|
|
2410
|
+
);
|
|
2411
|
+
return { ...item, status: "pending" };
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
async function createMissingManagedCards(items, board, ctx, warnings) {
|
|
2415
|
+
const created = /* @__PURE__ */ new Map();
|
|
2416
|
+
for (const item of items) {
|
|
2417
|
+
if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
|
|
2418
|
+
try {
|
|
2419
|
+
const result = await addTask2(ctx.projectRoot, board.id, {
|
|
2420
|
+
title: item.content,
|
|
2421
|
+
description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
|
|
2422
|
+
});
|
|
2423
|
+
if (!result) {
|
|
2424
|
+
warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
created.set(item.id, result.task.id);
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
warnings.push(
|
|
2430
|
+
`Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return created;
|
|
2435
|
+
}
|
|
2160
2436
|
async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
2161
2437
|
let synced = 0;
|
|
2162
2438
|
const warnings = [];
|
|
@@ -2194,6 +2470,16 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2194
2470
|
transitionComment: `Todo returned to queue: ${item.content}`
|
|
2195
2471
|
});
|
|
2196
2472
|
}
|
|
2473
|
+
for (const item of items) {
|
|
2474
|
+
if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2478
|
+
if (task?.status !== "completed") continue;
|
|
2479
|
+
warnings.push(
|
|
2480
|
+
`"${item.content}" is already Done on the Kanban board and a completed card cannot be reopened; the row stays completed. Create a follow-up card for any remaining work.`
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2197
2483
|
for (const item of items) {
|
|
2198
2484
|
if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2199
2485
|
continue;
|
|
@@ -2245,17 +2531,25 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2245
2531
|
const active = items.find(
|
|
2246
2532
|
(item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
|
|
2247
2533
|
);
|
|
2534
|
+
const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
|
|
2248
2535
|
if (active?.kanbanTaskId) {
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2536
|
+
if (activeStage === "review" || activeStage === "done") {
|
|
2537
|
+
warnings.push(
|
|
2538
|
+
`"${active.content}" is in ${activeStage === "review" ? "Review" : "Done"} awaiting acceptance; not re-activating it from the todo list. ` + (activeStage === "review" ? "Call kanban start_task explicitly to reopen it as a repair." : "Done is terminal; reopen only by creating a follow-up card.")
|
|
2539
|
+
);
|
|
2540
|
+
} else {
|
|
2541
|
+
await execute({
|
|
2542
|
+
action: "start_task",
|
|
2543
|
+
boardId: board.id,
|
|
2544
|
+
taskId: active.kanbanTaskId,
|
|
2545
|
+
author: actor,
|
|
2546
|
+
agentId: actor,
|
|
2547
|
+
transitionComment: `Todo activated: ${active.content}`
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2257
2550
|
}
|
|
2258
|
-
if (active?.kanbanTaskId &&
|
|
2551
|
+
if (active?.kanbanTaskId && activeStage === "review") {
|
|
2552
|
+
} else if (active?.kanbanTaskId && completionPending) {
|
|
2259
2553
|
warnings.push(
|
|
2260
2554
|
"A completed todo is still awaiting acceptance; the next independent Kanban task was started."
|
|
2261
2555
|
);
|
|
@@ -2342,29 +2636,47 @@ var todoTool = {
|
|
|
2342
2636
|
}
|
|
2343
2637
|
}
|
|
2344
2638
|
const boardId = activeBoardId(items, ctx);
|
|
2345
|
-
|
|
2346
|
-
const
|
|
2639
|
+
let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
|
|
2640
|
+
const managed = board?.lifecycle?.mode === "managed";
|
|
2641
|
+
let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
2642
|
+
const creationWarnings = [];
|
|
2643
|
+
if (managed && board) {
|
|
2644
|
+
const managedBoardId = board.id;
|
|
2645
|
+
const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
|
|
2646
|
+
if (created.size > 0) {
|
|
2647
|
+
boundItems = boundItems.map((item) => {
|
|
2648
|
+
const taskId = created.get(item.id);
|
|
2649
|
+
return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
|
|
2650
|
+
});
|
|
2651
|
+
board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
|
|
2652
|
+
boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
|
|
2653
|
+
}
|
|
2654
|
+
boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
|
|
2655
|
+
}
|
|
2347
2656
|
ctx.state.replaceTodos(boundItems);
|
|
2348
|
-
const kanbanSync =
|
|
2349
|
-
|
|
2657
|
+
const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
|
|
2658
|
+
kanbanSync.warnings.unshift(...creationWarnings);
|
|
2659
|
+
if (managed && board) {
|
|
2350
2660
|
const unresolved = boundItems.filter(
|
|
2351
2661
|
(item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
|
|
2352
2662
|
);
|
|
2353
2663
|
if (unresolved.length > 0) {
|
|
2354
2664
|
kanbanSync.warnings.push(
|
|
2355
|
-
`${unresolved.length} Todo row(s)
|
|
2665
|
+
`${unresolved.length} Todo row(s) could not be bound to a Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
|
|
2356
2666
|
);
|
|
2357
2667
|
}
|
|
2358
2668
|
}
|
|
2669
|
+
const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
|
|
2670
|
+
if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
|
|
2359
2671
|
let projectedBoard = board;
|
|
2360
|
-
if (
|
|
2672
|
+
if (managed && board) {
|
|
2361
2673
|
const refreshed = await getBoard4(ctx.projectRoot, board.id);
|
|
2362
2674
|
if (refreshed) {
|
|
2363
2675
|
projectedBoard = refreshed;
|
|
2364
2676
|
applyManagedKanbanBoardToTodos(ctx, refreshed);
|
|
2365
2677
|
}
|
|
2366
2678
|
}
|
|
2367
|
-
if (
|
|
2679
|
+
if (!managed) {
|
|
2368
2680
|
mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
2369
2681
|
}
|
|
2370
2682
|
const completedPlanIds = /* @__PURE__ */ new Set();
|