kadence 0.3.2 → 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,520 @@
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
+
172
+ ## [0.4.1] — 2026-09-13
173
+
174
+ `0.4.0` was tagged and never published: these three were found by using it
175
+ before it reached anyone, so `0.4.1` is the first published 0.4. Nothing below
176
+ changes the shape of an event or renames a field in `kadence/v1`.
177
+
178
+ ### Fixed
179
+
180
+ - **The fold dropped who wrote a comment or a history line.** Every event
181
+ carries `source` — `"human"` or `"agent"`, from `KADENCE_SOURCE` — and it is
182
+ validated on read, but the projection kept it only on decisions and notes. So
183
+ `task show` printed an agent's comment exactly like a person's, and
184
+ `task show --json` gave an agent no way to tell them apart: `actor` is the git
185
+ identity, which a person and their agent share. The one surface the product's
186
+ headline is about could not show the thing it claims. Comments and history
187
+ entries now carry `source`, `task show` marks an agent's line `[agent]` the
188
+ way `decision list` already did, and a note shows the mark it always knew.
189
+ The snapshot shape test now records nested records too — it is what should
190
+ have failed when `source` reached the event and not the comment. Cache version
191
+ bumped to `kadence-snapshot/11`.
192
+ - **`kadence ready` offered work that had already started.** The filter knew
193
+ about finished, blocked-by and claimed-by-others, and nothing about the board:
194
+ a task sitting in `in_review`, or parked in the `blocked` column, was listed
195
+ first with `kadence task claim KAD-1` printed under it. An agent following
196
+ `prime → ready → claim` took work somebody was reviewing. Ready now means
197
+ ready to *start* — anything in the started column or past it is left out,
198
+ using the board's own boundary, so a team that renames its columns keeps a
199
+ `ready` that means what its board means. When that is why the list is empty,
200
+ the message says so: `Nothing ready (2 already started)`.
201
+ - **The performance fixture moved all 500 tasks into `in_progress`**, so the
202
+ `ready` guardrail was timing a filter over an empty answer and asserting only
203
+ that it was fast. Every third move parks a task back in `todo` now; the
204
+ guardrail times a real result.
205
+
206
+ ## [0.4.0] — 2026-09-13
207
+
208
+ Planned as slices 0.4.0 through 0.4.4 and published as one minor release.
209
+
210
+ The agent loop: four commands that answer "what next" without folding the whole
211
+ board by hand. Everything here is additive within `kadence/v1` — no field or
212
+ command was renamed, and `--json` responses gain keys rather than losing them.
213
+
214
+ ### Added
215
+
216
+ - **`kadence ready`** — open tasks with no live blockers, not claimed by anyone
217
+ else, priority first and age second. `--json` returns five fields per task
218
+ rather than the whole record, because this goes into an agent's context on
219
+ every session. An empty result names the reason: how many are blocked, how
220
+ many belong to someone else. Measured at 9 ms over 10,000 events.
221
+ - **`kadence prime`** — the session preamble: active sprint and days left, your
222
+ claimed and in-progress work, how many tasks are ready, decisions in force by
223
+ title, the last five notes, and four commands to go deeper. Held to 40 lines
224
+ and 3 KB by a test; 16 lines and 493 bytes on this repository. It carries the
225
+ part that changes and points at the part that does not, because the static
226
+ half already lives in `AGENTS.md`.
227
+ - **`kadence init --hooks`** — adds a `SessionStart` hook running `kadence prime`
228
+ to `.claude/settings.json`. Only with the flag: that file belongs to the user
229
+ and is committed to their repository. The write is an upsert — hooks of other
230
+ events, other matchers and other commands survive, running it twice leaves one
231
+ hook, and a file that does not parse is reported rather than replaced.
232
+ - **`kadence task claim` / `task release`** — take a task, or give it back.
233
+ `claim` with no argument takes the top of `ready`, which is one step instead
234
+ of two. There is **no lock**, and the message says so: two machines can each
235
+ claim before either pushes. See [ADR-011](docs/decisions/011-claims-as-events.md).
236
+ - **`kadence note "text" [--task KAD-1]`** and `note list` — something learned
237
+ that was never a choice. Deliberately not a decision: no `--why`, no
238
+ `supersedes`, no number. The help says when to reach for `decision add`
239
+ instead. Notes show up in `task show` and in `prime`.
240
+ - **`claimedBy` and `contestedBy`** on the task record in `--json`, and a
241
+ `Claimed:` line in `task show`.
242
+ - **TUI**: `R` shows only what can be started now, `C` claims the selected task
243
+ or releases it if it is yours, and a card carries a claim mark — a separate
244
+ colour when the claim is contested. Both actions call the same functions the
245
+ CLI does.
246
+
247
+ ### Changed
248
+
249
+ - **A contested claim is reported, not rejected.** When two people claim one
250
+ task, the earliest by ULID holds and the later one is kept, with the task
251
+ reading `contested` under both names. Rejecting the second would make the
252
+ owner depend on which branch merged first — the same invariant that keeps
253
+ dependency cycles and orphaned status columns alive rather than dropped. It is
254
+ the first conflict in kadence that names people rather than values.
255
+ - **The state cache version moved from 2 to 4.** The projected shape changed
256
+ twice: three claim fields on a task, then notes on the project state. Missing
257
+ exactly this bump is what shipped as a bug in 0.3.1, so it is now pinned to
258
+ the field lists by a test.
259
+ - The agent section written into `AGENTS.md` and `CLAUDE.md` grew by one line —
260
+ `kadence prime` first — and by no more than that.
261
+
262
+ ### Added — the branch, the human, and the evidence behind done (0.4.1)
263
+
264
+ - **`kadence task list --branch`** — the work this branch introduced, measured
265
+ against `--base` (`main`, or `init.defaultBranch`). Membership is derived from
266
+ git at read time and never stored: writing a branch name into an event would
267
+ go stale the moment the branch is renamed or merged, and would make the same
268
+ events fold differently depending on where they were written. A detached HEAD
269
+ and an unknown base both fail by name rather than reporting no work.
270
+ The measurement that justified the flag, including what would make it wrong,
271
+ is in [branch-context-2026-09.md](docs/research/branch-context-2026-09.md).
272
+ - **Acceptance criteria** — `task ac add|check|uncheck|list`. The number is a
273
+ position in the folded list, assigned like `KAD-N` and never stored, so two
274
+ branches can each add a criterion and merge without renumbering. Moving a task
275
+ to `done` with unchecked criteria **warns and proceeds**: the checklist is
276
+ evidence, not a gate, and refusing would make the board lie about where the
277
+ work is.
278
+ - **Definition of done** — `board config --dod "tests green,docs updated"`. The
279
+ criteria are **copied** into each new task, never referenced, so raising the
280
+ standard cannot reach back and change what finished work had promised.
281
+ `task add --no-dod` skips them.
282
+ - **Milestones** — `milestone create|add|list|close`. Grouping by outcome, where
283
+ an epic groups by structure and a sprint by time. Not a third hierarchy: a
284
+ task carries at most one, as a field beside `sprint`. `MS-N` is derived from
285
+ ULID order like every other label, and progress is counted in points, the same
286
+ unit as velocity and burndown.
287
+ - **`kadence stats`** — counts by status and assignee, open blockers, contested
288
+ claims, and the velocity of the last three closed sprints.
289
+ - **`kadence completion install`** — zsh, bash and fish, generated from one list
290
+ rather than three hand-written scripts. Without a terminal it prints the
291
+ script instead of writing, because that is how people pipe it into a file.
292
+ For zsh it writes the file and prints the `fpath` line rather than editing a
293
+ shell rc, which is the user's file.
294
+ - **`board --json --summary`** — the column state without `history` and
295
+ `comments`. Those are the two fields that grow with how long a task has been
296
+ worked on rather than with how many tasks there are.
297
+ - **TUI**: `b` shows only what the branch introduced, `M` filters to one
298
+ milestone, and the card dialog carries the acceptance-criteria checklist with
299
+ space to toggle. Every toggle calls the same command the CLI does.
300
+
301
+ ### Changed
302
+
303
+ - `task list --json` and `board --json` gain a `milestone` field on each task,
304
+ carrying the `MS-N` label. The response without `--summary` is otherwise
305
+ unchanged; the contract only ever gains.
306
+ - The state cache version moved again with the projected shape.
307
+
308
+ ### Added — reports, and the two things they needed first (0.4.3)
309
+
310
+ What the neighbours call reports is, here, a fold over timestamps the journal
311
+ already carries. The reasoning, the sources and the verdict per report are in
312
+ [reports-discovery-2026-09.md](docs/research/reports-discovery-2026-09.md).
313
+ Velocity and cycle time stay out of the headline; they are a consequence, served
314
+ to the person who asks.
315
+
316
+ - **`kadence report flow`** — the four flow metrics the Kanban Guide mandates,
317
+ and what sits around them: work in progress, throughput per ISO week, cycle /
318
+ lead / response time as **p50 / p85 / p95 in calendar days**, aging work
319
+ against the p85, created versus resolved, and days spent blocked. There is no
320
+ mean anywhere, on purpose. Every response names the window and the column
321
+ work counts as started from, so a number is never quoted without its terms.
322
+ Empty sections say why they are empty.
323
+ - **`kadence report cfd`** — tasks per column at the end of every day of the
324
+ window. A move backwards subtracts. The first version re-walked each task's
325
+ history per day and cost 150 ms on 10,000 events; it is a single pass now, at
326
+ 4 ms.
327
+ - **`kadence board config --started <status>`** — the column cycle time and a
328
+ sprint's actual hours are measured from. It was the literal `in_progress` in
329
+ three places while columns were configurable, so a board named
330
+ `todo,doing,review,done` closed every sprint with `actualHours: null` and
331
+ never said why. It is a folded setting now, validated against the live
332
+ columns, and a column change that strands it warns instead of going quiet.
333
+ - **`kadence compact`** — folds months older than `--keep-months` into one
334
+ archive file each. The primitive existed since 0.1 and was reachable only from
335
+ tests. `--dry-run` says what would move and writes nothing. It touches no git
336
+ state, and it says the one thing worth knowing: an archived month is a single
337
+ file, so compact on one branch and merge before compacting on another.
338
+
339
+ ### Added — three experiments (0.4.2)
340
+
341
+ Each of these was a standing "no". Each is now tried in the shape that survives
342
+ the three constraints, and each has a kill condition written before the code, in
343
+ [feature-adoption-2026-09.md](docs/product/feature-adoption-2026-09.md).
344
+
345
+ - **`kadence board export --html`** — the board, the sprint, the burndown,
346
+ milestones and decisions in force, as **one self-contained file**. No script
347
+ tag, no stylesheet link, no font, no image: the page asks the network for
348
+ nothing and opens from disk. That is what makes it an experiment in place of a
349
+ web UI rather than a web UI with extra steps. A test asserts the absence, not
350
+ the appearance. Acceptance criteria are printed in full rather than counted,
351
+ because a file attached to a pull request is evidence and "2/3" is not.
352
+ - **`kadence board export --md`**, and `--readme` to update the section between
353
+ markers in an existing README. It will not create one — that would be a
354
+ different command and a bigger promise.
355
+ - **`@kadence/github`** — a separate package that publishes tasks to GitHub
356
+ Issues **in one direction**, by shelling out to `gh`. The core is unchanged
357
+ and still opens no socket; the credential stays with a tool the user already
358
+ trusts. A marker in the issue body makes a second publish an edit rather than
359
+ a duplicate, and the issue itself says that edits made there are overwritten.
360
+ Nothing is ever read back. See
361
+ [ADR-012](docs/decisions/012-network-only-in-packages.md), written before the
362
+ code. Every test runs against a recorded `gh`, never the real one.
363
+ - **`kadence task doc add KAD-1 docs/design.md`** — creates the file from a
364
+ four-line template and records the link in one call. It never overwrites: a
365
+ file that is already there is the document, and replacing it with a template
366
+ would destroy the thing being linked.
367
+
368
+ ### Added — attention signals, and labels that survive a merge (0.4.4)
369
+
370
+ - **`kadence report attention`** — work the board presents as active while
371
+ nobody is moving it. Four signals, one definition: `stalled` (no event for N
372
+ days), `unowned` (in flight for N days with no assignee and no claim),
373
+ `stale_claim` (a claim older than N days with no move since), and
374
+ `dead_blocker` (`blockedBy` pointing at a finished task — the one signal that
375
+ waits for no threshold). `--since` is the days of silence, default 7. No new
376
+ event type, no new field: the report is a fold over what the journal already
377
+ holds, and it costs 0.5 ms over 10,000 events on top of the fold.
378
+
379
+ Two candidates were left out on purpose and the module says why. "Open task
380
+ with no owner" is the definition of a backlog. "Criteria added and never
381
+ checked" would fire on every task in any repository that configured a
382
+ Definition of Done, because `task add` copies it in unchecked — loudest in
383
+ the repositories that took our advice.
384
+ - **`prime` carries an attention line** — up to three tasks, and only when
385
+ there is something to say.
386
+ - **`task.label_added` / `task.label_removed`** — labels move as deltas. This
387
+ was the one field where "every intent is preserved" was untrue: `task.updated`
388
+ carried the whole array and the fold replaced the whole array, so two branches
389
+ adding different labels merged without a conflict and one label vanished with
390
+ no warning. Not a merge failure — a fold storing *state* in an event, the
391
+ exact mistake the product exists to avoid. Reasoning in
392
+ [ADR-013](docs/decisions/013-labels-as-deltas.md).
393
+ - **`labels` in `ready --json`**, the seventh field, and in the guaranteed set.
394
+
395
+ Source for this slice: a tech lead's feedback of 2026-09-10 — drift comes not
396
+ from where state lives but from work that stops being watched. Append-only
397
+ removes the drift between board and journal; it promises nothing about the
398
+ drift between journal and reality. The README now says so.
399
+
400
+ ### Fixed before release
401
+
402
+ - **`compact` could destroy an archive it had already written.** A branch that
403
+ compacts a month, merges, and then delivers one more event for that month
404
+ gets a fresh month directory beside the archive — `append` files an event by
405
+ its own timestamp. The second compaction replaced the archive with the
406
+ directory's contents alone. Reproduced: three events became one, silently, in
407
+ a journal whose one promise is that nothing is lost. Archives are merged by
408
+ id now, and the dry run counts only what is not archived yet. The primitive
409
+ had been this way since 0.1; this release is what made it reachable.
410
+ - **A reopened task stayed `done` on the cumulative flow chart** and was not
411
+ counted as work in progress, because `task.reopened` moves a task without a
412
+ `task.moved`. Both now follow it.
413
+ - **A finished task went on accruing blocked days.** Work that is over is not
414
+ blocked, whatever its blocker is doing.
415
+ - **A started boundary that is not one of the columns inflated work in progress**
416
+ instead of emptying the report — the default state of every board that renames
417
+ `in_progress`, and the opposite of what `board config` warns.
418
+ - **`--started` was dropped when given with `--statuses` or `--dod`**, which is
419
+ the natural first command on a custom board. It travels in the same event now,
420
+ validated against the list being set.
421
+ - **The sprint report and `report flow` disagreed about what counts as started.**
422
+ A task that skipped the started column had a cycle time in one and no actual
423
+ hours in the other. One definition serves both.
424
+ - **A deferred `task.reopened` read the boundary as of the end of the fold**
425
+ rather than as of its own position in ULID order. Resolved the way claims and
426
+ criteria already are.
427
+ - `--started done` was accepted, `--since 007` became 7 before the parser saw
428
+ it, `--keep-months abc` reported "NaN", and nothing said the dates are UTC.
429
+ - **The "cold start" performance figure was a warm one.** The perf test removed
430
+ the state cache once, then took the best of three runs — the first run built
431
+ the cache and the next two read it, so "11 ms cold" was a 12 ms warm read.
432
+ Measured with the cache removed before every run: **199 ms cold when every
433
+ event is a separate file, 21 ms cold with a compacted archive.** The first of
434
+ those is the budget itself, with no room. The test now removes the cache per
435
+ iteration, the README carries the honest numbers, and the finding feeds the
436
+ reports work: compaction exists in the core but is not yet a command anyone
437
+ can run.
438
+ - **Two commands could write outside the repository.** `task doc add` and
439
+ `board export --file` both described containment in a comment and did not
440
+ enforce it: `../pwned.md` created a file in the parent directory, and the doc
441
+ link went into the append-only journal as a path meaningless on any other
442
+ machine. Both now resolve and verify before anything is written.
443
+ - **A task title containing the board end marker corrupted README.md**, and the
444
+ corruption grew by one copy on every export. The marker is now neutralised
445
+ where text enters, and the end marker is searched for after the begin marker
446
+ rather than from the top of the file.
447
+ - **A failed `gh issue list` read as "no issue exists"**, so a rate limit
448
+ created a duplicate and reported success. The lookup is now a value rather
449
+ than an absence, and runs once for the batch instead of once per task.
450
+ - **A batch that failed halfway said nothing about what was already
451
+ published**, and the JSON path discarded the partial record entirely. Both now
452
+ name it.
453
+ - **`@kadence/github` was never typechecked or built in CI** — nothing imports
454
+ its entry point, so the root config never saw it. That is why two of the bugs
455
+ above survived to review. CI now gates the package, and greps the core bundle
456
+ for network imports.
457
+ - Decisions and milestones vanished from an export of a board with no tasks yet.
458
+ - A `|` in a configured status broke the Markdown table.
459
+ - **Three event types were dropped instead of held pending.**
460
+ `milestone.task_added`, `milestone.closed` and `task.criterion_checked` were
461
+ handled as they arrived, so an event carrying a lower ULID than the entity it
462
+ names vanished — no `pending`, no `rejected`, no trace. Reachable for the same
463
+ reason claims are: clocks disagree between machines, so an assignment written
464
+ on a lagging clock sorts before the `task.created` it refers to. Milestone
465
+ events now go through the same defer path every task event uses, and criterion
466
+ checked-state is folded from the task's history, where the order is ULID order.
467
+ - **`completion install` could overwrite a file it did not write.** Every
468
+ generated script now carries a marker; a file without it is left alone and the
469
+ command says what is there, with `--force` for the deliberate case. Both target
470
+ directories are shared — a distribution package writes into the bash one.
471
+ - **The `milestone` field could carry a raw ULID** when the milestone was created
472
+ on a branch that has not merged. The contract promises `MS-N`; it is now `MS-N`
473
+ or `null`.
474
+ - `--branch` on a detached HEAD returned `invalid_argument` and exit 2. The flag
475
+ was understood; the repository state is what refuses, so it is
476
+ `conflicting_state` and exit 1.
477
+ - `--summary` omitted `contestedBy`, so a contested task read as a cleanly owned
478
+ one in the response large boards are steered towards.
479
+ - Acceptance criteria were reachable only through `task show`, which made
480
+ "which tasks have unchecked criteria" cost one call per task. `criteria` and
481
+ `openCriteria` are now on every task record.
482
+
483
+ ### Notes
484
+
485
+ The plan set a target of 80 KB for `--summary` on a 1000-task board, taken from
486
+ Probe C. **That target was wrong, and the measurement says so:** a full board of
487
+ 1000 tasks is 855 KB, `--summary` is 275 KB, and even the narrowest useful set
488
+ of three fields is 104 KB. Task titles are irreducible — 1000 of them do not fit
489
+ in 80 KB. The property the flag actually exists for is a different one and it
490
+ holds: a summary record costs the same whether the task carries one event or
491
+ five hundred, while the full record grows past fifty times the size. That is
492
+ what the test pins.
493
+
494
+ ### Notes
495
+
496
+ The bundle grew from 35 KB to 71 KB across 0.4. The export templates sit in the
497
+ fast path rather than behind a dynamic import, and that was measured rather than
498
+ assumed: reading the whole bundle costs under a millisecond, and every
499
+ non-interactive command still runs in about 100 ms against a 200 ms budget. The
500
+ lazy-import rule exists for `blessed`, a heavy third-party dependency that reads
501
+ terminfo at runtime — not for 16 KB of our own strings. `blessed` is still absent
502
+ from `dist/cli.js`.
503
+
504
+ At release the build prints an 82 KB bundle, `npm pack` a 76 KB tarball with
505
+ 233 KB unpacked, and `--version` returns in 60–70 ms on this machine; `blessed`
506
+ is still absent from `dist/cli.js`.
507
+
508
+ Claim state is folded from each task's own history rather than as events
509
+ arrive. It has to be: an event about a task whose `task.created` has not merged
510
+ is replayed after the main loop, so a claim carrying a lower ULID could be
511
+ applied last and hand the task to the wrong person. Reachable rather than
512
+ theoretical, because clocks disagree between machines.
513
+
514
+ The TUI additions were exercised by hand by the owner on 2026-09-13 before the
515
+ tag, as CLAUDE.md requires: four TUI bugs have shipped past a green suite in
516
+ this project, so a green suite alone is not evidence there.
517
+
3
518
  ## [0.3.2] — 2026-09-09
4
519
 
5
520
  Nothing in the package changed. `src/`, `scripts/`, `package.json` and the