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/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
- return {
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, null, 2);
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 'summary';
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') return { ok: true };
155
- const epicLookup = await buildEpicLookup();
156
- return kanban.shapeTask(task, {
157
- view: returnShape === 'full' ? 'full' : 'summary',
158
- epicLookup
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
- description: 'list/show=tasks; list_epics/show_epic=initiative containers; help=token playbook',
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
- description: 'Verification scenarios',
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
- type: 'string',
500
- description: "Required for move/update/delete/plan_* except plan_create. '014' or '14'."
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
- description: 'move/update/delete/epic_* response size. Prefer none. Default summary. create/epic_create return full once.'
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
- stdout: {
541
- type: 'string',
542
- description: 'plan_evidence: test stdout (truncate to last ~2KB if huge)'
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
- stderr: {
545
- type: 'string',
546
- description: 'plan_evidence: test stderr (truncate 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'] }
547
620
  },
548
- exit_code: {
549
- type: 'integer',
550
- description: 'plan_evidence: process exit code'
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 || 'list';
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
- let tasks = await kanban.allTasks();
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
- // summary omits epic_goals; no lookup needed for list tokens
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 epicLookup = await buildEpicLookup();
629
- 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, {
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 || 'full',
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: args.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 shaped = kanban.shapeTask(created, {
695
- view: 'full',
696
- epicLookup: await buildEpicLookup()
697
- });
698
- const warnings = kanban.createFieldWarnings(createPayload);
699
- result = warnings.length > 0
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 shapedEpic = kanban.shapeEpic(createdEpic, [], { view: 'full' });
715
- const epicWarnings = kanban.createEpicFieldWarnings(epicPayload);
716
- result = epicWarnings.length > 0
717
- ? {
718
- ...shapedEpic,
719
- warnings: epicWarnings,
720
- missing_recommended: kanban.missingRecommendedEpicCreateFields(epicPayload)
721
- }
722
- : 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
+ }
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, task_id: deleted.task_id }
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 = 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
+ );
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,