claude-code-kanban 5.1.1 → 5.2.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.
Files changed (42) hide show
  1. package/README.md +3 -2
  2. package/lib/auto-compact.js +42 -0
  3. package/lib/claude-settings.js +21 -0
  4. package/lib/dispatch.js +11 -4
  5. package/lib/retention.js +137 -0
  6. package/lib/session-events.js +30 -7
  7. package/lib/terminal.js +23 -3
  8. package/package.json +1 -1
  9. package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
  10. package/plugin/plugins/claude-code-kanban/hooks/hooks.json +14 -7
  11. package/plugin/plugins/claude-code-kanban/monitors.json +3 -3
  12. package/plugin/plugins/claude-code-kanban/scripts/postman.js +2 -2
  13. package/plugin/plugins/claude-code-kanban/skills/{kanban-dispatch → dispatch}/SKILL.md +2 -2
  14. package/plugin/plugins/claude-code-kanban/skills/{kanban-follow → follow}/SKILL.md +17 -9
  15. package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +1 -1
  16. package/public/app.js +699 -138
  17. package/public/fonts/OFL-IBM-Plex.txt +93 -0
  18. package/public/fonts/OFL-Playfair-Display.txt +93 -0
  19. package/public/fonts/fonts.css +244 -0
  20. package/public/fonts/ibm-plex-mono-cyrillic-400.woff2 +0 -0
  21. package/public/fonts/ibm-plex-mono-cyrillic-500.woff2 +0 -0
  22. package/public/fonts/ibm-plex-mono-cyrillic-600.woff2 +0 -0
  23. package/public/fonts/ibm-plex-mono-cyrillic-ext-400.woff2 +0 -0
  24. package/public/fonts/ibm-plex-mono-cyrillic-ext-500.woff2 +0 -0
  25. package/public/fonts/ibm-plex-mono-cyrillic-ext-600.woff2 +0 -0
  26. package/public/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
  27. package/public/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
  28. package/public/fonts/ibm-plex-mono-latin-600.woff2 +0 -0
  29. package/public/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
  30. package/public/fonts/ibm-plex-mono-latin-ext-500.woff2 +0 -0
  31. package/public/fonts/ibm-plex-mono-latin-ext-600.woff2 +0 -0
  32. package/public/fonts/ibm-plex-mono-vietnamese-400.woff2 +0 -0
  33. package/public/fonts/ibm-plex-mono-vietnamese-500.woff2 +0 -0
  34. package/public/fonts/ibm-plex-mono-vietnamese-600.woff2 +0 -0
  35. package/public/fonts/playfair-display-cyrillic-400.woff2 +0 -0
  36. package/public/fonts/playfair-display-latin-400.woff2 +0 -0
  37. package/public/fonts/playfair-display-latin-ext-400.woff2 +0 -0
  38. package/public/fonts/playfair-display-vietnamese-400.woff2 +0 -0
  39. package/public/index.html +20 -14
  40. package/public/style.css +277 -44
  41. package/server.js +152 -15
  42. package/skill-guides/dispatch.md +1 -1
package/README.md CHANGED
@@ -71,8 +71,9 @@ Run `claude` in any project. You do not configure anything per project. Claude C
71
71
  - **Answer prompts from the board.** When Claude asks for permission, asks a question or waits for plan approval, the session gets an amber highlight and the ask shows with Allow and Deny buttons or an answer form. The terminal prompt stays open, and the first answer wins. [Answer prompts from the board](https://nikiforovall.blog/claude-code-kanban/guides/waiting-prompts/)
72
72
  - **Embedded terminal.** Run a real Claude Code process for any session next to its board (<kbd>Ctrl</kbd>+<kbd>&#96;</kbd>). <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>R</kbd> resumes a past session and <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd> swaps to the previous one. The terminal is off by default when the board runs alone. Start it with `--enable-terminal` and open the `#t=<token>` link the server prints. [Embedded terminal](https://nikiforovall.blog/claude-code-kanban/guides/embedded-terminal/)
73
73
  - **New session.** <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>N</kbd> opens a dialog to pick a folder, a name, a model, an optional git worktree and a first prompt. Needs the terminal.
74
- - **Dispatch.** Hand a written task to a new session with `claude-code-kanban dispatch start`, or ask Claude to do it with the `kanban-dispatch` skill. Add `--report` to get the outcome back. Needs the terminal. [Dispatch tasks to other sessions](https://nikiforovall.blog/claude-code-kanban/guides/dispatch/)
75
- - **Steer with card moves.** Run `/claude-code-kanban:kanban-follow` in a session, then drag its cards. Claude starts, parks or stops the task. [Claude Code plugin skills](https://nikiforovall.blog/claude-code-kanban/guides/plugin-skills/)
74
+ - **Dispatch.** Hand a written task to a new session with `claude-code-kanban dispatch start`, or ask Claude to do it with the `dispatch` skill. Add `--report` to get the outcome back. Needs the terminal. [Dispatch tasks to other sessions](https://nikiforovall.blog/claude-code-kanban/guides/dispatch/)
75
+ - **Steer with card moves.** Run `/claude-code-kanban:follow` in a session, then drag its cards. Claude starts, parks or stops the task. [Claude Code plugin skills](https://nikiforovall.blog/claude-code-kanban/guides/plugin-skills/)
76
+ - **Review comments.** Select text in a previewed file or the plan, add comments and send them to the session in one step. [Review comments](https://nikiforovall.blog/claude-code-kanban/guides/review-comments/)
76
77
 
77
78
  <picture>
78
79
  <source media="(prefers-color-scheme: dark)" srcset="website/public/shots/themes/ember-11-waiting-prompt-dark.webp">
@@ -0,0 +1,42 @@
1
+ const path = require('node:path');
2
+ const { readSettings } = require('./claude-settings');
3
+
4
+ const MIN_WINDOW = 100000;
5
+ const MAX_WINDOW = 1000000;
6
+
7
+ function toInt(v) {
8
+ const n = typeof v === 'number' ? v : typeof v === 'string' && /^\d+$/.test(v.trim()) ? Number(v) : NaN;
9
+ return Number.isFinite(n) ? n : null;
10
+ }
11
+
12
+ // Claude Code merges settings as project local > project > user, and an env var beats the
13
+ // `autoCompactWindow` key. Shell env and managed settings are not visible from here.
14
+ // https://code.claude.com/docs/en/env-vars.md, https://code.claude.com/docs/en/settings-reference.md
15
+ function getAutoCompact(claudeDir, projectPath) {
16
+ const files = [path.join(claudeDir, 'settings.json')];
17
+ if (projectPath) {
18
+ files.push(path.join(projectPath, '.claude', 'settings.json'), path.join(projectPath, '.claude', 'settings.local.json'));
19
+ }
20
+ let enabled = true;
21
+ let envWindow = null;
22
+ let keyWindow = null;
23
+ let pct = null;
24
+ for (const file of files) {
25
+ const s = readSettings(file);
26
+ if (!s || typeof s !== 'object') continue;
27
+ if (typeof s.autoCompactEnabled === 'boolean') enabled = s.autoCompactEnabled;
28
+ keyWindow = toInt(s.autoCompactWindow) ?? keyWindow;
29
+ envWindow = toInt(s.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW) ?? envWindow;
30
+ pct = toInt(s.env?.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE) ?? pct;
31
+ }
32
+ if (!enabled) return null;
33
+ const window = envWindow ?? keyWindow;
34
+ const validPct = pct != null && pct >= 1 && pct <= 100 ? pct : null;
35
+ if (window == null && validPct == null) return null;
36
+ return {
37
+ window: window == null ? null : Math.min(MAX_WINDOW, Math.max(MIN_WINDOW, window)),
38
+ pct: validPct,
39
+ };
40
+ }
41
+
42
+ module.exports = { getAutoCompact };
@@ -0,0 +1,21 @@
1
+ const fs = require('node:fs');
2
+
3
+ const fileCache = new Map();
4
+
5
+ // Parsed Claude Code settings file, or null when it is missing or invalid. Re-read only
6
+ // after its mtime changes.
7
+ function readSettings(file) {
8
+ const mtime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null;
9
+ const hit = fileCache.get(file);
10
+ if (hit && hit.mtime === mtime) return hit.data;
11
+ let data = null;
12
+ if (mtime !== null) {
13
+ try {
14
+ data = JSON.parse(fs.readFileSync(file, 'utf8'));
15
+ } catch {}
16
+ }
17
+ fileCache.set(file, { mtime, data });
18
+ return data;
19
+ }
20
+
21
+ module.exports = { readSettings };
package/lib/dispatch.js CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  const crypto = require('node:crypto');
10
10
  const { tokenMatches } = require('./terminal');
11
- const { clampWait } = require('./session-events');
11
+ const { clampWait, EVENT_PREFIX } = require('./session-events');
12
12
 
13
13
  const OUTCOMES = new Set(['succeeded', 'failed']);
14
14
  const MAX_SUMMARY = 4000;
@@ -45,9 +45,16 @@ function formatPreamble(record, cli = 'claude-code-kanban') {
45
45
 
46
46
  // The line reaches the parent through the postman at hook trust level; the summary is
47
47
  // last so no summary text can pose as a further field.
48
+ const DISPATCH_OUTCOME = {
49
+ succeeded: 'reported success',
50
+ failed: 'reported failure',
51
+ exited: 'ended without a report',
52
+ };
53
+
48
54
  function formatDispatchLine(r) {
49
- const head = `cck:1 dispatch.${r.status} ${r.id} session=${r.session}`;
50
- return r.summary ? `${head} summary=${r.summary}` : head;
55
+ const outcome = DISPATCH_OUTCOME[r.status] || r.status;
56
+ const head = `${EVENT_PREFIX} Dispatch ${r.id} (session ${r.session}) ${outcome}.`;
57
+ return r.summary ? `${head} Summary: ${r.summary}` : head;
51
58
  }
52
59
 
53
60
  function publicView(r) {
@@ -169,4 +176,4 @@ function createDispatchRegistry({ onSettle, now = Date.now } = {}) {
169
176
  return { create, attach, discard, settle, sessionExited, list, wait };
170
177
  }
171
178
 
172
- module.exports = { createDispatchRegistry, formatPreamble, formatDispatchLine, isPeerName };
179
+ module.exports = { createDispatchRegistry, formatPreamble, formatDispatchLine, isPeerName, DISPATCH_OUTCOME };
@@ -0,0 +1,137 @@
1
+ // Expiry of per-session state cck writes. The rule and the sweep: docs/retention.md.
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { readSettings } = require('./claude-settings');
6
+ const { DISPATCH_OUTCOME } = require('./dispatch');
7
+
8
+ const DAY_MS = 24 * 60 * 60 * 1000;
9
+ const DEFAULT_CLEANUP_DAYS = 30;
10
+ // A started session writes its transcript a moment after the record; the sweep must not
11
+ // read that gap as a deleted transcript.
12
+ const GRACE_MS = 60 * 60 * 1000;
13
+ const MAX_DISPATCHED = 500;
14
+ const OUTCOMES = new Set(Object.keys(DISPATCH_OUTCOME));
15
+
16
+ // Only the config dir's settings: retention is per config dir, not per project.
17
+ function retentionMs(claudeDir) {
18
+ const days = readSettings(path.join(claudeDir, 'settings.json'))?.cleanupPeriodDays;
19
+ return (Number.isInteger(days) && days > 0 ? days : DEFAULT_CLEANUP_DAYS) * DAY_MS;
20
+ }
21
+
22
+ // `known` is the set of session ids with a transcript, or null when the scan found none,
23
+ // so a failed scan never reads as every transcript gone.
24
+ function isExpired(id, at, { known, maxAgeMs, now }) {
25
+ const age = now - at;
26
+ if (age < GRACE_MS) return false;
27
+ return (known && !known.has(id)) || age > maxAgeMs;
28
+ }
29
+
30
+ /**
31
+ * Sessions started through `dispatch start`, kept after the dispatch settles so the card
32
+ * keeps its marker.
33
+ * @param {object} o
34
+ * @param {() => object|null} o.load returns `{version: 1, sessions: {[id]: {parent, status, at}}}` or null
35
+ * @param {(data: object) => void} o.save
36
+ */
37
+ function createDispatchedStore({ load, save, now = Date.now }) {
38
+ // Insertion order is oldest first, so the cap drops the first key.
39
+ const entries = new Map();
40
+ const saved = load()?.sessions;
41
+ if (saved && typeof saved === 'object') {
42
+ for (const [id, e] of Object.entries(saved)) {
43
+ if (!e || !Number.isFinite(e.at)) continue;
44
+ // The dispatch registry is in memory: a dispatch that was running when the server
45
+ // stopped can never settle, so it reads as ended.
46
+ const status = OUTCOMES.has(e.status) ? e.status : 'exited';
47
+ entries.set(id, { parent: typeof e.parent === 'string' ? e.parent : null, status, at: e.at });
48
+ }
49
+ }
50
+
51
+ const persist = () => save({ version: 1, sessions: Object.fromEntries(entries) });
52
+
53
+ function record(id, parent) {
54
+ entries.delete(id);
55
+ entries.set(id, { parent: parent || null, status: 'running', at: now() });
56
+ if (entries.size > MAX_DISPATCHED) entries.delete(entries.keys().next().value);
57
+ persist();
58
+ }
59
+
60
+ function settle(id, status) {
61
+ const e = entries.get(id);
62
+ if (!e || e.status !== 'running' || !OUTCOMES.has(status)) return;
63
+ e.status = status;
64
+ persist();
65
+ }
66
+
67
+ // Returns how many entries went; writes only when one did.
68
+ function prune({ known, maxAgeMs }) {
69
+ const t = now();
70
+ let removed = 0;
71
+ for (const [id, e] of entries) {
72
+ if (isExpired(id, e.at, { known, maxAgeMs, now: t })) {
73
+ entries.delete(id);
74
+ removed++;
75
+ }
76
+ }
77
+ if (removed) persist();
78
+ return removed;
79
+ }
80
+
81
+ return { record, settle, prune, get: (id) => entries.get(id) || null };
82
+ }
83
+
84
+ // Names only, no stat or parse: the sweep needs which transcripts exist, not what they hold.
85
+ // Null when none is found, so a missing or unreadable projects dir never reads as every
86
+ // transcript gone.
87
+ async function listTranscriptIds(projectsDir) {
88
+ const ids = new Set();
89
+ let dirs;
90
+ try {
91
+ dirs = await fs.readdir(projectsDir, { withFileTypes: true });
92
+ } catch {
93
+ return null;
94
+ }
95
+ for (const d of dirs) {
96
+ if (!d.isDirectory()) continue;
97
+ try {
98
+ for (const f of await fs.readdir(path.join(projectsDir, d.name))) {
99
+ if (f.endsWith('.jsonl')) ids.add(f.slice(0, -'.jsonl'.length));
100
+ }
101
+ } catch {}
102
+ }
103
+ return ids.size ? ids : null;
104
+ }
105
+
106
+ // `<dir>/<session id>/<ts>.md`, one folder per session.
107
+ async function pruneSessionDirs(dir, { known, maxAgeMs, now = Date.now() }) {
108
+ let removed = 0;
109
+ let sessionDirs;
110
+ try {
111
+ sessionDirs = await fs.readdir(dir, { withFileTypes: true });
112
+ } catch {
113
+ return 0;
114
+ }
115
+ for (const d of sessionDirs) {
116
+ if (!d.isDirectory()) continue;
117
+ const sessionDir = path.join(dir, d.name);
118
+ try {
119
+ const files = await fs.readdir(sessionDir);
120
+ let kept = 0;
121
+ for (const f of files) {
122
+ const file = path.join(sessionDir, f);
123
+ const { mtimeMs } = await fs.stat(file);
124
+ if (isExpired(d.name, mtimeMs, { known, maxAgeMs, now })) {
125
+ await fs.rm(file, { force: true });
126
+ removed++;
127
+ } else kept++;
128
+ }
129
+ // An old mtime means no file was added since the read, so a review written right
130
+ // now cannot lose its folder.
131
+ if (!kept && now - (await fs.stat(sessionDir)).mtimeMs >= GRACE_MS) await fs.rmdir(sessionDir);
132
+ } catch {}
133
+ }
134
+ return removed;
135
+ }
136
+
137
+ module.exports = { createDispatchedStore, listTranscriptIds, pruneSessionDirs, retentionMs, GRACE_MS, MAX_DISPATCHED, DAY_MS };
@@ -29,13 +29,33 @@ function sanitizeEventLine(line) {
29
29
  return line.replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 1500);
30
30
  }
31
31
 
32
- // Everything after `description=` is the description verbatim to end of line, so no amount
33
- // of board text can pose as a further field. That leaves the subject as the only value
34
- // that needs delimiting.
32
+ // Lines are plain sentences so they still read right once the skill that explains them has
33
+ // left the agent's context. EVENT_PREFIX marks the board as the sender.
34
+ const EVENT_PREFIX = '[kanban board]';
35
+
36
+ // Everything after `Description:` is the description verbatim to end of line, so no amount
37
+ // of board text can pose as a further part of the line. That leaves the subject as the only
38
+ // value that needs delimiting.
35
39
  function formatTaskMoved(taskId, prevStatus, task) {
36
40
  const subject = String(task.subject || '').replace(/(["\\])/g, '\\$1');
37
- const head = `cck:1 task.moved ${taskId} ${prevStatus || 'none'}>${task.status} subject="${subject}"`;
38
- return task.description ? `${head} description=${task.description}` : head;
41
+ const from = prevStatus ? ` from ${prevStatus}` : '';
42
+ const head = `${EVENT_PREFIX} The user moved task ${taskId} "${subject}"${from} to ${task.status}.`;
43
+ return task.description ? `${head} Description: ${task.description}` : head;
44
+ }
45
+
46
+ // The comments themselves live in the review file; the line only points at it, because a
47
+ // batch of quotes would not survive the one-line cap. The path comes last and runs to end
48
+ // of line, so a space in the config dir cannot split it.
49
+ function formatReviewSubmitted(count, label, reviewFile) {
50
+ const comments = `${count} review comment${count === 1 ? '' : 's'}`;
51
+ return `${EVENT_PREFIX} The user left ${comments} on ${label}. Address them: ${reviewFile}`;
52
+ }
53
+
54
+ // A postman between two polls has no waiter for a moment, so this can miss a live one.
55
+ // The caller then falls back to another route, which only costs a duplicate route, never
56
+ // a lost review.
57
+ function hasSessionListener(sessionId) {
58
+ return !!sessionEventBuckets.get(sessionId)?.waiters.size;
39
59
  }
40
60
 
41
61
  function enqueueSessionEvent(sessionId, line) {
@@ -59,8 +79,8 @@ function clampWait(sec) {
59
79
  return Math.min(Math.max(Number(sec) || 0, 0), MAX_WAIT_SEC);
60
80
  }
61
81
 
62
- // Each topic rides its own bucket, so the kanban-dispatch postman never prints task moves
63
- // the user did not grant with kanban-follow. No topic is the task-move bucket.
82
+ // Each topic rides its own bucket, so the dispatch postman never prints task moves
83
+ // the user did not grant with the follow skill. No topic is the task-move bucket.
64
84
  function topicKey(topic, sessionId) {
65
85
  return topic ? `${topic}:${sessionId}` : sessionId;
66
86
  }
@@ -109,9 +129,12 @@ function drain(sessionId, bucket) {
109
129
  }
110
130
 
111
131
  module.exports = {
132
+ EVENT_PREFIX,
112
133
  sessionEventBuckets,
113
134
  sanitizeEventLine,
114
135
  formatTaskMoved,
136
+ formatReviewSubmitted,
137
+ hasSessionListener,
115
138
  enqueueSessionEvent,
116
139
  handleSessionEvents,
117
140
  topicKey,
package/lib/terminal.js CHANGED
@@ -356,16 +356,24 @@ function createTerminalService(o) {
356
356
  // Waits for claude to turn on bracketed paste (its input box is live), then for the
357
357
  // screen to settle. A folder-trust question also takes input, and Enter there would
358
358
  // answer it, so the prompt waits for as long as that question is on screen.
359
+ function bracketed(text) {
360
+ return `\x1b[200~${text.replace(/\r?\n/g, '\r')}\x1b[201~`;
361
+ }
362
+
363
+ function onTrustScreen(s) {
364
+ return /\btrust\b/i.test(screenText(s.term));
365
+ }
366
+
359
367
  function queuePrompt(s, prompt) {
360
368
  let armed = false;
361
369
  let timer = null;
362
- const text = `\x1b[200~${prompt.replace(/\r?\n/g, '\r')}\x1b[201~`;
370
+ const text = bracketed(prompt);
363
371
  s.onOutput = (data) => {
364
372
  if (!armed && data.includes('\x1b[?2004h')) armed = true;
365
373
  if (!armed) return;
366
374
  clearTimeout(timer);
367
375
  timer = setTimeout(() => {
368
- if (s.exited || /\btrust\b/i.test(screenText(s.term))) return;
376
+ if (s.exited || onTrustScreen(s)) return;
369
377
  s.onOutput = null;
370
378
  s.pty.write(text);
371
379
  setTimeout(() => { if (!s.exited) s.pty.write('\r'); }, 100);
@@ -654,6 +662,18 @@ function createTerminalService(o) {
654
662
  return r.error ? r : { id: r.session.id, cwd: r.session.cwd };
655
663
  }
656
664
 
665
+ // Pastes into claude's input box and stops there: Enter would also submit whatever the
666
+ // user had half-typed, and over a permission dialog it would answer it. A fork runs under
667
+ // a new session id, so its terminal id does not name the session the text is for. A
668
+ // pending queued prompt owns the input box until it is sent.
669
+ function paste(id, text) {
670
+ const s = sessions.get(id);
671
+ if (!s || s.ended || s.exited || s.onOutput || s.mode === 'shell' || s.mode === 'fork') return false;
672
+ if (onTrustScreen(s)) return false;
673
+ s.pty.write(bracketed(text));
674
+ return true;
675
+ }
676
+
657
677
  function shutdown() {
658
678
  shuttingDown = true;
659
679
  clearTimeout(saveTimer);
@@ -663,7 +683,7 @@ function createTerminalService(o) {
663
683
  sessions.clear();
664
684
  }
665
685
 
666
- return { token, handleUpgrade, clientConfig, list, isRunning, end, authorized, startNew, restore, shutdown, unavailableReason };
686
+ return { token, handleUpgrade, clientConfig, list, isRunning, end, authorized, startNew, paste, restore, shutdown, unavailableReason };
667
687
  }
668
688
 
669
689
  module.exports = { createTerminalService, readTerminalConfig, ptyEnv, shellArgs, resolveShell, claudeArgsFor, parseNewSpec, findPickProcess, tokenMatches };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
5
5
  "main": "server.js",
6
6
  "type": "commonjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.16.1",
3
+ "version": "2.17.0",
4
4
  "description": "claude-code-kanban dashboard integration: agent activity tracking, context statusline, skills to drive the board from a session and to follow it",
5
5
  "experimental": {
6
6
  "monitors": "./monitors.json"
@@ -6,7 +6,8 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
9
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
10
+ "shell": "bash",
10
11
  "timeout": 5
11
12
  }
12
13
  ]
@@ -18,7 +19,8 @@
18
19
  "hooks": [
19
20
  {
20
21
  "type": "command",
21
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
22
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
23
+ "shell": "bash",
22
24
  "timeout": 5
23
25
  }
24
26
  ]
@@ -30,7 +32,8 @@
30
32
  "hooks": [
31
33
  {
32
34
  "type": "command",
33
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
35
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
36
+ "shell": "bash",
34
37
  "timeout": 5
35
38
  }
36
39
  ]
@@ -42,7 +45,8 @@
42
45
  "hooks": [
43
46
  {
44
47
  "type": "command",
45
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
48
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
49
+ "shell": "bash",
46
50
  "timeout": 5
47
51
  }
48
52
  ]
@@ -54,7 +58,8 @@
54
58
  "hooks": [
55
59
  {
56
60
  "type": "command",
57
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
61
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
62
+ "shell": "bash",
58
63
  "timeout": 5
59
64
  }
60
65
  ]
@@ -66,7 +71,8 @@
66
71
  "hooks": [
67
72
  {
68
73
  "type": "command",
69
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/approval-gate.sh",
74
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/approval-gate.sh\"",
75
+ "shell": "bash",
70
76
  "timeout": 86400
71
77
  }
72
78
  ]
@@ -78,7 +84,8 @@
78
84
  "hooks": [
79
85
  {
80
86
  "type": "command",
81
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
87
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
88
+ "shell": "bash",
82
89
  "timeout": 5
83
90
  }
84
91
  ]
@@ -1,14 +1,14 @@
1
1
  [
2
2
  {
3
3
  "name": "kanban-doorbell",
4
- "description": "Notifies this session when its tasks are moved on the kanban board.",
4
+ "description": "Notifies this session when its tasks are moved on the kanban board or the user sends it review comments.",
5
5
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/postman.js\"",
6
- "when": "on-skill-invoke:claude-code-kanban:kanban-follow"
6
+ "when": "on-skill-invoke:claude-code-kanban:follow"
7
7
  },
8
8
  {
9
9
  "name": "kanban-dispatch-inbox",
10
10
  "description": "Notifies this session when a session it dispatched through cck reports or exits.",
11
11
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/postman.js\" --topic dispatch --keep-backlog",
12
- "when": "on-skill-invoke:claude-code-kanban:kanban-dispatch"
12
+ "when": "on-skill-invoke:claude-code-kanban:dispatch"
13
13
  }
14
14
  ]
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // The lines we print carry board text (the card subject and description), which the
13
13
  // session is told to read as the user's own brief. That is only safe because we are armed
14
- // by an explicit `kanban-follow` invocation: the user asked to follow the board before anything the
14
+ // by an explicit `follow` invocation: the user asked to follow the board before anything the
15
15
  // board says can reach the model.
16
16
 
17
17
  const fs = require('fs');
@@ -29,7 +29,7 @@ const RETRY_MS = 15000;
29
29
  // Windows sometimes fails a loopback connect with ETIMEDOUT while the board is up, so that
30
30
  // error gets a few short waits before the normal one.
31
31
  const CONNECT_RETRY_MS = [250, 500, 1000, 2000];
32
- // `--topic dispatch` is the kanban-dispatch inbox: reports from sessions this one started.
32
+ // `--topic dispatch` is the dispatch inbox: reports from sessions this one started.
33
33
  const TOPIC = process.argv.includes('--topic') ? process.argv[process.argv.indexOf('--topic') + 1] : null;
34
34
  // A dispatch report is a result, not an instruction, so a late attach still wants it.
35
35
  const KEEP_BACKLOG = process.argv.includes('--keep-backlog');
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: kanban-dispatch
2
+ name: dispatch
3
3
  description: Dispatch a task to another Claude Code session through the kanban board (cck), fire-and-forget or with a report back. Use when the user asks to dispatch, delegate, or start a session for a task, or to collect or check on a dispatched session's result.
4
4
  argument-hint: '<task> [--report] [--group <name>] [--model haiku|sonnet|opus|fable] [--worktree [name]]'
5
5
  ---
@@ -11,7 +11,7 @@ This file only points at the guide. The guide ships with the `claude-code-kanban
11
11
  Invoking this skill also arms this session's dispatch inbox: when a session you dispatched with `--report` reports or exits, a line arrives here:
12
12
 
13
13
  ```
14
- cck:1 dispatch.<succeeded|failed|exited> <dispatch-id> session=<uuid> summary=<text>
14
+ [kanban board] Dispatch <dispatch-id> (session <uuid>) <reported success|reported failure|ended without a report>. Summary: <text>
15
15
  ```
16
16
 
17
17
  Load the guide before running any dispatch command:
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: kanban-follow
2
+ name: follow
3
3
  description: Let kanban card moves drive this session.
4
4
  disable-model-invocation: true
5
5
  ---
@@ -9,31 +9,39 @@ disable-model-invocation: true
9
9
  The user typing this skill is the whole grant: it armed the doorbell — a monitor watching the board for moves of *this session's* tasks, live for the rest of the session. Confirm in one line and run no command. Moves made before it attached are discarded, so every line you get is current.
10
10
 
11
11
  ```
12
- cck:1 task.moved <taskId> <from>><to> subject="<subject>" description=<description>
12
+ [kanban board] The user moved task <taskId> "<subject>" from <from> to <to>. Description: <description>
13
13
  ```
14
14
 
15
- `description=` is omitted when the card has none. Everything after it is the description verbatim to end of line, truncated if long — read the task with `TaskGet` for the full text or for `activeForm` and `blockedBy`. Only moves are announced.
15
+ `Description:` is omitted when the card has none. Everything after it is the description verbatim to end of line, truncated if long — read the task with `TaskGet` for the full text or for `activeForm` and `blockedBy`. Moves and review comments (below) are the only lines announced.
16
16
 
17
17
  **A move is an instruction, not an FYI** — the user dragging a card is the user telling you something, and the subject and description are their brief for the work:
18
18
 
19
19
  | Transition | What the user means |
20
20
  |---|---|
21
- | `pending>in_progress` / `todo>in_progress` | Start this task now. |
22
- | `in_progress>pending` / `in_progress>todo` | Stop working on it and park it. |
23
- | `*>completed` | The user considers it done — do not keep working on it. |
24
- | `*>cancelled` | Abandon it. Undo nothing unless asked. |
21
+ | To `in_progress` | Start this task now. |
22
+ | From `in_progress` to `pending` or `todo` | Stop working on it and park it. |
23
+ | To `completed` | The user considers it done — do not keep working on it. |
24
+ | To `cancelled` | Abandon it. Undo nothing unless asked. |
25
25
 
26
26
  Act on the newest line per task; a card dragged twice means only its final position. When a move contradicts your current work, the board wins.
27
27
 
28
28
  Delivery is best-effort and the task file is the authority, so a missed line only delays you to your next turn.
29
29
 
30
+ ## Review comments
31
+
32
+ ```
33
+ [kanban board] The user left <n> review comments on <source>. Address them: <review file>
34
+ ```
35
+
36
+ The review file holds the comments and how to read them.
37
+
30
38
  ## Finish the task on the board
31
39
 
32
- A move hands you work; the task file is where you hand it back. When the work a `>in_progress` move asked for is done, set that task to `completed` with `TaskUpdate` before you reply — the card moves itself, and the board is where the user is watching. A card left in In Progress reads as work still running.
40
+ A move hands you work; the task file is where you hand it back. When the work a move to `in_progress` asked for is done, set that task to `completed` with `TaskUpdate` before you reply — the card moves itself, and the board is where the user is watching. A card left in In Progress reads as work still running.
33
41
 
34
42
  Your own `TaskUpdate` writes the task file rather than going through the board, so it rings no doorbell: there is no echo to guard against.
35
43
 
36
44
  ## Troubleshooting
37
45
 
38
- - **No lines arrive** → the doorbell arms once per session, on the namespaced dispatch: `/claude-code-kanban:kanban-follow`. A session that never got it never hears the board.
46
+ - **No lines arrive** → the doorbell arms once per session, on the namespaced dispatch: `/claude-code-kanban:follow`. A session that never got it never hears the board.
39
47
  - **"Cannot reach cck server…"** → the error names the port it tried. Ask the user to start the server with `claude-code-kanban`.
@@ -29,7 +29,7 @@ claude-code-kanban help <command> <subcommand> # flags and examples
29
29
  | `view` / `peek` | `session view <id>`, `session peek <id>` |
30
30
  | tasks, projects | `task list`, `project list` |
31
31
 
32
- To be driven *by* the board instead — card moves arriving as instructions — the user types `/claude-code-kanban:kanban-follow`.
32
+ To be driven *by* the board instead — card moves arriving as instructions — the user types `/claude-code-kanban:follow`.
33
33
 
34
34
  ## Troubleshooting
35
35