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/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
- return {
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, null, 2);
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 'summary';
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 epicLookup = await buildEpicLookup();
173
- const allTasks = await kanban.allTasks();
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: returnShape === 'full' ? 'full' : 'summary',
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
- description: 'list/show=tasks; context=compact next action; list_epics/show_epic=initiative containers; help=token playbook',
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
- type: 'string',
535
- description: "Required for move/update/delete/plan_* except plan_create. '014' or '14'."
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
- description: 'move/update/delete/epic_* response size. Prefer none. Default summary. create/epic_create return full once.'
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
- test_command: {
576
- type: 'string',
577
- description: 'plan_evidence: optional exact command run'
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
- stdout: {
580
- type: 'string',
581
- description: 'plan_evidence: optional test stdout (truncate to last ~2KB if huge)'
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
- stderr: {
584
- type: 'string',
585
- description: 'plan_evidence: optional test stderr (truncate if huge)'
586
- },
587
- exit_code: {
588
- type: 'integer',
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 || 'list';
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 epicLookup = await buildEpicLookup();
678
- const allTasks = await kanban.allTasks();
679
- result = kanban.shapeTask(await kanban.getTask(args.task_id), {
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 || 'full',
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 shaped = kanban.shapeTask(created, {
758
- view: 'full',
759
- epicLookup: await buildEpicLookup()
760
- });
761
- const warnings = kanban.createFieldWarnings(createPayload);
762
- result = warnings.length > 0
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 shapedEpic = kanban.shapeEpic(createdEpic, [], { view: 'full' });
778
- const epicWarnings = kanban.createEpicFieldWarnings(epicPayload);
779
- result = epicWarnings.length > 0
780
- ? {
781
- ...shapedEpic,
782
- warnings: epicWarnings,
783
- missing_recommended: kanban.missingRecommendedEpicCreateFields(epicPayload)
784
- }
785
- : shapedEpic;
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, task_id: deleted.task_id }
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 = await plan.advance({ task_id: args.task_id, index: args.index });
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanbango",
3
- "version": "3.8.0",
3
+ "version": "5.1.0",
4
4
  "description": "JSON-first local Kanban board with web GUI, CLI, and MCP server",
5
5
  "main": "index.js",
6
6
  "bin": {