@pi-unipi/kanboard 2.20.5 → 3.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -66
- package/index.ts +209 -63
- package/package.json +18 -23
- package/skills/kanboard/SKILL.md +106 -0
- package/src/bin.ts +210 -0
- package/src/commands.ts +1368 -0
- package/src/guard.ts +143 -0
- package/src/runner.ts +817 -0
- package/src/settings.ts +158 -0
- package/src/shapes.ts +220 -0
- package/commands.ts +0 -83
- package/parser/checkbox-parser.ts +0 -165
- package/parser/frontmatter.ts +0 -26
- package/parser/index.ts +0 -94
- package/parser/milestones.ts +0 -110
- package/parser/plans.ts +0 -111
- package/server/index.ts +0 -266
- package/server/routes/milestone.ts +0 -38
- package/server/routes/workflow.ts +0 -41
- package/skills/kanboard-doctor/SKILL.md +0 -71
- package/types.ts +0 -67
- package/ui/components/checklist.ts +0 -58
- package/ui/components/copy-button.ts +0 -23
- package/ui/components/status-badge.ts +0 -21
- package/ui/layouts/base.ts +0 -68
- package/ui/milestone/page.ts +0 -140
- package/ui/static/app.js +0 -73
- package/ui/static/style.css +0 -729
- package/ui/workflow/page.ts +0 -182
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kanboard
|
|
3
|
+
description: "Kanboard — the project's deferred-work board. Use when the user asks to note a task for later, to see what is on the board, or while working a board task: read it with `unipi-kanboard show`, add notes, block with a question, or file follow-up work."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kanboard
|
|
7
|
+
|
|
8
|
+
The board is a per-project list of deferred work: backlog, todo, in progress, in
|
|
9
|
+
review, blocked, done, cancelled (and an archive). **The board files are owned by
|
|
10
|
+
the `unipi-kanboard` binary** — never edit `task.md` files by hand; every write
|
|
11
|
+
goes through the CLI so the format and the transition rules hold.
|
|
12
|
+
|
|
13
|
+
## The CLI
|
|
14
|
+
|
|
15
|
+
Always pass the actor and the project, and call it by absolute path (it may not
|
|
16
|
+
be on `PATH`):
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
<binary> --actor agent --project <slug> list [--ready] [--json]
|
|
20
|
+
<binary> --actor agent --project <slug> show <ID>
|
|
21
|
+
<binary> --actor agent --project <slug> next # what claim-next would pick + why
|
|
22
|
+
<binary> --actor agent --project <slug> chain <ID> # upstream deps + downstream dependents
|
|
23
|
+
<binary> --actor agent --project <slug> search "<text>" [--all] # id/title/body, archived excluded unless --all
|
|
24
|
+
<binary> --actor agent --project <slug> add "<title>" [--status todo] [--after <ID>] [--body-file <f>] [--attach <file>]…
|
|
25
|
+
<binary> --actor agent --project <slug> note <ID> "<text>"
|
|
26
|
+
<binary> --actor agent --project <slug> attach <ID> <file> --note "<what it shows>"
|
|
27
|
+
<binary> --actor agent --project <slug> attachments <ID>
|
|
28
|
+
<binary> --actor agent --project <slug> edit <ID> --title|--body|--labels … # only tasks you created, while in backlog/todo
|
|
29
|
+
<binary> --actor agent --project <slug> move <ID> blocked --comment "<what you need>"
|
|
30
|
+
<binary> --actor agent --project <slug> link <ID> --after <DEP>
|
|
31
|
+
<binary> --actor agent --project <slug> unlink <ID> --after <DEP>
|
|
32
|
+
<binary> --actor agent --project <slug> order <ID> --top|--bottom|--before <ID>
|
|
33
|
+
<binary> --actor agent --project <slug> queue <ID>… | queue --list # your session's work queue (limit: setting queueMax, default 10)
|
|
34
|
+
<binary> --actor agent --project <slug> unqueue [<ID>…] # no ids clears it
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--json` gives machine-readable output for every subcommand (task objects carry
|
|
38
|
+
`ready`, `waitingFor`, `depsStatus` and `staleness`).
|
|
39
|
+
|
|
40
|
+
`settings show` reads the pi runtime and effective limits; `settings set`
|
|
41
|
+
and `rotate-token` are user-only — the agent is refused.
|
|
42
|
+
|
|
43
|
+
## Lanes and who may move what
|
|
44
|
+
|
|
45
|
+
| Move | Who |
|
|
46
|
+
|---|---|
|
|
47
|
+
| backlog ↔ todo | user, agent |
|
|
48
|
+
| todo → in progress | **the runner only** (`claim-next`) |
|
|
49
|
+
| edit a task | agent — only tasks it created, and only in backlog/todo |
|
|
50
|
+
| in progress → in review | **the runner only** (when your turn ends) |
|
|
51
|
+
| in progress → blocked | agent, system — **comment required** (what you need) |
|
|
52
|
+
| blocked → todo | user only — comment required (the answer) |
|
|
53
|
+
| in review → done | user only |
|
|
54
|
+
| in review → todo/backlog | user only — comment required (rework note) |
|
|
55
|
+
| anything → cancelled | user only |
|
|
56
|
+
| in review → archived | user only (one-click archive) |
|
|
57
|
+
| done/cancelled → archived | user (or automatically) |
|
|
58
|
+
|
|
59
|
+
`done`, `cancelled` and `archived` are final: nothing leaves them — file a new
|
|
60
|
+
task instead (the UI's "Duplicate" does that for you). Old archived/cancelled
|
|
61
|
+
tasks may be moved to cold storage (`projects/<slug>/cold/`); the board never
|
|
62
|
+
lists them — read the files there directly if you need one.
|
|
63
|
+
|
|
64
|
+
## Rules for agents
|
|
65
|
+
|
|
66
|
+
1. **Never pass `--actor user`.** That is an honour system: pretending to be the
|
|
67
|
+
user to cancel a task or mark it done breaks the board's whole contract.
|
|
68
|
+
2. **Never move a task to `in_review` or `done`.** The runner writes those when
|
|
69
|
+
your turn ends; claiming them yourself loses the summary and the review step.
|
|
70
|
+
3. **Never cancel.** If a task should be dropped, block it with
|
|
71
|
+
`move <ID> blocked --comment "suggest cancel: <why>"` and let the user decide.
|
|
72
|
+
4. **To ask the user something, block the task and stop**: `move <ID> blocked
|
|
73
|
+
--comment "<exactly what you need>"`. The answer arrives as a comment the next
|
|
74
|
+
time the task is claimed.
|
|
75
|
+
5. **Work only on the task you were given.** Follow-up work goes to the board as
|
|
76
|
+
a new task in Backlog (`add "<title>"`), optionally `link <new> --after <ID>`.
|
|
77
|
+
A session holds one claim at a time, and at most `maxSessions` (default 2)
|
|
78
|
+
sessions may run tasks in a project — if the board refuses a claim, that is
|
|
79
|
+
why. You may
|
|
80
|
+
block only the task your own session is running (`move <ID> blocked` checks
|
|
81
|
+
`--session`/`UNIPI_KANBOARD_SESSION` against the claim).
|
|
82
|
+
6. **To have tasks worked during a `/unipi:kanboard-do` turn, queue them** with
|
|
83
|
+
`queue <IDs>` (Todo tasks, up to the queue limit — setting `queueMax`, default 10) — the runner starts them one by one
|
|
84
|
+
after the turn ends. Never claim or start them yourself.
|
|
85
|
+
7. Use `note <ID> "<text>"` for progress worth remembering (decisions, what you
|
|
86
|
+
verified, what you left undone) — it is the activity log the next reader sees.
|
|
87
|
+
8. Dependencies form a DAG: a task is ready only when every dep reached the chain
|
|
88
|
+
gate (`in_review` by default, `done` when configured). Cancelled deps block
|
|
89
|
+
forever, so unlink or re-plan instead of waiting. `chain <ID>` shows the
|
|
90
|
+
whole line, `next` shows what would be picked and why others wait.
|
|
91
|
+
|
|
92
|
+
## Terminal-only execution
|
|
93
|
+
|
|
94
|
+
The web UI can create, edit, reorder, link and move tasks, but it **never runs a
|
|
95
|
+
task** — there is no run button. Work starts only from a terminal with
|
|
96
|
+
`/unipi:kanboard-autowork start` (or the queue after a `/unipi:kanboard-do`). (The Done column's "Summarize & archive" does call the
|
|
97
|
+
agent command set in the board's Settings, but only to write a summary.)
|
|
98
|
+
|
|
99
|
+
## Attachments
|
|
100
|
+
|
|
101
|
+
Users attach screenshots, logs and documents in the board UI; they appear in the
|
|
102
|
+
text as markdown with `att:<ID>/<name>` references, and `show <ID> --json` lists
|
|
103
|
+
them under `attachments` with an absolute `path` — read the file from there
|
|
104
|
+
(use your image-reading tool for images). To hand back evidence, `attach` a file:
|
|
105
|
+
it is stored beside the board and the comment embeds it, so the user sees the
|
|
106
|
+
image or file inline.
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/kanboard — binary resolution + CLI bridge.
|
|
3
|
+
*
|
|
4
|
+
* One writer for the board is the Rust binary; this module finds it and runs
|
|
5
|
+
* it. Resolution order (first hit wins):
|
|
6
|
+
* 1. `UNIPI_KANBOARD_BIN` (an explicit path — tests and tinkerers)
|
|
7
|
+
* 2. the platform package `@pi-unipi/kanboard-<platform>-<arch>` (K4 ships these)
|
|
8
|
+
* 3. the dev build: `<repo>/crates/kanboard/target/{release,debug}/unipi-kanboard`
|
|
9
|
+
*
|
|
10
|
+
* Nothing is downloaded, built or guessed at runtime: when no binary is found,
|
|
11
|
+
* every kanboard command reports "kanboard binary unavailable for <platform>-<arch>"
|
|
12
|
+
* and does nothing else.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { dirname, join, resolve } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
export type BinarySource = "env" | "platform-package" | "dev-build";
|
|
22
|
+
|
|
23
|
+
export interface KanboardBinary {
|
|
24
|
+
path: string;
|
|
25
|
+
source: BinarySource;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `<platform>-<arch>` as used by the platform packages and in messages. */
|
|
29
|
+
export function platformKey(platform: string = process.platform, arch: string = process.arch): string {
|
|
30
|
+
return `${platform}-${arch}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function exeSuffix(platform: string = process.platform): string {
|
|
34
|
+
return platform === "win32" ? ".exe" : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Repo root, from `packages/kanboard/src/bin.ts` → up three levels. */
|
|
38
|
+
export function repoRoot(): string {
|
|
39
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isRunnable(path: string): boolean {
|
|
43
|
+
try {
|
|
44
|
+
return existsSync(path);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Where the platform package's binary lives, or null when the package is not
|
|
52
|
+
* installed. `from` is injectable so tests can simulate an installed layout.
|
|
53
|
+
*/
|
|
54
|
+
export function platformPackagePath(from?: string): string | null {
|
|
55
|
+
const name = `@pi-unipi/kanboard-${platformKey()}`;
|
|
56
|
+
try {
|
|
57
|
+
const require = createRequire(from ?? import.meta.url);
|
|
58
|
+
const pkg = require.resolve(`${name}/package.json`);
|
|
59
|
+
return join(dirname(pkg), "bin", `unipi-kanboard${exeSuffix()}`);
|
|
60
|
+
} catch {
|
|
61
|
+
// Not installed (K4 ships it) — fall through to the dev build.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function devBuildPath(): string | null {
|
|
67
|
+
const root = repoRoot();
|
|
68
|
+
const names = [`unipi-kanboard${exeSuffix()}`];
|
|
69
|
+
for (const profile of ["release", "debug"]) {
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const candidate = join(root, "crates", "kanboard", "target", profile, name);
|
|
72
|
+
if (isRunnable(candidate)) return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveBinary(env: NodeJS.ProcessEnv = process.env, from?: string): KanboardBinary | null {
|
|
79
|
+
const explicit = env.UNIPI_KANBOARD_BIN?.trim();
|
|
80
|
+
if (explicit) {
|
|
81
|
+
return isRunnable(explicit) ? { path: explicit, source: "env" } : null;
|
|
82
|
+
}
|
|
83
|
+
const packaged = platformPackagePath(from);
|
|
84
|
+
if (packaged && isRunnable(packaged)) return { path: packaged, source: "platform-package" };
|
|
85
|
+
const dev = devBuildPath();
|
|
86
|
+
if (dev) return { path: dev, source: "dev-build" };
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function unavailableMessage(platform: string = process.platform, arch: string = process.arch): string {
|
|
91
|
+
return `kanboard binary unavailable for ${platform}-${arch}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A rule/usage error the binary reported (its message is meant to be shown). */
|
|
95
|
+
export class KanboardCliError extends Error {
|
|
96
|
+
readonly code: number;
|
|
97
|
+
readonly kind: string;
|
|
98
|
+
|
|
99
|
+
constructor(message: string, code: number, kind: string) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "KanboardCliError";
|
|
102
|
+
this.code = code;
|
|
103
|
+
this.kind = kind;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface RunCliOptions {
|
|
108
|
+
/** Ask the binary for `--json` and parse it (default true). */
|
|
109
|
+
json?: boolean;
|
|
110
|
+
env?: NodeJS.ProcessEnv;
|
|
111
|
+
cwd?: string;
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
/** Extra env on top of `env` (UNIPI_KANBOARD_HOME etc.). */
|
|
114
|
+
extraEnv?: Record<string, string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface KanboardCli {
|
|
118
|
+
binary: KanboardBinary;
|
|
119
|
+
run<T = unknown>(args: string[], options?: RunCliOptions): Promise<T>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
123
|
+
|
|
124
|
+
/** Wrap a resolved binary into a runner that parses JSON and surfaces rule messages. */
|
|
125
|
+
export function createCli(binary: KanboardBinary, baseEnv: NodeJS.ProcessEnv = process.env): KanboardCli {
|
|
126
|
+
return {
|
|
127
|
+
binary,
|
|
128
|
+
run<T = unknown>(args: string[], options: RunCliOptions = {}): Promise<T> {
|
|
129
|
+
const json = options.json !== false;
|
|
130
|
+
const argv = json && !args.includes("--json") ? [...args, "--json"] : args;
|
|
131
|
+
const childEnv: NodeJS.ProcessEnv = {
|
|
132
|
+
...baseEnv,
|
|
133
|
+
...(options.env ?? {}),
|
|
134
|
+
...(options.extraEnv ?? {}),
|
|
135
|
+
// The UI/extension acts as the user; the agent's own invocations are the
|
|
136
|
+
// only ones that set actor=agent (via the skill/prompt).
|
|
137
|
+
UNIPI_KANBOARD_ACTOR: (options.extraEnv?.UNIPI_KANBOARD_ACTOR ?? baseEnv.UNIPI_KANBOARD_ACTOR ?? "user"),
|
|
138
|
+
};
|
|
139
|
+
return new Promise<T>((resolvePromise, reject) => {
|
|
140
|
+
execFile(
|
|
141
|
+
binary.path,
|
|
142
|
+
argv,
|
|
143
|
+
{
|
|
144
|
+
cwd: options.cwd,
|
|
145
|
+
env: childEnv,
|
|
146
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
147
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
148
|
+
windowsHide: true,
|
|
149
|
+
},
|
|
150
|
+
(error, stdout, stderr) => {
|
|
151
|
+
const out = String(stdout ?? "").trim();
|
|
152
|
+
const err = String(stderr ?? "").trim();
|
|
153
|
+
if (error && !out && !err) {
|
|
154
|
+
reject(new KanboardCliError(`${error.message}`, 1, "io"));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const decoded = decode(stderr, stdout);
|
|
158
|
+
if (error) {
|
|
159
|
+
const payload = decode(err, out);
|
|
160
|
+
if (payload && typeof payload === "object" && "error" in payload) {
|
|
161
|
+
const detail = payload as { error?: string; kind?: string };
|
|
162
|
+
reject(
|
|
163
|
+
new KanboardCliError(
|
|
164
|
+
detail.error ?? error.message,
|
|
165
|
+
(error as { code?: number }).code ?? 1,
|
|
166
|
+
detail.kind ?? "rule",
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
reject(new KanboardCliError(err || out || error.message, (error as { code?: number }).code ?? 1, "io"));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!json) {
|
|
175
|
+
resolvePromise(out as unknown as T);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
resolvePromise(JSON.parse(out) as T);
|
|
180
|
+
} catch (parseError) {
|
|
181
|
+
reject(new KanboardCliError(`unexpected output from unipi-kanboard: ${out.slice(0, 200)}`, 1, "parse"));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
void decoded;
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function decode(stderr: string, stdout: string): unknown {
|
|
193
|
+
for (const candidate of [stderr, stdout]) {
|
|
194
|
+
const text = candidate.trim();
|
|
195
|
+
if (!text.startsWith("{")) continue;
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(text);
|
|
198
|
+
} catch {
|
|
199
|
+
// keep looking
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Resolve + wrap, or report why nothing can run. */
|
|
206
|
+
export function openCli(env: NodeJS.ProcessEnv = process.env): KanboardCli | { error: string } {
|
|
207
|
+
const binary = resolveBinary(env);
|
|
208
|
+
if (!binary) return { error: unavailableMessage() };
|
|
209
|
+
return createCli(binary, env);
|
|
210
|
+
}
|