@wrongstack/tools 1.0.7 → 1.0.9
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/_win32-resolve.d.ts +5 -0
- package/dist/audit.js +5 -5
- package/dist/bash.js +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/builtin.js +41 -41
- package/dist/{chunk-N3OTTQNC.js → chunk-2OCAAFAT.js} +44 -25
- package/dist/{chunk-5MP76C72.js → chunk-2UVAIS5O.js} +24 -29
- package/dist/{chunk-SIIOHBNY.js → chunk-3RRK33YM.js} +2 -2
- package/dist/{chunk-M2W36JT7.js → chunk-4CE3RKEP.js} +6 -3
- package/dist/{chunk-QJARSK2G.js → chunk-4SUGLJLO.js} +11 -3
- package/dist/{chunk-AH7J5HWL.js → chunk-5RBUYFRV.js} +15 -7
- package/dist/{chunk-WKLUOXR4.js → chunk-7GOE2ZTZ.js} +2 -2
- package/dist/{chunk-O6AGFJBO.js → chunk-7V33PHT6.js} +49 -77
- package/dist/{chunk-CAF4L6KN.js → chunk-C3WP6ZOT.js} +36 -48
- package/dist/{chunk-KMXPP32M.js → chunk-C6ZOXNXV.js} +13 -3
- package/dist/{chunk-EGT5QLCB.js → chunk-D6RQZMZS.js} +123 -255
- package/dist/{chunk-S6CGSFLX.js → chunk-DBG7UQGK.js} +10 -3
- package/dist/{chunk-2W4XTULI.js → chunk-DLZCOEVG.js} +50 -8
- package/dist/{chunk-NL45OWEO.js → chunk-DOAHONOS.js} +7 -12
- package/dist/{chunk-3LQJXJDM.js → chunk-E7A4SLKR.js} +2 -2
- package/dist/{chunk-JB5JLY7V.js → chunk-ENE22I5W.js} +387 -394
- package/dist/{chunk-SEONDNLN.js → chunk-G7CYW5EP.js} +2 -2
- package/dist/{chunk-YPSIG23D.js → chunk-HV7RWDCL.js} +3 -3
- package/dist/{chunk-TU6UVATU.js → chunk-HYHE4VNA.js} +3 -3
- package/dist/{chunk-HIRVCXLP.js → chunk-IE4KDAOB.js} +3 -3
- package/dist/{chunk-YA2ZYCJY.js → chunk-JPK2HHBK.js} +1 -1
- package/dist/{chunk-7E55DAFW.js → chunk-KHUCKLOR.js} +12 -7
- package/dist/{chunk-R3HIZVFS.js → chunk-KPDCKSN5.js} +23 -3
- package/dist/{chunk-B3MGJBFI.js → chunk-KS2OEZWV.js} +23 -47
- package/dist/{chunk-UI3ZJIG3.js → chunk-LKLIQUCW.js} +14 -8
- package/dist/{chunk-62OAECEQ.js → chunk-LLSFOUZF.js} +8 -8
- package/dist/{chunk-YZQSUL5G.js → chunk-M4ZHHCCW.js} +14 -6
- package/dist/{chunk-IXIS7RSG.js → chunk-MRXWOOOR.js} +29 -47
- package/dist/{chunk-C4DSQBTT.js → chunk-NEYUPZL6.js} +3 -3
- package/dist/{chunk-6XYFYLN7.js → chunk-O2MHULBO.js} +13 -8
- package/dist/{chunk-B62LKMWQ.js → chunk-OKZJJDOG.js} +86 -188
- package/dist/{chunk-JKFIDWQG.js → chunk-ON6ET5ER.js} +52 -9
- package/dist/{chunk-KF7GMVNU.js → chunk-OUERRFEM.js} +32 -11
- package/dist/{chunk-4DK5WZGR.js → chunk-OXO773CW.js} +127 -196
- package/dist/{chunk-GF4JHLCO.js → chunk-PWZUHZP7.js} +2 -2
- package/dist/{chunk-CLJXKFVS.js → chunk-QA37PFYL.js} +29 -68
- package/dist/{chunk-A3LNNBYO.js → chunk-QAWI3RSK.js} +41 -83
- package/dist/{chunk-ZMCV3XOV.js → chunk-SPNXDTCJ.js} +29 -63
- package/dist/{chunk-H2VB7GCR.js → chunk-ULI2J6TX.js} +27 -14
- package/dist/{chunk-EMA2XENR.js → chunk-UVYRQKPZ.js} +4 -3
- package/dist/{chunk-2NTHMZV3.js → chunk-VMJY6FYJ.js} +693 -276
- package/dist/{chunk-KPR53VKZ.js → chunk-WEUEU53R.js} +2 -2
- package/dist/{chunk-ZOUG3KKS.js → chunk-XPVYZ2S6.js} +2 -2
- package/dist/{chunk-5U5ULQX2.js → chunk-XZLVJD56.js} +5 -1
- package/dist/{chunk-HCPSVK7J.js → chunk-YKSOSI2N.js} +47 -31
- package/dist/codebase-index/codebase-ast-replace-tool.d.ts +5 -6
- package/dist/codebase-index/codebase-context-tool.d.ts +4 -6
- package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +1 -2
- package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +0 -2
- package/dist/codebase-index/codebase-index-tool.d.ts +0 -2
- package/dist/codebase-index/codebase-invariant-check-tool.d.ts +1 -2
- package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +0 -2
- package/dist/codebase-index/codebase-repo-map-tool.d.ts +1 -2
- package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-targeted-test-tool.d.ts +1 -2
- package/dist/codebase-index/index.js +8 -8
- package/dist/codebase-index/parser-worker-script.js +3 -3
- package/dist/codebase-index/project-server.js +5 -5
- package/dist/codebase-index/worker.js +4 -4
- package/dist/design.js +1 -1
- package/dist/diff.js +1 -1
- package/dist/edit.js +7 -7
- package/dist/exec.d.ts +7 -7
- package/dist/exec.js +2 -2
- package/dist/format.js +5 -5
- package/dist/git.js +1 -1
- package/dist/glob.js +1 -1
- package/dist/{go-parser-VDPRQEGM.js → go-parser-Z4H6SR3R.js} +6 -3
- package/dist/grep.js +1 -1
- package/dist/index.js +45 -45
- package/dist/install.js +5 -5
- package/dist/json.d.ts +0 -1
- package/dist/json.js +1 -1
- package/dist/kanban-presence.d.ts +6 -0
- package/dist/kanban-serializer.d.ts +9 -1
- package/dist/kanban-split-task-handler.d.ts +1 -1
- package/dist/kanban-tool-results.d.ts +57 -2
- package/dist/kanban-tool-types.d.ts +16 -5
- package/dist/kanban.d.ts +14 -0
- package/dist/kanban.js +9 -1
- package/dist/languages/index.js +3 -3
- package/dist/{legacy-bridge-4T4OTSQP.js → legacy-bridge-BPT2S2F4.js} +5 -5
- package/dist/lint.js +5 -5
- package/dist/logs.js +1 -1
- package/dist/mode.d.ts +3 -2
- package/dist/mode.js +1 -1
- package/dist/outdated.js +2 -2
- package/dist/pack.js +42 -42
- package/dist/{parser-dispatch-4E2SDQUO.js → parser-dispatch-WG4DNBHH.js} +4 -4
- package/dist/patch.js +7 -7
- package/dist/plan.d.ts +2 -1
- package/dist/plan.js +3 -3
- package/dist/{py-parser-MAW6DHFE.js → py-parser-4PFZYAJ7.js} +3 -3
- package/dist/read.js +7 -7
- package/dist/replace.js +7 -7
- package/dist/search.d.ts +0 -6
- package/dist/search.js +1 -1
- package/dist/security-ast-scan-tool.d.ts +1 -2
- package/dist/session-shell.js +2 -2
- package/dist/task.d.ts +2 -1
- package/dist/task.js +3 -3
- package/dist/test.d.ts +8 -1
- package/dist/test.js +5 -5
- package/dist/todo.js +2 -2
- package/dist/tool-search.d.ts +2 -0
- package/dist/tool-search.js +1 -1
- package/dist/tool-tier.js +43 -43
- package/dist/tool-use.d.ts +2 -2
- package/dist/tool-use.js +1 -1
- package/dist/tree.js +1 -1
- package/dist/typecheck.js +5 -5
- package/dist/wasm/bash/tree-sitter-bash.wasm +0 -0
- package/dist/wasm/c/tree-sitter-c.wasm +0 -0
- package/dist/wasm/c_sharp/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/checksums.json +16 -0
- package/dist/wasm/cpp/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/elixir/tree-sitter-elixir.wasm +0 -0
- package/dist/wasm/go/tree-sitter-go.wasm +0 -0
- package/dist/wasm/java/tree-sitter-java.wasm +0 -0
- package/dist/wasm/kotlin/tree-sitter-kotlin.wasm +0 -0
- package/dist/wasm/php/tree-sitter-php.wasm +0 -0
- package/dist/wasm/python/tree-sitter-python.wasm +0 -0
- package/dist/wasm/ruby/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/rust/tree-sitter-rust.wasm +0 -0
- package/dist/wasm/swift/tree-sitter-swift.wasm +0 -0
- package/dist/wasm/tree-sitter-runtime.wasm +0 -0
- package/dist/win32.d.ts +1 -1
- package/dist/win32.js +3 -1
- package/dist/write.js +7 -7
- package/package.json +10 -10
|
@@ -3,9 +3,6 @@ import {
|
|
|
3
3
|
taskFileToSerializedGraph
|
|
4
4
|
} from "./chunk-YZECCW3F.js";
|
|
5
5
|
|
|
6
|
-
// src/kanban.ts
|
|
7
|
-
import { stripLifecycleIssues as stripLifecycleIssues2 } from "@wrongstack/kanban";
|
|
8
|
-
|
|
9
6
|
// src/kanban-board-actions.ts
|
|
10
7
|
import { loadTasks } from "@wrongstack/core/storage";
|
|
11
8
|
import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
@@ -31,9 +28,163 @@ import {
|
|
|
31
28
|
updateBoard
|
|
32
29
|
} from "@wrongstack/kanban";
|
|
33
30
|
|
|
31
|
+
// src/kanban-tool-results.ts
|
|
32
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
33
|
+
import {
|
|
34
|
+
decodeLifecycleIssues,
|
|
35
|
+
stripLifecycleIssues
|
|
36
|
+
} from "@wrongstack/kanban";
|
|
37
|
+
function atomicityNudge(task) {
|
|
38
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
39
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
40
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
41
|
+
}
|
|
42
|
+
function readEnvGateEnforcement() {
|
|
43
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
44
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
45
|
+
}
|
|
46
|
+
var CLASSIFIER_HINT = {
|
|
47
|
+
NOT_FOUND: "ENOENT",
|
|
48
|
+
CONFLICT: "EBUSY",
|
|
49
|
+
UNAVAILABLE: "ECONNREFUSED"
|
|
50
|
+
};
|
|
51
|
+
function formatKanbanErrorMessage(code, message, opts) {
|
|
52
|
+
let text = `[${code}] ${message}`;
|
|
53
|
+
if (opts.committed) text += ` Already committed: ${opts.committed}`;
|
|
54
|
+
const extra = (opts.issues ?? []).filter((issue) => issue.message.trim() !== message.trim());
|
|
55
|
+
if (extra.length > 0) {
|
|
56
|
+
text += ` Issues: ${extra.map((issue) => issue.field ? `${issue.field}: ${issue.message}` : issue.message).join(" | ")}`;
|
|
57
|
+
}
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
var KanbanToolError = class extends Error {
|
|
61
|
+
kanbanCode;
|
|
62
|
+
/** Classifier hint (see CLASSIFIER_HINT); not the Kanban code. */
|
|
63
|
+
code;
|
|
64
|
+
retryable;
|
|
65
|
+
issues;
|
|
66
|
+
committed;
|
|
67
|
+
/** The bare message, without the code prefix, committed note, or issues tail. */
|
|
68
|
+
detail;
|
|
69
|
+
constructor(code, message, opts = {}) {
|
|
70
|
+
super(
|
|
71
|
+
formatKanbanErrorMessage(code, message, opts),
|
|
72
|
+
opts.cause !== void 0 ? { cause: opts.cause } : void 0
|
|
73
|
+
);
|
|
74
|
+
this.detail = message;
|
|
75
|
+
this.name = code === "ABORTED" ? "AbortError" : "KanbanToolError";
|
|
76
|
+
this.kanbanCode = code;
|
|
77
|
+
this.retryable = opts.retryable ?? (code === "CONFLICT" || code === "UNAVAILABLE");
|
|
78
|
+
const hint = CLASSIFIER_HINT[code];
|
|
79
|
+
this.code = hint === "ENOENT" || this.retryable ? hint : void 0;
|
|
80
|
+
this.issues = opts.issues;
|
|
81
|
+
this.committed = opts.committed;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var KanbanInputError = class extends ToolValidationError {
|
|
85
|
+
kanbanCode = "INVALID_INPUT";
|
|
86
|
+
retryable = false;
|
|
87
|
+
issues = void 0;
|
|
88
|
+
constructor(message, field) {
|
|
89
|
+
super({ message: `[INVALID_INPUT] ${message}`, ...field !== void 0 ? { field } : {} });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function invalidInput(message, field) {
|
|
93
|
+
return new KanbanInputError(message, field);
|
|
94
|
+
}
|
|
95
|
+
function notFound(message, opts = {}) {
|
|
96
|
+
return new KanbanToolError("NOT_FOUND", message, opts);
|
|
97
|
+
}
|
|
98
|
+
function refused(message, opts = {}) {
|
|
99
|
+
return new KanbanToolError("REFUSED", message, opts);
|
|
100
|
+
}
|
|
101
|
+
function conflict(message, opts = {}) {
|
|
102
|
+
return new KanbanToolError("CONFLICT", message, opts);
|
|
103
|
+
}
|
|
104
|
+
function isKanbanToolFailure(err) {
|
|
105
|
+
return err instanceof Error && typeof err.kanbanCode === "string" && typeof err.retryable === "boolean";
|
|
106
|
+
}
|
|
107
|
+
function errorCode(err) {
|
|
108
|
+
const code = err.code;
|
|
109
|
+
return typeof code === "string" ? code : void 0;
|
|
110
|
+
}
|
|
111
|
+
function lifecycleIssuesOf(err) {
|
|
112
|
+
const own = err.issues;
|
|
113
|
+
if (Array.isArray(own)) return own;
|
|
114
|
+
return decodeLifecycleIssues(err);
|
|
115
|
+
}
|
|
116
|
+
var UNAVAILABLE_MESSAGE_PATTERNS = [
|
|
117
|
+
/^Kanban project server is disabled/,
|
|
118
|
+
/^Failed to connect to kanban project server/,
|
|
119
|
+
/^Kanban server .+: /,
|
|
120
|
+
/^Connection closed$/,
|
|
121
|
+
/^connect-timeout$/,
|
|
122
|
+
/^Request \S+ timed out$/
|
|
123
|
+
];
|
|
124
|
+
var UNAVAILABLE_ERRNO = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "EPIPE", "ETIMEDOUT"]);
|
|
125
|
+
function toKanbanToolError(err, committed) {
|
|
126
|
+
if (!(err instanceof Error)) return err;
|
|
127
|
+
if (isKanbanToolFailure(err)) {
|
|
128
|
+
if (!committed) return err;
|
|
129
|
+
return err instanceof KanbanToolError ? new KanbanToolError(err.kanbanCode, err.detail, {
|
|
130
|
+
issues: err.issues,
|
|
131
|
+
retryable: err.retryable,
|
|
132
|
+
committed: err.committed ? `${err.committed} ${committed}` : committed,
|
|
133
|
+
cause: err
|
|
134
|
+
}) : err;
|
|
135
|
+
}
|
|
136
|
+
const code = errorCode(err);
|
|
137
|
+
if (err.name === "KanbanLifecycleError" || code === "LIFECYCLE") {
|
|
138
|
+
return refused(stripLifecycleIssues(err.message), {
|
|
139
|
+
issues: lifecycleIssuesOf(err),
|
|
140
|
+
committed,
|
|
141
|
+
cause: err
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (err.name === "StaleWriteError" || code === "STALE_WRITE") {
|
|
145
|
+
return conflict(err.message, { retryable: true, committed, cause: err });
|
|
146
|
+
}
|
|
147
|
+
if (code !== void 0 && UNAVAILABLE_ERRNO.has(code) || UNAVAILABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(err.message))) {
|
|
148
|
+
return new KanbanToolError("UNAVAILABLE", err.message, {
|
|
149
|
+
retryable: true,
|
|
150
|
+
committed,
|
|
151
|
+
cause: err
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (/^Ambiguous kanban (task|board) id /.test(err.message)) {
|
|
155
|
+
return invalidInput(err.message, "taskId");
|
|
156
|
+
}
|
|
157
|
+
return err;
|
|
158
|
+
}
|
|
159
|
+
function okBoard(board, message = "Board loaded.") {
|
|
160
|
+
return { ok: true, message, board };
|
|
161
|
+
}
|
|
162
|
+
function okTask(board, task, message) {
|
|
163
|
+
return { ok: true, message, board, task };
|
|
164
|
+
}
|
|
165
|
+
function resolveTaskRef(board, taskRef) {
|
|
166
|
+
if (!taskRef?.trim()) return void 0;
|
|
167
|
+
const exact = board.tasks.find((task) => task.id === taskRef);
|
|
168
|
+
if (exact) return exact;
|
|
169
|
+
const matches = board.tasks.filter((task) => task.id.startsWith(taskRef));
|
|
170
|
+
if (matches.length > 1) {
|
|
171
|
+
throw invalidInput(
|
|
172
|
+
`Ambiguous kanban task id "${taskRef}": ${matches.slice(0, 5).map((task) => task.id).join(", ")}`,
|
|
173
|
+
"taskId"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return matches[0];
|
|
177
|
+
}
|
|
178
|
+
|
|
34
179
|
// src/kanban-board-inputs.ts
|
|
35
180
|
function agentSettableGate(enforcement) {
|
|
36
|
-
if (enforcement === void 0
|
|
181
|
+
if (enforcement === void 0) return {};
|
|
182
|
+
if (enforcement !== "strict" && enforcement !== "soft") {
|
|
183
|
+
throw invalidInput(
|
|
184
|
+
`gateEnforcement "${String(enforcement)}" is not settable from the kanban tool; only "strict" or "soft" (switching the completion gate off is a human decision made through board config).`,
|
|
185
|
+
"gateEnforcement"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
37
188
|
return { completionGate: { enforcement } };
|
|
38
189
|
}
|
|
39
190
|
function boardCreateInput(input, title) {
|
|
@@ -77,34 +228,12 @@ function duplicateBoardOptions(input) {
|
|
|
77
228
|
|
|
78
229
|
// src/kanban-split-task-handler.ts
|
|
79
230
|
import { getBoard, splitTask } from "@wrongstack/kanban";
|
|
80
|
-
|
|
81
|
-
// src/kanban-tool-results.ts
|
|
82
|
-
function atomicityNudge(task) {
|
|
83
|
-
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
84
|
-
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
85
|
-
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
86
|
-
}
|
|
87
|
-
function readEnvGateEnforcement() {
|
|
88
|
-
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
89
|
-
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
90
|
-
}
|
|
91
|
-
function fail(message) {
|
|
92
|
-
return { ok: false, message };
|
|
93
|
-
}
|
|
94
|
-
function okBoard(board, message = "Board loaded.") {
|
|
95
|
-
return { ok: true, message, board };
|
|
96
|
-
}
|
|
97
|
-
function okTask(board, task, message) {
|
|
98
|
-
return { ok: true, message, board, task };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// src/kanban-split-task-handler.ts
|
|
102
231
|
async function handleSplitTask(projectRoot, input, extraSplitOptions, eventContext) {
|
|
103
232
|
const boardId = input.boardId;
|
|
104
233
|
const taskId = input.taskId;
|
|
105
234
|
const childTitles = input.childTitles;
|
|
106
235
|
if (!boardId || !taskId || !childTitles?.length) {
|
|
107
|
-
|
|
236
|
+
throw invalidInput("split requires boardId, taskId, and at least one childTitles.");
|
|
108
237
|
}
|
|
109
238
|
const {
|
|
110
239
|
targetColumnId,
|
|
@@ -134,12 +263,12 @@ async function handleSplitTask(projectRoot, input, extraSplitOptions, eventConte
|
|
|
134
263
|
},
|
|
135
264
|
eventContext
|
|
136
265
|
);
|
|
137
|
-
if (!result)
|
|
138
|
-
const freshParent = result.board.tasks?.find((t) => t.id === taskId);
|
|
266
|
+
if (!result) throw notFound("Task not found.");
|
|
267
|
+
const freshParent = result.board.tasks?.find((t) => t.id === taskId) ?? result.board.tasks?.find((t) => t.id === result.parent?.id);
|
|
139
268
|
if (!freshParent) {
|
|
140
|
-
|
|
141
|
-
`
|
|
142
|
-
);
|
|
269
|
+
throw conflict(`Parent ${taskId} not found in the board returned by the split.`, {
|
|
270
|
+
committed: `the split was written \u2014 child task(s) [${result.children.map((c) => c.id).join(", ")}] were created. Re-read the board with get_board.`
|
|
271
|
+
});
|
|
143
272
|
}
|
|
144
273
|
return {
|
|
145
274
|
ok: true,
|
|
@@ -165,11 +294,13 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
165
294
|
return { ok: true, message: `${boards.length} board(s).`, boards };
|
|
166
295
|
}
|
|
167
296
|
case "get_board": {
|
|
297
|
+
if (!input.boardId) throw invalidInput("get_board requires boardId.", "boardId");
|
|
168
298
|
const board = await requireBoard(projectRoot, input.boardId);
|
|
169
|
-
|
|
299
|
+
if (!board) throw notFound("Board not found.");
|
|
300
|
+
return okBoard(board);
|
|
170
301
|
}
|
|
171
302
|
case "create_board": {
|
|
172
|
-
if (!input.title)
|
|
303
|
+
if (!input.title) throw invalidInput("create_board requires title.", "title");
|
|
173
304
|
const existing = (await listBoards(projectRoot)).filter(
|
|
174
305
|
(candidate) => (candidate.kind ?? "project") === "project"
|
|
175
306
|
);
|
|
@@ -180,52 +311,65 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
180
311
|
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
181
312
|
}
|
|
182
313
|
case "update_board": {
|
|
183
|
-
if (!input.boardId)
|
|
314
|
+
if (!input.boardId) throw invalidInput("update_board requires boardId.", "boardId");
|
|
184
315
|
const board = await updateBoard(projectRoot, input.boardId, boardUpdatePatch(input));
|
|
185
|
-
|
|
316
|
+
if (!board) throw notFound("Board not found.");
|
|
317
|
+
return okBoard(board, "Board updated.");
|
|
186
318
|
}
|
|
187
319
|
case "adopt_managed_lifecycle": {
|
|
188
320
|
if (!input.boardId || !input.author || !input.transitionComment) {
|
|
189
|
-
|
|
321
|
+
throw invalidInput(
|
|
190
322
|
"adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
|
|
191
323
|
);
|
|
192
324
|
}
|
|
193
325
|
if (input.columns?.length !== 5) {
|
|
194
|
-
|
|
195
|
-
"adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
|
|
326
|
+
throw invalidInput(
|
|
327
|
+
"adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done.",
|
|
328
|
+
"columns"
|
|
196
329
|
);
|
|
197
330
|
}
|
|
198
331
|
const [backlog, todo, running, review, done] = input.columns;
|
|
199
332
|
if (!backlog || !todo || !running || !review || !done) {
|
|
200
|
-
|
|
333
|
+
throw invalidInput(
|
|
334
|
+
"adopt_managed_lifecycle columns must contain five nonblank ids.",
|
|
335
|
+
"columns"
|
|
336
|
+
);
|
|
201
337
|
}
|
|
202
338
|
const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
|
|
203
339
|
columns: { backlog, todo, running, review, done },
|
|
204
340
|
actor: input.author,
|
|
205
341
|
comment: input.transitionComment
|
|
206
342
|
});
|
|
207
|
-
|
|
343
|
+
if (!board) throw notFound("Board not found.");
|
|
344
|
+
return okBoard(board, "Managed lifecycle adopted without moving existing cards.");
|
|
208
345
|
}
|
|
209
346
|
case "release_managed_lifecycle": {
|
|
210
|
-
if (!input.boardId)
|
|
347
|
+
if (!input.boardId) {
|
|
348
|
+
throw invalidInput("release_managed_lifecycle requires boardId.", "boardId");
|
|
349
|
+
}
|
|
211
350
|
const board = await updateBoard(projectRoot, input.boardId, { lifecycle: null });
|
|
212
|
-
|
|
351
|
+
if (!board) throw notFound("Board not found.");
|
|
352
|
+
return okBoard(
|
|
213
353
|
board,
|
|
214
354
|
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
215
|
-
)
|
|
355
|
+
);
|
|
216
356
|
}
|
|
217
357
|
case "duplicate_board": {
|
|
218
|
-
if (!input.boardId)
|
|
358
|
+
if (!input.boardId) throw invalidInput("duplicate_board requires boardId.", "boardId");
|
|
219
359
|
const board = await duplicateBoard(projectRoot, input.boardId, duplicateBoardOptions(input));
|
|
220
|
-
|
|
360
|
+
if (!board) throw notFound("Board not found.");
|
|
361
|
+
return okBoard(board, "Board duplicated.");
|
|
221
362
|
}
|
|
222
363
|
case "delete_board": {
|
|
223
|
-
if (!input.boardId)
|
|
364
|
+
if (!input.boardId) throw invalidInput("delete_board requires boardId.", "boardId");
|
|
224
365
|
const removed = await removeBoard(projectRoot, input.boardId);
|
|
225
|
-
|
|
366
|
+
if (!removed) throw notFound("Board not found.");
|
|
367
|
+
return { ok: true, message: "Board deleted." };
|
|
226
368
|
}
|
|
227
369
|
case "generate_board": {
|
|
228
|
-
if (!input.description)
|
|
370
|
+
if (!input.description) {
|
|
371
|
+
throw invalidInput("generate_board requires description.", "description");
|
|
372
|
+
}
|
|
229
373
|
const boardInput = createBoardFromText({
|
|
230
374
|
description: input.description,
|
|
231
375
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
@@ -241,17 +385,17 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
241
385
|
return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
|
|
242
386
|
}
|
|
243
387
|
case "export_markdown": {
|
|
388
|
+
if (!input.boardId) throw invalidInput("export_markdown requires boardId.", "boardId");
|
|
244
389
|
const board = await requireBoard(projectRoot, input.boardId);
|
|
245
|
-
if (!board)
|
|
390
|
+
if (!board) throw notFound("Board not found.");
|
|
246
391
|
return {
|
|
247
392
|
ok: true,
|
|
248
393
|
message: "Board exported.",
|
|
249
|
-
board,
|
|
250
394
|
markdown: exportBoardAsMarkdown(board)
|
|
251
395
|
};
|
|
252
396
|
}
|
|
253
397
|
case "export_task_graph": {
|
|
254
|
-
if (!input.boardId)
|
|
398
|
+
if (!input.boardId) throw invalidInput("export_task_graph requires boardId.", "boardId");
|
|
255
399
|
const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
|
|
256
400
|
...input.graphId !== void 0 ? { graphId: input.graphId } : {},
|
|
257
401
|
...input.specId !== void 0 ? { specId: input.specId } : {},
|
|
@@ -259,7 +403,7 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
259
403
|
...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
|
|
260
404
|
...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
|
|
261
405
|
});
|
|
262
|
-
if (!exported)
|
|
406
|
+
if (!exported) throw notFound("Board not found.");
|
|
263
407
|
return {
|
|
264
408
|
ok: true,
|
|
265
409
|
message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
|
|
@@ -269,7 +413,7 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
269
413
|
}
|
|
270
414
|
case "sync_task_graph": {
|
|
271
415
|
if (!input.boardId || !input.taskGraph) {
|
|
272
|
-
|
|
416
|
+
throw invalidInput("sync_task_graph requires boardId and taskGraph.");
|
|
273
417
|
}
|
|
274
418
|
const graph = deserializeTaskGraph(input.taskGraph);
|
|
275
419
|
const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
|
|
@@ -283,14 +427,16 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
283
427
|
...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
|
|
284
428
|
...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
|
|
285
429
|
});
|
|
286
|
-
|
|
430
|
+
if (!result) throw notFound("Board not found.");
|
|
431
|
+
return {
|
|
287
432
|
ok: true,
|
|
288
433
|
message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
|
|
289
434
|
board: result.board
|
|
290
|
-
}
|
|
435
|
+
};
|
|
291
436
|
}
|
|
292
437
|
case "create_from_graph": {
|
|
293
|
-
if (!input.taskGraph)
|
|
438
|
+
if (!input.taskGraph)
|
|
439
|
+
throw invalidInput("create_from_graph requires taskGraph.", "taskGraph");
|
|
294
440
|
const graph = deserializeTaskGraph(input.taskGraph);
|
|
295
441
|
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
296
442
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
@@ -309,9 +455,13 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
309
455
|
}
|
|
310
456
|
case "import_session_tasks": {
|
|
311
457
|
const taskPath = ctx.meta?.["task.path"];
|
|
312
|
-
if (!taskPath)
|
|
458
|
+
if (!taskPath) {
|
|
459
|
+
return { ok: true, imported: 0, message: "No session task file for this session." };
|
|
460
|
+
}
|
|
313
461
|
const file = await loadTasks(taskPath);
|
|
314
|
-
if (!file || file.tasks.length === 0)
|
|
462
|
+
if (!file || file.tasks.length === 0) {
|
|
463
|
+
return { ok: true, imported: 0, message: "No session tasks to import." };
|
|
464
|
+
}
|
|
315
465
|
const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
|
|
316
466
|
const graph = deserializeTaskGraph(taskFileToSerializedGraph(file.tasks, sessionId));
|
|
317
467
|
const tags = ["session", `session:${sessionId}`];
|
|
@@ -325,11 +475,17 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
325
475
|
archiveMissingTasks: true,
|
|
326
476
|
includeCompletedTasks: true
|
|
327
477
|
});
|
|
328
|
-
|
|
478
|
+
if (!result) {
|
|
479
|
+
throw conflict("Session board vanished mid-sync; nothing was imported. Retry.", {
|
|
480
|
+
retryable: true
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
329
484
|
ok: true,
|
|
485
|
+
imported: file.tasks.length,
|
|
330
486
|
message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
|
|
331
487
|
board: result.board
|
|
332
|
-
}
|
|
488
|
+
};
|
|
333
489
|
}
|
|
334
490
|
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
335
491
|
title: `Session tasks (${sessionId.slice(0, 8)})`,
|
|
@@ -338,6 +494,7 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
338
494
|
});
|
|
339
495
|
return {
|
|
340
496
|
ok: true,
|
|
497
|
+
imported: file.tasks.length,
|
|
341
498
|
message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
|
|
342
499
|
board
|
|
343
500
|
};
|
|
@@ -383,12 +540,14 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
|
383
540
|
};
|
|
384
541
|
}
|
|
385
542
|
case "events": {
|
|
386
|
-
if (!input.boardId)
|
|
543
|
+
if (!input.boardId) throw invalidInput("events requires boardId.", "boardId");
|
|
387
544
|
const eventList = await listKanbanEvents(projectRoot, input.boardId);
|
|
545
|
+
const limit = typeof input.limit === "number" && Number.isFinite(input.limit) && input.limit > 0 ? Math.floor(input.limit) : void 0;
|
|
546
|
+
const events = limit !== void 0 ? eventList.slice(-limit) : eventList;
|
|
388
547
|
return {
|
|
389
548
|
ok: true,
|
|
390
|
-
message: `${eventList.length} event(s).`,
|
|
391
|
-
events
|
|
549
|
+
message: events.length === eventList.length ? `${eventList.length} event(s).` : `${events.length} most recent of ${eventList.length} event(s).`,
|
|
550
|
+
events
|
|
392
551
|
};
|
|
393
552
|
}
|
|
394
553
|
case "queue_health": {
|
|
@@ -420,11 +579,11 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
420
579
|
const eventContext = { sessionId, ...actor !== void 0 ? { actor } : {} };
|
|
421
580
|
switch (input.action) {
|
|
422
581
|
case "get_contract_graph": {
|
|
423
|
-
if (!input.boardId)
|
|
582
|
+
if (!input.boardId) throw invalidInput("get_contract_graph requires boardId.");
|
|
424
583
|
const found = await getContractGraph(projectRoot, input.boardId);
|
|
425
|
-
if (!found)
|
|
584
|
+
if (!found) throw notFound("Board not found.");
|
|
426
585
|
const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
|
|
427
|
-
if (input.taskId && !evaluated)
|
|
586
|
+
if (input.taskId && !evaluated) throw notFound("Task not found on this board.");
|
|
428
587
|
return {
|
|
429
588
|
ok: true,
|
|
430
589
|
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.",
|
|
@@ -434,7 +593,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
434
593
|
};
|
|
435
594
|
}
|
|
436
595
|
case "configure_contract_graph": {
|
|
437
|
-
if (!input.boardId)
|
|
596
|
+
if (!input.boardId) throw invalidInput("configure_contract_graph requires boardId.");
|
|
438
597
|
const enforcement = input.contractEnforcement ?? "advisory";
|
|
439
598
|
const board = await configureContractGraph(
|
|
440
599
|
projectRoot,
|
|
@@ -442,14 +601,15 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
442
601
|
enforcement,
|
|
443
602
|
eventContext
|
|
444
603
|
);
|
|
445
|
-
|
|
604
|
+
if (!board) throw notFound("Board not found.");
|
|
605
|
+
return okBoard(board, `Contract map enforcement set to ${enforcement}.`);
|
|
446
606
|
}
|
|
447
607
|
case "upsert_contract_node": {
|
|
448
608
|
if (!input.boardId || !input.taskId) {
|
|
449
|
-
|
|
609
|
+
throw invalidInput("upsert_contract_node requires boardId and taskId.");
|
|
450
610
|
}
|
|
451
611
|
if (!input.contractNodeKind || !input.contractNodeTitle) {
|
|
452
|
-
|
|
612
|
+
throw invalidInput("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
|
|
453
613
|
}
|
|
454
614
|
const waiver = input.contractNodeState === "waived" ? {
|
|
455
615
|
actor: actor ?? "agent",
|
|
@@ -457,7 +617,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
457
617
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
458
618
|
} : void 0;
|
|
459
619
|
if (waiver && !waiver.reason.trim()) {
|
|
460
|
-
|
|
620
|
+
throw invalidInput("A waived contract node requires contractWaiverReason.");
|
|
461
621
|
}
|
|
462
622
|
const result = await upsertContractNode(
|
|
463
623
|
projectRoot,
|
|
@@ -477,7 +637,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
477
637
|
},
|
|
478
638
|
eventContext
|
|
479
639
|
);
|
|
480
|
-
if (!result)
|
|
640
|
+
if (!result) throw notFound("Board or task not found.");
|
|
481
641
|
return {
|
|
482
642
|
ok: true,
|
|
483
643
|
message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
|
|
@@ -487,7 +647,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
487
647
|
}
|
|
488
648
|
case "remove_contract_node": {
|
|
489
649
|
if (!input.boardId || !input.contractNodeId) {
|
|
490
|
-
|
|
650
|
+
throw invalidInput("remove_contract_node requires boardId and contractNodeId.");
|
|
491
651
|
}
|
|
492
652
|
const board = await removeContractNode(
|
|
493
653
|
projectRoot,
|
|
@@ -495,13 +655,17 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
495
655
|
input.contractNodeId,
|
|
496
656
|
eventContext
|
|
497
657
|
);
|
|
498
|
-
|
|
658
|
+
if (!board) throw notFound("Contract node not found.");
|
|
659
|
+
return okBoard(board, "Contract node removed, along with every edge that touched it.");
|
|
499
660
|
}
|
|
500
661
|
case "add_contract_edge": {
|
|
501
662
|
if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
|
|
502
|
-
|
|
663
|
+
throw invalidInput(
|
|
664
|
+
"add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo."
|
|
665
|
+
);
|
|
503
666
|
}
|
|
504
|
-
if (!input.contractEdgeType)
|
|
667
|
+
if (!input.contractEdgeType)
|
|
668
|
+
throw invalidInput("add_contract_edge requires contractEdgeType.");
|
|
505
669
|
const result = await addContractEdge(
|
|
506
670
|
projectRoot,
|
|
507
671
|
input.boardId,
|
|
@@ -516,7 +680,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
516
680
|
},
|
|
517
681
|
eventContext
|
|
518
682
|
);
|
|
519
|
-
if (!result)
|
|
683
|
+
if (!result) throw notFound("Board not found.");
|
|
520
684
|
return {
|
|
521
685
|
ok: true,
|
|
522
686
|
message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
|
|
@@ -526,7 +690,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
526
690
|
}
|
|
527
691
|
case "remove_contract_edge": {
|
|
528
692
|
if (!input.boardId || !input.contractEdgeId) {
|
|
529
|
-
|
|
693
|
+
throw invalidInput("remove_contract_edge requires boardId and contractEdgeId.");
|
|
530
694
|
}
|
|
531
695
|
const board = await removeContractEdge(
|
|
532
696
|
projectRoot,
|
|
@@ -534,7 +698,8 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
|
|
|
534
698
|
input.contractEdgeId,
|
|
535
699
|
eventContext
|
|
536
700
|
);
|
|
537
|
-
|
|
701
|
+
if (!board) throw notFound("Contract edge not found.");
|
|
702
|
+
return okBoard(board, "Contract edge removed.");
|
|
538
703
|
}
|
|
539
704
|
default:
|
|
540
705
|
return void 0;
|
|
@@ -581,13 +746,13 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
581
746
|
switch (input.action) {
|
|
582
747
|
case "assess_atomicity": {
|
|
583
748
|
if (!input.boardId || !input.taskId) {
|
|
584
|
-
|
|
749
|
+
throw invalidInput("assess_atomicity requires boardId and taskId.");
|
|
585
750
|
}
|
|
586
751
|
const result = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
|
|
587
752
|
assessedBy: "agent",
|
|
588
753
|
eventContext
|
|
589
754
|
});
|
|
590
|
-
if (!result)
|
|
755
|
+
if (!result) throw notFound("Task not found.");
|
|
591
756
|
const failing = result.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
|
|
592
757
|
const guidance = result.assessment.verdict === "needs_decomposition" ? ` This task should be split before dispatch \u2014 call propose_decomposition with 2+ subtasks (each with one verifiable success criterion). Reasons: ${failing.join(" | ")}` : result.assessment.verdict === "composite" ? " Container task: work happens in its children; it is verified via subtask aggregation." : "";
|
|
593
758
|
return okTask(
|
|
@@ -598,15 +763,16 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
598
763
|
}
|
|
599
764
|
case "propose_decomposition": {
|
|
600
765
|
if (!input.boardId || !input.taskId || !input.subtasks?.length) {
|
|
601
|
-
|
|
766
|
+
throw invalidInput("propose_decomposition requires boardId, taskId, and subtasks (2+).");
|
|
602
767
|
}
|
|
603
768
|
if (input.subtasks.length < 2) {
|
|
604
|
-
|
|
769
|
+
throw invalidInput("propose_decomposition requires at least two subtasks.", "subtasks");
|
|
605
770
|
}
|
|
606
771
|
const invalid = input.subtasks.find(
|
|
607
772
|
(subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
|
|
608
773
|
);
|
|
609
|
-
if (invalid)
|
|
774
|
+
if (invalid)
|
|
775
|
+
throw invalidInput("Every proposed subtask needs a non-blank title.", "subtasks");
|
|
610
776
|
const result = await proposeTaskDecomposition(
|
|
611
777
|
projectRoot,
|
|
612
778
|
input.boardId,
|
|
@@ -618,15 +784,25 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
618
784
|
},
|
|
619
785
|
eventContext
|
|
620
786
|
);
|
|
621
|
-
if (!result)
|
|
787
|
+
if (!result) throw notFound("Task not found.");
|
|
622
788
|
const message = result.proposal.status === "applied" ? `Decomposition applied: ${result.proposal.appliedChildTaskIds?.length ?? 0} child tasks created (parent marked atomic).` : 'Decomposition proposal recorded \u2014 awaiting approval (board policy is "propose"). It can be approved from the WebUI or via update_task.';
|
|
623
789
|
return okTask(result.board, result.task, message);
|
|
624
790
|
}
|
|
625
791
|
case "verify_completion": {
|
|
626
792
|
if (!input.boardId || !input.taskId) {
|
|
627
|
-
|
|
793
|
+
throw invalidInput("verify_completion requires boardId and taskId.");
|
|
794
|
+
}
|
|
795
|
+
let verResult;
|
|
796
|
+
try {
|
|
797
|
+
verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId, {
|
|
798
|
+
persist: false
|
|
799
|
+
});
|
|
800
|
+
} catch (err) {
|
|
801
|
+
if (err instanceof Error && /^(Board|Task) not found\b/.test(err.message)) {
|
|
802
|
+
throw notFound(err.message, { cause: err });
|
|
803
|
+
}
|
|
804
|
+
throw err;
|
|
628
805
|
}
|
|
629
|
-
const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
|
|
630
806
|
const persistedBoard = await updateTask(
|
|
631
807
|
projectRoot,
|
|
632
808
|
input.boardId,
|
|
@@ -638,20 +814,15 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
638
814
|
eventContext
|
|
639
815
|
);
|
|
640
816
|
if (!persistedBoard) {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
board: verResult.board
|
|
646
|
-
};
|
|
817
|
+
throw conflict(
|
|
818
|
+
`Verification ran (verdict: ${verResult.report.verdict}) but its report could not be saved: the task is no longer on the board. Nothing was written; re-run verify_completion.`,
|
|
819
|
+
{ retryable: true }
|
|
820
|
+
);
|
|
647
821
|
}
|
|
648
822
|
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
649
|
-
const freshTask = persistedBoard.tasks?.find((t) => t.id ===
|
|
650
|
-
const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
|
|
823
|
+
const freshTask = persistedBoard.tasks?.find((t) => t.id === verResult.task.id);
|
|
651
824
|
return {
|
|
652
|
-
ok:
|
|
653
|
-
verResult.report.verdict
|
|
654
|
-
),
|
|
825
|
+
ok: true,
|
|
655
826
|
verdict: verResult.report.verdict,
|
|
656
827
|
message: verResult.report.markdownSummary,
|
|
657
828
|
board: persistedBoard,
|
|
@@ -693,7 +864,7 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
693
864
|
}
|
|
694
865
|
case "add_dependency": {
|
|
695
866
|
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
696
|
-
|
|
867
|
+
throw invalidInput("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
697
868
|
}
|
|
698
869
|
const board = await addDependency(
|
|
699
870
|
projectRoot,
|
|
@@ -702,11 +873,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
702
873
|
input.dependencyTaskId,
|
|
703
874
|
eventContext
|
|
704
875
|
);
|
|
705
|
-
|
|
876
|
+
if (!board) throw notFound("Task not found.");
|
|
877
|
+
return okBoard(board, "Dependency added.");
|
|
706
878
|
}
|
|
707
879
|
case "add_goal_metric": {
|
|
708
880
|
if (!input.boardId || !input.taskId || !input.metricName) {
|
|
709
|
-
|
|
881
|
+
throw invalidInput("add_goal_metric requires boardId, taskId, and metricName.");
|
|
710
882
|
}
|
|
711
883
|
const board = await addGoalMetricToTask(
|
|
712
884
|
projectRoot,
|
|
@@ -723,11 +895,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
723
895
|
},
|
|
724
896
|
eventContext
|
|
725
897
|
);
|
|
726
|
-
|
|
898
|
+
if (!board) throw notFound("Task not found.");
|
|
899
|
+
return okBoard(board, "Goal metric added.");
|
|
727
900
|
}
|
|
728
901
|
case "update_goal_metric": {
|
|
729
902
|
if (!input.boardId || !input.taskId || !input.metricId) {
|
|
730
|
-
|
|
903
|
+
throw invalidInput("update_goal_metric requires boardId, taskId, and metricId.");
|
|
731
904
|
}
|
|
732
905
|
const board = await updateGoalMetricOnTask(
|
|
733
906
|
projectRoot,
|
|
@@ -745,11 +918,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
745
918
|
},
|
|
746
919
|
eventContext
|
|
747
920
|
);
|
|
748
|
-
|
|
921
|
+
if (!board) throw notFound("Metric not found.");
|
|
922
|
+
return okBoard(board, "Goal metric updated.");
|
|
749
923
|
}
|
|
750
924
|
case "add_check": {
|
|
751
925
|
if (!input.boardId || !input.taskId || !input.checkDescription) {
|
|
752
|
-
|
|
926
|
+
throw invalidInput("add_check requires boardId, taskId, and checkDescription.");
|
|
753
927
|
}
|
|
754
928
|
const board = await addCheckToTask(
|
|
755
929
|
projectRoot,
|
|
@@ -763,11 +937,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
763
937
|
},
|
|
764
938
|
eventContext
|
|
765
939
|
);
|
|
766
|
-
|
|
940
|
+
if (!board) throw notFound("Task not found.");
|
|
941
|
+
return okBoard(board, "Check added.");
|
|
767
942
|
}
|
|
768
943
|
case "update_check": {
|
|
769
944
|
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
770
|
-
|
|
945
|
+
throw invalidInput("update_check requires boardId, taskId, and checkId.");
|
|
771
946
|
}
|
|
772
947
|
const board = await updateCheckOnTask(
|
|
773
948
|
projectRoot,
|
|
@@ -784,11 +959,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
784
959
|
},
|
|
785
960
|
eventContext
|
|
786
961
|
);
|
|
787
|
-
|
|
962
|
+
if (!board) throw notFound("Check not found.");
|
|
963
|
+
return okBoard(board, "Check updated.");
|
|
788
964
|
}
|
|
789
965
|
case "remove_check": {
|
|
790
966
|
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
791
|
-
|
|
967
|
+
throw invalidInput("remove_check requires boardId, taskId, and checkId.");
|
|
792
968
|
}
|
|
793
969
|
const board = await removeCheckFromTask(
|
|
794
970
|
projectRoot,
|
|
@@ -797,11 +973,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
797
973
|
input.checkId,
|
|
798
974
|
eventContext
|
|
799
975
|
);
|
|
800
|
-
|
|
976
|
+
if (!board) throw notFound("Check not found on this task.");
|
|
977
|
+
return okBoard(board, "Acceptance criterion removed.");
|
|
801
978
|
}
|
|
802
979
|
case "add_note": {
|
|
803
980
|
if (!input.boardId || !input.taskId || !input.note)
|
|
804
|
-
|
|
981
|
+
throw invalidInput("add_note requires boardId, taskId, and note.");
|
|
805
982
|
const board = await addNoteToTask(
|
|
806
983
|
projectRoot,
|
|
807
984
|
input.boardId,
|
|
@@ -812,11 +989,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
812
989
|
},
|
|
813
990
|
eventContext
|
|
814
991
|
);
|
|
815
|
-
|
|
992
|
+
if (!board) throw notFound("Task not found.");
|
|
993
|
+
return okBoard(board, "Note added.");
|
|
816
994
|
}
|
|
817
995
|
case "add_link": {
|
|
818
996
|
if (!input.boardId || !input.taskId || !input.url)
|
|
819
|
-
|
|
997
|
+
throw invalidInput("add_link requires boardId, taskId, and url.");
|
|
820
998
|
const board = await addLinkToTask(
|
|
821
999
|
projectRoot,
|
|
822
1000
|
input.boardId,
|
|
@@ -828,11 +1006,14 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
|
|
|
828
1006
|
},
|
|
829
1007
|
eventContext
|
|
830
1008
|
);
|
|
831
|
-
|
|
1009
|
+
if (!board) throw notFound("Task not found.");
|
|
1010
|
+
return okBoard(board, "Link added.");
|
|
832
1011
|
}
|
|
833
1012
|
case "split_atomic": {
|
|
834
1013
|
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
835
|
-
|
|
1014
|
+
throw invalidInput(
|
|
1015
|
+
"split_atomic requires boardId, taskId, and childTitles (at least one)."
|
|
1016
|
+
);
|
|
836
1017
|
}
|
|
837
1018
|
return handleSplitTask(projectRoot, input, { atomic: true }, eventContext);
|
|
838
1019
|
}
|
|
@@ -862,7 +1043,7 @@ import {
|
|
|
862
1043
|
repairManagedTaskProjection,
|
|
863
1044
|
resolveAutoAccept,
|
|
864
1045
|
setTaskChain,
|
|
865
|
-
stripLifecycleIssues,
|
|
1046
|
+
stripLifecycleIssues as stripLifecycleIssues2,
|
|
866
1047
|
transferTaskToBoard,
|
|
867
1048
|
transitionTask,
|
|
868
1049
|
updateTask as updateTask2,
|
|
@@ -898,7 +1079,12 @@ function taskInput(input) {
|
|
|
898
1079
|
// neither the create input nor the patch, so the instruction described a
|
|
899
1080
|
// capability that did not exist and the attempt was silently dropped.
|
|
900
1081
|
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
901
|
-
|
|
1082
|
+
// `childTitles` are TITLES of children to create, never ids. They used to
|
|
1083
|
+
// be written straight into `childTaskIds`, recording dangling "ids" such as
|
|
1084
|
+
// "Child 1". add_task creates those children after the parent exists (via
|
|
1085
|
+
// splitTask, the one path that creates child cards); explicit ids of
|
|
1086
|
+
// existing children go through `childTaskIds`.
|
|
1087
|
+
...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
|
|
902
1088
|
...input.checkDescription !== void 0 ? {
|
|
903
1089
|
successCriteria: [
|
|
904
1090
|
{
|
|
@@ -1053,6 +1239,37 @@ async function syncContextTask(ctx, task, options = {}) {
|
|
|
1053
1239
|
} catch {
|
|
1054
1240
|
}
|
|
1055
1241
|
}
|
|
1242
|
+
async function rollbackStartedAssignment(projectRoot, boardId, taskId, prior, leaseId, eventContext) {
|
|
1243
|
+
try {
|
|
1244
|
+
const restored = prior ? await updateTaskAssignment(projectRoot, boardId, taskId, prior, {
|
|
1245
|
+
...eventContext,
|
|
1246
|
+
expectedLeaseId: leaseId
|
|
1247
|
+
}) : await releaseTaskClaim(
|
|
1248
|
+
projectRoot,
|
|
1249
|
+
boardId,
|
|
1250
|
+
taskId,
|
|
1251
|
+
{ clearAssignee: false, expectedLeaseId: leaseId },
|
|
1252
|
+
eventContext
|
|
1253
|
+
);
|
|
1254
|
+
if (restored) return void 0;
|
|
1255
|
+
return `the running assignment (lease ${leaseId}) could not be rolled back \u2014 the lease changed or the task is gone; release it with release_task.`;
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
return `the running assignment (lease ${leaseId}) could not be rolled back (${err instanceof Error ? err.message : String(err)}); release it with release_task.`;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function joinCommitted(...notes) {
|
|
1261
|
+
const present = notes.filter((note) => Boolean(note));
|
|
1262
|
+
return present.length ? present.join(" ") : void 0;
|
|
1263
|
+
}
|
|
1264
|
+
async function missingOrFenced(projectRoot, boardId, taskId, expectedLeaseId, what) {
|
|
1265
|
+
if (expectedLeaseId !== void 0 && await getTask(projectRoot, boardId, taskId)) {
|
|
1266
|
+
throw conflict(
|
|
1267
|
+
`${what}: the task's current lease no longer matches expectedLeaseId "${expectedLeaseId}" (it was recovered or reassigned). Nothing was written.`,
|
|
1268
|
+
{ retryable: false }
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
throw notFound(`${what}: task not found.`);
|
|
1272
|
+
}
|
|
1056
1273
|
async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
1057
1274
|
const eventContext = {
|
|
1058
1275
|
sessionId: ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session",
|
|
@@ -1060,21 +1277,57 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1060
1277
|
};
|
|
1061
1278
|
switch (input.action) {
|
|
1062
1279
|
case "add_task": {
|
|
1063
|
-
if (!input.boardId || !input.title)
|
|
1280
|
+
if (!input.boardId || !input.title)
|
|
1281
|
+
throw invalidInput("add_task requires boardId and title.");
|
|
1282
|
+
const childTitles = input.childTitles;
|
|
1283
|
+
if (childTitles !== void 0 && (!Array.isArray(childTitles) || childTitles.some((title) => typeof title !== "string" || !title.trim()))) {
|
|
1284
|
+
throw invalidInput("add_task childTitles must be non-blank strings.", "childTitles");
|
|
1285
|
+
}
|
|
1064
1286
|
const result = await addTask2(projectRoot, input.boardId, taskInput(input), eventContext);
|
|
1065
|
-
if (!result)
|
|
1287
|
+
if (!result) throw notFound("Board not found.");
|
|
1288
|
+
if (childTitles?.length) {
|
|
1289
|
+
let split;
|
|
1290
|
+
try {
|
|
1291
|
+
split = await handleSplitTask(
|
|
1292
|
+
projectRoot,
|
|
1293
|
+
{ action: "split_task", boardId: result.board.id, taskId: result.task.id, childTitles },
|
|
1294
|
+
{},
|
|
1295
|
+
eventContext
|
|
1296
|
+
);
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
const removed = await removeTask(
|
|
1299
|
+
projectRoot,
|
|
1300
|
+
result.board.id,
|
|
1301
|
+
result.task.id,
|
|
1302
|
+
eventContext
|
|
1303
|
+
).catch(() => null);
|
|
1304
|
+
throw toKanbanToolError(
|
|
1305
|
+
err,
|
|
1306
|
+
removed ? void 0 : `task ${result.task.id} was created but its children were not, and removing it failed; delete it with delete_task.`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
const parent = split.task ?? result.task;
|
|
1310
|
+
await syncContextTask(ctx, parent);
|
|
1311
|
+
return {
|
|
1312
|
+
ok: true,
|
|
1313
|
+
message: `Task added with ${split.children?.length ?? 0} child task(s).${atomicityNudge(parent)}`,
|
|
1314
|
+
board: split.board ?? result.board,
|
|
1315
|
+
task: parent,
|
|
1316
|
+
children: split.children
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1066
1319
|
await syncContextTask(ctx, result.task);
|
|
1067
1320
|
return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
|
|
1068
1321
|
}
|
|
1069
1322
|
case "split_task": {
|
|
1070
1323
|
if (!input.boardId || !input.taskId || !input.childTitles?.length) {
|
|
1071
|
-
|
|
1324
|
+
throw invalidInput("split_task requires boardId, taskId, and childTitles.");
|
|
1072
1325
|
}
|
|
1073
1326
|
return handleSplitTask(projectRoot, input, {}, eventContext);
|
|
1074
1327
|
}
|
|
1075
1328
|
case "merge_tasks": {
|
|
1076
1329
|
if (!input.boardId || !input.taskIds?.length || !input.title) {
|
|
1077
|
-
|
|
1330
|
+
throw invalidInput("merge_tasks requires boardId, taskIds, and title.");
|
|
1078
1331
|
}
|
|
1079
1332
|
const result = await mergeTasks(
|
|
1080
1333
|
projectRoot,
|
|
@@ -1089,11 +1342,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1089
1342
|
},
|
|
1090
1343
|
eventContext
|
|
1091
1344
|
);
|
|
1092
|
-
|
|
1345
|
+
if (!result) throw notFound("Board or task not found.");
|
|
1346
|
+
return okTask(result.board, result.task, "Tasks merged.");
|
|
1093
1347
|
}
|
|
1094
1348
|
case "copy_task": {
|
|
1095
1349
|
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1096
|
-
|
|
1350
|
+
throw invalidInput("copy_task requires boardId, taskId, and targetBoardId.");
|
|
1097
1351
|
}
|
|
1098
1352
|
const result = await copyTaskToBoard(
|
|
1099
1353
|
projectRoot,
|
|
@@ -1108,11 +1362,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1108
1362
|
eventContext
|
|
1109
1363
|
}
|
|
1110
1364
|
);
|
|
1111
|
-
|
|
1365
|
+
if (!result) throw notFound("Board or task not found.");
|
|
1366
|
+
return okTask(result.targetBoard, result.task, "Task copied to target board.");
|
|
1112
1367
|
}
|
|
1113
1368
|
case "transfer_task": {
|
|
1114
1369
|
if (!input.boardId || !input.taskId || !input.targetBoardId) {
|
|
1115
|
-
|
|
1370
|
+
throw invalidInput("transfer_task requires boardId, taskId, and targetBoardId.");
|
|
1116
1371
|
}
|
|
1117
1372
|
const result = await transferTaskToBoard(
|
|
1118
1373
|
projectRoot,
|
|
@@ -1127,23 +1382,27 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1127
1382
|
eventContext
|
|
1128
1383
|
}
|
|
1129
1384
|
);
|
|
1130
|
-
|
|
1385
|
+
if (!result) throw notFound("Board or task not found.");
|
|
1386
|
+
return okTask(result.targetBoard, result.task, "Task transferred to target board.");
|
|
1131
1387
|
}
|
|
1132
1388
|
case "get_task": {
|
|
1133
|
-
if (!input.boardId || !input.taskId)
|
|
1389
|
+
if (!input.boardId || !input.taskId)
|
|
1390
|
+
throw invalidInput("get_task requires boardId and taskId.");
|
|
1134
1391
|
const task = await getTask(projectRoot, input.boardId, input.taskId);
|
|
1135
|
-
|
|
1392
|
+
if (!task) throw notFound("Task not found.");
|
|
1393
|
+
return { ok: true, message: "Task loaded.", task };
|
|
1136
1394
|
}
|
|
1137
1395
|
case "start_task": {
|
|
1138
1396
|
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1139
|
-
|
|
1397
|
+
throw invalidInput("start_task requires boardId, taskId, author, and transitionComment.");
|
|
1140
1398
|
}
|
|
1141
1399
|
let board = await getBoard3(projectRoot, input.boardId);
|
|
1142
|
-
|
|
1143
|
-
|
|
1400
|
+
if (!board) throw notFound("Board not found.");
|
|
1401
|
+
let task = resolveTaskRef(board, input.taskId);
|
|
1402
|
+
if (!task) throw notFound("Task not found.");
|
|
1144
1403
|
const readiness = evaluateContractGraphReadiness(board, task.id);
|
|
1145
1404
|
if (!readiness.ready) {
|
|
1146
|
-
|
|
1405
|
+
throw refused(
|
|
1147
1406
|
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1148
1407
|
);
|
|
1149
1408
|
}
|
|
@@ -1165,7 +1424,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1165
1424
|
},
|
|
1166
1425
|
eventContext
|
|
1167
1426
|
);
|
|
1168
|
-
if (!assigned)
|
|
1427
|
+
if (!assigned) throw notFound("Task assignment could not be started: task not found.");
|
|
1169
1428
|
const started = await updateTask2(
|
|
1170
1429
|
projectRoot,
|
|
1171
1430
|
board.id,
|
|
@@ -1184,6 +1443,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1184
1443
|
);
|
|
1185
1444
|
}
|
|
1186
1445
|
let stage = task.lifecycle?.currentStage;
|
|
1446
|
+
let movedToTodo = false;
|
|
1187
1447
|
if (stage === "backlog") {
|
|
1188
1448
|
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
1189
1449
|
to: "todo",
|
|
@@ -1191,7 +1451,8 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1191
1451
|
actor: input.author,
|
|
1192
1452
|
comment: input.transitionComment
|
|
1193
1453
|
});
|
|
1194
|
-
if (!moved)
|
|
1454
|
+
if (!moved) throw notFound("Task could not enter Todo: board or task not found.");
|
|
1455
|
+
movedToTodo = true;
|
|
1195
1456
|
board = moved.board;
|
|
1196
1457
|
task = moved.task;
|
|
1197
1458
|
stage = task.lifecycle?.currentStage;
|
|
@@ -1199,6 +1460,8 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1199
1460
|
if (stage === "todo" || stage === "review") {
|
|
1200
1461
|
const now = /* @__PURE__ */ new Date();
|
|
1201
1462
|
const leaseId = input.leaseId ?? randomUUID2();
|
|
1463
|
+
const priorAssignment = task.assignment ? { ...task.assignment } : void 0;
|
|
1464
|
+
const todoNote = movedToTodo ? "the card was moved Backlog \u2192 Todo." : void 0;
|
|
1202
1465
|
const assigned = await updateTaskAssignment(
|
|
1203
1466
|
projectRoot,
|
|
1204
1467
|
board.id,
|
|
@@ -1215,24 +1478,56 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1215
1478
|
},
|
|
1216
1479
|
eventContext
|
|
1217
1480
|
);
|
|
1218
|
-
if (!assigned)
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1481
|
+
if (!assigned) {
|
|
1482
|
+
throw notFound("Task assignment could not be started: task not found.", {
|
|
1483
|
+
committed: todoNote
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
const startedTaskId = task.id;
|
|
1487
|
+
const startedBoardId = board.id;
|
|
1488
|
+
let moved;
|
|
1489
|
+
try {
|
|
1490
|
+
moved = await transitionTask(projectRoot, startedBoardId, startedTaskId, {
|
|
1491
|
+
to: "running",
|
|
1492
|
+
sessionId: eventContext.sessionId,
|
|
1493
|
+
actor: input.author,
|
|
1494
|
+
comment: input.transitionComment
|
|
1495
|
+
});
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
const rollback = await rollbackStartedAssignment(
|
|
1498
|
+
projectRoot,
|
|
1499
|
+
startedBoardId,
|
|
1500
|
+
startedTaskId,
|
|
1501
|
+
priorAssignment,
|
|
1502
|
+
leaseId,
|
|
1503
|
+
eventContext
|
|
1504
|
+
);
|
|
1505
|
+
throw toKanbanToolError(err, joinCommitted(todoNote, rollback));
|
|
1506
|
+
}
|
|
1507
|
+
if (!moved) {
|
|
1508
|
+
const rollback = await rollbackStartedAssignment(
|
|
1509
|
+
projectRoot,
|
|
1510
|
+
startedBoardId,
|
|
1511
|
+
startedTaskId,
|
|
1512
|
+
priorAssignment,
|
|
1513
|
+
leaseId,
|
|
1514
|
+
eventContext
|
|
1515
|
+
);
|
|
1516
|
+
throw conflict("Task could not enter Running: the board or task disappeared.", {
|
|
1517
|
+
committed: joinCommitted(todoNote, rollback)
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1226
1520
|
board = moved.board;
|
|
1227
1521
|
task = moved.task;
|
|
1228
1522
|
stage = task.lifecycle?.currentStage;
|
|
1229
1523
|
}
|
|
1230
1524
|
if (stage !== "running" || task.assignment?.status !== "running") {
|
|
1231
|
-
|
|
1232
|
-
`start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"})
|
|
1525
|
+
throw refused(
|
|
1526
|
+
`start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`,
|
|
1527
|
+
{ committed: movedToTodo ? "the card was moved Backlog \u2192 Todo." : void 0 }
|
|
1233
1528
|
);
|
|
1234
1529
|
}
|
|
1235
|
-
ctx.setCurrentKanbanTask(task.id, board.id);
|
|
1530
|
+
ctx.setCurrentKanbanTask?.(task.id, board.id);
|
|
1236
1531
|
await syncContextTask(ctx, task);
|
|
1237
1532
|
return okTask(
|
|
1238
1533
|
board,
|
|
@@ -1241,7 +1536,8 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1241
1536
|
);
|
|
1242
1537
|
}
|
|
1243
1538
|
case "update_task": {
|
|
1244
|
-
if (!input.boardId || !input.taskId)
|
|
1539
|
+
if (!input.boardId || !input.taskId)
|
|
1540
|
+
throw invalidInput("update_task requires boardId and taskId.");
|
|
1245
1541
|
const board = await updateTask2(
|
|
1246
1542
|
projectRoot,
|
|
1247
1543
|
input.boardId,
|
|
@@ -1249,20 +1545,23 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1249
1545
|
taskPatch(input),
|
|
1250
1546
|
eventContext
|
|
1251
1547
|
);
|
|
1252
|
-
if (board)
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
board.tasks.find((t) => t.id === input.taskId)
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
return board ? okBoard(board, "Task updated.") : fail("Task not found.");
|
|
1548
|
+
if (!board) throw notFound("Task not found.");
|
|
1549
|
+
await syncContextTask(ctx, resolveTaskRef(board, input.taskId));
|
|
1550
|
+
return okBoard(board, "Task updated.");
|
|
1259
1551
|
}
|
|
1260
1552
|
case "transition_task": {
|
|
1261
1553
|
if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
|
|
1262
|
-
|
|
1554
|
+
throw invalidInput(
|
|
1263
1555
|
"transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
|
|
1264
1556
|
);
|
|
1265
1557
|
}
|
|
1558
|
+
if (input.tickChecks?.length && input.lifecycleStage !== "done") {
|
|
1559
|
+
throw invalidInput(
|
|
1560
|
+
`tickChecks only applies to transition_task with lifecycleStage "done" (got "${input.lifecycleStage}"). Tick criteria with update_check instead.`,
|
|
1561
|
+
"tickChecks"
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
let preGateSaved = false;
|
|
1266
1565
|
if (input.lifecycleStage === "done") {
|
|
1267
1566
|
const boardBefore = await getBoard3(projectRoot, input.boardId);
|
|
1268
1567
|
const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
|
|
@@ -1270,45 +1569,52 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1270
1569
|
const preGate = await verifyTaskCompletion2(projectRoot, input.boardId, taskBefore.id, {
|
|
1271
1570
|
persist: false
|
|
1272
1571
|
});
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1572
|
+
preGateSaved = Boolean(
|
|
1573
|
+
await updateTask2(
|
|
1574
|
+
projectRoot,
|
|
1575
|
+
input.boardId,
|
|
1576
|
+
taskBefore.id,
|
|
1577
|
+
{
|
|
1578
|
+
verificationReport: preGate.report,
|
|
1579
|
+
successCriteria: preGate.task.successCriteria
|
|
1580
|
+
},
|
|
1581
|
+
eventContext
|
|
1582
|
+
)
|
|
1282
1583
|
);
|
|
1283
1584
|
}
|
|
1284
1585
|
}
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1586
|
+
const preGateNote = preGateSaved ? "the pre-gate verification report was saved on the card (evidence only; the card did not move)." : void 0;
|
|
1587
|
+
let result;
|
|
1588
|
+
try {
|
|
1589
|
+
result = await transitionTask(projectRoot, input.boardId, input.taskId, {
|
|
1590
|
+
to: input.lifecycleStage,
|
|
1591
|
+
sessionId: eventContext.sessionId,
|
|
1592
|
+
actor: input.author,
|
|
1593
|
+
comment: input.transitionComment,
|
|
1594
|
+
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
1595
|
+
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
1596
|
+
...input.attachmentUrl !== void 0 ? {
|
|
1597
|
+
attachment: {
|
|
1598
|
+
url: input.attachmentUrl,
|
|
1599
|
+
type: input.attachmentType ?? "url",
|
|
1600
|
+
...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
|
|
1601
|
+
}
|
|
1602
|
+
} : {},
|
|
1603
|
+
patch: taskPatch(input)
|
|
1604
|
+
});
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
throw toKanbanToolError(err, preGateNote);
|
|
1303
1607
|
}
|
|
1304
|
-
if (result
|
|
1305
|
-
|
|
1608
|
+
if (!result) throw notFound("Board or task not found.", { committed: preGateNote });
|
|
1609
|
+
if (input.lifecycleStage === "done" && result.task.verificationReport) {
|
|
1610
|
+
recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
|
|
1306
1611
|
}
|
|
1307
|
-
|
|
1612
|
+
await syncContextTask(ctx, result.task);
|
|
1613
|
+
return okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`);
|
|
1308
1614
|
}
|
|
1309
1615
|
case "repair_managed_projection": {
|
|
1310
1616
|
if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
|
|
1311
|
-
|
|
1617
|
+
throw invalidInput(
|
|
1312
1618
|
"repair_managed_projection requires boardId, taskId, author, and transitionComment."
|
|
1313
1619
|
);
|
|
1314
1620
|
}
|
|
@@ -1319,15 +1625,16 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1319
1625
|
if (result?.task) {
|
|
1320
1626
|
await syncContextTask(ctx, result.task);
|
|
1321
1627
|
}
|
|
1322
|
-
|
|
1628
|
+
if (!result) throw notFound("Board or task not found.");
|
|
1629
|
+
return okTask(
|
|
1323
1630
|
result.board,
|
|
1324
1631
|
result.task,
|
|
1325
1632
|
"Managed card projection repaired from lifecycle history."
|
|
1326
|
-
)
|
|
1633
|
+
);
|
|
1327
1634
|
}
|
|
1328
1635
|
case "move_task": {
|
|
1329
1636
|
if (!input.boardId || !input.taskId || !input.targetColumnId) {
|
|
1330
|
-
|
|
1637
|
+
throw invalidInput("move_task requires boardId, taskId, and targetColumnId.");
|
|
1331
1638
|
}
|
|
1332
1639
|
const board = await moveTask(
|
|
1333
1640
|
projectRoot,
|
|
@@ -1337,30 +1644,28 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1337
1644
|
input.order,
|
|
1338
1645
|
eventContext
|
|
1339
1646
|
);
|
|
1340
|
-
if (board)
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
board.tasks.find((t) => t.id === input.taskId)
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
return board ? okBoard(board, "Task moved.") : fail("Move failed.");
|
|
1647
|
+
if (!board) throw notFound("Move failed: board or task not found.");
|
|
1648
|
+
await syncContextTask(ctx, resolveTaskRef(board, input.taskId));
|
|
1649
|
+
return okBoard(board, "Task moved.");
|
|
1347
1650
|
}
|
|
1348
1651
|
case "delete_task": {
|
|
1349
|
-
if (!input.boardId || !input.taskId)
|
|
1652
|
+
if (!input.boardId || !input.taskId)
|
|
1653
|
+
throw invalidInput("delete_task requires boardId and taskId.");
|
|
1350
1654
|
const boardBefore = await getBoard3(projectRoot, input.boardId);
|
|
1351
|
-
const taskToDelete = boardBefore
|
|
1655
|
+
const taskToDelete = boardBefore ? resolveTaskRef(boardBefore, input.taskId) : void 0;
|
|
1352
1656
|
const board = await removeTask(projectRoot, input.boardId, input.taskId, eventContext);
|
|
1353
|
-
if (board && ctx.currentKanbanTaskId ===
|
|
1657
|
+
if (board && taskToDelete && ctx.currentKanbanTaskId === taskToDelete.id) {
|
|
1354
1658
|
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
1355
1659
|
}
|
|
1356
1660
|
if (board && taskToDelete) {
|
|
1357
1661
|
await syncContextTask(ctx, taskToDelete, { remove: true });
|
|
1358
1662
|
}
|
|
1359
|
-
|
|
1663
|
+
if (!board) throw notFound("Task not found.");
|
|
1664
|
+
return okBoard(board, "Task deleted.");
|
|
1360
1665
|
}
|
|
1361
1666
|
case "set_chain": {
|
|
1362
1667
|
if (!input.boardId || !input.taskIds?.length) {
|
|
1363
|
-
|
|
1668
|
+
throw invalidInput("set_chain requires boardId and taskIds.");
|
|
1364
1669
|
}
|
|
1365
1670
|
const result = await setTaskChain(
|
|
1366
1671
|
projectRoot,
|
|
@@ -1372,28 +1677,30 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1372
1677
|
},
|
|
1373
1678
|
eventContext
|
|
1374
1679
|
);
|
|
1375
|
-
|
|
1680
|
+
if (!result) throw notFound("Board or task not found.");
|
|
1681
|
+
return {
|
|
1376
1682
|
ok: true,
|
|
1377
1683
|
message: `Chain set: ${result.chainId}`,
|
|
1378
1684
|
board: result.board,
|
|
1379
1685
|
chain: result.tasks
|
|
1380
|
-
}
|
|
1686
|
+
};
|
|
1381
1687
|
}
|
|
1382
1688
|
case "get_chain": {
|
|
1383
1689
|
if (!input.boardId || !(input.taskId || input.chainId)) {
|
|
1384
|
-
|
|
1690
|
+
throw invalidInput("get_chain requires boardId and taskId or chainId.");
|
|
1385
1691
|
}
|
|
1386
1692
|
const result = await getTaskChain(
|
|
1387
1693
|
projectRoot,
|
|
1388
1694
|
input.boardId,
|
|
1389
1695
|
input.taskId ?? input.chainId ?? ""
|
|
1390
1696
|
);
|
|
1391
|
-
|
|
1697
|
+
if (!result) throw notFound("Chain not found.");
|
|
1698
|
+
return {
|
|
1392
1699
|
ok: true,
|
|
1393
1700
|
message: `Chain loaded: ${result.chainId}`,
|
|
1394
1701
|
board: result.board,
|
|
1395
1702
|
chain: result.tasks
|
|
1396
|
-
}
|
|
1703
|
+
};
|
|
1397
1704
|
}
|
|
1398
1705
|
case "claim_task": {
|
|
1399
1706
|
const result = await claimReadyTask(
|
|
@@ -1406,11 +1713,14 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1406
1713
|
},
|
|
1407
1714
|
eventContext
|
|
1408
1715
|
);
|
|
1409
|
-
|
|
1716
|
+
if (!result) {
|
|
1717
|
+
return { ok: true, claimed: false, message: "No ready kanban task matched the claim." };
|
|
1718
|
+
}
|
|
1719
|
+
return { ...okTask(result.board, result.task, "Task claimed."), claimed: true };
|
|
1410
1720
|
}
|
|
1411
1721
|
case "release_task": {
|
|
1412
1722
|
if (!input.boardId || !input.taskId) {
|
|
1413
|
-
|
|
1723
|
+
throw invalidInput("release_task requires boardId and taskId.");
|
|
1414
1724
|
}
|
|
1415
1725
|
const board = await releaseTaskClaim(
|
|
1416
1726
|
projectRoot,
|
|
@@ -1423,10 +1733,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1423
1733
|
},
|
|
1424
1734
|
eventContext
|
|
1425
1735
|
);
|
|
1426
|
-
|
|
1736
|
+
if (!board) throw notFound("Task not found.");
|
|
1737
|
+
return okBoard(board, "Task claim released.");
|
|
1427
1738
|
}
|
|
1428
1739
|
case "assign_task": {
|
|
1429
|
-
if (!input.boardId || !input.taskId)
|
|
1740
|
+
if (!input.boardId || !input.taskId)
|
|
1741
|
+
throw invalidInput("assign_task requires boardId and taskId.");
|
|
1430
1742
|
const board = await assignTask(
|
|
1431
1743
|
projectRoot,
|
|
1432
1744
|
input.boardId,
|
|
@@ -1434,11 +1746,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1434
1746
|
assignmentInput(input),
|
|
1435
1747
|
eventContext
|
|
1436
1748
|
);
|
|
1437
|
-
|
|
1749
|
+
if (!board) throw notFound("Task not found.");
|
|
1750
|
+
return okBoard(board, "Task assigned.");
|
|
1438
1751
|
}
|
|
1439
1752
|
case "mark_assignment": {
|
|
1440
1753
|
if (!input.boardId || !input.taskId)
|
|
1441
|
-
|
|
1754
|
+
throw invalidInput("mark_assignment requires boardId and taskId.");
|
|
1442
1755
|
const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
|
|
1443
1756
|
const board = await updateTaskAssignment(
|
|
1444
1757
|
projectRoot,
|
|
@@ -1463,7 +1776,15 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1463
1776
|
...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
|
|
1464
1777
|
}
|
|
1465
1778
|
);
|
|
1466
|
-
if (!board)
|
|
1779
|
+
if (!board) {
|
|
1780
|
+
return missingOrFenced(
|
|
1781
|
+
projectRoot,
|
|
1782
|
+
input.boardId,
|
|
1783
|
+
input.taskId,
|
|
1784
|
+
input.expectedLeaseId,
|
|
1785
|
+
"Assignment not updated"
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1467
1788
|
if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
|
|
1468
1789
|
const envGate = readEnvGateEnforcement();
|
|
1469
1790
|
const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
|
|
@@ -1503,7 +1824,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1503
1824
|
});
|
|
1504
1825
|
} catch (err) {
|
|
1505
1826
|
lifecycleWarnings.push(
|
|
1506
|
-
`Lifecycle transition to Running deferred: ${
|
|
1827
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues2(err instanceof Error ? err.message : String(err))}`
|
|
1507
1828
|
);
|
|
1508
1829
|
}
|
|
1509
1830
|
}
|
|
@@ -1526,14 +1847,16 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1526
1847
|
});
|
|
1527
1848
|
} catch (err) {
|
|
1528
1849
|
lifecycleWarnings.push(
|
|
1529
|
-
`Lifecycle transition to Review failed: ${
|
|
1850
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues2(err instanceof Error ? err.message : String(err))}`
|
|
1530
1851
|
);
|
|
1531
1852
|
}
|
|
1532
1853
|
if (transitionResult) {
|
|
1533
1854
|
const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
|
|
1534
1855
|
if (hasCriteria) {
|
|
1535
1856
|
try {
|
|
1536
|
-
const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId
|
|
1857
|
+
const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId, {
|
|
1858
|
+
persist: false
|
|
1859
|
+
});
|
|
1537
1860
|
if (verResult.report) {
|
|
1538
1861
|
recordKanbanVerificationEvidence(ctx, verResult.report);
|
|
1539
1862
|
}
|
|
@@ -1603,7 +1926,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1603
1926
|
}
|
|
1604
1927
|
case "heartbeat_assignment": {
|
|
1605
1928
|
if (!input.boardId || !input.taskId) {
|
|
1606
|
-
|
|
1929
|
+
throw invalidInput("heartbeat_assignment requires boardId and taskId.");
|
|
1607
1930
|
}
|
|
1608
1931
|
const board = await heartbeatTaskAssignment(
|
|
1609
1932
|
projectRoot,
|
|
@@ -1616,10 +1939,19 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1616
1939
|
},
|
|
1617
1940
|
eventContext
|
|
1618
1941
|
);
|
|
1619
|
-
|
|
1942
|
+
if (!board) {
|
|
1943
|
+
return missingOrFenced(
|
|
1944
|
+
projectRoot,
|
|
1945
|
+
input.boardId,
|
|
1946
|
+
input.taskId,
|
|
1947
|
+
input.expectedLeaseId,
|
|
1948
|
+
"Heartbeat not recorded"
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
return okBoard(board, "Assignment heartbeat updated.");
|
|
1620
1952
|
}
|
|
1621
1953
|
case "recover_stale": {
|
|
1622
|
-
if (!input.boardId)
|
|
1954
|
+
if (!input.boardId) throw invalidInput("recover_stale requires boardId.");
|
|
1623
1955
|
const policyFields = [
|
|
1624
1956
|
input.recoveryPolicyFailOnCostCeiling !== void 0,
|
|
1625
1957
|
input.recoveryPolicyReleaseOnFailureKinds !== void 0,
|
|
@@ -1651,12 +1983,16 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1651
1983
|
},
|
|
1652
1984
|
eventContext
|
|
1653
1985
|
);
|
|
1654
|
-
|
|
1986
|
+
if (!result) {
|
|
1987
|
+
if (!await getBoard3(projectRoot, input.boardId)) throw notFound("Board not found.");
|
|
1988
|
+
return { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
|
|
1989
|
+
}
|
|
1990
|
+
return {
|
|
1655
1991
|
ok: true,
|
|
1656
1992
|
message: `Recovered ${result.tasks.length} stale assignment(s).`,
|
|
1657
1993
|
board: result.board,
|
|
1658
1994
|
recoveredTasks: result.tasks
|
|
1659
|
-
}
|
|
1995
|
+
};
|
|
1660
1996
|
}
|
|
1661
1997
|
default:
|
|
1662
1998
|
return void 0;
|
|
@@ -1665,8 +2001,29 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1665
2001
|
|
|
1666
2002
|
// src/kanban-presence.ts
|
|
1667
2003
|
import { touchKanbanPresence } from "@wrongstack/kanban";
|
|
2004
|
+
|
|
2005
|
+
// src/kanban-tool-types.ts
|
|
2006
|
+
var KANBAN_READ_ONLY_ACTIONS = [
|
|
2007
|
+
"list_boards",
|
|
2008
|
+
"get_board",
|
|
2009
|
+
"export_markdown",
|
|
2010
|
+
"export_task_graph",
|
|
2011
|
+
"search_tasks",
|
|
2012
|
+
"ready_tasks",
|
|
2013
|
+
"snapshot",
|
|
2014
|
+
"workbench",
|
|
2015
|
+
"get_task",
|
|
2016
|
+
"get_chain",
|
|
2017
|
+
"events",
|
|
2018
|
+
"queue_health",
|
|
2019
|
+
"get_contract_graph"
|
|
2020
|
+
];
|
|
2021
|
+
|
|
2022
|
+
// src/kanban-presence.ts
|
|
2023
|
+
var READ_ONLY = new Set(KANBAN_READ_ONLY_ACTIONS);
|
|
1668
2024
|
function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
1669
2025
|
return async (result) => {
|
|
2026
|
+
if (READ_ONLY.has(input.action)) return result;
|
|
1670
2027
|
const boardId = result.board?.id ?? input.boardId;
|
|
1671
2028
|
if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
|
|
1672
2029
|
try {
|
|
@@ -1677,7 +2034,7 @@ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
|
1677
2034
|
taskId: input.taskId ?? result.task?.id,
|
|
1678
2035
|
runTaskId: input.runTaskId
|
|
1679
2036
|
});
|
|
1680
|
-
return board ? { ...result, board } : result;
|
|
2037
|
+
return board && result.board ? { ...result, board } : result;
|
|
1681
2038
|
} catch {
|
|
1682
2039
|
return result;
|
|
1683
2040
|
}
|
|
@@ -1691,40 +2048,86 @@ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
1691
2048
|
"export_markdown",
|
|
1692
2049
|
"export_task_graph"
|
|
1693
2050
|
]);
|
|
2051
|
+
var KANBAN_TRANSCRIPT_ITEM_CAP = 100;
|
|
2052
|
+
var KANBAN_LIST_FIELDS = [
|
|
2053
|
+
{ field: "events", tail: true },
|
|
2054
|
+
{ field: "tasks" },
|
|
2055
|
+
{ field: "boards" },
|
|
2056
|
+
{ field: "recoveredTasks" },
|
|
2057
|
+
{ field: "chain" },
|
|
2058
|
+
{ field: "children" }
|
|
2059
|
+
];
|
|
2060
|
+
var KANBAN_OBJECT_TRANSCRIPT_BYTE_CAP = 65536;
|
|
2061
|
+
var KANBAN_PRIMARY_OBJECT_TRANSCRIPT_BYTE_CAP = 262144;
|
|
2062
|
+
var KANBAN_OBJECT_FIELDS = [
|
|
2063
|
+
{ field: "snapshot", primaryFor: "snapshot" },
|
|
2064
|
+
{ field: "workbench", primaryFor: "workbench" },
|
|
2065
|
+
{ field: "taskGraph", primaryFor: "export_task_graph" }
|
|
2066
|
+
];
|
|
2067
|
+
function byteLength(value) {
|
|
2068
|
+
try {
|
|
2069
|
+
return Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
|
|
2070
|
+
} catch {
|
|
2071
|
+
return 0;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
function objectSummary(field, value) {
|
|
2075
|
+
const summary = {};
|
|
2076
|
+
if (value && typeof value === "object") {
|
|
2077
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2078
|
+
if (Array.isArray(entry)) summary[`${key}Count`] = entry.length;
|
|
2079
|
+
else if (typeof entry === "number" || typeof entry === "string") summary[key] = entry;
|
|
2080
|
+
}
|
|
2081
|
+
if (field === "workbench") {
|
|
2082
|
+
const totals = value.totals;
|
|
2083
|
+
if (totals && typeof totals === "object") summary["totals"] = totals;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return summary;
|
|
2087
|
+
}
|
|
1694
2088
|
function serializeKanbanOutput(output, input) {
|
|
1695
2089
|
const action = input && typeof input === "object" ? input.action : void 0;
|
|
2090
|
+
const actionName = typeof action === "string" ? action : "";
|
|
2091
|
+
const compact = { ...output };
|
|
2092
|
+
const truncated = { ...output.truncated };
|
|
1696
2093
|
const board = output.board;
|
|
1697
|
-
if (board) {
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
if (!keepFull) {
|
|
1701
|
-
try {
|
|
1702
|
-
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
1703
|
-
} catch {
|
|
1704
|
-
boardBytes = 0;
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
2094
|
+
if (board && !KANBAN_FULL_BOARD_ACTIONS.has(actionName)) {
|
|
2095
|
+
const boardBytes = byteLength(board);
|
|
2096
|
+
if (boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
1708
2097
|
const columns = {};
|
|
1709
2098
|
for (const column of board.columns) {
|
|
1710
2099
|
columns[column.title || column.id] = board.tasks.filter(
|
|
1711
2100
|
(task) => task.columnId === column.id
|
|
1712
2101
|
).length;
|
|
1713
2102
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
totalTasks: board.tasks.length,
|
|
1721
|
-
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
1722
|
-
}
|
|
2103
|
+
compact["board"] = {
|
|
2104
|
+
id: board.id,
|
|
2105
|
+
title: board.title,
|
|
2106
|
+
columns,
|
|
2107
|
+
totalTasks: board.tasks.length,
|
|
2108
|
+
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
1723
2109
|
};
|
|
1724
|
-
return JSON.stringify(compact, null, 2);
|
|
1725
2110
|
}
|
|
1726
2111
|
}
|
|
1727
|
-
|
|
2112
|
+
for (const { field, tail } of KANBAN_LIST_FIELDS) {
|
|
2113
|
+
const list = output[field];
|
|
2114
|
+
if (!Array.isArray(list) || list.length <= KANBAN_TRANSCRIPT_ITEM_CAP) continue;
|
|
2115
|
+
compact[field] = tail ? list.slice(-KANBAN_TRANSCRIPT_ITEM_CAP) : list.slice(0, KANBAN_TRANSCRIPT_ITEM_CAP);
|
|
2116
|
+
truncated[field] = { shown: KANBAN_TRANSCRIPT_ITEM_CAP, total: list.length };
|
|
2117
|
+
}
|
|
2118
|
+
for (const { field, primaryFor } of KANBAN_OBJECT_FIELDS) {
|
|
2119
|
+
const value = output[field];
|
|
2120
|
+
if (value === void 0 || value === null) continue;
|
|
2121
|
+
const cap = actionName === primaryFor ? KANBAN_PRIMARY_OBJECT_TRANSCRIPT_BYTE_CAP : KANBAN_OBJECT_TRANSCRIPT_BYTE_CAP;
|
|
2122
|
+
const bytes = byteLength(value);
|
|
2123
|
+
if (bytes <= cap) continue;
|
|
2124
|
+
compact[field] = {
|
|
2125
|
+
...objectSummary(field, value),
|
|
2126
|
+
note: `${field} (${bytes} bytes) omitted from the transcript; narrow it with boardId, query, or limit.`
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
if (Object.keys(truncated).length > 0) compact["truncated"] = truncated;
|
|
2130
|
+
return JSON.stringify(compact, null, 2);
|
|
1728
2131
|
}
|
|
1729
2132
|
|
|
1730
2133
|
// src/kanban-tool-schema.ts
|
|
@@ -1800,10 +2203,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1800
2203
|
taskId: { type: "string" },
|
|
1801
2204
|
taskIds: { type: "array", items: { type: "string" } },
|
|
1802
2205
|
chainId: { type: "string" },
|
|
1803
|
-
fromNodeId: { type: "string" },
|
|
1804
|
-
toNodeId: { type: "string" },
|
|
1805
|
-
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1806
|
-
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1807
2206
|
columnId: { type: "string" },
|
|
1808
2207
|
targetBoardId: { type: "string" },
|
|
1809
2208
|
targetColumnId: { type: "string" },
|
|
@@ -1812,6 +2211,10 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1812
2211
|
dueDate: { type: "string" },
|
|
1813
2212
|
tags: { type: "array", items: { type: "string" } },
|
|
1814
2213
|
labels: { type: "array", items: { type: "string" } },
|
|
2214
|
+
label: {
|
|
2215
|
+
type: "string",
|
|
2216
|
+
description: "search_tasks / ready_tasks / snapshot: filter by a single label (takes precedence over labels[0])."
|
|
2217
|
+
},
|
|
1815
2218
|
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
1816
2219
|
taskType: {
|
|
1817
2220
|
type: "string",
|
|
@@ -1832,7 +2235,10 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1832
2235
|
},
|
|
1833
2236
|
order: { type: "number" },
|
|
1834
2237
|
query: { type: "string" },
|
|
1835
|
-
limit: {
|
|
2238
|
+
limit: {
|
|
2239
|
+
type: "number",
|
|
2240
|
+
description: "ready_tasks: max results. workbench: per-lane and alert limit. events: return only the most recent N events."
|
|
2241
|
+
},
|
|
1836
2242
|
agentId: { type: "string" },
|
|
1837
2243
|
name: { type: "string" },
|
|
1838
2244
|
role: { type: "string" },
|
|
@@ -1873,7 +2279,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1873
2279
|
},
|
|
1874
2280
|
required: ["checkId", "checkStatus"]
|
|
1875
2281
|
},
|
|
1876
|
-
description:
|
|
2282
|
+
description: '`transition_task` with lifecycleStage "done" only (rejected for any other stage): flip one or more manual criteria before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused.'
|
|
1877
2283
|
},
|
|
1878
2284
|
attachmentUrl: { type: "string" },
|
|
1879
2285
|
attachmentTitle: { type: "string" },
|
|
@@ -2028,7 +2434,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
2028
2434
|
includeCompletedTasks: { type: "boolean" },
|
|
2029
2435
|
preserveAssignment: { type: "boolean" },
|
|
2030
2436
|
preserveDependencies: { type: "boolean" },
|
|
2031
|
-
moveTasksToColumnId: { type: "string" },
|
|
2032
2437
|
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
2033
2438
|
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
2034
2439
|
gateEnforcement: {
|
|
@@ -2071,51 +2476,63 @@ var kanbanTool = {
|
|
|
2071
2476
|
inputSchema: KANBAN_INPUT_SCHEMA,
|
|
2072
2477
|
async execute(input, ctx, _opts) {
|
|
2073
2478
|
const signal = _opts?.signal ?? ctx.signal ?? new AbortController().signal;
|
|
2074
|
-
if (signal.aborted)
|
|
2479
|
+
if (signal.aborted) {
|
|
2480
|
+
throw new KanbanToolError("ABORTED", "Operation aborted before any Kanban work ran.");
|
|
2481
|
+
}
|
|
2075
2482
|
if (!input || typeof input !== "object" || typeof input.action !== "string" || !input.action.trim()) {
|
|
2076
|
-
|
|
2483
|
+
throw invalidInput("kanban: action is required and must be a non-empty string.", "action");
|
|
2077
2484
|
}
|
|
2078
2485
|
const normalizedInput = {
|
|
2079
2486
|
...input,
|
|
2080
2487
|
action: input.action.trim()
|
|
2081
2488
|
};
|
|
2082
2489
|
const projectRoot = ctx.projectRoot;
|
|
2083
|
-
if (!projectRoot)
|
|
2490
|
+
if (!projectRoot) {
|
|
2491
|
+
throw new KanbanToolError("UNAVAILABLE", "No project root is available.", {
|
|
2492
|
+
retryable: false
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2084
2495
|
const withPresence = createKanbanPresenceWrapper(projectRoot, normalizedInput, ctx);
|
|
2496
|
+
let result;
|
|
2085
2497
|
try {
|
|
2086
|
-
|
|
2087
|
-
const decompositionResult = await handleKanbanDecompositionAction(projectRoot, normalizedInput, ctx);
|
|
2088
|
-
if (decompositionResult !== void 0) return decompositionResult;
|
|
2089
|
-
const boardResult = await handleKanbanBoardAction(projectRoot, normalizedInput, ctx);
|
|
2090
|
-
if (boardResult !== void 0) return boardResult;
|
|
2091
|
-
const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, normalizedInput, ctx);
|
|
2092
|
-
if (lifecycleResult !== void 0) return lifecycleResult;
|
|
2093
|
-
const contractResult = await handleKanbanContractAction(
|
|
2094
|
-
projectRoot,
|
|
2095
|
-
normalizedInput,
|
|
2096
|
-
normalizedInput.author ?? normalizedInput.agentId,
|
|
2097
|
-
ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session"
|
|
2098
|
-
);
|
|
2099
|
-
if (contractResult !== void 0) return contractResult;
|
|
2100
|
-
const detailResult = await handleKanbanDetailAction(projectRoot, normalizedInput, ctx);
|
|
2101
|
-
if (detailResult !== void 0) return detailResult;
|
|
2102
|
-
return fail(`Unknown kanban action: ${normalizedInput.action}`);
|
|
2103
|
-
})();
|
|
2104
|
-
if (signal.aborted) return fail("Operation aborted.");
|
|
2105
|
-
return await withPresence(result);
|
|
2498
|
+
result = await dispatchKanbanAction(projectRoot, normalizedInput, ctx);
|
|
2106
2499
|
} catch (err) {
|
|
2107
|
-
|
|
2500
|
+
throw toKanbanToolError(err);
|
|
2108
2501
|
}
|
|
2502
|
+
if (signal.aborted) return result;
|
|
2503
|
+
return await withPresence(result);
|
|
2109
2504
|
},
|
|
2110
2505
|
serialize(output, input) {
|
|
2111
2506
|
return serializeKanbanOutput(output, input);
|
|
2112
2507
|
}
|
|
2113
2508
|
};
|
|
2509
|
+
async function dispatchKanbanAction(projectRoot, input, ctx) {
|
|
2510
|
+
const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
|
|
2511
|
+
if (decompositionResult !== void 0) return decompositionResult;
|
|
2512
|
+
const boardResult = await handleKanbanBoardAction(projectRoot, input, ctx);
|
|
2513
|
+
if (boardResult !== void 0) return boardResult;
|
|
2514
|
+
const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, input, ctx);
|
|
2515
|
+
if (lifecycleResult !== void 0) return lifecycleResult;
|
|
2516
|
+
const contractResult = await handleKanbanContractAction(
|
|
2517
|
+
projectRoot,
|
|
2518
|
+
input,
|
|
2519
|
+
input.author ?? input.agentId,
|
|
2520
|
+
ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session"
|
|
2521
|
+
);
|
|
2522
|
+
if (contractResult !== void 0) return contractResult;
|
|
2523
|
+
const detailResult = await handleKanbanDetailAction(projectRoot, input, ctx);
|
|
2524
|
+
if (detailResult !== void 0) return detailResult;
|
|
2525
|
+
throw invalidInput(`Unknown kanban action: ${input.action}`, "action");
|
|
2526
|
+
}
|
|
2114
2527
|
|
|
2115
2528
|
export {
|
|
2529
|
+
KanbanToolError,
|
|
2530
|
+
KanbanInputError,
|
|
2531
|
+
isKanbanToolFailure,
|
|
2116
2532
|
kanbanEvidenceKey,
|
|
2117
2533
|
kanbanEvidencePointer,
|
|
2118
2534
|
recordKanbanVerificationEvidence,
|
|
2535
|
+
KANBAN_READ_ONLY_ACTIONS,
|
|
2119
2536
|
kanbanTool
|
|
2120
2537
|
};
|
|
2121
|
-
//# sourceMappingURL=chunk-
|
|
2538
|
+
//# sourceMappingURL=chunk-VMJY6FYJ.js.map
|