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
@@ -14,19 +14,113 @@ export function sessionName(title) {
14
14
 
15
15
  export const findClaude = () => shell.findClaude()
16
16
 
17
+ /**
18
+ * What is under the cursor: the text after the `❯` of the input box, as it reads on screen.
19
+ *
20
+ * The box is the last line with a `❯` on it — the sent messages above it are echoed with the
21
+ * same mark, but they are above it. Null when there is no such line at all: a dialog, the trust
22
+ * question, the interface still drawing. A menu with its `❯` on the chosen row reads as text,
23
+ * which is the right answer, because a key pressed there picks something.
24
+ */
25
+ export function promptText(screen) {
26
+ if (!screen) return null
27
+ const lines = screen.split('\n')
28
+ for (let i = lines.length - 1; i >= 0; i--) {
29
+ const m = /^\s*❯(.*)$/.exec(lines[i])
30
+ if (m) return m[1].trim()
31
+ }
32
+ return null
33
+ }
34
+
35
+ /**
36
+ * Free to write in: nothing after the `❯`, or only the hint Claude Code prints in grey on a box
37
+ * that has never been typed in — `Try "how do I log an error?"` — which the next key replaces.
38
+ */
39
+ export function promptIsEmpty(screen) {
40
+ const text = promptText(screen)
41
+ return text !== null && (text === '' || text.startsWith('Try "'))
42
+ }
43
+
44
+ /**
45
+ * How long a session is left alone after a rename was typed into it: long enough for the name
46
+ * in its session file to change, which is how the next round knows it took. After a refusal —
47
+ * the window not in front, something under the cursor — it is looked at again sooner.
48
+ */
49
+ export const RENAME_RETRY_MS = 60000
50
+ export const RENAME_REFUSED_MS = 5000
51
+ const attempted = new Map() // session id -> { name, until }
52
+
53
+ /**
54
+ * The same clearing out `forgetDeadSessions` does in watcher.js, for the same reason: a rename
55
+ * attempted against a session that has since ended is a note about nobody, and nothing was ever
56
+ * removing it.
57
+ */
58
+ export function forgetRenameAttempts(alive) {
59
+ for (const sessionId of attempted.keys()) if (!alive.has(sessionId)) attempted.delete(sessionId)
60
+ }
61
+
62
+ /**
63
+ * Whether a live session is waiting to be renamed, and can be right now.
64
+ *
65
+ * Only a session that is sitting idle — the raw status from its session file: no turn running,
66
+ * no dialog open — and only where the platform can type a command into it and read back what
67
+ * it typed. A session file without a `name` is an older Claude Code: k0 could type the command
68
+ * but never see whether it took, and it would go on typing it, so there the name catches up
69
+ * when the session ends.
70
+ */
71
+ export function renameDue(story, session, now = Date.now()) {
72
+ if (!session || session.status !== 'idle' || !('name' in session)) return false
73
+ if (!story.terminal_window_id || !capabilities.terminal.commands) return false
74
+ const name = sessionName(story.title)
75
+ if (session.name === name) return false
76
+ const last = attempted.get(story.session_id)
77
+ return !(last && last.name === name && last.until > now)
78
+ }
79
+
80
+ /**
81
+ * Types `/rename` with the story's name into the session's window — if that window is the one in
82
+ * front of you. The adapter never raises it: the keyboard belongs to whatever you are doing, so
83
+ * the rename waits until you look at the session, and happens then.
84
+ */
85
+ export async function renameLive(story) {
86
+ const name = sessionName(story.title)
87
+ const line = `/rename ${name}`
88
+ // Written down before the keys go in, not after: the watching loop asks again a second later,
89
+ // and typing takes longer than that — two rounds typing the same command into one box is a
90
+ // box with the command in it twice.
91
+ attempted.set(story.session_id, { name, until: Date.now() + RENAME_REFUSED_MS })
92
+ const res = await terminal.command(line, story.terminal_window_id, {
93
+ ready: promptIsEmpty,
94
+ verify: (screen) => promptText(screen) === line,
95
+ })
96
+ attempted.set(story.session_id, { name, until: Date.now() + (res.sent ? RENAME_RETRY_MS : RENAME_REFUSED_MS) })
97
+ return { ...res, name }
98
+ }
99
+
17
100
  /** In driving mode the terminal has to be readable from across the room. */
18
101
  const DRIVING_FONT_SIZE = 22
19
102
 
103
+ /** And a window worth glancing at from there takes the whole screen, not its usual share of it. */
104
+ const DRIVING_COVERAGE = 1
105
+
20
106
  /** How big the text has to be right now: it depends only on the mode in force. */
21
107
  const fontSize = () => (isDriving() ? Promise.resolve(DRIVING_FONT_SIZE) : terminal.defaultFontSize())
22
108
 
23
- export async function setTerminalFont(handles) {
24
- return terminal.setFont(handles, await fontSize())
109
+ /** And how much screen the window takes. Undefined means "your usual share", whatever it is. */
110
+ const coverage = () => (isDriving() ? DRIVING_COVERAGE : undefined)
111
+
112
+ /**
113
+ * Puts every window k0 owns the way the mode in force wants it, text and size in one gesture.
114
+ * It runs on every mode change and once more at startup, which is what repairs the windows a
115
+ * server restart left behind at the wrong size.
116
+ */
117
+ export async function applyModeToWindows(handles) {
118
+ return terminal.applyMode(handles, { fontSize: await fontSize(), coverage: coverage() })
25
119
  }
26
120
 
27
- export const relayoutWindows = (handles) => terminal.relayout(handles)
121
+ export const relayoutWindows = (handles) => terminal.relayout(handles, { coverage: coverage() })
28
122
  export const setWindowTitle = (handle, title) => terminal.setTitle(handle, title)
29
- export const focusWindow = (handle) => terminal.focus(handle)
123
+ export const focusWindow = (handle, title) => terminal.focus(handle, title)
30
124
  export const closeTerminal = ({ winId, pid }) => terminal.close({ handle: winId, pid })
31
125
 
32
126
  /** Waits for Claude to really be up: the session shows up in ~/.claude/sessions. */
@@ -69,11 +163,25 @@ async function waitForPrompt(handle, timeoutMs = 15000) {
69
163
  }
70
164
 
71
165
  /**
72
- * Starts (or resumes) a card's session.
166
+ * What every session is started with, new or resumed.
167
+ *
168
+ * Claude Code decides when it starts, model by model, whether a session gets its task tools —
169
+ * TaskCreate, TaskUpdate and the rest — and a newer model such as Opus 5 does not get them unless
170
+ * this is in the environment. The commands k0 ships keep a task per step of the work, so without
171
+ * it a session is told to do something it has no tool for, and nothing warns: the call is refused
172
+ * and the model carries on without. It is read at startup, which is why a resume carries it too.
173
+ */
174
+ export const SESSION_ENV = { CLAUDE_CODE_ENABLE_TODO_TOOLS: '1' }
175
+
176
+ /**
177
+ * Starts (or resumes) a story's session.
73
178
  * mode: 'start' assigns a new session id, 'resume' reopens the existing one.
179
+ *
180
+ * `story` is the flat row `db.listStories()` hands over, session and all: `auto_send` lives on
181
+ * the session now, and it is read from there through the same row rather than looked up again.
74
182
  */
75
- export async function launch({ card, sessionId, mode = 'start' }) {
76
- const name = sessionName(card.title)
183
+ export async function launch({ story, sessionId, mode = 'start' }) {
184
+ const name = sessionName(story.title)
77
185
  const bin = findClaude()
78
186
  const resume = mode === 'resume'
79
187
  const args = []
@@ -97,11 +205,11 @@ export async function launch({ card, sessionId, mode = 'start' }) {
97
205
  args.push('-n', name)
98
206
  // With "send it for me" the prompt goes straight to the CLI and starts without touching
99
207
  // anything.
100
- const autoSend = mode === 'start' && card.auto_send && card.prompt?.trim()
101
- if (autoSend) args.push(card.prompt.trim())
208
+ const autoSend = mode === 'start' && story.auto_send && story.prompt?.trim()
209
+ if (autoSend) args.push(story.prompt.trim())
102
210
 
103
- const command = terminal.buildCommand({ cwd: card.project_path, bin, args })
104
- const winId = await terminal.open({ command, title: name, fontSize: await fontSize() })
211
+ const command = terminal.buildCommand({ cwd: story.project_path, bin, args, env: SESSION_ENV })
212
+ const winId = await terminal.open({ command, title: name, fontSize: await fontSize(), coverage: coverage() })
105
213
  const up = await waitForSession(sessionId)
106
214
 
107
215
  if (autoSend) return { name, up, winId, pasted: true, autoSent: true }
@@ -109,17 +217,17 @@ export async function launch({ card, sessionId, mode = 'start' }) {
109
217
  // ago, and putting it back under the cursor is at best a stale instruction in the way — at
110
218
  // worst it is sent, because where pasting is unavailable the fallback below types it. This
111
219
  // matters more now that k0 closes forgotten terminals by itself: a Resume is no longer rare.
112
- if (resume || !up || !card.prompt?.trim()) return { name, up, winId, pasted: false }
220
+ if (resume || !up || !story.prompt?.trim()) return { name, up, winId, pasted: false }
113
221
 
114
222
  await waitForPrompt(winId)
115
223
 
116
224
  if (capabilities.terminal.pasteWithoutSending) {
117
- const res = await terminal.paste(card.prompt.trim(), winId)
225
+ const res = await terminal.paste(story.prompt.trim(), winId)
118
226
  if (res.pasted) return { name, up, winId, ...res }
119
227
  }
120
228
 
121
229
  // Pasting is unavailable or was refused — on macOS that means no Accessibility permission.
122
230
  // We type it instead, which sends it.
123
- const fallback = await terminal.type(card.prompt.trim(), winId)
231
+ const fallback = await terminal.type(story.prompt.trim(), winId)
124
232
  return { name, up, winId, pasted: false, autoSent: fallback.written, ...fallback }
125
233
  }
package/server/machine.js CHANGED
@@ -10,7 +10,7 @@ export { parsePs, cpuSeconds } from '../platform/shared/ps.js'
10
10
  // The honest number for "what does this session cost" is not the `claude` process: it is its
11
11
  // whole subtree. A 200 MB claude can have an MCP server under it with Chrome behind that, and
12
12
  // a 200 MB `tsc` started from a commit hook. That is where the memory goes, and it is what no
13
- // other tool can attribute to the right card — Activity Monitor has never heard of cards.
13
+ // other tool can attribute to the right story — Activity Monitor has never heard of stories.
14
14
  //
15
15
  // Summing the resident memory of a subtree counts shared memory twice between parent and
16
16
  // children, so the total is a little generous. It is the usual approximation, and it is the
@@ -155,6 +155,15 @@ export function loadOf(pid) {
155
155
  return { rss, cpu: snap.cpu === null ? null : cpu / CORES, procs: pids.size, top: heaviest(byName) }
156
156
  }
157
157
 
158
+ /**
159
+ * What k0 itself weighs, measured the same way as everything else on the board.
160
+ *
161
+ * The chip has always said what the sessions cost and what Chrome costs, and never what the thing
162
+ * doing the measuring costs. That is the one number nobody could see, and for a server that runs
163
+ * under launchd for weeks it is the one that should be hardest to hide from.
164
+ */
165
+ export const self = () => loadOf(process.pid)
166
+
158
167
  /**
159
168
  * How the machine is doing, and what is taking it up that is not a k0 session. `others` exists
160
169
  * so k0 does not take blame that belongs to Chrome: with swap full, the sessions on their own