kanbango 3.1.0 → 3.3.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/.ai/lessons.jsonl +3 -0
- package/.ai/retro/last-run.json +1 -1
- package/.opencode/plans/action-error-recipe.md +121 -0
- package/AGENTS.md +4 -2
- package/CHANGELOG.md +27 -0
- package/LLM_AGENTS.md +1 -1
- package/README.md +11 -3
- package/agent-playbook.js +18 -9
- package/bin/kanban.js +143 -10
- package/gui-registry.js +14 -1
- package/index.html +645 -39
- package/kanban.js +474 -110
- package/mcp-server.js +253 -45
- package/package.json +1 -1
- package/tests/run.js +3 -0
package/mcp-server.js
CHANGED
|
@@ -76,6 +76,101 @@ function invalidRequest(message, hint, details) {
|
|
|
76
76
|
return kanban.createKanbanError('VALIDATION_ERROR', message, hint, details, false, 400);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const MANAGE_ACTIONS = [
|
|
80
|
+
'create',
|
|
81
|
+
'move',
|
|
82
|
+
'update',
|
|
83
|
+
'delete',
|
|
84
|
+
'epic_create',
|
|
85
|
+
'epic_update',
|
|
86
|
+
'epic_archive',
|
|
87
|
+
'epic_unarchive',
|
|
88
|
+
'epic_delete',
|
|
89
|
+
'plan_create',
|
|
90
|
+
'plan_advance',
|
|
91
|
+
'plan_evidence',
|
|
92
|
+
'plan_done',
|
|
93
|
+
'plan_status',
|
|
94
|
+
'comment_add'
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const GUI_ACTIONS = ['start', 'stop', 'status'];
|
|
98
|
+
|
|
99
|
+
const ACTION_EXAMPLES = {
|
|
100
|
+
kanban_manage: '{"action":"create","title":"Ship image","col":"planned"}',
|
|
101
|
+
kanban_gui: '{"action":"status"}'
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function receivedKeys(args) {
|
|
105
|
+
return Object.keys(args && typeof args === 'object' ? args : {});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function actionRecipeMessage(toolName, allowed, opts = {}) {
|
|
109
|
+
const keys = opts.received_keys || [];
|
|
110
|
+
const example = ACTION_EXAMPLES[toolName] || `{"action":"${allowed[0]}"}`;
|
|
111
|
+
const lines = [];
|
|
112
|
+
|
|
113
|
+
if (opts.kind === 'unknown') {
|
|
114
|
+
lines.push(`Unknown action "${opts.action}" on ${toolName}.`);
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(`Missing required top-level field "action" on ${toolName}.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lines.push('Pass action next to other args (not nested under params).');
|
|
120
|
+
lines.push(`Valid: ${allowed.join(', ')}.`);
|
|
121
|
+
lines.push(`Example: ${example}`);
|
|
122
|
+
lines.push(keys.length > 0 ? `You sent keys: ${keys.join(', ')}` : 'You sent keys: (none)');
|
|
123
|
+
return lines.join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function actionRecipeHint() {
|
|
127
|
+
return [
|
|
128
|
+
'Retry the same tool with top-level action set to one Valid value.',
|
|
129
|
+
'Common mistake: omitting action, or putting it under params (jira/gitlab style) — kanban uses top-level action.'
|
|
130
|
+
].join(' ');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function requireToolAction(args, allowed, toolName) {
|
|
134
|
+
const action = args && args.action;
|
|
135
|
+
const keys = receivedKeys(args);
|
|
136
|
+
if (action === undefined || action === null || action === '') {
|
|
137
|
+
throw kanban.createKanbanError(
|
|
138
|
+
'MISSING_REQUIRED_FIELD',
|
|
139
|
+
actionRecipeMessage(toolName, allowed, {
|
|
140
|
+
kind: 'missing',
|
|
141
|
+
received_keys: keys,
|
|
142
|
+
action: action === undefined ? null : action
|
|
143
|
+
}),
|
|
144
|
+
actionRecipeHint(),
|
|
145
|
+
{
|
|
146
|
+
field: 'action',
|
|
147
|
+
tool: toolName,
|
|
148
|
+
received_keys: keys,
|
|
149
|
+
allowed_actions: allowed,
|
|
150
|
+
action: action === undefined ? null : action
|
|
151
|
+
},
|
|
152
|
+
false,
|
|
153
|
+
400
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return action;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function unknownToolAction(action, args, allowed, toolName) {
|
|
160
|
+
const keys = receivedKeys(args);
|
|
161
|
+
return invalidRequest(
|
|
162
|
+
actionRecipeMessage(toolName, allowed, { kind: 'unknown', action, received_keys: keys }),
|
|
163
|
+
actionRecipeHint(),
|
|
164
|
+
{
|
|
165
|
+
field: 'action',
|
|
166
|
+
tool: toolName,
|
|
167
|
+
action,
|
|
168
|
+
received_keys: keys,
|
|
169
|
+
allowed_actions: allowed
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
79
174
|
function serializeResult(result) {
|
|
80
175
|
if (typeof result === 'string') return result;
|
|
81
176
|
|
|
@@ -146,30 +241,42 @@ function formatTaskResult(task, returnShape) {
|
|
|
146
241
|
return kanban.shapeTask(task, { view: returnShape === 'full' ? 'full' : 'summary' });
|
|
147
242
|
}
|
|
148
243
|
|
|
244
|
+
function guiIdentity(extra = {}) {
|
|
245
|
+
const cwd = extra.cwd || process.cwd();
|
|
246
|
+
return {
|
|
247
|
+
...extra,
|
|
248
|
+
cwd,
|
|
249
|
+
project: extra.project || guiRegistry.projectLabel(cwd)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
149
253
|
async function startGuiServer(port) {
|
|
150
254
|
if (port !== undefined && port !== null && port !== '' && !normalizePort(port)) {
|
|
151
255
|
throw invalidRequest('Invalid port', 'Use an integer between 1 and 65535', { port });
|
|
152
256
|
}
|
|
153
257
|
|
|
154
258
|
if (ownsGuiProcess() && guiPort) {
|
|
155
|
-
return {
|
|
259
|
+
return guiIdentity({
|
|
156
260
|
status: 'already_running',
|
|
157
261
|
owned: true,
|
|
158
262
|
port: guiPort,
|
|
159
263
|
pid: guiProcess.pid,
|
|
160
264
|
url: `http://localhost:${guiPort}`
|
|
161
|
-
};
|
|
265
|
+
});
|
|
162
266
|
}
|
|
163
267
|
|
|
164
268
|
const existing = await guiRegistry.discoverRunningGui();
|
|
165
269
|
if (existing) {
|
|
166
|
-
return {
|
|
270
|
+
return guiIdentity({
|
|
167
271
|
status: 'already_running',
|
|
168
272
|
owned: false,
|
|
169
273
|
port: existing.port,
|
|
170
274
|
pid: existing.pid,
|
|
171
|
-
url: existing.url
|
|
172
|
-
|
|
275
|
+
url: existing.url,
|
|
276
|
+
cwd: existing.cwd,
|
|
277
|
+
project: existing.project,
|
|
278
|
+
started_at: existing.started_at
|
|
279
|
+
});
|
|
173
280
|
}
|
|
174
281
|
|
|
175
282
|
const desiredPort = guiRegistry.resolvePreferredGuiPort(port);
|
|
@@ -194,19 +301,24 @@ async function startGuiServer(port) {
|
|
|
194
301
|
const ready = await waitForGuiReady(childPid);
|
|
195
302
|
guiPort = ready.port;
|
|
196
303
|
|
|
197
|
-
return {
|
|
304
|
+
return guiIdentity({
|
|
198
305
|
status: 'started',
|
|
199
306
|
owned: true,
|
|
200
307
|
port: ready.port,
|
|
201
308
|
pid: ready.pid,
|
|
202
|
-
url: ready.url
|
|
203
|
-
|
|
309
|
+
url: ready.url,
|
|
310
|
+
cwd: ready.cwd,
|
|
311
|
+
project: ready.project,
|
|
312
|
+
started_at: ready.started_at
|
|
313
|
+
});
|
|
204
314
|
}
|
|
205
315
|
|
|
206
316
|
async function stopGuiServer() {
|
|
207
317
|
if (ownsGuiProcess()) {
|
|
208
318
|
const port = guiPort;
|
|
209
319
|
const pid = guiProcess.pid;
|
|
320
|
+
const project = guiRegistry.projectLabel();
|
|
321
|
+
const cwd = process.cwd();
|
|
210
322
|
guiProcess.kill();
|
|
211
323
|
|
|
212
324
|
const deadline = Date.now() + 2000;
|
|
@@ -219,7 +331,7 @@ async function stopGuiServer() {
|
|
|
219
331
|
await guiRegistry.clearGuiPortFile({ force: true });
|
|
220
332
|
guiProcess = null;
|
|
221
333
|
guiPort = null;
|
|
222
|
-
return { status: 'stopping', owned: true, port, pid };
|
|
334
|
+
return { status: 'stopping', owned: true, port, pid, project, cwd };
|
|
223
335
|
}
|
|
224
336
|
|
|
225
337
|
guiProcess = null;
|
|
@@ -230,25 +342,28 @@ async function stopGuiServer() {
|
|
|
230
342
|
return { status: 'not_running' };
|
|
231
343
|
}
|
|
232
344
|
|
|
233
|
-
return {
|
|
345
|
+
return guiIdentity({
|
|
234
346
|
status: 'external_running',
|
|
235
347
|
owned: false,
|
|
236
348
|
port: discovered.port,
|
|
237
349
|
pid: discovered.pid,
|
|
238
350
|
url: discovered.url,
|
|
351
|
+
cwd: discovered.cwd,
|
|
352
|
+
project: discovered.project,
|
|
353
|
+
started_at: discovered.started_at,
|
|
239
354
|
hint: 'GUI was not started by this MCP process; stop refused. Stop it from the owning terminal or kill that PID manually.'
|
|
240
|
-
};
|
|
355
|
+
});
|
|
241
356
|
}
|
|
242
357
|
|
|
243
358
|
async function guiStatus() {
|
|
244
359
|
if (ownsGuiProcess() && guiPort) {
|
|
245
|
-
return {
|
|
360
|
+
return guiIdentity({
|
|
246
361
|
status: 'running',
|
|
247
362
|
owned: true,
|
|
248
363
|
port: guiPort,
|
|
249
364
|
pid: guiProcess.pid,
|
|
250
365
|
url: `http://localhost:${guiPort}`
|
|
251
|
-
};
|
|
366
|
+
});
|
|
252
367
|
}
|
|
253
368
|
|
|
254
369
|
guiProcess = null;
|
|
@@ -259,15 +374,16 @@ async function guiStatus() {
|
|
|
259
374
|
return { status: 'not_running' };
|
|
260
375
|
}
|
|
261
376
|
|
|
262
|
-
return {
|
|
377
|
+
return guiIdentity({
|
|
263
378
|
status: 'external_running',
|
|
264
379
|
owned: false,
|
|
265
380
|
port: discovered.port,
|
|
266
381
|
pid: discovered.pid,
|
|
267
382
|
url: discovered.url,
|
|
268
383
|
cwd: discovered.cwd,
|
|
384
|
+
project: discovered.project,
|
|
269
385
|
started_at: discovered.started_at
|
|
270
|
-
};
|
|
386
|
+
});
|
|
271
387
|
}
|
|
272
388
|
|
|
273
389
|
const server = new Server(
|
|
@@ -314,6 +430,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
314
430
|
type: 'string',
|
|
315
431
|
description: 'Filter list by epic id or title'
|
|
316
432
|
},
|
|
433
|
+
include_archived: {
|
|
434
|
+
type: 'boolean',
|
|
435
|
+
description: 'list/list_epics: include archived epics and their tasks (default false)'
|
|
436
|
+
},
|
|
437
|
+
include_done: {
|
|
438
|
+
type: 'boolean',
|
|
439
|
+
description: 'list_epics: include status=done epics without archived (default false)'
|
|
440
|
+
},
|
|
441
|
+
status: {
|
|
442
|
+
type: 'string',
|
|
443
|
+
enum: ['empty', 'planned', 'active', 'done', 'archived'],
|
|
444
|
+
description: 'list_epics: exact status filter (overrides live-only default)'
|
|
445
|
+
},
|
|
317
446
|
view: {
|
|
318
447
|
type: 'string',
|
|
319
448
|
enum: READ_VIEWS,
|
|
@@ -336,19 +465,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
336
465
|
properties: {
|
|
337
466
|
action: {
|
|
338
467
|
type: 'string',
|
|
339
|
-
enum:
|
|
340
|
-
|
|
341
|
-
'move',
|
|
342
|
-
'update',
|
|
343
|
-
'epic_create',
|
|
344
|
-
'epic_update',
|
|
345
|
-
'plan_create',
|
|
346
|
-
'plan_advance',
|
|
347
|
-
'plan_evidence',
|
|
348
|
-
'plan_done',
|
|
349
|
-
'plan_status'
|
|
350
|
-
],
|
|
351
|
-
description: 'create|move|update daily; epic_create|epic_update for containers; plan_* for multi-step work'
|
|
468
|
+
enum: MANAGE_ACTIONS,
|
|
469
|
+
description: 'create|move|update|delete daily; epic_*; plan_*; comment_add'
|
|
352
470
|
},
|
|
353
471
|
title: {
|
|
354
472
|
type: 'string',
|
|
@@ -367,7 +485,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
367
485
|
},
|
|
368
486
|
epic_id: {
|
|
369
487
|
type: 'string',
|
|
370
|
-
description: 'Required for epic_update; optional
|
|
488
|
+
description: 'Required for epic_update|epic_archive|epic_unarchive|epic_delete; optional link id'
|
|
371
489
|
},
|
|
372
490
|
description: {
|
|
373
491
|
type: 'string',
|
|
@@ -418,9 +536,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
418
536
|
type: 'string',
|
|
419
537
|
description: 'Freeform notes'
|
|
420
538
|
},
|
|
539
|
+
text: {
|
|
540
|
+
type: 'string',
|
|
541
|
+
description: 'Required for comment_add. Non-empty comment body.'
|
|
542
|
+
},
|
|
543
|
+
author: {
|
|
544
|
+
type: 'string',
|
|
545
|
+
description: 'Optional comment author (default user). e.g. kocur-reviewer, worker'
|
|
546
|
+
},
|
|
421
547
|
task_id: {
|
|
422
548
|
type: 'string',
|
|
423
|
-
description: "Required for move/update/plan_* except plan_create. '014' or '14'."
|
|
549
|
+
description: "Required for move/update/delete/comment_add/plan_* except plan_create. '014' or '14'."
|
|
424
550
|
},
|
|
425
551
|
column: {
|
|
426
552
|
type: 'string',
|
|
@@ -434,7 +560,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
434
560
|
return: {
|
|
435
561
|
type: 'string',
|
|
436
562
|
enum: ['none', 'summary', 'full'],
|
|
437
|
-
description: 'move/update/
|
|
563
|
+
description: 'move/update/delete/epic_* response size. Prefer none. Default summary. create/epic_create return full once.'
|
|
438
564
|
},
|
|
439
565
|
index: {
|
|
440
566
|
type: 'integer',
|
|
@@ -482,7 +608,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
482
608
|
properties: {
|
|
483
609
|
action: {
|
|
484
610
|
type: 'string',
|
|
485
|
-
enum:
|
|
611
|
+
enum: GUI_ACTIONS,
|
|
486
612
|
description: 'start | stop (owned only) | status'
|
|
487
613
|
},
|
|
488
614
|
port: {
|
|
@@ -517,13 +643,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
517
643
|
|
|
518
644
|
if (operation === 'list') {
|
|
519
645
|
await kanban.migrateEpicGroups();
|
|
646
|
+
const epics = await kanban.listEpicEntities();
|
|
520
647
|
let tasks = await kanban.allTasks();
|
|
521
648
|
if (args.col) {
|
|
522
649
|
tasks = tasks.filter((task) => task.column === args.col);
|
|
523
650
|
}
|
|
651
|
+
// Explicit epic filter bypasses live-only hide (agent asked for that initiative)
|
|
524
652
|
if (args.epic || args.epic_id) {
|
|
525
653
|
const filter = args.epic_id || args.epic;
|
|
526
654
|
tasks = tasks.filter((task) => kanban.taskMatchesEpicFilter(task, filter));
|
|
655
|
+
} else {
|
|
656
|
+
tasks = kanban.filterTasksForList(tasks, epics, {
|
|
657
|
+
include_archived: args.include_archived,
|
|
658
|
+
include_done: args.include_done
|
|
659
|
+
});
|
|
527
660
|
}
|
|
528
661
|
|
|
529
662
|
result = tasks.map((task) => kanban.shapeTask(task, readOptions));
|
|
@@ -544,10 +677,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
544
677
|
const epicView = args.view === 'full' || args.view === 'planning' || args.view === 'execution'
|
|
545
678
|
? (args.view === 'execution' ? 'planning' : args.view)
|
|
546
679
|
: 'summary';
|
|
547
|
-
|
|
680
|
+
const shaped = epics.map((epic) => kanban.shapeEpic(epic, tasks, {
|
|
548
681
|
view: Array.isArray(args.fields) && args.fields.length > 0 ? undefined : epicView,
|
|
549
682
|
fields: args.fields
|
|
550
683
|
}));
|
|
684
|
+
result = kanban.filterShapedEpics(shaped, {
|
|
685
|
+
include_archived: args.include_archived,
|
|
686
|
+
include_done: args.include_done,
|
|
687
|
+
status: args.status
|
|
688
|
+
});
|
|
551
689
|
} else if (operation === 'show_epic') {
|
|
552
690
|
const epicRef = args.epic_id || args.epic;
|
|
553
691
|
if (!epicRef) {
|
|
@@ -576,7 +714,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
576
714
|
}
|
|
577
715
|
|
|
578
716
|
case 'kanban_manage': {
|
|
579
|
-
const action = args
|
|
717
|
+
const action = requireToolAction(args, MANAGE_ACTIONS, 'kanban_manage');
|
|
580
718
|
const returnShape = normalizeReturnShape(args.return);
|
|
581
719
|
|
|
582
720
|
switch (action) {
|
|
@@ -638,6 +776,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
638
776
|
if (args.in_scope !== undefined) epicPatch.in_scope = args.in_scope;
|
|
639
777
|
if (args.out_of_scope !== undefined) epicPatch.out_of_scope = args.out_of_scope;
|
|
640
778
|
if (args.notes !== undefined) epicPatch.notes = args.notes;
|
|
779
|
+
if (args.patch && args.patch.archived !== undefined) {
|
|
780
|
+
epicPatch.archived = args.patch.archived;
|
|
781
|
+
}
|
|
641
782
|
const updatedEpic = await kanban.updateEpicEntity(epicId, epicPatch);
|
|
642
783
|
if (returnShape === 'none') {
|
|
643
784
|
result = { ok: true, epic_id: updatedEpic.id };
|
|
@@ -649,6 +790,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
649
790
|
break;
|
|
650
791
|
}
|
|
651
792
|
|
|
793
|
+
case 'epic_archive':
|
|
794
|
+
case 'epic_unarchive': {
|
|
795
|
+
const epicId = args.epic_id || args.epic;
|
|
796
|
+
if (!epicId) {
|
|
797
|
+
throw invalidRequest(
|
|
798
|
+
`epic_id is required for '${action}'`,
|
|
799
|
+
'Provide an epic id like E001',
|
|
800
|
+
{ action }
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const toggled = action === 'epic_archive'
|
|
804
|
+
? await kanban.archiveEpic(epicId)
|
|
805
|
+
: await kanban.unarchiveEpic(epicId);
|
|
806
|
+
if (returnShape === 'none') {
|
|
807
|
+
result = { ok: true, epic_id: toggled.id, archived: toggled.archived };
|
|
808
|
+
} else if (returnShape === 'summary') {
|
|
809
|
+
result = kanban.shapeEpic(toggled, await kanban.allTasks(), { view: 'summary' });
|
|
810
|
+
} else {
|
|
811
|
+
result = kanban.shapeEpic(toggled, await kanban.allTasks(), { view: 'full' });
|
|
812
|
+
}
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
case 'epic_delete': {
|
|
817
|
+
const epicId = args.epic_id || args.epic;
|
|
818
|
+
if (!epicId) {
|
|
819
|
+
throw invalidRequest(
|
|
820
|
+
"epic_id is required for 'epic_delete'",
|
|
821
|
+
'Provide an epic id like E001',
|
|
822
|
+
{ action }
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
result = await kanban.deleteEpic(epicId);
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
case 'delete': {
|
|
830
|
+
if (!args.task_id) {
|
|
831
|
+
throw invalidRequest(
|
|
832
|
+
"task_id is required for 'delete'",
|
|
833
|
+
'Provide a task ID',
|
|
834
|
+
{ action }
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const deleted = await kanban.deleteTask(args.task_id);
|
|
838
|
+
result = returnShape === 'none'
|
|
839
|
+
? { ok: true, task_id: deleted.task_id }
|
|
840
|
+
: deleted;
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
|
|
652
844
|
case 'move': {
|
|
653
845
|
if (!args.task_id) {
|
|
654
846
|
throw invalidRequest(
|
|
@@ -720,18 +912,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
720
912
|
result = await plan.status(args.task_id);
|
|
721
913
|
break;
|
|
722
914
|
|
|
915
|
+
case 'comment_add': {
|
|
916
|
+
if (!args.task_id) {
|
|
917
|
+
throw invalidRequest(
|
|
918
|
+
"task_id is required for 'comment_add'",
|
|
919
|
+
'Provide a task ID',
|
|
920
|
+
{ action }
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (!args.text) {
|
|
924
|
+
throw invalidRequest(
|
|
925
|
+
"text is required for 'comment_add'",
|
|
926
|
+
'Provide a non-empty comment body',
|
|
927
|
+
{ action }
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const added = await kanban.addComment(args.task_id, args.text, args.author);
|
|
931
|
+
result = returnShape === 'none'
|
|
932
|
+
? { ok: true, comment_id: added.comment.id }
|
|
933
|
+
: returnShape === 'full'
|
|
934
|
+
? added
|
|
935
|
+
: { ok: true, task_id: added.task_id, comment_id: added.comment.id, count: added.comments.length };
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
|
|
723
939
|
default:
|
|
724
|
-
throw
|
|
725
|
-
`Unknown action: ${action}`,
|
|
726
|
-
'Use create, move, update, epic_create, epic_update, plan_create, plan_advance, plan_evidence, plan_done, or plan_status',
|
|
727
|
-
{ action }
|
|
728
|
-
);
|
|
940
|
+
throw unknownToolAction(action, args, MANAGE_ACTIONS, 'kanban_manage');
|
|
729
941
|
}
|
|
730
942
|
break;
|
|
731
943
|
}
|
|
732
944
|
|
|
733
945
|
case 'kanban_gui': {
|
|
734
|
-
const action = args
|
|
946
|
+
const action = requireToolAction(args, GUI_ACTIONS, 'kanban_gui');
|
|
735
947
|
|
|
736
948
|
switch (action) {
|
|
737
949
|
case 'start': {
|
|
@@ -747,11 +959,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
747
959
|
break;
|
|
748
960
|
}
|
|
749
961
|
default:
|
|
750
|
-
throw
|
|
751
|
-
`Unknown action: ${action}`,
|
|
752
|
-
'Use one of: start, stop, status',
|
|
753
|
-
{ action }
|
|
754
|
-
);
|
|
962
|
+
throw unknownToolAction(action, args, GUI_ACTIONS, 'kanban_gui');
|
|
755
963
|
}
|
|
756
964
|
break;
|
|
757
965
|
}
|
package/package.json
CHANGED
package/tests/run.js
CHANGED
|
@@ -15,9 +15,12 @@ function runNode(scriptPath, args, label) {
|
|
|
15
15
|
|
|
16
16
|
runNode(path.join('bin', 'kanban.js'), ['list', '--json'], 'CLI list');
|
|
17
17
|
runNode(path.join('tests', 'update-tasks.test.js'), [], 'Update tasks test');
|
|
18
|
+
runNode(path.join('tests', 'comments.test.js'), [], 'Comments test');
|
|
18
19
|
runNode(path.join('tests', 'read-views.test.js'), [], 'Read views test');
|
|
19
20
|
runNode(path.join('tests', 'mcp-server.test.js'), [], 'MCP server test');
|
|
20
21
|
runNode(path.join('tests', 'gui-port.test.js'), [], 'GUI port test');
|
|
21
22
|
runNode(path.join('tests', 'plan-workflow.test.js'), [], 'Plan workflow test');
|
|
22
23
|
runNode(path.join('tests', 'agent-playbook.test.js'), [], 'Agent playbook test');
|
|
23
24
|
runNode(path.join('tests', 'epics.test.js'), [], 'Epics test');
|
|
25
|
+
runNode(path.join('tests', 'delete-archive.test.js'), [], 'Delete/archive test');
|
|
26
|
+
runNode(path.join('tests', 'race-conditions.test.js'), [], 'Race conditions test');
|