local-kanban 1.13.0 → 1.14.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/CHANGELOG.md +41 -0
- package/README.md +1 -0
- package/README.ru.md +1 -0
- package/bin/kb +129 -25
- package/package.json +1 -1
- package/skills/kanban/SKILL.md +35 -13
- package/src/config.js +1 -0
- package/src/sync/github.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,47 @@ version it goes out as and opens an empty one again. Released sections are histo
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.14.0]
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`kb` shows how tasks are linked, and can make a sub-task.** The board keeps parents, sub-tasks and
|
|
16
|
+
related tasks, but `kb show` and `kb take` printed none of them, so Claude could not know that two
|
|
17
|
+
tasks were meant to go together. Both commands now print one line per link: `↑ parent`,
|
|
18
|
+
`↓ subtask`, `↔ related`, with the key, title and status. `kb link A B child` makes B a sub-task
|
|
19
|
+
of A, `kb link A B parent` makes it A's parent; without a third word the link is related, as before.
|
|
20
|
+
- **`kb info` prints the project's description and its documents.** The description is where owners
|
|
21
|
+
keep the context for work, and it never reached Claude. A `docs:` line now lists the documents to
|
|
22
|
+
read before a task (`CLAUDE.md`, `README.md`, `ARCHITECTURE.md` …) and says so plainly when there is
|
|
23
|
+
no `CLAUDE.md` or the project folder is not on this machine.
|
|
24
|
+
- **`kb check <key> <n>` / `kb uncheck` tick checklist items from the CLI.** Claude could read the
|
|
25
|
+
checklist but not tick it, so at acceptance every box was empty and the human could not tell what
|
|
26
|
+
of the list was done. The item is addressed by its number in the order `kb show` prints it.
|
|
27
|
+
- **`KB_GH_BIN` points the board at `gh` when it lives somewhere unusual.** The board looks for `gh`
|
|
28
|
+
in the usual install locations (Homebrew, `/usr/bin`, `~/.local/bin`) and then on `PATH`; a copy
|
|
29
|
+
anywhere else can now be named explicitly.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`kb` no longer loses what the human wrote in a card.** `kb take` printed only the latest note
|
|
34
|
+
from the human, so a send-back followed by a second note lost the first; `kb show` printed the last
|
|
35
|
+
five comments of any author, and Claude's own notes plus the commit stats pushed the human's note
|
|
36
|
+
out without a word. Both now print every note the human left since Claude's last report, each
|
|
37
|
+
comment carries its date and time, and anything left out is counted ("… 3 more comments not
|
|
38
|
+
printed") instead of dropped silently.
|
|
39
|
+
- **A commit stat is no longer printed as the human speaking.** `kb show` marked everything that was
|
|
40
|
+
not Claude's with 👤, including the commit stat the board attaches on its own. It is marked ⎇ now.
|
|
41
|
+
- **`kb take` tells a blocked To do from an empty one.** When every card in To do was blocked it
|
|
42
|
+
answered a bare `empty`, and Claude reported an empty column while the human could see cards in
|
|
43
|
+
it. It now says `empty (2 blocked)`; `kb next` does the same.
|
|
44
|
+
- **`kb take` and `kb show` print the issue the same way** — `issue: #42 <link>`. One printed the
|
|
45
|
+
number and the other the link.
|
|
46
|
+
- **The checklist now reaches Claude.** A card's checklist is shown on the board right under the
|
|
47
|
+
description, but `kb show` and `kb take` printed everything about the task except it — the CLI is
|
|
48
|
+
the only channel Claude has, so the items simply did not exist for it: a card with three checklist
|
|
49
|
+
items and an empty description arrived as a bare title. Both commands now print the items as
|
|
50
|
+
`- [ ]` / `- [x]` lines, in the board's order, right after the description.
|
|
51
|
+
|
|
11
52
|
## [1.13.0]
|
|
12
53
|
|
|
13
54
|
### Added
|
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ Releases live in `main` (stable, tagged); day-to-day development happens in `dev
|
|
|
142
142
|
| `KB_PANEL_INFO` | — | optional path to an external panel's info.json for category sync |
|
|
143
143
|
| `KB_SSE_MAX` | `20` | cap on concurrent SSE connections |
|
|
144
144
|
| `KB_SKILLS_EXTRA` | — | extra skill root directories, `:`-separated |
|
|
145
|
+
| `KB_GH_BIN` | — | path to the `gh` binary when it is not in a usual place (Homebrew, `/usr/bin`, `~/.local/bin`) |
|
|
145
146
|
|
|
146
147
|
The legacy names `PANEL_URL` and `PANEL_INFO` (without the prefix) still work as a fallback.
|
|
147
148
|
|
package/README.ru.md
CHANGED
|
@@ -141,6 +141,7 @@ npm run update # git pull + npm install + pm2 restart (если примени
|
|
|
141
141
|
| `KB_PANEL_INFO` | — | опциональный info.json внешней панели для синка категорий |
|
|
142
142
|
| `KB_SSE_MAX` | `20` | потолок одновременных SSE-соединений |
|
|
143
143
|
| `KB_SKILLS_EXTRA` | — | дополнительные корни скиллов через `:` |
|
|
144
|
+
| `KB_GH_BIN` | — | путь к `gh`, если он лежит не в обычном месте (Homebrew, `/usr/bin`, `~/.local/bin`) |
|
|
144
145
|
|
|
145
146
|
Старые имена `PANEL_URL` и `PANEL_INFO` (без префикса) продолжают работать как запасные.
|
|
146
147
|
|
package/bin/kb
CHANGED
|
@@ -63,8 +63,96 @@ function taskLine(t) {
|
|
|
63
63
|
return `${t.key} ${t.status}${flags ? ' ' + flags : ''} ${t.title}${labels}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// The checklist is part of the task's spec: the board shows it right under the description, and
|
|
67
|
+
// this CLI is the only channel Claude has — an item that is not printed here does not exist for
|
|
68
|
+
// Claude. Markdown task-list lines, `- [ ]` open and `- [x]` done, in the board's order.
|
|
69
|
+
function printChecklist(t) {
|
|
70
|
+
for (const c of (t.checklist || [])) console.log(`- [${c.done ? 'x' : ' '}] ${c.text}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// here, and the skill's rule "related tasks solved together are taken together" cannot be followed
|
|
74
|
+
// without knowing they are related. `rel` is relative to this task: 'parent' — the other task is
|
|
75
|
+
// this one's parent, 'child' — the other task is a sub-task of this one, 'related' — symmetric.
|
|
76
|
+
const REL = { parent: ['↑', 'parent'], child: ['↓', 'subtask'], related: ['↔', 'related'] };
|
|
77
|
+
function printLinks(t) {
|
|
78
|
+
for (const l of (t.links || [])) {
|
|
79
|
+
const [arrow, word] = REL[l.rel] || REL.related;
|
|
80
|
+
console.log(`${arrow} ${word} ${l.key} ${l.title} (${l.status})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const linksLine = (id, links) => `${id} ${links.map((l) => (REL[l.rel] || REL.related)[0] + l.key).join(' ') || '—'}`;
|
|
84
|
+
|
|
85
|
+
// the skill had to explain two formats. The number goes into commit messages, the link is for eyes.
|
|
86
|
+
function printIssue(t) {
|
|
87
|
+
if (t.gh_issue_number) console.log(`issue: #${t.gh_issue_number}${t.gh_issue_url ? ' ' + t.gh_issue_url : ''}`);
|
|
88
|
+
else if (t.gh_issue_url) console.log(`issue: ${t.gh_issue_url}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// sees cards in the column, and a bare "empty" made Claude report that there was nothing there.
|
|
92
|
+
function emptyLine(todo) {
|
|
93
|
+
const blocked = todo.filter((t) => t.blocked).length;
|
|
94
|
+
return blocked ? `empty (${blocked} blocked)` : 'empty';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function printAttachments(t) {
|
|
98
|
+
for (const a of (t.attachments || [])) console.log(`📎 screenshot (open it with Read): ${a.path}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Comments. Three authors: the human ('me'), Claude, and 'git' — the commit stat the board attaches
|
|
102
|
+
// by itself on the move to review. Each gets its own mark: a commit printed as 👤 read as the human
|
|
103
|
+
const WHO = { claude: '🤖', git: '⎇' };
|
|
104
|
+
|
|
105
|
+
// The database keeps UTC ('YYYY-MM-DD HH:MM:SS'); print local time, to the minute — enough to tell
|
|
106
|
+
function when(c) {
|
|
107
|
+
const d = new Date(String(c.created_at || '').replace(' ', 'T') + 'Z');
|
|
108
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
109
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
110
|
+
return `[${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}] `;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printComment(c) {
|
|
114
|
+
const at = when(c);
|
|
115
|
+
if (c.body) console.log(`> ${WHO[c.author] || '👤'} ${at}${c.author === 'git' ? 'commit ' : ''}${c.body}`);
|
|
116
|
+
if (c.image_path) console.log(`📎 image from a comment ${at}(open it with Read): ${c.image_path}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Claude's last report — not just the latest one (a send-back plus a follow-up note lost the first)
|
|
120
|
+
// and not whatever happens to fit a window of the last few (Claude's own notes and commit stats
|
|
121
|
+
// pushed the human out of it). Those are always printed; `window` adds the last N comments of any
|
|
122
|
+
// author on top, and the latest images stay too — they are usually the correction itself. When the
|
|
123
|
+
// human has said nothing since the last report, the last thing they did say is printed, dated, so
|
|
124
|
+
// it can be told apart from a fresh one. Whatever is left out is counted, never dropped silently.
|
|
125
|
+
function printComments(cs, window) {
|
|
126
|
+
const keep = new Set();
|
|
127
|
+
let lastReport = -1;
|
|
128
|
+
cs.forEach((c, i) => { if (c.author === 'claude') lastReport = i; });
|
|
129
|
+
const human = (c) => c.author !== 'claude' && c.author !== 'git';
|
|
130
|
+
cs.forEach((c, i) => { if (i > lastReport && human(c)) keep.add(i); });
|
|
131
|
+
if (!keep.size) {
|
|
132
|
+
const i = cs.findLastIndex((c) => human(c) && c.body);
|
|
133
|
+
if (i >= 0) keep.add(i);
|
|
134
|
+
}
|
|
135
|
+
for (let i = Math.max(0, cs.length - window); i < cs.length; i++) keep.add(i);
|
|
136
|
+
cs.map((c, i) => (c.image_path ? i : -1)).filter((i) => i >= 0).slice(-3).forEach((i) => keep.add(i));
|
|
137
|
+
const hidden = cs.length - keep.size;
|
|
138
|
+
if (hidden > 0) console.log(`… ${hidden} more comment${hidden === 1 ? '' : 's'} not printed — the board has them all`);
|
|
139
|
+
cs.forEach((c, i) => { if (keep.has(i)) printComment(c); });
|
|
140
|
+
}
|
|
141
|
+
|
|
66
142
|
const [cmd, ...args] = process.argv.slice(2);
|
|
67
143
|
|
|
144
|
+
async function tick(done) {
|
|
145
|
+
const [id, n] = args;
|
|
146
|
+
const no = Number(n);
|
|
147
|
+
if (!id || !Number.isInteger(no) || no < 1) die(`kb ${done ? 'check' : 'uncheck'} <key> <n> (n = the item number, from 1, in the order kb show prints them)`);
|
|
148
|
+
const t = await api('GET', `/api/tasks/${id}`);
|
|
149
|
+
const items = t.checklist || [];
|
|
150
|
+
if (no > items.length) die(`kb: ${t.key} has ${items.length ? `only ${items.length} checklist item${items.length === 1 ? '' : 's'}` : 'no checklist'}`);
|
|
151
|
+
const item = await api('PATCH', `/api/tasks/${id}/checklist/${items[no - 1].id}`, { done });
|
|
152
|
+
const open = items.filter((c, i) => (i === no - 1 ? !item.done : !c.done)).length;
|
|
153
|
+
console.log(`${t.key} - [${item.done ? 'x' : ' '}] ${item.text} (${open} open)`);
|
|
154
|
+
}
|
|
155
|
+
|
|
68
156
|
const commands = {
|
|
69
157
|
async p() {
|
|
70
158
|
const ps = await api('GET', '/api/projects');
|
|
@@ -91,28 +179,27 @@ const commands = {
|
|
|
91
179
|
if (!proj) die('kb next <project>');
|
|
92
180
|
const ts = await api('GET', `/api/tasks?${new URLSearchParams({ project: proj, status: 'todo' })}`);
|
|
93
181
|
const free = ts.filter((t) => !t.blocked);
|
|
94
|
-
console.log(free.length ? taskLine(free[0]) :
|
|
182
|
+
console.log(free.length ? taskLine(free[0]) : emptyLine(ts));
|
|
95
183
|
},
|
|
96
184
|
|
|
97
|
-
// take the next task and put it straight into prep (study before code); prints the
|
|
98
|
-
//
|
|
185
|
+
// take the next task and put it straight into prep (study before code); prints what the human
|
|
186
|
+
// wrote since Claude's last report (notes from a send-back). Writing code → kb mv <key> doing.
|
|
99
187
|
async take() {
|
|
100
188
|
const [proj] = args;
|
|
101
189
|
if (!proj) die('kb take <project>');
|
|
102
190
|
const ts = await api('GET', `/api/tasks?${new URLSearchParams({ project: proj, status: 'todo' })}`);
|
|
103
191
|
const free = ts.filter((t) => !t.blocked);
|
|
104
|
-
if (!free.length) return console.log(
|
|
192
|
+
if (!free.length) return console.log(emptyLine(ts));
|
|
105
193
|
const t = await api('PATCH', `/api/tasks/${free[0].id}`, { status: 'prep' });
|
|
106
194
|
console.log(taskLine({ ...t, labels: t.labels }));
|
|
107
195
|
if (t.description) console.log(t.description);
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const c of cs.filter((c) => c.image_path).slice(-3)) console.log(`📎 image from a comment (open it with Read): ${c.image_path}`);
|
|
196
|
+
// PATCH answers with the bare row; the checklist and the attachments live on the full card
|
|
197
|
+
const full = await api('GET', `/api/tasks/${t.id}`);
|
|
198
|
+
printChecklist(full);
|
|
199
|
+
printLinks(full);
|
|
200
|
+
printIssue(full);
|
|
201
|
+
printAttachments(full);
|
|
202
|
+
printComments(await api('GET', `/api/tasks/${t.id}/comments`), 0);
|
|
116
203
|
},
|
|
117
204
|
|
|
118
205
|
// leave a note and move to review in one command
|
|
@@ -131,13 +218,11 @@ const commands = {
|
|
|
131
218
|
console.log(`${t.key} ${t.project} ${t.status}${t.blocked ? ` blocked: ${t.blocked_reason || ''}` : ''}${PRI[t.priority] ? ' ' + PRI[t.priority] : ''}${t.labels.length ? ' [' + t.labels.join(',') + ']' : ''}`);
|
|
132
219
|
console.log(t.title);
|
|
133
220
|
if (t.description) console.log(t.description);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (c.image_path) console.log(`📎 image from a comment (open it with Read): ${c.image_path}`);
|
|
140
|
-
}
|
|
221
|
+
printChecklist(t);
|
|
222
|
+
printLinks(t);
|
|
223
|
+
printIssue(t);
|
|
224
|
+
printAttachments(t);
|
|
225
|
+
printComments(await api('GET', `/api/tasks/${id}/comments`), 5);
|
|
141
226
|
},
|
|
142
227
|
|
|
143
228
|
async add() {
|
|
@@ -222,20 +307,28 @@ const commands = {
|
|
|
222
307
|
console.log(`${t.key} 📝`);
|
|
223
308
|
},
|
|
224
309
|
|
|
310
|
+
// kb link A B [child|parent|related] — `child`: B becomes a sub-task of A, `parent`: B becomes
|
|
311
|
+
// A's parent. Without a third word the link is plain `related`, as it always was. A pair holds
|
|
312
|
+
// one link, so linking again with another word re-types it.
|
|
225
313
|
async link() {
|
|
226
|
-
const [id, other] = args;
|
|
227
|
-
if (!id || !other) die('kb link <key> <key> (
|
|
228
|
-
const links = await api('POST', `/api/tasks/${id}/links`, { key: other });
|
|
229
|
-
console.log(
|
|
314
|
+
const [id, other, rel = 'related'] = args;
|
|
315
|
+
if (!id || !other || !REL[rel]) die('kb link <key> <key> [child|parent|related] (child: the second task becomes a sub-task of the first)');
|
|
316
|
+
const links = await api('POST', `/api/tasks/${id}/links`, { key: other, rel });
|
|
317
|
+
console.log(linksLine(id, links));
|
|
230
318
|
},
|
|
231
319
|
|
|
232
320
|
async unlink() {
|
|
233
321
|
const [id, other] = args;
|
|
234
322
|
if (!id || !other) die('kb unlink <key> <key>');
|
|
235
323
|
const links = await api('DELETE', `/api/tasks/${id}/links/${other}`);
|
|
236
|
-
console.log(
|
|
324
|
+
console.log(linksLine(id, links));
|
|
237
325
|
},
|
|
238
326
|
|
|
327
|
+
// empty and could not tell what of the list was done. The item is addressed by its number — the
|
|
328
|
+
// position in which show/take print it (the board's order), counting from 1.
|
|
329
|
+
async check() { await tick(true); },
|
|
330
|
+
async uncheck() { await tick(false); },
|
|
331
|
+
|
|
239
332
|
async block() {
|
|
240
333
|
const [id, reason] = args;
|
|
241
334
|
if (!id) die('kb block <key> ["reason"]');
|
|
@@ -268,6 +361,17 @@ const commands = {
|
|
|
268
361
|
const services = parsePm2Services(p.pm2_services);
|
|
269
362
|
if (services.length) out.push(`pm2: ${services.join(', ')}`);
|
|
270
363
|
if (p.domain) out.push(`domain: ${p.domain}`);
|
|
364
|
+
// CLAUDE.md", "deployed by skill X", DNS facts) — info printed everything but it
|
|
365
|
+
if (p.description) out.push(`description: ${p.description}`);
|
|
366
|
+
// page; without this line Claude went looking by itself and sometimes missed. A missing
|
|
367
|
+
// CLAUDE.md is said out loud — it is a signal (the task starts without architecture notes).
|
|
368
|
+
if (p.path) {
|
|
369
|
+
const d = await api('GET', `/api/projects/${encodeURIComponent(p.slug)}/docs`);
|
|
370
|
+
const found = (d.docs || []).filter((x) => x.exists).map((x) => x.name);
|
|
371
|
+
if (!d.path_exists) out.push('docs: — the project folder does not exist on this machine');
|
|
372
|
+
else if (!found.includes('CLAUDE.md')) out.push(`docs: no CLAUDE.md${found.length ? ` — read ${found.join(', ')} instead` : ', and no README.md / ARCHITECTURE.md either'}`);
|
|
373
|
+
else out.push(`docs: ${found.join(', ')}`);
|
|
374
|
+
}
|
|
271
375
|
console.log(out.join('\n'));
|
|
272
376
|
},
|
|
273
377
|
|
|
@@ -297,7 +401,7 @@ const commands = {
|
|
|
297
401
|
};
|
|
298
402
|
|
|
299
403
|
if (!cmd || !commands[cmd]) {
|
|
300
|
-
die('kb p|ls|next|take|show|add|mv|review|done|cancel|pri|lbl|desc|note|link|unlink|block|unblock|rm|info|svc|sync|retry');
|
|
404
|
+
die('kb p|ls|next|take|show|add|mv|review|done|cancel|pri|lbl|desc|note|link|unlink|check|uncheck|block|unblock|rm|info|svc|sync|retry');
|
|
301
405
|
}
|
|
302
406
|
commands[cmd]().catch((e) => {
|
|
303
407
|
// die() inside a command already exited the process — only real exceptions reach here.
|
package/package.json
CHANGED
package/skills/kanban/SKILL.md
CHANGED
|
@@ -15,10 +15,14 @@ Statuses: `backlog` → `todo` → `prep` → `doing` → `deploy` → `review`
|
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
kb take <proj> # take the next task from todo → prep; prints the description,
|
|
18
|
-
# the
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# the checklist as "- [ ] item" / "- [x] item" lines (that IS the
|
|
19
|
+
# spec — do every open item, and verify them one by one before
|
|
20
|
+
# review), the issue as "issue: #N <link>" (#N goes into commit
|
|
21
|
+
# messages; kb show prints the same line), EVERY note the
|
|
22
|
+
# human left since your last report, dated — "> 👤 [2026-09-02 11:30]
|
|
23
|
+
# …" (if the task was sent back, act on them!) and the paths of
|
|
24
|
+
# attached screenshots as "📎 <path>" — OPEN those with Read, they
|
|
25
|
+
# are context for the task.
|
|
22
26
|
# started writing code → kb mv <key> doing.
|
|
23
27
|
kb review <key> "note" # leave a note and move to review. NOT atomic: the note is stored
|
|
24
28
|
# EVEN IF the move is rejected (backlog and todo cannot go straight
|
|
@@ -27,7 +31,11 @@ kb review <key> "note" # leave a note and move to review. NOT atomic: the
|
|
|
27
31
|
# never went through doing.
|
|
28
32
|
kb p # projects: slug (PREFIX) b0 t2 pp0 d1 dp0 r1 ✓5 !1 (pp = prep)
|
|
29
33
|
kb ls <proj> [-s st] [-a] # tasks (without -a, backlog/done/cancelled are hidden)
|
|
30
|
-
kb show <key> # details + issue +
|
|
34
|
+
kb show <key> # details + checklist + issue + comments: the last five plus every
|
|
35
|
+
# human note since your last report. Marks: 🤖 yours, 👤 the
|
|
36
|
+
# human's, ⎇ a commit stat the board attached by itself (not the
|
|
37
|
+
# human speaking). "… N more comments not printed" = the rest are
|
|
38
|
+
# on the board — nothing is dropped silently.
|
|
31
39
|
kb add <proj> "title" [-d "desc"] [-p high|med|low] [-s todo] # goes to backlog by default!
|
|
32
40
|
kb mv <key> todo|prep|doing|deploy|review|done|cancelled
|
|
33
41
|
kb done <key> · kb cancel <key> · kb pri <key> high|med|low|none
|
|
@@ -40,9 +48,16 @@ kb lbl <key> +bug -ui # labels: bug docs duplicate enhancement ui featur
|
|
|
40
48
|
# label goes into the LABELS palette first, and into this file.
|
|
41
49
|
kb desc <key> "description" # set or overwrite a task description (used when grooming the backlog)
|
|
42
50
|
kb note <key> "text" # a standalone comment (rarely needed — kb review covers it)
|
|
43
|
-
kb
|
|
51
|
+
kb check <key> <n> # tick checklist item n (from 1, in the order show/take print them)
|
|
52
|
+
# · kb uncheck <key> <n> clears it. Answers "- [x] item (2 open)"
|
|
53
|
+
kb link <key> <key> [child|parent] # link two tasks (the board draws a line); `child` makes the
|
|
54
|
+
# second task a sub-task of the first, `parent` its parent, no word
|
|
55
|
+
# = plain related · kb unlink to unlink
|
|
44
56
|
kb block <key> "why" · kb unblock <key>
|
|
45
|
-
kb info <proj> # path, deploy_skill, server, pm2, domain
|
|
57
|
+
kb info <proj> # path, deploy_skill, server, pm2, domain, the project's description
|
|
58
|
+
# (the owner's context for work — read it) and "docs: CLAUDE.md,
|
|
59
|
+
# README.md" — the documents to read before the task; "docs: no
|
|
60
|
+
# CLAUDE.md — read README.md instead" is a signal, not an empty line
|
|
46
61
|
kb svc <proj> # live status of the project's services (local pm2 or your own PANEL_URL)
|
|
47
62
|
kb sync # state of the sync queue (or "sync is off" in local-only mode)
|
|
48
63
|
```
|
|
@@ -92,15 +107,18 @@ Claude takes a task as far as *review* and stops. Only the user moves anything t
|
|
|
92
107
|
**When the prompt lists specific keys** (the 📋 button on the "To do" column produces them) those
|
|
93
108
|
are SPECIFIC tasks, not a queue. Take each one with `kb show <key>` (NOT `kb take`, which pulls
|
|
94
109
|
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
|
|
110
|
+
`kb show` does not change the status. If `kb show` printed `> 👤 [date] Send back: …`, the task was
|
|
111
|
+
returned — the date tells a fresh send-back from an old one you already answered: open the `📎` screenshots with Read and act on that note FIRST. Then follow steps 2–6
|
|
97
112
|
for each. Use `kb take X` only when asked to "work through project X" without specific keys.
|
|
98
113
|
|
|
99
114
|
1. `kb take X` — take a task (lands in prep; description and issue are already in the output).
|
|
100
|
-
"empty" → report it and stop.
|
|
115
|
+
"empty" → report it and stop. "empty (N blocked)" is NOT an empty column: every card in To do
|
|
116
|
+
is blocked — list them (`kb ls X -s todo`) and report that, not "nothing to do". `> 👤 …` lines are what the human wrote since your last report
|
|
117
|
+
(a send-back and its follow-ups) — act on all of them first.
|
|
101
118
|
`📎 <path>` — open the screenshot with Read.
|
|
102
119
|
2. **HARD RULE — starting a task (never skip, even for a one-line change).** `kb info X` → go to
|
|
103
|
-
`path
|
|
120
|
+
`path`; its `description:` and `docs:` lines say what the owner wants you to know and which
|
|
121
|
+
documents the project has. Before any code you MUST read: (a) the project's `CLAUDE.md` if it has one — **and if
|
|
104
122
|
it does not, find where the project keeps its architecture or research document (`README.md`,
|
|
105
123
|
`EXPERIMENTS.md`, `docs/`) and read that; apply the "doc in the same commit" rule to it**;
|
|
106
124
|
(b) the deploy skill at `deploy_skill_path` from `kb info`, if one is set. The matching commit
|
|
@@ -126,7 +144,9 @@ for each. Use `kb take X` only when asked to "work through project X" without sp
|
|
|
126
144
|
The generic `deploy` skill ships with the board (`skills/deploy/`) and works purely off
|
|
127
145
|
`kb info` data. Production commands (ssh/rsync) will ask for confirmation — that is expected.
|
|
128
146
|
5. `kb review <key> "<two plain sentences: 1) what the problem was 2) how you solved it>"` → take
|
|
129
|
-
the next one.
|
|
147
|
+
the next one. **If the task has a checklist, tick each item as you finish it (`kb check <key>
|
|
148
|
+
<n>`).** Before review every item is either ticked or named in the note as not done — the human
|
|
149
|
+
accepts the task by those boxes, and an unticked done item reads as undone.
|
|
130
150
|
6. Stuck, or the decision is the human's → `kb block <key> "reason"` and move on.
|
|
131
151
|
7. At the end, a one-line summary: how many in review, how many blocked.
|
|
132
152
|
|
|
@@ -161,7 +181,9 @@ One line of summary at the end.
|
|
|
161
181
|
- **You found an unrelated problem along the way** → do not fix it silently. Create a task
|
|
162
182
|
(`kb add <proj> "..." -d "..."`) and ask in chat: do it now, defer it, or is it for the human.
|
|
163
183
|
- **Related tasks solved together** → take both at once (both in doing), fix them in one pass and
|
|
164
|
-
link them with `kb link`.
|
|
184
|
+
link them with `kb link`. `kb show`/`kb take` print a task's links as `↑ parent KEY title
|
|
185
|
+
(status)`, `↓ subtask …`, `↔ related …` — look at them before you start: a linked task in To do
|
|
186
|
+
is usually meant to go together with this one, and a parent explains why the task exists.
|
|
165
187
|
- **A discussion or question task** (no code): study it in `prep`, put the full answer in
|
|
166
188
|
`kb note <key> "..."`, then `kb mv <key> review`. You write no code and do not move it to done.
|
|
167
189
|
- A "check whether the service is alive" task → `kb svc <proj>` (one line per service, no ssh).
|
package/src/config.js
CHANGED
|
@@ -38,3 +38,4 @@ export const allowedHosts = () => env('KB_ALLOWED_HOSTS').split(',').map((s) =>
|
|
|
38
38
|
export const langOverride = () => env('KB_LANG');
|
|
39
39
|
export const ghOwnerEnv = () => env('KB_GH_OWNER');
|
|
40
40
|
export const ghRepoEnv = () => env('KB_GH_REPO');
|
|
41
|
+
export const ghBinEnv = () => expandPath(env('KB_GH_BIN'));
|
package/src/sync/github.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { db, ghOwner, ghRepo, uiLang } from '../db.js';
|
|
4
|
+
import { ghBinEnv } from '../config.js';
|
|
4
5
|
const GH_CANDIDATES = [
|
|
5
6
|
'/opt/homebrew/bin/gh', '/usr/local/bin/gh', '/usr/bin/gh',
|
|
6
7
|
'/home/linuxbrew/.linuxbrew/bin/gh', `${process.env.HOME}/.local/bin/gh`,
|
|
7
8
|
];
|
|
8
|
-
const
|
|
9
|
+
const GH_FOUND = GH_CANDIDATES.find((p) => existsSync(p)) || 'gh';
|
|
10
|
+
const ghBin = () => ghBinEnv() || GH_FOUND;
|
|
9
11
|
|
|
10
12
|
const PERMANENT = [
|
|
11
13
|
/validation failed/i,
|
|
@@ -19,7 +21,7 @@ export function isPermanent(stderr) {
|
|
|
19
21
|
|
|
20
22
|
export function gh(args, { timeout = 60000 } = {}) {
|
|
21
23
|
return new Promise((resolve, reject) => {
|
|
22
|
-
execFile(
|
|
24
|
+
execFile(ghBin(), args, { timeout, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
23
25
|
if (err) {
|
|
24
26
|
const e = new Error((stderr || err.message || '').trim().slice(0, 500));
|
|
25
27
|
e.permanent = isPermanent(String(stderr));
|