kanbango 3.3.0 → 3.5.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 CHANGED
@@ -5,3 +5,4 @@
5
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
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
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}
8
+ {"id":"260819-ztl","ts":"2026-08-19","scope":"proj","tags":["config","kanban","validation"],"rule":"DO Field-policy config: skip only ENOENT; invalid JSON/shape must throw CONFIG_INVALID, never silently fall back to def…","when":"config load / required fields","sev":2,"hits":1}
@@ -1 +1 @@
1
- {"ts":"2026-08-13T06:43:28.413Z","sessionID":"ses_038ddd435ffeginZz7W4t3X27v","added":["260813-9t0"],"bumped":[]}
1
+ {"ts":"2026-08-19T20:04:53.425Z","sessionID":"ses_feb8269efffe9L33YfwuSnVUVa","added":["260819-ztl"],"bumped":[]}
@@ -0,0 +1,25 @@
1
+ # kanbango quality policy
2
+ # CLI/GUI entrypoints use console.log as user-facing output (not debug leftovers).
3
+ version: 1
4
+ include: []
5
+ exclude:
6
+ - node_modules/**
7
+ - backlog/**
8
+ - examples/**
9
+ languages:
10
+ - javascript
11
+ - markdown
12
+ rules:
13
+ enabled: true
14
+ debug-leftover: off
15
+ swallowed-exception: error
16
+ hidden-fallback: error
17
+ unsafe-pattern: error
18
+ placeholder-todo: warning
19
+ dead-code: warning
20
+ cognitive-complexity: warning
21
+ function-length: warning
22
+ nesting-depth: warning
23
+ duplicate-blocks: warning
24
+ gate:
25
+ fail_on: error
package/AGENTS.md CHANGED
@@ -11,6 +11,8 @@ Source of truth: `agent-playbook.js` (also MCP tool descriptions + `kanban_read`
11
11
  - list_epics default = live only (hides done + archived); `include_archived` / `status` to widen
12
12
  - list tasks default hides tasks under done/archived epics; show/show_epic by id always works
13
13
  - show: `view=execution` while coding; `full` only if needed
14
+ - show task returns `epic_goals` (parent compass; `""` if none) — check drift vs goal
15
+ - log architecture decisions: `update` `adr={decision,why}` append; `show_epic` full rolls up `adrs`
14
16
  - epic_create with description/goals; create tasks with `epic=E001` (prefer id)
15
17
  - create once with `description`, `specs`, `in_scope`, `out_of_scope`, `acceptance_criteria`
16
18
  - move/update/delete: `return=none`; subtasks = full array replace (no toggle)
package/CHANGELOG.md CHANGED
@@ -7,14 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [3.3.0] - 2026-08-13
10
+ ## [3.5.0] - 2026-08-24
11
11
 
12
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`)
13
+ - GUI cockpit layout: rail of initiatives, NOW panel for `active`, columns icebox/planned/done, paper document for task and epic
14
+ - Paper shows spec, in/out scope, AC, steps; read-only `adr[]` and `evidence[]` when present
15
+ - Task `show` injects parent `epic_goals` (empty string when none) as a compass
16
+ - Task `adr[]` with append via `update adr={decision,why}`; `show_epic` full rolls up child `adrs`
17
+ - Tests: `tests/gui-cockpit.test.js`, `tests/kanban-epic-goals-adr.test.js`
18
+
19
+ ### Changed
20
+ - GUI no longer uses epic swimlanes; epics filter the board from the rail
21
+ - Active is NOW, not a fourth board column
22
+
23
+ ## [3.4.1] - 2026-08-20
15
24
 
16
25
  ### 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`
26
+ - CLI `kanban mcp` / `npx kanbango mcp` starts the MCP server (was `Unknown command: mcp` client `-32000 Connection closed`)
27
+
28
+ ## [3.4.0] - 2026-08-20
29
+
30
+ ### Added
31
+ - GUI renders fenced ` ```mermaid ` blocks in task Details and epic info panels (flowchart, sequence, gantt, pie, …)
32
+ - Local Mermaid vendor served from `node_modules` (`/vendor/mermaid.min.js`); dual-export fence parser `fenced-text.js`
33
+ - Soft-fail per diagram: broken Mermaid shows source and does not break the board
34
+ - Dependency `mermaid`; tracked `package-lock.json`
35
+ - Tests: `tests/fenced-text.test.js`, `tests/kanban-mermaid-vendor.test.js`
36
+ - In-process lock + atomic writes for create/update/delete (race-safe board mutations)
37
+ - `.ait-quality.yml` project quality policy (CLI `console.log` treated as UX output)
38
+
39
+ ### Changed
40
+ - Edit mode still shows raw mermaid fences (no live preview)
41
+
42
+ ## [3.3.0] - 2026-08-19
43
+
44
+ ### Note
45
+ - Version 3.3.0 was published on npm; this repo release for Mermaid GUI is **3.4.0**.
18
46
 
19
47
  ## [3.2.0] - 2026-07-31
20
48
 
package/README.md CHANGED
@@ -152,7 +152,7 @@ Once connected, your agent gets access to these tools:
152
152
  | Tool | What it does |
153
153
  |------|-------------|
154
154
  | `kanban_read` | List/show tasks (`view=summary` by default — cheap) |
155
- | `kanban_manage` | Create, move, update, comment_add, plan_* workflow |
155
+ | `kanban_manage` | Create, move, update, plan_* workflow |
156
156
  | `kanban_gui` | Start / status / stop (stop only kills GUI this MCP started) |
157
157
 
158
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).
@@ -169,8 +169,8 @@ Your agent stays in sync with your real board — every change is persisted as J
169
169
  | `kanban show <ID>` | Show task details |
170
170
  | `kanban add <TITLE>` | Add a new task |
171
171
  | `kanban move <ID> <COL>` | Move task |
172
+ | `kanban mcp` | Start MCP server (stdio) |
172
173
  | `kanban mcp-init` | Generate MCP config files |
173
- | `kanban comment <ID> <text> [--author NAME]` | Append a comment to a task |
174
174
  | `kanban plan <action> --json '{...}'` | Accepted-plan workflow (create/advance/evidence/done/status) |
175
175
 
176
176
  ## Web GUI
package/agent-playbook.js CHANGED
@@ -10,10 +10,12 @@ const DROP_IN_RULE = [
10
10
  '- list: col filter, view=summary; keep task_ids; no full-board re-list after writes',
11
11
  '- list_epics default = live only (hides done + archived); include_archived / status to widen',
12
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
+ '- show: view=execution while coding; full only if needed',
14
+ '- show task returns epic_goals (parent compass); empty string if no epic/goals — check drift vs goal',
15
+ '- log architecture decisions: update adr={decision,why} append; show_epic full rolls up adrs',
14
16
  '- epic_create with description/goals; create tasks with epic=E001 (prefer id)',
15
17
  '- create once with description,specs,in_scope,out_of_scope,acceptance_criteria',
16
- '- move/update/delete: return=none; subtasks=full array replace; comment_add: task_id+text',
18
+ '- move/update/delete: return=none; subtasks=full array replace',
17
19
  '- cleanup: delete (task), epic_delete (cascade tasks), epic_archive / epic_unarchive',
18
20
  '- non-trivial: plan_create → plan_advance → plan_evidence (real tests, truncated logs) → plan_done',
19
21
  '- gui: status before start; stop only owned; external_running = do not kill'
@@ -23,7 +25,9 @@ const TOOL_DESCRIPTIONS = {
23
25
  kanban_read: [
24
26
  'Read board. TOKEN RULES: list defaults to view=summary (id/title/col/progress only).',
25
27
  'Hierarchy: epic (container/context) → task (work) → subtasks (steps).',
26
- 'Always pass col when possible. Prefer show+view=execution over full (includes comments).',
28
+ 'Always pass col when possible. Prefer show+view=execution over full.',
29
+ 'show task (planning/execution/full) includes epic_goals (parent epic goals; "" if none) — use as compass; check progress toward goal.',
30
+ 'execution/full also include adr[]; show_epic full rolls up child adrs (not stored on epic file).',
27
31
  'list_epics default live only (empty|planned|active; hides done+archived).',
28
32
  'Pass include_archived=true or status=done|archived to see closed initiatives.',
29
33
  'list hides tasks under done/archived epics unless include_archived/include_done; show/show_epic by id always works.',
@@ -36,13 +40,14 @@ const TOOL_DESCRIPTIONS = {
36
40
  'Write board / plan. TOKEN RULES: one create with all planning fields beats many updates;',
37
41
  'after write use return=none (or summary). Do not dump full task unless needed.',
38
42
  '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).',
43
+ 'plan_create→plan_advance→plan_evidence→plan_done (non-trivial only).',
40
44
  'epic_create: title + description/goals/in_scope/out_of_scope. Link tasks via epic=E001.',
45
+ 'Log architecture decisions on the task: update with adr={decision,why} appends one entry (why this choice).',
41
46
  'delete: task_id (hard remove). epic_delete: epic_id, always cascades child tasks.',
42
47
  'epic_archive/epic_unarchive: epic_id — hide/restore initiative without deleting history.',
43
48
  'create/plan_create: title required; also send description,specs,in_scope,out_of_scope,acceptance_criteria',
44
49
  '(missing → warnings, not failure). move: task_id+column. update: task_id + fields or subtasks[] full list',
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.',
50
+ '(no toggle). plan_evidence needs real test run: diff,test_command,stdout,stderr,exit_code — truncate logs.',
46
51
  'Example create: {"action":"create","title":"Ship image","epic":"E001","description":"...","specs":"...",',
47
52
  '"in_scope":["CLI"],"out_of_scope":["GUI"],"acceptance_criteria":["npm test passes"],"col":"planned"}'
48
53
  ].join(' '),
@@ -64,7 +69,8 @@ const MUST_CONTAIN = [
64
69
  'epic_create',
65
70
  'epic_archive',
66
71
  'epic_delete',
67
- 'comment_add'
72
+ 'epic_goals',
73
+ 'adr'
68
74
  ];
69
75
 
70
76
  function playbookHelpPayload() {
package/bin/kanban.js CHANGED
@@ -336,6 +336,46 @@ function injectProjectIntoHtml(html, project) {
336
336
  .replace(/<h1>Donna Kanban<\/h1>/, `<h1>${safe}</h1>`);
337
337
  }
338
338
 
339
+ /** Allowlisted GUI vendor files only (no open node_modules). */
340
+ const VENDOR_FILES = {
341
+ '/vendor/fenced-text.js': {
342
+ filePath: path.join(__dirname, '..', 'fenced-text.js'),
343
+ contentType: 'application/javascript; charset=utf-8'
344
+ },
345
+ '/vendor/mermaid.min.js': {
346
+ filePath: path.join(__dirname, '..', 'node_modules', 'mermaid', 'dist', 'mermaid.min.js'),
347
+ contentType: 'application/javascript; charset=utf-8'
348
+ }
349
+ };
350
+
351
+ function sendVendorFile(res, requestPath) {
352
+ const entry = VENDOR_FILES[requestPath];
353
+ if (!entry) return false;
354
+ if (!fs.existsSync(entry.filePath)) {
355
+ sendJson(res, 404, {
356
+ error: {
357
+ code: 'VENDOR_MISSING',
358
+ message: `Vendor file missing: ${requestPath}`,
359
+ hint: 'Run npm install in the kanbango package root'
360
+ }
361
+ });
362
+ return true;
363
+ }
364
+ res.writeHead(200, {
365
+ 'Content-Type': entry.contentType,
366
+ 'Cache-Control': 'public, max-age=3600'
367
+ });
368
+ fs.createReadStream(entry.filePath).pipe(res);
369
+ return true;
370
+ }
371
+
372
+ function logGuiPortCleanupError(err) {
373
+ // Port file may already be gone on shutdown — do not block exit.
374
+ if (err && err.code !== 'ENOENT') {
375
+ console.error('gui port cleanup:', err.message || err);
376
+ }
377
+ }
378
+
339
379
  async function serveWeb(port) {
340
380
  const htmlTemplate = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf-8');
341
381
  const project = guiRegistry.projectLabel();
@@ -352,6 +392,10 @@ async function serveWeb(port) {
352
392
  return;
353
393
  }
354
394
 
395
+ if (req.method === 'GET' && sendVendorFile(res, requestPath)) {
396
+ return;
397
+ }
398
+
355
399
  if (requestPath === '/api/board') {
356
400
  await kanban.migrateEpicGroups();
357
401
  const includeArchived = url.searchParams.get('include_archived') === 'true';
@@ -579,8 +623,8 @@ async function serveWeb(port) {
579
623
  async function cleanupGuiPortFile() {
580
624
  try {
581
625
  await guiRegistry.clearGuiPortFile({ pid: process.pid });
582
- } catch {
583
- // best-effort cleanup
626
+ } catch (err) {
627
+ logGuiPortCleanupError(err);
584
628
  }
585
629
  }
586
630
 
@@ -705,6 +749,9 @@ async function main() {
705
749
  }
706
750
 
707
751
  await cliMcpInit({ useNpx, onlyClaude, onlyOpenCode, force });
752
+ } else if (cmd === 'mcp') {
753
+ const mcpServer = require('../mcp-server.js');
754
+ await mcpServer.main();
708
755
  } else if (cmd === 'migrate') {
709
756
  const dryRun = args.includes('--dry-run');
710
757
  await cliMigrate(dryRun);
@@ -789,18 +836,6 @@ async function main() {
789
836
  await cliAdd(args[1], column, epicGroup);
790
837
  } else if (cmd === 'plan' && args[1] && args[2] === '--json') {
791
838
  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
- }
804
839
  } else {
805
840
  console.error('Unknown command:', cmd);
806
841
  process.exit(1);
package/fenced-text.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Split plain text into text / mermaid fence segments.
3
+ * Dual export: CommonJS (Node tests) + browser global FencedText.
4
+ */
5
+ (function (root, factory) {
6
+ if (typeof module === 'object' && module.exports) {
7
+ module.exports = factory();
8
+ } else {
9
+ root.FencedText = factory();
10
+ }
11
+ })(typeof self !== 'undefined' ? self : this, function () {
12
+ 'use strict';
13
+
14
+ // ```mermaid ... ``` (language case-insensitive). CRLF ok. Other fences stay plain text.
15
+ var FENCE_RE = /^```([^\r\n`]*)\r?\n([\s\S]*?)^```[ \t]*\r?$/gm;
16
+
17
+ function parseFencedText(text) {
18
+ var source = text == null ? '' : String(text);
19
+ if (!source) {
20
+ return [];
21
+ }
22
+
23
+ var segments = [];
24
+ var lastIndex = 0;
25
+ var match;
26
+ FENCE_RE.lastIndex = 0;
27
+
28
+ while ((match = FENCE_RE.exec(source)) !== null) {
29
+ if (match.index > lastIndex) {
30
+ segments.push({ type: 'text', value: source.slice(lastIndex, match.index) });
31
+ }
32
+
33
+ var lang = String(match[1] || '').trim().toLowerCase();
34
+ // Strip optional info string after language (e.g. mermaid title="x")
35
+ if (lang.indexOf(' ') !== -1) {
36
+ lang = lang.split(/\s+/)[0];
37
+ }
38
+ var body = match[2];
39
+ if (lang === 'mermaid') {
40
+ segments.push({ type: 'mermaid', value: body.replace(/\s+$/, '') });
41
+ } else {
42
+ segments.push({ type: 'text', value: match[0] });
43
+ }
44
+
45
+ lastIndex = match.index + match[0].length;
46
+ }
47
+
48
+ if (lastIndex < source.length) {
49
+ segments.push({ type: 'text', value: source.slice(lastIndex) });
50
+ }
51
+
52
+ if (segments.length === 0) {
53
+ segments.push({ type: 'text', value: source });
54
+ }
55
+
56
+ return segments;
57
+ }
58
+
59
+ return {
60
+ parseFencedText: parseFencedText
61
+ };
62
+ });