@pi-unipi/kanboard 3.0.0-alpha.29 → 3.0.0-alpha.30
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/index.ts +3 -0
- package/package.json +7 -7
- package/skills/kanboard/SKILL.md +13 -5
- package/src/commands.ts +27 -15
- package/src/move.ts +63 -0
package/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
isChildProcess,
|
|
30
30
|
registerCommandRunner,
|
|
31
31
|
registerEvidenceContributor,
|
|
32
|
+
registerMoveHandler,
|
|
32
33
|
registerNudgeProvider,
|
|
33
34
|
registerProgressRenderer,
|
|
34
35
|
setSharedKanboardStatus,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
|
|
38
39
|
import { openCli, type KanboardCli } from "./src/bin.js";
|
|
39
40
|
import { kanboardInvocations } from "./src/guard.js";
|
|
41
|
+
import { kanboardMoveHandler } from "./src/move.js";
|
|
40
42
|
import { maybeBadgeToolResult, registerBadgeRenderer } from "./src/badges.js";
|
|
41
43
|
import {
|
|
42
44
|
registerKanboardCommands,
|
|
@@ -69,6 +71,7 @@ export const SKILL_REVEAL_EVENT = "unipi:skills:reveal";
|
|
|
69
71
|
export const KANBOARD_SKILL = "kanboard";
|
|
70
72
|
|
|
71
73
|
export default function (pi: ExtensionAPI) {
|
|
74
|
+
registerMoveHandler(kanboardMoveHandler);
|
|
72
75
|
// One session id shared by the agent's bash calls (refined at session_start
|
|
73
76
|
// from the pi session id; children keep the value they inherited).
|
|
74
77
|
process.env.UNIPI_KANBOARD_SESSION ??= `pi-${process.pid}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/kanboard",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.30",
|
|
4
4
|
"description": "Kanboard v3 — deferred-work board: CLI/daemon bridge, task runner and skill",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
34
|
+
"@pi-unipi/core": "3.0.0-alpha.30"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"themes": []
|
|
49
49
|
},
|
|
50
50
|
"optionalDependencies": {
|
|
51
|
-
"@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.
|
|
52
|
-
"@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.
|
|
53
|
-
"@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.
|
|
54
|
-
"@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.
|
|
55
|
-
"@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.
|
|
51
|
+
"@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.30",
|
|
52
|
+
"@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.30",
|
|
53
|
+
"@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.30",
|
|
54
|
+
"@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.30",
|
|
55
|
+
"@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.30"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/skills/kanboard/SKILL.md
CHANGED
|
@@ -26,9 +26,10 @@ be on `PATH`):
|
|
|
26
26
|
<binary> --actor agent --project <slug> attach <ID> <file> --note "<what it shows>"
|
|
27
27
|
<binary> --actor agent --project <slug> attachments <ID>
|
|
28
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> start <ID> # todo → in progress,
|
|
29
|
+
<binary> --actor agent --project <slug> start <ID> # todo → in progress, or blocked → in progress to resume a task you blocked (either way claims it for your session and costs a -do slot)
|
|
30
30
|
<binary> --actor agent --project <slug> finish <ID> --comment "<summary>" [--attach <file>]… # in progress → in review, only a task you started (free)
|
|
31
31
|
<binary> --actor agent --project <slug> move <ID> blocked --comment "<what you need>" [--attach <file>]…
|
|
32
|
+
<binary> --actor agent --project <slug> move <ID> todo [--comment "<why>"] # unblock your own blocked task (comment optional) — then `start <ID>` to resume it, or leave it in todo
|
|
32
33
|
<binary> --actor agent --project <slug> link <ID> --after <DEP>
|
|
33
34
|
<binary> --actor agent --project <slug> unlink <ID> --after <DEP>
|
|
34
35
|
<binary> --actor agent --project <slug> order <ID> --top|--bottom|--before <ID>
|
|
@@ -54,13 +55,14 @@ and `rotate-token` are user-only — the agent is refused.
|
|
|
54
55
|
|---|---|
|
|
55
56
|
| backlog ↔ todo | user, agent |
|
|
56
57
|
| todo → in progress | the agent with `start <ID>` (claims it for its session; costs a `-do` slot) |
|
|
58
|
+
| blocked → in progress | the agent with `start <ID>`, to resume a task it blocked — same claim/session-cap rules, same `-do` slot cost |
|
|
57
59
|
| edit a task | agent — only tasks it created, and only in backlog/todo |
|
|
58
60
|
| in progress → in review | the agent with `finish <ID> --comment` — only a task its own session `start`ed |
|
|
59
61
|
| in progress → blocked | agent, system — **comment required** (what you need) |
|
|
60
62
|
| in progress → todo | user or system `release` — comment required (the session died or gave the task up) |
|
|
61
|
-
| blocked → todo | user
|
|
63
|
+
| blocked → todo | **user or agent** — comment optional (the answer, when there is one) |
|
|
62
64
|
| in review → done | user only |
|
|
63
|
-
| in review → todo/backlog | user only — comment
|
|
65
|
+
| in review → todo/backlog | user only — comment optional (a rework note, when there is one) |
|
|
64
66
|
| anything → cancelled | user only |
|
|
65
67
|
| in review → archived | user only (one-click archive) |
|
|
66
68
|
| done/cancelled → archived | user (or automatically) |
|
|
@@ -91,11 +93,17 @@ lists them — read the files there directly if you need one.
|
|
|
91
93
|
4. **Follow the blocking rule in your task prompt**: by default work
|
|
92
94
|
autonomously and record assumptions with `note <ID> "assumed: <what/why>"`;
|
|
93
95
|
block (with a comment saying exactly what you need) only when you truly
|
|
94
|
-
cannot continue — `move <ID> blocked --comment "<what you need>"`.
|
|
96
|
+
cannot continue — `move <ID> blocked --comment "<what you need>"`. Once you
|
|
97
|
+
have what you needed, you may unblock it yourself — `move <ID> todo` (a
|
|
98
|
+
comment is optional, unlike blocking) — and resume it with `start <ID>`
|
|
99
|
+
(blocked → in progress; the same claim and session-cap rules as a fresh
|
|
100
|
+
`start`, and it still costs a `-do` slot). You do not have to wait for the
|
|
101
|
+
user to answer in the board if you already have the answer.
|
|
95
102
|
5. **Work only on the task you were given.** Follow-up work goes to the board as
|
|
96
103
|
a new task in Backlog (`add "<title>"`), optionally `link <new> --after <ID>`.
|
|
97
104
|
At most `maxSessions` (default 2) sessions may hold tasks in a project at
|
|
98
|
-
once — if the board refuses a `start`, that is why
|
|
105
|
+
once — if the board refuses a `start`, that is why (this cap covers resuming
|
|
106
|
+
a blocked task too). You may
|
|
99
107
|
block only the task your own session is running (`move <ID> blocked` checks
|
|
100
108
|
`--session`/`UNIPI_KANBOARD_SESSION` against the claim).
|
|
101
109
|
6. **Sidekicks and subagents can read the board but never write it** — every
|
package/src/commands.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const HELP = `Kanboard commands
|
|
|
35
35
|
/unipi:kanboard close shut the board down
|
|
36
36
|
/unipi:kanboard onboard register this project
|
|
37
37
|
/unipi:kanboard status board and claims
|
|
38
|
-
/unipi:kanboard show [--all]
|
|
38
|
+
/unipi:kanboard show [<lane>] [--all] the board, in chat (lanes + claim order)
|
|
39
39
|
/unipi:kanboard doctor check the setup
|
|
40
40
|
/unipi:kanboard-add [-p 1-5] <title> add a task; lines below the title are the description (paste images there)
|
|
41
41
|
/unipi:kanboard-do <request> grant the agent task slots + a write budget
|
|
@@ -886,6 +886,13 @@ const SHOW_EXTRA_LANES: Array<{ id: string; label: string }> = [
|
|
|
886
886
|
{ id: "cancelled", label: "Cancelled" },
|
|
887
887
|
{ id: "archived", label: "Archived" },
|
|
888
888
|
];
|
|
889
|
+
const SHOW_FILTERS = ["backlog", "todo", "in_progress", "blocked", "in_review", "done", "cancelled", "archived"];
|
|
890
|
+
|
|
891
|
+
function showLanes(all: boolean, filter?: string): Array<{ id: string; label: string }> {
|
|
892
|
+
const lanes = [...SHOW_LANES, ...SHOW_EXTRA_LANES];
|
|
893
|
+
return filter ? lanes.filter((lane) => lane.id === filter) : all ? lanes : SHOW_LANES;
|
|
894
|
+
}
|
|
895
|
+
|
|
889
896
|
const PRIO_GLYPH: Record<string, string> = { urgent: "⇈", high: "↑", medium: "·", low: "↓", none: " " };
|
|
890
897
|
const PRIO_RANK: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3, none: 4 };
|
|
891
898
|
|
|
@@ -977,8 +984,9 @@ function laneRows(tasks: ShowTask[], lane: string): ShowRow[] {
|
|
|
977
984
|
}
|
|
978
985
|
|
|
979
986
|
/** Plain-text fallback (also what the message stores as content). */
|
|
980
|
-
export function renderShowPlain(project: string, tasks: ShowTask[], all: boolean): string {
|
|
981
|
-
const lanes = all
|
|
987
|
+
export function renderShowPlain(project: string, tasks: ShowTask[], all: boolean, filter?: string): string {
|
|
988
|
+
const lanes = showLanes(all, filter);
|
|
989
|
+
if (filter) tasks = tasks.filter((task) => task.status === filter);
|
|
982
990
|
const lines = [`${project} · ${tasks.length} tasks`];
|
|
983
991
|
for (const lane of lanes) {
|
|
984
992
|
const inLane = tasks.filter((task) => task.status === lane.id);
|
|
@@ -996,7 +1004,7 @@ export function renderShowPlain(project: string, tasks: ShowTask[], all: boolean
|
|
|
996
1004
|
|
|
997
1005
|
/** The themed renderer registered for SHOW_CUSTOM_TYPE. */
|
|
998
1006
|
export function showRenderer(
|
|
999
|
-
message: { content: unknown; details?: { project?: string; tasks?: ShowTask[]; all?: boolean } },
|
|
1007
|
+
message: { content: unknown; details?: { project?: string; tasks?: ShowTask[]; all?: boolean; lane?: string } },
|
|
1000
1008
|
_options: unknown,
|
|
1001
1009
|
theme: Theme,
|
|
1002
1010
|
): { render: (width: number) => string[]; invalidate: () => void } {
|
|
@@ -1004,10 +1012,11 @@ export function showRenderer(
|
|
|
1004
1012
|
render(width: number): string[] {
|
|
1005
1013
|
const details = message.details;
|
|
1006
1014
|
if (!details?.tasks) return [typeof message.content === "string" ? message.content : ""];
|
|
1007
|
-
const lanes = details.all
|
|
1008
|
-
const
|
|
1015
|
+
const lanes = showLanes(details.all ?? false, details.lane);
|
|
1016
|
+
const tasks = details.tasks.filter((task) => !details.lane || task.status === details.lane);
|
|
1017
|
+
const lines: string[] = [theme.bold(`${details.project ?? "board"} · ${tasks.length} tasks`)];
|
|
1009
1018
|
for (const lane of lanes) {
|
|
1010
|
-
const inLane =
|
|
1019
|
+
const inLane = tasks.filter((task) => task.status === lane.id);
|
|
1011
1020
|
if (inLane.length === 0) {
|
|
1012
1021
|
lines.push(` ${theme.fg("dim", `${lane.label} — empty`)}`);
|
|
1013
1022
|
continue;
|
|
@@ -1043,7 +1052,7 @@ export function showRenderer(
|
|
|
1043
1052
|
};
|
|
1044
1053
|
}
|
|
1045
1054
|
|
|
1046
|
-
async function runShow(deps: CommandDeps, ctx: ExtensionCommandContext, pi: ExtensionAPI, all: boolean): Promise<void> {
|
|
1055
|
+
async function runShow(deps: CommandDeps, ctx: ExtensionCommandContext, pi: ExtensionAPI, all: boolean, lane?: string): Promise<void> {
|
|
1047
1056
|
const client = deps.cli;
|
|
1048
1057
|
if (!client) {
|
|
1049
1058
|
ctx.ui.notify(`kanboard: ${deps.unavailable}`, "warning");
|
|
@@ -1055,7 +1064,7 @@ async function runShow(deps: CommandDeps, ctx: ExtensionCommandContext, pi: Exte
|
|
|
1055
1064
|
try {
|
|
1056
1065
|
// --all: this display lists every lane; the CLI bare default hides backlog/cancelled (UNI-62).
|
|
1057
1066
|
const payload = (await client.run(["list", "--all", "--json"], { cwd: ctx.cwd })) as { tasks?: ShowTask[] };
|
|
1058
|
-
tasks = payload.tasks ?? [];
|
|
1067
|
+
tasks = (payload.tasks ?? []).filter((task) => !lane || task.status === lane);
|
|
1059
1068
|
} catch (error) {
|
|
1060
1069
|
ctx.ui.notify(`kanboard: show failed — ${error instanceof KanboardCliError ? error.message : String(error)}`, "error");
|
|
1061
1070
|
return;
|
|
@@ -1063,9 +1072,9 @@ async function runShow(deps: CommandDeps, ctx: ExtensionCommandContext, pi: Exte
|
|
|
1063
1072
|
pi.sendMessage(
|
|
1064
1073
|
{
|
|
1065
1074
|
customType: SHOW_CUSTOM_TYPE,
|
|
1066
|
-
content: renderShowPlain(slug, tasks, all),
|
|
1075
|
+
content: renderShowPlain(slug, tasks, all, lane),
|
|
1067
1076
|
display: true,
|
|
1068
|
-
details: { project: slug, tasks, all },
|
|
1077
|
+
details: { project: slug, tasks, all, lane },
|
|
1069
1078
|
},
|
|
1070
1079
|
{ triggerTurn: false },
|
|
1071
1080
|
);
|
|
@@ -1095,7 +1104,7 @@ const SUB_DESCRIPTIONS: Record<Subcommand, string> = {
|
|
|
1095
1104
|
onboard: "Register this project on the board",
|
|
1096
1105
|
status: "Daemon, project counts and claims",
|
|
1097
1106
|
doctor: "Check the whole setup (binary, daemon, project, agent, claims)",
|
|
1098
|
-
show: "The board in chat:
|
|
1107
|
+
show: "The board in chat: show <lane> filters a lane; --all adds cancelled/archived",
|
|
1099
1108
|
};
|
|
1100
1109
|
|
|
1101
1110
|
/** /unipi:kanboard … */
|
|
@@ -1128,7 +1137,10 @@ export function kanboardCompletions(prefix: string): CompletionItem[] | null {
|
|
|
1128
1137
|
const showFlags = /^(show\s+)(\S*)$/.exec(raw);
|
|
1129
1138
|
if (showFlags) {
|
|
1130
1139
|
const [, before, partial] = showFlags;
|
|
1131
|
-
return [
|
|
1140
|
+
return [
|
|
1141
|
+
...SHOW_FILTERS.map((lane) => ({ value: lane, label: lane, description: `Show only ${lane}` })),
|
|
1142
|
+
{ value: "--all", label: "--all", description: "Include cancelled and archived lanes" },
|
|
1143
|
+
]
|
|
1132
1144
|
.filter((item) => item.value.startsWith(partial))
|
|
1133
1145
|
.map((item) => ({ ...item, value: fullArgs(before!, item.value) }));
|
|
1134
1146
|
}
|
|
@@ -1284,7 +1296,7 @@ export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): v
|
|
|
1284
1296
|
// stays plain text as a fallback and the context filter drops it.
|
|
1285
1297
|
if (typeof pi.registerMessageRenderer === "function") {
|
|
1286
1298
|
pi.registerMessageRenderer(SHOW_CUSTOM_TYPE, (message, options, theme) =>
|
|
1287
|
-
showRenderer(message as { content: unknown; details?: { project?: string; tasks?: ShowTask[]; all?: boolean } }, options, theme),
|
|
1299
|
+
showRenderer(message as { content: unknown; details?: { project?: string; tasks?: ShowTask[]; all?: boolean; lane?: string } }, options, theme),
|
|
1288
1300
|
);
|
|
1289
1301
|
}
|
|
1290
1302
|
|
|
@@ -1379,7 +1391,7 @@ export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): v
|
|
|
1379
1391
|
await runDoctor(deps, ctx, pi);
|
|
1380
1392
|
return;
|
|
1381
1393
|
case "show":
|
|
1382
|
-
await runShow(deps, ctx, pi, parts.includes("--all"));
|
|
1394
|
+
await runShow(deps, ctx, pi, parts.includes("--all"), parts.find((part) => SHOW_FILTERS.includes(part)));
|
|
1383
1395
|
return;
|
|
1384
1396
|
case "open":
|
|
1385
1397
|
if (!deps.cli) {
|
package/src/move.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { projectSettingsPath, setSettings, type MoveContext, type MoveHandler } from "@pi-unipi/core";
|
|
5
|
+
import { createCli, resolveBinary } from "./bin.js";
|
|
6
|
+
|
|
7
|
+
export function kanboardHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
8
|
+
return env.UNIPI_KANBOARD_HOME?.trim() || join(homedir(), ".unipi", "kanboard");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function projects(): Array<{ slug: string; root: string }> {
|
|
12
|
+
const root = join(kanboardHome(), "projects");
|
|
13
|
+
if (!existsSync(root)) return [];
|
|
14
|
+
const result: Array<{ slug: string; root: string }> = [];
|
|
15
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
16
|
+
if (!entry.isDirectory()) continue;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(join(root, entry.name, "project.json"), "utf8"));
|
|
19
|
+
if (typeof parsed.slug === "string" && typeof parsed.root === "string") result.push(parsed);
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function updateSettings(ctx: MoveContext, slug: string): void {
|
|
26
|
+
const file = projectSettingsPath(ctx.newRoot, "kanboard");
|
|
27
|
+
ctx.backup(file);
|
|
28
|
+
setSettings("kanboard", { slug, root: ctx.newRoot }, "project", ctx.newRoot);
|
|
29
|
+
ctx.log({ area: "kanboard", action: "settings-cache", from: ctx.oldRoot, to: file, result: "ok" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const kanboardMoveHandler: MoveHandler = {
|
|
33
|
+
id: "kanboard",
|
|
34
|
+
label: "Kanboard",
|
|
35
|
+
async discoverOrphans() {
|
|
36
|
+
return [...new Set(projects().filter((p) => !existsSync(p.root)).map((p) => p.root))];
|
|
37
|
+
},
|
|
38
|
+
scan(ctx) {
|
|
39
|
+
const match = projects().find((p) => p.root === ctx.oldRoot);
|
|
40
|
+
if (!match) return [];
|
|
41
|
+
const binary = resolveBinary();
|
|
42
|
+
return [{
|
|
43
|
+
area: "kanboard",
|
|
44
|
+
description: `project ${match.slug}: rebind root ${ctx.oldRoot} → ${ctx.newRoot} via unipi-kanboard project rebind; update ${projectSettingsPath(ctx.newRoot, "kanboard")}` + (!binary ? "; binary unavailable (will fail without changing project.json)" : ""),
|
|
45
|
+
async apply() {
|
|
46
|
+
if (ctx.dryRun) return;
|
|
47
|
+
if (!binary) throw new Error("kanboard binary unavailable — update kanboard binary or build the dev binary");
|
|
48
|
+
ctx.backup(join(kanboardHome(), "projects", match.slug, "project.json"));
|
|
49
|
+
const client = createCli(binary, { ...process.env, UNIPI_KANBOARD_HOME: kanboardHome(), UNIPI_KANBOARD_ACTOR: "user" });
|
|
50
|
+
try {
|
|
51
|
+
await client.run(["project", "rebind", match.slug, "--root", ctx.newRoot], { cwd: ctx.newRoot });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
const reason = /unrecognized subcommand|unknown.*rebind|unexpected argument.*rebind/i.test(message) ? `${message}; update kanboard binary` : message;
|
|
55
|
+
ctx.log({ area: "kanboard", action: "rebind", from: ctx.oldRoot, to: ctx.newRoot, result: `failed: ${reason}` });
|
|
56
|
+
throw new Error(reason);
|
|
57
|
+
}
|
|
58
|
+
ctx.log({ area: "kanboard", action: "rebind", from: ctx.oldRoot, to: ctx.newRoot, result: "ok" });
|
|
59
|
+
updateSettings(ctx, match.slug);
|
|
60
|
+
},
|
|
61
|
+
}];
|
|
62
|
+
},
|
|
63
|
+
};
|