kanbango 3.8.0 → 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 +23 -18
- package/API.md +58 -170
- package/CHANGELOG.md +39 -1
- package/LLM_AGENTS.md +21 -502
- package/README.md +18 -16
- package/agent-playbook.js +18 -57
- package/bin/kanban.js +344 -304
- package/config-wizard.js +4 -4
- package/index.html +59 -2
- package/kanban.js +503 -283
- package/mcp-server.js +259 -201
- package/package.json +1 -1
- package/plan.js +19 -24
- package/plugins/tui-kanban-controller.js +117 -0
- package/plugins/tui-kanban.tsx +314 -0
- package/workflow.js +15 -14
- package/.ai/lessons.jsonl +0 -14
- package/.ai/retro/close.json +0 -1
- package/.ai/retro/last-run.json +0 -1
- package/.ait-quality.yml +0 -26
- 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 -38
package/mcp-server.js
CHANGED
|
@@ -21,15 +21,6 @@ function activeCols() {
|
|
|
21
21
|
return kanban.COLS.slice();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function transitionHint() {
|
|
25
|
-
const map = kanban.COLUMN_TRANSITIONS || {};
|
|
26
|
-
const parts = Object.keys(map).map((from) => {
|
|
27
|
-
const to = (map[from] || []).join('|');
|
|
28
|
-
return `${from}→${to || '(none)'}`;
|
|
29
|
-
});
|
|
30
|
-
return parts.join('; ') || 'see backlog/kanbango.json columns';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
24
|
function normalizePort(value) {
|
|
34
25
|
return guiRegistry.normalizeGuiPort(value);
|
|
35
26
|
}
|
|
@@ -72,8 +63,37 @@ async function waitForGuiReady(pid, timeoutMs = GUI_READY_TIMEOUT_MS) {
|
|
|
72
63
|
);
|
|
73
64
|
}
|
|
74
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
|
+
|
|
75
95
|
function serializeError(error) {
|
|
76
|
-
|
|
96
|
+
const payload = {
|
|
77
97
|
error: {
|
|
78
98
|
code: error.code || 'INTERNAL_ERROR',
|
|
79
99
|
message: error.message,
|
|
@@ -82,6 +102,9 @@ function serializeError(error) {
|
|
|
82
102
|
retryable: Boolean(error.retryable)
|
|
83
103
|
}
|
|
84
104
|
};
|
|
105
|
+
const next = nextToolCall(error);
|
|
106
|
+
if (next) payload.error.next_tool_call = next;
|
|
107
|
+
return payload;
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
function invalidRequest(message, hint, details) {
|
|
@@ -91,7 +114,7 @@ function invalidRequest(message, hint, details) {
|
|
|
91
114
|
function serializeResult(result) {
|
|
92
115
|
if (typeof result === 'string') return result;
|
|
93
116
|
|
|
94
|
-
const text = JSON.stringify(result
|
|
117
|
+
const text = JSON.stringify(result);
|
|
95
118
|
if (typeof text === 'string') return text;
|
|
96
119
|
|
|
97
120
|
throw kanban.createKanbanError(
|
|
@@ -117,7 +140,7 @@ function textResponse(result, isError = false) {
|
|
|
117
140
|
}
|
|
118
141
|
|
|
119
142
|
function normalizeReturnShape(returnShape) {
|
|
120
|
-
if (returnShape === undefined) return '
|
|
143
|
+
if (returnShape === undefined) return 'none';
|
|
121
144
|
if (!['none', 'summary', 'full'].includes(returnShape)) {
|
|
122
145
|
throw invalidRequest(
|
|
123
146
|
`Unsupported return value: ${returnShape}`,
|
|
@@ -162,6 +185,88 @@ async function buildEpicLookup() {
|
|
|
162
185
|
return lookup;
|
|
163
186
|
}
|
|
164
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
|
+
|
|
165
270
|
async function formatTaskResult(task, returnShape) {
|
|
166
271
|
if (returnShape === 'none') {
|
|
167
272
|
if (Array.isArray(task.unblocked_tasks) && task.unblocked_tasks.length > 0) {
|
|
@@ -169,10 +274,12 @@ async function formatTaskResult(task, returnShape) {
|
|
|
169
274
|
}
|
|
170
275
|
return { ok: true };
|
|
171
276
|
}
|
|
172
|
-
const
|
|
173
|
-
const
|
|
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;
|
|
174
281
|
const shaped = kanban.shapeTask(task, {
|
|
175
|
-
view
|
|
282
|
+
view,
|
|
176
283
|
epicLookup,
|
|
177
284
|
allTasks
|
|
178
285
|
});
|
|
@@ -191,6 +298,21 @@ function guiIdentity(extra = {}) {
|
|
|
191
298
|
};
|
|
192
299
|
}
|
|
193
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
|
+
|
|
194
316
|
async function startGuiServer(port) {
|
|
195
317
|
if (port !== undefined && port !== null && port !== '' && !normalizePort(port)) {
|
|
196
318
|
throw invalidRequest('Invalid port', 'Use an integer between 1 and 65535', { port });
|
|
@@ -277,21 +399,7 @@ async function stopGuiServer() {
|
|
|
277
399
|
|
|
278
400
|
guiProcess = null;
|
|
279
401
|
guiPort = null;
|
|
280
|
-
|
|
281
|
-
const discovered = await guiRegistry.discoverRunningGui();
|
|
282
|
-
if (!discovered) {
|
|
283
|
-
return { status: 'not_running' };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return guiIdentity({
|
|
287
|
-
status: 'external_running',
|
|
288
|
-
owned: false,
|
|
289
|
-
port: discovered.port,
|
|
290
|
-
pid: discovered.pid,
|
|
291
|
-
url: discovered.url,
|
|
292
|
-
cwd: discovered.cwd,
|
|
293
|
-
project: discovered.project,
|
|
294
|
-
started_at: discovered.started_at,
|
|
402
|
+
return discoveredGuiPayload(await guiRegistry.discoverRunningGui(), {
|
|
295
403
|
hint: 'GUI was not started by this MCP process; stop refused. Stop it from the owning terminal or kill that PID manually.'
|
|
296
404
|
});
|
|
297
405
|
}
|
|
@@ -309,22 +417,7 @@ async function guiStatus() {
|
|
|
309
417
|
|
|
310
418
|
guiProcess = null;
|
|
311
419
|
guiPort = null;
|
|
312
|
-
|
|
313
|
-
const discovered = await guiRegistry.discoverRunningGui();
|
|
314
|
-
if (!discovered) {
|
|
315
|
-
return { status: 'not_running' };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return guiIdentity({
|
|
319
|
-
status: 'external_running',
|
|
320
|
-
owned: false,
|
|
321
|
-
port: discovered.port,
|
|
322
|
-
pid: discovered.pid,
|
|
323
|
-
url: discovered.url,
|
|
324
|
-
cwd: discovered.cwd,
|
|
325
|
-
project: discovered.project,
|
|
326
|
-
started_at: discovered.started_at
|
|
327
|
-
});
|
|
420
|
+
return discoveredGuiPayload(await guiRegistry.discoverRunningGui());
|
|
328
421
|
}
|
|
329
422
|
|
|
330
423
|
const server = new Server(
|
|
@@ -353,51 +446,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
353
446
|
operation: {
|
|
354
447
|
type: 'string',
|
|
355
448
|
enum: ['list', 'show', 'list_epics', 'show_epic', 'context', 'help'],
|
|
356
|
-
|
|
357
|
-
default: 'list'
|
|
358
|
-
},
|
|
359
|
-
task_id: {
|
|
360
|
-
type: 'string',
|
|
361
|
-
description: "Required for show. Numeric id: '014' or '14'."
|
|
362
|
-
},
|
|
363
|
-
epic_id: {
|
|
364
|
-
type: 'string',
|
|
365
|
-
description: "Required for show_epic. Epic id like 'E001'."
|
|
366
|
-
},
|
|
367
|
-
col: {
|
|
368
|
-
type: 'string',
|
|
369
|
-
enum: COLS,
|
|
370
|
-
description: 'Filter list by column (saves tokens — prefer this)'
|
|
371
|
-
},
|
|
372
|
-
epic: {
|
|
373
|
-
type: 'string',
|
|
374
|
-
description: 'Filter list by epic id or title'
|
|
375
|
-
},
|
|
376
|
-
include_archived: {
|
|
377
|
-
type: 'boolean',
|
|
378
|
-
description: 'list/list_epics: include archived epics and their tasks (default false)'
|
|
379
|
-
},
|
|
380
|
-
include_done: {
|
|
381
|
-
type: 'boolean',
|
|
382
|
-
description: 'list_epics: include status=done epics without archived (default false)'
|
|
449
|
+
default: 'context'
|
|
383
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' },
|
|
384
457
|
status: {
|
|
385
458
|
type: 'string',
|
|
386
|
-
enum: ['empty', 'planned', 'active', 'done', 'archived']
|
|
387
|
-
description: 'list_epics: exact status filter (overrides live-only default)'
|
|
388
|
-
},
|
|
389
|
-
view: {
|
|
390
|
-
type: 'string',
|
|
391
|
-
enum: READ_VIEWS,
|
|
392
|
-
description: 'summary=board scan (default); planning=scope/AC; execution=+subtasks; full=everything. Prefer smallest that works.'
|
|
459
|
+
enum: ['empty', 'planned', 'active', 'done', 'archived']
|
|
393
460
|
},
|
|
461
|
+
view: { type: 'string', enum: READ_VIEWS },
|
|
394
462
|
fields: {
|
|
395
463
|
type: 'array',
|
|
396
|
-
description: 'Exact fields only (overrides view). Use when you need 1–2 fields.',
|
|
397
464
|
items: { type: 'string' }
|
|
398
465
|
}
|
|
399
466
|
},
|
|
400
|
-
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'] }
|
|
401
478
|
}
|
|
402
479
|
},
|
|
403
480
|
{
|
|
@@ -423,73 +500,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
423
500
|
'plan_evidence',
|
|
424
501
|
'plan_done',
|
|
425
502
|
'plan_status'
|
|
426
|
-
]
|
|
427
|
-
description: 'create|move|update|delete daily; epic_create|epic_update|epic_archive|epic_unarchive|epic_delete; plan_* multi-step'
|
|
428
|
-
},
|
|
429
|
-
title: {
|
|
430
|
-
type: 'string',
|
|
431
|
-
description: 'Required for create, plan_create, epic_create'
|
|
503
|
+
]
|
|
432
504
|
},
|
|
505
|
+
title: { type: 'string' },
|
|
433
506
|
col: {
|
|
434
507
|
type: 'string',
|
|
435
508
|
enum: COLS,
|
|
436
|
-
default: 'planned'
|
|
437
|
-
description: 'create column, or update shortcut for column'
|
|
509
|
+
default: 'planned'
|
|
438
510
|
},
|
|
439
511
|
epic: {
|
|
440
512
|
type: 'string',
|
|
441
|
-
default: '—'
|
|
442
|
-
description: 'Epic id or title (create/update/plan_create). Prefer E001.'
|
|
443
|
-
},
|
|
444
|
-
epic_id: {
|
|
445
|
-
type: 'string',
|
|
446
|
-
description: 'Required for epic_update|epic_archive|epic_unarchive|epic_delete; optional link id'
|
|
447
|
-
},
|
|
448
|
-
description: {
|
|
449
|
-
type: 'string',
|
|
450
|
-
description: 'Why/context (recommended on create / epic_create)'
|
|
451
|
-
},
|
|
452
|
-
goals: {
|
|
453
|
-
type: 'string',
|
|
454
|
-
description: 'Epic outcome one-liner (recommended on epic_create)'
|
|
455
|
-
},
|
|
456
|
-
specs: {
|
|
457
|
-
type: 'string',
|
|
458
|
-
description: 'Technical constraints (recommended on create)'
|
|
513
|
+
default: '—'
|
|
459
514
|
},
|
|
515
|
+
epic_id: { type: 'string' },
|
|
516
|
+
description: { type: 'string' },
|
|
517
|
+
goals: { type: 'string' },
|
|
518
|
+
specs: { type: 'string' },
|
|
460
519
|
in_scope: {
|
|
461
520
|
type: 'array',
|
|
462
|
-
description: 'In-scope bullets (recommended on create)',
|
|
463
521
|
items: { type: 'string' }
|
|
464
522
|
},
|
|
465
523
|
out_of_scope: {
|
|
466
524
|
type: 'array',
|
|
467
|
-
description: 'Out-of-scope bullets (recommended on create)',
|
|
468
525
|
items: { type: 'string' }
|
|
469
526
|
},
|
|
470
527
|
acceptance_criteria: {
|
|
471
528
|
type: 'array',
|
|
472
|
-
description: 'Done criteria (recommended on create)',
|
|
473
529
|
items: { type: 'string' }
|
|
474
530
|
},
|
|
475
531
|
test_cases: {
|
|
476
532
|
type: 'array',
|
|
477
|
-
description: 'Verification scenarios',
|
|
478
533
|
items: { type: 'string' }
|
|
479
534
|
},
|
|
480
535
|
depends_on: {
|
|
481
536
|
type: 'array',
|
|
482
|
-
description: 'Task ids that must be done before this task can enter active/gates',
|
|
483
537
|
items: { type: 'string' }
|
|
484
538
|
},
|
|
485
539
|
files: {
|
|
486
540
|
type: 'array',
|
|
487
|
-
description: 'Touched file paths for this task (create/update/plan_evidence)',
|
|
488
541
|
items: { type: 'string' }
|
|
489
542
|
},
|
|
490
543
|
subtasks: {
|
|
491
544
|
type: 'array',
|
|
492
|
-
description: 'Full subtask list replace on update (send complete array, not a single toggle)',
|
|
493
545
|
items: {
|
|
494
546
|
type: 'object',
|
|
495
547
|
properties: {
|
|
@@ -500,13 +552,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
500
552
|
}
|
|
501
553
|
}
|
|
502
554
|
},
|
|
503
|
-
notes: {
|
|
504
|
-
type: 'string',
|
|
505
|
-
description: 'Freeform notes'
|
|
506
|
-
},
|
|
555
|
+
notes: { type: 'string' },
|
|
507
556
|
adr: {
|
|
508
|
-
description:
|
|
509
|
-
'Append one ADR: { decision, why }. Or replace full array of { id?, decision, why, created? }.',
|
|
510
557
|
oneOf: [
|
|
511
558
|
{
|
|
512
559
|
type: 'object',
|
|
@@ -530,67 +577,55 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
530
577
|
}
|
|
531
578
|
]
|
|
532
579
|
},
|
|
533
|
-
task_id: {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
},
|
|
537
|
-
column: {
|
|
538
|
-
type: 'string',
|
|
539
|
-
enum: COLS,
|
|
540
|
-
description:
|
|
541
|
-
'Target column for move (not col). Must be a legal transition from the current column. '
|
|
542
|
-
+ `Allowed (from project config): ${transitionHint()}. `
|
|
543
|
-
+ 'Illegal → INVALID_TRANSITION + allowed_columns.'
|
|
544
|
-
},
|
|
545
|
-
patch: {
|
|
546
|
-
type: 'object',
|
|
547
|
-
description: 'Bulk update object; merged with top-level field shortcuts'
|
|
548
|
-
},
|
|
580
|
+
task_id: { type: 'string' },
|
|
581
|
+
column: { type: 'string', enum: COLS },
|
|
582
|
+
patch: { type: 'object' },
|
|
549
583
|
return: {
|
|
550
584
|
type: 'string',
|
|
551
585
|
enum: ['none', 'summary', 'full'],
|
|
552
|
-
|
|
553
|
-
},
|
|
554
|
-
index: {
|
|
555
|
-
type: 'integer',
|
|
556
|
-
description: 'plan_advance: subtask index; omit = first incomplete'
|
|
586
|
+
default: 'none'
|
|
557
587
|
},
|
|
588
|
+
index: { type: 'integer' },
|
|
558
589
|
steps: {
|
|
559
590
|
type: 'array',
|
|
560
|
-
items: { type: 'string' }
|
|
561
|
-
description: 'create/plan_create: checklist texts → subtasks (create uses when subtasks omitted; plan_create uses as-is, no forced TDD steps)'
|
|
562
|
-
},
|
|
563
|
-
project_root: {
|
|
564
|
-
type: 'string',
|
|
565
|
-
description: 'plan_create: root for optional test-runner detect (default cwd); missing runner → null, not error'
|
|
566
|
-
},
|
|
567
|
-
diff: {
|
|
568
|
-
type: 'string',
|
|
569
|
-
description: 'plan_evidence: short diff or note (one of diff/summary/test_command required)'
|
|
570
|
-
},
|
|
571
|
-
summary: {
|
|
572
|
-
type: 'string',
|
|
573
|
-
description: 'plan_evidence: short implementation summary (alternative to diff)'
|
|
591
|
+
items: { type: 'string' }
|
|
574
592
|
},
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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'
|
|
578
610
|
},
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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'] }
|
|
582
620
|
},
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
description: 'plan_evidence: optional process exit code'
|
|
621
|
+
{
|
|
622
|
+
if: {
|
|
623
|
+
properties: { action: { const: 'move' } },
|
|
624
|
+
required: ['action']
|
|
625
|
+
},
|
|
626
|
+
then: { required: ['task_id', 'column'] }
|
|
590
627
|
}
|
|
591
|
-
|
|
592
|
-
required: ['action'],
|
|
593
|
-
additionalProperties: false
|
|
628
|
+
]
|
|
594
629
|
}
|
|
595
630
|
},
|
|
596
631
|
{
|
|
@@ -610,7 +645,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
610
645
|
}
|
|
611
646
|
},
|
|
612
647
|
required: ['action'],
|
|
613
|
-
additionalProperties: false
|
|
648
|
+
additionalProperties: false,
|
|
649
|
+
examples: [
|
|
650
|
+
{ action: 'status' },
|
|
651
|
+
{ action: 'start' }
|
|
652
|
+
]
|
|
614
653
|
}
|
|
615
654
|
}
|
|
616
655
|
]
|
|
@@ -625,7 +664,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
625
664
|
|
|
626
665
|
switch (name) {
|
|
627
666
|
case 'kanban_read': {
|
|
628
|
-
const operation = args.operation || '
|
|
667
|
+
const operation = args.operation || 'context';
|
|
629
668
|
|
|
630
669
|
if (operation === 'help') {
|
|
631
670
|
result = playbook.playbookHelpPayload();
|
|
@@ -659,7 +698,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
659
698
|
} else {
|
|
660
699
|
tasks = kanban.filterTasksForList(tasks, epics, {
|
|
661
700
|
include_archived: args.include_archived,
|
|
662
|
-
include_done: args.include_done
|
|
701
|
+
include_done: args.include_done,
|
|
702
|
+
col: args.col
|
|
663
703
|
});
|
|
664
704
|
}
|
|
665
705
|
|
|
@@ -674,9 +714,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
674
714
|
}
|
|
675
715
|
|
|
676
716
|
await kanban.migrateEpicGroups();
|
|
677
|
-
const
|
|
678
|
-
const allTasks = await kanban.allTasks();
|
|
679
|
-
|
|
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, {
|
|
680
734
|
...readOptions,
|
|
681
735
|
epicLookup,
|
|
682
736
|
allTasks
|
|
@@ -711,7 +765,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
711
765
|
const epic = await kanban.getEpicEntity(link.epic_id);
|
|
712
766
|
const tasks = await kanban.allTasks();
|
|
713
767
|
result = kanban.shapeEpic(epic, tasks, {
|
|
714
|
-
view: args.view || '
|
|
768
|
+
view: args.view || 'summary',
|
|
715
769
|
fields: args.fields
|
|
716
770
|
});
|
|
717
771
|
} else {
|
|
@@ -754,14 +808,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
754
808
|
};
|
|
755
809
|
const epicRef = args.epic_id || args.epic || '—';
|
|
756
810
|
const created = await kanban.doCreate(args.title, args.col || 'planned', epicRef, createPayload);
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
? { ...shaped, warnings, missing_recommended: kanban.missingRecommendedCreateFields(createPayload) }
|
|
764
|
-
: 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
|
+
}
|
|
765
817
|
break;
|
|
766
818
|
}
|
|
767
819
|
|
|
@@ -774,15 +826,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
774
826
|
notes: args.notes
|
|
775
827
|
};
|
|
776
828
|
const createdEpic = await kanban.doCreateEpic(args.title, epicPayload);
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
+
}
|
|
786
839
|
break;
|
|
787
840
|
}
|
|
788
841
|
|
|
@@ -862,7 +915,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
862
915
|
}
|
|
863
916
|
const deleted = await kanban.deleteTask(args.task_id);
|
|
864
917
|
result = returnShape === 'none'
|
|
865
|
-
? { ok: true
|
|
918
|
+
? { ok: true }
|
|
866
919
|
: deleted;
|
|
867
920
|
break;
|
|
868
921
|
}
|
|
@@ -926,19 +979,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
926
979
|
missing_recommended: kanban.missingRecommendedCreateFields(args)
|
|
927
980
|
};
|
|
928
981
|
}
|
|
982
|
+
result = formatPlanResult(result, returnShape);
|
|
929
983
|
break;
|
|
930
984
|
}
|
|
931
985
|
case 'plan_advance':
|
|
932
|
-
result =
|
|
986
|
+
result = formatPlanResult(
|
|
987
|
+
await plan.advance({ task_id: args.task_id, index: args.index }),
|
|
988
|
+
returnShape
|
|
989
|
+
);
|
|
933
990
|
break;
|
|
934
991
|
case 'plan_evidence':
|
|
935
|
-
result = await plan.evidence(args);
|
|
992
|
+
result = formatPlanResult(await plan.evidence(args), returnShape);
|
|
936
993
|
break;
|
|
937
994
|
case 'plan_done':
|
|
938
|
-
result = await plan.done({ task_id: args.task_id });
|
|
995
|
+
result = formatPlanResult(await plan.done({ task_id: args.task_id }), returnShape);
|
|
939
996
|
break;
|
|
940
997
|
case 'plan_status':
|
|
941
|
-
result = await plan.status(args.task_id);
|
|
998
|
+
result = formatPlanResult(await plan.status(args.task_id), returnShape === 'none' ? 'summary' : returnShape);
|
|
942
999
|
break;
|
|
943
1000
|
|
|
944
1001
|
default:
|
|
@@ -1043,6 +1100,7 @@ async function main() {
|
|
|
1043
1100
|
module.exports = {
|
|
1044
1101
|
serializeError,
|
|
1045
1102
|
serializeResult,
|
|
1103
|
+
formatPlanResult,
|
|
1046
1104
|
textResponse,
|
|
1047
1105
|
startGuiServer,
|
|
1048
1106
|
stopGuiServer,
|