kanbango 3.1.0 → 3.3.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 +3 -0
- package/.ai/retro/last-run.json +1 -1
- package/.opencode/plans/action-error-recipe.md +121 -0
- package/AGENTS.md +4 -2
- package/CHANGELOG.md +27 -0
- package/LLM_AGENTS.md +1 -1
- package/README.md +11 -3
- package/agent-playbook.js +18 -9
- package/bin/kanban.js +143 -10
- package/gui-registry.js +14 -1
- package/index.html +645 -39
- package/kanban.js +474 -110
- package/mcp-server.js +253 -45
- package/package.json +1 -1
- package/tests/run.js +3 -0
package/.ai/lessons.jsonl
CHANGED
|
@@ -2,3 +2,6 @@
|
|
|
2
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
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
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}
|
|
5
|
+
{"id":"260731-o6g","ts":"2026-07-31","scope":"proj","tags":["kanban","epics","mcp"],"rule":"DO Default list/list_epics hide done+archived; hard-delete cascades epic children; archive is reversible flag","when":"kanban cleanup context","sev":2,"hits":1}
|
|
6
|
+
{"id":"260803-b27","ts":"2026-08-03","scope":"proj","tags":["tests","race","fs"],"rule":"DO Serialize board file mutations with an in-process lock and atomic write (temp+rename/link); race tests should assert…","when":"kanban concurrent IO","sev":2,"hits":1}
|
|
7
|
+
{"id":"260813-9t0","ts":"2026-08-13","scope":"proj","tags":["mcp","errors","dx"],"rule":"DO MCP missing-field errors: put Valid list + Example JSON + sent keys in message so the agent can retry without guessi…","when":"MCP tool validation / agent-facing erro…","sev":2,"hits":1}
|
package/.ai/retro/last-run.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"ts":"2026-
|
|
1
|
+
{"ts":"2026-08-13T06:43:28.413Z","sessionID":"ses_038ddd435ffeginZz7W4t3X27v","added":["260813-9t0"],"bumped":[]}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Action error recipe (implement)
|
|
2
|
+
|
|
3
|
+
Status: approved (user said go). Plan mode blocked code edits — switch to **build** and apply.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
Missing/empty/unknown `action` on `kanban_manage` / `kanban_gui`: **message** is a full retry recipe (Valid + Example JSON + sent keys).
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
1. `mcp-server.js` — replace `requireToolAction`; add helpers; unknown-action defaults
|
|
10
|
+
2. `tests/mcp-server.test.js` — assert recipe content
|
|
11
|
+
3. `CHANGELOG.md` — one-line unreleased note
|
|
12
|
+
|
|
13
|
+
## mcp-server.js
|
|
14
|
+
|
|
15
|
+
Replace current `requireToolAction` block (after `GUI_ACTIONS`) with:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const ACTION_EXAMPLES = {
|
|
19
|
+
kanban_manage: '{"action":"create","title":"Ship image","col":"planned"}',
|
|
20
|
+
kanban_gui: '{"action":"status"}'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function receivedKeys(args) {
|
|
24
|
+
return Object.keys(args && typeof args === 'object' ? args : {});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function actionRecipeMessage(toolName, allowed, opts = {}) {
|
|
28
|
+
const keys = opts.received_keys || [];
|
|
29
|
+
const example = ACTION_EXAMPLES[toolName] || `{"action":"${allowed[0]}"}`;
|
|
30
|
+
const lines = [];
|
|
31
|
+
if (opts.kind === 'unknown') {
|
|
32
|
+
lines.push(`Unknown action "${opts.action}" on ${toolName}.`);
|
|
33
|
+
} else {
|
|
34
|
+
lines.push(`Missing required top-level field "action" on ${toolName}.`);
|
|
35
|
+
}
|
|
36
|
+
lines.push('Pass action next to other args (not nested under params).');
|
|
37
|
+
lines.push(`Valid: ${allowed.join(', ')}.`);
|
|
38
|
+
lines.push(`Example: ${example}`);
|
|
39
|
+
lines.push(keys.length > 0 ? `You sent keys: ${keys.join(', ')}` : 'You sent keys: (none)');
|
|
40
|
+
return lines.join(' ');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function actionRecipeHint() {
|
|
44
|
+
return [
|
|
45
|
+
'Retry the same tool with top-level action set to one Valid value.',
|
|
46
|
+
'Common mistake: omitting action, or putting it under params (jira/gitlab style) — kanban uses top-level action.'
|
|
47
|
+
].join(' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function requireToolAction(args, allowed, toolName) {
|
|
51
|
+
const action = args && args.action;
|
|
52
|
+
const keys = receivedKeys(args);
|
|
53
|
+
if (action === undefined || action === null || action === '') {
|
|
54
|
+
throw kanban.createKanbanError(
|
|
55
|
+
'MISSING_REQUIRED_FIELD',
|
|
56
|
+
actionRecipeMessage(toolName, allowed, {
|
|
57
|
+
kind: 'missing',
|
|
58
|
+
received_keys: keys,
|
|
59
|
+
action: action === undefined ? null : action
|
|
60
|
+
}),
|
|
61
|
+
actionRecipeHint(),
|
|
62
|
+
{
|
|
63
|
+
field: 'action',
|
|
64
|
+
tool: toolName,
|
|
65
|
+
received_keys: keys,
|
|
66
|
+
allowed_actions: allowed,
|
|
67
|
+
action: action === undefined ? null : action
|
|
68
|
+
},
|
|
69
|
+
false,
|
|
70
|
+
400
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return action;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function unknownToolAction(action, args, allowed, toolName) {
|
|
77
|
+
const keys = receivedKeys(args);
|
|
78
|
+
return invalidRequest(
|
|
79
|
+
actionRecipeMessage(toolName, allowed, { kind: 'unknown', action, received_keys: keys }),
|
|
80
|
+
actionRecipeHint(),
|
|
81
|
+
{
|
|
82
|
+
field: 'action',
|
|
83
|
+
tool: toolName,
|
|
84
|
+
action,
|
|
85
|
+
received_keys: keys,
|
|
86
|
+
allowed_actions: allowed
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Manage default: `throw unknownToolAction(action, args, MANAGE_ACTIONS, 'kanban_manage');`
|
|
93
|
+
GUI default: `throw unknownToolAction(action, args, GUI_ACTIONS, 'kanban_gui');`
|
|
94
|
+
|
|
95
|
+
## tests/mcp-server.test.js
|
|
96
|
+
|
|
97
|
+
Assert on missing/empty/unknown/gui:
|
|
98
|
+
- message includes `Missing required top-level field "action"` (or `Unknown action "nope"`)
|
|
99
|
+
- `Valid:` + key actions + `Example:` + `"action":`
|
|
100
|
+
- sent keys line
|
|
101
|
+
- `details.allowed_actions`
|
|
102
|
+
- no `undefined` in message
|
|
103
|
+
- codes unchanged: MISSING_REQUIRED_FIELD / VALIDATION_ERROR
|
|
104
|
+
|
|
105
|
+
## CHANGELOG
|
|
106
|
+
|
|
107
|
+
```md
|
|
108
|
+
### Fixed
|
|
109
|
+
- MCP `kanban_manage` / `kanban_gui`: missing/empty/unknown `action` errors put a full retry recipe in `message` (Valid list + Example JSON + sent keys); missing/empty use `MISSING_REQUIRED_FIELD` instead of `Unknown action: undefined`
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Verify
|
|
113
|
+
```bash
|
|
114
|
+
node tests/mcp-server.test.js
|
|
115
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"kanban_manage","arguments":{"title":"x"}}}' | node mcp-server.js
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Acceptance
|
|
119
|
+
- [ ] Missing action message is copy-paste retry recipe
|
|
120
|
+
- [ ] Unknown action same clarity
|
|
121
|
+
- [ ] Tests green
|
package/AGENTS.md
CHANGED
|
@@ -8,11 +8,13 @@ Source of truth: `agent-playbook.js` (also MCP tool descriptions + `kanban_read`
|
|
|
8
8
|
|
|
9
9
|
- hierarchy: epic (context) → task (work+plan) → subtasks (steps)
|
|
10
10
|
- list: `col` filter, `view=summary`; keep `task_id`s; no full-board re-list after every write
|
|
11
|
-
- list_epics
|
|
11
|
+
- list_epics default = live only (hides done + archived); `include_archived` / `status` to widen
|
|
12
|
+
- list tasks default hides tasks under done/archived epics; show/show_epic by id always works
|
|
12
13
|
- show: `view=execution` while coding; `full` only if needed
|
|
13
14
|
- epic_create with description/goals; create tasks with `epic=E001` (prefer id)
|
|
14
15
|
- create once with `description`, `specs`, `in_scope`, `out_of_scope`, `acceptance_criteria`
|
|
15
|
-
- move/update: `return=none`; subtasks = full array replace (no toggle)
|
|
16
|
+
- move/update/delete: `return=none`; subtasks = full array replace (no toggle)
|
|
17
|
+
- cleanup: `delete` (task), `epic_delete` (cascade), `epic_archive` / `epic_unarchive`
|
|
16
18
|
- non-trivial work: `plan_create` → `plan_advance` → `plan_evidence` (real tests, truncated logs) → `plan_done`
|
|
17
19
|
- gui: `status` before `start`; `stop` only owned; `external_running` = do not kill
|
|
18
20
|
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [3.3.0] - 2026-08-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Task comments: append-only `comments[]` on tasks; MCP `comment_add` (`task_id` + `text`, optional `author`); CLI `kanban comment <ID> <text> [--author NAME]`
|
|
14
|
+
- Comments included in `execution` / `full` task views (omitted from `summary`)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- MCP `kanban_manage` / `kanban_gui`: missing/empty/unknown `action` errors put a full retry recipe in `message` (Valid list + Example JSON + sent keys); missing/empty use `MISSING_REQUIRED_FIELD` instead of `Unknown action: undefined`
|
|
18
|
+
|
|
19
|
+
## [3.2.0] - 2026-07-31
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- GUI project label: browser tab title, header, port file, and `kanban_gui` start/status include `project` (basename of cwd; override with `KANBANGO_PROJECT_NAME`)
|
|
23
|
+
- Epic field `archived` + status `archived` (overrides derived child rollup)
|
|
24
|
+
- Hard delete: MCP `delete` / `epic_delete` (cascade child tasks), CLI `kanban rm` / `kanban epic rm`, HTTP `DELETE`
|
|
25
|
+
- Archive: MCP `epic_archive` / `epic_unarchive`, CLI `kanban epic archive|unarchive`, HTTP `POST .../archive|unarchive`
|
|
26
|
+
- `list_epics` default live-only (empty|planned|active); `include_archived`, `include_done`, `status` filters
|
|
27
|
+
- Task `list` hides tasks under done/archived epics unless `include_archived`/`include_done` (explicit `epic=` filter still shows them)
|
|
28
|
+
- GUI: Archive/Unarchive/Delete on epic swimlane, Delete on task card, Show archived toggle
|
|
29
|
+
- Tests: `tests/delete-archive.test.js`
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Agent playbook documents cleanup + live-only epic discovery
|
|
33
|
+
- `show` / `show_epic` by id still return archived items (explicit lookup)
|
|
34
|
+
|
|
8
35
|
## [3.1.0] - 2026-07-30
|
|
9
36
|
|
|
10
37
|
### Added
|
package/LLM_AGENTS.md
CHANGED
|
@@ -249,7 +249,7 @@ Control the web GUI server: start, stop, or check status.
|
|
|
249
249
|
2. Else `KANBANGO_GUI_PORT` env
|
|
250
250
|
3. Else stable hash of project cwd in range `5510–5999`
|
|
251
251
|
|
|
252
|
-
If the preferred port is busy, the server picks the next free port. Always trust the returned `url` / `port` (also written to `backlog/.kanbango-gui.json`).
|
|
252
|
+
If the preferred port is busy, the server picks the next free port. Always trust the returned `url` / `port` (also written to `backlog/.kanbango-gui.json`). Responses also include `project` (basename of project cwd, or `KANBANGO_PROJECT_NAME`) so agents and humans can tell boards apart; the GUI tab title and header show the same label.
|
|
253
253
|
|
|
254
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.
|
|
255
255
|
|
package/README.md
CHANGED
|
@@ -35,10 +35,17 @@ kanban list --json
|
|
|
35
35
|
kanban epic add "Phase 1" --description "Why this initiative" --goals "Ship X"
|
|
36
36
|
kanban add "My task" --col planned --epic E001
|
|
37
37
|
|
|
38
|
-
# List epics / show epic with child rollup
|
|
38
|
+
# List epics / show epic with child rollup (default: live only)
|
|
39
39
|
kanban epic list --json
|
|
40
|
+
kanban epic list --all --json
|
|
40
41
|
kanban epic show E001
|
|
41
42
|
|
|
43
|
+
# Archive or delete (epic_delete cascades child tasks)
|
|
44
|
+
kanban epic archive E001
|
|
45
|
+
kanban epic unarchive E001
|
|
46
|
+
kanban epic rm E001
|
|
47
|
+
kanban rm 001
|
|
48
|
+
|
|
42
49
|
# Show details
|
|
43
50
|
kanban show 001
|
|
44
51
|
|
|
@@ -130,7 +137,7 @@ Auto-start the web GUI with MCP (opt-in):
|
|
|
130
137
|
}
|
|
131
138
|
```
|
|
132
139
|
|
|
133
|
-
Optional: pin the port with `KANBANGO_GUI_PORT` (e.g. `"5821"`). Without it, each project gets a stable port in `5510–5999` derived from the project path. The real URL is always available via `kanban_gui` → `status` (and written to `backlog/.kanbango-gui.json` while the GUI runs).
|
|
140
|
+
Optional: pin the port with `KANBANGO_GUI_PORT` (e.g. `"5821"`). Without it, each project gets a stable port in `5510–5999` derived from the project path. The real URL is always available via `kanban_gui` → `status` (and written to `backlog/.kanbango-gui.json` while the GUI runs). The browser tab title, GUI header, and `kanban_gui` responses include `project` (folder name of the project cwd; override with `KANBANGO_PROJECT_NAME`).
|
|
134
141
|
|
|
135
142
|
Or generate the config files automatically:
|
|
136
143
|
|
|
@@ -145,7 +152,7 @@ Once connected, your agent gets access to these tools:
|
|
|
145
152
|
| Tool | What it does |
|
|
146
153
|
|------|-------------|
|
|
147
154
|
| `kanban_read` | List/show tasks (`view=summary` by default — cheap) |
|
|
148
|
-
| `kanban_manage` | Create, move, update, plan_* workflow |
|
|
155
|
+
| `kanban_manage` | Create, move, update, comment_add, plan_* workflow |
|
|
149
156
|
| `kanban_gui` | Start / status / stop (stop only kills GUI this MCP started) |
|
|
150
157
|
|
|
151
158
|
**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).
|
|
@@ -163,6 +170,7 @@ Your agent stays in sync with your real board — every change is persisted as J
|
|
|
163
170
|
| `kanban add <TITLE>` | Add a new task |
|
|
164
171
|
| `kanban move <ID> <COL>` | Move task |
|
|
165
172
|
| `kanban mcp-init` | Generate MCP config files |
|
|
173
|
+
| `kanban comment <ID> <text> [--author NAME]` | Append a comment to a task |
|
|
166
174
|
| `kanban plan <action> --json '{...}'` | Accepted-plan workflow (create/advance/evidence/done/status) |
|
|
167
175
|
|
|
168
176
|
## Web GUI
|
package/agent-playbook.js
CHANGED
|
@@ -8,11 +8,13 @@ const DROP_IN_RULE = [
|
|
|
8
8
|
'Kanbango MCP — token rules:',
|
|
9
9
|
'- hierarchy: epic (context) → task (work+plan) → subtasks (steps)',
|
|
10
10
|
'- list: col filter, view=summary; keep task_ids; no full-board re-list after writes',
|
|
11
|
-
'- list_epics
|
|
12
|
-
'-
|
|
11
|
+
'- list_epics default = live only (hides done + archived); include_archived / status to widen',
|
|
12
|
+
'- list tasks default hides tasks under done/archived epics; show/show_epic by id always works',
|
|
13
|
+
'- show: view=execution while coding (includes comments); full only if needed',
|
|
13
14
|
'- epic_create with description/goals; create tasks with epic=E001 (prefer id)',
|
|
14
15
|
'- create once with description,specs,in_scope,out_of_scope,acceptance_criteria',
|
|
15
|
-
'- move/update: return=none; subtasks=full array replace',
|
|
16
|
+
'- move/update/delete: return=none; subtasks=full array replace; comment_add: task_id+text',
|
|
17
|
+
'- cleanup: delete (task), epic_delete (cascade tasks), epic_archive / epic_unarchive',
|
|
16
18
|
'- non-trivial: plan_create → plan_advance → plan_evidence (real tests, truncated logs) → plan_done',
|
|
17
19
|
'- gui: status before start; stop only owned; external_running = do not kill'
|
|
18
20
|
].join('\n');
|
|
@@ -21,8 +23,10 @@ const TOOL_DESCRIPTIONS = {
|
|
|
21
23
|
kanban_read: [
|
|
22
24
|
'Read board. TOKEN RULES: list defaults to view=summary (id/title/col/progress only).',
|
|
23
25
|
'Hierarchy: epic (container/context) → task (work) → subtasks (steps).',
|
|
24
|
-
'Always pass col when possible. Prefer show+view=execution over full.',
|
|
25
|
-
'list_epics
|
|
26
|
+
'Always pass col when possible. Prefer show+view=execution over full (includes comments).',
|
|
27
|
+
'list_epics default live only (empty|planned|active; hides done+archived).',
|
|
28
|
+
'Pass include_archived=true or status=done|archived to see closed initiatives.',
|
|
29
|
+
'list hides tasks under done/archived epics unless include_archived/include_done; show/show_epic by id always works.',
|
|
26
30
|
'Do not re-list the whole board after every write — keep task_id from create/move.',
|
|
27
31
|
'Task IDs numeric ("014"); epic IDs "E001". views: summary|planning|execution|full; fields[] overrides view.',
|
|
28
32
|
'operation=help returns this playbook as short text (no board I/O).'
|
|
@@ -31,12 +35,14 @@ const TOOL_DESCRIPTIONS = {
|
|
|
31
35
|
kanban_manage: [
|
|
32
36
|
'Write board / plan. TOKEN RULES: one create with all planning fields beats many updates;',
|
|
33
37
|
'after write use return=none (or summary). Do not dump full task unless needed.',
|
|
34
|
-
'Actions: create|move|update (daily); epic_create|epic_update
|
|
35
|
-
'plan_create→plan_advance→plan_evidence→plan_done (non-trivial only).',
|
|
38
|
+
'Actions: create|move|update|delete (daily); epic_create|epic_update|epic_archive|epic_unarchive|epic_delete;',
|
|
39
|
+
'comment_add (append-only history); plan_create→plan_advance→plan_evidence→plan_done (non-trivial only).',
|
|
36
40
|
'epic_create: title + description/goals/in_scope/out_of_scope. Link tasks via epic=E001.',
|
|
41
|
+
'delete: task_id (hard remove). epic_delete: epic_id, always cascades child tasks.',
|
|
42
|
+
'epic_archive/epic_unarchive: epic_id — hide/restore initiative without deleting history.',
|
|
37
43
|
'create/plan_create: title required; also send description,specs,in_scope,out_of_scope,acceptance_criteria',
|
|
38
44
|
'(missing → warnings, not failure). move: task_id+column. update: task_id + fields or subtasks[] full list',
|
|
39
|
-
'(no toggle). plan_evidence needs real test run: diff,test_command,stdout,stderr,exit_code — truncate logs.',
|
|
45
|
+
'(no toggle). comment_add: task_id+text, optional author. plan_evidence needs real test run: diff,test_command,stdout,stderr,exit_code — truncate logs.',
|
|
40
46
|
'Example create: {"action":"create","title":"Ship image","epic":"E001","description":"...","specs":"...",',
|
|
41
47
|
'"in_scope":["CLI"],"out_of_scope":["GUI"],"acceptance_criteria":["npm test passes"],"col":"planned"}'
|
|
42
48
|
].join(' '),
|
|
@@ -55,7 +61,10 @@ const MUST_CONTAIN = [
|
|
|
55
61
|
'plan_create',
|
|
56
62
|
'external_running',
|
|
57
63
|
'subtasks',
|
|
58
|
-
'epic_create'
|
|
64
|
+
'epic_create',
|
|
65
|
+
'epic_archive',
|
|
66
|
+
'epic_delete',
|
|
67
|
+
'comment_add'
|
|
59
68
|
];
|
|
60
69
|
|
|
61
70
|
function playbookHelpPayload() {
|
package/bin/kanban.js
CHANGED
|
@@ -145,8 +145,9 @@ async function cliMcpInit(options) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
async function cliList(colFilter, epicFilter, asJson) {
|
|
148
|
+
async function cliList(colFilter, epicFilter, asJson, listOptions = {}) {
|
|
149
149
|
await kanban.migrateEpicGroups();
|
|
150
|
+
const epics = await kanban.listEpicEntities();
|
|
150
151
|
let tasks = await kanban.allTasks();
|
|
151
152
|
|
|
152
153
|
if (colFilter) {
|
|
@@ -154,6 +155,8 @@ async function cliList(colFilter, epicFilter, asJson) {
|
|
|
154
155
|
}
|
|
155
156
|
if (epicFilter) {
|
|
156
157
|
tasks = tasks.filter((task) => kanban.taskMatchesEpicFilter(task, epicFilter));
|
|
158
|
+
} else {
|
|
159
|
+
tasks = kanban.filterTasksForList(tasks, epics, listOptions);
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
if (asJson) {
|
|
@@ -207,11 +210,14 @@ async function cliShow(taskId) {
|
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
async function cliEpicList(asJson) {
|
|
213
|
+
async function cliEpicList(asJson, listOptions = {}) {
|
|
211
214
|
await kanban.migrateEpicGroups();
|
|
212
215
|
const tasks = await kanban.allTasks();
|
|
213
216
|
const epics = await kanban.listEpicEntities();
|
|
214
|
-
const shaped =
|
|
217
|
+
const shaped = kanban.filterShapedEpics(
|
|
218
|
+
epics.map((epic) => kanban.shapeEpic(epic, tasks, { view: 'summary' })),
|
|
219
|
+
listOptions
|
|
220
|
+
);
|
|
215
221
|
|
|
216
222
|
if (asJson) {
|
|
217
223
|
console.log(JSON.stringify(shaped, null, 2));
|
|
@@ -225,8 +231,9 @@ async function cliEpicList(asJson) {
|
|
|
225
231
|
|
|
226
232
|
for (const epic of shaped) {
|
|
227
233
|
const p = epic.progress || {};
|
|
234
|
+
const flag = epic.archived ? ' [archived]' : '';
|
|
228
235
|
console.log(
|
|
229
|
-
` ${epic.id.padEnd(6)} ${epic.status.padEnd(8)} ${String(epic.title).substring(0, 40).padEnd(40)} ${p.tasks_done || 0}/${p.tasks_total || 0} tasks`
|
|
236
|
+
` ${epic.id.padEnd(6)} ${epic.status.padEnd(8)} ${String(epic.title).substring(0, 40).padEnd(40)} ${p.tasks_done || 0}/${p.tasks_total || 0} tasks${flag}`
|
|
230
237
|
);
|
|
231
238
|
}
|
|
232
239
|
}
|
|
@@ -265,6 +272,38 @@ async function cliEpicUpdate(epicId, patch) {
|
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
274
|
|
|
275
|
+
async function cliEpicArchive(epicId, archived) {
|
|
276
|
+
try {
|
|
277
|
+
const epic = archived
|
|
278
|
+
? await kanban.archiveEpic(epicId)
|
|
279
|
+
: await kanban.unarchiveEpic(epicId);
|
|
280
|
+
console.log(`✓ Epic ${epic.id} ${archived ? 'archived' : 'unarchived'}`);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(`✗ ${error.message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function cliEpicDelete(epicId) {
|
|
288
|
+
try {
|
|
289
|
+
const result = await kanban.deleteEpic(epicId);
|
|
290
|
+
console.log(`✓ Deleted epic ${result.epic_id} (+${result.deleted_task_count} tasks)`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(`✗ ${error.message}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function cliDelete(taskId) {
|
|
298
|
+
try {
|
|
299
|
+
const result = await kanban.deleteTask(taskId);
|
|
300
|
+
console.log(`✓ Deleted task ${shortId(result.task_id)}: ${result.title}`);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(`✗ ${error.message}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
268
307
|
async function cliMove(taskId, column) {
|
|
269
308
|
const success = await kanban.doMove(taskId, column);
|
|
270
309
|
if (success) {
|
|
@@ -286,8 +325,21 @@ async function cliAdd(title, column, epicGroup) {
|
|
|
286
325
|
}
|
|
287
326
|
}
|
|
288
327
|
|
|
328
|
+
function injectProjectIntoHtml(html, project) {
|
|
329
|
+
const safe = String(project)
|
|
330
|
+
.replace(/&/g, '&')
|
|
331
|
+
.replace(/</g, '<')
|
|
332
|
+
.replace(/>/g, '>')
|
|
333
|
+
.replace(/"/g, '"');
|
|
334
|
+
return html
|
|
335
|
+
.replace(/<title>[^<]*<\/title>/, `<title>${safe} · Kanban</title>`)
|
|
336
|
+
.replace(/<h1>Donna Kanban<\/h1>/, `<h1>${safe}</h1>`);
|
|
337
|
+
}
|
|
338
|
+
|
|
289
339
|
async function serveWeb(port) {
|
|
290
|
-
const
|
|
340
|
+
const htmlTemplate = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf-8');
|
|
341
|
+
const project = guiRegistry.projectLabel();
|
|
342
|
+
const html = injectProjectIntoHtml(htmlTemplate, project);
|
|
291
343
|
|
|
292
344
|
const server = http.createServer(async (req, res) => {
|
|
293
345
|
const url = new URL(req.url, `http://localhost:${port}`);
|
|
@@ -302,7 +354,13 @@ async function serveWeb(port) {
|
|
|
302
354
|
|
|
303
355
|
if (requestPath === '/api/board') {
|
|
304
356
|
await kanban.migrateEpicGroups();
|
|
305
|
-
const
|
|
357
|
+
const includeArchived = url.searchParams.get('include_archived') === 'true';
|
|
358
|
+
const epics = await kanban.listEpicEntities();
|
|
359
|
+
// GUI: show done-epic tasks; only hide archived unless toggled
|
|
360
|
+
const tasks = kanban.filterTasksForList(await kanban.allTasks(), epics, {
|
|
361
|
+
live_only: false,
|
|
362
|
+
include_archived: includeArchived
|
|
363
|
+
});
|
|
306
364
|
sendJson(res, 200, tasks);
|
|
307
365
|
return;
|
|
308
366
|
}
|
|
@@ -311,7 +369,16 @@ async function serveWeb(port) {
|
|
|
311
369
|
await kanban.migrateEpicGroups();
|
|
312
370
|
const tasks = await kanban.allTasks();
|
|
313
371
|
const epics = await kanban.listEpicEntities();
|
|
314
|
-
|
|
372
|
+
const shaped = epics.map((epic) => kanban.shapeEpic(epic, tasks, { view: 'full' }));
|
|
373
|
+
const includeArchived = url.searchParams.get('include_archived') === 'true';
|
|
374
|
+
const includeDone = url.searchParams.get('include_done') === 'true';
|
|
375
|
+
const status = url.searchParams.get('status') || undefined;
|
|
376
|
+
// GUI default: all non-archived (live + done). Agents use MCP live-only.
|
|
377
|
+
const liveOnly = url.searchParams.get('live_only') === 'true';
|
|
378
|
+
const filtered = kanban.filterShapedEpics(shaped, liveOnly
|
|
379
|
+
? { include_archived: includeArchived, include_done: includeDone, status }
|
|
380
|
+
: { live_only: false, include_archived: includeArchived, status });
|
|
381
|
+
sendJson(res, 200, filtered);
|
|
315
382
|
return;
|
|
316
383
|
}
|
|
317
384
|
|
|
@@ -370,6 +437,33 @@ async function serveWeb(port) {
|
|
|
370
437
|
return;
|
|
371
438
|
}
|
|
372
439
|
|
|
440
|
+
if (req.method === 'DELETE') {
|
|
441
|
+
const epicDeleteMatch = requestPath.match(/^\/api\/epic-entities\/([^/]+)$/);
|
|
442
|
+
if (epicDeleteMatch) {
|
|
443
|
+
const result = await kanban.deleteEpic(epicDeleteMatch[1]);
|
|
444
|
+
sendJson(res, 200, result);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const taskDeleteMatch = requestPath.match(/^\/api\/(?:epics|tasks)\/([^/]+)$/);
|
|
449
|
+
if (taskDeleteMatch) {
|
|
450
|
+
const result = await kanban.deleteTask(taskDeleteMatch[1]);
|
|
451
|
+
sendJson(res, 200, result);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (req.method === 'POST') {
|
|
457
|
+
const epicArchiveMatch = requestPath.match(/^\/api\/epic-entities\/([^/]+)\/(archive|unarchive)$/);
|
|
458
|
+
if (epicArchiveMatch) {
|
|
459
|
+
const epic = epicArchiveMatch[2] === 'archive'
|
|
460
|
+
? await kanban.archiveEpic(epicArchiveMatch[1])
|
|
461
|
+
: await kanban.unarchiveEpic(epicArchiveMatch[1]);
|
|
462
|
+
sendJson(res, 200, kanban.shapeEpic(epic, await kanban.allTasks(), { view: 'full' }));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
373
467
|
if (req.method === 'PATCH') {
|
|
374
468
|
const epicUpdateMatch = requestPath.match(/^\/api\/epic-entities\/([^/]+)$/);
|
|
375
469
|
if (epicUpdateMatch) {
|
|
@@ -504,6 +598,7 @@ async function serveWeb(port) {
|
|
|
504
598
|
});
|
|
505
599
|
|
|
506
600
|
console.log(`\x1b[1;32m→ Kanban GUI: ${portInfo.url}\x1b[0m`);
|
|
601
|
+
console.log(` Project: ${portInfo.project || project}`);
|
|
507
602
|
console.log(` Backlog: ${BACKLOG}`);
|
|
508
603
|
console.log(' Ctrl+C żeby zamknąć');
|
|
509
604
|
}
|
|
@@ -617,6 +712,7 @@ async function main() {
|
|
|
617
712
|
let colFilter = null;
|
|
618
713
|
let epicFilter = null;
|
|
619
714
|
let asJson = false;
|
|
715
|
+
let includeArchived = false;
|
|
620
716
|
|
|
621
717
|
for (let i = 1; i < args.length; i++) {
|
|
622
718
|
if (args[i] === '--col' && args[i + 1]) {
|
|
@@ -625,14 +721,31 @@ async function main() {
|
|
|
625
721
|
epicFilter = args[++i];
|
|
626
722
|
} else if (args[i] === '--json') {
|
|
627
723
|
asJson = true;
|
|
724
|
+
} else if (args[i] === '--include-archived') {
|
|
725
|
+
includeArchived = true;
|
|
628
726
|
}
|
|
629
727
|
}
|
|
630
728
|
|
|
631
|
-
await cliList(colFilter, epicFilter, asJson);
|
|
729
|
+
await cliList(colFilter, epicFilter, asJson, { include_archived: includeArchived });
|
|
632
730
|
} else if (cmd === 'epic') {
|
|
633
731
|
const sub = args[1];
|
|
634
732
|
if (sub === 'list') {
|
|
635
|
-
|
|
733
|
+
let includeArchived = false;
|
|
734
|
+
let includeDone = false;
|
|
735
|
+
let status;
|
|
736
|
+
for (let i = 2; i < args.length; i++) {
|
|
737
|
+
if (args[i] === '--include-archived') includeArchived = true;
|
|
738
|
+
else if (args[i] === '--include-done') includeDone = true;
|
|
739
|
+
else if (args[i] === '--all') {
|
|
740
|
+
includeArchived = true;
|
|
741
|
+
includeDone = true;
|
|
742
|
+
} else if (args[i] === '--status' && args[i + 1]) status = args[++i];
|
|
743
|
+
}
|
|
744
|
+
await cliEpicList(args.includes('--json'), {
|
|
745
|
+
include_archived: includeArchived,
|
|
746
|
+
include_done: includeDone,
|
|
747
|
+
status
|
|
748
|
+
});
|
|
636
749
|
} else if (sub === 'show' && args[2]) {
|
|
637
750
|
await cliEpicShow(args[2]);
|
|
638
751
|
} else if (sub === 'add' && args[2]) {
|
|
@@ -645,12 +758,20 @@ async function main() {
|
|
|
645
758
|
await cliEpicAdd(args[2], { description, goals });
|
|
646
759
|
} else if (sub === 'update' && args[2] && args[3]) {
|
|
647
760
|
await cliEpicUpdate(args[2], parseJsonPayload(args[3]));
|
|
761
|
+
} else if (sub === 'archive' && args[2]) {
|
|
762
|
+
await cliEpicArchive(args[2], true);
|
|
763
|
+
} else if (sub === 'unarchive' && args[2]) {
|
|
764
|
+
await cliEpicArchive(args[2], false);
|
|
765
|
+
} else if ((sub === 'rm' || sub === 'delete') && args[2]) {
|
|
766
|
+
await cliEpicDelete(args[2]);
|
|
648
767
|
} else {
|
|
649
|
-
console.error('Usage: kanban epic list|show|add|update ...');
|
|
768
|
+
console.error('Usage: kanban epic list|show|add|update|archive|unarchive|rm ...');
|
|
650
769
|
process.exit(1);
|
|
651
770
|
}
|
|
652
771
|
} else if (cmd === 'show' && args[1]) {
|
|
653
772
|
await cliShow(args[1]);
|
|
773
|
+
} else if ((cmd === 'rm' || cmd === 'delete') && args[1]) {
|
|
774
|
+
await cliDelete(args[1]);
|
|
654
775
|
} else if (cmd === 'move' && args[1] && args[2]) {
|
|
655
776
|
await cliMove(args[1], args[2]);
|
|
656
777
|
} else if (cmd === 'add' && args[1]) {
|
|
@@ -668,6 +789,18 @@ async function main() {
|
|
|
668
789
|
await cliAdd(args[1], column, epicGroup);
|
|
669
790
|
} else if (cmd === 'plan' && args[1] && args[2] === '--json') {
|
|
670
791
|
await cliPlan(args[1], parseJsonPayload(args[3]));
|
|
792
|
+
} else if (cmd === 'comment' && args[1] && args[2]) {
|
|
793
|
+
let author = 'user';
|
|
794
|
+
for (let i = 3; i < args.length; i++) {
|
|
795
|
+
if (args[i] === '--author' && args[i + 1]) author = args[++i];
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const added = await kanban.addComment(args[1], args[2], author);
|
|
799
|
+
console.log(JSON.stringify({ ok: true, task_id: added.task_id, comment_id: added.comment.id, count: added.comments.length }));
|
|
800
|
+
} catch (error) {
|
|
801
|
+
console.error(`✗ ${error.message}`);
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
671
804
|
} else {
|
|
672
805
|
console.error('Unknown command:', cmd);
|
|
673
806
|
process.exit(1);
|
package/gui-registry.js
CHANGED
|
@@ -21,6 +21,15 @@ function guiPortFilePath() {
|
|
|
21
21
|
return path.join(BACKLOG, GUI_PORT_FILE);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function projectLabel(cwd = process.cwd()) {
|
|
25
|
+
const fromEnv = process.env.KANBANGO_PROJECT_NAME;
|
|
26
|
+
if (fromEnv !== undefined && fromEnv !== null && String(fromEnv).trim()) {
|
|
27
|
+
return String(fromEnv).trim();
|
|
28
|
+
}
|
|
29
|
+
const base = path.basename(String(cwd || '').replace(/[/\\]+$/, ''));
|
|
30
|
+
return base || 'kanban';
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
function hashCwdToPort(cwd = process.cwd()) {
|
|
25
34
|
let hash = 0;
|
|
26
35
|
const input = String(cwd);
|
|
@@ -79,11 +88,13 @@ async function writeGuiPortFile({ port, pid = process.pid } = {}) {
|
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
await ensureBacklogDir();
|
|
91
|
+
const cwd = process.cwd();
|
|
82
92
|
const data = {
|
|
83
93
|
port: normalizedPort,
|
|
84
94
|
pid,
|
|
85
95
|
url: `http://localhost:${normalizedPort}`,
|
|
86
|
-
cwd
|
|
96
|
+
cwd,
|
|
97
|
+
project: projectLabel(cwd),
|
|
87
98
|
started_at: new Date().toISOString()
|
|
88
99
|
};
|
|
89
100
|
await fs.writeFile(guiPortFilePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
@@ -129,11 +140,13 @@ async function discoverRunningGui() {
|
|
|
129
140
|
pid: info.pid,
|
|
130
141
|
url: info.url || `http://localhost:${info.port}`,
|
|
131
142
|
cwd: info.cwd,
|
|
143
|
+
project: info.project || projectLabel(info.cwd),
|
|
132
144
|
started_at: info.started_at
|
|
133
145
|
};
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
module.exports = {
|
|
149
|
+
projectLabel,
|
|
137
150
|
hashCwdToPort,
|
|
138
151
|
normalizeGuiPort,
|
|
139
152
|
resolvePreferredGuiPort,
|