kanbango 3.6.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +25 -18
- package/API.md +58 -170
- package/CHANGELOG.md +76 -0
- package/LLM_AGENTS.md +21 -502
- package/README.md +45 -12
- package/agent-playbook.js +27 -49
- package/bin/kanban.js +395 -300
- package/config-wizard.js +441 -0
- package/index.html +124 -36
- package/kanban.js +892 -263
- package/mcp-server.js +319 -196
- package/package.json +1 -1
- package/plan.js +39 -40
- package/plugins/tui-kanban-controller.js +117 -0
- package/plugins/tui-kanban.tsx +314 -0
- package/workflow.js +368 -21
- package/.ai/lessons.jsonl +0 -9
- package/.ai/retro/close.json +0 -1
- package/.ai/retro/last-run.json +0 -1
- package/.ait-quality.yml +0 -25
- package/bin/kanban-cmd.js +0 -40
- package/kan2.md +0 -76
- package/kanbango.md +0 -48
- package/planv2.md +0 -317
- package/tests/fixtures/fake-opencode.js +0 -69
- package/tests/index.js +0 -19
- package/tests/kanban-cli.js +0 -118
- package/tests/kanban.js +0 -104
- package/tests/run.js +0 -33
package/mcp-server.js
CHANGED
|
@@ -11,13 +11,16 @@ const plan = require('./plan.js');
|
|
|
11
11
|
const guiRegistry = require('./gui-registry.js');
|
|
12
12
|
const playbook = require('./agent-playbook.js');
|
|
13
13
|
|
|
14
|
-
const COLS = kanban.COLS;
|
|
15
14
|
const READ_VIEWS = Object.keys(kanban.VIEW_FIELDS);
|
|
16
15
|
const GUI_READY_TIMEOUT_MS = 8000;
|
|
17
16
|
const GUI_READY_POLL_MS = 50;
|
|
18
17
|
let guiProcess = null;
|
|
19
18
|
let guiPort = null;
|
|
20
19
|
|
|
20
|
+
function activeCols() {
|
|
21
|
+
return kanban.COLS.slice();
|
|
22
|
+
}
|
|
23
|
+
|
|
21
24
|
function normalizePort(value) {
|
|
22
25
|
return guiRegistry.normalizeGuiPort(value);
|
|
23
26
|
}
|
|
@@ -60,8 +63,37 @@ async function waitForGuiReady(pid, timeoutMs = GUI_READY_TIMEOUT_MS) {
|
|
|
60
63
|
);
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
function nextToolCall(error) {
|
|
67
|
+
const code = error && error.code;
|
|
68
|
+
const details = (error && error.details) || {};
|
|
69
|
+
|
|
70
|
+
if (code === 'TASK_NOT_FOUND') {
|
|
71
|
+
return {
|
|
72
|
+
tool: 'kanban_read',
|
|
73
|
+
arguments: { operation: 'list', col: 'planned' }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (code === 'INVALID_TRANSITION') {
|
|
78
|
+
const allowed = details.allowed_columns;
|
|
79
|
+
if (!Array.isArray(allowed) || allowed.length === 0 || !details.task_id) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
tool: 'kanban_manage',
|
|
84
|
+
arguments: {
|
|
85
|
+
action: 'move',
|
|
86
|
+
task_id: details.task_id,
|
|
87
|
+
column: allowed[0]
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
function serializeError(error) {
|
|
64
|
-
|
|
96
|
+
const payload = {
|
|
65
97
|
error: {
|
|
66
98
|
code: error.code || 'INTERNAL_ERROR',
|
|
67
99
|
message: error.message,
|
|
@@ -70,6 +102,9 @@ function serializeError(error) {
|
|
|
70
102
|
retryable: Boolean(error.retryable)
|
|
71
103
|
}
|
|
72
104
|
};
|
|
105
|
+
const next = nextToolCall(error);
|
|
106
|
+
if (next) payload.error.next_tool_call = next;
|
|
107
|
+
return payload;
|
|
73
108
|
}
|
|
74
109
|
|
|
75
110
|
function invalidRequest(message, hint, details) {
|
|
@@ -79,7 +114,7 @@ function invalidRequest(message, hint, details) {
|
|
|
79
114
|
function serializeResult(result) {
|
|
80
115
|
if (typeof result === 'string') return result;
|
|
81
116
|
|
|
82
|
-
const text = JSON.stringify(result
|
|
117
|
+
const text = JSON.stringify(result);
|
|
83
118
|
if (typeof text === 'string') return text;
|
|
84
119
|
|
|
85
120
|
throw kanban.createKanbanError(
|
|
@@ -105,7 +140,7 @@ function textResponse(result, isError = false) {
|
|
|
105
140
|
}
|
|
106
141
|
|
|
107
142
|
function normalizeReturnShape(returnShape) {
|
|
108
|
-
if (returnShape === undefined) return '
|
|
143
|
+
if (returnShape === undefined) return 'none';
|
|
109
144
|
if (!['none', 'summary', 'full'].includes(returnShape)) {
|
|
110
145
|
throw invalidRequest(
|
|
111
146
|
`Unsupported return value: ${returnShape}`,
|
|
@@ -150,13 +185,108 @@ async function buildEpicLookup() {
|
|
|
150
185
|
return lookup;
|
|
151
186
|
}
|
|
152
187
|
|
|
188
|
+
function withMissing(payload, missing) {
|
|
189
|
+
if (!missing || missing.length === 0) return payload;
|
|
190
|
+
return { ...payload, missing };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatCreateAck(id, missing) {
|
|
194
|
+
return withMissing({ ok: true, id }, missing);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readFields(readOptions) {
|
|
198
|
+
if (Array.isArray(readOptions.fields) && readOptions.fields.length > 0) {
|
|
199
|
+
return readOptions.fields;
|
|
200
|
+
}
|
|
201
|
+
return kanban.VIEW_FIELDS[readOptions.view || 'summary'] || kanban.VIEW_FIELDS.summary;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function viewNeedsEpicLookup(readOptions) {
|
|
205
|
+
return readFields(readOptions).includes('epic_goals');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function viewNeedsAllTasks(readOptions) {
|
|
209
|
+
const fields = readFields(readOptions);
|
|
210
|
+
return fields.includes('blocked')
|
|
211
|
+
|| fields.includes('unmet_dependencies')
|
|
212
|
+
|| fields.includes('blocks');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function findTaskOnBoard(taskId, allTasks) {
|
|
216
|
+
const raw = String(taskId);
|
|
217
|
+
const exact = allTasks.find((task) => task.id === raw);
|
|
218
|
+
if (exact) return exact;
|
|
219
|
+
const num = parseInt(raw, 10);
|
|
220
|
+
if (!Number.isFinite(num)) return null;
|
|
221
|
+
return allTasks.find((task) => task.task_number === num) || null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function omitEmptyPlanFields(payload) {
|
|
225
|
+
const out = {};
|
|
226
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
227
|
+
if (value === undefined || value === null || value === '') continue;
|
|
228
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
229
|
+
out[key] = value;
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function compactPlanEvidence(evidence) {
|
|
235
|
+
if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) return undefined;
|
|
236
|
+
return omitEmptyPlanFields({
|
|
237
|
+
summary: evidence.summary,
|
|
238
|
+
test_command: evidence.test_command,
|
|
239
|
+
has_diff: evidence.diff ? true : undefined
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatPlanResult(planResult, returnShape) {
|
|
244
|
+
if (!planResult || typeof planResult !== 'object') return planResult;
|
|
245
|
+
if (returnShape === 'full') return planResult;
|
|
246
|
+
const out = { ok: planResult.ok !== false };
|
|
247
|
+
if (planResult.task_id) out.task_id = planResult.task_id;
|
|
248
|
+
if (planResult.runner !== undefined) out.runner = planResult.runner;
|
|
249
|
+
if (planResult.current_step !== undefined && planResult.current_step !== -1) {
|
|
250
|
+
out.current_step = planResult.current_step;
|
|
251
|
+
}
|
|
252
|
+
if (planResult.status) out.status = planResult.status;
|
|
253
|
+
if (planResult.column) out.column = planResult.column;
|
|
254
|
+
if (Array.isArray(planResult.unblocked_tasks) && planResult.unblocked_tasks.length > 0) {
|
|
255
|
+
out.unblocked_tasks = planResult.unblocked_tasks;
|
|
256
|
+
}
|
|
257
|
+
if (planResult.warnings) out.warnings = planResult.warnings;
|
|
258
|
+
if (planResult.missing_recommended) out.missing = planResult.missing_recommended;
|
|
259
|
+
if (returnShape === 'none') return omitEmptyPlanFields(out);
|
|
260
|
+
if (Array.isArray(planResult.subtasks)) {
|
|
261
|
+
out.progress = {
|
|
262
|
+
done: planResult.subtasks.filter((step) => step.done).length,
|
|
263
|
+
total: planResult.subtasks.length
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
out.evidence = compactPlanEvidence(planResult.evidence);
|
|
267
|
+
return omitEmptyPlanFields(out);
|
|
268
|
+
}
|
|
269
|
+
|
|
153
270
|
async function formatTaskResult(task, returnShape) {
|
|
154
|
-
if (returnShape === 'none')
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
271
|
+
if (returnShape === 'none') {
|
|
272
|
+
if (Array.isArray(task.unblocked_tasks) && task.unblocked_tasks.length > 0) {
|
|
273
|
+
return { ok: true, unblocked_tasks: task.unblocked_tasks };
|
|
274
|
+
}
|
|
275
|
+
return { ok: true };
|
|
276
|
+
}
|
|
277
|
+
const view = returnShape === 'full' ? 'full' : 'summary';
|
|
278
|
+
const readOptions = { view };
|
|
279
|
+
const epicLookup = viewNeedsEpicLookup(readOptions) ? await buildEpicLookup() : undefined;
|
|
280
|
+
const allTasks = viewNeedsAllTasks(readOptions) ? await kanban.allTasks() : undefined;
|
|
281
|
+
const shaped = kanban.shapeTask(task, {
|
|
282
|
+
view,
|
|
283
|
+
epicLookup,
|
|
284
|
+
allTasks
|
|
159
285
|
});
|
|
286
|
+
if (Array.isArray(task.unblocked_tasks)) {
|
|
287
|
+
shaped.unblocked_tasks = task.unblocked_tasks;
|
|
288
|
+
}
|
|
289
|
+
return shaped;
|
|
160
290
|
}
|
|
161
291
|
|
|
162
292
|
function guiIdentity(extra = {}) {
|
|
@@ -168,6 +298,21 @@ function guiIdentity(extra = {}) {
|
|
|
168
298
|
};
|
|
169
299
|
}
|
|
170
300
|
|
|
301
|
+
function discoveredGuiPayload(discovered, extra = {}) {
|
|
302
|
+
if (!discovered) return { status: 'not_running' };
|
|
303
|
+
return guiIdentity({
|
|
304
|
+
status: extra.status || 'external_running',
|
|
305
|
+
owned: extra.owned === undefined ? false : extra.owned,
|
|
306
|
+
port: discovered.port,
|
|
307
|
+
pid: discovered.pid,
|
|
308
|
+
url: discovered.url,
|
|
309
|
+
cwd: discovered.cwd,
|
|
310
|
+
project: discovered.project,
|
|
311
|
+
started_at: discovered.started_at,
|
|
312
|
+
...extra
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
171
316
|
async function startGuiServer(port) {
|
|
172
317
|
if (port !== undefined && port !== null && port !== '' && !normalizePort(port)) {
|
|
173
318
|
throw invalidRequest('Invalid port', 'Use an integer between 1 and 65535', { port });
|
|
@@ -254,21 +399,7 @@ async function stopGuiServer() {
|
|
|
254
399
|
|
|
255
400
|
guiProcess = null;
|
|
256
401
|
guiPort = null;
|
|
257
|
-
|
|
258
|
-
const discovered = await guiRegistry.discoverRunningGui();
|
|
259
|
-
if (!discovered) {
|
|
260
|
-
return { status: 'not_running' };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return guiIdentity({
|
|
264
|
-
status: 'external_running',
|
|
265
|
-
owned: false,
|
|
266
|
-
port: discovered.port,
|
|
267
|
-
pid: discovered.pid,
|
|
268
|
-
url: discovered.url,
|
|
269
|
-
cwd: discovered.cwd,
|
|
270
|
-
project: discovered.project,
|
|
271
|
-
started_at: discovered.started_at,
|
|
402
|
+
return discoveredGuiPayload(await guiRegistry.discoverRunningGui(), {
|
|
272
403
|
hint: 'GUI was not started by this MCP process; stop refused. Stop it from the owning terminal or kill that PID manually.'
|
|
273
404
|
});
|
|
274
405
|
}
|
|
@@ -286,22 +417,7 @@ async function guiStatus() {
|
|
|
286
417
|
|
|
287
418
|
guiProcess = null;
|
|
288
419
|
guiPort = null;
|
|
289
|
-
|
|
290
|
-
const discovered = await guiRegistry.discoverRunningGui();
|
|
291
|
-
if (!discovered) {
|
|
292
|
-
return { status: 'not_running' };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return guiIdentity({
|
|
296
|
-
status: 'external_running',
|
|
297
|
-
owned: false,
|
|
298
|
-
port: discovered.port,
|
|
299
|
-
pid: discovered.pid,
|
|
300
|
-
url: discovered.url,
|
|
301
|
-
cwd: discovered.cwd,
|
|
302
|
-
project: discovered.project,
|
|
303
|
-
started_at: discovered.started_at
|
|
304
|
-
});
|
|
420
|
+
return discoveredGuiPayload(await guiRegistry.discoverRunningGui());
|
|
305
421
|
}
|
|
306
422
|
|
|
307
423
|
const server = new Server(
|
|
@@ -317,6 +433,8 @@ const server = new Server(
|
|
|
317
433
|
);
|
|
318
434
|
|
|
319
435
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
436
|
+
await require('./workflow.js').ensureBoardConfig();
|
|
437
|
+
const COLS = activeCols();
|
|
320
438
|
return {
|
|
321
439
|
tools: [
|
|
322
440
|
{
|
|
@@ -327,52 +445,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
327
445
|
properties: {
|
|
328
446
|
operation: {
|
|
329
447
|
type: 'string',
|
|
330
|
-
enum: ['list', 'show', 'list_epics', 'show_epic', 'help'],
|
|
331
|
-
|
|
332
|
-
default: 'list'
|
|
333
|
-
},
|
|
334
|
-
task_id: {
|
|
335
|
-
type: 'string',
|
|
336
|
-
description: "Required for show. Numeric id: '014' or '14'."
|
|
337
|
-
},
|
|
338
|
-
epic_id: {
|
|
339
|
-
type: 'string',
|
|
340
|
-
description: "Required for show_epic. Epic id like 'E001'."
|
|
341
|
-
},
|
|
342
|
-
col: {
|
|
343
|
-
type: 'string',
|
|
344
|
-
enum: COLS,
|
|
345
|
-
description: 'Filter list by column (saves tokens — prefer this)'
|
|
346
|
-
},
|
|
347
|
-
epic: {
|
|
348
|
-
type: 'string',
|
|
349
|
-
description: 'Filter list by epic id or title'
|
|
350
|
-
},
|
|
351
|
-
include_archived: {
|
|
352
|
-
type: 'boolean',
|
|
353
|
-
description: 'list/list_epics: include archived epics and their tasks (default false)'
|
|
354
|
-
},
|
|
355
|
-
include_done: {
|
|
356
|
-
type: 'boolean',
|
|
357
|
-
description: 'list_epics: include status=done epics without archived (default false)'
|
|
448
|
+
enum: ['list', 'show', 'list_epics', 'show_epic', 'context', 'help'],
|
|
449
|
+
default: 'context'
|
|
358
450
|
},
|
|
451
|
+
task_id: { type: 'string' },
|
|
452
|
+
epic_id: { type: 'string' },
|
|
453
|
+
col: { type: 'string', enum: COLS },
|
|
454
|
+
epic: { type: 'string' },
|
|
455
|
+
include_archived: { type: 'boolean' },
|
|
456
|
+
include_done: { type: 'boolean' },
|
|
359
457
|
status: {
|
|
360
458
|
type: 'string',
|
|
361
|
-
enum: ['empty', 'planned', 'active', 'done', 'archived']
|
|
362
|
-
description: 'list_epics: exact status filter (overrides live-only default)'
|
|
363
|
-
},
|
|
364
|
-
view: {
|
|
365
|
-
type: 'string',
|
|
366
|
-
enum: READ_VIEWS,
|
|
367
|
-
description: 'summary=board scan (default); planning=scope/AC; execution=+subtasks; full=everything. Prefer smallest that works.'
|
|
459
|
+
enum: ['empty', 'planned', 'active', 'done', 'archived']
|
|
368
460
|
},
|
|
461
|
+
view: { type: 'string', enum: READ_VIEWS },
|
|
369
462
|
fields: {
|
|
370
463
|
type: 'array',
|
|
371
|
-
description: 'Exact fields only (overrides view). Use when you need 1–2 fields.',
|
|
372
464
|
items: { type: 'string' }
|
|
373
465
|
}
|
|
374
466
|
},
|
|
375
|
-
additionalProperties: false
|
|
467
|
+
additionalProperties: false,
|
|
468
|
+
examples: [
|
|
469
|
+
{ operation: 'context' },
|
|
470
|
+
{ operation: 'show', task_id: '014' },
|
|
471
|
+
{ operation: 'list', col: 'active' }
|
|
472
|
+
],
|
|
473
|
+
if: {
|
|
474
|
+
properties: { operation: { const: 'show' } },
|
|
475
|
+
required: ['operation']
|
|
476
|
+
},
|
|
477
|
+
then: { required: ['task_id'] }
|
|
376
478
|
}
|
|
377
479
|
},
|
|
378
480
|
{
|
|
@@ -398,63 +500,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
398
500
|
'plan_evidence',
|
|
399
501
|
'plan_done',
|
|
400
502
|
'plan_status'
|
|
401
|
-
]
|
|
402
|
-
description: 'create|move|update|delete daily; epic_create|epic_update|epic_archive|epic_unarchive|epic_delete; plan_* multi-step'
|
|
403
|
-
},
|
|
404
|
-
title: {
|
|
405
|
-
type: 'string',
|
|
406
|
-
description: 'Required for create, plan_create, epic_create'
|
|
503
|
+
]
|
|
407
504
|
},
|
|
505
|
+
title: { type: 'string' },
|
|
408
506
|
col: {
|
|
409
507
|
type: 'string',
|
|
410
508
|
enum: COLS,
|
|
411
|
-
default: 'planned'
|
|
412
|
-
description: 'create column, or update shortcut for column'
|
|
509
|
+
default: 'planned'
|
|
413
510
|
},
|
|
414
511
|
epic: {
|
|
415
512
|
type: 'string',
|
|
416
|
-
default: '—'
|
|
417
|
-
description: 'Epic id or title (create/update/plan_create). Prefer E001.'
|
|
418
|
-
},
|
|
419
|
-
epic_id: {
|
|
420
|
-
type: 'string',
|
|
421
|
-
description: 'Required for epic_update|epic_archive|epic_unarchive|epic_delete; optional link id'
|
|
422
|
-
},
|
|
423
|
-
description: {
|
|
424
|
-
type: 'string',
|
|
425
|
-
description: 'Why/context (recommended on create / epic_create)'
|
|
426
|
-
},
|
|
427
|
-
goals: {
|
|
428
|
-
type: 'string',
|
|
429
|
-
description: 'Epic outcome one-liner (recommended on epic_create)'
|
|
430
|
-
},
|
|
431
|
-
specs: {
|
|
432
|
-
type: 'string',
|
|
433
|
-
description: 'Technical constraints (recommended on create)'
|
|
513
|
+
default: '—'
|
|
434
514
|
},
|
|
515
|
+
epic_id: { type: 'string' },
|
|
516
|
+
description: { type: 'string' },
|
|
517
|
+
goals: { type: 'string' },
|
|
518
|
+
specs: { type: 'string' },
|
|
435
519
|
in_scope: {
|
|
436
520
|
type: 'array',
|
|
437
|
-
description: 'In-scope bullets (recommended on create)',
|
|
438
521
|
items: { type: 'string' }
|
|
439
522
|
},
|
|
440
523
|
out_of_scope: {
|
|
441
524
|
type: 'array',
|
|
442
|
-
description: 'Out-of-scope bullets (recommended on create)',
|
|
443
525
|
items: { type: 'string' }
|
|
444
526
|
},
|
|
445
527
|
acceptance_criteria: {
|
|
446
528
|
type: 'array',
|
|
447
|
-
description: 'Done criteria (recommended on create)',
|
|
448
529
|
items: { type: 'string' }
|
|
449
530
|
},
|
|
450
531
|
test_cases: {
|
|
451
532
|
type: 'array',
|
|
452
|
-
|
|
533
|
+
items: { type: 'string' }
|
|
534
|
+
},
|
|
535
|
+
depends_on: {
|
|
536
|
+
type: 'array',
|
|
537
|
+
items: { type: 'string' }
|
|
538
|
+
},
|
|
539
|
+
files: {
|
|
540
|
+
type: 'array',
|
|
453
541
|
items: { type: 'string' }
|
|
454
542
|
},
|
|
455
543
|
subtasks: {
|
|
456
544
|
type: 'array',
|
|
457
|
-
description: 'Full subtask list replace on update (send complete array, not a single toggle)',
|
|
458
545
|
items: {
|
|
459
546
|
type: 'object',
|
|
460
547
|
properties: {
|
|
@@ -465,13 +552,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
465
552
|
}
|
|
466
553
|
}
|
|
467
554
|
},
|
|
468
|
-
notes: {
|
|
469
|
-
type: 'string',
|
|
470
|
-
description: 'Freeform notes'
|
|
471
|
-
},
|
|
555
|
+
notes: { type: 'string' },
|
|
472
556
|
adr: {
|
|
473
|
-
description:
|
|
474
|
-
'Append one ADR: { decision, why }. Or replace full array of { id?, decision, why, created? }.',
|
|
475
557
|
oneOf: [
|
|
476
558
|
{
|
|
477
559
|
type: 'object',
|
|
@@ -495,63 +577,55 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
495
577
|
}
|
|
496
578
|
]
|
|
497
579
|
},
|
|
498
|
-
task_id: {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
},
|
|
502
|
-
column: {
|
|
503
|
-
type: 'string',
|
|
504
|
-
enum: COLS,
|
|
505
|
-
description:
|
|
506
|
-
'Target column for move (not col). Must be a legal transition from the current column. '
|
|
507
|
-
+ 'icebox→planned; planned→active|icebox|testing; active→planned|testing|icebox; '
|
|
508
|
-
+ 'testing→active|review; review→active|done; done→active. Illegal → INVALID_TRANSITION + allowed_columns.'
|
|
509
|
-
},
|
|
510
|
-
patch: {
|
|
511
|
-
type: 'object',
|
|
512
|
-
description: 'Bulk update object; merged with top-level field shortcuts'
|
|
513
|
-
},
|
|
580
|
+
task_id: { type: 'string' },
|
|
581
|
+
column: { type: 'string', enum: COLS },
|
|
582
|
+
patch: { type: 'object' },
|
|
514
583
|
return: {
|
|
515
584
|
type: 'string',
|
|
516
585
|
enum: ['none', 'summary', 'full'],
|
|
517
|
-
|
|
518
|
-
},
|
|
519
|
-
index: {
|
|
520
|
-
type: 'integer',
|
|
521
|
-
description: 'plan_advance: subtask index; omit = first incomplete'
|
|
586
|
+
default: 'none'
|
|
522
587
|
},
|
|
588
|
+
index: { type: 'integer' },
|
|
523
589
|
steps: {
|
|
524
590
|
type: 'array',
|
|
525
|
-
items: { type: 'string' }
|
|
526
|
-
description: 'plan_create: implementation steps between red/green test steps'
|
|
527
|
-
},
|
|
528
|
-
project_root: {
|
|
529
|
-
type: 'string',
|
|
530
|
-
description: 'plan_create: root for test-runner detect (default cwd)'
|
|
531
|
-
},
|
|
532
|
-
diff: {
|
|
533
|
-
type: 'string',
|
|
534
|
-
description: 'plan_evidence: short diff or summary (not whole repo)'
|
|
535
|
-
},
|
|
536
|
-
test_command: {
|
|
537
|
-
type: 'string',
|
|
538
|
-
description: 'plan_evidence: exact command run'
|
|
591
|
+
items: { type: 'string' }
|
|
539
592
|
},
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
593
|
+
project_root: { type: 'string' },
|
|
594
|
+
diff: { type: 'string' },
|
|
595
|
+
summary: { type: 'string' },
|
|
596
|
+
test_command: { type: 'string' },
|
|
597
|
+
stdout: { type: 'string' },
|
|
598
|
+
stderr: { type: 'string' },
|
|
599
|
+
exit_code: { type: 'integer' }
|
|
600
|
+
},
|
|
601
|
+
required: ['action'],
|
|
602
|
+
additionalProperties: false,
|
|
603
|
+
examples: [
|
|
604
|
+
{
|
|
605
|
+
action: 'create',
|
|
606
|
+
title: 'Ship image',
|
|
607
|
+
epic: 'E001',
|
|
608
|
+
steps: ['Impl'],
|
|
609
|
+
col: 'planned'
|
|
543
610
|
},
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
611
|
+
{ action: 'move', task_id: '014', column: 'testing' }
|
|
612
|
+
],
|
|
613
|
+
allOf: [
|
|
614
|
+
{
|
|
615
|
+
if: {
|
|
616
|
+
properties: { action: { const: 'create' } },
|
|
617
|
+
required: ['action']
|
|
618
|
+
},
|
|
619
|
+
then: { required: ['title'] }
|
|
547
620
|
},
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
621
|
+
{
|
|
622
|
+
if: {
|
|
623
|
+
properties: { action: { const: 'move' } },
|
|
624
|
+
required: ['action']
|
|
625
|
+
},
|
|
626
|
+
then: { required: ['task_id', 'column'] }
|
|
551
627
|
}
|
|
552
|
-
|
|
553
|
-
required: ['action'],
|
|
554
|
-
additionalProperties: false
|
|
628
|
+
]
|
|
555
629
|
}
|
|
556
630
|
},
|
|
557
631
|
{
|
|
@@ -571,7 +645,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
571
645
|
}
|
|
572
646
|
},
|
|
573
647
|
required: ['action'],
|
|
574
|
-
additionalProperties: false
|
|
648
|
+
additionalProperties: false,
|
|
649
|
+
examples: [
|
|
650
|
+
{ action: 'status' },
|
|
651
|
+
{ action: 'start' }
|
|
652
|
+
]
|
|
575
653
|
}
|
|
576
654
|
}
|
|
577
655
|
]
|
|
@@ -586,19 +664,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
586
664
|
|
|
587
665
|
switch (name) {
|
|
588
666
|
case 'kanban_read': {
|
|
589
|
-
const operation = args.operation || '
|
|
667
|
+
const operation = args.operation || 'context';
|
|
590
668
|
|
|
591
669
|
if (operation === 'help') {
|
|
592
670
|
result = playbook.playbookHelpPayload();
|
|
593
671
|
break;
|
|
594
672
|
}
|
|
595
673
|
|
|
674
|
+
await require('./workflow.js').ensureBoardConfig();
|
|
675
|
+
|
|
676
|
+
if (operation === 'context') {
|
|
677
|
+
result = await kanban.getContextPayload({
|
|
678
|
+
epic: args.epic,
|
|
679
|
+
epic_id: args.epic_id
|
|
680
|
+
});
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
|
|
596
684
|
const readOptions = normalizeReadOptions(args, 'summary');
|
|
597
685
|
|
|
598
686
|
if (operation === 'list') {
|
|
599
687
|
await kanban.migrateEpicGroups();
|
|
600
688
|
const epics = await kanban.listEpicEntities();
|
|
601
|
-
|
|
689
|
+
const allBoardTasks = await kanban.allTasks();
|
|
690
|
+
let tasks = allBoardTasks;
|
|
602
691
|
if (args.col) {
|
|
603
692
|
tasks = tasks.filter((task) => task.column === args.col);
|
|
604
693
|
}
|
|
@@ -609,12 +698,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
609
698
|
} else {
|
|
610
699
|
tasks = kanban.filterTasksForList(tasks, epics, {
|
|
611
700
|
include_archived: args.include_archived,
|
|
612
|
-
include_done: args.include_done
|
|
701
|
+
include_done: args.include_done,
|
|
702
|
+
col: args.col
|
|
613
703
|
});
|
|
614
704
|
}
|
|
615
705
|
|
|
616
|
-
|
|
617
|
-
result = tasks.map((task) => kanban.shapeTask(task, readOptions));
|
|
706
|
+
result = tasks.map((task) => kanban.shapeTask(task, { ...readOptions, allTasks: allBoardTasks }));
|
|
618
707
|
} else if (operation === 'show') {
|
|
619
708
|
if (!args.task_id) {
|
|
620
709
|
throw invalidRequest(
|
|
@@ -625,10 +714,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
625
714
|
}
|
|
626
715
|
|
|
627
716
|
await kanban.migrateEpicGroups();
|
|
628
|
-
const
|
|
629
|
-
|
|
717
|
+
const needBoard = viewNeedsAllTasks(readOptions);
|
|
718
|
+
const allTasks = needBoard ? await kanban.allTasks() : undefined;
|
|
719
|
+
const task = allTasks
|
|
720
|
+
? findTaskOnBoard(args.task_id, allTasks)
|
|
721
|
+
: await kanban.getTask(args.task_id);
|
|
722
|
+
if (!task) {
|
|
723
|
+
throw kanban.createKanbanError(
|
|
724
|
+
'TASK_NOT_FOUND',
|
|
725
|
+
`Task ${args.task_id} was not found`,
|
|
726
|
+
'Call kanban_read with operation=list to discover valid task ids',
|
|
727
|
+
{ task_id: args.task_id },
|
|
728
|
+
false,
|
|
729
|
+
404
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
const epicLookup = viewNeedsEpicLookup(readOptions) ? await buildEpicLookup() : undefined;
|
|
733
|
+
result = kanban.shapeTask(task, {
|
|
630
734
|
...readOptions,
|
|
631
|
-
epicLookup
|
|
735
|
+
epicLookup,
|
|
736
|
+
allTasks
|
|
632
737
|
});
|
|
633
738
|
} else if (operation === 'list_epics') {
|
|
634
739
|
await kanban.migrateEpicGroups();
|
|
@@ -660,13 +765,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
660
765
|
const epic = await kanban.getEpicEntity(link.epic_id);
|
|
661
766
|
const tasks = await kanban.allTasks();
|
|
662
767
|
result = kanban.shapeEpic(epic, tasks, {
|
|
663
|
-
view: args.view || '
|
|
768
|
+
view: args.view || 'summary',
|
|
664
769
|
fields: args.fields
|
|
665
770
|
});
|
|
666
771
|
} else {
|
|
667
772
|
throw invalidRequest(
|
|
668
773
|
`Unknown operation: ${operation}`,
|
|
669
|
-
'Use one of: list, show, list_epics, show_epic, help',
|
|
774
|
+
'Use one of: list, show, list_epics, show_epic, context, help',
|
|
670
775
|
{ operation }
|
|
671
776
|
);
|
|
672
777
|
}
|
|
@@ -676,9 +781,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
676
781
|
case 'kanban_manage': {
|
|
677
782
|
const action = args.action;
|
|
678
783
|
const returnShape = normalizeReturnShape(args.return);
|
|
784
|
+
await require('./workflow.js').ensureBoardConfig();
|
|
679
785
|
|
|
680
786
|
switch (action) {
|
|
681
787
|
case 'create': {
|
|
788
|
+
let subtasks = args.subtasks;
|
|
789
|
+
if (subtasks === undefined && Array.isArray(args.steps)) {
|
|
790
|
+
subtasks = args.steps.filter(Boolean).map(String).map((text, index) => ({
|
|
791
|
+
id: `st-${index + 1}`,
|
|
792
|
+
text,
|
|
793
|
+
done: false,
|
|
794
|
+
description: ''
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
682
797
|
const createPayload = {
|
|
683
798
|
description: args.description,
|
|
684
799
|
specs: args.specs,
|
|
@@ -686,19 +801,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
686
801
|
out_of_scope: args.out_of_scope,
|
|
687
802
|
acceptance_criteria: args.acceptance_criteria,
|
|
688
803
|
test_cases: args.test_cases,
|
|
689
|
-
subtasks
|
|
690
|
-
notes: args.notes
|
|
804
|
+
subtasks,
|
|
805
|
+
notes: args.notes,
|
|
806
|
+
depends_on: args.depends_on,
|
|
807
|
+
files: args.files
|
|
691
808
|
};
|
|
692
809
|
const epicRef = args.epic_id || args.epic || '—';
|
|
693
810
|
const created = await kanban.doCreate(args.title, args.col || 'planned', epicRef, createPayload);
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
? { ...shaped, warnings, missing_recommended: kanban.missingRecommendedCreateFields(createPayload) }
|
|
701
|
-
: shaped;
|
|
811
|
+
const missing = kanban.missingRecommendedCreateFields(createPayload);
|
|
812
|
+
if (returnShape === 'none') {
|
|
813
|
+
result = formatCreateAck(created.id, missing);
|
|
814
|
+
} else {
|
|
815
|
+
result = withMissing(await formatTaskResult(created, returnShape), missing);
|
|
816
|
+
}
|
|
702
817
|
break;
|
|
703
818
|
}
|
|
704
819
|
|
|
@@ -711,15 +826,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
711
826
|
notes: args.notes
|
|
712
827
|
};
|
|
713
828
|
const createdEpic = await kanban.doCreateEpic(args.title, epicPayload);
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
829
|
+
const missing = kanban.missingRecommendedEpicCreateFields(epicPayload);
|
|
830
|
+
if (returnShape === 'none') {
|
|
831
|
+
result = formatCreateAck(createdEpic.id, missing);
|
|
832
|
+
} else {
|
|
833
|
+
const view = returnShape === 'full' ? 'full' : 'summary';
|
|
834
|
+
result = withMissing(
|
|
835
|
+
kanban.shapeEpic(createdEpic, await kanban.allTasks(), { view }),
|
|
836
|
+
missing
|
|
837
|
+
);
|
|
838
|
+
}
|
|
723
839
|
break;
|
|
724
840
|
}
|
|
725
841
|
|
|
@@ -799,7 +915,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
799
915
|
}
|
|
800
916
|
const deleted = await kanban.deleteTask(args.task_id);
|
|
801
917
|
result = returnShape === 'none'
|
|
802
|
-
? { ok: true
|
|
918
|
+
? { ok: true }
|
|
803
919
|
: deleted;
|
|
804
920
|
break;
|
|
805
921
|
}
|
|
@@ -842,6 +958,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
842
958
|
if (args.test_cases !== undefined) patch.test_cases = args.test_cases;
|
|
843
959
|
if (args.subtasks !== undefined) patch.subtasks = args.subtasks;
|
|
844
960
|
if (args.notes !== undefined) patch.notes = args.notes;
|
|
961
|
+
if (args.depends_on !== undefined) patch.depends_on = args.depends_on;
|
|
962
|
+
if (args.files !== undefined) patch.files = args.files;
|
|
845
963
|
if (args.adr !== undefined) patch.adr = args.adr;
|
|
846
964
|
if (args.epic_id !== undefined) patch.epic_id = args.epic_id;
|
|
847
965
|
else if (args.epic !== undefined) patch.epic = args.epic;
|
|
@@ -861,19 +979,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
861
979
|
missing_recommended: kanban.missingRecommendedCreateFields(args)
|
|
862
980
|
};
|
|
863
981
|
}
|
|
982
|
+
result = formatPlanResult(result, returnShape);
|
|
864
983
|
break;
|
|
865
984
|
}
|
|
866
985
|
case 'plan_advance':
|
|
867
|
-
result =
|
|
986
|
+
result = formatPlanResult(
|
|
987
|
+
await plan.advance({ task_id: args.task_id, index: args.index }),
|
|
988
|
+
returnShape
|
|
989
|
+
);
|
|
868
990
|
break;
|
|
869
991
|
case 'plan_evidence':
|
|
870
|
-
result = await plan.evidence(args);
|
|
992
|
+
result = formatPlanResult(await plan.evidence(args), returnShape);
|
|
871
993
|
break;
|
|
872
994
|
case 'plan_done':
|
|
873
|
-
result = await plan.done({ task_id: args.task_id });
|
|
995
|
+
result = formatPlanResult(await plan.done({ task_id: args.task_id }), returnShape);
|
|
874
996
|
break;
|
|
875
997
|
case 'plan_status':
|
|
876
|
-
result = await plan.status(args.task_id);
|
|
998
|
+
result = formatPlanResult(await plan.status(args.task_id), returnShape === 'none' ? 'summary' : returnShape);
|
|
877
999
|
break;
|
|
878
1000
|
|
|
879
1001
|
default:
|
|
@@ -978,6 +1100,7 @@ async function main() {
|
|
|
978
1100
|
module.exports = {
|
|
979
1101
|
serializeError,
|
|
980
1102
|
serializeResult,
|
|
1103
|
+
formatPlanResult,
|
|
981
1104
|
textResponse,
|
|
982
1105
|
startGuiServer,
|
|
983
1106
|
stopGuiServer,
|