local-kanban 1.6.3
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/CHANGELOG.md +381 -0
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/README.ru.md +144 -0
- package/bin/kb +306 -0
- package/bin/local-kanban +217 -0
- package/ecosystem.example.cjs +25 -0
- package/package.json +52 -0
- package/public/app.js +18 -0
- package/public/boot-fallback.js +3 -0
- package/public/drawer-sync.js +14 -0
- package/public/favicon-180.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +6 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-192.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/index.html +138 -0
- package/public/js/board.js +698 -0
- package/public/js/chaos.js +247 -0
- package/public/js/core.js +821 -0
- package/public/js/dash.js +114 -0
- package/public/js/drawer.js +919 -0
- package/public/js/horizon.js +140 -0
- package/public/js/init.js +208 -0
- package/public/js/project.js +338 -0
- package/public/js/settings.js +905 -0
- package/public/js/sidebar.js +640 -0
- package/public/js/sse.js +386 -0
- package/public/manifest.webmanifest +32 -0
- package/public/offline.html +39 -0
- package/public/style.css +1247 -0
- package/public/sw.js +31 -0
- package/scripts/raycast/open-kanban.sh +18 -0
- package/scripts/update.js +43 -0
- package/skills/deploy/SKILL.md +70 -0
- package/skills/kanban/SKILL.md +217 -0
- package/src/backup.js +111 -0
- package/src/bus.js +10 -0
- package/src/config.js +40 -0
- package/src/db.js +321 -0
- package/src/error-handler.js +11 -0
- package/src/file-snapshot.js +22 -0
- package/src/origin-guard.js +32 -0
- package/src/pm2-services.js +31 -0
- package/src/query-params.js +17 -0
- package/src/repo-base.js +23 -0
- package/src/restore.js +153 -0
- package/src/routes/dashboard.js +132 -0
- package/src/routes/events.js +30 -0
- package/src/routes/horizons.js +66 -0
- package/src/routes/projects.js +532 -0
- package/src/routes/skills.js +116 -0
- package/src/routes/system.js +285 -0
- package/src/routes/tasks.js +477 -0
- package/src/schema.sql +135 -0
- package/src/server.js +100 -0
- package/src/skills.js +119 -0
- package/src/sync/github.js +265 -0
- package/src/sync/worker.js +136 -0
- package/src/task-shape.js +29 -0
package/public/sw.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const OFFLINE_CACHE = 'kb-offline-v1';
|
|
2
|
+
const OFFLINE_URL = '/offline.html';
|
|
3
|
+
|
|
4
|
+
self.addEventListener('install', (e) => {
|
|
5
|
+
e.waitUntil(caches.open(OFFLINE_CACHE).then((c) => c.add(OFFLINE_URL)));
|
|
6
|
+
self.skipWaiting();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
self.addEventListener('activate', (e) => {
|
|
10
|
+
e.waitUntil((async () => {
|
|
11
|
+
const keys = await caches.keys();
|
|
12
|
+
await Promise.all(keys.filter((k) => k !== OFFLINE_CACHE).map((k) => caches.delete(k)));
|
|
13
|
+
await self.clients.claim();
|
|
14
|
+
})());
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
self.addEventListener('fetch', (e) => {
|
|
18
|
+
const req = e.request;
|
|
19
|
+
if (req.mode !== 'navigate') return;
|
|
20
|
+
e.respondWith((async () => {
|
|
21
|
+
try {
|
|
22
|
+
return await fetch(req);
|
|
23
|
+
} catch {
|
|
24
|
+
return (await caches.match(OFFLINE_URL))
|
|
25
|
+
|| new Response('<h1>No connection to the board</h1>', {
|
|
26
|
+
status: 503,
|
|
27
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
})());
|
|
31
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# A Raycast Script Command that opens the board. Written in English on purpose: this file ships in
|
|
4
|
+
# the package, and the @raycast lines below are not prose — Raycast parses them, and without
|
|
5
|
+
# @raycast.title it refuses to register the command at all.
|
|
6
|
+
# Install once, in the Raycast UI:
|
|
7
|
+
# 1. Raycast -> Settings -> Extensions -> Script Commands -> "Add Script Directory"
|
|
8
|
+
# -> pick this folder (.../local-kanban/scripts/raycast).
|
|
9
|
+
# 2. Find "Open Kanban" in Raycast -> Cmd+K -> "Configure Command" -> "Record Hotkey".
|
|
10
|
+
# After that the hotkey opens the board from any application.
|
|
11
|
+
# Running on a different port: change the address on the last line.
|
|
12
|
+
# @raycast.schemaVersion 1
|
|
13
|
+
# @raycast.title Open Kanban
|
|
14
|
+
# @raycast.mode silent
|
|
15
|
+
# @raycast.packageName local-kanban
|
|
16
|
+
# @raycast.icon 📋
|
|
17
|
+
# @raycast.description Open the local kanban board (localhost:3100)
|
|
18
|
+
open "http://localhost:3100"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const run = (cmd, args, opts = {}) =>
|
|
8
|
+
spawnSync(cmd, args, { cwd: ROOT, stdio: 'inherit', ...opts });
|
|
9
|
+
const out = (cmd, args) => {
|
|
10
|
+
try { return execFileSync(cmd, args, { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }
|
|
11
|
+
catch { return null; }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const dirty = out('git', ['status', '--porcelain']);
|
|
15
|
+
if (dirty === null) { console.error('this is not a git repository — update with npm i -g local-kanban@latest'); process.exit(1); }
|
|
16
|
+
if (dirty) {
|
|
17
|
+
console.error('the working tree is not clean — commit or stash your changes first:\n' + dirty);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('→ git pull');
|
|
22
|
+
if (run('git', ['pull', '--ff-only']).status !== 0) {
|
|
23
|
+
console.error('git pull failed (local history diverged from origin?) — sort it out by hand');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('→ npm install');
|
|
28
|
+
if (run('npm', ['install', '--no-fund', '--no-audit']).status !== 0) process.exit(1);
|
|
29
|
+
|
|
30
|
+
const pm2List = out('pm2', ['jlist']);
|
|
31
|
+
let restarted = false;
|
|
32
|
+
if (pm2List) {
|
|
33
|
+
try {
|
|
34
|
+
const procs = JSON.parse(pm2List.slice(pm2List.indexOf('[')));
|
|
35
|
+
if (procs.some((p) => p.name === 'kanban')) {
|
|
36
|
+
console.log('→ pm2 restart kanban');
|
|
37
|
+
restarted = run('pm2', ['restart', 'kanban']).status === 0;
|
|
38
|
+
}
|
|
39
|
+
} catch { }
|
|
40
|
+
}
|
|
41
|
+
console.log(restarted
|
|
42
|
+
? '✓ the board is updated and restarted'
|
|
43
|
+
: '✓ the code is updated — restart the server (pm2 restart kanban / local-kanban start)');
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy
|
|
3
|
+
description: Generic deployment for any project on the kanban board — driven entirely by `kb info <proj>` (path, server, pm2, domain), nothing hardcoded. Triggers — "deploy <project>", "ship it", "update it on the server", and the deploy step of the kanban working loop. Flow: review changes → commit → push → pull on the server → install/build → restart → verify. A local project with no server takes the short branch: commit + restart the local pm2 process.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Generic deploy (driven by kb info)
|
|
7
|
+
|
|
8
|
+
This skill is not tied to any particular server: everything comes from the board's registry. Does
|
|
9
|
+
the project have its own flow? Then it has its own `deploy_skill` in `kb info` — read that
|
|
10
|
+
SKILL.md at `deploy_skill_path` and follow it instead of this one.
|
|
11
|
+
|
|
12
|
+
## 0. The data
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
kb info <proj> # path (local copy), server (ssh host), server_path (path on the server),
|
|
16
|
+
# pm2 (process names), domain (for verification)
|
|
17
|
+
```
|
|
18
|
+
- SSH access uses the alias or address from `server` via `ssh <server>`: keys, users and ports live
|
|
19
|
+
in the user's `~/.ssh/config`. The board stores no secrets.
|
|
20
|
+
- No `server` → the project is local: steps 1–2, then straight to 5 (a local restart if it has pm2).
|
|
21
|
+
|
|
22
|
+
## 1. Review the changes
|
|
23
|
+
|
|
24
|
+
- `git -C <path> status --porcelain` plus `git -C <path> diff --stat` — what has moved away from HEAD.
|
|
25
|
+
- Do not commit junk (temp files, .DS_Store, stray logs).
|
|
26
|
+
- If the working tree is clean and everything is already pushed, go straight to step 3.
|
|
27
|
+
|
|
28
|
+
## 2. Commit + push
|
|
29
|
+
|
|
30
|
+
- Commit by meaning, with the task's issue number when the deployment is part of the kanban loop.
|
|
31
|
+
- `git -C <path> push origin <current branch>`.
|
|
32
|
+
|
|
33
|
+
## 3. Update on the server
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
ssh <server> "cd <server_path> && git pull --ff-only"
|
|
37
|
+
```
|
|
38
|
+
- `git pull` failed because of local edits on the server → STOP and tell the human: the server has
|
|
39
|
+
drifted, and overwriting it silently (`reset --hard`) is not allowed. Inspect it with
|
|
40
|
+
`ssh <server> "cd <server_path> && git status --porcelain"`.
|
|
41
|
+
|
|
42
|
+
## 4. Install / build (by project type — check package.json or the manifest)
|
|
43
|
+
|
|
44
|
+
- Node: `npm ci || npm install`; if there is a build script, `npm run build`.
|
|
45
|
+
- Python: `pip install -r requirements.txt` inside the project's venv.
|
|
46
|
+
- Static sites with no build step — skip.
|
|
47
|
+
|
|
48
|
+
## 5. Restart
|
|
49
|
+
|
|
50
|
+
- pm2 processes from `kb info`: `ssh <server> "pm2 restart <names>"` (for a local project,
|
|
51
|
+
`pm2 restart <names>` without ssh).
|
|
52
|
+
- No pm2 → whatever the project uses (a systemd unit, docker compose). If it is unclear, ask the
|
|
53
|
+
human instead of guessing.
|
|
54
|
+
|
|
55
|
+
## 6. Verify
|
|
56
|
+
|
|
57
|
+
- With a `domain`: `curl -sS -o /dev/null -w '%{http_code}' https://<domain>` (or http, whichever
|
|
58
|
+
the project uses) — expect 200/301.
|
|
59
|
+
- Check exactly what you changed: the specific page or endpoint, not just the home page.
|
|
60
|
+
- pm2: `kb svc <proj>` — processes online, restart counts not climbing.
|
|
61
|
+
- Verification failed → discuss the rollback with the human (git revert plus another deploy); never
|
|
62
|
+
roll back silently.
|
|
63
|
+
|
|
64
|
+
## Rules
|
|
65
|
+
|
|
66
|
+
- Production commands (ssh, rsync, restart) run with the user's confirmation — that is expected,
|
|
67
|
+
do not work around it.
|
|
68
|
+
- The code lives in git: editing directly on the server is forbidden. Spotted drift? Report it,
|
|
69
|
+
do not overwrite it.
|
|
70
|
+
- One task, one deployment; related tasks that are ready together ship in a single run.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kanban
|
|
3
|
+
description: Local kanban task board (the `kb` CLI, localhost:3100). Triggers — "what's on the board", "add a task", "work through project X", "do TIC-3", "take the next task", "accept TIC-3", "send TIC-3 back", "show the board", "block this task". Statuses and comments are mirrored to GitHub automatically (when sync is configured) — you never have to think about it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kanban (kb)
|
|
7
|
+
|
|
8
|
+
Every task lives in a local SQLite database behind the `kb` CLI. GitHub (issues + Projects v2) is a mirror; the sync is optional and runs in the background — ignore it.
|
|
9
|
+
Tasks are addressed by key: `TIC-3` (project prefix + number). Case does not matter.
|
|
10
|
+
|
|
11
|
+
Statuses: `backlog` → `todo` → `prep` → `doing` → `deploy` → `review` → `done` (+ `cancelled`). Blocked is a flag.
|
|
12
|
+
`prep` is where you study the task before writing code (reading CLAUDE.md, reading files). The work timer does NOT run there — only `doing` and `deploy` count as work. `kb take` puts a task in `prep`; the moment you start writing code, run `kb mv <key> doing`.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
kb take <proj> # take the next task from todo → prep; prints the description,
|
|
18
|
+
# the issue number (for commit messages), the last note from the
|
|
19
|
+
# human (if the task was sent back, act on it!) and the paths of
|
|
20
|
+
# attached screenshots as "📎 <path>" — OPEN those with Read,
|
|
21
|
+
# they are context for the task.
|
|
22
|
+
# started writing code → kb mv <key> doing.
|
|
23
|
+
kb review <key> "note" # leave a note and move to review. NOT atomic: the note is stored
|
|
24
|
+
# EVEN IF the move is rejected (backlog and todo cannot go straight
|
|
25
|
+
# to review — go through prep or doing first; prep→review is fine).
|
|
26
|
+
# Comments are not de-duplicated, so don't call this on a task that
|
|
27
|
+
# never went through doing.
|
|
28
|
+
kb p # projects: slug (PREFIX) b0 t2 pp0 d1 dp0 r1 ✓5 !1 (pp = prep)
|
|
29
|
+
kb ls <proj> [-s st] [-a] # tasks (without -a, backlog/done/cancelled are hidden)
|
|
30
|
+
kb show <key> # details + issue + latest comments
|
|
31
|
+
kb add <proj> "title" [-d "desc"] [-p high|med|low] [-s todo] # goes to backlog by default!
|
|
32
|
+
kb mv <key> todo|prep|doing|deploy|review|done|cancelled
|
|
33
|
+
kb done <key> · kb cancel <key> · kb pri <key> high|med|low|none
|
|
34
|
+
kb lbl <key> +bug -ui # labels: bug docs duplicate enhancement ui feature security noclaude ask …
|
|
35
|
+
# noclaude = done by hand (without Claude) → counted separately on the dashboard
|
|
36
|
+
# ask = ask clarifying questions and WAIT for an answer before starting
|
|
37
|
+
# (see Special cases)
|
|
38
|
+
# ⚠️ only labels from the palette (LABELS in github.js): kb lbl / kb add
|
|
39
|
+
# reject an unknown label immediately (exit code 2). A genuinely new
|
|
40
|
+
# label goes into the LABELS palette first, and into this file.
|
|
41
|
+
kb desc <key> "description" # set or overwrite a task description (used when grooming the backlog)
|
|
42
|
+
kb note <key> "text" # a standalone comment (rarely needed — kb review covers it)
|
|
43
|
+
kb link <key> <key> # link two tasks (the board draws a line) · kb unlink to unlink
|
|
44
|
+
kb block <key> "why" · kb unblock <key>
|
|
45
|
+
kb info <proj> # path, deploy_skill, server, pm2, domain
|
|
46
|
+
kb svc <proj> # live status of the project's services (local pm2 or your own PANEL_URL)
|
|
47
|
+
kb sync # state of the sync queue (or "sync is off" in local-only mode)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## A new project goes on the board FIRST (hard rule)
|
|
51
|
+
|
|
52
|
+
**The moment you create or start a new project — a new folder, site, bot, service — register it
|
|
53
|
+
on the board before writing any task or any code for it.** Otherwise the work happens off the
|
|
54
|
+
board: it has no task key and no deploy metadata. The board does NOT register projects on its own:
|
|
55
|
+
local folders under `LOCAL_ROOT` (`~/claude-projects`) appear in the sidebar with an "+ add" row,
|
|
56
|
+
but a project living anywhere else has to be registered by hand.
|
|
57
|
+
|
|
58
|
+
Register it through the API rather than raw SQL — the API emits an SSE event so the board updates
|
|
59
|
+
live, and the GitHub project is provisioned on the first task:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# python3, so quoting doesn't get in the way
|
|
63
|
+
import json, urllib.request
|
|
64
|
+
body = {
|
|
65
|
+
"slug": "myproj", "name": "myproj.com",
|
|
66
|
+
"path": "<path to the project's code>",
|
|
67
|
+
"deploy_skill": "deploy", # or empty if there is no deployment
|
|
68
|
+
"server": "<host>", "domain": "myproj.com",
|
|
69
|
+
"category": "<sidebar group>",
|
|
70
|
+
"description": "…",
|
|
71
|
+
}
|
|
72
|
+
req = urllib.request.Request("http://localhost:3100/api/projects",
|
|
73
|
+
data=json.dumps(body).encode(), method="POST",
|
|
74
|
+
headers={"Content-Type": "application/json"})
|
|
75
|
+
print(urllib.request.urlopen(req, timeout=10).read().decode()) # 201 + the object, including prefix
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Registry fields (PATCHABLE): `name, prefix, path, server_path, deploy_skill, server,
|
|
79
|
+
pm2_services, domain, category, description, pinned, archived`. `slug` and `name` are required;
|
|
80
|
+
`prefix` (the `PREFIX-N` task key) is generated for you. Edit later with
|
|
81
|
+
`PATCH /api/projects/:slug`. When a human registers a project by hand, their path is the
|
|
82
|
+
"+ Add project" wizard at the bottom of the sidebar; the API snippet above is yours.
|
|
83
|
+
|
|
84
|
+
**Verify it landed (do not skip):** `kb info <slug>` (path/deploy_skill/server/domain filled in),
|
|
85
|
+
`kb p | grep <slug>` (visible on the board), `kb sync` (the queue is not stuck). Only then create
|
|
86
|
+
tasks (`kb add`) and start working.
|
|
87
|
+
|
|
88
|
+
## The working loop ("work through project X")
|
|
89
|
+
|
|
90
|
+
Claude takes a task as far as *review* and stops. Only the user moves anything to done/cancelled.
|
|
91
|
+
|
|
92
|
+
**When the prompt lists specific keys** (the 📋 button on the "To do" column produces them) those
|
|
93
|
+
are SPECIFIC tasks, not a queue. Take each one with `kb show <key>` (NOT `kb take`, which pulls
|
|
94
|
+
ONE next task from the project's `todo`) and **move it to prep right away: `kb mv <key> prep`** —
|
|
95
|
+
`kb show` does not change the status. If `kb show` printed `> 👤 Send back: …`, the task was
|
|
96
|
+
returned: open the `📎` screenshots with Read and act on that note FIRST. Then follow steps 2–6
|
|
97
|
+
for each. Use `kb take X` only when asked to "work through project X" without specific keys.
|
|
98
|
+
|
|
99
|
+
1. `kb take X` — take a task (lands in prep; description and issue are already in the output).
|
|
100
|
+
"empty" → report it and stop. `> 👤 …` is a note from a send-back, act on it first.
|
|
101
|
+
`📎 <path>` — open the screenshot with Read.
|
|
102
|
+
2. **HARD RULE — starting a task (never skip, even for a one-line change).** `kb info X` → go to
|
|
103
|
+
`path`. Before any code you MUST read: (a) the project's `CLAUDE.md` if it has one — **and if
|
|
104
|
+
it does not, find where the project keeps its architecture or research document (`README.md`,
|
|
105
|
+
`EXPERIMENTS.md`, `docs/`) and read that; apply the "doc in the same commit" rule to it**;
|
|
106
|
+
(b) the deploy skill at `deploy_skill_path` from `kb info`, if one is set. The matching commit
|
|
107
|
+
rule: **if you change code whose behaviour is documented in the project's `CLAUDE.md`, update
|
|
108
|
+
that section in the SAME commit.** When you edit a document with a script, gate the commit on
|
|
109
|
+
the script succeeding in one chain (`python … && git add … && git commit …`): a script that
|
|
110
|
+
failed on a missing anchor will not stop the next shell command, and the commit would go out
|
|
111
|
+
without the document. Started writing code → `kb mv X doing` (the timer starts).
|
|
112
|
+
3. Reference the task's issue in commit messages: `git commit -m "... (#<N>)"` (or `owner/repo#<N>`
|
|
113
|
+
when you commit outside the issues repo) — N comes from the `take`/`show` output. The board
|
|
114
|
+
attaches commit stats to the task on the move to `review`: it looks for a commit that really
|
|
115
|
+
references the issue (`git log -1 --grep=#N`). **When sync is off** (the task has no issue
|
|
116
|
+
number) stats come from the last commit made while the task was in progress, so move the task
|
|
117
|
+
to `doing` BEFORE its commits. Commit with `git -C <path>` or after `cd <path>` — the shell's
|
|
118
|
+
cwd may be outside the repository. A project not under git simply skips the commit step; that
|
|
119
|
+
is normal. **Several tasks touching one file** → either one commit referencing both issues
|
|
120
|
+
(`… (#A, #B)`), or the "revert the hunk → commit → restore it" trick; with 3+ interleaved
|
|
121
|
+
changes, slice the full diff into per-issue patch files and apply them with `git apply`,
|
|
122
|
+
committing in between.
|
|
123
|
+
4. If the project has a `deploy_skill`: `kb mv <key> deploy` → run the deployment with that skill →
|
|
124
|
+
step 5. Without a deploy_skill, go straight to step 5. Take the skill from `deploy_skill_path`
|
|
125
|
+
in `kb info` (if the skill is not in the available list, just read its SKILL.md at that path).
|
|
126
|
+
The generic `deploy` skill ships with the board (`skills/deploy/`) and works purely off
|
|
127
|
+
`kb info` data. Production commands (ssh/rsync) will ask for confirmation — that is expected.
|
|
128
|
+
5. `kb review <key> "<two plain sentences: 1) what the problem was 2) how you solved it>"` → take
|
|
129
|
+
the next one.
|
|
130
|
+
6. Stuck, or the decision is the human's → `kb block <key> "reason"` and move on.
|
|
131
|
+
7. At the end, a one-line summary: how many in review, how many blocked.
|
|
132
|
+
|
|
133
|
+
## Grooming the backlog
|
|
134
|
+
|
|
135
|
+
Triggered by a prompt like `Groom the backlog [of X] (grooming mode):` with a list of keys (the 📋
|
|
136
|
+
button on the "Backlog" column). This is NOT "do the tasks" — no code is written, no statuses move,
|
|
137
|
+
**every task stays in the backlog**. The goal is to declutter and prepare.
|
|
138
|
+
|
|
139
|
+
For each key: 1) `kb show` — title, description, comments; 2) no description → `kb desc <key> "…"`
|
|
140
|
+
(one or two sentences, invent nothing); 3) classify: `kb pri` by meaning (unclear — leave it) plus
|
|
141
|
+
`kb lbl`; 4) anything unclear → ask via `kb note`, do not guess; 5) do not change the status.
|
|
142
|
+
One line of summary at the end.
|
|
143
|
+
|
|
144
|
+
## User commands for a task in review
|
|
145
|
+
|
|
146
|
+
- "accept TIC-3" → `kb done TIC-3`.
|
|
147
|
+
- "send TIC-3 back: <what's wrong>" → `kb unblock` if needed, `kb mv TIC-3 doing`, fix it, then the
|
|
148
|
+
loop again from step 4. **First REPRODUCE the exact scenario from the note** (curl or a browser,
|
|
149
|
+
following the real flow) and confirm the actual cause — only then fix it. A second attempt at a
|
|
150
|
+
returned task often cures a plausible but wrong cause.
|
|
151
|
+
- "cancel TIC-3" → `kb cancel TIC-3`.
|
|
152
|
+
- "do TIC-3" (a specific task in any status except done) → `kb show TIC-3` (the `👤` note comes
|
|
153
|
+
first; open `📎` screenshots with Read) and **`kb mv TIC-3 prep` right away**, then steps 2–6.
|
|
154
|
+
|
|
155
|
+
## Special cases
|
|
156
|
+
|
|
157
|
+
- **The `ask` label on a task** (visible in `kb show`/`kb take` output) → this is a BLOCKING step:
|
|
158
|
+
study the task in `prep`, then ask your clarifying questions in chat — how you understood the
|
|
159
|
+
task, what you intend to change and where — EVEN IF everything looks obvious, and WAIT for an
|
|
160
|
+
answer. Do not move to `doing` or write code until the human replies.
|
|
161
|
+
- **You found an unrelated problem along the way** → do not fix it silently. Create a task
|
|
162
|
+
(`kb add <proj> "..." -d "..."`) and ask in chat: do it now, defer it, or is it for the human.
|
|
163
|
+
- **Related tasks solved together** → take both at once (both in doing), fix them in one pass and
|
|
164
|
+
link them with `kb link`.
|
|
165
|
+
- **A discussion or question task** (no code): study it in `prep`, put the full answer in
|
|
166
|
+
`kb note <key> "..."`, then `kb mv <key> review`. You write no code and do not move it to done.
|
|
167
|
+
- A "check whether the service is alive" task → `kb svc <proj>` (one line per service, no ssh).
|
|
168
|
+
- Work time is measured for you: doing/deploy runs the timer, review/done stops it.
|
|
169
|
+
- Verifying the result: text and config changes — curl/grep (cheap). Compiled projects — **a build**
|
|
170
|
+
is the cheap check (`cargo build`, `go build ./...`, `tsc --noEmit`, …). **For layout and visual
|
|
171
|
+
changes a screenshot IS the cheap check** — curl and grep never catch a broken layout: serve the
|
|
172
|
+
project from its root and screenshot the page with a headless browser (playwright), on desktop
|
|
173
|
+
(1280) and on mobile (390/360). Mobile "cut off" bugs are horizontal overflow — compare
|
|
174
|
+
`scrollWidth` with `clientWidth` and find elements whose `getBoundingClientRect().right >
|
|
175
|
+
clientWidth`, ignoring those inside an `overflow-x:auto` ancestor.
|
|
176
|
+
- **Driving the board's own UI in a browser** (drag & drop, drawer, SSE): use playwright directly,
|
|
177
|
+
`page.goto('http://localhost:3100', { waitUntil: 'domcontentloaded' })` — NOT `networkidle`, the
|
|
178
|
+
SSE connection never closes. Wait for `waitForSelector('#projects .proj')`. **The dashboard is
|
|
179
|
+
what opens on start**, so click a project in the sidebar first, then wait for `.col-body .card`.
|
|
180
|
+
Top-level functions are exposed on `window` (`openDrawer`, `getSetting`, …); `state` is not —
|
|
181
|
+
read state from the DOM. Test mutations on a throwaway task (`POST /api/tasks` → check →
|
|
182
|
+
`DELETE /api/tasks/<key>`); task numbers are not reused, which is normal. For copy buttons, grant
|
|
183
|
+
the clipboard permissions to the browser context or `readText()` silently returns empty. Call
|
|
184
|
+
actions that open a confirmation modal without returning the promise:
|
|
185
|
+
`page.evaluate((k) => { window.deleteTask(k); })` — returning it deadlocks; click the modal as a
|
|
186
|
+
separate step.
|
|
187
|
+
|
|
188
|
+
## Token rules (hard)
|
|
189
|
+
|
|
190
|
+
- Never dump the whole board into the chat. One action, one line of `kb` output, no retelling.
|
|
191
|
+
- `kb show` only when you need the description in order to work.
|
|
192
|
+
- **Going into the database directly: it is `data/kanban.db`, NOT a `kanban.db` in the repo root.**
|
|
193
|
+
Gotcha: `sqlite3 <path>` SILENTLY creates an empty file when nothing is there, so a typo leaves a
|
|
194
|
+
junk 0-byte `.db` in a clean repository. Check the path before running it, and look at
|
|
195
|
+
`git status` after any sqlite session.
|
|
196
|
+
- Read task details with `kb show`/`kb take`, not `curl /api/tasks…|jq`: descriptions contain raw
|
|
197
|
+
newlines and a shell JSON parser chokes on them. If you truly need the API, save the response to
|
|
198
|
+
a file and parse it leniently (`python3 … strict=False`).
|
|
199
|
+
- **Bulk-create tasks or comments with a script** (`subprocess.run([kb,'add',…])` / `execFile`)
|
|
200
|
+
rather than assembling shell commands: backticks, quotes and newlines get mangled by the shell.
|
|
201
|
+
`kb add` prints the key as the first word of its output — take it and attach labels with `kb lbl`.
|
|
202
|
+
- **Bulk moves (`kb mv`/`kb review`) from a script must check `returncode`, not printed output**: a
|
|
203
|
+
rejected move exits non-zero, yet `kb review` still stores the comment. Verify with `kb p` at the
|
|
204
|
+
end that the counts add up.
|
|
205
|
+
- Removed an element from a page — verify with grep on **the text or markup**, not the class name
|
|
206
|
+
(the CSS rule of the same name stays behind).
|
|
207
|
+
- The review comment is **exactly two plain sentences, no jargon**: (1) what the problem was,
|
|
208
|
+
(2) how you solved it.
|
|
209
|
+
- The final chat message is one line per task: `TIC-3 review — summary board is ready`.
|
|
210
|
+
- `kb: board is not responding` → restart the board's server (`pm2 restart kanban` / `npm start`),
|
|
211
|
+
wait two seconds and retry once. **Exit codes are distinct: `1` — the board is down (a restart
|
|
212
|
+
and a retry make sense), `2` — the request was rejected (retrying is pointless, read the
|
|
213
|
+
message), `3` — `kb` itself broke (stack trace under `KB_DEBUG=1`), `4` — the board answered,
|
|
214
|
+
but with a failure of its own (5xx) or with something that is not JSON. Gate on the code, not on
|
|
215
|
+
the message text.** Code `4` is worth reading closely: it means the board falls over on this
|
|
216
|
+
request, or `KB_URL` points at something that is not the board at all.
|
|
217
|
+
- The web board is for the human: http://localhost:3100 — do not open it, do not scrape it.
|
package/src/backup.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { cpSync, mkdirSync, existsSync, readdirSync, unlinkSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { db, DATA_DIR, logError } from './db.js';
|
|
4
|
+
|
|
5
|
+
const DIR = join(DATA_DIR, 'backups');
|
|
6
|
+
|
|
7
|
+
const KEEP_DAILY = 7;
|
|
8
|
+
const KEEP_MANUAL = 5;
|
|
9
|
+
const DAILY_RE = /^kanban-\d{4}-\d{2}-\d{2}\.db$/;
|
|
10
|
+
const MANUAL_RE = /^kanban-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.db$/;
|
|
11
|
+
|
|
12
|
+
function backupFiles(re) {
|
|
13
|
+
if (!existsSync(DIR)) return [];
|
|
14
|
+
return readdirSync(DIR).filter((f) => re.test(f)).sort();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rotate(re, keep) {
|
|
18
|
+
const files = backupFiles(re);
|
|
19
|
+
while (files.length > keep) unlinkSync(join(DIR, files.shift()));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let tmpSeq = 0;
|
|
23
|
+
let running = null;
|
|
24
|
+
async function snapshot(path) {
|
|
25
|
+
while (running) { try { await running; } catch { } }
|
|
26
|
+
const done = (async () => {
|
|
27
|
+
const tmp = `${path}.${process.pid}.${++tmpSeq}.part`;
|
|
28
|
+
rmSync(tmp, { force: true });
|
|
29
|
+
try {
|
|
30
|
+
await db.backup(tmp);
|
|
31
|
+
renameSync(tmp, path);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
rmSync(tmp, { force: true });
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
return path;
|
|
37
|
+
})();
|
|
38
|
+
running = done;
|
|
39
|
+
try { return await done; } finally { if (running === done) running = null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const ATTACH_MIRROR = join(DIR, 'attachments');
|
|
43
|
+
function mirrorAttachments() {
|
|
44
|
+
const src = join(DATA_DIR, 'attachments');
|
|
45
|
+
if (!existsSync(src)) return;
|
|
46
|
+
cpSync(src, ATTACH_MIRROR, { recursive: true, force: false, errorOnExist: false });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function listBackups() {
|
|
50
|
+
const rows = [
|
|
51
|
+
...backupFiles(DAILY_RE).map((name) => ({ name, kind: 'daily' })),
|
|
52
|
+
...backupFiles(MANUAL_RE).map((name) => ({ name, kind: 'manual' })),
|
|
53
|
+
];
|
|
54
|
+
return rows
|
|
55
|
+
.map(({ name, kind }) => {
|
|
56
|
+
const s = statSync(join(DIR, name));
|
|
57
|
+
return { name, kind, size: s.size, mtime: s.mtime.toISOString() };
|
|
58
|
+
})
|
|
59
|
+
.sort((a, b) => b.mtime.localeCompare(a.mtime));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function backupPath(name) {
|
|
63
|
+
const found = listBackups().find((b) => b.name === name);
|
|
64
|
+
return found ? join(DIR, found.name) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const backupState = { last_ok: null, last_error: null, last_error_at: null };
|
|
68
|
+
|
|
69
|
+
export async function backupNow() {
|
|
70
|
+
mkdirSync(DIR, { recursive: true });
|
|
71
|
+
const path = join(DIR, `kanban-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.db`);
|
|
72
|
+
await snapshot(path);
|
|
73
|
+
mirrorAttachments();
|
|
74
|
+
rotate(MANUAL_RE, KEEP_MANUAL);
|
|
75
|
+
backupState.last_ok = new Date().toISOString();
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const BACKUP_EVERY_MS = 6 * 3600 * 1000;
|
|
80
|
+
const BACKUP_RETRY_MS = 10 * 60 * 1000;
|
|
81
|
+
export function startBackups() {
|
|
82
|
+
mkdirSync(DIR, { recursive: true });
|
|
83
|
+
let timer = null;
|
|
84
|
+
const again = (ms) => { clearTimeout(timer); timer = setTimeout(run, ms); timer.unref?.(); };
|
|
85
|
+
function run() {
|
|
86
|
+
const path = join(DIR, `kanban-${new Date().toISOString().slice(0, 10)}.db`);
|
|
87
|
+
if (existsSync(path)) {
|
|
88
|
+
mirrorAttachments();
|
|
89
|
+
backupState.last_ok = backupState.last_ok || statSync(path).mtime.toISOString();
|
|
90
|
+
again(BACKUP_EVERY_MS);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
snapshot(path)
|
|
94
|
+
.then(() => {
|
|
95
|
+
mirrorAttachments();
|
|
96
|
+
rotate(DAILY_RE, KEEP_DAILY);
|
|
97
|
+
backupState.last_ok = new Date().toISOString();
|
|
98
|
+
backupState.last_error = null;
|
|
99
|
+
console.log(`backup: ${path}`);
|
|
100
|
+
again(BACKUP_EVERY_MS);
|
|
101
|
+
})
|
|
102
|
+
.catch((e) => {
|
|
103
|
+
backupState.last_error = e.message;
|
|
104
|
+
backupState.last_error_at = new Date().toISOString();
|
|
105
|
+
logError('server', 'backup', `daily backup failed: ${e.message}`, e.stack);
|
|
106
|
+
console.error('backup failed:', e.message);
|
|
107
|
+
again(BACKUP_RETRY_MS);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
run();
|
|
111
|
+
}
|
package/src/bus.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { sseMax } from './config.js';
|
|
3
|
+
|
|
4
|
+
export const bus = new EventEmitter();
|
|
5
|
+
export const SSE_MAX = sseMax();
|
|
6
|
+
bus.setMaxListeners(SSE_MAX + 10);
|
|
7
|
+
|
|
8
|
+
export function emit(type, data) {
|
|
9
|
+
bus.emit('event', { type, data });
|
|
10
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { join, dirname, resolve, sep } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
|
|
8
|
+
const packaged = ROOT.split(sep).includes('node_modules');
|
|
9
|
+
const defaultDataDir = () => {
|
|
10
|
+
if (existsSync(join(ROOT, 'data', 'kanban.db'))) return join(ROOT, 'data');
|
|
11
|
+
return packaged ? join(homedir(), '.local-kanban') : join(ROOT, 'data');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const env = (...names) => {
|
|
15
|
+
for (const n of names) {
|
|
16
|
+
const v = process.env[n];
|
|
17
|
+
if (v !== undefined && v !== '') return v;
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function expandPath(p) {
|
|
23
|
+
if (!p) return p;
|
|
24
|
+
const home = p === '~' || p.startsWith('~/') ? join(homedir(), p.slice(1)) : p;
|
|
25
|
+
return resolve(home);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DATA_DIR = expandPath(env('KB_DATA_DIR')) || defaultDataDir();
|
|
29
|
+
export const PORT = Number(env('PORT') || 3100);
|
|
30
|
+
|
|
31
|
+
export const localRoot = () => expandPath(env('KB_LOCAL_ROOT'))
|
|
32
|
+
|| (env('KB_DATA_DIR') ? join(DATA_DIR, 'projects') : join(homedir(), 'claude-projects'));
|
|
33
|
+
export const panelUrl = () => env('KB_PANEL_URL', 'PANEL_URL');
|
|
34
|
+
export const panelInfo = () => env('KB_PANEL_INFO', 'PANEL_INFO');
|
|
35
|
+
export const skillsExtra = () => env('KB_SKILLS_EXTRA').split(':').filter(Boolean);
|
|
36
|
+
export const sseMax = () => Number(env('KB_SSE_MAX')) || 20;
|
|
37
|
+
export const allowedHosts = () => env('KB_ALLOWED_HOSTS').split(',').map((s) => s.trim()).filter(Boolean);
|
|
38
|
+
export const langOverride = () => env('KB_LANG');
|
|
39
|
+
export const ghOwnerEnv = () => env('KB_GH_OWNER');
|
|
40
|
+
export const ghRepoEnv = () => env('KB_GH_REPO');
|