@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 CHANGED
@@ -1,92 +1,210 @@
1
1
  # @pi-unipi/kanboard
2
2
 
3
- Visualization for workflow data. An HTTP server with htmx + Alpine.js UI shows your milestones, specs, plans, and tasks in a web browser. A TUI overlay gives you a kanban board without leaving Pi.
3
+ The pi half of **kanboard v3**: a per-project board for deferred work. The
4
+ storage, transition rules and web UI live in the Rust binary
5
+ ([`crates/kanboard`](../../crates/kanboard) — one writer for every change); this
6
+ package is the terminal-side bridge: commands, the task runner, hub settings and
7
+ the `kanboard` skill.
4
8
 
5
- Parses 8 document types from `.unipi/docs/` — specs, plans, milestones, quick-work, debug, fix, chore, and review — and renders them as cards with progress indicators.
9
+ Spec: [`docs/specs/2026-09-24-kanboard-v3-design.md`](../../docs/specs/2026-09-24-kanboard-v3-design.md).
6
10
 
7
11
  ## Commands
8
12
 
9
- | Command | Description |
10
- |---------|-------------|
11
- | `/unipi:kanboard` | Toggle kanboard server on/off |
12
- | `/unipi:kanboard-doctor` | Diagnose and fix parser issues |
13
+ `/unipi:kanboard [open|close|onboard|status|doctor]` — bare lists everything
14
+ (display-only, never enters the LLM context):
15
+
16
+ | Sub | What it does |
17
+ |---|---|
18
+ | `open [--host H] [--port N]` | Ensure the daemon (reuse a healthy one, else spawn `serve` detached) and print `http://127.0.0.1:<port>/p/<slug>`. The browser opens only when `openBrowser` is on. |
19
+ | `onboard` | `project add` for this workspace and remembers the slug. Idempotent. |
20
+ | `close` | Shut down the board daemon — the web UI goes offline until the next `open`. Running tasks are unaffected. |
21
+ | `status` | Daemon pid/port, project counts, active claims (session/pid/host/staleness), this session's queue, and the runner's current task. |
22
+ | `doctor` | ✓/✗ setup check (binary, daemon health, project, summary agent, bind, claims) as a display-only message. |
23
+
24
+ The other three commands are separate slash commands:
25
+
26
+ | Command | What it does |
27
+ |---|---|
28
+ | `/unipi:kanboard-add [-p 1-5] [--after ID] [--status backlog\|todo] <title>` | Capture a task — no agent turn. Lines below the title are the body; existing file paths pasted there are attached. `-p` maps 1 none · 2 low · 3 medium · 4 high · 5 urgent. |
29
+ | `/unipi:kanboard-do <request>` | Reveal the skill and hand the request to the agent with board writes enabled for that turn. The agent may add/move/queue/note; it cannot run tasks — queued ids are drained by the runner when the turn ends. |
30
+ | `/unipi:kanboard-autowork start\|stop` | `start` runs ready tasks one by one (queue order first, then `claim-next`); `stop` finishes the current task, then stops. |
31
+
32
+ The old `add`/`work`/`stop` subcommands and bare-text capture now just point at
33
+ these commands.
34
+
35
+ ## Runner (`/unipi:kanboard-autowork start`)
36
+
37
+ One job per session. The **runner owns the lifecycle transitions** the agent is
38
+ not allowed to write:
39
+
40
+ 1. `claim-next --session <sid> --pid <ppid> --host <host> --gate <chainGate>` —
41
+ nothing ready → `Nothing ready (N waiting on deps, M blocked)`.
42
+ 2. **jev picks the mode** (one `choice` call over title + body ≤2000 chars):
43
+ `direct` (small clear change) · `plan` (needs an approved plan) · `goal`
44
+ (multi-turn objective with verification). jev null → `direct`; the choice is
45
+ logged to `~/.unipi/logs/kanboard.log` with `UNIPI_DEBUG_KANBOARD=1`.
46
+ 3. `set-run --mode`, then the task goes to the agent as a user message: title,
47
+ body, last 10 activity entries, each dependency with its status and last note,
48
+ and the rules (block with a comment to ask a question; never write
49
+ `in_review`/`done`/`cancelled`; work only on this task).
50
+ - **plan** → plan mode is entered through workflow's `unipi:plan-enter` runner
51
+ first; approval stays interactive; a discarded plan releases the task to Todo
52
+ with `plan discarded`.
53
+ - **goal** → long-horizon's `unipi:goal-start` runner starts a goal with the
54
+ task as the objective; the goal id is recorded with `set-run --goal` and
55
+ completion is read back with `unipi:goal-status`.
56
+ 4. Run end (a `/plan` settle after the last `agent_end`, once the agent reports
57
+ idle with no queued messages): the task is re-read — if the agent blocked it,
58
+ that is respected and reported (`▣ UNI-12 blocked: <comment>`) and the loop
59
+ continues; otherwise `release --to in_review --comment <last assistant text
60
+ ≤500 chars>`. `Esc` (aborted turn) → `release --to todo --comment "interrupted
61
+ by user"` and the loop stops. Session shutdown → `release --to todo` with
62
+ `session ended`.
63
+ 5. After each task: `✓ UNI-12 → In Review: <first line>`, then the loop takes
64
+ the next id from the session **queue** (`claim-next --id`; entries that went
65
+ stale are dropped with a notice), falls back to `claim-next` while autowork
66
+ is on, and stops when nothing is ready. The event loop is never blocked.
67
+
68
+ Footer: `▣ UNI-12 · direct` while a task runs. The claimed task is persisted with
69
+ `pi.appendEntry("unipi:kanboard-runner", …)`, so `/reload` or a resume offers to
70
+ resume it or releases it to Todo.
71
+
72
+ ## Settings (hub section "Kanboard")
73
+
74
+ | Setting | Default | Notes |
75
+ |---|---|---|
76
+ | `chainGate` | `in_review` | `done` waits for a finished dependency |
77
+ | `idleMin` | `10` | Passed to `serve --idle-min` |
78
+ | `port` | `0` | Passed to `serve --port` (0 = OS-assigned) |
79
+ | `archiveAfterDays` | `0` | > 0 → `archive-sweep --after-days N` on session start (fire and forget) |
80
+ | `openBrowser` | `false` | Open the board in a browser on `open` |
81
+ | `requireAuth` | `false` | Also require the access token on 127.0.0.1 (remote always does) |
82
+ | `keepToken` | `false` | Reuse `<home>/token` across daemon restarts |
83
+ | `queueMax` | `10` | Tasks a session may queue (0 = unlimited); passed to the CLI as `UNIPI_KANBOARD_QUEUE_MAX` |
84
+ | `maxSessions` | `2` | Distinct sessions running tasks per project (`UNIPI_KANBOARD_MAX_SESSIONS`) |
85
+ | `turnAddLimit` | `20` | `add` calls allowed per -do turn or runner task (0 = unlimited) |
86
+ | *actions* | | `Open board…`, `Stop daemon`, `Summary agent command…`, `Rotate access token` |
87
+
88
+ `Summary agent command…` writes through `settings set agent-command` and
89
+ `Rotate access token` runs `rotate-token` — both are **user-only** (actor=agent
90
+ is refused), as are `settings set` calls generally. `settings show` is read-only.
91
+
92
+ ## Binary resolution
93
+
94
+ 1. `UNIPI_KANBOARD_BIN` (explicit path)
95
+ 2. `@pi-unipi/kanboard-<platform>-<arch>/bin/unipi-kanboard[.exe]` (K4 ships these)
96
+ 3. the dev build `<repo>/crates/kanboard/target/{release,debug}/unipi-kanboard`
97
+
98
+ Nothing found → every command reports
99
+ `kanboard binary unavailable for <platform>-<arch>` and does nothing else.
100
+
101
+ **Agent bash env:** pi has no extension-level mechanism to add env vars to the
102
+ `bash` tool (only replacing bash via `registerTool` + `BashToolOptions`, which
103
+ would change tool schemas mid-session and break the prefix cache — spec principle
104
+ 4 forbids that). So the task prompt and the skill pass `--actor agent --project
105
+ <slug>` explicitly and call the binary by absolute path.
106
+
107
+ ## Skill
108
+
109
+ `skills/kanboard/SKILL.md` describes the CLI, the lanes, who may move what, and
110
+ the rules agents must follow. It is a normal pi skill (jev skill-judging can
111
+ reveal it on intent), and `/unipi:kanboard-do` force-reveals it by emitting
112
+ `unipi:skills:reveal`, which utility turns into the usual append-only reveal
113
+ message — the system prompt is never touched.
114
+
115
+ ## Platforms and packaging
116
+
117
+ | Platform | npm package | Rust target | Notes |
118
+ |---|---|---|---|
119
+ | Linux x64 | `@pi-unipi/kanboard-linux-x64` | `x86_64-unknown-linux-musl` | static-pie, 3.9 MB |
120
+ | Linux arm64 | `@pi-unipi/kanboard-linux-arm64` | `aarch64-unknown-linux-musl` | static |
121
+ | macOS arm64 | `@pi-unipi/kanboard-darwin-arm64` | `aarch64-apple-darwin` | |
122
+ | macOS x64 | `@pi-unipi/kanboard-darwin-x64` | `x86_64-apple-darwin` | cross-built from macos-14 |
123
+ | Windows x64 | `@pi-unipi/kanboard-win32-x64` | `x86_64-pc-windows-msvc` | |
124
+
125
+ They are **optional dependencies** of this package (`os`/`cpu` gated), so `npm install`
126
+ pulls exactly one. `.github/workflows/kanboard-binaries.yml` builds them (tests +
127
+ clippy on native targets, release build per target, artifact per platform) and, on
128
+ a `v*` tag, attaches the binaries to the GitHub release. Publishing to npm:
129
+
130
+ ```bash
131
+ npm run publish:kanboard -- --dry-run # what would ship
132
+ npm run publish:kanboard # publishes, or skips loudly
133
+ ```
13
134
 
14
- ## Web Pages
135
+ The script **skips any platform package whose `bin/` is empty** (and exits 2), so
136
+ an empty platform package can never be published. CI publishes only when an
137
+ `NPM_TOKEN` secret exists — this repository has none, so the release job attaches
138
+ artifacts and says so.
15
139
 
16
- ### Milestones (`/`)
17
- - Phases with progress bars
18
- - Checklist items with status indicators (done/todo)
19
- - Collapsible sections per phase
140
+ Local packaging proof (no registry, no network):
20
141
 
21
- ### Workflow (`/workflow`)
22
- - Cards grouped by document type
23
- - Progress indicators per card
24
- - Filtering by status (All, To Do, In Progress, Done)
142
+ ```bash
143
+ node scripts/test-kanboard-packaging.mjs
144
+ # packs packages/kanboard + the linux-x64 platform package, installs both into a
145
+ # temp node_modules, resolves the binary through src/bin.ts and runs --version
146
+ ```
25
147
 
26
- ## TUI Overlay
148
+ ## Remote access
27
149
 
28
- Two tabs accessible via the kanboard overlay:
150
+ The daemon binds `127.0.0.1` by default — local only, no token. For access from
151
+ another machine either **tunnel** it (nothing to configure):
29
152
 
30
- - **Tasks** — Flat list of all tasks from all documents with status icons
31
- - **Board** — Kanban columns (To Do / In Progress / Done)
153
+ ```bash
154
+ ssh -N -L 37473:127.0.0.1:37473 <hostname> # then open http://127.0.0.1:37473
155
+ ```
32
156
 
33
- ### Controls
157
+ …or bind a reachable interface, which turns on the **token gate**:
34
158
 
35
- | Key | Action |
36
- |-----|--------|
37
- | `j/k` | Navigate up/down |
38
- | `h/l` | Switch columns (Board tab) |
39
- | `Tab` or `b` | Switch between Tasks/Board tabs |
40
- | `t` | Switch to Tasks tab |
41
- | `gg/G` | Jump to top/bottom |
42
- | `q/Esc` | Close overlay |
159
+ ```bash
160
+ /unipi:kanboard open --host 0.0.0.0 --port 37473 # every interface
161
+ /unipi:kanboard open --host tailscale # the tailnet IPv4
162
+ ```
43
163
 
44
- ## Special Triggers
164
+ `--host` and `--port` override the `host`/`port` settings for that invocation
165
+ only. `tailscale` resolves through `tailscale ip -4` (clear error when tailscale
166
+ is not installed). For a wildcard bind the printed URLs cover the machine
167
+ hostname, every non-internal IPv4 and the tailnet address, and a warning says
168
+ `board is reachable from the network; anyone with the link can edit it`.
45
169
 
46
- Kanboard registers with the info-screen dashboard, showing document count, tasks done, total tasks, and completion percentage. The footer subscribes to kanboard registry data to display task stats in the status bar.
170
+ **Token model.** Any non-loopback bind generates a 32-byte token (base64url) and
171
+ writes it to `daemon.json` alongside `host`. Every request must carry it:
47
172
 
48
- ## Parser System
173
+ - `?t=<token>` — sets an `HttpOnly; SameSite=Strict` cookie and 303-redirects to
174
+ the same URL without the parameter (so the token leaves the address bar),
175
+ - the `kb_token` cookie, or
176
+ - `Authorization: Bearer <token>`.
49
177
 
50
- Kanboard parses 8 document types:
178
+ Missing or wrong tokens answer `401` with a page saying to open the link printed
179
+ by `/unipi:kanboard open`. `/api/health` stays reachable but returns only
180
+ `{ok, version}` (no pid) off-loopback, and POSTs are refused when their `Origin`
181
+ does not match the request `Host` (drive-by CSRF), in both modes. Loopback binds
182
+ keep no token at all.
51
183
 
52
- | Type | Directory | What's Parsed |
53
- |------|-----------|---------------|
54
- | Spec | `specs/` | `- [ ]` / `- [x]` checklist items |
55
- | Plan | `plans/` | `unstarted:` / `in-progress:` / `completed:` statuses |
56
- | Milestone | `MILESTONES.md` | Phase headers + checklist items |
57
- | Quick-work | `quick-work/` | Title + checklist items |
58
- | Debug | `debug/` | Headers + checklists |
59
- | Fix | `fix/` | Headers + checklists + related debug ref |
60
- | Chore | `chore/` | Chore steps as checklist items |
61
- | Review | `reviews/` | Review remarks as checklist items |
184
+ Changing the binding needs a restart: if a daemon already runs with a different
185
+ host/port, `serve` reports `bindingChanged` and `/unipi:kanboard open` stops the
186
+ old one and starts the new one (`restarted kanboard on 0.0.0.0:37473`). The
187
+ daemon runs no jobs, so nothing is lost.
62
188
 
63
- Parsers are resilient — they collect warnings per file and return partial results. Warnings are surfaced in the kanboard-doctor skill.
189
+ ## Storage
64
190
 
65
- ## API Endpoints
191
+ `~/.unipi/kanboard/` (`UNIPI_KANBOARD_HOME` overrides it): `daemon.json` +
192
+ `daemon.lock` for the daemon, and `projects/<slug>/{project.json,board.lock,tasks/*.md,queues/<session>.json}`.
193
+ The extension never edits those files — the binary owns them.
66
194
 
67
- | Method | Path | Description |
68
- |--------|------|-------------|
69
- | GET | `/` | Milestone page |
70
- | GET | `/workflow` | Workflow page |
71
- | GET | `/api/milestones` | Milestone JSON data |
72
- | GET | `/api/workflow` | Workflow JSON data |
73
- | POST | `/api/docs/:type/:file/items/:line` | Update item status |
195
+ ## Troubleshooting
74
196
 
75
- ## Configurables
197
+ | Symptom | Fix |
198
+ |---|---|
199
+ | `kanboard binary unavailable for <platform>-<arch>` | No `UNIPI_KANBOARD_BIN`, no platform package and no dev build. Build `crates/kanboard` (`cargo build --release`) or set `UNIPI_KANBOARD_BIN`. |
200
+ | The board says *"N task file(s) need repair"* | A file was edited by hand. One bad file no longer blocks the board (it is skipped and reported); run `unipi-kanboard validate --fix`, then `validate`. |
201
+ | `UNI-5 is unreadable: … (line N)` | That task's own file is broken — repair it before moving/noting it. |
202
+ | The daemon looks stale | `unipi-kanboard status` (pid + liveness), then `/unipi:kanboard close`, `unipi-kanboard stop` (SIGTERM, ≤3s) or the hub's **Stop daemon** action. |
203
+ | Nothing is ready | `unipi-kanboard list --ready --json` shows `waitingFor`; a cancelled dependency blocks forever — `link`/`unlink` to re-plan. |
204
+ | The runner prompts for permission on every board call | Fixed in auto mode: `unipi-kanboard … --actor agent` is allow-listed by the permission gate (ask mode still asks). |
76
205
 
77
- Default port configuration from `@pi-unipi/core`:
206
+ ## Tests
78
207
 
79
- ```typescript
80
- KANBOARD_DEFAULTS = {
81
- PORT: 8165, // Starting port
82
- MAX_PORT: 8175, // Maximum port to try
83
- }
208
+ ```bash
209
+ npm test -w packages/kanboard # bin resolution, commands, runner, settings
84
210
  ```
85
-
86
- - Port allocation: tries 8165, increments on EADDRINUSE
87
- - PID file: `.unipi/kanboard.pid`
88
- - Graceful shutdown on SIGINT/SIGTERM
89
-
90
- ## License
91
-
92
- MIT
package/index.ts CHANGED
@@ -1,71 +1,217 @@
1
1
  /**
2
- * @pi-unipi/kanboard — Extension entry
2
+ * @pi-unipi/kanboard — pi extension.
3
3
  *
4
- * Visualization layer for unipi workflow data.
5
- * HTTP server with htmx + Alpine.js UI, modular parsers, TUI overlay, and kanban board.
4
+ * Bridges the terminal to the board: `/unipi:kanboard` (open/close/onboard/
5
+ * status/doctor — bare lists the commands), `/unipi:kanboard-add`,
6
+ * `/unipi:kanboard-do` (opens the board-write window for one turn),
7
+ * `/unipi:kanboard-autowork` (the runner loop: queue first, then claim-next).
8
+ * The runner owns claim → In Progress and run-end → In Review. The board itself
9
+ * is written by the Rust binary (`crates/kanboard`); this extension never edits
10
+ * task files. Bash calls into the binary are gated by the write window
11
+ * (src/guard.ts): reads always pass, writes need a -do turn or a running task.
6
12
  */
7
13
 
8
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
- import { MODULES, KANBOARD_COMMANDS } from "@pi-unipi/core";
10
- import { registerCommands } from "./commands.js";
11
-
12
- /** Package version */
13
- const VERSION = "0.1.0";
14
-
15
- export default function (pi: ExtensionAPI): void {
16
-
17
- // Register commands
18
- registerCommands(pi);
19
-
20
- // Note: Badge generation on first message is handled by the utility module.
21
- // Kanboard no longer manages badge generation to avoid duplication.
22
-
23
- // Register info-screen group
24
- const registry = globalThis.__unipi_info_registry;
25
- if (registry) {
26
- registry.registerGroup({
27
- id: "kanboard",
28
- name: "Kanboard",
29
- icon: "📋",
30
- priority: 50,
31
- config: {
32
- showByDefault: true,
33
- stats: [
34
- { id: "status", label: "Server Status", show: true },
35
- { id: "url", label: "URL", show: true },
36
- { id: "docs", label: "Documents", show: true },
37
- { id: "tasks", label: "Tasks", show: true },
38
- ],
14
+ import { dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
+ import {
18
+ MODULES,
19
+ UNIPI_EVENTS,
20
+ emitEvent,
21
+ getPackageVersion,
22
+ initUnipiDirs,
23
+ registerCommandRunner,
24
+ getSettings,
25
+ } from "@pi-unipi/core";
26
+
27
+ import { openCli, type KanboardCli } from "./src/bin.js";
28
+ import { createWriteGuard } from "./src/guard.js";
29
+ import {
30
+ drainQueueAfterDo,
31
+ registerKanboardCommands,
32
+ runOpen,
33
+ syncPiRuntime,
34
+ runRotateTokenAction,
35
+ runStopDaemon,
36
+ type CommandDeps,
37
+ } from "./src/commands.js";
38
+ import { createDebugLog, createRunner, registerPlanEventListener, type Runner } from "./src/runner.js";
39
+ import {
40
+ ACTION_OPEN,
41
+ ACTION_STOP_DAEMON,
42
+ ACTION_ROTATE_TOKEN,
43
+ readKanboardSettings,
44
+ registerKanboardSettings,
45
+ applyLimitEnv,
46
+ } from "./src/settings.js";
47
+
48
+ const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
49
+
50
+ /** Utility owns the frozen judged set, so kanboard asks it to reveal the skill. */
51
+ export const SKILL_REVEAL_EVENT = "unipi:skills:reveal";
52
+ export const KANBOARD_SKILL = "kanboard";
53
+
54
+ export default function (pi: ExtensionAPI) {
55
+ // One session id shared by the runner and the agent's bash calls.
56
+ process.env.UNIPI_KANBOARD_SESSION ??= `pi-${process.pid}`;
57
+ // Limits travel through the environment; refresh on load and before every
58
+ // tool_call (see the guard registration in commands.ts).
59
+ applyLimitEnv(readKanboardSettings());
60
+ const debug = createDebugLog();
61
+ registerKanboardSettings();
62
+
63
+ let cli: KanboardCli | null = null;
64
+ let unavailable: string | null = null;
65
+ let runner: Runner | null = null;
66
+ const guard = createWriteGuard(
67
+ () => {
68
+ const status = runner?.status();
69
+ return status?.phase === "running" ? status.taskId : null;
70
+ },
71
+ () => readKanboardSettings().turnAddLimit,
72
+ );
73
+ const sessionId = (): string => process.env.UNIPI_KANBOARD_SESSION ?? `pi-${process.pid}`;
74
+
75
+ const projectSlug = (): string => {
76
+ const fromEnv = process.env.UNIPI_KANBOARD_PROJECT?.trim();
77
+ if (fromEnv) return fromEnv;
78
+ try {
79
+ const settings = getSettings("kanboard", process.cwd()) as { slug?: string };
80
+ return typeof settings.slug === "string" ? settings.slug : "";
81
+ } catch {
82
+ return "";
83
+ }
84
+ };
85
+
86
+ // Live getters: the command is registered before the binary is resolved, so
87
+ // the deps object must read the current state at call time.
88
+ const buildDeps = (): CommandDeps => ({
89
+ get cli() {
90
+ return cli;
39
91
  },
40
- dataProvider: async () => {
41
- const { createDefaultRegistry } = await import("./parser/index.js");
42
- const registry = await createDefaultRegistry();
43
- const docs = registry.parseAll(".unipi/docs");
44
- const totalItems = docs.reduce((sum, d) => sum + d.items.length, 0);
45
- const doneItems = docs.reduce(
46
- (sum, d) => sum + d.items.filter((i) => i.status === "done").length,
47
- 0,
48
- );
49
-
50
- return {
51
- status: {
52
- value: "Ready",
53
- detail: "Server not running (use /unipi:kanboard to start)",
54
- },
55
- url: {
56
- value: "—",
57
- detail: "Start server to get URL",
58
- },
59
- docs: {
60
- value: String(docs.length),
61
- detail: `${docs.length} documents parsed`,
62
- },
63
- tasks: {
64
- value: `${doneItems}/${totalItems}`,
65
- detail: `${totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0}% complete`,
66
- },
67
- };
92
+ get unavailable() {
93
+ return unavailable;
68
94
  },
95
+ settings: () => readKanboardSettings(process.cwd()),
96
+ revealSkill,
97
+ work: (ctx) => runner?.work(ctx) ?? Promise.resolve(),
98
+ stop: (ctx) => runner?.stop(ctx),
99
+ drainQueue: (ctx) => runner?.drain(ctx) ?? Promise.resolve(),
100
+ status: () => runner?.status() ?? { taskId: null, mode: null, phase: "idle" },
101
+ guard,
102
+ session: sessionId,
103
+ debug,
104
+ }) as CommandDeps;
105
+
106
+ const revealSkill = (ctx: ExtensionContext | { cwd?: string }): void => {
107
+ // Append-only reveal (never the system prompt), so the prefix cache holds.
108
+ emitEvent(pi, SKILL_REVEAL_EVENT, { names: [KANBOARD_SKILL], ctx });
109
+ debug(`reveal requested for ${KANBOARD_SKILL}`);
110
+ };
111
+
112
+ const attach = (ctx: ExtensionContext): boolean => {
113
+ if (cli) return true;
114
+ const opened = openCli();
115
+ if ("error" in opened) {
116
+ unavailable = opened.error;
117
+ return false;
118
+ }
119
+ cli = opened;
120
+ unavailable = null;
121
+ debug(`binary: ${opened.binary.path} (${opened.binary.source})`);
122
+ if (!runner) {
123
+ runner = createRunner({
124
+ pi,
125
+ cli,
126
+ project: projectSlug,
127
+ cwd: ctx.cwd,
128
+ settings: () => readKanboardSettings(ctx.cwd),
129
+ debug,
130
+ });
131
+ registerPlanEventListener(pi, runner);
132
+ }
133
+ return true;
134
+ };
135
+
136
+ registerKanboardCommands(pi, buildDeps());
137
+
138
+ registerCommandRunner(ACTION_OPEN, async (ctx) => {
139
+ const context = ctx as ExtensionContext | undefined;
140
+ if (!context?.ui) return;
141
+ if (!attach(context)) {
142
+ context.ui.notify(`kanboard: ${unavailable}`, "warning");
143
+ return;
144
+ }
145
+ await runOpen(buildDeps(), context);
146
+ });
147
+
148
+ registerCommandRunner(ACTION_STOP_DAEMON, async (ctx) => {
149
+ const context = ctx as ExtensionContext | undefined;
150
+ if (!context?.ui) return;
151
+ await runStopDaemon(buildDeps(), context);
152
+ });
153
+
154
+ registerCommandRunner(ACTION_ROTATE_TOKEN, async (ctx) => {
155
+ const context = ctx as ExtensionContext | undefined;
156
+ if (!context?.ui) return;
157
+ await runRotateTokenAction(buildDeps(), context);
158
+ });
159
+
160
+ pi.on("session_start", async (_event, ctx) => {
161
+ // Kanboard's own ambient children (summaries, --list-models) get this env:
162
+ // they must not re-sync the runtime or spawn a daemon.
163
+ if (process.env.UNIPI_KANBOARD_CHILD) return;
164
+ initUnipiDirs();
165
+ if (attach(ctx as unknown as ExtensionContext)) {
166
+ const deps = buildDeps();
167
+ if (deps.cli) void syncPiRuntime(deps, ctx as unknown as ExtensionContext);
168
+ }
169
+ emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
170
+ name: MODULES.KANBOARD,
171
+ version: VERSION,
172
+ commands: ["kanboard"],
173
+ tools: [],
69
174
  });
70
- }
175
+
176
+ if (!attach(ctx as unknown as ExtensionContext)) {
177
+ debug(`unavailable: ${unavailable}`);
178
+ return;
179
+ }
180
+ const client = cli!;
181
+ const settings = readKanboardSettings(ctx.cwd);
182
+ if (settings.archiveAfterDays > 0 || settings.retentionDays > 0) {
183
+ // Fire and forget: sweeping must never delay startup.
184
+ void client
185
+ .run([
186
+ "archive-sweep",
187
+ "--after-days",
188
+ String(settings.archiveAfterDays),
189
+ "--retention-days",
190
+ String(settings.retentionDays),
191
+ ])
192
+ .then((payload) => debug(`archive-sweep: ${JSON.stringify(payload)}`))
193
+ .catch((error) => debug(`archive-sweep failed: ${error instanceof Error ? error.message : String(error)}`));
194
+ }
195
+ await runner?.onSessionStart(ctx as unknown as ExtensionContext);
196
+ });
197
+
198
+ let drainPending = false;
199
+ pi.on("agent_end", async (event, ctx) => {
200
+ runner?.onAgentEnd(event as { messages?: unknown[] }, ctx as unknown as ExtensionContext);
201
+ // A -do window closes on the first real agent_end (the 150ms echo guard
202
+ // inside onAgentEnd skips the previous turn's late end). The drain itself
203
+ // runs at agent_settled: sending a task prompt while the turn is still
204
+ // finalizing would queue it as a follow-up that never gets delivered.
205
+ if (guard.onAgentEnd()) drainPending = true;
206
+ });
207
+
208
+ pi.on("agent_settled", async (_event, ctx) => {
209
+ if (!drainPending) return;
210
+ drainPending = false;
211
+ await drainQueueAfterDo(buildDeps(), ctx as unknown as ExtensionContext);
212
+ });
213
+
214
+ pi.on("session_shutdown", async (_event, ctx) => {
215
+ await runner?.onSessionShutdown(ctx as unknown as ExtensionContext);
216
+ });
71
217
  }
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "2.20.5",
4
- "description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
3
+ "version": "3.0.0-alpha.1",
4
+ "description": "Kanboard v3 \u2014 deferred-work board: CLI/daemon bridge, task runner and skill",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "license": "MIT",
8
8
  "author": "Neuron Mr White",
9
+ "scripts": {
10
+ "test": "npx tsx --test tests/**/*.test.ts"
11
+ },
9
12
  "repository": {
10
13
  "type": "git",
11
14
  "url": "git+https://github.com/Neuron-Mr-White/unipi.git",
@@ -14,40 +17,25 @@
14
17
  "keywords": [
15
18
  "pi-package",
16
19
  "pi-extension",
17
- "pi-coding-agent",
18
20
  "unipi",
19
21
  "kanboard",
20
- "kanban",
21
- "visualization",
22
- "milestones",
23
- "workflow"
22
+ "tasks"
24
23
  ],
25
24
  "files": [
26
25
  "index.ts",
27
- "commands.ts",
28
- "types.ts",
29
- "parser/**/*.ts",
30
- "server/**/*.ts",
31
- "tui/**/*.ts",
32
- "ui/**/*.ts",
33
- "ui/**/*.css",
34
- "ui/**/*.js",
35
- "skills/**/*",
26
+ "src/**/*.ts",
27
+ "skills/**/*.md",
36
28
  "README.md"
37
29
  ],
38
30
  "publishConfig": {
39
31
  "access": "public"
40
32
  },
41
33
  "dependencies": {
42
- "@pi-unipi/core": "2.20.5"
34
+ "@pi-unipi/core": "3.0.0-alpha.1"
43
35
  },
44
36
  "peerDependencies": {
45
- "@earendil-works/pi-coding-agent": "^0.84.0",
46
- "@earendil-works/pi-tui": "^0.84.0"
47
- },
48
- "devDependencies": {
49
- "@types/node": "^25.6.0",
50
- "typescript": "^6.0.0"
37
+ "@earendil-works/pi-coding-agent": "^0.87.1",
38
+ "typebox": "^1.1.38"
51
39
  },
52
40
  "pi": {
53
41
  "extensions": [
@@ -58,5 +46,12 @@
58
46
  ],
59
47
  "prompts": [],
60
48
  "themes": []
49
+ },
50
+ "optionalDependencies": {
51
+ "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.1",
52
+ "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.1",
53
+ "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.1",
54
+ "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.1",
55
+ "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.1"
61
56
  }
62
57
  }