k0-board 0.4.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.
- package/.claude/skills/k0-changelog/SKILL.md +2 -2
- package/.claude/skills/k0-discuss/SKILL.md +105 -0
- package/.claude/skills/k0-discuss/references/rounds.md +280 -0
- package/.claude/skills/k0-epic/SKILL.md +196 -0
- package/.claude/skills/k0-links/.claude-plugin/plugin.json +6 -0
- package/.claude/skills/k0-links/hooks/hooks.json +1 -0
- package/.claude/skills/k0-links/hooks/links.js +128 -0
- package/.claude/skills/k0-links/hooks/register.js +204 -0
- package/.claude/skills/k0-next/SKILL.md +82 -0
- package/.claude/skills/k0-order/SKILL.md +123 -0
- package/.claude/skills/k0-plan/SKILL.md +156 -0
- package/.claude/skills/k0-split/SKILL.md +169 -0
- package/.claude/skills/k0-story/SKILL.md +108 -0
- package/.claude/skills/k0-ultracode/SKILL.md +251 -0
- package/.claude/skills/k0-ultracode/references/agent-brief.md +71 -0
- package/.claude/skills/k0-ultracode/references/modes.md +49 -0
- package/.claude/skills/k0-verify/SKILL.md +219 -0
- package/.claude/skills/k0-whatsnew/SKILL.md +95 -0
- package/.claude/skills/k0-work/SKILL.md +212 -0
- package/CHANGELOG.md +361 -1
- package/README.md +931 -111
- package/bin/k0-board.js +79 -11
- package/menubar/K0MenuBar.swift +35 -5
- package/package.json +13 -1
- package/platform/contract.js +30 -5
- package/platform/darwin/terminal.js +230 -56
- package/platform/index.js +2 -1
- package/platform/linux/index.js +3 -0
- package/platform/linux/tray.py +10 -10
- package/platform/shared/command.js +19 -3
- package/platform/shared/tmux.js +32 -14
- package/platform/win32/index.js +5 -2
- package/platform/win32/terminal.js +24 -8
- package/platform/win32/tray.ps1 +13 -13
- package/server/backlog.js +618 -0
- package/server/changelog.js +13 -13
- package/server/db.js +1800 -146
- package/server/files.js +60 -21
- package/server/git.js +53 -14
- package/server/idle.js +15 -13
- package/server/index.js +1251 -186
- package/server/launcher.js +122 -14
- package/server/machine.js +10 -1
- package/server/mirror.js +1048 -0
- package/server/prices.js +59 -0
- package/server/projects.js +57 -12
- package/server/servers.js +22 -1
- package/server/sessions.js +5 -5
- package/server/settings.js +25 -2
- package/server/update.js +324 -0
- package/server/usage.js +642 -0
- package/server/watcher.js +67 -13
- package/server/worktree.js +600 -0
- package/server/writer.js +98 -26
- package/web/awake.js +62 -0
- package/web/base.css +4 -0
- package/web/board.css +396 -16
- package/web/board.js +1182 -152
- package/web/changelog.js +3 -3
- package/web/files.js +73 -16
- package/web/index.html +109 -12
- package/web/list.css +536 -0
- package/web/list.js +1015 -0
- package/web/mentions.js +27 -9
- package/web/recency.js +10 -10
- package/web/units.js +1 -1
- package/web/usage.css +169 -0
- package/web/usage.html +78 -0
- package/web/usage.js +447 -0
- package/web/view.js +22 -3
- package/web/whatsnew.css +211 -0
- package/web/whatsnew.html +49 -0
- 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.
|