kanbango 2.4.0 → 3.0.2
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 +4 -0
- package/.ai/retro/close.json +1 -0
- package/.ai/retro/last-run.json +1 -0
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +43 -0
- package/LLM_AGENTS.md +110 -28
- package/README.md +6 -3
- package/agent-playbook.js +71 -0
- package/bin/kanban-cmd.js +1 -1
- package/bin/kanban.js +6 -6
- package/gui-registry.js +148 -0
- package/index.js +4 -0
- package/kanban.js +44 -133
- package/mcp-server.js +124 -85
- package/package.json +1 -1
- package/plan.js +1 -1
- package/tests/run.js +2 -0
package/mcp-server.js
CHANGED
|
@@ -8,6 +8,8 @@ const path = require('path');
|
|
|
8
8
|
const pkg = require('./package.json');
|
|
9
9
|
const kanban = require('./kanban.js');
|
|
10
10
|
const plan = require('./plan.js');
|
|
11
|
+
const guiRegistry = require('./gui-registry.js');
|
|
12
|
+
const playbook = require('./agent-playbook.js');
|
|
11
13
|
|
|
12
14
|
const COLS = kanban.COLS;
|
|
13
15
|
const READ_VIEWS = Object.keys(kanban.VIEW_FIELDS);
|
|
@@ -17,7 +19,11 @@ let guiProcess = null;
|
|
|
17
19
|
let guiPort = null;
|
|
18
20
|
|
|
19
21
|
function normalizePort(value) {
|
|
20
|
-
return
|
|
22
|
+
return guiRegistry.normalizeGuiPort(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ownsGuiProcess() {
|
|
26
|
+
return Boolean(guiProcess && guiProcess.exitCode === null);
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
function sleep(ms) {
|
|
@@ -41,7 +47,7 @@ async function waitForGuiReady(pid, timeoutMs = GUI_READY_TIMEOUT_MS) {
|
|
|
41
47
|
);
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
const info = await
|
|
50
|
+
const info = await guiRegistry.discoverRunningGui();
|
|
45
51
|
if (info && info.pid === pid) return info;
|
|
46
52
|
|
|
47
53
|
await sleep(GUI_READY_POLL_MS);
|
|
@@ -145,27 +151,28 @@ async function startGuiServer(port) {
|
|
|
145
151
|
throw invalidRequest('Invalid port', 'Use an integer between 1 and 65535', { port });
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
if (
|
|
154
|
+
if (ownsGuiProcess() && guiPort) {
|
|
149
155
|
return {
|
|
150
156
|
status: 'already_running',
|
|
157
|
+
owned: true,
|
|
151
158
|
port: guiPort,
|
|
152
159
|
pid: guiProcess.pid,
|
|
153
160
|
url: `http://localhost:${guiPort}`
|
|
154
161
|
};
|
|
155
162
|
}
|
|
156
163
|
|
|
157
|
-
const existing = await
|
|
164
|
+
const existing = await guiRegistry.discoverRunningGui();
|
|
158
165
|
if (existing) {
|
|
159
|
-
guiPort = existing.port;
|
|
160
166
|
return {
|
|
161
167
|
status: 'already_running',
|
|
168
|
+
owned: false,
|
|
162
169
|
port: existing.port,
|
|
163
170
|
pid: existing.pid,
|
|
164
171
|
url: existing.url
|
|
165
172
|
};
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
const desiredPort =
|
|
175
|
+
const desiredPort = guiRegistry.resolvePreferredGuiPort(port);
|
|
169
176
|
await kanban.ensureBacklogDir();
|
|
170
177
|
|
|
171
178
|
const scriptPath = path.join(__dirname, 'bin', 'kanban.js');
|
|
@@ -189,6 +196,7 @@ async function startGuiServer(port) {
|
|
|
189
196
|
|
|
190
197
|
return {
|
|
191
198
|
status: 'started',
|
|
199
|
+
owned: true,
|
|
192
200
|
port: ready.port,
|
|
193
201
|
pid: ready.pid,
|
|
194
202
|
url: ready.url
|
|
@@ -196,61 +204,70 @@ async function startGuiServer(port) {
|
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
async function stopGuiServer() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (!trackedRunning && !discovered) {
|
|
203
|
-
guiProcess = null;
|
|
204
|
-
guiPort = null;
|
|
205
|
-
return { status: 'not_running' };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const port = trackedRunning ? guiPort : discovered.port;
|
|
209
|
-
const pid = trackedRunning ? guiProcess.pid : discovered.pid;
|
|
210
|
-
|
|
211
|
-
if (trackedRunning) {
|
|
207
|
+
if (ownsGuiProcess()) {
|
|
208
|
+
const port = guiPort;
|
|
209
|
+
const pid = guiProcess.pid;
|
|
212
210
|
guiProcess.kill();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
211
|
+
|
|
212
|
+
const deadline = Date.now() + 2000;
|
|
213
|
+
while (Date.now() < deadline) {
|
|
214
|
+
const still = await guiRegistry.discoverRunningGui();
|
|
215
|
+
if (!still || still.pid !== pid) break;
|
|
216
|
+
await sleep(50);
|
|
218
217
|
}
|
|
219
|
-
}
|
|
220
218
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await sleep(50);
|
|
219
|
+
await guiRegistry.clearGuiPortFile({ force: true });
|
|
220
|
+
guiProcess = null;
|
|
221
|
+
guiPort = null;
|
|
222
|
+
return { status: 'stopping', owned: true, port, pid };
|
|
226
223
|
}
|
|
227
224
|
|
|
228
|
-
await kanban.clearGuiPortFile({ force: true });
|
|
229
225
|
guiProcess = null;
|
|
230
226
|
guiPort = null;
|
|
231
227
|
|
|
232
|
-
|
|
228
|
+
const discovered = await guiRegistry.discoverRunningGui();
|
|
229
|
+
if (!discovered) {
|
|
230
|
+
return { status: 'not_running' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
status: 'external_running',
|
|
235
|
+
owned: false,
|
|
236
|
+
port: discovered.port,
|
|
237
|
+
pid: discovered.pid,
|
|
238
|
+
url: discovered.url,
|
|
239
|
+
hint: 'GUI was not started by this MCP process; stop refused. Stop it from the owning terminal or kill that PID manually.'
|
|
240
|
+
};
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
async function guiStatus() {
|
|
236
|
-
if (
|
|
244
|
+
if (ownsGuiProcess() && guiPort) {
|
|
237
245
|
return {
|
|
238
246
|
status: 'running',
|
|
247
|
+
owned: true,
|
|
239
248
|
port: guiPort,
|
|
240
249
|
pid: guiProcess.pid,
|
|
241
250
|
url: `http://localhost:${guiPort}`
|
|
242
251
|
};
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
|
|
254
|
+
guiProcess = null;
|
|
255
|
+
guiPort = null;
|
|
256
|
+
|
|
257
|
+
const discovered = await guiRegistry.discoverRunningGui();
|
|
246
258
|
if (!discovered) {
|
|
247
|
-
guiProcess = null;
|
|
248
|
-
guiPort = null;
|
|
249
259
|
return { status: 'not_running' };
|
|
250
260
|
}
|
|
251
261
|
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
return {
|
|
263
|
+
status: 'external_running',
|
|
264
|
+
owned: false,
|
|
265
|
+
port: discovered.port,
|
|
266
|
+
pid: discovered.pid,
|
|
267
|
+
url: discovered.url,
|
|
268
|
+
cwd: discovered.cwd,
|
|
269
|
+
started_at: discovered.started_at
|
|
270
|
+
};
|
|
254
271
|
}
|
|
255
272
|
|
|
256
273
|
const server = new Server(
|
|
@@ -270,37 +287,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
270
287
|
tools: [
|
|
271
288
|
{
|
|
272
289
|
name: 'kanban_read',
|
|
273
|
-
description:
|
|
290
|
+
description: playbook.TOOL_DESCRIPTIONS.kanban_read,
|
|
274
291
|
inputSchema: {
|
|
275
292
|
type: 'object',
|
|
276
293
|
properties: {
|
|
277
294
|
operation: {
|
|
278
295
|
type: 'string',
|
|
279
|
-
enum: ['list', 'show'],
|
|
280
|
-
description:
|
|
296
|
+
enum: ['list', 'show', 'help'],
|
|
297
|
+
description: 'list=scan board; show=one task (needs task_id); help=token playbook (no board I/O)',
|
|
281
298
|
default: 'list'
|
|
282
299
|
},
|
|
283
300
|
task_id: {
|
|
284
301
|
type: 'string',
|
|
285
|
-
|
|
302
|
+
description: "Required for show. Numeric id: '014' or '14'."
|
|
286
303
|
},
|
|
287
304
|
col: {
|
|
288
305
|
type: 'string',
|
|
289
306
|
enum: COLS,
|
|
290
|
-
description: '
|
|
307
|
+
description: 'Filter list by column (saves tokens — prefer this)'
|
|
291
308
|
},
|
|
292
309
|
epic: {
|
|
293
310
|
type: 'string',
|
|
294
|
-
description: '
|
|
311
|
+
description: 'Filter list by epic group'
|
|
295
312
|
},
|
|
296
313
|
view: {
|
|
297
314
|
type: 'string',
|
|
298
315
|
enum: READ_VIEWS,
|
|
299
|
-
description: '
|
|
316
|
+
description: 'summary=board scan (default); planning=scope/AC; execution=+subtasks; full=everything. Prefer smallest that works.'
|
|
300
317
|
},
|
|
301
318
|
fields: {
|
|
302
319
|
type: 'array',
|
|
303
|
-
description: '
|
|
320
|
+
description: 'Exact fields only (overrides view). Use when you need 1–2 fields.',
|
|
304
321
|
items: { type: 'string' }
|
|
305
322
|
}
|
|
306
323
|
},
|
|
@@ -309,61 +326,61 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
309
326
|
},
|
|
310
327
|
{
|
|
311
328
|
name: 'kanban_manage',
|
|
312
|
-
description:
|
|
329
|
+
description: playbook.TOOL_DESCRIPTIONS.kanban_manage,
|
|
313
330
|
inputSchema: {
|
|
314
331
|
type: 'object',
|
|
315
332
|
properties: {
|
|
316
333
|
action: {
|
|
317
334
|
type: 'string',
|
|
318
|
-
|
|
319
|
-
|
|
335
|
+
enum: ['create', 'move', 'update', 'plan_create', 'plan_advance', 'plan_evidence', 'plan_done', 'plan_status'],
|
|
336
|
+
description: 'create|move|update daily; plan_* only for accepted multi-step work with tests'
|
|
320
337
|
},
|
|
321
338
|
title: {
|
|
322
339
|
type: 'string',
|
|
323
|
-
description:
|
|
340
|
+
description: 'Required for create and plan_create'
|
|
324
341
|
},
|
|
325
342
|
col: {
|
|
326
343
|
type: 'string',
|
|
327
344
|
enum: COLS,
|
|
328
345
|
default: 'planned',
|
|
329
|
-
description:
|
|
346
|
+
description: 'create column, or update shortcut for column'
|
|
330
347
|
},
|
|
331
348
|
epic: {
|
|
332
349
|
type: 'string',
|
|
333
350
|
default: '—',
|
|
334
|
-
description:
|
|
351
|
+
description: 'Epic group (create/update/plan_create)'
|
|
335
352
|
},
|
|
336
353
|
description: {
|
|
337
354
|
type: 'string',
|
|
338
|
-
description:
|
|
355
|
+
description: 'Why/context (recommended on create)'
|
|
339
356
|
},
|
|
340
357
|
specs: {
|
|
341
358
|
type: 'string',
|
|
342
|
-
description:
|
|
359
|
+
description: 'Technical constraints (recommended on create)'
|
|
343
360
|
},
|
|
344
361
|
in_scope: {
|
|
345
362
|
type: 'array',
|
|
346
|
-
description:
|
|
363
|
+
description: 'In-scope bullets (recommended on create)',
|
|
347
364
|
items: { type: 'string' }
|
|
348
365
|
},
|
|
349
366
|
out_of_scope: {
|
|
350
367
|
type: 'array',
|
|
351
|
-
description:
|
|
368
|
+
description: 'Out-of-scope bullets (recommended on create)',
|
|
352
369
|
items: { type: 'string' }
|
|
353
370
|
},
|
|
354
371
|
acceptance_criteria: {
|
|
355
372
|
type: 'array',
|
|
356
|
-
description:
|
|
373
|
+
description: 'Done criteria (recommended on create)',
|
|
357
374
|
items: { type: 'string' }
|
|
358
375
|
},
|
|
359
376
|
test_cases: {
|
|
360
377
|
type: 'array',
|
|
361
|
-
description:
|
|
378
|
+
description: 'Verification scenarios',
|
|
362
379
|
items: { type: 'string' }
|
|
363
380
|
},
|
|
364
381
|
subtasks: {
|
|
365
382
|
type: 'array',
|
|
366
|
-
description:
|
|
383
|
+
description: 'Full subtask list replace on update (send complete array, not a single toggle)',
|
|
367
384
|
items: {
|
|
368
385
|
type: 'object',
|
|
369
386
|
properties: {
|
|
@@ -376,58 +393,58 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
376
393
|
},
|
|
377
394
|
notes: {
|
|
378
395
|
type: 'string',
|
|
379
|
-
description:
|
|
396
|
+
description: 'Freeform notes'
|
|
380
397
|
},
|
|
381
398
|
task_id: {
|
|
382
399
|
type: 'string',
|
|
383
|
-
|
|
400
|
+
description: "Required for move/update/plan_* except plan_create. '014' or '14'."
|
|
384
401
|
},
|
|
385
402
|
column: {
|
|
386
403
|
type: 'string',
|
|
387
404
|
enum: COLS,
|
|
388
|
-
description:
|
|
405
|
+
description: 'Target column for move (not col)'
|
|
389
406
|
},
|
|
390
407
|
patch: {
|
|
391
408
|
type: 'object',
|
|
392
|
-
description:
|
|
409
|
+
description: 'Bulk update object; merged with top-level field shortcuts'
|
|
393
410
|
},
|
|
394
411
|
return: {
|
|
395
412
|
type: 'string',
|
|
396
413
|
enum: ['none', 'summary', 'full'],
|
|
397
|
-
description:
|
|
414
|
+
description: 'move/update response size. Prefer none. Default summary. create always returns full task once.'
|
|
398
415
|
},
|
|
399
416
|
index: {
|
|
400
417
|
type: 'integer',
|
|
401
|
-
description:
|
|
418
|
+
description: 'plan_advance: subtask index; omit = first incomplete'
|
|
402
419
|
},
|
|
403
420
|
steps: {
|
|
404
421
|
type: 'array',
|
|
405
422
|
items: { type: 'string' },
|
|
406
|
-
description:
|
|
423
|
+
description: 'plan_create: implementation steps between red/green test steps'
|
|
407
424
|
},
|
|
408
425
|
project_root: {
|
|
409
426
|
type: 'string',
|
|
410
|
-
description:
|
|
427
|
+
description: 'plan_create: root for test-runner detect (default cwd)'
|
|
411
428
|
},
|
|
412
429
|
diff: {
|
|
413
430
|
type: 'string',
|
|
414
|
-
description:
|
|
431
|
+
description: 'plan_evidence: short diff or summary (not whole repo)'
|
|
415
432
|
},
|
|
416
433
|
test_command: {
|
|
417
434
|
type: 'string',
|
|
418
|
-
description:
|
|
435
|
+
description: 'plan_evidence: exact command run'
|
|
419
436
|
},
|
|
420
437
|
stdout: {
|
|
421
438
|
type: 'string',
|
|
422
|
-
description:
|
|
439
|
+
description: 'plan_evidence: test stdout (truncate to last ~2KB if huge)'
|
|
423
440
|
},
|
|
424
441
|
stderr: {
|
|
425
442
|
type: 'string',
|
|
426
|
-
description:
|
|
443
|
+
description: 'plan_evidence: test stderr (truncate if huge)'
|
|
427
444
|
},
|
|
428
445
|
exit_code: {
|
|
429
446
|
type: 'integer',
|
|
430
|
-
description:
|
|
447
|
+
description: 'plan_evidence: process exit code'
|
|
431
448
|
}
|
|
432
449
|
},
|
|
433
450
|
required: ['action'],
|
|
@@ -436,18 +453,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
436
453
|
},
|
|
437
454
|
{
|
|
438
455
|
name: 'kanban_gui',
|
|
439
|
-
description:
|
|
456
|
+
description: playbook.TOOL_DESCRIPTIONS.kanban_gui,
|
|
440
457
|
inputSchema: {
|
|
441
458
|
type: 'object',
|
|
442
459
|
properties: {
|
|
443
460
|
action: {
|
|
444
461
|
type: 'string',
|
|
445
462
|
enum: ['start', 'stop', 'status'],
|
|
446
|
-
description:
|
|
463
|
+
description: 'start | stop (owned only) | status'
|
|
447
464
|
},
|
|
448
465
|
port: {
|
|
449
466
|
type: 'integer',
|
|
450
|
-
description:
|
|
467
|
+
description: 'Optional start port; else KANBANGO_GUI_PORT or stable 5510-5999'
|
|
451
468
|
}
|
|
452
469
|
},
|
|
453
470
|
required: ['action'],
|
|
@@ -467,6 +484,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
467
484
|
switch (name) {
|
|
468
485
|
case 'kanban_read': {
|
|
469
486
|
const operation = args.operation || 'list';
|
|
487
|
+
|
|
488
|
+
if (operation === 'help') {
|
|
489
|
+
result = playbook.playbookHelpPayload();
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
|
|
470
493
|
const readOptions = normalizeReadOptions(args, 'summary');
|
|
471
494
|
|
|
472
495
|
if (operation === 'list') {
|
|
@@ -492,7 +515,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
492
515
|
} else {
|
|
493
516
|
throw invalidRequest(
|
|
494
517
|
`Unknown operation: ${operation}`,
|
|
495
|
-
'Use one of: list, show',
|
|
518
|
+
'Use one of: list, show, help',
|
|
496
519
|
{ operation }
|
|
497
520
|
);
|
|
498
521
|
}
|
|
@@ -505,7 +528,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
505
528
|
|
|
506
529
|
switch (action) {
|
|
507
530
|
case 'create': {
|
|
508
|
-
const
|
|
531
|
+
const createPayload = {
|
|
509
532
|
description: args.description,
|
|
510
533
|
specs: args.specs,
|
|
511
534
|
in_scope: args.in_scope,
|
|
@@ -514,8 +537,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
514
537
|
test_cases: args.test_cases,
|
|
515
538
|
subtasks: args.subtasks,
|
|
516
539
|
notes: args.notes
|
|
517
|
-
}
|
|
518
|
-
|
|
540
|
+
};
|
|
541
|
+
const created = await kanban.doCreate(args.title, args.col || 'planned', args.epic || '—', createPayload);
|
|
542
|
+
const shaped = kanban.shapeTask(created, { view: 'full' });
|
|
543
|
+
const warnings = kanban.createFieldWarnings(createPayload);
|
|
544
|
+
result = warnings.length > 0
|
|
545
|
+
? { ...shaped, warnings, missing_recommended: kanban.missingRecommendedCreateFields(createPayload) }
|
|
546
|
+
: shaped;
|
|
519
547
|
break;
|
|
520
548
|
}
|
|
521
549
|
|
|
@@ -564,9 +592,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
564
592
|
break;
|
|
565
593
|
}
|
|
566
594
|
|
|
567
|
-
case 'plan_create':
|
|
595
|
+
case 'plan_create': {
|
|
568
596
|
result = await plan.create(args);
|
|
597
|
+
const planWarnings = kanban.createFieldWarnings(args);
|
|
598
|
+
if (planWarnings.length > 0 && result && typeof result === 'object') {
|
|
599
|
+
result = {
|
|
600
|
+
...result,
|
|
601
|
+
warnings: planWarnings,
|
|
602
|
+
missing_recommended: kanban.missingRecommendedCreateFields(args)
|
|
603
|
+
};
|
|
604
|
+
}
|
|
569
605
|
break;
|
|
606
|
+
}
|
|
570
607
|
case 'plan_advance':
|
|
571
608
|
result = await plan.advance({ task_id: args.task_id, index: args.index });
|
|
572
609
|
break;
|
|
@@ -642,9 +679,10 @@ async function maybeAutoStartGui() {
|
|
|
642
679
|
function installGuiShutdownHooks() {
|
|
643
680
|
let shuttingDown = false;
|
|
644
681
|
|
|
645
|
-
async function
|
|
682
|
+
async function shutdownOwnedGui() {
|
|
646
683
|
if (shuttingDown) return;
|
|
647
684
|
shuttingDown = true;
|
|
685
|
+
if (!ownsGuiProcess()) return;
|
|
648
686
|
try {
|
|
649
687
|
await stopGuiServer();
|
|
650
688
|
} catch {
|
|
@@ -653,7 +691,7 @@ function installGuiShutdownHooks() {
|
|
|
653
691
|
}
|
|
654
692
|
|
|
655
693
|
process.once('exit', () => {
|
|
656
|
-
if (
|
|
694
|
+
if (ownsGuiProcess()) {
|
|
657
695
|
try {
|
|
658
696
|
guiProcess.kill();
|
|
659
697
|
} catch {
|
|
@@ -662,10 +700,10 @@ function installGuiShutdownHooks() {
|
|
|
662
700
|
}
|
|
663
701
|
});
|
|
664
702
|
process.once('SIGINT', () => {
|
|
665
|
-
|
|
703
|
+
shutdownOwnedGui().finally(() => process.exit(0));
|
|
666
704
|
});
|
|
667
705
|
process.once('SIGTERM', () => {
|
|
668
|
-
|
|
706
|
+
shutdownOwnedGui().finally(() => process.exit(0));
|
|
669
707
|
});
|
|
670
708
|
}
|
|
671
709
|
|
|
@@ -685,7 +723,8 @@ module.exports = {
|
|
|
685
723
|
startGuiServer,
|
|
686
724
|
stopGuiServer,
|
|
687
725
|
guiStatus,
|
|
688
|
-
resolvePreferredGuiPort:
|
|
726
|
+
resolvePreferredGuiPort: guiRegistry.resolvePreferredGuiPort,
|
|
727
|
+
playbook,
|
|
689
728
|
server,
|
|
690
729
|
main
|
|
691
730
|
};
|
package/package.json
CHANGED
package/plan.js
CHANGED
|
@@ -147,7 +147,7 @@ async function done(payload = {}) {
|
|
|
147
147
|
throw planError('PLAN_INCOMPLETE', 'Plan has incomplete subtasks',
|
|
148
148
|
'Advance every plan step before marking the workflow done', { incomplete });
|
|
149
149
|
}
|
|
150
|
-
const updated = await kanban.updateTask(task.id, { column: 'done', plan: { ...
|
|
150
|
+
const updated = await kanban.updateTask(task.id, { column: 'done', plan: { ...task.plan, status: 'done' } });
|
|
151
151
|
return result(updated, { status: 'done' });
|
|
152
152
|
}
|
|
153
153
|
|
package/tests/run.js
CHANGED
|
@@ -18,3 +18,5 @@ runNode(path.join('tests', 'update-tasks.test.js'), [], 'Update tasks test');
|
|
|
18
18
|
runNode(path.join('tests', 'read-views.test.js'), [], 'Read views test');
|
|
19
19
|
runNode(path.join('tests', 'mcp-server.test.js'), [], 'MCP server test');
|
|
20
20
|
runNode(path.join('tests', 'gui-port.test.js'), [], 'GUI port test');
|
|
21
|
+
runNode(path.join('tests', 'plan-workflow.test.js'), [], 'Plan workflow test');
|
|
22
|
+
runNode(path.join('tests', 'agent-playbook.test.js'), [], 'Agent playbook test');
|