kanbango 3.6.2 → 3.8.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 +5 -0
- package/.ai/retro/last-run.json +1 -1
- package/.ait-quality.yml +1 -0
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +38 -0
- package/LLM_AGENTS.md +1 -1
- package/README.md +34 -3
- package/agent-playbook.js +30 -13
- package/bin/kanban.js +62 -7
- package/config-wizard.js +441 -0
- package/index.html +67 -36
- package/kanban.js +429 -20
- package/mcp-server.js +87 -22
- package/package.json +1 -1
- package/plan.js +31 -27
- package/tests/run.js +5 -0
- package/workflow.js +364 -18
package/mcp-server.js
CHANGED
|
@@ -11,13 +11,25 @@ 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
|
+
|
|
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
|
+
|
|
21
33
|
function normalizePort(value) {
|
|
22
34
|
return guiRegistry.normalizeGuiPort(value);
|
|
23
35
|
}
|
|
@@ -151,12 +163,23 @@ async function buildEpicLookup() {
|
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
async function formatTaskResult(task, returnShape) {
|
|
154
|
-
if (returnShape === 'none')
|
|
166
|
+
if (returnShape === 'none') {
|
|
167
|
+
if (Array.isArray(task.unblocked_tasks) && task.unblocked_tasks.length > 0) {
|
|
168
|
+
return { ok: true, unblocked_tasks: task.unblocked_tasks };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
155
172
|
const epicLookup = await buildEpicLookup();
|
|
156
|
-
|
|
173
|
+
const allTasks = await kanban.allTasks();
|
|
174
|
+
const shaped = kanban.shapeTask(task, {
|
|
157
175
|
view: returnShape === 'full' ? 'full' : 'summary',
|
|
158
|
-
epicLookup
|
|
176
|
+
epicLookup,
|
|
177
|
+
allTasks
|
|
159
178
|
});
|
|
179
|
+
if (Array.isArray(task.unblocked_tasks)) {
|
|
180
|
+
shaped.unblocked_tasks = task.unblocked_tasks;
|
|
181
|
+
}
|
|
182
|
+
return shaped;
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
function guiIdentity(extra = {}) {
|
|
@@ -317,6 +340,8 @@ const server = new Server(
|
|
|
317
340
|
);
|
|
318
341
|
|
|
319
342
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
343
|
+
await require('./workflow.js').ensureBoardConfig();
|
|
344
|
+
const COLS = activeCols();
|
|
320
345
|
return {
|
|
321
346
|
tools: [
|
|
322
347
|
{
|
|
@@ -327,8 +352,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
327
352
|
properties: {
|
|
328
353
|
operation: {
|
|
329
354
|
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',
|
|
355
|
+
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',
|
|
332
357
|
default: 'list'
|
|
333
358
|
},
|
|
334
359
|
task_id: {
|
|
@@ -452,6 +477,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
452
477
|
description: 'Verification scenarios',
|
|
453
478
|
items: { type: 'string' }
|
|
454
479
|
},
|
|
480
|
+
depends_on: {
|
|
481
|
+
type: 'array',
|
|
482
|
+
description: 'Task ids that must be done before this task can enter active/gates',
|
|
483
|
+
items: { type: 'string' }
|
|
484
|
+
},
|
|
485
|
+
files: {
|
|
486
|
+
type: 'array',
|
|
487
|
+
description: 'Touched file paths for this task (create/update/plan_evidence)',
|
|
488
|
+
items: { type: 'string' }
|
|
489
|
+
},
|
|
455
490
|
subtasks: {
|
|
456
491
|
type: 'array',
|
|
457
492
|
description: 'Full subtask list replace on update (send complete array, not a single toggle)',
|
|
@@ -504,8 +539,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
504
539
|
enum: COLS,
|
|
505
540
|
description:
|
|
506
541
|
'Target column for move (not col). Must be a legal transition from the current column. '
|
|
507
|
-
+
|
|
508
|
-
+ '
|
|
542
|
+
+ `Allowed (from project config): ${transitionHint()}. `
|
|
543
|
+
+ 'Illegal → INVALID_TRANSITION + allowed_columns.'
|
|
509
544
|
},
|
|
510
545
|
patch: {
|
|
511
546
|
type: 'object',
|
|
@@ -523,31 +558,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
523
558
|
steps: {
|
|
524
559
|
type: 'array',
|
|
525
560
|
items: { type: 'string' },
|
|
526
|
-
description: 'plan_create:
|
|
561
|
+
description: 'create/plan_create: checklist texts → subtasks (create uses when subtasks omitted; plan_create uses as-is, no forced TDD steps)'
|
|
527
562
|
},
|
|
528
563
|
project_root: {
|
|
529
564
|
type: 'string',
|
|
530
|
-
description: 'plan_create: root for test-runner detect (default cwd)'
|
|
565
|
+
description: 'plan_create: root for optional test-runner detect (default cwd); missing runner → null, not error'
|
|
531
566
|
},
|
|
532
567
|
diff: {
|
|
533
568
|
type: 'string',
|
|
534
|
-
description: 'plan_evidence: short diff or
|
|
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)'
|
|
535
574
|
},
|
|
536
575
|
test_command: {
|
|
537
576
|
type: 'string',
|
|
538
|
-
description: 'plan_evidence: exact command run'
|
|
577
|
+
description: 'plan_evidence: optional exact command run'
|
|
539
578
|
},
|
|
540
579
|
stdout: {
|
|
541
580
|
type: 'string',
|
|
542
|
-
description: 'plan_evidence: test stdout (truncate to last ~2KB if huge)'
|
|
581
|
+
description: 'plan_evidence: optional test stdout (truncate to last ~2KB if huge)'
|
|
543
582
|
},
|
|
544
583
|
stderr: {
|
|
545
584
|
type: 'string',
|
|
546
|
-
description: 'plan_evidence: test stderr (truncate if huge)'
|
|
585
|
+
description: 'plan_evidence: optional test stderr (truncate if huge)'
|
|
547
586
|
},
|
|
548
587
|
exit_code: {
|
|
549
588
|
type: 'integer',
|
|
550
|
-
description: 'plan_evidence: process exit code'
|
|
589
|
+
description: 'plan_evidence: optional process exit code'
|
|
551
590
|
}
|
|
552
591
|
},
|
|
553
592
|
required: ['action'],
|
|
@@ -593,12 +632,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
593
632
|
break;
|
|
594
633
|
}
|
|
595
634
|
|
|
635
|
+
await require('./workflow.js').ensureBoardConfig();
|
|
636
|
+
|
|
637
|
+
if (operation === 'context') {
|
|
638
|
+
result = await kanban.getContextPayload({
|
|
639
|
+
epic: args.epic,
|
|
640
|
+
epic_id: args.epic_id
|
|
641
|
+
});
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
596
645
|
const readOptions = normalizeReadOptions(args, 'summary');
|
|
597
646
|
|
|
598
647
|
if (operation === 'list') {
|
|
599
648
|
await kanban.migrateEpicGroups();
|
|
600
649
|
const epics = await kanban.listEpicEntities();
|
|
601
|
-
|
|
650
|
+
const allBoardTasks = await kanban.allTasks();
|
|
651
|
+
let tasks = allBoardTasks;
|
|
602
652
|
if (args.col) {
|
|
603
653
|
tasks = tasks.filter((task) => task.column === args.col);
|
|
604
654
|
}
|
|
@@ -613,8 +663,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
613
663
|
});
|
|
614
664
|
}
|
|
615
665
|
|
|
616
|
-
|
|
617
|
-
result = tasks.map((task) => kanban.shapeTask(task, readOptions));
|
|
666
|
+
result = tasks.map((task) => kanban.shapeTask(task, { ...readOptions, allTasks: allBoardTasks }));
|
|
618
667
|
} else if (operation === 'show') {
|
|
619
668
|
if (!args.task_id) {
|
|
620
669
|
throw invalidRequest(
|
|
@@ -626,9 +675,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
626
675
|
|
|
627
676
|
await kanban.migrateEpicGroups();
|
|
628
677
|
const epicLookup = await buildEpicLookup();
|
|
678
|
+
const allTasks = await kanban.allTasks();
|
|
629
679
|
result = kanban.shapeTask(await kanban.getTask(args.task_id), {
|
|
630
680
|
...readOptions,
|
|
631
|
-
epicLookup
|
|
681
|
+
epicLookup,
|
|
682
|
+
allTasks
|
|
632
683
|
});
|
|
633
684
|
} else if (operation === 'list_epics') {
|
|
634
685
|
await kanban.migrateEpicGroups();
|
|
@@ -666,7 +717,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
666
717
|
} else {
|
|
667
718
|
throw invalidRequest(
|
|
668
719
|
`Unknown operation: ${operation}`,
|
|
669
|
-
'Use one of: list, show, list_epics, show_epic, help',
|
|
720
|
+
'Use one of: list, show, list_epics, show_epic, context, help',
|
|
670
721
|
{ operation }
|
|
671
722
|
);
|
|
672
723
|
}
|
|
@@ -676,9 +727,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
676
727
|
case 'kanban_manage': {
|
|
677
728
|
const action = args.action;
|
|
678
729
|
const returnShape = normalizeReturnShape(args.return);
|
|
730
|
+
await require('./workflow.js').ensureBoardConfig();
|
|
679
731
|
|
|
680
732
|
switch (action) {
|
|
681
733
|
case 'create': {
|
|
734
|
+
let subtasks = args.subtasks;
|
|
735
|
+
if (subtasks === undefined && Array.isArray(args.steps)) {
|
|
736
|
+
subtasks = args.steps.filter(Boolean).map(String).map((text, index) => ({
|
|
737
|
+
id: `st-${index + 1}`,
|
|
738
|
+
text,
|
|
739
|
+
done: false,
|
|
740
|
+
description: ''
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
682
743
|
const createPayload = {
|
|
683
744
|
description: args.description,
|
|
684
745
|
specs: args.specs,
|
|
@@ -686,8 +747,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
686
747
|
out_of_scope: args.out_of_scope,
|
|
687
748
|
acceptance_criteria: args.acceptance_criteria,
|
|
688
749
|
test_cases: args.test_cases,
|
|
689
|
-
subtasks
|
|
690
|
-
notes: args.notes
|
|
750
|
+
subtasks,
|
|
751
|
+
notes: args.notes,
|
|
752
|
+
depends_on: args.depends_on,
|
|
753
|
+
files: args.files
|
|
691
754
|
};
|
|
692
755
|
const epicRef = args.epic_id || args.epic || '—';
|
|
693
756
|
const created = await kanban.doCreate(args.title, args.col || 'planned', epicRef, createPayload);
|
|
@@ -842,6 +905,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
842
905
|
if (args.test_cases !== undefined) patch.test_cases = args.test_cases;
|
|
843
906
|
if (args.subtasks !== undefined) patch.subtasks = args.subtasks;
|
|
844
907
|
if (args.notes !== undefined) patch.notes = args.notes;
|
|
908
|
+
if (args.depends_on !== undefined) patch.depends_on = args.depends_on;
|
|
909
|
+
if (args.files !== undefined) patch.files = args.files;
|
|
845
910
|
if (args.adr !== undefined) patch.adr = args.adr;
|
|
846
911
|
if (args.epic_id !== undefined) patch.epic_id = args.epic_id;
|
|
847
912
|
else if (args.epic !== undefined) patch.epic = args.epic;
|
package/package.json
CHANGED
package/plan.js
CHANGED
|
@@ -55,17 +55,13 @@ async function detectTestRunner(projectRoot = process.cwd()) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
'Add a supported project manifest or set OPENCODE_TEST_COMMAND', { project_root: projectRoot });
|
|
58
|
+
return null;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
function planSubtasks(implementationSteps) {
|
|
63
|
-
const steps =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
...implementationSteps,
|
|
67
|
-
'Run tests and confirm green'
|
|
68
|
-
];
|
|
62
|
+
const steps = Array.isArray(implementationSteps)
|
|
63
|
+
? implementationSteps.filter(Boolean).map(String)
|
|
64
|
+
: [];
|
|
69
65
|
return steps.map((text, index) => ({ id: `st-${index + 1}`, text, done: false, description: '' }));
|
|
70
66
|
}
|
|
71
67
|
|
|
@@ -93,6 +89,8 @@ async function create(payload = {}) {
|
|
|
93
89
|
test_cases: payload.test_cases,
|
|
94
90
|
subtasks: planSubtasks(implementationSteps),
|
|
95
91
|
notes: payload.notes,
|
|
92
|
+
depends_on: payload.depends_on,
|
|
93
|
+
files: payload.files,
|
|
96
94
|
plan: { runner, status: 'active' },
|
|
97
95
|
evidence: []
|
|
98
96
|
});
|
|
@@ -116,28 +114,30 @@ async function advance(payload = {}) {
|
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
async function evidence(payload = {}) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
const diff = payload.diff !== undefined ? String(payload.diff) : '';
|
|
118
|
+
const summary = payload.summary !== undefined ? String(payload.summary) : '';
|
|
119
|
+
const testCommand = payload.test_command !== undefined ? String(payload.test_command) : '';
|
|
120
|
+
if (!diff && !summary && !testCommand) {
|
|
121
|
+
throw planError('MISSING_REQUIRED_FIELD', 'diff, summary, or test_command is required',
|
|
122
|
+
'Provide at least one of diff, summary, or test_command', { field: 'diff' });
|
|
125
123
|
}
|
|
126
|
-
if (!Number.isInteger(payload.exit_code)) {
|
|
124
|
+
if (payload.exit_code !== undefined && !Number.isInteger(payload.exit_code)) {
|
|
127
125
|
throw planError('VALIDATION_ERROR', 'exit_code must be an integer',
|
|
128
|
-
'Use the process exit code from the test command', { field: 'exit_code' });
|
|
126
|
+
'Use the process exit code from the test command, or omit exit_code', { field: 'exit_code' });
|
|
129
127
|
}
|
|
130
128
|
const task = await kanban.getTask(payload.task_id);
|
|
131
|
-
const
|
|
132
|
-
diff
|
|
133
|
-
test_command:
|
|
134
|
-
stdout: String(payload.stdout),
|
|
135
|
-
stderr: String(payload.stderr),
|
|
136
|
-
exit_code: payload.exit_code,
|
|
129
|
+
const patch = { evidence: [...task.evidence, {
|
|
130
|
+
diff,
|
|
131
|
+
test_command: testCommand,
|
|
132
|
+
stdout: payload.stdout !== undefined ? String(payload.stdout) : '',
|
|
133
|
+
stderr: payload.stderr !== undefined ? String(payload.stderr) : '',
|
|
134
|
+
exit_code: Number.isInteger(payload.exit_code) ? payload.exit_code : null,
|
|
135
|
+
summary,
|
|
137
136
|
created: new Date().toISOString()
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
}] };
|
|
138
|
+
if (payload.files !== undefined) patch.files = payload.files;
|
|
139
|
+
const updated = await kanban.updateTask(task.id, patch);
|
|
140
|
+
return result(updated, { evidence: patch.evidence[patch.evidence.length - 1] });
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
async function done(payload = {}) {
|
|
@@ -147,11 +147,15 @@ 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 workflow = require('./workflow.js');
|
|
151
|
+
const targetColumn = await workflow.planDoneColumn();
|
|
150
152
|
const updated = await kanban.updateTask(task.id, {
|
|
151
|
-
column:
|
|
153
|
+
column: targetColumn,
|
|
152
154
|
plan: { ...(task.plan || {}), status: 'done' }
|
|
153
155
|
});
|
|
154
|
-
|
|
156
|
+
const extra = { status: 'done', column: updated.column };
|
|
157
|
+
if (Array.isArray(updated.unblocked_tasks)) extra.unblocked_tasks = updated.unblocked_tasks;
|
|
158
|
+
return result(updated, extra);
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
async function status(taskId) {
|
package/tests/run.js
CHANGED
|
@@ -5,6 +5,8 @@ function runNode(scriptPath, args, label) {
|
|
|
5
5
|
const fullPath = path.join(process.cwd(), scriptPath);
|
|
6
6
|
const result = spawnSync(process.execPath, [fullPath, ...(args || [])], {
|
|
7
7
|
stdio: 'inherit',
|
|
8
|
+
// Agent shells often export KANBANGO_AUTO_GUI=1; MCP child tests would hang.
|
|
9
|
+
env: { ...process.env, KANBANGO_AUTO_GUI: '0' }
|
|
8
10
|
});
|
|
9
11
|
if (result.status !== 0) {
|
|
10
12
|
console.error(`✗ ${label} failed`);
|
|
@@ -23,10 +25,13 @@ runNode(path.join('tests', 'fenced-text.test.js'), [], 'Fenced text test');
|
|
|
23
25
|
runNode(path.join('tests', 'kanban-mermaid-vendor.test.js'), [], 'Kanban mermaid vendor route test');
|
|
24
26
|
runNode(path.join('tests', 'plan-workflow.test.js'), [], 'Plan workflow test');
|
|
25
27
|
runNode(path.join('tests', 'workflow-gates.test.js'), [], 'Workflow gates test');
|
|
28
|
+
runNode(path.join('tests', 'board-config.test.js'), [], 'Board config columns/agents test');
|
|
29
|
+
runNode(path.join('tests', 'config-wizard.test.js'), [], 'Config wizard TUI test');
|
|
26
30
|
runNode(path.join('tests', 'kanban.js'), [], 'Kanban core test');
|
|
27
31
|
runNode(path.join('tests', 'index.js'), [], 'Package index export test');
|
|
28
32
|
runNode(path.join('tests', 'bin-kanban.test.js'), [], 'CLI bin/kanban test');
|
|
29
33
|
runNode(path.join('tests', 'agent-playbook.test.js'), [], 'Agent playbook test');
|
|
34
|
+
runNode(path.join('tests', 'agent-context.test.js'), [], 'Agent context / depends_on / files test');
|
|
30
35
|
runNode(path.join('tests', 'epics.test.js'), [], 'Epics test');
|
|
31
36
|
runNode(path.join('tests', 'kanban-epic-goals-adr.test.js'), [], 'Epic goals + ADR test');
|
|
32
37
|
runNode(path.join('tests', 'delete-archive.test.js'), [], 'Delete/archive test');
|