kadence 0.3.2 → 0.4.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
@@ -1,5 +1,351 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1] — 2026-09-13
4
+
5
+ `0.4.0` was tagged and never published: these three were found by using it
6
+ before it reached anyone, so `0.4.1` is the first published 0.4. Nothing below
7
+ changes the shape of an event or renames a field in `kadence/v1`.
8
+
9
+ ### Fixed
10
+
11
+ - **The fold dropped who wrote a comment or a history line.** Every event
12
+ carries `source` — `"human"` or `"agent"`, from `KADENCE_SOURCE` — and it is
13
+ validated on read, but the projection kept it only on decisions and notes. So
14
+ `task show` printed an agent's comment exactly like a person's, and
15
+ `task show --json` gave an agent no way to tell them apart: `actor` is the git
16
+ identity, which a person and their agent share. The one surface the product's
17
+ headline is about could not show the thing it claims. Comments and history
18
+ entries now carry `source`, `task show` marks an agent's line `[agent]` the
19
+ way `decision list` already did, and a note shows the mark it always knew.
20
+ The snapshot shape test now records nested records too — it is what should
21
+ have failed when `source` reached the event and not the comment. Cache version
22
+ bumped to `kadence-snapshot/11`.
23
+ - **`kadence ready` offered work that had already started.** The filter knew
24
+ about finished, blocked-by and claimed-by-others, and nothing about the board:
25
+ a task sitting in `in_review`, or parked in the `blocked` column, was listed
26
+ first with `kadence task claim KAD-1` printed under it. An agent following
27
+ `prime → ready → claim` took work somebody was reviewing. Ready now means
28
+ ready to *start* — anything in the started column or past it is left out,
29
+ using the board's own boundary, so a team that renames its columns keeps a
30
+ `ready` that means what its board means. When that is why the list is empty,
31
+ the message says so: `Nothing ready (2 already started)`.
32
+ - **The performance fixture moved all 500 tasks into `in_progress`**, so the
33
+ `ready` guardrail was timing a filter over an empty answer and asserting only
34
+ that it was fast. Every third move parks a task back in `todo` now; the
35
+ guardrail times a real result.
36
+
37
+ ## [0.4.0] — 2026-09-13
38
+
39
+ Planned as slices 0.4.0 through 0.4.4 and published as one minor release.
40
+
41
+ The agent loop: four commands that answer "what next" without folding the whole
42
+ board by hand. Everything here is additive within `kadence/v1` — no field or
43
+ command was renamed, and `--json` responses gain keys rather than losing them.
44
+
45
+ ### Added
46
+
47
+ - **`kadence ready`** — open tasks with no live blockers, not claimed by anyone
48
+ else, priority first and age second. `--json` returns five fields per task
49
+ rather than the whole record, because this goes into an agent's context on
50
+ every session. An empty result names the reason: how many are blocked, how
51
+ many belong to someone else. Measured at 9 ms over 10,000 events.
52
+ - **`kadence prime`** — the session preamble: active sprint and days left, your
53
+ claimed and in-progress work, how many tasks are ready, decisions in force by
54
+ title, the last five notes, and four commands to go deeper. Held to 40 lines
55
+ and 3 KB by a test; 16 lines and 493 bytes on this repository. It carries the
56
+ part that changes and points at the part that does not, because the static
57
+ half already lives in `AGENTS.md`.
58
+ - **`kadence init --hooks`** — adds a `SessionStart` hook running `kadence prime`
59
+ to `.claude/settings.json`. Only with the flag: that file belongs to the user
60
+ and is committed to their repository. The write is an upsert — hooks of other
61
+ events, other matchers and other commands survive, running it twice leaves one
62
+ hook, and a file that does not parse is reported rather than replaced.
63
+ - **`kadence task claim` / `task release`** — take a task, or give it back.
64
+ `claim` with no argument takes the top of `ready`, which is one step instead
65
+ of two. There is **no lock**, and the message says so: two machines can each
66
+ claim before either pushes. See [ADR-011](docs/decisions/011-claims-as-events.md).
67
+ - **`kadence note "text" [--task KAD-1]`** and `note list` — something learned
68
+ that was never a choice. Deliberately not a decision: no `--why`, no
69
+ `supersedes`, no number. The help says when to reach for `decision add`
70
+ instead. Notes show up in `task show` and in `prime`.
71
+ - **`claimedBy` and `contestedBy`** on the task record in `--json`, and a
72
+ `Claimed:` line in `task show`.
73
+ - **TUI**: `R` shows only what can be started now, `C` claims the selected task
74
+ or releases it if it is yours, and a card carries a claim mark — a separate
75
+ colour when the claim is contested. Both actions call the same functions the
76
+ CLI does.
77
+
78
+ ### Changed
79
+
80
+ - **A contested claim is reported, not rejected.** When two people claim one
81
+ task, the earliest by ULID holds and the later one is kept, with the task
82
+ reading `contested` under both names. Rejecting the second would make the
83
+ owner depend on which branch merged first — the same invariant that keeps
84
+ dependency cycles and orphaned status columns alive rather than dropped. It is
85
+ the first conflict in kadence that names people rather than values.
86
+ - **The state cache version moved from 2 to 4.** The projected shape changed
87
+ twice: three claim fields on a task, then notes on the project state. Missing
88
+ exactly this bump is what shipped as a bug in 0.3.1, so it is now pinned to
89
+ the field lists by a test.
90
+ - The agent section written into `AGENTS.md` and `CLAUDE.md` grew by one line —
91
+ `kadence prime` first — and by no more than that.
92
+
93
+ ### Added — the branch, the human, and the evidence behind done (0.4.1)
94
+
95
+ - **`kadence task list --branch`** — the work this branch introduced, measured
96
+ against `--base` (`main`, or `init.defaultBranch`). Membership is derived from
97
+ git at read time and never stored: writing a branch name into an event would
98
+ go stale the moment the branch is renamed or merged, and would make the same
99
+ events fold differently depending on where they were written. A detached HEAD
100
+ and an unknown base both fail by name rather than reporting no work.
101
+ The measurement that justified the flag, including what would make it wrong,
102
+ is in [branch-context-2026-09.md](docs/research/branch-context-2026-09.md).
103
+ - **Acceptance criteria** — `task ac add|check|uncheck|list`. The number is a
104
+ position in the folded list, assigned like `KAD-N` and never stored, so two
105
+ branches can each add a criterion and merge without renumbering. Moving a task
106
+ to `done` with unchecked criteria **warns and proceeds**: the checklist is
107
+ evidence, not a gate, and refusing would make the board lie about where the
108
+ work is.
109
+ - **Definition of done** — `board config --dod "tests green,docs updated"`. The
110
+ criteria are **copied** into each new task, never referenced, so raising the
111
+ standard cannot reach back and change what finished work had promised.
112
+ `task add --no-dod` skips them.
113
+ - **Milestones** — `milestone create|add|list|close`. Grouping by outcome, where
114
+ an epic groups by structure and a sprint by time. Not a third hierarchy: a
115
+ task carries at most one, as a field beside `sprint`. `MS-N` is derived from
116
+ ULID order like every other label, and progress is counted in points, the same
117
+ unit as velocity and burndown.
118
+ - **`kadence stats`** — counts by status and assignee, open blockers, contested
119
+ claims, and the velocity of the last three closed sprints.
120
+ - **`kadence completion install`** — zsh, bash and fish, generated from one list
121
+ rather than three hand-written scripts. Without a terminal it prints the
122
+ script instead of writing, because that is how people pipe it into a file.
123
+ For zsh it writes the file and prints the `fpath` line rather than editing a
124
+ shell rc, which is the user's file.
125
+ - **`board --json --summary`** — the column state without `history` and
126
+ `comments`. Those are the two fields that grow with how long a task has been
127
+ worked on rather than with how many tasks there are.
128
+ - **TUI**: `b` shows only what the branch introduced, `M` filters to one
129
+ milestone, and the card dialog carries the acceptance-criteria checklist with
130
+ space to toggle. Every toggle calls the same command the CLI does.
131
+
132
+ ### Changed
133
+
134
+ - `task list --json` and `board --json` gain a `milestone` field on each task,
135
+ carrying the `MS-N` label. The response without `--summary` is otherwise
136
+ unchanged; the contract only ever gains.
137
+ - The state cache version moved again with the projected shape.
138
+
139
+ ### Added — reports, and the two things they needed first (0.4.3)
140
+
141
+ What the neighbours call reports is, here, a fold over timestamps the journal
142
+ already carries. The reasoning, the sources and the verdict per report are in
143
+ [reports-discovery-2026-09.md](docs/research/reports-discovery-2026-09.md).
144
+ Velocity and cycle time stay out of the headline; they are a consequence, served
145
+ to the person who asks.
146
+
147
+ - **`kadence report flow`** — the four flow metrics the Kanban Guide mandates,
148
+ and what sits around them: work in progress, throughput per ISO week, cycle /
149
+ lead / response time as **p50 / p85 / p95 in calendar days**, aging work
150
+ against the p85, created versus resolved, and days spent blocked. There is no
151
+ mean anywhere, on purpose. Every response names the window and the column
152
+ work counts as started from, so a number is never quoted without its terms.
153
+ Empty sections say why they are empty.
154
+ - **`kadence report cfd`** — tasks per column at the end of every day of the
155
+ window. A move backwards subtracts. The first version re-walked each task's
156
+ history per day and cost 150 ms on 10,000 events; it is a single pass now, at
157
+ 4 ms.
158
+ - **`kadence board config --started <status>`** — the column cycle time and a
159
+ sprint's actual hours are measured from. It was the literal `in_progress` in
160
+ three places while columns were configurable, so a board named
161
+ `todo,doing,review,done` closed every sprint with `actualHours: null` and
162
+ never said why. It is a folded setting now, validated against the live
163
+ columns, and a column change that strands it warns instead of going quiet.
164
+ - **`kadence compact`** — folds months older than `--keep-months` into one
165
+ archive file each. The primitive existed since 0.1 and was reachable only from
166
+ tests. `--dry-run` says what would move and writes nothing. It touches no git
167
+ state, and it says the one thing worth knowing: an archived month is a single
168
+ file, so compact on one branch and merge before compacting on another.
169
+
170
+ ### Added — three experiments (0.4.2)
171
+
172
+ Each of these was a standing "no". Each is now tried in the shape that survives
173
+ the three constraints, and each has a kill condition written before the code, in
174
+ [feature-adoption-2026-09.md](docs/product/feature-adoption-2026-09.md).
175
+
176
+ - **`kadence board export --html`** — the board, the sprint, the burndown,
177
+ milestones and decisions in force, as **one self-contained file**. No script
178
+ tag, no stylesheet link, no font, no image: the page asks the network for
179
+ nothing and opens from disk. That is what makes it an experiment in place of a
180
+ web UI rather than a web UI with extra steps. A test asserts the absence, not
181
+ the appearance. Acceptance criteria are printed in full rather than counted,
182
+ because a file attached to a pull request is evidence and "2/3" is not.
183
+ - **`kadence board export --md`**, and `--readme` to update the section between
184
+ markers in an existing README. It will not create one — that would be a
185
+ different command and a bigger promise.
186
+ - **`@kadence/github`** — a separate package that publishes tasks to GitHub
187
+ Issues **in one direction**, by shelling out to `gh`. The core is unchanged
188
+ and still opens no socket; the credential stays with a tool the user already
189
+ trusts. A marker in the issue body makes a second publish an edit rather than
190
+ a duplicate, and the issue itself says that edits made there are overwritten.
191
+ Nothing is ever read back. See
192
+ [ADR-012](docs/decisions/012-network-only-in-packages.md), written before the
193
+ code. Every test runs against a recorded `gh`, never the real one.
194
+ - **`kadence task doc add KAD-1 docs/design.md`** — creates the file from a
195
+ four-line template and records the link in one call. It never overwrites: a
196
+ file that is already there is the document, and replacing it with a template
197
+ would destroy the thing being linked.
198
+
199
+ ### Added — attention signals, and labels that survive a merge (0.4.4)
200
+
201
+ - **`kadence report attention`** — work the board presents as active while
202
+ nobody is moving it. Four signals, one definition: `stalled` (no event for N
203
+ days), `unowned` (in flight for N days with no assignee and no claim),
204
+ `stale_claim` (a claim older than N days with no move since), and
205
+ `dead_blocker` (`blockedBy` pointing at a finished task — the one signal that
206
+ waits for no threshold). `--since` is the days of silence, default 7. No new
207
+ event type, no new field: the report is a fold over what the journal already
208
+ holds, and it costs 0.5 ms over 10,000 events on top of the fold.
209
+
210
+ Two candidates were left out on purpose and the module says why. "Open task
211
+ with no owner" is the definition of a backlog. "Criteria added and never
212
+ checked" would fire on every task in any repository that configured a
213
+ Definition of Done, because `task add` copies it in unchecked — loudest in
214
+ the repositories that took our advice.
215
+ - **`prime` carries an attention line** — up to three tasks, and only when
216
+ there is something to say.
217
+ - **`task.label_added` / `task.label_removed`** — labels move as deltas. This
218
+ was the one field where "every intent is preserved" was untrue: `task.updated`
219
+ carried the whole array and the fold replaced the whole array, so two branches
220
+ adding different labels merged without a conflict and one label vanished with
221
+ no warning. Not a merge failure — a fold storing *state* in an event, the
222
+ exact mistake the product exists to avoid. Reasoning in
223
+ [ADR-013](docs/decisions/013-labels-as-deltas.md).
224
+ - **`labels` in `ready --json`**, the seventh field, and in the guaranteed set.
225
+
226
+ Source for this slice: a tech lead's feedback of 2026-09-10 — drift comes not
227
+ from where state lives but from work that stops being watched. Append-only
228
+ removes the drift between board and journal; it promises nothing about the
229
+ drift between journal and reality. The README now says so.
230
+
231
+ ### Fixed before release
232
+
233
+ - **`compact` could destroy an archive it had already written.** A branch that
234
+ compacts a month, merges, and then delivers one more event for that month
235
+ gets a fresh month directory beside the archive — `append` files an event by
236
+ its own timestamp. The second compaction replaced the archive with the
237
+ directory's contents alone. Reproduced: three events became one, silently, in
238
+ a journal whose one promise is that nothing is lost. Archives are merged by
239
+ id now, and the dry run counts only what is not archived yet. The primitive
240
+ had been this way since 0.1; this release is what made it reachable.
241
+ - **A reopened task stayed `done` on the cumulative flow chart** and was not
242
+ counted as work in progress, because `task.reopened` moves a task without a
243
+ `task.moved`. Both now follow it.
244
+ - **A finished task went on accruing blocked days.** Work that is over is not
245
+ blocked, whatever its blocker is doing.
246
+ - **A started boundary that is not one of the columns inflated work in progress**
247
+ instead of emptying the report — the default state of every board that renames
248
+ `in_progress`, and the opposite of what `board config` warns.
249
+ - **`--started` was dropped when given with `--statuses` or `--dod`**, which is
250
+ the natural first command on a custom board. It travels in the same event now,
251
+ validated against the list being set.
252
+ - **The sprint report and `report flow` disagreed about what counts as started.**
253
+ A task that skipped the started column had a cycle time in one and no actual
254
+ hours in the other. One definition serves both.
255
+ - **A deferred `task.reopened` read the boundary as of the end of the fold**
256
+ rather than as of its own position in ULID order. Resolved the way claims and
257
+ criteria already are.
258
+ - `--started done` was accepted, `--since 007` became 7 before the parser saw
259
+ it, `--keep-months abc` reported "NaN", and nothing said the dates are UTC.
260
+ - **The "cold start" performance figure was a warm one.** The perf test removed
261
+ the state cache once, then took the best of three runs — the first run built
262
+ the cache and the next two read it, so "11 ms cold" was a 12 ms warm read.
263
+ Measured with the cache removed before every run: **199 ms cold when every
264
+ event is a separate file, 21 ms cold with a compacted archive.** The first of
265
+ those is the budget itself, with no room. The test now removes the cache per
266
+ iteration, the README carries the honest numbers, and the finding feeds the
267
+ reports work: compaction exists in the core but is not yet a command anyone
268
+ can run.
269
+ - **Two commands could write outside the repository.** `task doc add` and
270
+ `board export --file` both described containment in a comment and did not
271
+ enforce it: `../pwned.md` created a file in the parent directory, and the doc
272
+ link went into the append-only journal as a path meaningless on any other
273
+ machine. Both now resolve and verify before anything is written.
274
+ - **A task title containing the board end marker corrupted README.md**, and the
275
+ corruption grew by one copy on every export. The marker is now neutralised
276
+ where text enters, and the end marker is searched for after the begin marker
277
+ rather than from the top of the file.
278
+ - **A failed `gh issue list` read as "no issue exists"**, so a rate limit
279
+ created a duplicate and reported success. The lookup is now a value rather
280
+ than an absence, and runs once for the batch instead of once per task.
281
+ - **A batch that failed halfway said nothing about what was already
282
+ published**, and the JSON path discarded the partial record entirely. Both now
283
+ name it.
284
+ - **`@kadence/github` was never typechecked or built in CI** — nothing imports
285
+ its entry point, so the root config never saw it. That is why two of the bugs
286
+ above survived to review. CI now gates the package, and greps the core bundle
287
+ for network imports.
288
+ - Decisions and milestones vanished from an export of a board with no tasks yet.
289
+ - A `|` in a configured status broke the Markdown table.
290
+ - **Three event types were dropped instead of held pending.**
291
+ `milestone.task_added`, `milestone.closed` and `task.criterion_checked` were
292
+ handled as they arrived, so an event carrying a lower ULID than the entity it
293
+ names vanished — no `pending`, no `rejected`, no trace. Reachable for the same
294
+ reason claims are: clocks disagree between machines, so an assignment written
295
+ on a lagging clock sorts before the `task.created` it refers to. Milestone
296
+ events now go through the same defer path every task event uses, and criterion
297
+ checked-state is folded from the task's history, where the order is ULID order.
298
+ - **`completion install` could overwrite a file it did not write.** Every
299
+ generated script now carries a marker; a file without it is left alone and the
300
+ command says what is there, with `--force` for the deliberate case. Both target
301
+ directories are shared — a distribution package writes into the bash one.
302
+ - **The `milestone` field could carry a raw ULID** when the milestone was created
303
+ on a branch that has not merged. The contract promises `MS-N`; it is now `MS-N`
304
+ or `null`.
305
+ - `--branch` on a detached HEAD returned `invalid_argument` and exit 2. The flag
306
+ was understood; the repository state is what refuses, so it is
307
+ `conflicting_state` and exit 1.
308
+ - `--summary` omitted `contestedBy`, so a contested task read as a cleanly owned
309
+ one in the response large boards are steered towards.
310
+ - Acceptance criteria were reachable only through `task show`, which made
311
+ "which tasks have unchecked criteria" cost one call per task. `criteria` and
312
+ `openCriteria` are now on every task record.
313
+
314
+ ### Notes
315
+
316
+ The plan set a target of 80 KB for `--summary` on a 1000-task board, taken from
317
+ Probe C. **That target was wrong, and the measurement says so:** a full board of
318
+ 1000 tasks is 855 KB, `--summary` is 275 KB, and even the narrowest useful set
319
+ of three fields is 104 KB. Task titles are irreducible — 1000 of them do not fit
320
+ in 80 KB. The property the flag actually exists for is a different one and it
321
+ holds: a summary record costs the same whether the task carries one event or
322
+ five hundred, while the full record grows past fifty times the size. That is
323
+ what the test pins.
324
+
325
+ ### Notes
326
+
327
+ The bundle grew from 35 KB to 71 KB across 0.4. The export templates sit in the
328
+ fast path rather than behind a dynamic import, and that was measured rather than
329
+ assumed: reading the whole bundle costs under a millisecond, and every
330
+ non-interactive command still runs in about 100 ms against a 200 ms budget. The
331
+ lazy-import rule exists for `blessed`, a heavy third-party dependency that reads
332
+ terminfo at runtime — not for 16 KB of our own strings. `blessed` is still absent
333
+ from `dist/cli.js`.
334
+
335
+ At release the build prints an 82 KB bundle, `npm pack` a 76 KB tarball with
336
+ 233 KB unpacked, and `--version` returns in 60–70 ms on this machine; `blessed`
337
+ is still absent from `dist/cli.js`.
338
+
339
+ Claim state is folded from each task's own history rather than as events
340
+ arrive. It has to be: an event about a task whose `task.created` has not merged
341
+ is replayed after the main loop, so a claim carrying a lower ULID could be
342
+ applied last and hand the task to the wrong person. Reachable rather than
343
+ theoretical, because clocks disagree between machines.
344
+
345
+ The TUI additions were exercised by hand by the owner on 2026-09-13 before the
346
+ tag, as CLAUDE.md requires: four TUI bugs have shipped past a green suite in
347
+ this project, so a green suite alone is not evidence there.
348
+
3
349
  ## [0.3.2] — 2026-09-09
4
350
 
5
351
  Nothing in the package changed. `src/`, `scripts/`, `package.json` and the
package/README.md CHANGED
@@ -76,7 +76,15 @@ journal is append-only, one file per event.
76
76
  keeps every step, so «how did we get here» has an answer.
77
77
 
78
78
  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 reality.
79
+ which is why the board can never drift from the journal. No one maintains a
80
+ column by hand, so no column can be stale in the way a task file can.
81
+
82
+ **What that does not buy you.** A journal records what was written to it. If a
83
+ piece of work stops being touched, the board stays perfectly accurate and
84
+ perfectly uninformative about it — the same way `git log` is honest about a
85
+ branch nobody pushed to. kadence removes the drift between what is shown and
86
+ what was recorded. It cannot remove the drift between what was recorded and
87
+ what is actually happening; that still takes someone looking.
80
88
 
81
89
  ---
82
90
 
@@ -104,11 +112,69 @@ kadence init
104
112
 
105
113
  kadence sprint create "Sprint 14"
106
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"
107
116
  kadence task comment KAD-1 "Session cookie is fine — the redirect drops it."
108
117
  kadence task move KAD-1 done
109
118
  kadence sprint close
110
119
  ```
111
120
 
121
+ **When several people — or several agents — work the same board:**
122
+
123
+ ```bash
124
+ kadence ready # open, unblocked, nobody else's
125
+ kadence task claim # take the top of that list, in one step
126
+ kadence task release KAD-1
127
+ ```
128
+
129
+ There is no lock, and the message says so. Two machines can each claim before
130
+ either pushes; the merge keeps both claims and the task reads `contested` with
131
+ both names. Refusing the second one would make the owner depend on which branch
132
+ merged first, and that is the property the whole design rests on.
133
+
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:**
148
+
149
+ ```bash
150
+ kadence board export --html # one self-contained file: no server, no network
151
+ kadence board export --md --readme
152
+ ```
153
+
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
161
+ ```
162
+
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.
166
+
167
+ **And what the branch you are on is actually about:**
168
+
169
+ ```bash
170
+ kadence task list --branch
171
+ ```
172
+
173
+ Nothing is stored for that: which tasks belong to a branch lives in git's
174
+ history and is read when you ask. It narrows the answer between three and
175
+ twenty times, [measured](docs/research/branch-context-2026-09.md) on real board
176
+ sizes.
177
+
112
178
  **The board, when you want to look at it:**
113
179
 
114
180
  ```
@@ -119,7 +185,7 @@ $ kadence ui
119
185
  | ^# KAD-1 Auth epic || . KAD-4 Tokens @dev||!! KAD-7 Crash [] || v KAD-2 Export |
120
186
  | * KAD-3 Login form || || || v KAD-5 Docs |
121
187
  +---------------------++--------------------++--------------------++--------------------+
122
- arrows move enter details m status a assign e edit s sprint / filter q quit
188
+ arrows move enter details m status a assign e edit s sprint R ready b branch q quit
123
189
  ```
124
190
 
125
191
  Keyboard, mouse, drag between columns, every field editable in place. It calls
@@ -149,6 +215,18 @@ from state changes the team already made.
149
215
  Files first. Every command speaks `--json`, every response carries
150
216
  `schema: "kadence/v1"`, stdout is JSON and nothing else, warnings go to stderr.
151
217
 
218
+ Start a session with one command:
219
+
220
+ ```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
223
+ ```
224
+
225
+ `prime` is held to forty lines and three kilobytes by a test, because it is paid
226
+ for on every turn that follows. `kadence init --hooks` will add it as a
227
+ `SessionStart` hook — only with the flag, because `.claude/settings.json` is
228
+ yours.
229
+
152
230
  ```bash
153
231
  kadence board --json
154
232
  kadence task show KAD-1 --json # full history and comments
@@ -209,11 +287,11 @@ either moves both or changes nothing. A typo does not leave half a board.
209
287
 
210
288
  | | |
211
289
  |---|---|
212
- | Install | 38 KB packed — 132 KB of kadence, plus 1.8 MB of blessed |
213
- | Startup | 80 ms |
214
- | 10,000 events | 28 ms cold, 7 ms warm |
215
- | Journal on disk | 1.9 MB |
216
- | One task, as an agent reads it | 948 bytes — the same at 10 tasks or 1,000 |
290
+ | Install | 76 KB packed — 233 KB of kadence, plus 1.8 MB of blessed |
291
+ | Startup | 65 ms |
292
+ | 10,000 events | 21 ms cold with a compacted archive, 12 ms warm — 199 ms cold if every event is still a separate file |
293
+ | Journal on disk | under 5 MB |
294
+ | One task, as an agent reads it | 982 bytes, or 304 with `--summary` — the same however long the task has been worked on |
217
295
 
218
296
  These are tests. They fail the build on regression, which is why they are still
219
297
  true.
@@ -224,7 +302,7 @@ true.
224
302
 
225
303
  **Verified.** The merge thesis, on real git branches. Performance and size, by
226
304
  tests that fail if they regress. That the conflict problem exists in the wild —
227
- measured, not assumed. 548 tests, including an end-to-end run through the
305
+ measured, not assumed. 898 tests, including an end-to-end run through the
228
306
  installed binary.
229
307
 
230
308
  **Not verified.** That teams and their AI agents actually lose enough context to want
@@ -256,8 +334,9 @@ unit-tested.
256
334
  ```
257
335
 
258
336
  Every command appends one event. State is folded from the journal on read, so
259
- the board cannot drift from reality. Two branches writing at once produce two
260
- different files, and git merges them without a conflict by construction.
337
+ the board cannot drift from the journal — nothing is maintained by hand. Two
338
+ branches writing at once produce two different files, and git merges them
339
+ without a conflict by construction.
261
340
 
262
341
  Design decisions, each recording what was measured and what would make us
263
342
  revisit it: [docs/decisions/](docs/decisions/).
@@ -266,7 +345,7 @@ revisit it: [docs/decisions/](docs/decisions/).
266
345
 
267
346
  ```bash
268
347
  npm install
269
- npm test # 548 tests
348
+ npm test # 898 tests
270
349
  npm run build # 35 KB bundle
271
350
  ```
272
351
 
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import{p as ee}from"./chunk-CDGGOWGY.js";import S from"blessed";var u={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 te(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,o){return`{${o}-fg}${n}{/}`}function q(n,o){let d=k(pe[n.priority],me[n.priority]),h=k(ye[n.type]??"\xB7","gray"),f=k(n.label,"cyan"),c=[];n.blockedBy.length>0&&c.push(k("\u2298","red")),n.contestedBy.length>0?c.push(k("\u2691","magenta")):n.claimedBy!==null&&c.push(k("\u2713","green"));let E=n.criteria.filter(L=>!L.checked).length;if(E>0?c.push(k(`${n.criteria.length-E}/${n.criteria.length}`,"yellow")):n.criteria.length>0&&c.push(k("\u2611","green")),n.comments.length>0&&c.push(k("\u{1F4AC}","gray")),n.due!==null){let L=n.due<new Date().toISOString().slice(0,10);c.push(k(L?"\u23F0":"\u{1F4C5}",L?"red":"gray"))}let C=[n.assignee!==null?k(`@${n.assignee.split("@")[0]}`,"gray"):"",n.estimate!==null?k(String(n.estimate),"yellow"):"",c.join("")].filter(L=>L.length>0).join(" "),p=`${N(d)}${N(h)} ${N(f)} `,y=C.length>0?` ${N(C)}`:"",T=Math.max(4,o-p.length-y.length),O=[...n.title],U=O.length>T?`${O.slice(0,T-1).join("")}\u2026`:n.title;return`${d}${h} ${f} ${U}${C.length>0?` ${C}`:""}`}function G(n,o){return o?`{cyan-bg}{black-fg}${N(n)}{/}`:` ${n}`}function W(n,o,d,h=12){let f=`${n.padEnd(h)} ${o}`;return d?`{cyan-bg}{black-fg}\u25B8 ${f}{/}`:` ${f}`}function N(n){return n.replace(/\{[^}]*\}/g,"")}var ne=["\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 ie(n){let o=[],d=!1;function h(){d=!0,setImmediate(()=>{d=!1})}return{dispatch(f){if(d)return;let c=o[o.length-1];c!==void 0?c(f):n(f)},push(f){o.push(f),h();let c=!1;return()=>{if(c)return;c=!0;let E=o.lastIndexOf(f);E!==-1&&o.splice(E,1),h()}},isDialogOpen:()=>o.length>0,depth:()=>o.length}}function se(n){if(n.ch==="")return!1;let o=n.ch.charCodeAt(0);return o<32||o===127}var be=["up","down","pageup","pagedown","home","end"],re=["escape","q"];function z(n){return be.includes(n.name)}function oe(n){return re.includes(n.name)||re.includes(n.ch)}function _(n,o,d){return d===0?0:(n+o+d)%d}var le="cancelled";function Se(n){let o=S.screen({smartCSR:!0,title:"kadence",fullUnicode:!0,mouse:!0}),{state:d}=n.reload(),h=n.branchEventIds(),f="",c=!1,E=!1,C="",p=[],y=0,T=ie(t=>ce(t));function O(t=0){try{o.destroy()}catch{}process.exit(t)}process.on("SIGINT",()=>O(0)),process.on("SIGTERM",()=>O(0)),o.program.on("keypress",(t,r)=>{let i={ch:t??"",name:r?.name??""};(i.name==="C-c"||i.ch==="")&&O(0),!(se(i)&&i.name!=="enter"&&i.name!=="escape")&&T.dispatch(i)});let U=S.box({parent:o,top:0,height:1,width:"100%",tags:!0,style:{fg:u.headerFg,bg:u.headerBg}}),L=S.box({parent:o,bottom:0,height:1,width:"100%",tags:!0,content:` ${ne}`,style:{fg:u.hint}}),J=S.box({parent:o,bottom:1,height:1,width:"100%",tags:!0,style:{fg:u.warn}});function b(t,r=u.warn){J.setContent(` ${t}`),J.style.fg=r,o.render()}function A(){let t=d.statuses.filter(i=>i!==le),r=d.orphanStatuses.filter(i=>i!==le);return[...t,...r]}function ae(t){let r=f.toLowerCase(),i=c?new Set(ee(d.tasks,{viewer:n.actor,statuses:d.statuses,started:d.started}).map(s=>s.id)):null,e=E&&!("reason"in h)?h:null,l=e===null?null:s=>e.ids.has(s.id)||s.history.some(w=>e.ids.has(w.id)),a=C===""?null:d.milestones.find(s=>s.label===C)?.id??null;return d.tasks.filter(s=>s.status===t&&(i===null||i.has(s.id))&&(l===null||l(s))&&(a===null||s.milestone===a)&&(r===""||s.title.toLowerCase().includes(r)||(s.assignee??"").toLowerCase().includes(r)||s.labels.some(w=>w.toLowerCase().includes(r))))}function Q(){for(let i of p)i.box.destroy();p=[];let t=A(),r=Math.max(Math.floor(100/Math.max(t.length,1)),12);t.forEach((i,e)=>{let l=S.box({parent:o,top:1,left:`${e*r}%`,width:`${r}%`,bottom:2,label:` ${i} `,border:{type:"line"},style:{border:{fg:u.border},label:{fg:te(i)}}}),a=S.list({parent:l,top:0,left:0,right:0,bottom:0,keys:!1,mouse:!1,interactive:!1,tags:!0,scrollable:!0,style:{selected:{fg:u.selectedFg,bg:u.selectedBg},item:{fg:"white"}}});p.push({status:i,box:l,list:a,tasks:[],cursor:0})})}function m(t=!1){t&&(d=n.reload().state,h=n.branchEventIds()),p.length!==A().length&&Q();let r=0,i=0;p.forEach((s,w)=>{s.tasks=ae(s.status),r+=s.tasks.length,i+=s.tasks.reduce((R,F)=>R+(F.estimate??0),0);let P=s.box.width-4;s.cursor=Math.min(s.cursor,Math.max(0,s.tasks.length-1)),s.list.setItems(s.tasks.map((R,F)=>G(q(R,P),w===y&&F===s.cursor)));let Y=s.tasks.reduce((R,F)=>R+(F.estimate??0),0);s.box.setLabel(` ${s.status} (${s.tasks.length}${Y>0?`, ${Y}`:""}) `),s.box.style.border.fg=w===y?u.borderFocus:u.border});let e=d.sprints.find(s=>s.status==="active"),l=e===void 0?"no active sprint":e.name,a=(f===""?"":` filter: "${f}"`)+(c?" ready only":"")+(E&&!("reason"in h)?` branch: ${h.name}`:"")+(C===""?"":` milestone: ${C}`);U.setContent(` kadence ${l} ${r} tasks, ${i} points${a}`),d.cycles.length>0&&b(`${d.cycles.length} dependency cycle(s) \u2014 see kadence task list`,u.danger),o.render()}function I(){let t=p[y];return t?.tasks[t.cursor]}function V(){p.forEach((t,r)=>{let i=t.box.width-4;t.list.setItems(t.tasks.map((e,l)=>G(q(e,i),r===y&&l===t.cursor))),t.box.style.border.fg=r===y?u.borderFocus:u.border}),o.render()}function x(t){t!==null?b(t,u.warn):b(""),m(!0)}function K(t,r,i){let e=T.push(()=>{}),l=S.prompt({parent:o,top:"center",left:"center",width:"60%",height:7,border:{type:"line"},style:{border:{fg:u.borderFocus}},keys:!0,mouse:!0});l.input(t,r,(a,s)=>{l.destroy(),e(),o.render(),typeof s=="string"&&i(s.trim())})}function de(t){let r=S.box({parent:o,top:"center",left:"center",width:"75%",height:"80%",border:{type:"line"},label:` ${t.label} `,style:{border:{fg:u.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,g)=>({kind:"field",index:g})),...t.criteria.map(v=>({kind:"criterion",n:v.n}))],l=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:u.selectedFg,bg:u.selectedBg}}}),a=0,s=S.box({parent:r,top:i.length+t.criteria.length+1,left:1,right:1,bottom:0,tags:!0,scrollable:!0});function w(){l.setItems(e().map((g,M)=>g.kind==="field"?W(i[g.index].label,i[g.index].value(),M===a):W(` ${g.n}.`,`[${t.criteria.find($=>$.n===g.n)?.checked===!0?"x":" "}] ${t.criteria.find($=>$.n===g.n)?.text??""}`,M===a)));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(g=>` {gray-fg}${g.author}:{/} ${g.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(g=>g!=="");s.setContent(v.join(`
7
+ `)),o.render()}w();let P=T.push(v=>ge(v)),Y=()=>{P(),r.destroy(),m(!0)};function R(){d=n.reload().state;let v=d.tasks.find(g=>g.id===t.id);if(v===void 0){Y();return}Object.assign(t,v),w()}function F(){let v=Z(()=>n.edit(t.id));v!==null&&b(v),R()}function ge(v){let{ch:g,name:M}=v;if(M==="up"||g==="k")return a=_(a,-1,e().length),w();if(M==="down"||g==="j")return a=_(a,1,e().length),w();if(g===" "||M==="space"){let $=e()[a];if($===void 0||$.kind!=="criterion")return;let j=t.criteria.find(D=>D.n===$.n);if(j===void 0)return;let B=n.toggleCriterion(t.id,$.n,j.checked);return B!==null&&b(B),R()}if(oe(v))return Y();if(g==="e")return F();if(M==="enter"){let $=e()[a];if($!==void 0&&$.kind==="criterion"){let B=t.criteria.find(fe=>fe.n===$.n);if(B===void 0)return;let D=n.toggleCriterion(t.id,$.n,B.checked);return D!==null&&b(D),R()}let j=i[a];if(j===void 0)return;if(j.key==="description")return F();K(j.hint,j.value()==="\u2014"?"":j.value(),B=>{let D=n.setField(t.id,j.key,B);D!==null&&b(D),R()})}}o.render()}function ue(){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:o,top:"center",left:"center",width:60,height:Math.min(t.length+2,30),border:{type:"line"},label:" Keys ",style:{border:{fg:u.borderFocus}},tags:!0,scrollable:!0,keys:!0,mouse:!0,content:t.join(`
8
+ `)}),i=T.push(e=>{z(e)||(i(),r.destroy(),m())});r.on("click",()=>{i(),r.destroy(),m()}),o.render()}function X(t,r,i){let e=i.length>0?`
9
+
10
+ {gray-fg}${i.map(s=>`${s.key} ${s.label}`).join(" ")} esc close{/}`:`
11
+
12
+ {gray-fg}press any key to close{/}`,l=S.box({parent:o,top:"center",left:"center",width:"80%",height:"70%",border:{type:"line"},label:` ${t} `,style:{border:{fg:u.borderFocus}},tags:!0,scrollable:!0,mouse:!0,content:r+e}),a=T.push(s=>{if(z(s))return;let w=i.find(P=>P.key===s.ch);a(),l.destroy(),w!==void 0?w.run():m()});o.render()}function ce(t){let{ch:r,name:i}=t;if(i==="left"||r==="h")return y=Math.max(0,y-1),m();if(i==="right"||r==="l")return y=Math.min(p.length-1,y+1),m();if(i==="up"||r==="k"){let e=p[y];e!==void 0&&(e.cursor=_(e.cursor,-1,e.tasks.length),V());return}if(i==="down"||r==="j"){let e=p[y];e!==void 0&&(e.cursor=_(e.cursor,1,e.tasks.length),V());return}if(i==="enter"){let e=I();e!==void 0&&de(e);return}if(r==="["||r==="]"){let e=I();if(e===void 0)return;let l=r==="]"?1:-1,a=p[y+l];return a===void 0?void 0:(x(n.move(e.id,a.status)),y+=l,m())}if(r==="m"){let e=I();return e===void 0?void 0:K(`Move ${e.label} to (${A().join(", ")}):`,"",l=>x(n.move(e.id,l)))}if(r==="a"){let e=I();return e===void 0?void 0:K(`Assign ${e.label} to (or "none"):`,e.assignee??"",l=>x(n.assign(e.id,l)))}if(r==="c"){let e=I();return e===void 0?void 0:K(`Comment on ${e.label}:`,"",l=>x(n.comment(e.id,l)))}if(r==="t"){let e=I();return e===void 0?void 0:K(`Log time on ${e.label} (2h, 90m, -30m):`,"",l=>x(n.logTime(e.id,l)))}if(r==="p"){let e=I();return e===void 0?void 0:K(`Priority for ${e.label} (low, normal, high, urgent):`,e.priority,l=>x(n.setPriority(e.id,l)))}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":`,"",l=>{l.toLowerCase()==="yes"?x(n.remove(e.id)):b("Not deleted.")})}if(r==="e"){let e=I();return e===void 0?void 0:x(Z(()=>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 X("Sprint",n.sprintStatus(),[{key:"b",label:"burndown",run:()=>X("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):",f,e=>{f=e==="*"?"":e,m()});if(i==="escape"&&f!=="")return f="",b("Filter cleared."),m();if(r==="r")return b("Reloaded."),m(!0);if(r==="R")return c=!c,b(c?"Showing only what can be started now.":"Showing every task."),m();if(r==="b")return"reason"in h?b(h.reason==="detached"?"HEAD is detached, so there is no branch to filter by.":`There is no branch "${h.base}" to compare against.`,u.warn):(E=!E,b(E?`Showing only what ${h.name} introduced since ${h.base}.`:"Showing every task."),m());if(r==="M"){if(d.milestones.length===0)return b("No milestones yet. Create one with kadence milestone create.",u.warn);let e=d.milestones.map(l=>`${l.label} ${l.name}`).join(", ");return K(`Milestone (${e}; empty for all):`,C,l=>{let a=l.trim();if(a==="")return C="",b("Showing every task."),m();let s=d.milestones.find(w=>w.label.toUpperCase()===a.toUpperCase()||w.name.toLowerCase()===a.toLowerCase());if(s===void 0)return b(`No milestone "${a}".`,u.warn);C=s.label,b(`Showing ${s.label} ${s.name}.`),m()})}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 ue();(r==="q"||i==="C-c")&&O(0)}function Z(t){let r=o;r.leave();let i=t();return r.enter(),i}let H=null;p.forEach(()=>{}),o.on("mousedown",t=>{if(T.isDialogOpen())return;let r=p.findIndex(a=>{let s=a.box.left;return t.x>=s&&t.x<s+a.box.width});if(r===-1)return;y=r;let i=p[r],e=t.y-i.box.top-1,l=i.tasks[e];l!==void 0&&(i.cursor=e,H={task:l,from:r}),m()}),o.on("mouseup",t=>{if(T.isDialogOpen()||H===null)return;let r=p.findIndex(i=>{let e=i.box.left;return t.x>=e&&t.x<e+i.box.width});r!==-1&&r!==H.from&&(x(n.move(H.task.id,p[r].status)),y=r,m()),H=null}),Q(),m()}export{Se as runBoardUi};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ var I=["backlog","todo","in_progress","blocked","in_review","done","cancelled"],S="done",A="cancelled",P="in_progress",O="backlog",nt=["task","bug","story","epic"],st=["low","normal","high","urgent"];function $(t,e){for(let n=t.length-1;n>=0;n--)if(t[n].id<e)return t[n].started;return P}function pt(t){let e=[...t].sort((r,d)=>r.id<d.id?-1:r.id>d.id?1:0),n=new Map,u=new Map,a=new Map,o=new Map,p=new Map,s=[],l=null,i=[],f={started:P,changes:[]},y=[],g=[];for(let r of e)r.type==="task.created"?Y(n,r):r.type==="sprint.created"?Z(u,r):r.type==="milestone.created"&&q(a,r);for(let r of e){if(r.type==="decision.recorded"){let d=r.data??{},w=T=>typeof d[T]=="string"&&d[T].length>0?d[T]:null,c=w("title"),h=w("why");c!==null&&h!==null&&p.set(r.id,{id:r.id,label:"",title:c,why:h,rejected:w("rejected"),task:w("task"),docs:Array.isArray(d.docs)?d.docs.filter(T=>typeof T=="string"):[],supersedes:w("supersedes"),supersededBy:null,at:r.ts,by:r.actor,source:r.source});continue}if(r.type==="note.recorded"){let d=b(r.data?.text);d!==null&&s.push({id:r.id,text:d,task:b(r.data?.task),at:r.ts,by:r.actor,source:r.source});continue}if(r.type==="template.saved"){let d=typeof r.data?.name=="string"?r.data.name:null;d!==null&&o.set(d,{name:d,fields:r.data?.fields??{},author:r.actor});continue}if(r.type==="board.configured"){let d=r.data?.statuses;if(Array.isArray(d)){let h=d.filter(T=>typeof T=="string"&&T.length>0);h.length>0&&(l=h)}let w=r.data?.dod;Array.isArray(w)&&(i=w.filter(h=>typeof h=="string"&&h.length>0));let c=b(r.data?.started);c!==null&&(f.started=c,f.changes.push({id:r.id,started:c}));continue}if(r.type==="template.deleted"){let d=typeof r.data?.name=="string"?r.data.name:null;d!==null&&o.delete(d);continue}rt(r,n,u,a,y,g,f)}let k=g,m=[...n.values()].sort((r,d)=>r.id<d.id?-1:1);m.forEach((r,d)=>{r.label=`KAD-${d+1}`,ot(r),r.criteria.sort((w,c)=>w.id<c.id?-1:1),r.criteria.forEach((w,c)=>{w.n=c+1}),it(r)});let x=new Set(m.map(r=>r.id));for(let r of m)r.parent!==null&&!x.has(r.parent)&&(r.parent=null),r.blockedBy=r.blockedBy.filter(d=>x.has(d));let v=[...p.values()].sort((r,d)=>r.id<d.id?-1:1);v.forEach((r,d)=>{r.label=`DEC-${d+1}`});for(let r of v){if(r.supersedes===null)continue;let d=p.get(r.supersedes);d!==void 0&&(d.supersededBy=r.id)}let M=[...a.values()].sort((r,d)=>r.id<d.id?-1:1);M.forEach((r,d)=>{r.label=`MS-${d+1}`});let B=new Map;for(let r of m){if(r.milestone===null||!a.has(r.milestone))continue;let d=B.get(r.milestone)??[];d.push(r.id),B.set(r.milestone,d)}for(let r of M){r.taskIds=B.get(r.id)??[];let d=r.taskIds.map(w=>n.get(w)).filter(w=>w!==void 0);r.totalTasks=d.length,r.doneTasks=d.filter(w=>w.status===S).length,r.totalPoints=d.reduce((w,c)=>w+(c.estimate??0),0),r.donePoints=d.filter(w=>w.status===S).reduce((w,c)=>w+(c.estimate??0),0)}for(let r of v)r.task!==null&&!x.has(r.task)&&(r.task=null);for(let r of s)r.task!==null&&!x.has(r.task)&&(r.task=null);return{tasks:m,decisions:v,milestones:M,notes:s,sprints:[...u.values()].sort((r,d)=>r.id<d.id?-1:1),statuses:l??[...I],dod:i,started:f.started,startedChanges:[...f.changes],orphanStatuses:[...new Set(m.map(r=>r.status).filter(r=>!(l??I).includes(r)))].sort(),templates:[...o.values()].sort((r,d)=>r.name.localeCompare(d.name)),cycles:at(m),pending:k,rejected:y}}function Y(t,e){if(t.has(e.entity))return;let n=e.data??{};t.set(e.entity,{id:e.entity,label:"",title:typeof n.title=="string"?n.title:"(untitled)",description:b(n.description),type:z(n.type),priority:G(n.priority),status:O,labels:J(n.labels),assignee:b(n.assignee),reporter:e.actor,sprint:null,milestone:null,parent:b(n.parent),blockedBy:[],due:b(n.due),claimedBy:null,claimedAt:null,claimedEventId:null,contestedBy:[],criteria:[],comments:[],docs:[],estimate:typeof n.estimate=="number"?n.estimate:null,loggedHours:0,createdAt:e.ts,updatedAt:e.ts,history:[]})}function Z(t,e){if(t.has(e.entity))return;let n=e.data??{};t.set(e.entity,{id:e.entity,name:typeof n.name=="string"?n.name:"(untitled)",description:b(n.description),startDate:b(n.startDate),endDate:b(n.endDate),status:"planned",closedBy:null,taskIds:[]})}function q(t,e){if(t.has(e.entity))return;let n=e.data??{};t.set(e.entity,{id:e.entity,label:"",name:typeof n.name=="string"?n.name:"(untitled)",due:b(n.due),status:"open",closedBy:null,taskIds:[],donePoints:0,totalPoints:0,doneTasks:0,totalTasks:0})}function rt(t,e,n,u,a,o,p){let s=t.data??{};if(t.type==="milestone.created")return q(u,t),!0;if(t.type==="milestone.task_added"){let i=b(s.task);if(i===null)return!0;let f=e.get(i);return f===void 0?(o.push(t),!1):(f.milestone=t.entity,!0)}if(t.type==="milestone.closed"){let i=u.get(t.entity);return i===void 0?(o.push(t),!1):(i.closedBy===null?(i.status="closed",i.closedBy=t.id):a.push(t),!0)}if(t.type==="task.created")return Y(e,t),W(e.get(t.entity),t),!0;if(t.type==="sprint.created")return Z(n,t),!0;if(t.type.startsWith("sprint.")){let i=n.get(t.entity);if(i===void 0)return o.push(t),!1;if(t.type==="sprint.started")return i.status==="planned"&&(i.status="active"),!0;if(t.type==="sprint.updated")return i.status==="closed"?(a.push(t),!0):(typeof s.name=="string"&&(i.name=s.name),s.description!==void 0&&(i.description=b(s.description)),s.startDate!==void 0&&(i.startDate=b(s.startDate)),s.endDate!==void 0&&(i.endDate=b(s.endDate)),!0);if(t.type==="sprint.closed")return i.closedBy===null?(i.status="closed",i.closedBy=t.id):a.push(t),!0;if(t.type==="sprint.cancelled")return i.status==="planned"?i.status="cancelled":a.push(t),!0;if(t.type==="sprint.task_added"){if(i.status==="closed")return a.push(t),!0;let f=typeof s.task=="string"?s.task:null;if(f===null)return!0;let y=e.get(f);return y===void 0?(o.push(t),!1):(y.sprint=i.id,i.taskIds.includes(f)||i.taskIds.push(f),W(y,t),!0)}return!0}let l=e.get(t.entity);if(l===void 0)return o.push(t),!1;if(t.type==="task.deleted")return e.delete(t.entity),!0;switch(t.type){case"task.moved":{let i=s.to;typeof i=="string"&&i.length>0&&(l.status=i);break}case"task.cancelled":l.status=A;break;case"task.reopened":l.status=$(p.changes,t.id);break;case"task.assigned":l.assignee=b(s.assignee);break;case"task.updated":typeof s.title=="string"&&(l.title=s.title),s.description!==void 0&&(l.description=b(s.description)),s.type!==void 0&&(l.type=z(s.type)),s.priority!==void 0&&(l.priority=G(s.priority)),s.labels!==void 0&&(l.labels=J(s.labels)),s.due!==void 0&&(l.due=b(s.due)),typeof s.estimate=="number"&&(l.estimate=s.estimate);break;case"task.parent_set":l.parent=b(s.parent);break;case"task.blocked_by_added":{let i=b(s.blocker);i!==null&&!l.blockedBy.includes(i)&&l.blockedBy.push(i);break}case"task.blocked_by_removed":{let i=b(s.blocker);i!==null&&(l.blockedBy=l.blockedBy.filter(f=>f!==i));break}case"task.label_added":{let i=b(s.label);i!==null&&!l.labels.includes(i)&&l.labels.push(i);break}case"task.label_removed":{let i=b(s.label);i!==null&&(l.labels=l.labels.filter(f=>f!==i));break}case"task.criterion_added":{let i=b(s.text);i!==null&&l.criteria.push({id:t.id,n:0,text:i,checked:!1,checkedBy:null});break}case"task.criterion_checked":case"task.criterion_unchecked":break;case"task.doc_linked":{let i=b(s.path);i!==null&&!l.docs.includes(i)&&l.docs.push(i);break}case"task.time_logged":{let i=s.hours;typeof i=="number"&&Number.isFinite(i)&&(l.loggedHours=Math.max(0,l.loggedHours+i));break}case"task.commented":{let i=b(s.text);i!==null&&l.comments.push({id:t.id,author:t.actor,ts:t.ts,text:i,source:t.source});break}default:break}return W(l,t),!0}function it(t){let e=new Map(t.criteria.map(n=>[n.id,n]));for(let n of t.criteria)n.checked=!1,n.checkedBy=null;for(let n of t.history){if(n.type!=="task.criterion_checked"&&n.type!=="task.criterion_unchecked")continue;let u=b(n.data.criterion),a=u===null?void 0:e.get(u);if(a===void 0)continue;let o=n.type==="task.criterion_checked";a.checked=o,a.checkedBy=o?n.actor:null}}function ot(t){let e=null,n=null,u=null,a=[],o=()=>{e=null,n=null,u=null,a=[]};for(let p of t.history)if(p.type==="task.claimed"){let s=b(p.data.by)??p.actor;e===null?(e=s,n=p.ts,u=p.id):s!==e&&!a.includes(s)&&a.push(s)}else if(p.type==="task.released"){let s=b(p.data.by)??p.actor;e===s?o():a=a.filter(l=>l!==s)}else(p.type==="task.cancelled"||p.type==="task.moved"&&p.data.to===S)&&o();t.claimedBy=e,t.claimedAt=n,t.claimedEventId=u,t.contestedBy=a}function W(t,e){t.history.push({id:e.id,type:e.type,actor:e.actor,ts:e.ts,source:e.source,data:e.data??{}}),e.ts>t.updatedAt&&(t.updatedAt=e.ts)}function b(t){return typeof t=="string"&&t.length>0?t:null}function z(t){return nt.includes(t)?t:"task"}function G(t){return st.includes(t)?t:"normal"}function J(t){return Array.isArray(t)?t.filter(e=>typeof e=="string"&&e.length>0):[]}function at(t){let e=new Map(t.map(o=>[o.id,o])),n=[],u=new Set,a=(o,p)=>{let s=new Map,l=(i,f)=>{let y=s.get(i);if(y==="done")return;if(y==="visiting"){let k=f.indexOf(i),m=f.slice(k),x=`${o}:${[...m].sort().join(",")}`;u.has(x)||(u.add(x),n.push({kind:o,path:[...m,i]}));return}s.set(i,"visiting");let g=e.get(i);if(g!==void 0)for(let k of p(g))l(k,[...f,i]);s.set(i,"done")};for(let i of t)l(i.id,[])};return a("parent",o=>o.parent===null?[]:[o.parent]),a("blocking",o=>o.blockedBy),n}var D=864e5;function C(t){return new Date(t).toISOString().slice(0,10)}function E(t){return Date.parse(`${t}T00:00:00.000Z`)}function lt(t){let e=new Date(E(t)),n=(e.getUTCDay()+6)%7;return C(e.getTime()-n*D)}function V(t,e){let n=C(t.getTime());return{from:C(E(n)-(e-1)*D),to:n,days:e}}function Q(t,e){let n=t.slice(0,10);return n>=e.from&&n<=e.to}function F(t,e){if(t.length===0)return 0;let n=Math.max(1,Math.ceil(e*t.length));return t[n-1]}var _=t=>Math.round(t*10)/10;function K(t){if(t.length===0)return null;let e=[...t].sort((n,u)=>n-u);return{shown:{n:e.length,p50:_(F(e,.5)),p85:_(F(e,.85)),p95:_(F(e,.95))},rawP85:F(e,.85)}}function N(t,e){let n=t.indexOf(e);return n===-1?null:new Set(t.slice(0,n))}function H(t,e,n=[],u=[]){let a=N(n,e),o=s=>s===S||s===A?!1:a===null?s===e:s===e||!a.has(s);return t.history.find(s=>s.type==="task.reopened"?o($(u,s.id)):s.type==="task.moved"&&typeof s.data.to=="string"&&o(s.data.to))?.ts??null}function dt(t,e){if(e.status===S||e.status===A)return!1;let n=N(t.statuses,t.started);return n===null?e.status===t.started:n.has(e.status)?!1:H(e,t.started,t.statuses,t.startedChanges)!==null}function R(t){if(t.status!==S)return null;for(let e=t.history.length-1;e>=0;e--){let n=t.history[e];if(n.type==="task.moved"&&n.data.to===S)return n.ts}return null}function ut(t){if(t.status!==A)return null;for(let e=t.history.length-1;e>=0;e--){let n=t.history[e];if(n.type==="task.cancelled"||n.type==="task.moved"&&n.data.to===A)return n.ts}return null}function L(t,e){return Math.max(0,(Date.parse(e)-Date.parse(t))/D)}function ct(t,e,n,u){let a=R(t)??ut(t),o=a===null?u:Date.parse(a),p=[],s=new Map;for(let k of t.history){let m=typeof k.data.blocker=="string"?k.data.blocker:null;if(m!==null){if(k.type==="task.blocked_by_added")s.has(m)||s.set(m,Date.parse(k.ts));else if(k.type==="task.blocked_by_removed"){let x=s.get(m);x!==void 0&&(p.push([x,Math.min(Date.parse(k.ts),o)]),s.delete(m))}}}for(let[k,m]of s){let x=Math.min(e.get(k)??u,o);p.push([m,Math.max(m,x)])}let l=E(n.from),i=E(n.to)+D,f=p.map(([k,m])=>[Math.max(k,l),Math.min(m,i)]).filter(([k,m])=>m>k).sort((k,m)=>k[0]-m[0]),y=0,g=null;for(let[k,m]of f)g===null||k>g[1]?(g!==null&&(y+=g[1]-g[0]),g=[k,m]):m>g[1]&&(g[1]=m);return g!==null&&(y+=g[1]-g[0]),y/D}function mt(t,e=new Date,n=30){let u=V(e,n),a=e.getTime(),o=[],p=t.tasks.filter(c=>c.status!==A),s=p.filter(c=>{let h=R(c);return h!==null&&Q(h,u)}),l=[],i=[],f=[];for(let c of s){let h=R(c),T=H(c,t.started,t.statuses,t.startedChanges);i.push(L(c.createdAt,h)),T!==null&&(l.push(L(T,h)),f.push(L(c.createdAt,T)))}let y=K(l),g=y?.shown??null,k=p.filter(c=>dt(t,c)),m=k.map(c=>{let h=L(H(c,t.started,t.statuses,t.startedChanges),e.toISOString());return{label:c.label,title:c.title,status:c.status,ageDays:_(h),overP85:y!==null&&h>y.rawP85}}).sort((c,h)=>h.ageDays-c.ageDays),x=new Map,v=c=>{let h=lt(c),T=x.get(h);return T===void 0&&(T={week:h,finished:0,created:0},x.set(h,T)),T};for(let c=E(u.from);c<=E(u.to);c+=D)v(C(c));for(let c of s)v(R(c).slice(0,10)).finished+=1;for(let c of p)Q(c.createdAt,u)&&(v(c.createdAt.slice(0,10)).created+=1);let M=[...x.values()].sort((c,h)=>c.week<h.week?-1:1),B=new Map;for(let c of t.tasks){let h=R(c);h!==null&&B.set(c.id,Date.parse(h))}let r=0,d=0;for(let c of p){let h=ct(c,B,u,a);h>0&&(r+=1,d+=h)}t.tasks.length===0?o.push("No tasks yet."):(k.length===0&&s.length===0&&o.push(`Nothing has crossed "${t.started}" yet. If work starts in another column, say so: kadence board config --started <status>`),s.length===0?o.push(`Nothing finished between ${u.from} and ${u.to}; cycle time needs finished work.`):l.length===0&&o.push(`${s.length} item(s) finished without ever entering "${t.started}", so there is no cycle time to measure.`),t.statuses.includes(t.started)||o.push(`"${t.started}" is not one of the board's columns; set one with kadence board config --started.`));let w=g===null?null:`${g.n<10?`From ${g.n} finished items (a small sample): `:""}85% of finished items took ${g.p85} calendar days or less.`;return{window:u,started:t.started,unit:"calendar days",wip:k.length,finished:s.length,perWeek:M,cycleTime:g,leadTime:K(i)?.shown??null,responseTime:K(f)?.shown??null,sle:w,aging:m,blocked:{tasks:r,days:_(d)},notes:o}}function ht(t,e=new Date,n=30){let u=V(e,n),a=[...t.statuses];for(let l of t.tasks)a.includes(l.status)||a.push(l.status);let o=O;!a.includes(o)&&t.tasks.length>0&&a.unshift(o);let p=t.tasks.map(l=>{let i=[];for(let f of l.history)f.type==="task.moved"&&typeof f.data.to=="string"?i.push({at:Date.parse(f.ts),status:f.data.to}):f.type==="task.cancelled"?i.push({at:Date.parse(f.ts),status:A}):f.type==="task.reopened"&&i.push({at:Date.parse(f.ts),status:$(t.startedChanges,f.id)});return{createdAt:Date.parse(l.createdAt),changes:i,next:0,status:o}}),s=[];for(let l=E(u.from);l<=E(u.to);l+=D){let i=l+D,f={};for(let y of a)f[y]=0;for(let y of p){for(;y.next<y.changes.length&&y.changes[y.next].at<i;)y.status=y.changes[y.next].status,y.next+=1;y.createdAt>=i||(f[y.status]=(f[y.status]??0)+1)}s.push({date:C(l),counts:f})}return{window:u,statuses:a,days:s}}var j={urgent:0,high:1,normal:2,low:3};function X(t,e){return t!==null&&t.toLowerCase().includes(e)}function Tt(t,e,n=new Date){let u=n.toISOString().slice(0,10),a=e.search?.toLowerCase();return t.filter(o=>{if(a!==void 0&&a.length>0){let p=o.comments.some(s=>s.text.toLowerCase().includes(a));if(!X(o.title,a)&&!X(o.description,a)&&!p)return!1}if(e.status!==void 0&&o.status!==e.status||e.type!==void 0&&o.type!==e.type||e.priority!==void 0&&o.priority!==e.priority)return!1;if(e.assignee!==void 0){let p=e.assignee.toLowerCase(),s=(o.assignee??"").toLowerCase();if(p==="none"?s!=="":s!==p)return!1}if(e.label!==void 0){let p=e.label.toLowerCase();if(!o.labels.some(s=>s.toLowerCase()===p))return!1}return!(e.sprint!==void 0&&o.sprint!==e.sprint||e.overdue===!0&&(o.due===null||o.due>=u)||e.dueBefore!==void 0&&(o.due===null||o.due>=e.dueBefore))})}function xt(t,e){let n=[...t];switch(e){case"priority":return n.sort((u,a)=>j[u.priority]-j[a.priority]);case"due":return n.sort((u,a)=>tt(u.due,a.due,(o,p)=>o<p?-1:o>p?1:0));case"estimate":return n.sort((u,a)=>tt(u.estimate,a.estimate,(o,p)=>p-o));default:return n.sort((u,a)=>u.id<a.id?-1:u.id>a.id?1:0)}}function tt(t,e,n){return t===null&&e===null?0:t===null?1:e===null?-1:n(t,e)}function St(t){let e=[];return t.search!==void 0&&e.push(`search="${t.search}"`),t.status!==void 0&&e.push(`status=${t.status}`),t.type!==void 0&&e.push(`type=${t.type}`),t.priority!==void 0&&e.push(`priority=${t.priority}`),t.assignee!==void 0&&e.push(`assignee=${t.assignee}`),t.label!==void 0&&e.push(`label=${t.label}`),t.overdue===!0&&e.push("overdue"),t.dueBefore!==void 0&&e.push(`due before ${t.dueBefore}`),e.length===0?`No tasks yet.
3
+ Create the first one:
4
+ kadence task add "title"`:`No tasks match ${e.join(" and ")}.
5
+ Try fewer filters:
6
+ kadence task list`}function et(t){return N(t.statuses??I,t.started??P)}function U(t){return t.status===S||t.status===A}function At(t,e={}){let n=e.viewer??null,u=new Set(t.filter(U).map(s=>s.id)),a=e.assignee==="me"?n:e.assignee??null,o=et(e);return t.filter(s=>{if(U(s)||o!==null&&!o.has(s.status)||s.blockedBy.some(l=>!u.has(l))||s.claimedBy!==null&&s.claimedBy!==n&&s.contestedBy.length===0)return!1;if(a!==null){if(a==="none")return s.assignee===null;if(s.assignee!==a)return!1}return!0}).sort((s,l)=>j[s.priority]-j[l.priority]||(s.id<l.id?-1:1))}function vt(t,e,n,u={}){if(t.length===0)return`No tasks yet.
7
+ Create the first one:
8
+ kadence task add "title"`;let a=t.filter(g=>!U(g));if(a.length===0)return"Nothing ready: every task is done or cancelled.";let o=new Set(t.filter(U).map(g=>g.id)),p=et(u),s=p===null?0:a.filter(g=>!p.has(g.status)).length,l=a.filter(g=>g.blockedBy.some(k=>!o.has(k))).length,i=a.filter(g=>g.claimedBy!==null&&g.claimedBy!==e&&g.contestedBy.length===0).length,f=[];return s>0&&f.push(`${s} already started`),l>0&&f.push(`${l} blocked`),i>0&&f.push(`${i} claimed by someone else`),n!==void 0&&f.push(`filtered to assignee=${n}`),`Nothing ready${f.length>0?` (${f.join(", ")})`:""}.
9
+ See the whole board:
10
+ kadence task list`}var ft=["created","priority","due","estimate"];function Dt(t){return ft.includes(t)}export{I as a,S as b,A as c,nt as d,st as e,pt as f,H as g,dt as h,R as i,mt as j,ht as k,Tt as l,xt as m,St as n,U as o,At as p,vt as q,ft as r,Dt as s};