k0-board 0.4.0 → 0.6.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 (77) 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/LICENSE +83 -18
  22. package/README.md +1141 -119
  23. package/bin/k0-board.js +127 -12
  24. package/menubar/K0MenuBar.swift +35 -5
  25. package/package.json +14 -2
  26. package/platform/contract.js +30 -5
  27. package/platform/darwin/terminal.js +230 -56
  28. package/platform/index.js +2 -1
  29. package/platform/linux/index.js +3 -0
  30. package/platform/linux/tray.py +10 -10
  31. package/platform/shared/command.js +19 -3
  32. package/platform/shared/tmux.js +32 -14
  33. package/platform/win32/index.js +5 -2
  34. package/platform/win32/terminal.js +24 -8
  35. package/platform/win32/tray.ps1 +13 -13
  36. package/server/backlog.js +618 -0
  37. package/server/changelog.js +13 -13
  38. package/server/cloud.js +355 -0
  39. package/server/db.js +2117 -146
  40. package/server/files.js +60 -21
  41. package/server/git.js +53 -14
  42. package/server/idle.js +15 -13
  43. package/server/index.js +1341 -186
  44. package/server/launcher.js +122 -14
  45. package/server/machine.js +10 -1
  46. package/server/mirror.js +1069 -0
  47. package/server/prices.js +59 -0
  48. package/server/projects.js +57 -12
  49. package/server/servers.js +22 -1
  50. package/server/sessions.js +5 -5
  51. package/server/settings.js +25 -2
  52. package/server/sync.js +383 -0
  53. package/server/update.js +325 -0
  54. package/server/usage.js +642 -0
  55. package/server/watcher.js +67 -13
  56. package/server/worktree.js +600 -0
  57. package/server/writer.js +98 -26
  58. package/web/account.js +265 -0
  59. package/web/awake.js +62 -0
  60. package/web/base.css +97 -0
  61. package/web/board.css +396 -16
  62. package/web/board.js +1182 -152
  63. package/web/changelog.js +3 -3
  64. package/web/files.js +73 -16
  65. package/web/index.html +110 -12
  66. package/web/list.css +536 -0
  67. package/web/list.js +1015 -0
  68. package/web/mentions.js +27 -9
  69. package/web/recency.js +10 -10
  70. package/web/units.js +1 -1
  71. package/web/usage.css +169 -0
  72. package/web/usage.html +78 -0
  73. package/web/usage.js +447 -0
  74. package/web/view.js +22 -3
  75. package/web/whatsnew.css +211 -0
  76. package/web/whatsnew.html +49 -0
  77. package/web/whatsnew.js +339 -0
@@ -0,0 +1,128 @@
1
+ // ── What a reply points at ────────────────────────────────────────────────────
2
+ // Claude's replies are full of places — "open localhost:4321/projects/furniture/", "the photos are
3
+ // in docs/projects/sideboard-140/", "it goes live on alessioragni.com/projects/furniture/" — and in
4
+ // the terminal all of it is dead text. This finds them in the reply, in the order they were said,
5
+ // so the mod can put them in a row above the prompt where one click opens each.
6
+ //
7
+ // It is pure on purpose: the mod's environment has no Node, the test runner has no Claude Code,
8
+ // and this file has to run in both. Whether a path is a folder, and whether k0 is listening, is
9
+ // for register.js to ask.
10
+ //
11
+ // Not imported from web/refs.js because the mod is copied into ~/.claude/skills/, where nothing
12
+ // of k0 is next to it. The extension list is the server's `DOC_EXT`, kept in step by a test.
13
+
14
+ /** The extensions the viewer can show. Same list as `DOC_EXT` in server/files.js. */
15
+ export const DOC = new Set([
16
+ '.md', '.markdown', '.mdx', '.html', '.htm', '.txt', '.rtf',
17
+ '.pdf', '.docx', '.doc', '.odt', '.pages',
18
+ ])
19
+
20
+ /** A reply naming more than this is a listing, and a row of forty buttons is noise. */
21
+ export const MAX_REFS = 12
22
+
23
+ export const extOf = (p) => {
24
+ const n = p.replace(/\/+$/, '').slice(p.replace(/\/+$/, '').lastIndexOf('/') + 1)
25
+ const i = n.lastIndexOf('.')
26
+ return i > 0 ? n.slice(i).toLowerCase() : ''
27
+ }
28
+
29
+ /** Fenced blocks are code being shown, not places being pointed at. */
30
+ const FENCE = /^\s*(```+|~~~+)[\s\S]*?^\s*\1\s*$/gm
31
+
32
+ const END = `[^\\s)>\\]\`'"<]`
33
+ /** Every way a reply writes an address: with a scheme, a local port, or a domain with a path. */
34
+ const WEB = new RegExp(
35
+ [
36
+ `https?:\\/\\/${END}+`,
37
+ `(?:localhost|127\\.0\\.0\\.1|\\[::1\\]):\\d{2,5}(?:\\/${END}*)?`,
38
+ `(?<![\\w./-])(?:[a-z0-9-]+\\.)+[a-z]{2,}\\/${END}*`,
39
+ ].join('|'),
40
+ 'gi'
41
+ )
42
+ /** A path: something with a slash in it, relative, absolute or from home. */
43
+ const PATH = /(?<![\w.:/~-])(?:~\/|\/|\.{1,2}\/)?[A-Za-z0-9_.@-]+(?:\/[A-Za-z0-9_.@-]+)*\/?/g
44
+
45
+ /** Punctuation stuck on the end is not part of the name: "…it is in docs/plan/." */
46
+ const trim = (t) => t.replace(/[.,;:!?]+$/, '')
47
+
48
+ /** The address a written one means: a scheme added where the reply left it out. */
49
+ export function webUrl(written) {
50
+ if (/^https?:\/\//i.test(written)) return written
51
+ if (/^(?:localhost|127\.0\.0\.1|\[::1\])[:/]/i.test(written)) return `http://${written}`
52
+ return `https://${written}`
53
+ }
54
+
55
+ /**
56
+ * The places a reply points at, in the order it says them, each as `{ kind, written }`:
57
+ * `web` for an address, `path` for something on disk. Which paths are real, and what they are,
58
+ * is not known here.
59
+ */
60
+ export function references(text) {
61
+ const clean = String(text ?? '').replace(FENCE, '')
62
+ const found = []
63
+ const taken = [] // the stretches already claimed by an address, so a path is not found inside one
64
+ for (const m of clean.matchAll(WEB)) {
65
+ const written = trim(m[0])
66
+ found.push({ i: m.index, kind: 'web', written })
67
+ taken.push([m.index, m.index + m[0].length])
68
+ }
69
+ for (const m of clean.matchAll(PATH)) {
70
+ const written = trim(m[0])
71
+ // A bare name counts only when it is a document: `README.md` is a place, `index` is a word.
72
+ if (!written.includes('/') && !DOC.has(extOf(written))) continue
73
+ if (taken.some(([a, b]) => m.index < b && m.index + m[0].length > a)) continue
74
+ if (/^\/+$/.test(written)) continue
75
+ found.push({ i: m.index, kind: 'path', written })
76
+ }
77
+ const seen = new Set()
78
+ return found
79
+ .sort((a, b) => a.i - b.i)
80
+ .filter((r) => !seen.has(r.written) && seen.add(r.written))
81
+ .map(({ kind, written }) => ({ kind, written }))
82
+ }
83
+
84
+ /** `written` as an absolute path, or null when it cannot be one. */
85
+ export function absolute(written, root, home) {
86
+ const p = written.replace(/\/+$/, '')
87
+ if (p.startsWith('~/')) return home ? `${home.replace(/\/+$/, '')}/${p.slice(2)}` : null
88
+ if (p.startsWith('/')) return p
89
+ if (!root) return null
90
+ const parts = `${root.replace(/\/+$/, '')}/${p}`.split('/')
91
+ const out = []
92
+ for (const s of parts) {
93
+ if (s === '..') out.pop()
94
+ else if (s !== '.' && s !== '') out.push(s)
95
+ }
96
+ return '/' + out.join('/')
97
+ }
98
+
99
+ /** `abs` as a path inside `root`, or null when it is somewhere else. */
100
+ export function inside(abs, root) {
101
+ const base = String(root || '').replace(/\/+$/, '')
102
+ return base && abs.startsWith(base + '/') ? abs.slice(base.length + 1) : null
103
+ }
104
+
105
+ /** The viewer's address for one file, the same shape the board's git mark opens. */
106
+ export const viewerUrl = (port, root, rel) =>
107
+ `http://localhost:${port}/files.html?${new URLSearchParams({ repo: root, f: rel })}`
108
+
109
+ /** The address of an artifact, out of whatever the Artifact tool answered. */
110
+ export const artifactUrl = (text) =>
111
+ String(text ?? '').match(/https:\/\/claude\.ai\/(?:code\/)?artifact\/[A-Za-z0-9_-]+/)?.[0] ?? null
112
+
113
+ /** An artifact's title, out of its page: the `<title>` the page carries, when it carries one. */
114
+ export const titleOf = (html) => {
115
+ const t = String(html ?? '').match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]
116
+ return t ? t.replace(/\s+/g, ' ').trim() || null : null
117
+ }
118
+
119
+ /** The artifacts a reply mentions, by title or by address, with where it mentions them. */
120
+ export function artifactsIn(text, artifacts) {
121
+ const src = String(text ?? '')
122
+ const out = []
123
+ for (const [title, url] of artifacts) {
124
+ const at = [src.indexOf(title), src.indexOf(url)].filter((i) => i >= 0)
125
+ if (at.length) out.push({ i: Math.min(...at), title, url })
126
+ }
127
+ return out
128
+ }
@@ -0,0 +1,204 @@
1
+ // ── k0-links: what the last reply pointed at, one click away ──────────────────
2
+ // A mod: Claude Code runs these hooks inside every session, k0 or not. They do something only
3
+ // while k0 is running on this machine.
4
+ //
5
+ // At the end of each reply it reads what the reply pointed at — addresses, folders, documents,
6
+ // the artifacts this session published — and puts them in a row just above the prompt. One plain
7
+ // click on an entry opens it: an address in the browser, a folder in Finder, a document in k0's
8
+ // viewer, an artifact on its claude.ai page.
9
+ //
10
+ // The reply itself is left exactly as Claude Code draws it. The macOS Terminal cannot open a link
11
+ // drawn inside the text, and redrawing Claude's replies to work around that was the wrong price:
12
+ // the row is the mod's own space, and its entries are buttons, which the terminal does not have
13
+ // to understand — Claude Code receives the click itself (in its fullscreen mode) and the mod opens
14
+ // the target with the system's own `open`.
15
+
16
+ import {
17
+ DOC,
18
+ MAX_REFS,
19
+ references,
20
+ webUrl,
21
+ absolute,
22
+ inside,
23
+ extOf,
24
+ viewerUrl,
25
+ artifactUrl,
26
+ titleOf,
27
+ artifactsIn,
28
+ } from './links.js'
29
+
30
+ /** A title this short is a word, and would be found in every other reply. */
31
+ const MIN_TITLE = 4
32
+ /** What each kind of entry looks like in the row: the place it opens in, at a glance. */
33
+ const ICON = { web: '🌐', folder: '📁', doc: '📄', artifact: '✦' }
34
+
35
+ let port = 4319
36
+ let root = null
37
+ let home = null
38
+ let on = false // whether k0 answered the last time it was asked
39
+ const artifacts = new Map() // title → claude.ai address
40
+ let refs = [] // what the last reply pointed at: { icon, label, target }
41
+
42
+ /** @type {import('claude-code').Register} */
43
+ export const register = (hook) => {
44
+ hook('session.start', async ($, e, next) => {
45
+ const set = await $.env.get('K0_PORT')
46
+ if (set && /^\d+$/.test(set)) port = Number(set)
47
+ home = (await $.env.get('HOME')) ?? null
48
+ root = await topLevel($, e.cwd)
49
+ return next(e)
50
+ })
51
+
52
+ hook('prompt.submit', ($, e, next) => {
53
+ if (refs.length) {
54
+ refs = []
55
+ $.ui.invalidate('ui.render')
56
+ }
57
+ return next(e)
58
+ })
59
+
60
+ hook('tool.call', { tool: 'Artifact' }, async ($, e, next) => {
61
+ const ran = await next(e)
62
+ try {
63
+ await remember($, e, ran)
64
+ } catch {
65
+ // An artifact that could not be read back is simply not in the row.
66
+ }
67
+ return ran
68
+ })
69
+
70
+ hook('turn.complete', ($, e, next) => {
71
+ if (!e.agentId) void refresh($, e.answer)
72
+ return next(e)
73
+ })
74
+
75
+ hook('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
76
+ if (!on || !refs.length || e.props.hasSurvey) return next(e)
77
+ const { Box, Button } = $.ui.resolve(e)
78
+ return h(
79
+ Box,
80
+ { flexWrap: 'wrap', columnGap: 3 },
81
+ ...refs.map((r, i) =>
82
+ h(Button, {
83
+ key: `ref-${i}`,
84
+ label: `${r.icon} ${r.label}`,
85
+ plain: true,
86
+ onPress: () => void launch($, r.target),
87
+ })
88
+ )
89
+ )
90
+ })
91
+ }
92
+
93
+ /** The repository the session is in, as k0 knows it: git's top level, else the folder itself. */
94
+ async function topLevel($, cwd) {
95
+ try {
96
+ const { exitCode, stdout } = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd })
97
+ if (exitCode === 0 && stdout.trim()) return stdout.trim()
98
+ } catch {
99
+ // No git on this machine: the folder is the best guess there is.
100
+ }
101
+ return cwd
102
+ }
103
+
104
+ /**
105
+ * Asks k0 which of `paths` are documents its viewer has in this repository. Null when k0 is not
106
+ * answering; `known: false` when it answers but has never seen this folder.
107
+ */
108
+ async function exist($, paths) {
109
+ try {
110
+ const r = await $.http.fetch(`http://localhost:${port}/api/files/exist`, {
111
+ method: 'POST',
112
+ headers: { 'content-type': 'application/json' },
113
+ body: JSON.stringify({ repo: root, paths }),
114
+ })
115
+ if (r.ok) return { known: true, paths: new Set(JSON.parse(r.text).paths ?? []) }
116
+ // k0's own refusal for a folder it has never opened a session in — anything else on this port
117
+ // is not k0, and is treated as k0 not running.
118
+ if (r.status === 400 && /repository/i.test(r.text)) return { known: false, paths: new Set() }
119
+ } catch {
120
+ // Nobody listening: k0 is off.
121
+ }
122
+ return null
123
+ }
124
+
125
+ /** What a folder or file on disk is, or null when there is nothing there. */
126
+ async function kindOf($, abs) {
127
+ try {
128
+ return (await $.fs.stat(abs)).kind
129
+ } catch {
130
+ return null
131
+ }
132
+ }
133
+
134
+ /** Reads the reply that just ended, and redraws the row once, at the end. */
135
+ async function refresh($, answer) {
136
+ const found = references(answer)
137
+ const docs = []
138
+ const out = []
139
+ for (const r of found) {
140
+ if (r.kind === 'web') {
141
+ out.push({ icon: ICON.web, label: r.written, target: webUrl(r.written) })
142
+ continue
143
+ }
144
+ const abs = absolute(r.written, root, home)
145
+ if (!abs) continue
146
+ const kind = await kindOf($, abs)
147
+ // An address written without its scheme, the domain on disk nowhere: still an address.
148
+ if (!kind && /^(?:[a-z0-9-]+\.)+[a-z]{2,}\//i.test(r.written)) {
149
+ out.push({ icon: ICON.web, label: r.written, target: webUrl(r.written) })
150
+ } else if (kind === 'dir') {
151
+ out.push({ icon: ICON.folder, label: r.written, target: abs })
152
+ } else if (kind === 'file' && DOC.has(extOf(abs))) {
153
+ const rel = inside(abs, root)
154
+ if (rel) {
155
+ const entry = { icon: ICON.doc, label: r.written, target: null, rel }
156
+ docs.push(entry)
157
+ out.push(entry)
158
+ }
159
+ }
160
+ // Code and every other file: not a place to go, it stays out of the row.
161
+ }
162
+
163
+ const got = await exist($, docs.map((d) => d.rel))
164
+ on = Boolean(got)
165
+ for (const d of docs) if (got?.known && got.paths.has(d.rel)) d.target = viewerUrl(port, root, d.rel)
166
+
167
+ const mentioned = artifactsIn(answer, artifacts).map((a) => ({ icon: ICON.artifact, label: a.title, target: a.url }))
168
+ const seen = new Set()
169
+ refs = [...out.filter((r) => r.target), ...mentioned]
170
+ .filter((r) => !seen.has(r.target) && seen.add(r.target))
171
+ .slice(0, MAX_REFS)
172
+ $.ui.invalidate('ui.render')
173
+ }
174
+
175
+ /** Opens a target the way the system opens it: a browser tab, a Finder window. */
176
+ async function launch($, target) {
177
+ for (const argv of [['open', target], ['xdg-open', target]]) {
178
+ try {
179
+ const { exitCode } = await $.process.run(argv)
180
+ if (exitCode === 0) return
181
+ } catch {
182
+ // Not this system's opener: try the next one.
183
+ }
184
+ }
185
+ $.ui.toast(`Could not open ${target}`)
186
+ }
187
+
188
+ /** A published artifact, kept under its title so the reply that mentions it can list it. */
189
+ async function remember($, e, ran) {
190
+ if (ran.deny !== undefined || ran.isError) return
191
+ if ((e.action ?? 'publish') !== 'publish' || e.asset) return
192
+ const url = artifactUrl(ran.text) ?? artifactUrl(e.url)
193
+ if (!url) return
194
+ let title = null
195
+ if (typeof e.file_path === 'string' && /\.html?$/i.test(e.file_path)) {
196
+ title = titleOf(await $.fs.read(e.file_path))
197
+ }
198
+ title ??= e.title ?? null
199
+ if (!title && typeof e.file_path === 'string') title = e.file_path.split('/').pop().replace(/\.[^.]+$/, '')
200
+ if (!title || title.length < MIN_TITLE) return
201
+ // A redeploy can rename the page: the address keeps one title, the newest.
202
+ for (const [t, u] of artifacts) if (u === url) artifacts.delete(t)
203
+ artifacts.set(title, url)
204
+ }
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: k0-next
3
+ description: Answers "what do I do now" from what the k0 server already knows — the order the stories are in, the star, the dependencies that are not done yet, what is open, and what has been sitting in Review waiting for somebody to look at it. One call and a sentence, not a round of reasoning. Use this skill when the user types "/k0-next" or says "what should I do now", "what's next", "pick the next story", "what do I pick up", "cosa faccio adesso", "cosa mi conviene fare", "qual è la prossima storia", "da dove riparto". Do not use it to decide priority — that is /k0-order — to discuss a story, or to guess an order the server has not been told.
4
+ ---
5
+
6
+ # /k0-next
7
+
8
+ One question, one answer: what to pick up in this repository right now.
9
+
10
+ ```bash
11
+ PORT="${K0_PORT:-4319}"
12
+ REPO="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"; REPO="${REPO%/.git}"
13
+ [ -d "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)"
14
+ curl -sS --max-time 30 --get --data-urlencode "repo=$REPO" \
15
+ "http://127.0.0.1:$PORT/api/backlog/next"
16
+ ```
17
+
18
+ `127.0.0.1`, not `k0.localhost`: browsers resolve that name by themselves, `curl` does not.
19
+ `--get --data-urlencode`, not `?repo=$REPO`: a repository path with a space in it makes `curl`
20
+ refuse the address outright, and one with a `#` in it quietly sends half.
21
+ `$SCRATCH` is the session's scratch directory; nothing is written outside it. `REPO` is the
22
+ repository the stories are filed under — the main one, even when this session sits in a
23
+ subdirectory of it or inside a worktree, which is why it is not `$PWD`.
24
+
25
+ If `curl` cannot connect, the k0 server is not running: say so (`k0-board start`, or the tray
26
+ icon → *Restart*) and **stop**. If the answer is `{"enabled": false}`, say in one line that the
27
+ backlog is switched off in k0's settings and **stop**. If `REPO` is empty — or the answer carries
28
+ an `error` and no story at all — this is not a checkout and the server refused the question: say
29
+ so, ask once which repository he means, and take the path he gives. That is not an empty backlog
30
+ and must never be reported as one.
31
+
32
+ **If the answer names no story at all**, do not report an empty backlog yet. Ask the board
33
+ itself, once, and let it say which of the two things is true:
34
+
35
+ ```bash
36
+ curl -sS --max-time 30 --get --data-urlencode "repo=$REPO" \
37
+ "http://127.0.0.1:$PORT/api/backlog" -o "$SCRATCH/k0-backlog.json"
38
+ ```
39
+
40
+ `enabled` false → the feature is off, and that is what you say. An `error` and no `stories` key →
41
+ the repository was not recognised, which is the answer above and not an empty board. Stories
42
+ present but none ready → say what is blocking them. `stories: []` → say the board is empty for
43
+ this repository.
44
+ "Nothing to pick up" and "the backlog is switched off" look identical from `/next` alone, and
45
+ telling a man his backlog is empty when it is merely off is the one answer worse than no answer.
46
+
47
+ The answer is otherwise the story to pick up and **why** — the server has already weighed the
48
+ order, the star, the dependencies, how long something has been in `Review`. That reasoning is
49
+ done. Do not redo it, do not second-guess it, and do not fetch the whole board to check its work.
50
+
51
+ ## What to say
52
+
53
+ Three lines at most, in the user's language:
54
+
55
+ > **K42 · 1.12 — Fix the API when the token expires.** Starred, nothing blocking it, and it is
56
+ > the first thing in the order.
57
+ >
58
+ > K37 has been in Review for four days.
59
+
60
+ The key, the title, the one sentence of why, and — only when the answer carries
61
+ one — the thing that has been waiting. **Then stop.** No question, no box, no "shall I start it".
62
+ He asked what to do next and he has been told; what he does with that is his next message.
63
+
64
+ If that next message says to start it, this is the call:
65
+
66
+ ```bash
67
+ curl -sS --max-time 30 -X POST "http://127.0.0.1:$PORT/api/backlog/story/$STORY_ID/start" \
68
+ -H 'content-type: application/json' -d '{"auto_send": false}'
69
+ ```
70
+
71
+ That opens a Claude Code session on the story, the same way the board's button does.
72
+
73
+ ## Rules
74
+
75
+ - **One call.** The second one happens only when `/next` came back empty, or when he asks for a
76
+ shortlist rather than the next thing — and then show three rows, not the board.
77
+ - **Never ask a question here.** The answer to "what now" is an answer, not a discussion, and a
78
+ question box on a read-only lookup is the beat this skill exists to skip.
79
+ - **Never invent an order.** If the server says nothing is ready — everything blocked, or the
80
+ backlog empty — say that plainly and say what is blocking it. Do not pick something anyway.
81
+ - **Never start the session unasked**, and never move a state.
82
+ - Talk to the user in his language. The keys and the state names stay as they are.
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: k0-order
3
+ description: Reorders the k0 backlog from an instruction said out loud — what goes to the top, what waits on what, what is starred. Resolves the keys to stories itself, applies the change through the API and answers in one line. Use this skill when the user types "/k0-order", or says "K51 to the top", "K42 depends on K37", "star K19", "move K7 below K12", "put the login one last", "unstar K3", "K51 in cima", "K42 dipende da K37", "metti la stella su K19", "sposta K7 sotto K12", "questa prima di quella". Do not use it to decide what to work on next (that is /k0-next), to change a story's state or title (/k0-story), or to break one up (/k0-split).
4
+ ---
5
+
6
+ # /k0-order <what to move, what waits on what, what to star>
7
+
8
+ Priority dictated at the speed it is thought: *K51 to the top, K42 waits on K37, star K19.*
9
+ Several instructions in one breath is the normal case, not the exception.
10
+
11
+ ## Procedure
12
+
13
+ 1. **Read the board.**
14
+
15
+ ```bash
16
+ PORT="${K0_PORT:-4319}"
17
+ REPO="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"; REPO="${REPO%/.git}"
18
+ [ -d "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)"
19
+ curl -sS --max-time 30 --get --data-urlencode "repo=$REPO" \
20
+ "http://127.0.0.1:$PORT/api/backlog" -o "$SCRATCH/k0-backlog.json"
21
+ ```
22
+
23
+ `127.0.0.1`, not `k0.localhost`: `curl` does not resolve that name. `--get --data-urlencode`,
24
+ not `?repo=$REPO`: a repository path with a space in it makes `curl` refuse the address
25
+ outright, and one with a `#` in it quietly sends half. `$SCRATCH` is the session's
26
+ scratch directory; nothing is written outside it. `REPO` is the repository the stories are
27
+ filed under — the main one, even when this session sits in a subdirectory of it or inside a
28
+ worktree, which is why it is not `$PWD`.
29
+
30
+ - `curl` cannot connect → the server is not running. Say so (`k0-board start`, or the tray
31
+ icon → *Restart*) and **stop**.
32
+ - `{"enabled": false}` → say in one line that the backlog is switched off in k0's settings and
33
+ **stop**.
34
+ - `REPO` empty → this is not a git repository. Ask once which repository this belongs to.
35
+
36
+ That one file has every key, title, state, star, `sort_hint` and dependency you need.
37
+ Do not ask the user for any of it and do not fetch it twice.
38
+
39
+ 2. **Resolve what he named.** `K42` is a key; *the login one* is a title. Keys are per
40
+ repository, so `K42` here is not `K42` next door — resolve inside the repository you are in.
41
+ If a key is not on this board, say so and ask which repository; do not guess at another one.
42
+ If a description matches two stories, ask which. Never act on a key you have not seen in the
43
+ file.
44
+
45
+ 3. **Work out every change before sending one.** The list is small, so hold it whole: a
46
+ contradiction inside one breath — *K7 to the top, and K7 after K12* — is caught now, and
47
+ asked about now, not half-applied.
48
+
49
+ 4. **Apply it.**
50
+
51
+ **Position.** Stories are ordered by `sort_hint` ascending, ties broken by age — the older
52
+ story wins a tie. So *to the top* is not a fixed number, it is **ten below the lowest
53
+ `sort_hint` on the board**, and *to the bottom* is ten above the highest. Both numbers are in
54
+ the file you read in step 1; work them out from it every time:
55
+
56
+ ```bash
57
+ # lowest sort_hint on the board is 20 → to the top is 10
58
+ curl -sS --max-time 30 -X PATCH "http://127.0.0.1:$PORT/api/backlog/story/$ID" \
59
+ -H 'content-type: application/json' -d '{"sort_hint": 10}'
60
+ ```
61
+
62
+ Never send a hardcoded number, and never the one in that example. Send `-1` twice and the
63
+ second story ties with the first, the tie goes to whichever is older, and the story he asked
64
+ to put on top lands second — while the receipt line says it went to the top. He has no way to
65
+ see that until he reloads the board.
66
+
67
+ To put one *between* two neighbours that have no room between them, renumber: take the
68
+ affected list in the order he now wants it, hand out 10, 20, 30 … and `PATCH` only the rows
69
+ whose number actually changed. Renumbering the ones that did not move is churn the mirror
70
+ then has to write out again.
71
+
72
+ **Star.**
73
+
74
+ ```bash
75
+ curl -sS --max-time 30 -X PATCH "http://127.0.0.1:$PORT/api/backlog/story/$ID" \
76
+ -H 'content-type: application/json' -d '{"starred": true}'
77
+ ```
78
+
79
+ **Dependency.** `story_id` is the one that waits, `depends_on` is the one it waits for — say
80
+ the sentence out loud before you fill it in, because inverted dependencies are silent and
81
+ only turn up weeks later.
82
+
83
+ ```bash
84
+ curl -sS --max-time 30 -X POST "http://127.0.0.1:$PORT/api/backlog/dependency" \
85
+ -H 'content-type: application/json' -d '{"story_id": 42, "depends_on": 37}'
86
+ ```
87
+
88
+ Removing one is the same call with `"remove": true`.
89
+
90
+ 5. **Say what you did, in one line.** In his language, keys and what changed, nothing else:
91
+
92
+ > K51 to the top, K42 now waits on K37, star on K19.
93
+
94
+ No table, no recap of the board, no offer to reorder anything else. He is mid-thought and the
95
+ line is a receipt, not a report.
96
+
97
+ ## Dependencies warn, they never block
98
+
99
+ A dependency marks a story on the board and nothing more. It does not stop it being started,
100
+ planned or finished, and this skill never refuses an instruction because of one. When a change
101
+ leaves a story waiting on something unfinished, that goes in the same receipt line as a clause:
102
+
103
+ > K42 now waits on K37, which is still Planned — a note, not a lock.
104
+
105
+ Two things are worth stopping for, and both are mistakes rather than priorities:
106
+
107
+ - **a story depending on itself** — say so and do not send it;
108
+ - **a loop** — K42 waits on K37, which already waits on K42. You can see it in the `deps` of the
109
+ file you read in step 1. Name the loop, do not send the call, and let him choose which edge he
110
+ actually meant.
111
+
112
+ ## Rules
113
+
114
+ - **Only order, star and dependencies.** Not state, not title, not epic, not deletion. If he asks
115
+ for one of those in the same breath, do the ordering, say which part you left alone and which
116
+ skill does it, and stop.
117
+ - **Never invent a key.** `key_num` is the server's to allocate. A key that is not on the board
118
+ is a mistake to report, not a story to create.
119
+ - **Never ask what the API can tell you.** The current order, who is starred, what already
120
+ depends on what, whether a story is blocked — it is all in step 1's answer.
121
+ - **Talk in the language the user speaks.**
122
+ - **No files outside the scratch directory**, and never write into `.k0/` by hand: the server
123
+ mirrors the change there on its own.