kadence 0.4.1 → 0.5.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 CHANGED
@@ -1,5 +1,174 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.5.0] — 2026-09-16
6
+
7
+ **The second author.** kadence was built for a team, and until now its first
8
+ run spoke to one person with a sprint. This release is the path the second
9
+ person walks: their agent learns to write the why, a machine without kadence
10
+ gets an install hint instead of an error, and the first screens talk about the
11
+ work and its decisions rather than points. Reports are still here; they are
12
+ just no longer the first thing anyone meets.
13
+
14
+ ### Added — the teammate path
15
+
16
+ - **The section `init` writes into `AGENTS.md` and `CLAUDE.md` teaches writes,
17
+ not only reads.** `task claim`, `note "…" --task`, and `decision add "…" --why`
18
+ sit beside the read commands, plus one line for an agent on a machine where
19
+ `kadence` is not on PATH: ask the human to install it; `npx` needs the network
20
+ and that is their call. Still 23 lines and 1.2 KB, under a test of 25 lines and
21
+ 2 KB; an older section is replaced in place by its version marker.
22
+ - **The `SessionStart` hook survives a machine without kadence.** It runs
23
+ `kadence prime` when the binary is there and otherwise prints one line asking
24
+ the human to install it, exiting 0 (DEC-12: no `npx`, because a hook written by
25
+ `init` must not download code on a teammate's machine). `init --hooks` upgrades
26
+ the old `kadence prime` hook in place instead of adding a second.
27
+ - **`.kadence/README.md` leads to decisions and teammates.** `decision list` and
28
+ `decision add` replace `sprint status` among the top commands, and a new
29
+ "Adding a teammate" section covers install, one `user.email` per person and
30
+ `KADENCE_SOURCE=agent`. Existing READMEs are still never overwritten.
31
+ - **README: the first ten minutes, "Adding a teammate" and "Removing kadence"** —
32
+ the last one lists everything `init` touches, so leaving is a table, not a
33
+ guess. Sprints, reports and exports moved to `docs/reports.md`.
34
+
35
+ ### Changed — the first run speaks about the work
36
+
37
+ - **`init`** suggests a first task *and* a first decision, names what to commit
38
+ so a teammate's agent finds it, and mentions `--hooks` when `.claude/` exists.
39
+ - **`prime`** no longer opens with "No active sprint." when there is none; with
40
+ no decision in force it shows one line on how to record one. `--json` unchanged.
41
+ - **`task add` without `--estimate`** says nothing outside a sprint; inside one it
42
+ names the sprint instead of velocity. `sprint add` says the same.
43
+ - **`kadence --help`** lists commands in the order a session meets them: `init,
44
+ prime, ready, task, decision, note, board, ui, schema`, then planning, reports
45
+ and maintenance.
46
+ - **The TUI header** without a sprint reads `N open · M ready · K decisions in
47
+ force`; points appear only when something is estimated.
48
+ - **`report --list`** puts `attention` first and groups burndown, velocity and
49
+ workload under "Sprint and team".
50
+
51
+ ### Fixed — the contract
52
+
53
+ - **`schema --json` lists every shipped command.** 21 entries were missing,
54
+ among them `sprint add/edit/start/list/burndown`, `task parent/unblock/cancel/delete`,
55
+ `template`, `completion` and `ui` — additive within `kadence/v1`. A test now
56
+ compares the binary's own commands and actions against the contract and fails
57
+ on any gap. Unknown `decision` actions now return `allowed`, and `task` lists
58
+ `doc` in it.
59
+
60
+ ### Tooling
61
+
62
+ - **`scripts/north-star.mjs`** counts repositories with two authors fourteen days
63
+ after their first event — `--local` for a checkout, `--search` through `gh` for
64
+ public ones — and appends a weekly row to `docs/research/north-star-log.md`.
65
+ A maintainer script: the product itself still opens no socket. First row: 0,
66
+ with the reason GitHub code search has not indexed even this repository.
67
+
68
+ ---
69
+
70
+ The command reference stops being prose. And the first two bugs found by using
71
+ the product on itself.
72
+
73
+ ### Added
74
+
75
+ - **`kadence report` is the whole catalogue.** `report burndown`, `report
76
+ velocity` and `report workload` join `flow`, `cfd` and `attention`, and
77
+ `report --list` names all six with what each one answers.
78
+
79
+ The folds were already here — the burndown under `sprint`, the workload
80
+ inside `stats` — so an agent reading `schema --json` could not find them and
81
+ `kadence report` was not the catalogue it looked like. `report burndown`
82
+ calls the same function `sprint burndown` does, resolved in one place, so the
83
+ two cannot drift. `report velocity` is new arithmetic over `sprintReport`,
84
+ and it answers with a range rather than an average (DEC-9): the spread
85
+ between sprints is the forecast, and a series shorter than four sprints says
86
+ so. `report workload` counts open tasks, points, work in progress and blocked
87
+ work per owner, with a row for unassigned work — no hours, no capacity, since
88
+ the journal has neither.
89
+
90
+ `--since` on a report that has no window, or `--sprint` on anything but the
91
+ burndown, is now an error rather than a silently ignored flag (DEC-10).
92
+
93
+ - **`kadence report <name> --html`** — a report as one self-contained page, with
94
+ charts. `flow` gets a percentile dot plot, a created-vs-finished column chart
95
+ and an aging-work chart with the p85 cycle time drawn as a reference line;
96
+ `cfd` gets a stacked area chart with the bands named where they are wide
97
+ enough; `attention` gets the idle days against the threshold. `--file` puts it
98
+ where you want it, and with `--json` the response is the path, not the report.
99
+
100
+ The charts are hand-written inline SVG. Every chart library is a script tag,
101
+ and the page's one promise is the one the board export already makes: it opens
102
+ from disk and asks the network for nothing — asserted structurally, down to
103
+ `url(` in the stylesheet and the `xmlns` an inline `<svg>` does not need.
104
+ Measured at 0.1 ms and 14 KB for the flow page, 1.7 ms and 301 KB for a
105
+ two-year cumulative flow diagram. Every chart is followed by the rows it was
106
+ drawn from: three of the eight categorical colours sit below 3:1 against
107
+ white, and the rule that buys them is that colour is never the only channel.
108
+
109
+ Reasoning in DEC-7 (a page per report rather than a Reports section inside the
110
+ board export — a report is a question with a window, and the board export has
111
+ none) and DEC-8 (the charts).
112
+
113
+ - **`kadence schema --json` and every `--help` now have a machine-readable
114
+ sibling.** `npm run reference` writes `dist/reference.json`: every command,
115
+ its usage line, its flags with their descriptions, its examples, and the
116
+ whole agent contract, read out of the binary rather than written about it.
117
+ The release workflow attaches it to the GitHub release of each tag.
118
+
119
+ This exists because the hand-typed CLI page on the site described 0.3 for the
120
+ whole of 0.4 — nine commands and two dozen flags shipped without it noticing,
121
+ and the agents page named ten of the fifteen error codes. A paragraph has no
122
+ test. A test now reads `src/cli/commands/` and fails when a command file has
123
+ no help behind it, which is the only way that gap stays closed.
124
+
125
+ The generator ships in the package rather than its output: measured at 1.9 KB
126
+ packed against 6.2 KB, and 4.9 KB unpacked against 39.7 KB, for the same
127
+ result. Nothing in the data is unavailable from the CLI itself.
128
+
129
+ - **`scripts/kadence.mjs`** — runs the build in this working tree, rebuilding
130
+ when `src/` is newer. For working on kadence with kadence, which is now how
131
+ this repository is run; `CLAUDE.md` says what that means in practice.
132
+
133
+ ### Fixed
134
+
135
+ - **`burndown.finalRemaining` was always `null`.** Both branches of
136
+ `sprint.status === 'closed' ? null : null` read the same, so the one number
137
+ the field exists for — what a closed sprint did not finish — was never in the
138
+ response, including in `sprint burndown --json`. It now carries the points
139
+ left on the sprint's last day.
140
+
141
+ - **A repeated flag crashed `decision add`.** `--rejected A --rejected B` threw
142
+ `o.rejected.trim is not a function`: cac hands a single flag back as a string
143
+ and a repeat as an array, and only `--doc` was normalised for it. The four
144
+ single-value flags went into `.trim()` as arrays.
145
+
146
+ Repeats now mean what each flag means. A second `--why`, `--task` or
147
+ `--supersedes` is a correction, so the last wins; a second `--rejected` is a
148
+ second alternative that was turned down — which is what a decision record is
149
+ for — so both are kept, joined rather than stored as an array because
150
+ `rejected` is a string in `kadence/v1` and the contract only ever gains
151
+ fields.
152
+
153
+ Found by recording a real decision about this repository with two rejected
154
+ options, with 898 tests green. The tests for it go through the built binary,
155
+ because calling the command directly is exactly the path that could not see
156
+ it.
157
+
158
+ ### Notes
159
+
160
+ Deleting several tasks by `KAD-N` in one loop removes the wrong ones. Labels
161
+ are derived while folding (I7), so removing `KAD-1` renumbers everything after
162
+ it and the next label in the list now belongs to a different task. Working as
163
+ designed, and a sharp edge: delete by ULID, or one at a time. Recorded as a
164
+ note in this repository's own journal rather than fixed, because the fix is not
165
+ obvious — warning on a bulk delete of labels would fire on the common case too.
166
+
167
+ A board-wide Definition of Done is engineering-shaped. `board config --dod`
168
+ copies its criteria into every new task, so "typecheck clean" landed on the
169
+ Probe B interview tasks, where it means nothing, and there is no `task ac
170
+ remove` to take it off. Noted, not yet answered.
171
+
3
172
  ## [0.4.1] — 2026-09-13
4
173
 
5
174
  `0.4.0` was tagged and never published: these three were found by using it
package/README.md CHANGED
@@ -53,10 +53,11 @@ That is the whole state of a piece of work, in one call, with no server to ask
53
53
  and no context to rebuild. A human reads it in `kadence task show`. An AI agent
54
54
  reads the same thing as JSON.
55
55
 
56
- **And it stays one call.** That answer is 948 bytes whether the project holds ten
57
- tasks or a thousand — while the journal behind it grows from 5 KB to 528 KB. The
58
- cost of asking does not grow with the history that makes the answer worth having.
59
- [Measured](docs/research/probe-c-agent-cost.md).
56
+ **And it stays one call.** That answer stays under a kilobyte whether the project
57
+ holds ten tasks or a thousand — while the journal behind it grows from 5 KB to
58
+ 528 KB. The cost of asking does not grow with the history that makes the answer
59
+ worth having. [Measured](docs/research/probe-c-agent-cost.md) at 948 bytes in
60
+ 0.2; 982 bytes at 0.4, after claims and acceptance criteria joined every record.
60
61
 
61
62
  ## Why events and not files
62
63
 
@@ -76,7 +77,7 @@ journal is append-only, one file per event.
76
77
  keeps every step, so «how did we get here» has an answer.
77
78
 
78
79
  State is still there when you want it — it is folded from the journal on read,
79
- which is why the board can never drift from the journal. No one maintains a
80
+ which is why the board cannot drift from the journal. No one maintains a
80
81
  column by hand, so no column can be stale in the way a task file can.
81
82
 
82
83
  **What that does not buy you.** A journal records what was written to it. If a
@@ -107,22 +108,53 @@ Full data: [probe-a-results.md](docs/research/probe-a-results.md).
107
108
 
108
109
  ## In practice
109
110
 
111
+ **The first ten minutes, for the person who owns `CLAUDE.md`:**
112
+
110
113
  ```bash
111
- kadence init
114
+ kadence init --hooks # .kadence/, a short section in AGENTS.md and CLAUDE.md,
115
+ # and a Claude Code hook that runs `kadence prime` at session start
116
+ kadence task add "Fix login" --type bug --priority high
117
+ kadence decision add "Keep sessions in Redis" \
118
+ --why "Revocation must be instant" --rejected "JWT: cannot revoke before expiry"
119
+ ```
120
+
121
+ Open a new agent session. The hook runs `kadence prime`, and the agent starts
122
+ knowing what is open, what is ready and **DEC-1 with its reason** — ask it how
123
+ sessions should be stored and it answers from the journal, not from a guess.
112
124
 
113
- kadence sprint create "Sprint 14"
114
- kadence task add "Fix login" -d "Broken since 2.3" --type bug --priority high --estimate 3
115
- kadence task ac add KAD-1 "tests green"
116
- kadence task comment KAD-1 "Session cookie is fine — the redirect drops it."
117
- kadence task move KAD-1 done
118
- kadence sprint close
125
+ ```bash
126
+ git add .kadence AGENTS.md CLAUDE.md .claude/settings.json .gitignore
127
+ git commit -m "Keep the team's work next to the code"
119
128
  ```
120
129
 
130
+ kadence never commits for you. Once that commit is pushed, the journal is the
131
+ team's, not yours.
132
+
133
+ ### Adding a teammate
134
+
135
+ 1. **They install it:** `npm install -g kadence` (Node 20 or newer). Nothing is
136
+ configured per person.
137
+ 2. **They pull.** Their agent reads the same section in `CLAUDE.md` or `AGENTS.md`
138
+ and, through the hook, runs `kadence prime` in its first session. On a machine
139
+ where kadence is not installed yet, the hook prints one line asking for the
140
+ install instead of failing.
141
+ 3. **One identity per person.** Authorship is `git config user.email`. The same
142
+ person on two machines with two addresses reads as two people — set the same
143
+ address everywhere.
144
+ 4. **Agents say so.** Put `KADENCE_SOURCE=agent` in the agent's environment; its
145
+ writes then carry `[agent]` next to the same email, so a person and their agent
146
+ stay distinguishable.
147
+
148
+ What you should see afterwards: `kadence task show KAD-1` lists their comment,
149
+ note or move with their address. That second author is the moment the journal
150
+ starts doing its job.
151
+
121
152
  **When several people — or several agents — work the same board:**
122
153
 
123
154
  ```bash
124
155
  kadence ready # open, unblocked, nobody else's
125
156
  kadence task claim # take the top of that list, in one step
157
+ kadence note "Redirect drops the cookie, not the session" --task KAD-1
126
158
  kadence task release KAD-1
127
159
  ```
128
160
 
@@ -131,38 +163,21 @@ either pushes; the merge keeps both claims and the task reads `contested` with
131
163
  both names. Refusing the second one would make the owner depend on which branch
132
164
  merged first, and that is the property the whole design rests on.
133
165
 
134
- **Grouping, and the evidence behind "done":**
135
-
136
- ```bash
137
- kadence milestone create "1.0" --due 2026-12-01
138
- kadence milestone add KAD-1 --milestone 1.0
139
- kadence board config --dod "tests green,docs updated" # every new task starts with these
140
- kadence task ac check KAD-1 1
141
- ```
142
-
143
- Moving a task to `done` with unchecked criteria **warns and carries on**. The
144
- checklist is evidence, not a gate: what `done` costs is the team's call, not the
145
- tool's.
146
-
147
- **A snapshot somebody can open, without running anything:**
166
+ **The board, when you want to look at it:**
148
167
 
149
- ```bash
150
- kadence board export --html # one self-contained file: no server, no network
151
- kadence board export --md --readme
152
168
  ```
169
+ $ kadence ui
153
170
 
154
- **When someone asks how long things take** — the answer is a fold over the same
155
- journal, in the Kanban Guide's own terms:
156
-
157
- ```bash
158
- kadence board config --started doing # the column cycle time counts from, if it is not in_progress
159
- kadence report flow # p50 / p85 / p95 in calendar days, WIP, aging work, blocked days
160
- kadence report cfd # tasks per column, per day
171
+ kadence 6 open · 2 ready · 1 decision in force
172
+ +- backlog (2) -------++- in_progress (1) --++- in_review (1) ----++- done (3) ---------+
173
+ | ^# KAD-1 Auth epic || . KAD-4 Tokens @dev||!! KAD-7 Crash [] || v KAD-2 Export |
174
+ | * KAD-3 Login form || || || v KAD-5 Docs |
175
+ +---------------------++--------------------++--------------------++--------------------+
176
+ arrows move enter details m status a assign e edit s sprint R ready b branch q quit
161
177
  ```
162
178
 
163
- No averages, on purpose. Every line names the window and the column it measured
164
- from. Velocity is not the pitch here and neither is cycle time; they are what the
165
- journal happens to know.
179
+ Keyboard, mouse, drag between columns, every field editable in place. It calls
180
+ the same commands the CLI does, so the two can never disagree.
166
181
 
167
182
  **And what the branch you are on is actually about:**
168
183
 
@@ -175,38 +190,30 @@ history and is read when you ask. It narrows the answer between three and
175
190
  twenty times, [measured](docs/research/branch-context-2026-09.md) on real board
176
191
  sizes.
177
192
 
178
- **The board, when you want to look at it:**
179
-
180
- ```
181
- $ kadence ui
182
-
183
- kadence Sprint 14 9 tasks, 28 points
184
- +- backlog (2) -------++- in_progress (1) --++- in_review (1) ----++- done (3) ---------+
185
- | ^# KAD-1 Auth epic || . KAD-4 Tokens @dev||!! KAD-7 Crash [] || v KAD-2 Export |
186
- | * KAD-3 Login form || || || v KAD-5 Docs |
187
- +---------------------++--------------------++--------------------++--------------------+
188
- arrows move enter details m status a assign e edit s sprint R ready b branch q quit
189
- ```
190
-
191
- Keyboard, mouse, drag between columns, every field editable in place. It calls
192
- the same commands the CLI does, so the two can never disagree.
193
-
194
- **And because the journal has the timestamps, the cost comes out of it for free:**
193
+ ### Also in the box
195
194
 
196
- ```
197
- $ kadence sprint close
195
+ Sprints (`sprint create/close`, with points and hours derived from moves),
196
+ milestones, acceptance criteria and a definition of done, templates, custom
197
+ columns, reports folded from the same journal (`report flow`, `cfd`,
198
+ `attention`; burndown, velocity and workload are in the repository and not yet
199
+ on npm), a self-contained HTML or Markdown export, `compact` for long journals,
200
+ and shell completion. None of it is required, and none of it is the point —
201
+ it is what the journal happens to know. Commands and caveats:
202
+ [docs/reports.md](docs/reports.md), and `--help` on each command.
198
203
 
199
- Sprint "Sprint 14" closed.
204
+ ### Removing kadence
200
205
 
201
- Velocity: 23 of 28 points
202
- Actual: 37h — 1.6h per point
206
+ Everything `init` touched, so you can undo it by hand:
203
207
 
204
- Carried over (2):
205
- · KAD-12 Auth refactor
206
- ```
208
+ | What | Where |
209
+ |---|---|
210
+ | The journal | `.kadence/` — yours to keep or delete; kadence never deletes it |
211
+ | The agent section | between `<!-- kadence:begin -->` and `<!-- kadence:end -->` in `AGENTS.md` and `CLAUDE.md` |
212
+ | The hook (only with `--hooks`) | the `SessionStart` entry that runs `kadence prime` in `.claude/settings.json` |
213
+ | The cache entry | the `.kadence/state.json` line in `.gitignore` |
207
214
 
208
- Nobody fills in a form. Nobody can forget to update it. The number is derived
209
- from state changes the team already made.
215
+ Then `npm uninstall -g kadence`. Nothing lives outside the repository and your
216
+ global `node_modules`.
210
217
 
211
218
  ---
212
219
 
@@ -218,8 +225,8 @@ Files first. Every command speaks `--json`, every response carries
218
225
  Start a session with one command:
219
226
 
220
227
  ```bash
221
- kadence prime # active sprint, your work, what is ready, decisions in force
222
- kadence ready --json # five fields per task, not the whole record
228
+ kadence prime # active sprint, your work, what is ready, decisions in force, recent notes
229
+ kadence ready --json # seven fields per task, not the whole record
223
230
  ```
224
231
 
225
232
  `prime` is held to forty lines and three kilobytes by a test, because it is paid
@@ -248,16 +255,19 @@ A failure carries `error.code` and, where the valid set is knowable, `allowed`
248
255
  which matters most for statuses, because they are configured per project and no
249
256
  documentation can tell an agent what yours are.
250
257
 
251
- An MCP wrapper stays on the roadmap as an **optional package**: it costs about
252
- 700 tokens a session over the CLI path — [we measured it](docs/research/probe-c-agent-cost.md),
253
- and it is not the saving the industry benchmarks suggest — but it would be a
254
- second way to say the same thing, and it would not work for agents that have no
255
- MCP client at all.
258
+ There is no MCP wrapper, and one gets built only as an **optional package**, when
259
+ someone who cannot run a CLI asks for it: it costs about 700 tokens a session
260
+ over the CLI path — [we measured it](docs/research/probe-c-agent-cost.md), and it
261
+ is not the saving the industry benchmarks suggest — it would be a second way to
262
+ say the same thing, and it would not work for agents that have no MCP client at
263
+ all.
256
264
 
257
- Ask for only what you need — a board of a thousand tasks is 803 KB in full, and
258
- a tenth of that with the fields an agent actually reads:
265
+ Ask for only what you need — a board of a thousand tasks is 855 KB in full,
266
+ 275 KB with `--summary`, and 104 KB with the three fields an agent actually
267
+ reads:
259
268
 
260
269
  ```bash
270
+ kadence board --json --summary
261
271
  kadence board --json --fields label,status,assignee
262
272
  ```
263
273
 
@@ -275,8 +285,16 @@ one; that is how a reversed decision keeps looking authoritative.
275
285
  `decision list` returns what is still in force, `--all` adds the history, and
276
286
  `task show --json` carries the decisions made about that task.
277
287
 
278
- Documents stay plain markdown — `kadence task doc KAD-1 docs/design.md` records
279
- only the link, which is the part git cannot express.
288
+ Something learned that was never a choice is a note, not a decision — no `--why`,
289
+ no number, and `prime` shows the latest:
290
+
291
+ ```bash
292
+ kadence note "The staging clock runs 40 s behind" --task KAD-1
293
+ ```
294
+
295
+ Documents stay plain markdown — `kadence task doc add KAD-1 docs/design.md`
296
+ records only the link, which is the part git cannot express. If the file does
297
+ not exist it is created from a template; an existing one is never overwritten.
280
298
 
281
299
  Bulk works everywhere and is all or nothing: `kadence task move KAD-1,KAD-2 done`
282
300
  either moves both or changes nothing. A typo does not leave half a board.
@@ -302,24 +320,35 @@ true.
302
320
 
303
321
  **Verified.** The merge thesis, on real git branches. Performance and size, by
304
322
  tests that fail if they regress. That the conflict problem exists in the wild —
305
- measured, not assumed. 898 tests, including an end-to-end run through the
306
- installed binary.
323
+ measured, not assumed. 998 tests in the repository today, including an
324
+ end-to-end run through the installed binary.
307
325
 
308
326
  **Not verified.** That teams and their AI agents actually lose enough context to want
309
327
  this. The bet rests on reasoning and on the industry naming the problem out
310
- loud — not on our own users. That research is
311
- [designed](docs/research/interview-script.md) and not yet run.
328
+ loud — not on our own users. That research, Probe B, is
329
+ [designed](docs/research/interview-script.md) and not yet run: as of
330
+ 2026-09-16, [zero conversations](docs/research/probe-b-results.md) and no
331
+ external users. [The strategy](docs/product/strategy.md) says what happens next
332
+ and on which dates.
333
+
334
+ **Not built, on purpose.** An MCP package — only if someone who cannot run a CLI
335
+ asks for it, not as an inevitability.
312
336
 
313
- **On the roadmap, not shipped.** The optional MCP package, kept as a response to
314
- someone who cannot use the CLI rather than as an inevitability.
337
+ **In the repository, not yet released.** `report burndown`, `report velocity`,
338
+ `report workload`, `report --list` and `report <name> --html`. `npm install`
339
+ gives you 0.4.1 without them.
315
340
 
316
341
  `kadence context <task>` was dropped: we measured what `task show --json` already
317
- returns and it is the whole history of one piece of work, 948 bytes, constant.
342
+ returns and it is the whole history of one piece of work, under a kilobyte,
343
+ constant.
318
344
  The only thing left to add was a different format, and nobody has asked for one.
319
345
 
320
346
  **Known limits.** Conflicts are real but rare: roughly one merge in two hundred.
321
347
  Terminal interaction is covered by manual testing; only the key router is
322
- unit-tested.
348
+ unit-tested. Deleting several tasks by `KAD-N` in one loop removes the wrong
349
+ ones, because labels are derived and renumber as earlier tasks go — delete by
350
+ ULID, or one at a time. There is no `task ac remove`, so a board-wide Definition
351
+ of Done lands on every new task unless it is added with `--no-dod`.
323
352
 
324
353
  ---
325
354
 
@@ -345,8 +374,9 @@ revisit it: [docs/decisions/](docs/decisions/).
345
374
 
346
375
  ```bash
347
376
  npm install
348
- npm test # 898 tests
349
- npm run build # 35 KB bundle
377
+ npm test # 998 tests; builds dist/cli.js first
378
+ npm run typecheck
379
+ npm run build # one 111 KB bundle, blessed kept external (82 KB at 0.4.1)
350
380
  ```
351
381
 
352
382
  `CLAUDE.md` documents the invariants, the boundaries, and the decisions that
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import{p as G}from"./chunk-CDGGOWGY.js";import S from"blessed";var c={border:"gray",borderFocus:"cyan",headerFg:"white",headerBg:"blue",selectedFg:"black",selectedBg:"cyan",dim:"gray",hint:"gray",warn:"yellow",danger:"red"},he={backlog:"gray",todo:"white",in_progress:"blue",doing:"blue",blocked:"red",in_review:"magenta",review:"magenta",done:"green",shipped:"green"};function ne(n){return he[n]??"white"}var pe={urgent:"\u203C",high:"\u2191",normal:" ",low:"\u2193"},me={urgent:"red",high:"yellow",normal:"white",low:"gray"},ye={bug:"\u2716",story:"\u25C6",epic:"\u2B22",task:"\xB7"};function k(n,s){return`{${s}-fg}${n}{/}`}function W(n,s){let d=k(pe[n.priority],me[n.priority]),p=k(ye[n.type]??"\xB7","gray"),h=k(n.label,"cyan"),g=[];n.blockedBy.length>0&&g.push(k("\u2298","red")),n.contestedBy.length>0?g.push(k("\u2691","magenta")):n.claimedBy!==null&&g.push(k("\u2713","green"));let E=n.criteria.filter(O=>!O.checked).length;if(E>0?g.push(k(`${n.criteria.length-E}/${n.criteria.length}`,"yellow")):n.criteria.length>0&&g.push(k("\u2611","green")),n.comments.length>0&&g.push(k("\u{1F4AC}","gray")),n.due!==null){let O=n.due<new Date().toISOString().slice(0,10);g.push(k(O?"\u23F0":"\u{1F4C5}",O?"red":"gray"))}let C=[n.assignee!==null?k(`@${n.assignee.split("@")[0]}`,"gray"):"",n.estimate!==null?k(String(n.estimate),"yellow"):"",g.join("")].filter(O=>O.length>0).join(" "),m=`${Y(d)}${Y(p)} ${Y(h)} `,b=C.length>0?` ${Y(C)}`:"",T=Math.max(4,s-m.length-b.length),F=[...n.title],q=F.length>T?`${F.slice(0,T-1).join("")}\u2026`:n.title;return`${d}${p} ${h} ${q}${C.length>0?` ${C}`:""}`}function z(n,s){return s?`{cyan-bg}{black-fg}${Y(n)}{/}`:` ${n}`}function J(n,s,d,p=12){let h=`${n.padEnd(p)} ${s}`;return d?`{cyan-bg}{black-fg}\u25B8 ${h}{/}`:` ${h}`}function Y(n){return n.replace(/\{[^}]*\}/g,"")}var re=["\u2190\u2192\u2191\u2193 move","enter details","[ ] shift","m status","a assign","c comment","e edit","n new","s sprint","/ filter","R ready","b branch","M milestone","C claim","? help","q quit (Ctrl-C forces)"].join(" ");function se(n){let s=[],d=!1;function p(){d=!0,setImmediate(()=>{d=!1})}return{dispatch(h){if(d)return;let g=s[s.length-1];g!==void 0?g(h):n(h)},push(h){s.push(h),p();let g=!1;return()=>{if(g)return;g=!0;let E=s.lastIndexOf(h);E!==-1&&s.splice(E,1),p()}},isDialogOpen:()=>s.length>0,depth:()=>s.length}}function oe(n){if(n.ch==="")return!1;let s=n.ch.charCodeAt(0);return s<32||s===127}var be=["up","down","pageup","pagedown","home","end"],ie=["escape","q"];function Q(n){return be.includes(n.name)}function le(n){return ie.includes(n.name)||ie.includes(n.ch)}function _(n,s,d){return d===0?0:(n+s+d)%d}var ae="cancelled",we="done";function ve(n){let s=[`${n.open} open`,`${n.ready} ready`,`${n.decisions} ${n.decisions===1?"decision":"decisions"} in force`,...n.points===null?[]:[`${n.points} points`]];return`kadence ${n.sprint===null?"":`${n.sprint} `}${s.join(" \xB7 ")}${n.filterNote}`}function Ie(n){let s=S.screen({smartCSR:!0,title:"kadence",fullUnicode:!0,mouse:!0}),{state:d}=n.reload(),p=n.branchEventIds(),h="",g=!1,E=!1,C="",m=[],b=0,T=se(t=>ge(t));function F(t=0){try{s.destroy()}catch{}process.exit(t)}process.on("SIGINT",()=>F(0)),process.on("SIGTERM",()=>F(0)),s.program.on("keypress",(t,r)=>{let i={ch:t??"",name:r?.name??""};(i.name==="C-c"||i.ch==="")&&F(0),!(oe(i)&&i.name!=="enter"&&i.name!=="escape")&&T.dispatch(i)});let q=S.box({parent:s,top:0,height:1,width:"100%",tags:!0,style:{fg:c.headerFg,bg:c.headerBg}}),O=S.box({parent:s,bottom:0,height:1,width:"100%",tags:!0,content:` ${re}`,style:{fg:c.hint}}),V=S.box({parent:s,bottom:1,height:1,width:"100%",tags:!0,style:{fg:c.warn}});function w(t,r=c.warn){V.setContent(` ${t}`),V.style.fg=r,s.render()}function A(){let t=d.statuses.filter(i=>i!==ae),r=d.orphanStatuses.filter(i=>i!==ae);return[...t,...r]}function de(t){let r=h.toLowerCase(),i=g?new Set(G(d.tasks,{viewer:n.actor,statuses:d.statuses,started:d.started}).map(a=>a.id)):null,e=E&&!("reason"in p)?p:null,o=e===null?null:a=>e.ids.has(a.id)||a.history.some(l=>e.ids.has(l.id)),u=C===""?null:d.milestones.find(a=>a.label===C)?.id??null;return d.tasks.filter(a=>a.status===t&&(i===null||i.has(a.id))&&(o===null||o(a))&&(u===null||a.milestone===u)&&(r===""||a.title.toLowerCase().includes(r)||(a.assignee??"").toLowerCase().includes(r)||a.labels.some(l=>l.toLowerCase().includes(r))))}function X(){for(let i of m)i.box.destroy();m=[];let t=A(),r=Math.max(Math.floor(100/Math.max(t.length,1)),12);t.forEach((i,e)=>{let o=S.box({parent:s,top:1,left:`${e*r}%`,width:`${r}%`,bottom:2,label:` ${i} `,border:{type:"line"},style:{border:{fg:c.border},label:{fg:ne(i)}}}),u=S.list({parent:o,top:0,left:0,right:0,bottom:0,keys:!1,mouse:!1,interactive:!1,tags:!0,scrollable:!0,style:{selected:{fg:c.selectedFg,bg:c.selectedBg},item:{fg:"white"}}});m.push({status:i,box:o,list:u,tasks:[],cursor:0})})}function y(t=!1){t&&(d=n.reload().state,p=n.branchEventIds()),m.length!==A().length&&X();let r=0,i=0,e=!1;m.forEach((l,N)=>{l.tasks=de(l.status),l.status!==we&&(r+=l.tasks.length,i+=l.tasks.reduce((R,M)=>R+(M.estimate??0),0)),l.tasks.some(R=>R.estimate!==null)&&(e=!0);let U=l.box.width-4;l.cursor=Math.min(l.cursor,Math.max(0,l.tasks.length-1)),l.list.setItems(l.tasks.map((R,M)=>z(W(R,U),N===b&&M===l.cursor)));let L=l.tasks.reduce((R,M)=>R+(M.estimate??0),0);l.box.setLabel(` ${l.status} (${l.tasks.length}${L>0?`, ${L}`:""}) `),l.box.style.border.fg=N===b?c.borderFocus:c.border});let o=d.sprints.find(l=>l.status==="active"),u=(h===""?"":` filter: "${h}"`)+(g?" ready only":"")+(E&&!("reason"in p)?` branch: ${p.name}`:"")+(C===""?"":` milestone: ${C}`),a=G(d.tasks,{viewer:n.actor,statuses:d.statuses,started:d.started}).length;q.setContent(` ${ve({sprint:o===void 0?null:o.name,open:r,ready:a,decisions:d.decisions.filter(l=>l.supersededBy===null).length,points:e?i:null,filterNote:u})}`),d.cycles.length>0&&w(`${d.cycles.length} dependency cycle(s) \u2014 see kadence task list`,c.danger),s.render()}function I(){let t=m[b];return t?.tasks[t.cursor]}function Z(){m.forEach((t,r)=>{let i=t.box.width-4;t.list.setItems(t.tasks.map((e,o)=>z(W(e,i),r===b&&o===t.cursor))),t.box.style.border.fg=r===b?c.borderFocus:c.border}),s.render()}function x(t){t!==null?w(t,c.warn):w(""),y(!0)}function K(t,r,i){let e=T.push(()=>{}),o=S.prompt({parent:s,top:"center",left:"center",width:"60%",height:7,border:{type:"line"},style:{border:{fg:c.borderFocus}},keys:!0,mouse:!0});o.input(t,r,(u,a)=>{o.destroy(),e(),s.render(),typeof a=="string"&&i(a.trim())})}function ue(t){let r=S.box({parent:s,top:"center",left:"center",width:"75%",height:"80%",border:{type:"line"},label:` ${t.label} `,style:{border:{fg:c.borderFocus}},tags:!0,keys:!0,mouse:!0}),i=[{key:"title",label:"title",value:()=>t.title,hint:"Title:"},{key:"description",label:"description",value:()=>(t.description??"\u2014").split(`
3
+ `)[0]??"\u2014",hint:"Description (one line here, e for $EDITOR):"},{key:"status",label:"status",value:()=>t.status,hint:`Status (${A().join(", ")}):`},{key:"type",label:"type",value:()=>t.type,hint:"Type (task, bug, story, epic):"},{key:"priority",label:"priority",value:()=>t.priority,hint:"Priority (low, normal, high, urgent):"},{key:"assignee",label:"assignee",value:()=>t.assignee??"\u2014",hint:'Assignee (or "none"):'},{key:"estimate",label:"estimate",value:()=>t.estimate===null?"\u2014":String(t.estimate),hint:"Estimate in points:"},{key:"due",label:"due",value:()=>t.due??"\u2014",hint:"Due date YYYY-MM-DD (empty clears):"},{key:"labels",label:"labels",value:()=>t.labels.length>0?t.labels.join(", "):"\u2014",hint:"Labels, comma separated:"}],e=()=>[...i.map((v,f)=>({kind:"field",index:f})),...t.criteria.map(v=>({kind:"criterion",n:v.n}))],o=S.list({parent:r,top:0,left:1,right:1,height:i.length+t.criteria.length,keys:!1,mouse:!1,interactive:!1,tags:!0,style:{selected:{fg:c.selectedFg,bg:c.selectedBg}}}),u=0,a=S.box({parent:r,top:i.length+t.criteria.length+1,left:1,right:1,bottom:0,tags:!0,scrollable:!0});function l(){o.setItems(e().map((f,B)=>f.kind==="field"?J(i[f.index].label,i[f.index].value(),B===u):J(` ${f.n}.`,`[${t.criteria.find($=>$.n===f.n)?.checked===!0?"x":" "}] ${t.criteria.find($=>$.n===f.n)?.text??""}`,B===u)));let v=[t.blockedBy.length>0?`{red-fg}blocked by ${t.blockedBy.length} task(s){/}`:"",t.parent!==null?"{gray-fg}has a parent{/}":"",t.loggedHours>0?`{gray-fg}logged ${t.loggedHours.toFixed(1)}h{/}`:"","",t.description!==null&&t.description.includes(`
4
+ `)?`{gray-fg}${t.description.split(`
5
+ `).slice(1).join(`
6
+ `)}{/}`:"",t.comments.length>0?`{cyan-fg}comments (${t.comments.length}){/}`:"",...t.comments.map(f=>` {gray-fg}${f.author}:{/} ${f.text}`),"",t.criteria.length>0?"{gray-fg}space toggles the criterion under the cursor{/}":"","{gray-fg}\u2191\u2193 field enter edit e description in $EDITOR esc close{/}","{gray-fg}description opens the editor, so it can hold paragraphs{/}"].filter(f=>f!=="");a.setContent(v.join(`
7
+ `)),s.render()}l();let N=T.push(v=>M(v)),U=()=>{N(),r.destroy(),y(!0)};function L(){d=n.reload().state;let v=d.tasks.find(f=>f.id===t.id);if(v===void 0){U();return}Object.assign(t,v),l()}function R(){let v=te(()=>n.edit(t.id));v!==null&&w(v),L()}function M(v){let{ch:f,name:B}=v;if(B==="up"||f==="k")return u=_(u,-1,e().length),l();if(B==="down"||f==="j")return u=_(u,1,e().length),l();if(f===" "||B==="space"){let $=e()[u];if($===void 0||$.kind!=="criterion")return;let j=t.criteria.find(H=>H.n===$.n);if(j===void 0)return;let D=n.toggleCriterion(t.id,$.n,j.checked);return D!==null&&w(D),L()}if(le(v))return U();if(f==="e")return R();if(B==="enter"){let $=e()[u];if($!==void 0&&$.kind==="criterion"){let D=t.criteria.find(fe=>fe.n===$.n);if(D===void 0)return;let H=n.toggleCriterion(t.id,$.n,D.checked);return H!==null&&w(H),L()}let j=i[u];if(j===void 0)return;if(j.key==="description")return R();K(j.hint,j.value()==="\u2014"?"":j.value(),D=>{let H=n.setField(t.id,j.key,D);H!==null&&w(H),L()})}}s.render()}function ce(){let t=["{cyan-fg}Navigation{/}"," \u2190 \u2192 h l move between columns"," \u2191 \u2193 k j move between tasks"," enter task details"," / filter escape clears it"," r reload from the journal"," R show only what can be started now (ready)"," b show only what this branch introduced"," M show only one milestone (empty clears it)"," C claim the selected task, or release it if it is yours","","{cyan-fg}Task actions{/}"," [ ] move one column left or right"," m move to a named status"," a assign c comment"," e edit description in $EDITOR"," p priority t log time"," n new task d delete","","{cyan-fg}Sprint{/}"," s sprint menu: status, start, close, burndown"," S add the selected task to the active sprint","","{cyan-fg}Mouse{/}"," click select a card"," drag move a card to another column","","{gray-fg}press any key to close{/}"],r=S.box({parent:s,top:"center",left:"center",width:60,height:Math.min(t.length+2,30),border:{type:"line"},label:" Keys ",style:{border:{fg:c.borderFocus}},tags:!0,scrollable:!0,keys:!0,mouse:!0,content:t.join(`
8
+ `)}),i=T.push(e=>{Q(e)||(i(),r.destroy(),y())});r.on("click",()=>{i(),r.destroy(),y()}),s.render()}function ee(t,r,i){let e=i.length>0?`
9
+
10
+ {gray-fg}${i.map(a=>`${a.key} ${a.label}`).join(" ")} esc close{/}`:`
11
+
12
+ {gray-fg}press any key to close{/}`,o=S.box({parent:s,top:"center",left:"center",width:"80%",height:"70%",border:{type:"line"},label:` ${t} `,style:{border:{fg:c.borderFocus}},tags:!0,scrollable:!0,mouse:!0,content:r+e}),u=T.push(a=>{if(Q(a))return;let l=i.find(N=>N.key===a.ch);u(),o.destroy(),l!==void 0?l.run():y()});s.render()}function ge(t){let{ch:r,name:i}=t;if(i==="left"||r==="h")return b=Math.max(0,b-1),y();if(i==="right"||r==="l")return b=Math.min(m.length-1,b+1),y();if(i==="up"||r==="k"){let e=m[b];e!==void 0&&(e.cursor=_(e.cursor,-1,e.tasks.length),Z());return}if(i==="down"||r==="j"){let e=m[b];e!==void 0&&(e.cursor=_(e.cursor,1,e.tasks.length),Z());return}if(i==="enter"){let e=I();e!==void 0&&ue(e);return}if(r==="["||r==="]"){let e=I();if(e===void 0)return;let o=r==="]"?1:-1,u=m[b+o];return u===void 0?void 0:(x(n.move(e.id,u.status)),b+=o,y())}if(r==="m"){let e=I();return e===void 0?void 0:K(`Move ${e.label} to (${A().join(", ")}):`,"",o=>x(n.move(e.id,o)))}if(r==="a"){let e=I();return e===void 0?void 0:K(`Assign ${e.label} to (or "none"):`,e.assignee??"",o=>x(n.assign(e.id,o)))}if(r==="c"){let e=I();return e===void 0?void 0:K(`Comment on ${e.label}:`,"",o=>x(n.comment(e.id,o)))}if(r==="t"){let e=I();return e===void 0?void 0:K(`Log time on ${e.label} (2h, 90m, -30m):`,"",o=>x(n.logTime(e.id,o)))}if(r==="p"){let e=I();return e===void 0?void 0:K(`Priority for ${e.label} (low, normal, high, urgent):`,e.priority,o=>x(n.setPriority(e.id,o)))}if(r==="n")return K("New task title:","",e=>x(n.create(e)));if(r==="d"){let e=I();return e===void 0?void 0:K(`Delete ${e.label}? type "yes":`,"",o=>{o.toLowerCase()==="yes"?x(n.remove(e.id)):w("Not deleted.")})}if(r==="e"){let e=I();return e===void 0?void 0:x(te(()=>n.edit(e.id)))}if(r==="S"){let e=I();return e===void 0?void 0:x(n.addToSprint(e.id))}if(r==="s")return ee("Sprint",n.sprintStatus(),[{key:"b",label:"burndown",run:()=>ee("Burndown",n.burndown(),[])},{key:"n",label:"start next sprint",run:()=>x(n.sprintStart())},{key:"x",label:"close sprint",run:()=>x(n.sprintClose())}]);if(r==="/")return K("Filter (title, assignee or label):",h,e=>{h=e==="*"?"":e,y()});if(i==="escape"&&h!=="")return h="",w("Filter cleared."),y();if(r==="r")return w("Reloaded."),y(!0);if(r==="R")return g=!g,w(g?"Showing only what can be started now.":"Showing every task."),y();if(r==="b")return"reason"in p?w(p.reason==="detached"?"HEAD is detached, so there is no branch to filter by.":`There is no branch "${p.base}" to compare against.`,c.warn):(E=!E,w(E?`Showing only what ${p.name} introduced since ${p.base}.`:"Showing every task."),y());if(r==="M"){if(d.milestones.length===0)return w("No milestones yet. Create one with kadence milestone create.",c.warn);let e=d.milestones.map(o=>`${o.label} ${o.name}`).join(", ");return K(`Milestone (${e}; empty for all):`,C,o=>{let u=o.trim();if(u==="")return C="",w("Showing every task."),y();let a=d.milestones.find(l=>l.label.toUpperCase()===u.toUpperCase()||l.name.toLowerCase()===u.toLowerCase());if(a===void 0)return w(`No milestone "${u}".`,c.warn);C=a.label,w(`Showing ${a.label} ${a.name}.`),y()})}if(r==="C"){let e=I();return e===void 0?void 0:x(e.claimedBy===null?n.claim(e.id):n.release(e.id))}if(r==="?")return ce();(r==="q"||i==="C-c")&&F(0)}function te(t){let r=s;r.leave();let i=t();return r.enter(),i}let P=null;m.forEach(()=>{}),s.on("mousedown",t=>{if(T.isDialogOpen())return;let r=m.findIndex(u=>{let a=u.box.left;return t.x>=a&&t.x<a+u.box.width});if(r===-1)return;b=r;let i=m[r],e=t.y-i.box.top-1,o=i.tasks[e];o!==void 0&&(i.cursor=e,P={task:o,from:r}),y()}),s.on("mouseup",t=>{if(T.isDialogOpen()||P===null)return;let r=m.findIndex(i=>{let e=i.box.left;return t.x>=e&&t.x<e+i.box.width});r!==-1&&r!==P.from&&(x(n.move(P.task.id,m[r].status)),b=r,y()),P=null}),X(),y()}export{ve as boardHeader,Ie as runBoardUi};