local-kanban 1.13.0 → 1.14.1

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 CHANGED
@@ -8,6 +8,57 @@ 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.1]
12
+
13
+ ### Changed
14
+
15
+ - **An `ask` task goes back to To do when the discussion ends without the work.** The skill told
16
+ Claude to ask and wait, but not what comes after: a task that was only discussed went to Review,
17
+ where a bare proposal reads as delivered, or stayed in Prep, a column the human cannot move a card
18
+ out of. Now Claude leaves the outcome of the discussion as a note and moves the card back to To do;
19
+ Review is only for work that was actually done.
20
+
21
+ ## [1.14.0]
22
+
23
+ ### Added
24
+
25
+ - **`kb` shows how tasks are linked, and can make a sub-task.** The board keeps parents, sub-tasks and
26
+ related tasks, but `kb show` and `kb take` printed none of them, so Claude could not know that two
27
+ tasks were meant to go together. Both commands now print one line per link: `↑ parent`,
28
+ `↓ subtask`, `↔ related`, with the key, title and status. `kb link A B child` makes B a sub-task
29
+ of A, `kb link A B parent` makes it A's parent; without a third word the link is related, as before.
30
+ - **`kb info` prints the project's description and its documents.** The description is where owners
31
+ keep the context for work, and it never reached Claude. A `docs:` line now lists the documents to
32
+ read before a task (`CLAUDE.md`, `README.md`, `ARCHITECTURE.md` …) and says so plainly when there is
33
+ no `CLAUDE.md` or the project folder is not on this machine.
34
+ - **`kb check <key> <n>` / `kb uncheck` tick checklist items from the CLI.** Claude could read the
35
+ checklist but not tick it, so at acceptance every box was empty and the human could not tell what
36
+ of the list was done. The item is addressed by its number in the order `kb show` prints it.
37
+ - **`KB_GH_BIN` points the board at `gh` when it lives somewhere unusual.** The board looks for `gh`
38
+ in the usual install locations (Homebrew, `/usr/bin`, `~/.local/bin`) and then on `PATH`; a copy
39
+ anywhere else can now be named explicitly.
40
+
41
+ ### Fixed
42
+
43
+ - **`kb` no longer loses what the human wrote in a card.** `kb take` printed only the latest note
44
+ from the human, so a send-back followed by a second note lost the first; `kb show` printed the last
45
+ five comments of any author, and Claude's own notes plus the commit stats pushed the human's note
46
+ out without a word. Both now print every note the human left since Claude's last report, each
47
+ comment carries its date and time, and anything left out is counted ("… 3 more comments not
48
+ printed") instead of dropped silently.
49
+ - **A commit stat is no longer printed as the human speaking.** `kb show` marked everything that was
50
+ not Claude's with 👤, including the commit stat the board attaches on its own. It is marked ⎇ now.
51
+ - **`kb take` tells a blocked To do from an empty one.** When every card in To do was blocked it
52
+ answered a bare `empty`, and Claude reported an empty column while the human could see cards in
53
+ it. It now says `empty (2 blocked)`; `kb next` does the same.
54
+ - **`kb take` and `kb show` print the issue the same way** — `issue: #42 <link>`. One printed the
55
+ number and the other the link.
56
+ - **The checklist now reaches Claude.** A card's checklist is shown on the board right under the
57
+ description, but `kb show` and `kb take` printed everything about the task except it — the CLI is
58
+ the only channel Claude has, so the items simply did not exist for it: a card with three checklist
59
+ items and an empty description arrived as a bare title. Both commands now print the items as
60
+ `- [ ]` / `- [x]` lines, in the board's order, right after the description.
61
+
11
62
  ## [1.13.0]
12
63
 
13
64
  ### 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]) : 'empty');
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 last
98
- // comment from the human (notes from a send-back). Writing code → kb mv <key> doing.
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('empty');
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
- if (t.gh_issue_number) console.log(`issue: ${t.gh_issue_number}`);
109
- const atts = await api('GET', `/api/tasks/${t.id}/attachments`);
110
- for (const a of atts) console.log(`📎 screenshot (open it with Read): ${a.path}`);
111
- const cs = await api('GET', `/api/tasks/${t.id}/comments`);
112
- const lastHuman = cs.filter((c) => c.author === 'me' && c.body).pop();
113
- if (lastHuman) console.log(`> 👤 ${lastHuman.body}`);
114
- // comments often carry corrections — print the paths of the latest images so they get opened
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
- if (t.gh_issue_url) console.log(`issue: ${t.gh_issue_url}`);
135
- for (const a of (t.attachments || [])) console.log(`📎 screenshot (open it with Read): ${a.path}`);
136
- const cs = await api('GET', `/api/tasks/${id}/comments`);
137
- for (const c of cs.slice(-5)) {
138
- if (c.body) console.log(`> ${c.author === 'claude' ? '🤖' : '👤'} ${c.body}`);
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> (link two tasks)');
228
- const links = await api('POST', `/api/tasks/${id}/links`, { key: other });
229
- console.log(`${id} ↔ ${links.map((l) => l.key).join(', ') || '—'}`);
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(`${id} ↔ ${links.map((l) => l.key).join(', ') || '—'}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-kanban",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "license": "MIT",
5
5
  "author": "Dannylamao",
6
6
  "repository": {
@@ -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 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.
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 + latest comments
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 link <key> <key> # link two tasks (the board draws a line) · kb unlink to unlink
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. `> 👤 …` is a note from a send-back, act on it first.
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`. Before any code you MUST read: (a) the project's `CLAUDE.md` if it has one — **and if
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,9 +144,12 @@ 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
- 7. At the end, a one-line summary: how many in review, how many blocked.
151
+ 7. At the end, a one-line summary: how many in review, how many went back to To do after a
152
+ discussion (`ask`), how many blocked.
132
153
 
133
154
  ## Grooming the backlog
134
155
 
@@ -158,12 +179,25 @@ One line of summary at the end.
158
179
  study the task in `prep`, then ask your clarifying questions in chat — how you understood the
159
180
  task, what you intend to change and where — EVEN IF everything looks obvious, and WAIT for an
160
181
  answer. Do not move to `doing` or write code until the human replies.
182
+ **How an `ask` task ends.** Agreed and done → the normal loop up to `review`. The turn passes to
183
+ the human with the work NOT done — you hand it back with questions, you proposed options and the
184
+ choice is theirs, they postponed it → `kb note <key> "…"` with the outcome of the discussion (what
185
+ you asked, what the human answered, what was agreed, what is still open) and **`kb mv <key>
186
+ todo`**; when they answer in chat, `kb mv <key> prep` and carry on. Not `review`: that column holds
187
+ finished work, and a bare proposal there reads as delivered. Not left in `prep` either: only Claude
188
+ moves that column, so the human cannot pull the card out, and your turn with the questions may be
189
+ the last one in the session. The note is mandatory — the chat does not survive between sessions,
190
+ and the next one would start the discussion from scratch.
161
191
  - **You found an unrelated problem along the way** → do not fix it silently. Create a task
162
192
  (`kb add <proj> "..." -d "..."`) and ask in chat: do it now, defer it, or is it for the human.
163
193
  - **Related tasks solved together** → take both at once (both in doing), fix them in one pass and
164
- link them with `kb link`.
194
+ link them with `kb link`. `kb show`/`kb take` print a task's links as `↑ parent KEY title
195
+ (status)`, `↓ subtask …`, `↔ related …` — look at them before you start: a linked task in To do
196
+ is usually meant to go together with this one, and a parent explains why the task exists.
165
197
  - **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.
198
+ `kb note <key> "..."`, then `kb mv <key> review`. **With the `ask` label it goes back to `todo`
199
+ instead** (see above): there the discussion is a step before the work, not the work itself. You
200
+ write no code and do not move it to done.
167
201
  - A "check whether the service is alive" task → `kb svc <proj>` (one line per service, no ssh).
168
202
  - Work time is measured for you: doing/deploy runs the timer, review/done stops it.
169
203
  - Verifying the result: text and config changes — curl/grep (cheap). Compiled projects — **a build**
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'));
@@ -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 GH = GH_CANDIDATES.find((p) => existsSync(p)) || 'gh';
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(GH, args, { timeout, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
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));