@pi-unipi/kanboard 2.20.5 → 3.0.0-alpha.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/README.md +184 -66
- package/index.ts +206 -63
- package/package.json +18 -23
- package/skills/kanboard/SKILL.md +106 -0
- package/src/bin.ts +210 -0
- package/src/commands.ts +1371 -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
package/README.md
CHANGED
|
@@ -1,92 +1,210 @@
|
|
|
1
1
|
# @pi-unipi/kanboard
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
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
|
-
##
|
|
148
|
+
## Remote access
|
|
27
149
|
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
-
|
|
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
|
-
|
|
157
|
+
…or bind a reachable interface, which turns on the **token gate**:
|
|
34
158
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
189
|
+
## Storage
|
|
64
190
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
+
## Tests
|
|
78
207
|
|
|
79
|
-
```
|
|
80
|
-
|
|
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,214 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @pi-unipi/kanboard —
|
|
2
|
+
* @pi-unipi/kanboard — pi extension.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
+
initUnipiDirs();
|
|
162
|
+
if (attach(ctx as unknown as ExtensionContext)) {
|
|
163
|
+
const deps = buildDeps();
|
|
164
|
+
if (deps.cli) void syncPiRuntime(deps, ctx as unknown as ExtensionContext);
|
|
165
|
+
}
|
|
166
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
167
|
+
name: MODULES.KANBOARD,
|
|
168
|
+
version: VERSION,
|
|
169
|
+
commands: ["kanboard"],
|
|
170
|
+
tools: [],
|
|
69
171
|
});
|
|
70
|
-
|
|
172
|
+
|
|
173
|
+
if (!attach(ctx as unknown as ExtensionContext)) {
|
|
174
|
+
debug(`unavailable: ${unavailable}`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const client = cli!;
|
|
178
|
+
const settings = readKanboardSettings(ctx.cwd);
|
|
179
|
+
if (settings.archiveAfterDays > 0 || settings.retentionDays > 0) {
|
|
180
|
+
// Fire and forget: sweeping must never delay startup.
|
|
181
|
+
void client
|
|
182
|
+
.run([
|
|
183
|
+
"archive-sweep",
|
|
184
|
+
"--after-days",
|
|
185
|
+
String(settings.archiveAfterDays),
|
|
186
|
+
"--retention-days",
|
|
187
|
+
String(settings.retentionDays),
|
|
188
|
+
])
|
|
189
|
+
.then((payload) => debug(`archive-sweep: ${JSON.stringify(payload)}`))
|
|
190
|
+
.catch((error) => debug(`archive-sweep failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
191
|
+
}
|
|
192
|
+
await runner?.onSessionStart(ctx as unknown as ExtensionContext);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
let drainPending = false;
|
|
196
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
197
|
+
runner?.onAgentEnd(event as { messages?: unknown[] }, ctx as unknown as ExtensionContext);
|
|
198
|
+
// A -do window closes on the first real agent_end (the 150ms echo guard
|
|
199
|
+
// inside onAgentEnd skips the previous turn's late end). The drain itself
|
|
200
|
+
// runs at agent_settled: sending a task prompt while the turn is still
|
|
201
|
+
// finalizing would queue it as a follow-up that never gets delivered.
|
|
202
|
+
if (guard.onAgentEnd()) drainPending = true;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
206
|
+
if (!drainPending) return;
|
|
207
|
+
drainPending = false;
|
|
208
|
+
await drainQueueAfterDo(buildDeps(), ctx as unknown as ExtensionContext);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
212
|
+
await runner?.onSessionShutdown(ctx as unknown as ExtensionContext);
|
|
213
|
+
});
|
|
71
214
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/kanboard",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0-alpha.0",
|
|
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
|
-
"
|
|
21
|
-
"visualization",
|
|
22
|
-
"milestones",
|
|
23
|
-
"workflow"
|
|
22
|
+
"tasks"
|
|
24
23
|
],
|
|
25
24
|
"files": [
|
|
26
25
|
"index.ts",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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": "
|
|
34
|
+
"@pi-unipi/core": "3.0.0-alpha.0"
|
|
43
35
|
},
|
|
44
36
|
"peerDependencies": {
|
|
45
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
46
|
-
"
|
|
47
|
-
},
|
|
48
|
-
"devDependencies": {
|
|
49
|
-
"@types/node": "^25.6.0",
|
|
50
|
-
"typescript": "^6.0.0"
|
|
37
|
+
"@earendil-works/pi-coding-agent": "^0.86.0",
|
|
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.0",
|
|
52
|
+
"@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.0",
|
|
53
|
+
"@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.0",
|
|
54
|
+
"@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.0",
|
|
55
|
+
"@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.0"
|
|
61
56
|
}
|
|
62
57
|
}
|