claude-code-kanban 5.1.1 → 5.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.
Files changed (50) hide show
  1. package/README.md +3 -2
  2. package/lib/auto-compact.js +42 -0
  3. package/lib/claude-dir.js +7 -1
  4. package/lib/claude-settings.js +21 -0
  5. package/lib/dispatch.js +11 -4
  6. package/lib/net-guard.js +41 -14
  7. package/lib/proc-stats.js +82 -0
  8. package/lib/retention.js +140 -0
  9. package/lib/session-events.js +30 -7
  10. package/lib/terminal.js +39 -6
  11. package/lib/worktrees.js +105 -0
  12. package/package.json +1 -1
  13. package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
  14. package/plugin/plugins/claude-code-kanban/hooks/hooks.json +15 -8
  15. package/plugin/plugins/claude-code-kanban/monitors.json +3 -3
  16. package/plugin/plugins/claude-code-kanban/scripts/postman.js +2 -2
  17. package/plugin/plugins/claude-code-kanban/skills/{kanban-dispatch → dispatch}/SKILL.md +3 -3
  18. package/plugin/plugins/claude-code-kanban/skills/{kanban-follow → follow}/SKILL.md +17 -9
  19. package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +1 -2
  20. package/public/app.js +1332 -490
  21. package/public/fonts/OFL-IBM-Plex.txt +93 -0
  22. package/public/fonts/OFL-Playfair-Display.txt +93 -0
  23. package/public/fonts/fonts.css +244 -0
  24. package/public/fonts/ibm-plex-mono-cyrillic-400.woff2 +0 -0
  25. package/public/fonts/ibm-plex-mono-cyrillic-500.woff2 +0 -0
  26. package/public/fonts/ibm-plex-mono-cyrillic-600.woff2 +0 -0
  27. package/public/fonts/ibm-plex-mono-cyrillic-ext-400.woff2 +0 -0
  28. package/public/fonts/ibm-plex-mono-cyrillic-ext-500.woff2 +0 -0
  29. package/public/fonts/ibm-plex-mono-cyrillic-ext-600.woff2 +0 -0
  30. package/public/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
  31. package/public/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
  32. package/public/fonts/ibm-plex-mono-latin-600.woff2 +0 -0
  33. package/public/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
  34. package/public/fonts/ibm-plex-mono-latin-ext-500.woff2 +0 -0
  35. package/public/fonts/ibm-plex-mono-latin-ext-600.woff2 +0 -0
  36. package/public/fonts/ibm-plex-mono-vietnamese-400.woff2 +0 -0
  37. package/public/fonts/ibm-plex-mono-vietnamese-500.woff2 +0 -0
  38. package/public/fonts/ibm-plex-mono-vietnamese-600.woff2 +0 -0
  39. package/public/fonts/playfair-display-cyrillic-400.woff2 +0 -0
  40. package/public/fonts/playfair-display-latin-400.woff2 +0 -0
  41. package/public/fonts/playfair-display-latin-ext-400.woff2 +0 -0
  42. package/public/fonts/playfair-display-vietnamese-400.woff2 +0 -0
  43. package/public/index.html +27 -17
  44. package/public/project-match.js +6 -4
  45. package/public/style.css +528 -116
  46. package/public/terminal-frame.js +341 -0
  47. package/public/terminal.html +27 -0
  48. package/public/vendor/claude-hub-sdk.js +13 -3
  49. package/server.js +204 -60
  50. package/skill-guides/dispatch.md +5 -5
@@ -0,0 +1,105 @@
1
+ // Which project paths are linked worktrees, and of which repo. The rule and the sweep:
2
+ // docs/retention.md.
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { GRACE_MS } = require('./retention');
7
+ const { encodeProjectDirName } = require('./claude-dir');
8
+
9
+ // A linked worktree's `.git` is a file holding `gitdir: <main>/.git/worktrees/<name>`, so the
10
+ // main checkout is readable without spawning git. Path shape alone would not do: only some
11
+ // worktrees live under `<repo>/.claude/worktrees/`, the rest sit beside the repo.
12
+ const GITDIR_WORKTREE_RE = /^gitdir:\s*(.*)[/\\]\.git[/\\]worktrees[/\\]([^/\\]+)[/\\]?$/;
13
+ // Where `claude -w` puts a worktree. Used only once the `.git` file is gone, because Claude
14
+ // Code removes worktrees while their transcripts stay.
15
+ const CLAUDE_WORKTREE_RE = /^(.*)[/\\]\.claude[/\\]worktrees[/\\]([^/\\]+)[/\\]?$/;
16
+ const MISS_CACHE_MAX = 500;
17
+
18
+ function readGitFile(dir) {
19
+ return fs.readFileSync(path.join(dir, '.git'), 'utf8');
20
+ }
21
+
22
+ function fromGitFile(dir, text) {
23
+ const m = GITDIR_WORKTREE_RE.exec(text.trim());
24
+ // Git writes the pointer with forward slashes on Windows; the project path uses the OS spelling.
25
+ return m ? { repo: path.resolve(dir, m[1]), name: m[2] } : null;
26
+ }
27
+
28
+ function fromPathShape(dir) {
29
+ const m = CLAUDE_WORKTREE_RE.exec(dir);
30
+ return m ? { repo: m[1], name: m[2] } : null;
31
+ }
32
+
33
+ /**
34
+ * Hits are saved, so a worktree keeps its repo after Claude Code deletes the checkout. Misses
35
+ * stay in memory: an ordinary checkout cannot become a linked worktree without being recreated.
36
+ * @param {object} o
37
+ * @param {() => object|null} o.load returns `{version: 1, worktrees: {[dir]: {repo, name, at}}}` or null
38
+ * @param {(data: object) => void} o.save
39
+ * @param {(fn: () => void) => void} [o.defer] when to save; one session list can resolve many new worktrees
40
+ */
41
+ function createWorktreeStore({ load, save, read = readGitFile, now = Date.now, defer = setImmediate }) {
42
+ const hits = new Map();
43
+ const misses = new Set();
44
+ const saved = load()?.worktrees;
45
+ if (saved && typeof saved === 'object') {
46
+ for (const [dir, e] of Object.entries(saved)) {
47
+ if (e && typeof e.repo === 'string' && typeof e.name === 'string' && Number.isFinite(e.at)) hits.set(dir, e);
48
+ }
49
+ }
50
+
51
+ let pending = false;
52
+ function persist() {
53
+ if (pending) return;
54
+ pending = true;
55
+ defer(() => {
56
+ pending = false;
57
+ save({ version: 1, worktrees: Object.fromEntries(hits) });
58
+ });
59
+ }
60
+
61
+ function resolve(dir) {
62
+ if (!dir) return null;
63
+ const hit = hits.get(dir);
64
+ if (hit) return { repo: hit.repo, name: hit.name };
65
+ if (misses.has(dir)) return null;
66
+
67
+ let worktree = null;
68
+ try {
69
+ worktree = fromGitFile(dir, read(dir));
70
+ } catch (e) {
71
+ // An ordinary checkout's `.git` is a directory, so the read throws EISDIR. That is the
72
+ // answer, and it costs one syscall instead of a stat followed by a read.
73
+ if (e.code === 'ENOENT') worktree = fromPathShape(dir);
74
+ }
75
+
76
+ if (!worktree) {
77
+ misses.add(dir);
78
+ if (misses.size > MISS_CACHE_MAX) misses.delete(misses.values().next().value);
79
+ return null;
80
+ }
81
+ hits.set(dir, { ...worktree, at: now() });
82
+ persist();
83
+ return worktree;
84
+ }
85
+
86
+ // `knownDirs` holds the project dir names that still have a transcript, or null when the scan
87
+ // found none, so a failed scan never drops every entry. Returns how many entries went.
88
+ function prune(knownDirs) {
89
+ if (!knownDirs) return 0;
90
+ const t = now();
91
+ let removed = 0;
92
+ for (const [dir, e] of hits) {
93
+ if (t - e.at >= GRACE_MS && !knownDirs.has(encodeProjectDirName(dir))) {
94
+ hits.delete(dir);
95
+ removed++;
96
+ }
97
+ }
98
+ if (removed) persist();
99
+ return removed;
100
+ }
101
+
102
+ return { resolve, prune };
103
+ }
104
+
105
+ module.exports = { createWorktreeStore };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "5.1.1",
3
+ "version": "5.3.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.2",
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,8 +32,9 @@
30
32
  "hooks": [
31
33
  {
32
34
  "type": "command",
33
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
34
- "timeout": 5
35
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
36
+ "shell": "bash",
37
+ "timeout": 30
35
38
  }
36
39
  ]
37
40
  }
@@ -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,7 +1,7 @@
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
- argument-hint: '<task> [--report] [--group <name>] [--model haiku|sonnet|opus|fable] [--worktree [name]]'
4
+ argument-hint: '<task> [--no-report] [--group <name>] [--model haiku|sonnet|opus|fable] [--worktree [name]]'
5
5
  ---
6
6
 
7
7
  # Kanban dispatch
@@ -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`.
@@ -2,7 +2,6 @@
2
2
  name: kanban
3
3
  description: Drive the kanban board — open, pin, preview, link, inspect.
4
4
  argument-hint: '[open|pin|unpin|preview|link] [target]'
5
- disable-model-invocation: true
6
5
  ---
7
6
 
8
7
  # Kanban Skill
@@ -29,7 +28,7 @@ claude-code-kanban help <command> <subcommand> # flags and examples
29
28
  | `view` / `peek` | `session view <id>`, `session peek <id>` |
30
29
  | tasks, projects | `task list`, `project list` |
31
30
 
32
- To be driven *by* the board instead — card moves arriving as instructions — the user types `/claude-code-kanban:kanban-follow`.
31
+ To be driven *by* the board instead — card moves arriving as instructions — the user types `/claude-code-kanban:follow`.
33
32
 
34
33
  ## Troubleshooting
35
34