kanbango 2.5.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/last-run.json +1 -1
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +36 -0
- package/LLM_AGENTS.md +73 -18
- 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 +2 -134
- package/mcp-server.js +106 -81
- package/package.json +1 -1
- package/plan.js +1 -1
- package/tests/run.js +2 -0
package/.ai/lessons.jsonl
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"id":"260729-3h4","ts":"2026-07-29","scope":"proj","tags":["tests","mcp","cli","review"],"rule":"DO Add automated tests for every new public CLI/MCP workflow before shipping","when":"api-review","sev":2,"hits":1}
|
|
2
|
+
{"id":"260729-1ur","ts":"2026-07-29","scope":"proj","tags":["javascript","objects"],"rule":"DON'T Use empty object fallbacks in spreads like `...(obj || {})` since spreading falsy is safe","when":"object spread","sev":2,"hits":1}
|
|
3
|
+
{"id":"260729-5eo","ts":"2026-07-29","scope":"proj","tags":["processes","mcp","ownership"],"rule":"DO Track process ownership before stopping or cleaning up discovered services","when":"gui-process management","sev":2,"hits":1}
|
|
4
|
+
{"id":"260729-1ch","ts":"2026-07-29","scope":"proj","tags":["gui","process","mcp"],"rule":"DO GUI/MCP stop must only SIGTERM processes spawned by the current process; treat port-file PIDs as external_running","when":"gui-process management","sev":2,"hits":1}
|
package/.ai/retro/last-run.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"ts":"2026-07-
|
|
1
|
+
{"ts":"2026-07-29T10:38:42.184Z","sessionID":"ses_052c1be3fffeNPoV7VO6z40X2I","added":["260729-1ch"],"bumped":[]}
|
package/AGENTS.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
This document provides build commands, testing procedures, and code style guidelines for working with the kanbango codebase.
|
|
4
4
|
|
|
5
|
+
## Kanbango MCP — token rules (when using this board)
|
|
6
|
+
|
|
7
|
+
Source of truth: `agent-playbook.js` (also MCP tool descriptions + `kanban_read` `operation=help`).
|
|
8
|
+
|
|
9
|
+
- list: `col` filter, `view=summary`; keep `task_id`s; no full-board re-list after every write
|
|
10
|
+
- show: `view=execution` while coding; `full` only if needed
|
|
11
|
+
- create once with `description`, `specs`, `in_scope`, `out_of_scope`, `acceptance_criteria`
|
|
12
|
+
- move/update: `return=none`; subtasks = full array replace (no toggle)
|
|
13
|
+
- non-trivial work: `plan_create` → `plan_advance` → `plan_evidence` (real tests, truncated logs) → `plan_done`
|
|
14
|
+
- gui: `status` before `start`; `stop` only owned; `external_running` = do not kill
|
|
15
|
+
|
|
5
16
|
## Build & Development Commands
|
|
6
17
|
|
|
7
18
|
### Available Scripts
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to kanbango will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.0.2] - 2026-07-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `agent-playbook.js` — single source of truth for agent token rules
|
|
12
|
+
- `kanban_read` `operation=help` returns the playbook (no board I/O)
|
|
13
|
+
- Test that `tools/list` descriptions match the playbook module
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- MCP tool descriptions are loaded from `agent-playbook.js` (not duplicated prose)
|
|
17
|
+
- `LLM_AGENTS.md` clarified as human setup docs; agents rely on tool descriptions
|
|
18
|
+
|
|
19
|
+
## [3.0.1] - 2026-07-29
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- MCP tool descriptions encode a token-efficient agent playbook (cheap list/show, fat create once, `return=none`, plan_* only when needed, GUI ownership)
|
|
23
|
+
- `LLM_AGENTS.md`, `AGENTS.md`, and `README.md` document the same rules for project-scoped agents
|
|
24
|
+
|
|
25
|
+
## [3.0.0] - 2026-07-29
|
|
26
|
+
|
|
27
|
+
### Breaking
|
|
28
|
+
- New task IDs are zero-padded numeric strings (`001`), not `PI-001-slug`
|
|
29
|
+
- MCP/CLI `toggle` removed; update the full `subtasks` list via `update`
|
|
30
|
+
- Legacy task field alias `tasks` removed; use `subtasks` only
|
|
31
|
+
- `kanban_gui` `stop` only stops a GUI **spawned by the current MCP process**. An external GUI returns `status: "external_running"` and is **not** sent SIGTERM (PID-reuse safety)
|
|
32
|
+
- `kanban_gui` `status` may return `running` (owned), `external_running` (discovered), or `not_running`
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Accepted-plan workflow: `plan.js` + CLI `kanban plan <action> --json` + MCP `plan_*` actions on `kanban_manage`
|
|
36
|
+
- `gui-registry.js` — GUI port file, preferred port, discover helpers (extracted from `kanban.js`)
|
|
37
|
+
- Regression tests for plan workflow and GUI process ownership
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Create field policy: only `title` is hard-required; `description`, `specs`, `in_scope`, `out_of_scope`, and `acceptance_criteria` are strongly recommended
|
|
41
|
+
- MCP `kanban_manage` create/plan_create returns `warnings` + `missing_recommended` when recommended fields are omitted (still succeeds)
|
|
42
|
+
- MCP tool descriptions and LLM agent docs push agents to always fill planning boundaries
|
|
43
|
+
|
|
8
44
|
## [2.5.0] - 2026-07-27
|
|
9
45
|
|
|
10
46
|
### Changed
|
package/LLM_AGENTS.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# MCP Server Guide for LLM Agents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Human-oriented setup guide. **Agents do not load this file automatically.**
|
|
4
|
+
|
|
5
|
+
Canonical usage rules live in code (`agent-playbook.js`) and are injected into MCP
|
|
6
|
+
`tools/list` descriptions. After install, the agent only needs the MCP server —
|
|
7
|
+
not this markdown.
|
|
8
|
+
|
|
9
|
+
Optional refresh in-session: `kanban_read` with `operation: "help"`.
|
|
4
10
|
|
|
5
11
|
## Quick Integration
|
|
6
12
|
|
|
@@ -63,11 +69,12 @@ Read tasks from kanban board. Can list all tasks, filter by column/epic, or get
|
|
|
63
69
|
**Operations:**
|
|
64
70
|
- `list` - Get all tasks (with optional filters)
|
|
65
71
|
- `show` - Get specific task details
|
|
72
|
+
- `help` - Return token playbook from `agent-playbook.js` (no board I/O)
|
|
66
73
|
|
|
67
74
|
**Parameters:**
|
|
68
75
|
```json
|
|
69
76
|
{
|
|
70
|
-
"operation": "list", // "list"
|
|
77
|
+
"operation": "list", // "list" | "show" | "help"
|
|
71
78
|
"task_id": "014", // Required for "show"
|
|
72
79
|
"col": "planned", // Optional: "active" | "planned" | "icebox" | "done"
|
|
73
80
|
"epic": "Phase 1" // Optional: filter by epic group
|
|
@@ -232,6 +239,11 @@ Patch-update task fields:
|
|
|
232
239
|
|
|
233
240
|
Control the web GUI server: start, stop, or check status.
|
|
234
241
|
|
|
242
|
+
**Ownership (important):**
|
|
243
|
+
- `stop` only sends SIGTERM to a GUI **spawned by this MCP process** (`owned: true`).
|
|
244
|
+
- If the GUI was started elsewhere (CLI `kanban serve`, another MCP), `stop` returns `external_running` and does **not** kill that PID.
|
|
245
|
+
- `status` distinguishes `running` (owned), `external_running` (discovered via port file), and `not_running`.
|
|
246
|
+
|
|
235
247
|
**Port resolution (start):**
|
|
236
248
|
1. Explicit `port` argument, if provided
|
|
237
249
|
2. Else `KANBANGO_GUI_PORT` env
|
|
@@ -242,9 +254,9 @@ If the preferred port is busy, the server picks the next free port. Always trust
|
|
|
242
254
|
**Auto-start with MCP:** set `KANBANGO_AUTO_GUI=1` in the MCP server env. GUI starts when MCP starts; use `status` to read the URL.
|
|
243
255
|
|
|
244
256
|
**Actions:**
|
|
245
|
-
- `start` - Launch the GUI server
|
|
246
|
-
- `stop` -
|
|
247
|
-
- `status` - Check
|
|
257
|
+
- `start` - Launch the GUI server (or report already_running owned/external)
|
|
258
|
+
- `stop` - Stop only MCP-owned GUI
|
|
259
|
+
- `status` - Check GUI state
|
|
248
260
|
|
|
249
261
|
**Parameters:**
|
|
250
262
|
```json
|
|
@@ -289,31 +301,57 @@ Check status:
|
|
|
289
301
|
```json
|
|
290
302
|
{
|
|
291
303
|
"status": "started",
|
|
304
|
+
"owned": true,
|
|
292
305
|
"port": 5623,
|
|
293
306
|
"pid": 12345,
|
|
294
307
|
"url": "http://localhost:5623"
|
|
295
308
|
}
|
|
296
309
|
```
|
|
297
310
|
|
|
298
|
-
**Response (stop):**
|
|
311
|
+
**Response (stop - owned):**
|
|
299
312
|
```json
|
|
300
313
|
{
|
|
301
314
|
"status": "stopping",
|
|
315
|
+
"owned": true,
|
|
302
316
|
"port": 5623,
|
|
303
317
|
"pid": 12345
|
|
304
318
|
}
|
|
305
319
|
```
|
|
306
320
|
|
|
307
|
-
**Response (
|
|
321
|
+
**Response (stop - external refused):**
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"status": "external_running",
|
|
325
|
+
"owned": false,
|
|
326
|
+
"port": 5623,
|
|
327
|
+
"pid": 12345,
|
|
328
|
+
"url": "http://localhost:5623",
|
|
329
|
+
"hint": "GUI was not started by this MCP process; stop refused."
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Response (status - running owned):**
|
|
308
334
|
```json
|
|
309
335
|
{
|
|
310
336
|
"status": "running",
|
|
337
|
+
"owned": true,
|
|
311
338
|
"port": 5623,
|
|
312
339
|
"pid": 12345,
|
|
313
340
|
"url": "http://localhost:5623"
|
|
314
341
|
}
|
|
315
342
|
```
|
|
316
343
|
|
|
344
|
+
**Response (status - external):**
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"status": "external_running",
|
|
348
|
+
"owned": false,
|
|
349
|
+
"port": 5623,
|
|
350
|
+
"pid": 99999,
|
|
351
|
+
"url": "http://localhost:5623"
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
317
355
|
**Response (status - not running):**
|
|
318
356
|
```json
|
|
319
357
|
{
|
|
@@ -426,17 +464,34 @@ Check status:
|
|
|
426
464
|
|
|
427
465
|
---
|
|
428
466
|
|
|
429
|
-
##
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
467
|
+
## Token-efficient playbook (source of truth)
|
|
468
|
+
|
|
469
|
+
**Edit `agent-playbook.js` only.** MCP tool descriptions and `operation=help` are built from it.
|
|
470
|
+
|
|
471
|
+
At runtime agents already receive the rules via tool descriptions. Humans can also call:
|
|
472
|
+
|
|
473
|
+
```json
|
|
474
|
+
{ "operation": "help" }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
on `kanban_read` (no board I/O).
|
|
478
|
+
|
|
479
|
+
### Drop-in for project `AGENTS.md` (optional)
|
|
480
|
+
|
|
481
|
+
Same bullets as `DROP_IN_RULE` in `agent-playbook.js` — copy if you want them outside MCP:
|
|
482
|
+
|
|
483
|
+
```text
|
|
484
|
+
Kanbango MCP — token rules:
|
|
485
|
+
- list: col filter, view=summary; keep task_ids; no full-board re-list after writes
|
|
486
|
+
- show: view=execution while coding; full only if needed
|
|
487
|
+
- create once with description,specs,in_scope,out_of_scope,acceptance_criteria
|
|
488
|
+
- move/update: return=none; subtasks=full array replace
|
|
489
|
+
- non-trivial: plan_create → plan_advance → plan_evidence (real tests, truncated logs) → plan_done
|
|
490
|
+
- gui: status before start; stop only owned; external_running = do not kill
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Columns
|
|
494
|
+
`planned` → `active` (few at a time) → `done`. Use `icebox` for parked work.
|
|
440
495
|
|
|
441
496
|
---
|
|
442
497
|
|
package/README.md
CHANGED
|
@@ -129,9 +129,11 @@ Once connected, your agent gets access to these tools:
|
|
|
129
129
|
|
|
130
130
|
| Tool | What it does |
|
|
131
131
|
|------|-------------|
|
|
132
|
-
| `kanban_read` | List tasks
|
|
133
|
-
| `kanban_manage` | Create, move,
|
|
134
|
-
| `kanban_gui` | Start
|
|
132
|
+
| `kanban_read` | List/show tasks (`view=summary` by default — cheap) |
|
|
133
|
+
| `kanban_manage` | Create, move, update, plan_* workflow |
|
|
134
|
+
| `kanban_gui` | Start / status / stop (stop only kills GUI this MCP started) |
|
|
135
|
+
|
|
136
|
+
**Token tip for agents:** rules ship inside MCP tool descriptions (`agent-playbook.js`). Optional: `kanban_read` → `operation: "help"`. Human setup notes: [LLM_AGENTS.md](./LLM_AGENTS.md).
|
|
135
137
|
|
|
136
138
|
Your agent stays in sync with your real board — every change is persisted as JSON files.
|
|
137
139
|
|
|
@@ -146,6 +148,7 @@ Your agent stays in sync with your real board — every change is persisted as J
|
|
|
146
148
|
| `kanban add <TITLE>` | Add a new task |
|
|
147
149
|
| `kanban move <ID> <COL>` | Move task |
|
|
148
150
|
| `kanban mcp-init` | Generate MCP config files |
|
|
151
|
+
| `kanban plan <action> --json '{...}'` | Accepted-plan workflow (create/advance/evidence/done/status) |
|
|
149
152
|
|
|
150
153
|
## Web GUI
|
|
151
154
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for agent-facing kanbango usage rules.
|
|
3
|
+
* MCP tool descriptions are built from this module — not from LLM_AGENTS.md.
|
|
4
|
+
* Docs may quote DROP_IN_RULE; keep them in sync by editing here only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DROP_IN_RULE = [
|
|
8
|
+
'Kanbango MCP — token rules:',
|
|
9
|
+
'- list: col filter, view=summary; keep task_ids; no full-board re-list after writes',
|
|
10
|
+
'- show: view=execution while coding; full only if needed',
|
|
11
|
+
'- create once with description,specs,in_scope,out_of_scope,acceptance_criteria',
|
|
12
|
+
'- move/update: return=none; subtasks=full array replace',
|
|
13
|
+
'- non-trivial: plan_create → plan_advance → plan_evidence (real tests, truncated logs) → plan_done',
|
|
14
|
+
'- gui: status before start; stop only owned; external_running = do not kill'
|
|
15
|
+
].join('\n');
|
|
16
|
+
|
|
17
|
+
const TOOL_DESCRIPTIONS = {
|
|
18
|
+
kanban_read: [
|
|
19
|
+
'Read board. TOKEN RULES: list defaults to view=summary (id/title/col/progress only).',
|
|
20
|
+
'Always pass col when possible. Prefer show+view=execution over full.',
|
|
21
|
+
'Do not re-list the whole board after every write — keep task_id from create/move.',
|
|
22
|
+
'IDs are numeric ("014" or "14"). views: summary|planning|execution|full; fields[] overrides view.',
|
|
23
|
+
'operation=help returns this playbook as short text (no board I/O).'
|
|
24
|
+
].join(' '),
|
|
25
|
+
|
|
26
|
+
kanban_manage: [
|
|
27
|
+
'Write board / plan. TOKEN RULES: one create with all planning fields beats many updates;',
|
|
28
|
+
'after write use return=none (or summary). Do not dump full task unless needed.',
|
|
29
|
+
'Actions: create|move|update (daily); plan_create→plan_advance→plan_evidence→plan_done (non-trivial only).',
|
|
30
|
+
'create/plan_create: title required; also send description,specs,in_scope,out_of_scope,acceptance_criteria',
|
|
31
|
+
'(missing → warnings, not failure). move: task_id+column. update: task_id + fields or subtasks[] full list',
|
|
32
|
+
'(no toggle). plan_evidence needs real test run: diff,test_command,stdout,stderr,exit_code — truncate logs.',
|
|
33
|
+
'Example create: {"action":"create","title":"Ship image","description":"...","specs":"...","in_scope":["CLI"],',
|
|
34
|
+
'"out_of_scope":["GUI"],"acceptance_criteria":["npm test passes"],"col":"planned"}'
|
|
35
|
+
].join(' '),
|
|
36
|
+
|
|
37
|
+
kanban_gui: [
|
|
38
|
+
'Web GUI control. Prefer status before start. stop only kills GUI this MCP spawned;',
|
|
39
|
+
'external_running = do not retry kill — use the returned url or leave it.',
|
|
40
|
+
'status: running|external_running|not_running. Rarely needed mid-task — open once if user wants UI.'
|
|
41
|
+
].join(' ')
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const MUST_CONTAIN = [
|
|
45
|
+
'TOKEN RULES',
|
|
46
|
+
'view=summary',
|
|
47
|
+
'return=none',
|
|
48
|
+
'plan_create',
|
|
49
|
+
'external_running',
|
|
50
|
+
'subtasks'
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function playbookHelpPayload() {
|
|
54
|
+
return {
|
|
55
|
+
source: 'agent-playbook.js',
|
|
56
|
+
note: 'Canonical rules for agents. Same text drives MCP tool descriptions. LLM_AGENTS.md is human docs only.',
|
|
57
|
+
drop_in_rule: DROP_IN_RULE,
|
|
58
|
+
tools: {
|
|
59
|
+
kanban_read: TOOL_DESCRIPTIONS.kanban_read,
|
|
60
|
+
kanban_manage: TOOL_DESCRIPTIONS.kanban_manage,
|
|
61
|
+
kanban_gui: TOOL_DESCRIPTIONS.kanban_gui
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
DROP_IN_RULE,
|
|
68
|
+
TOOL_DESCRIPTIONS,
|
|
69
|
+
MUST_CONTAIN,
|
|
70
|
+
playbookHelpPayload
|
|
71
|
+
};
|
package/bin/kanban-cmd.js
CHANGED
package/bin/kanban.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const kanban = require('../kanban.js');
|
|
4
4
|
const plan = require('../plan.js');
|
|
5
|
+
const guiRegistry = require('../gui-registry.js');
|
|
5
6
|
const http = require('http');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
8
9
|
|
|
9
10
|
const BACKLOG = path.join(process.cwd(), 'backlog');
|
|
10
|
-
const COLS = kanban.COLS;
|
|
11
11
|
|
|
12
12
|
function shortId(taskId) {
|
|
13
13
|
const match = taskId.match(/^(?:[A-Z]+-)?(\d+)/);
|
|
@@ -357,11 +357,11 @@ async function serveWeb(port) {
|
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
const actualPort = server.address().port;
|
|
360
|
-
const portInfo = await
|
|
360
|
+
const portInfo = await guiRegistry.writeGuiPortFile({ port: actualPort, pid: process.pid });
|
|
361
361
|
|
|
362
362
|
async function cleanupGuiPortFile() {
|
|
363
363
|
try {
|
|
364
|
-
await
|
|
364
|
+
await guiRegistry.clearGuiPortFile({ pid: process.pid });
|
|
365
365
|
} catch {
|
|
366
366
|
// best-effort cleanup
|
|
367
367
|
}
|
|
@@ -458,9 +458,9 @@ async function main() {
|
|
|
458
458
|
const args = process.argv.slice(2);
|
|
459
459
|
const cmd = args[0];
|
|
460
460
|
|
|
461
|
-
if (!cmd || cmd === 'serve') {
|
|
462
|
-
|
|
463
|
-
|
|
461
|
+
if (!cmd || cmd === 'serve') {
|
|
462
|
+
const port = guiRegistry.resolvePreferredGuiPort(args[1]);
|
|
463
|
+
await serveWeb(port);
|
|
464
464
|
} else if (cmd === 'init') {
|
|
465
465
|
await cliInit();
|
|
466
466
|
} else if (cmd === 'mcp-init') {
|
package/gui-registry.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const BACKLOG = path.join(process.cwd(), 'backlog');
|
|
5
|
+
const GUI_PORT_FILE = '.kanbango-gui.json';
|
|
6
|
+
const GUI_PORT_MIN = 5510;
|
|
7
|
+
const GUI_PORT_MAX = 5999;
|
|
8
|
+
const GUI_PORT_SPAN = GUI_PORT_MAX - GUI_PORT_MIN + 1;
|
|
9
|
+
|
|
10
|
+
function createGuiError(code, message, hint, details = {}, retryable = false, status = 400) {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.code = code;
|
|
13
|
+
error.hint = hint;
|
|
14
|
+
error.details = details;
|
|
15
|
+
error.retryable = retryable;
|
|
16
|
+
error.status = status;
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function guiPortFilePath() {
|
|
21
|
+
return path.join(BACKLOG, GUI_PORT_FILE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hashCwdToPort(cwd = process.cwd()) {
|
|
25
|
+
let hash = 0;
|
|
26
|
+
const input = String(cwd);
|
|
27
|
+
for (let i = 0; i < input.length; i++) {
|
|
28
|
+
hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0;
|
|
29
|
+
}
|
|
30
|
+
return GUI_PORT_MIN + (Math.abs(hash) % GUI_PORT_SPAN);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeGuiPort(value) {
|
|
34
|
+
const parsed = Number.parseInt(value, 10);
|
|
35
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolvePreferredGuiPort(explicitPort) {
|
|
42
|
+
if (explicitPort !== undefined && explicitPort !== null && explicitPort !== '') {
|
|
43
|
+
const fromArg = normalizeGuiPort(explicitPort);
|
|
44
|
+
if (fromArg) return fromArg;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fromEnv = normalizeGuiPort(process.env.KANBANGO_GUI_PORT);
|
|
48
|
+
if (fromEnv) return fromEnv;
|
|
49
|
+
|
|
50
|
+
return hashCwdToPort(process.cwd());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isPidAlive(pid) {
|
|
54
|
+
const n = Number.parseInt(pid, 10);
|
|
55
|
+
if (!Number.isFinite(n) || n <= 0) return false;
|
|
56
|
+
try {
|
|
57
|
+
process.kill(n, 0);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function ensureBacklogDir() {
|
|
65
|
+
await fs.mkdir(BACKLOG, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function writeGuiPortFile({ port, pid = process.pid } = {}) {
|
|
69
|
+
const normalizedPort = normalizeGuiPort(port);
|
|
70
|
+
if (!normalizedPort) {
|
|
71
|
+
throw createGuiError(
|
|
72
|
+
'VALIDATION_ERROR',
|
|
73
|
+
'Invalid GUI port',
|
|
74
|
+
'Use an integer between 1 and 65535',
|
|
75
|
+
{ port },
|
|
76
|
+
false,
|
|
77
|
+
400
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await ensureBacklogDir();
|
|
82
|
+
const data = {
|
|
83
|
+
port: normalizedPort,
|
|
84
|
+
pid,
|
|
85
|
+
url: `http://localhost:${normalizedPort}`,
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
started_at: new Date().toISOString()
|
|
88
|
+
};
|
|
89
|
+
await fs.writeFile(guiPortFilePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function readGuiPortFile() {
|
|
94
|
+
try {
|
|
95
|
+
const raw = await fs.readFile(guiPortFilePath(), 'utf-8');
|
|
96
|
+
const data = JSON.parse(raw);
|
|
97
|
+
if (!data || !normalizeGuiPort(data.port)) return null;
|
|
98
|
+
return data;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.code === 'ENOENT') return null;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function clearGuiPortFile({ pid, force = false } = {}) {
|
|
106
|
+
const info = await readGuiPortFile();
|
|
107
|
+
if (!info) return false;
|
|
108
|
+
if (!force && pid !== undefined && info.pid !== pid) return false;
|
|
109
|
+
if (!force && pid === undefined && info.pid !== process.pid) return false;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await fs.unlink(guiPortFilePath());
|
|
113
|
+
return true;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.code === 'ENOENT') return false;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function discoverRunningGui() {
|
|
121
|
+
const info = await readGuiPortFile();
|
|
122
|
+
if (!info || !isPidAlive(info.pid)) {
|
|
123
|
+
if (info) await clearGuiPortFile({ force: true });
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
status: 'running',
|
|
128
|
+
port: info.port,
|
|
129
|
+
pid: info.pid,
|
|
130
|
+
url: info.url || `http://localhost:${info.port}`,
|
|
131
|
+
cwd: info.cwd,
|
|
132
|
+
started_at: info.started_at
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
hashCwdToPort,
|
|
138
|
+
normalizeGuiPort,
|
|
139
|
+
resolvePreferredGuiPort,
|
|
140
|
+
isPidAlive,
|
|
141
|
+
writeGuiPortFile,
|
|
142
|
+
readGuiPortFile,
|
|
143
|
+
clearGuiPortFile,
|
|
144
|
+
discoverRunningGui,
|
|
145
|
+
guiPortFilePath,
|
|
146
|
+
GUI_PORT_MIN,
|
|
147
|
+
GUI_PORT_MAX
|
|
148
|
+
};
|
package/index.js
CHANGED
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
const kanban = require('./kanban.js');
|
|
12
12
|
const plan = require('./plan.js');
|
|
13
|
+
const guiRegistry = require('./gui-registry.js');
|
|
14
|
+
const playbook = require('./agent-playbook.js');
|
|
13
15
|
|
|
14
16
|
module.exports = {
|
|
15
17
|
kanban,
|
|
16
18
|
plan,
|
|
19
|
+
guiRegistry,
|
|
20
|
+
playbook,
|
|
17
21
|
};
|
package/kanban.js
CHANGED
|
@@ -3,10 +3,6 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
const BACKLOG = path.join(process.cwd(), 'backlog');
|
|
5
5
|
const COLS = ['active', 'planned', 'icebox', 'done'];
|
|
6
|
-
const GUI_PORT_FILE = '.kanbango-gui.json';
|
|
7
|
-
const GUI_PORT_MIN = 5510;
|
|
8
|
-
const GUI_PORT_MAX = 5999;
|
|
9
|
-
const GUI_PORT_SPAN = GUI_PORT_MAX - GUI_PORT_MIN + 1;
|
|
10
6
|
const STATUS_MAP = {
|
|
11
7
|
active: 'in_progress',
|
|
12
8
|
planned: 'planned',
|
|
@@ -383,7 +379,7 @@ async function findFile(epicId) {
|
|
|
383
379
|
const candidates = files
|
|
384
380
|
.filter((file) => (file.endsWith('.json') || file.endsWith('.md'))
|
|
385
381
|
&& path.basename(file, path.extname(file)) === epicId)
|
|
386
|
-
.sort((left,
|
|
382
|
+
.sort((left, _right) => (left.endsWith('.json') ? -1 : 1));
|
|
387
383
|
if (candidates[0]) {
|
|
388
384
|
return path.join(colDir, candidates[0]);
|
|
389
385
|
}
|
|
@@ -564,11 +560,6 @@ async function doCreate(title, column = 'planned', epicGroup = '—', extra = {}
|
|
|
564
560
|
validateColumn(column, 'col');
|
|
565
561
|
|
|
566
562
|
const nextId = await nextTaskNumber();
|
|
567
|
-
const slug = title
|
|
568
|
-
.toLowerCase()
|
|
569
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
570
|
-
.replace(/^-+|-+$/g, '')
|
|
571
|
-
.substring(0, 25);
|
|
572
563
|
const task = normalizeTask({
|
|
573
564
|
id: String(nextId).padStart(3, '0'),
|
|
574
565
|
title,
|
|
@@ -769,118 +760,6 @@ async function doUpdate(epicId, newTitle, newTasks) {
|
|
|
769
760
|
}
|
|
770
761
|
}
|
|
771
762
|
|
|
772
|
-
function guiPortFilePath() {
|
|
773
|
-
return path.join(BACKLOG, GUI_PORT_FILE);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function hashCwdToPort(cwd = process.cwd()) {
|
|
777
|
-
let hash = 0;
|
|
778
|
-
const input = String(cwd);
|
|
779
|
-
for (let i = 0; i < input.length; i++) {
|
|
780
|
-
hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0;
|
|
781
|
-
}
|
|
782
|
-
return GUI_PORT_MIN + (Math.abs(hash) % GUI_PORT_SPAN);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function normalizeGuiPort(value) {
|
|
786
|
-
const parsed = Number.parseInt(value, 10);
|
|
787
|
-
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
788
|
-
return null;
|
|
789
|
-
}
|
|
790
|
-
return parsed;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function resolvePreferredGuiPort(explicitPort) {
|
|
794
|
-
if (explicitPort !== undefined && explicitPort !== null && explicitPort !== '') {
|
|
795
|
-
const fromArg = normalizeGuiPort(explicitPort);
|
|
796
|
-
if (fromArg) return fromArg;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const fromEnv = normalizeGuiPort(process.env.KANBANGO_GUI_PORT);
|
|
800
|
-
if (fromEnv) return fromEnv;
|
|
801
|
-
|
|
802
|
-
return hashCwdToPort(process.cwd());
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function isPidAlive(pid) {
|
|
806
|
-
const n = Number.parseInt(pid, 10);
|
|
807
|
-
if (!Number.isFinite(n) || n <= 0) return false;
|
|
808
|
-
try {
|
|
809
|
-
process.kill(n, 0);
|
|
810
|
-
return true;
|
|
811
|
-
} catch {
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
async function writeGuiPortFile({ port, pid = process.pid } = {}) {
|
|
817
|
-
const normalizedPort = normalizeGuiPort(port);
|
|
818
|
-
if (!normalizedPort) {
|
|
819
|
-
throw createKanbanError(
|
|
820
|
-
'VALIDATION_ERROR',
|
|
821
|
-
'Invalid GUI port',
|
|
822
|
-
'Use an integer between 1 and 65535',
|
|
823
|
-
{ port },
|
|
824
|
-
false,
|
|
825
|
-
400
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
await ensureBacklogDir();
|
|
830
|
-
const data = {
|
|
831
|
-
port: normalizedPort,
|
|
832
|
-
pid,
|
|
833
|
-
url: `http://localhost:${normalizedPort}`,
|
|
834
|
-
cwd: process.cwd(),
|
|
835
|
-
started_at: new Date().toISOString()
|
|
836
|
-
};
|
|
837
|
-
await fs.writeFile(guiPortFilePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
838
|
-
return data;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
async function readGuiPortFile() {
|
|
842
|
-
try {
|
|
843
|
-
const raw = await fs.readFile(guiPortFilePath(), 'utf-8');
|
|
844
|
-
const data = JSON.parse(raw);
|
|
845
|
-
if (!data || !normalizeGuiPort(data.port)) return null;
|
|
846
|
-
return data;
|
|
847
|
-
} catch (error) {
|
|
848
|
-
if (error.code === 'ENOENT') return null;
|
|
849
|
-
return null;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
async function clearGuiPortFile({ pid, force = false } = {}) {
|
|
854
|
-
const info = await readGuiPortFile();
|
|
855
|
-
if (!info) return false;
|
|
856
|
-
if (!force && pid !== undefined && info.pid !== pid) return false;
|
|
857
|
-
if (!force && pid === undefined && info.pid !== process.pid) return false;
|
|
858
|
-
|
|
859
|
-
try {
|
|
860
|
-
await fs.unlink(guiPortFilePath());
|
|
861
|
-
return true;
|
|
862
|
-
} catch (error) {
|
|
863
|
-
if (error.code === 'ENOENT') return false;
|
|
864
|
-
throw error;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
async function discoverRunningGui() {
|
|
869
|
-
const info = await readGuiPortFile();
|
|
870
|
-
if (!info || !isPidAlive(info.pid)) {
|
|
871
|
-
if (info) await clearGuiPortFile({ force: true });
|
|
872
|
-
return null;
|
|
873
|
-
}
|
|
874
|
-
return {
|
|
875
|
-
status: 'running',
|
|
876
|
-
port: info.port,
|
|
877
|
-
pid: info.pid,
|
|
878
|
-
url: info.url || `http://localhost:${info.port}`,
|
|
879
|
-
cwd: info.cwd,
|
|
880
|
-
started_at: info.started_at
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
|
|
884
763
|
module.exports = {
|
|
885
764
|
ensureBacklogDir,
|
|
886
765
|
parseEpic,
|
|
@@ -899,19 +778,8 @@ module.exports = {
|
|
|
899
778
|
missingRecommendedCreateFields,
|
|
900
779
|
getProgress,
|
|
901
780
|
resolveTaskId,
|
|
902
|
-
hashCwdToPort,
|
|
903
|
-
normalizeGuiPort,
|
|
904
|
-
resolvePreferredGuiPort,
|
|
905
|
-
isPidAlive,
|
|
906
|
-
writeGuiPortFile,
|
|
907
|
-
readGuiPortFile,
|
|
908
|
-
clearGuiPortFile,
|
|
909
|
-
discoverRunningGui,
|
|
910
|
-
guiPortFilePath,
|
|
911
781
|
COLS,
|
|
912
782
|
STATUS_MAP,
|
|
913
783
|
VIEW_FIELDS,
|
|
914
|
-
RECOMMENDED_CREATE_FIELDS
|
|
915
|
-
GUI_PORT_MIN,
|
|
916
|
-
GUI_PORT_MAX
|
|
784
|
+
RECOMMENDED_CREATE_FIELDS
|
|
917
785
|
};
|
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
|
}
|
|
@@ -656,9 +679,10 @@ async function maybeAutoStartGui() {
|
|
|
656
679
|
function installGuiShutdownHooks() {
|
|
657
680
|
let shuttingDown = false;
|
|
658
681
|
|
|
659
|
-
async function
|
|
682
|
+
async function shutdownOwnedGui() {
|
|
660
683
|
if (shuttingDown) return;
|
|
661
684
|
shuttingDown = true;
|
|
685
|
+
if (!ownsGuiProcess()) return;
|
|
662
686
|
try {
|
|
663
687
|
await stopGuiServer();
|
|
664
688
|
} catch {
|
|
@@ -667,7 +691,7 @@ function installGuiShutdownHooks() {
|
|
|
667
691
|
}
|
|
668
692
|
|
|
669
693
|
process.once('exit', () => {
|
|
670
|
-
if (
|
|
694
|
+
if (ownsGuiProcess()) {
|
|
671
695
|
try {
|
|
672
696
|
guiProcess.kill();
|
|
673
697
|
} catch {
|
|
@@ -676,10 +700,10 @@ function installGuiShutdownHooks() {
|
|
|
676
700
|
}
|
|
677
701
|
});
|
|
678
702
|
process.once('SIGINT', () => {
|
|
679
|
-
|
|
703
|
+
shutdownOwnedGui().finally(() => process.exit(0));
|
|
680
704
|
});
|
|
681
705
|
process.once('SIGTERM', () => {
|
|
682
|
-
|
|
706
|
+
shutdownOwnedGui().finally(() => process.exit(0));
|
|
683
707
|
});
|
|
684
708
|
}
|
|
685
709
|
|
|
@@ -699,7 +723,8 @@ module.exports = {
|
|
|
699
723
|
startGuiServer,
|
|
700
724
|
stopGuiServer,
|
|
701
725
|
guiStatus,
|
|
702
|
-
resolvePreferredGuiPort:
|
|
726
|
+
resolvePreferredGuiPort: guiRegistry.resolvePreferredGuiPort,
|
|
727
|
+
playbook,
|
|
703
728
|
server,
|
|
704
729
|
main
|
|
705
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');
|