k0-board 0.3.0 → 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.
Files changed (82) hide show
  1. package/.claude/skills/k0-changelog/SKILL.md +2 -2
  2. package/.claude/skills/k0-discuss/SKILL.md +105 -0
  3. package/.claude/skills/k0-discuss/references/rounds.md +280 -0
  4. package/.claude/skills/k0-epic/SKILL.md +196 -0
  5. package/.claude/skills/k0-links/.claude-plugin/plugin.json +6 -0
  6. package/.claude/skills/k0-links/hooks/hooks.json +1 -0
  7. package/.claude/skills/k0-links/hooks/links.js +128 -0
  8. package/.claude/skills/k0-links/hooks/register.js +204 -0
  9. package/.claude/skills/k0-next/SKILL.md +82 -0
  10. package/.claude/skills/k0-order/SKILL.md +123 -0
  11. package/.claude/skills/k0-plan/SKILL.md +156 -0
  12. package/.claude/skills/k0-split/SKILL.md +169 -0
  13. package/.claude/skills/k0-story/SKILL.md +108 -0
  14. package/.claude/skills/k0-ultracode/SKILL.md +251 -0
  15. package/.claude/skills/k0-ultracode/references/agent-brief.md +71 -0
  16. package/.claude/skills/k0-ultracode/references/modes.md +49 -0
  17. package/.claude/skills/k0-verify/SKILL.md +219 -0
  18. package/.claude/skills/k0-whatsnew/SKILL.md +95 -0
  19. package/.claude/skills/k0-work/SKILL.md +212 -0
  20. package/CHANGELOG.md +426 -1
  21. package/README.md +1021 -119
  22. package/bin/k0-board.js +81 -11
  23. package/menubar/K0MenuBar.swift +35 -5
  24. package/package.json +13 -1
  25. package/platform/contract.js +37 -6
  26. package/platform/darwin/index.js +1 -0
  27. package/platform/darwin/shell.js +4 -1
  28. package/platform/darwin/terminal.js +230 -56
  29. package/platform/index.js +3 -1
  30. package/platform/linux/index.js +14 -0
  31. package/platform/linux/shell.js +10 -1
  32. package/platform/linux/tray.py +10 -10
  33. package/platform/shared/command.js +19 -3
  34. package/platform/shared/tmux.js +32 -14
  35. package/platform/win32/index.js +6 -2
  36. package/platform/win32/shell.js +4 -1
  37. package/platform/win32/terminal.js +24 -8
  38. package/platform/win32/tray.ps1 +13 -13
  39. package/server/backlog.js +618 -0
  40. package/server/changelog.js +13 -13
  41. package/server/db.js +1800 -146
  42. package/server/files.js +156 -26
  43. package/server/git.js +53 -14
  44. package/server/idle.js +15 -13
  45. package/server/index.js +1286 -189
  46. package/server/launcher.js +122 -14
  47. package/server/machine.js +10 -1
  48. package/server/mirror.js +1048 -0
  49. package/server/prices.js +59 -0
  50. package/server/projects.js +57 -12
  51. package/server/servers.js +22 -1
  52. package/server/sessions.js +5 -5
  53. package/server/settings.js +25 -2
  54. package/server/update.js +324 -0
  55. package/server/usage.js +642 -0
  56. package/server/watcher.js +67 -13
  57. package/server/worktree.js +600 -0
  58. package/server/writer.js +98 -26
  59. package/web/awake.js +62 -0
  60. package/web/base.css +4 -0
  61. package/web/board.css +404 -16
  62. package/web/board.js +1182 -152
  63. package/web/changelog.js +3 -3
  64. package/web/conf.js +118 -0
  65. package/web/files.css +206 -1
  66. package/web/files.html +10 -0
  67. package/web/files.js +533 -75
  68. package/web/fuzzy.js +73 -21
  69. package/web/index.html +109 -12
  70. package/web/json.js +145 -0
  71. package/web/list.css +536 -0
  72. package/web/list.js +1015 -0
  73. package/web/mentions.js +27 -9
  74. package/web/recency.js +10 -10
  75. package/web/units.js +1 -1
  76. package/web/usage.css +169 -0
  77. package/web/usage.html +78 -0
  78. package/web/usage.js +447 -0
  79. package/web/view.js +22 -3
  80. package/web/whatsnew.css +211 -0
  81. package/web/whatsnew.html +49 -0
  82. package/web/whatsnew.js +339 -0
package/web/mentions.js CHANGED
@@ -8,6 +8,10 @@
8
8
  // is why it costs nothing and never goes near the server: the file listing is already in the
9
9
  // browser.
10
10
  //
11
+ // The listing is not quite everything there is, though, so what it could not place it hands back
12
+ // under `unknown` rather than swallowing it. Asking the disk about those names is somebody
13
+ // else's job; this module stays a pure function of a text and a list.
14
+ //
11
15
  // What is written out in full and what is merely guessed at stay separate, because they are
12
16
  // two different degrees of certainty: mixing them would make a word that is only a hunch look
13
17
  // like a fact.
@@ -59,13 +63,17 @@ function index(files) {
59
63
  return { byName, byStem, byDir, dirTail, exts, stems: [...byStem.keys()] }
60
64
  }
61
65
 
62
- /** The pieces of text worth trying, lower case and in the order they appear. */
66
+ /**
67
+ * The pieces of text worth trying, in the order they appear: each one lower case for the
68
+ * matching, and as it was written for whoever has to go and look on a disk — there are file
69
+ * systems where `Report.pdf` and `report.pdf` are two different files.
70
+ */
63
71
  function tokens(text) {
64
72
  const out = []
65
73
  for (const m of String(text ?? '').matchAll(TOKEN)) {
66
74
  // Punctuation stuck on the end is not part of the name: "…it is in from-the-call.md."
67
- const t = low(m[0]).replace(/[._/-]+$/, '')
68
- if (t) out.push(t)
75
+ const raw = m[0].replace(/[._/-]+$/, '')
76
+ if (raw) out.push([low(raw), raw])
69
77
  }
70
78
  return out
71
79
  }
@@ -110,6 +118,12 @@ function dirFor(token, ix) {
110
118
  * ("summary"), or the whole beginning of a name ("survey" for `survey-round-two.md`).
111
119
  * - `missing` — names written with a dot that do not exist in here. Only those with an
112
120
  * extension the repository actually uses, otherwise `example.com` would look like a lost file.
121
+ * - `unknown` — the same names, all of them, before that judgement is made. The listing is not
122
+ * everything there is: a generated directory gives it only its finished documents, an ignored
123
+ * one gives it nothing, and `out/report.html` is in neither while being written out in full
124
+ * right there in the text. Whoever calls this can go and ask the disk about them; existence is
125
+ * a better answer than a guess about extensions, and it is the one thing this module cannot
126
+ * reach by itself.
113
127
  *
114
128
  * Every entry is `{ token, file, more }` for a file — `file` is the most recently touched one
115
129
  * when a name points at several, `more` are the rest — or `{ token, dir, files }` for a
@@ -123,6 +137,7 @@ export function mentions(text, files) {
123
137
  const named = []
124
138
  const maybe = []
125
139
  const missing = []
140
+ const unknown = []
126
141
  const missed = new Set()
127
142
 
128
143
  /** Drops the ones already listed and puts the most recent first. */
@@ -133,13 +148,16 @@ export function mentions(text, files) {
133
148
  }
134
149
 
135
150
  // First pass: what is written out in full.
136
- for (const t of toks) {
151
+ for (const [t, raw] of toks) {
137
152
  if (EXT.test(t)) {
138
153
  const hits = filesFor(t, ix)
139
154
  if (!hits) {
140
- if (ix.exts.has(EXT.exec(t)[1]) && !missed.has(t)) {
155
+ if (!missed.has(t)) {
141
156
  missed.add(t)
142
- missing.push(t)
157
+ unknown.push(raw)
158
+ // The listing knows this extension and still has no such file: that is a name worth
159
+ // saying out loud. Otherwise silence — `example.com` is not a lost document.
160
+ if (ix.exts.has(EXT.exec(t)[1])) missing.push(t)
143
161
  }
144
162
  continue
145
163
  }
@@ -159,7 +177,7 @@ export function mentions(text, files) {
159
177
 
160
178
  // File names first: the whole name without the dot, failing that the whole beginning of a
161
179
  // name cut at a dash — "survey" for `survey-round-two.md`.
162
- for (const t of toks) {
180
+ for (const [t] of toks) {
163
181
  if (!guess(t)) continue
164
182
  const hits =
165
183
  ix.byStem.get(t) ||
@@ -172,14 +190,14 @@ export function mentions(text, files) {
172
190
  // clue there is: "onboarding" in a text about onboarding is an ordinary word, and if it came
173
191
  // first it would carry off `interviews/onboarding/summary.md`, leaving "summary" to show the
174
192
  // summary of something else entirely.
175
- for (const t of toks) {
193
+ for (const [t] of toks) {
176
194
  if (!guess(t)) continue
177
195
  const d = dirFor(t, ix)
178
196
  const f = d ? fresh(ix.byDir.get(d)) : []
179
197
  if (f.length) keep(maybe, { token: t, dir: d, files: f }, f)
180
198
  }
181
199
 
182
- return { named, maybe, missing }
200
+ return { named, maybe, missing, unknown }
183
201
  }
184
202
 
185
203
  /** How many files a group of entries opens: a directory counts for what is inside it. */
package/web/recency.js CHANGED
@@ -14,19 +14,19 @@ export const DAY = 24 * 60 * 60 * 1000
14
14
  *
15
15
  * `IDLE` is deliberately not one of them, and it is the only debatable line in this file. It
16
16
  * means "your turn", so it looks like it belongs — but it is also the resting state of every
17
- * terminal you have left open, and on a real board nearly every live card is sitting in it. Let
17
+ * terminal you have left open, and on a real board nearly every live story is sitting in it. Let
18
18
  * it hold a column and nothing ever folds: a session you finished with three weeks ago and never
19
19
  * closed would keep its column at full width for ever. So `IDLE` gets the same treatment as
20
20
  * everything else — recent, and it stays; a fortnight old, and it goes to `Old`, where it is one
21
- * click away. Nothing is closed either way: the terminal is still open, and the card is still
21
+ * click away. Nothing is closed either way: the terminal is still open, and the story is still
22
22
  * exactly where you left it.
23
23
  */
24
24
  const DEMANDING = new Set(['ASK', 'PLANNED', 'WORKING', 'PLANNING'])
25
25
 
26
- /** The last time each repository was touched: the freshest of its cards. */
27
- export function lastTouched(cards) {
26
+ /** The last time each repository was touched: the freshest of its stories. */
27
+ export function lastTouched(stories) {
28
28
  const map = new Map()
29
- for (const c of cards) {
29
+ for (const c of stories) {
30
30
  const t = Number(c.updated_at) || 0
31
31
  if (t > (map.get(c.project_path) ?? 0)) map.set(c.project_path, t)
32
32
  }
@@ -34,9 +34,9 @@ export function lastTouched(cards) {
34
34
  }
35
35
 
36
36
  /** The repositories with something going on in them right now. */
37
- export function busy(cards) {
37
+ export function busy(stories) {
38
38
  const set = new Set()
39
- for (const c of cards) if (DEMANDING.has(c.status)) set.add(c.project_path)
39
+ for (const c of stories) if (DEMANDING.has(c.status)) set.add(c.project_path)
40
40
  return set
41
41
  }
42
42
 
@@ -71,10 +71,10 @@ export function cutoff(touched) {
71
71
  *
72
72
  * The order of `paths` is kept: whoever asked has already sorted them.
73
73
  */
74
- export function split({ paths, cards, folded = new Set(), held = new Set() }) {
75
- const touched = lastTouched(cards)
74
+ export function split({ paths, stories, folded = new Set(), held = new Set() }) {
75
+ const touched = lastTouched(stories)
76
76
  const line = cutoff(touched)
77
- const alive = busy(cards)
77
+ const alive = busy(stories)
78
78
  const open = []
79
79
  const old = []
80
80
  for (const p of paths) {
package/web/units.js CHANGED
@@ -9,7 +9,7 @@ const GB = 1073741824
9
9
  /**
10
10
  * What a session weighs, rounded in steps: tens of MB below a gigabyte, tenths of a GB above.
11
11
  *
12
- * The rounding is not decoration. It is the rounded number — the one you read on the card —
12
+ * The rounding is not decoration. It is the rounded number — the one you read on the post-it —
13
13
  * that goes into the signature of the refresh round: that way the board only redraws when the
14
14
  * figure on screen really changes, instead of on every sample, throwing away the pointer and
15
15
  * the buttons that appear under it.
package/web/usage.css ADDED
@@ -0,0 +1,169 @@
1
+ /* Usage: what the sessions consumed, a week at a time. Colours and fonts live in base.css.
2
+
3
+ One wide column. It is a page of numbers and charts read on a wide screen, so the container
4
+ takes the width and everything inside it — paragraphs included — goes edge to edge. */
5
+ @import url('/base.css');
6
+
7
+ :root {
8
+ /* The categorical order is fixed and follows the entity, never its rank: a model keeps its
9
+ colour from one week to the next. Validated for colour blindness on --paper. */
10
+ --s1: #2a78d6;
11
+ --s2: #eb6834;
12
+ --s3: #1baf7a;
13
+ --s4: #eda100;
14
+ --s5: #e87ba4;
15
+ --s6: #008300;
16
+ --other: #a8a293;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ background: var(--bg);
22
+ color: var(--ink);
23
+ font-family: var(--sans);
24
+ font-size: 1rem;
25
+ line-height: 1.5;
26
+ -webkit-font-smoothing: antialiased;
27
+ }
28
+
29
+ /* ── The top bar ─────────────────────────────────────────────────────── */
30
+ .topbar {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 14px;
34
+ padding: 10px 18px;
35
+ background: var(--paper);
36
+ border-bottom: 1px solid var(--line);
37
+ flex-wrap: wrap;
38
+ }
39
+ .topbar .home {
40
+ font: 700 1.2rem/1 var(--mono);
41
+ letter-spacing: 1px;
42
+ color: var(--ink);
43
+ text-decoration: none;
44
+ }
45
+ .topbar .home:hover { color: var(--muted); }
46
+ .topbar h1 { margin: 0; font: 600 1rem/1 var(--sans); color: var(--muted); flex: 1; }
47
+
48
+ .week { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: .9rem; }
49
+ .week button { padding: 3px 11px; font-size: 1rem; line-height: 1.2; }
50
+ .week button:disabled { opacity: .35; cursor: default; }
51
+ #span { min-width: 11rem; text-align: center; }
52
+
53
+ .topbar select {
54
+ font: inherit;
55
+ font-size: .82rem;
56
+ font-weight: 600;
57
+ color: var(--muted);
58
+ border: 1px solid var(--line);
59
+ background: var(--paper);
60
+ border-radius: 8px;
61
+ padding: 5px 9px;
62
+ cursor: pointer;
63
+ }
64
+
65
+ /* ── The sheet ───────────────────────────────────────────────────────── */
66
+ .sheet {
67
+ max-width: 1280px;
68
+ margin: 0 auto;
69
+ padding: 20px 18px 60px;
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 18px;
73
+ }
74
+
75
+ .notes:empty { display: none; }
76
+ .notes { display: flex; flex-direction: column; gap: 8px; }
77
+ .note {
78
+ margin: 0;
79
+ padding: 10px 14px;
80
+ border-radius: 10px;
81
+ background: var(--paper);
82
+ border: 1px solid var(--line);
83
+ font-size: .9rem;
84
+ color: var(--muted);
85
+ }
86
+ .note b { color: var(--ink); }
87
+ .note code { font: .82rem var(--mono); background: var(--bg); padding: 1px 5px; border-radius: 4px; }
88
+
89
+ .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
90
+ .tile {
91
+ background: var(--paper);
92
+ border: 1px solid var(--line);
93
+ border-radius: 12px;
94
+ padding: 12px 16px;
95
+ }
96
+ .tile .v { font: 700 1.6rem/1.2 var(--sans); font-variant-numeric: tabular-nums; }
97
+ .tile .k { font-size: .8rem; color: var(--muted); }
98
+ .tile .s { font-size: .78rem; color: var(--muted); margin-top: 2px; }
99
+
100
+ .panel {
101
+ background: var(--paper);
102
+ border: 1px solid var(--line);
103
+ border-radius: 12px;
104
+ padding: 14px 18px;
105
+ min-width: 0;
106
+ }
107
+ .panel h2 { margin: 0 0 4px; font: 600 .95rem/1.3 var(--sans); }
108
+ .caption { margin: 0 0 10px; font-size: .82rem; color: var(--muted); }
109
+
110
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 18px; }
111
+
112
+ /* ── Charts ──────────────────────────────────────────────────────────── */
113
+ .legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: .8rem; color: var(--muted); margin-bottom: 6px; }
114
+ .legend span { display: inline-flex; align-items: center; gap: 6px; }
115
+ .legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
116
+ .chart svg { display: block; width: 100%; height: auto; overflow: visible; }
117
+ .chart .grid-line { stroke: var(--line); stroke-width: 1; }
118
+ .chart .axis { fill: var(--muted); font-size: 11px; font-family: var(--sans); }
119
+ .chart .label { fill: var(--ink); font-size: 11px; font-weight: 600; font-family: var(--sans); }
120
+ .chart .empty { fill: var(--muted); font-size: 13px; font-family: var(--sans); }
121
+ .chart .hit { fill: transparent; cursor: default; }
122
+ .chart .hit:hover + .bar-outline, .chart .col:hover .seg { opacity: .85; }
123
+ .chart .line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
124
+ .chart .dot { stroke: var(--paper); stroke-width: 2; }
125
+
126
+ .tip {
127
+ position: fixed;
128
+ z-index: 10;
129
+ pointer-events: none;
130
+ background: var(--ink);
131
+ color: var(--paper);
132
+ font-size: .78rem;
133
+ line-height: 1.4;
134
+ padding: 7px 10px;
135
+ border-radius: 8px;
136
+ max-width: 320px;
137
+ box-shadow: 0 4px 14px rgba(0, 0, 0, .18);
138
+ }
139
+ .tip i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; margin-right: 6px; }
140
+
141
+ /* ── Tables ──────────────────────────────────────────────────────────── */
142
+ table { width: 100%; border-collapse: collapse; font-size: .84rem; font-variant-numeric: tabular-nums; }
143
+ th {
144
+ text-align: left;
145
+ font-weight: 600;
146
+ color: var(--muted);
147
+ font-size: .75rem;
148
+ padding: 6px 8px;
149
+ border-bottom: 1px solid var(--line);
150
+ white-space: nowrap;
151
+ }
152
+ td { padding: 6px 8px; border-bottom: 1px solid #efebe0; vertical-align: top; }
153
+ tr:last-child td { border-bottom: none; }
154
+ td.n, th.n { text-align: right; white-space: nowrap; }
155
+ td .sub { display: block; color: var(--muted); font-size: .75rem; }
156
+ td a { color: var(--ink); }
157
+ .swatch { width: 9px; height: 9px; border-radius: 2px; display: inline-block; margin-right: 7px; }
158
+ .muted { color: var(--muted); }
159
+ .warn { color: var(--warn); font-weight: 600; }
160
+ .scroll { overflow-x: auto; }
161
+ .sessions td:first-child { min-width: 16rem; }
162
+ .key { font: 600 .75rem var(--mono); color: var(--muted); margin-right: 6px; }
163
+ #more { margin-top: 10px; }
164
+
165
+ @media (max-width: 600px) {
166
+ .sheet { padding: 14px 16px 40px; }
167
+ .grid { grid-template-columns: 1fr; }
168
+ #span { min-width: 0; }
169
+ }
package/web/usage.html ADDED
@@ -0,0 +1,78 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>k0 — Usage</title>
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
+ <link rel="stylesheet" href="/usage.css">
9
+ </head>
10
+ <body>
11
+ <header class="topbar">
12
+ <a class="home" href="/" title="Back to the dashboard">k0</a>
13
+ <h1>Usage</h1>
14
+
15
+ <!-- A week at a time, because the limit that matters is a weekly one. -->
16
+ <div class="week" role="group" aria-label="Week">
17
+ <button type="button" id="prev" aria-label="Previous week">‹</button>
18
+ <span id="span"></span>
19
+ <button type="button" id="next" aria-label="Next week">›</button>
20
+ </div>
21
+
22
+ <select id="account" aria-label="Account"></select>
23
+ </header>
24
+
25
+ <main class="sheet">
26
+ <div class="notes" id="notes"></div>
27
+
28
+ <section class="tiles" id="tiles" aria-label="The week in numbers"></section>
29
+
30
+ <section class="panel">
31
+ <h2>What each day would have cost at API prices</h2>
32
+ <p class="caption">Stacked by model. With a subscription you do not pay this: it is the one scale on which a
33
+ Fable hour and a Sonnet hour can be compared.</p>
34
+ <div class="legend" id="cost-legend"></div>
35
+ <div class="chart" id="cost-chart"></div>
36
+ </section>
37
+
38
+ <section class="panel">
39
+ <h2>How full the weekly limit was</h2>
40
+ <p class="caption" id="limits-caption"></p>
41
+ <div class="legend" id="limits-legend"></div>
42
+ <div class="chart" id="limits-chart"></div>
43
+ </section>
44
+
45
+ <div class="grid">
46
+ <section class="panel">
47
+ <h2>By model and effort</h2>
48
+ <table id="t-models"></table>
49
+ </section>
50
+ <section class="panel">
51
+ <h2>Who did the work</h2>
52
+ <table id="t-agents"></table>
53
+ </section>
54
+ <section class="panel">
55
+ <h2>By repository</h2>
56
+ <table id="t-repos"></table>
57
+ </section>
58
+ <section class="panel">
59
+ <h2>By account</h2>
60
+ <table id="t-accounts"></table>
61
+ </section>
62
+ </div>
63
+
64
+ <section class="panel">
65
+ <h2>Sessions, the most expensive first</h2>
66
+ <p class="caption">“Worked” is the time Claude spent on your turns, not the time the window was open. The share of
67
+ the weekly limit is an estimate: the statusline says how full the week is, not who filled it, so each rise is
68
+ shared among the sessions that ran in the same hour.</p>
69
+ <div class="scroll"><table id="t-sessions" class="sessions"></table></div>
70
+ <button type="button" id="more" hidden></button>
71
+ </section>
72
+ </main>
73
+
74
+ <div class="tip" id="tip" role="tooltip" hidden></div>
75
+
76
+ <script type="module" src="/usage.js"></script>
77
+ </body>
78
+ </html>