@netnodeag/kraftwerk 0.81.0 → 0.82.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.
Files changed (69) hide show
  1. package/README.md +52 -0
  2. package/dist/cli/boards.d.ts +2 -0
  3. package/dist/cli/boards.js +486 -0
  4. package/dist/cli/init.js +1 -0
  5. package/dist/cli/kraftwerk.js +3 -0
  6. package/dist/client/routes.d.ts +14 -0
  7. package/dist/client/routes.js +14 -0
  8. package/dist/config.d.ts +19 -0
  9. package/dist/config.js +24 -1
  10. package/dist/core/attention.d.ts +2 -2
  11. package/dist/core/attention.js +22 -2
  12. package/dist/core/board-wake.d.ts +21 -0
  13. package/dist/core/board-wake.js +221 -0
  14. package/dist/core/boards.d.ts +256 -0
  15. package/dist/core/boards.js +936 -0
  16. package/dist/core/chat/acp.d.ts +1 -1
  17. package/dist/core/chat/acp.js +2 -1
  18. package/dist/core/chat/backend.d.ts +11 -0
  19. package/dist/core/chat/backend.js +9 -1
  20. package/dist/core/chat/context.js +16 -4
  21. package/dist/core/chat/pi.d.ts +1 -1
  22. package/dist/core/chat/pi.js +2 -1
  23. package/dist/core/chat/seats.js +3 -2
  24. package/dist/core/chat/sessions.d.ts +2 -0
  25. package/dist/core/chat/sessions.js +1 -0
  26. package/dist/core/chat/state.js +1 -1
  27. package/dist/core/chat/types.d.ts +11 -1
  28. package/dist/core/files.d.ts +1 -1
  29. package/dist/core/files.js +19 -1
  30. package/dist/core/git.js +3 -1
  31. package/dist/core/notifications.d.ts +1 -1
  32. package/dist/core/refs.d.ts +1 -1
  33. package/dist/core/refs.js +25 -1
  34. package/dist/core/search.d.ts +12 -0
  35. package/dist/core/search.js +19 -0
  36. package/dist/core/settings.d.ts +2 -0
  37. package/dist/core/settings.js +13 -1
  38. package/dist/core/trash.d.ts +1 -1
  39. package/dist/server/api/boards.d.ts +105 -0
  40. package/dist/server/api/boards.js +160 -0
  41. package/dist/server/api/chats.d.ts +2 -0
  42. package/dist/server/api/chats.js +9 -3
  43. package/dist/server/api/index.d.ts +105 -2
  44. package/dist/server/api/index.js +2 -0
  45. package/dist/server/api/workspace.d.ts +5 -0
  46. package/dist/server/api/workspace.js +2 -0
  47. package/dist/server/push.js +1 -1
  48. package/dist/server/server.js +3 -0
  49. package/package.json +1 -1
  50. package/schema/kraftwerk.schema.json +39 -0
  51. package/web/dist/assets/{dist-Dx3aajX1.js → dist-BBFUIftm.js} +1 -1
  52. package/web/dist/assets/{dist-ENZYo2jZ.js → dist-Bie1ebP9.js} +1 -1
  53. package/web/dist/assets/{dist-BiU4F98O.js → dist-BuwdjX6z.js} +1 -1
  54. package/web/dist/assets/{dist-8IOFzZx-.js → dist-C0ve4Tka.js} +1 -1
  55. package/web/dist/assets/{dist-FfKYtQxT.js → dist-C16bXf8c.js} +1 -1
  56. package/web/dist/assets/{dist-cewmjxFi.js → dist-CDUZdAAS.js} +1 -1
  57. package/web/dist/assets/{dist-BK0VhAPQ.js → dist-CH73jsgO.js} +1 -1
  58. package/web/dist/assets/{dist-Dy1SvhKC.js → dist-C_rJjRj7.js} +1 -1
  59. package/web/dist/assets/dist-DYICNp09.js +1 -0
  60. package/web/dist/assets/{dist-AhnSw4hy.js → dist-DtUB2Uu2.js} +1 -1
  61. package/web/dist/assets/{dist-Ba6WSVOB.js → dist-DvkrCSGL.js} +1 -1
  62. package/web/dist/assets/{editor-CaRa1NMC.js → editor-3z92i0tV.js} +3 -3
  63. package/web/dist/assets/{editor-CZMrhTTL.css → editor-C0_Snnzk.css} +1 -1
  64. package/web/dist/assets/index-DRhNELK_.js +232 -0
  65. package/web/dist/assets/index-unGEux3Y.css +2 -0
  66. package/web/dist/index.html +2 -2
  67. package/web/dist/assets/dist-lgUK2Fjt.js +0 -1
  68. package/web/dist/assets/index-Dpzux3OC.js +0 -231
  69. package/web/dist/assets/index-Dvt-vE1h.css +0 -2
package/README.md CHANGED
@@ -259,6 +259,7 @@ kraftwerk workspaces # every workspace on this machine; works
259
259
  kraftwerk devices # paired devices (a phone, another computer); devices pair|revoke
260
260
  kraftwerk remote on|off|pair # reach this machine from anywhere through the relay, end to end encrypted
261
261
  kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
262
+ kraftwerk boards|tasks # task boards per project or workspace; tasks create|show|move|assign|log|pick|remove
262
263
  kraftwerk contacts|places|events # who, where and when, one Markdown file each; … show|create|set|remove, events --from/--to
263
264
  kraftwerk ref <kind:slug> # resolve a reference (contact:anna-meier, project:relaunch, …); --file <md> resolves every one in a file
264
265
  kraftwerk trash # what was deleted; trash restore|purge <id>, trash empty
@@ -634,6 +635,57 @@ kraftwerk projects remove relaunch-netnode-ch
634
635
  The registry of workspaces on this machine, which answered to
635
636
  `kraftwerk projects` until 0.48, is `kraftwerk workspaces` now.
636
637
 
638
+ ## Boards and tasks
639
+
640
+ Turn on `boards:` in `kraftwerk.yml` (or the checkbox in settings) and every
641
+ project can have task boards, and the workspace its own:
642
+
643
+ ```yaml
644
+ boards:
645
+ root: kraftwerk-data/boards # default; the workspace's boards (a project's live in projects/<slug>/boards/)
646
+ columns: [draft, todo, progress, review, done] # default for boards that name none
647
+ ```
648
+
649
+ ```
650
+ projects/<slug>/boards/<board>/board.md # title, columns, default agent, stale_after + the board's rules
651
+ projects/<slug>/boards/<board>/tasks/<n>-<slug>/ # one task: task.md, log.md, its files
652
+ kraftwerk-data/boards/<board>/… # workspace boards, same shape
653
+ ```
654
+
655
+ A column is a slug or `{slug, label, limit, done, review}`: `review: true`
656
+ means tasks there wait for a human (they count as attention), `done: true`
657
+ means finished. The column is a field of `task.md`, so a move is a one-line
658
+ diff and the task's folder never moves. Task folders are numbered per board
659
+ (`50-write-a-press-release`); the number names the task in the CLI, the
660
+ folder name is its reference (`task:50-write-a-press-release`). The log is in
661
+ the journal format: `## 2026-11-05` days, `* 16:40 Moved Todo → In progress:
662
+ starting (lisa/claude)` lines, newest first; kraftwerk writes it on every
663
+ create, move, assign, pick and file change, and agents and humans add lines.
664
+
665
+ ```
666
+ kraftwerk boards [--project <p>] [--all] # the boards; boards show|create|set|remove
667
+ kraftwerk tasks [--project <p>] [--board <b>] # the tasks of a board (default: the project's `tasks` board)
668
+ kraftwerk tasks create "<title>" --status todo --assign lisa --file $KRAFTWERK_TEMP/task.md -m "…"
669
+ kraftwerk tasks show 50 | move 50 review -m "…" | assign 50 max -m "…" | assign 50 human | log 50 "…" | pick --as lisa | remove 50
670
+ ```
671
+
672
+ **Agents and the board.** A project chat and every agent see the boards
673
+ in their context: columns with counts, the open tasks, the rules written
674
+ in `board.md`, and the commands. Inside a session the CLI knows who it is
675
+ (`KRAFTWERK_AGENT`, `KRAFTWERK_ACTOR`), so `tasks pick` and `tasks log` need
676
+ no name. Waking is event-based: whenever a task has an agent as assignee
677
+ and sits in a work column — created there, moved there, assigned, handed
678
+ over — that agent gets a session with the task as its scope (the task
679
+ block: `task.md`, the files, the whole log, so a hand-over reads like a
680
+ message from the previous agent), or the message is posted into its open
681
+ session for that task. Draft, Review and Done never wake anyone; an agent
682
+ acting on its own task does not wake itself; a board's `agent:` is the
683
+ default assignee for tasks entering Todo unassigned. The events are lines in
684
+ `<output>/boards/events.jsonl`, local to the machine, so a clone of the
685
+ workspace never starts the same work twice. Only a human moves a task into a
686
+ done column; `tasks assign <n> human` clears the assignee and puts the task
687
+ into Review.
688
+
637
689
  ## Contacts, places and events
638
690
 
639
691
  Who, where and when, kept by the workspace itself so people and agents can
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerBoardCommands(program: Command): void;
@@ -0,0 +1,486 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { readFile } from "node:fs/promises";
4
+ import { appendTaskLog, assignTask, createBoard, createTask, DEFAULT_BOARD, deleteBoard, deleteTask, ensureDefaultBoard, getBoard, getTask, listAllBoards, listBoards, moveTask, pickTask, saveBoard, saveTask, } from "../core/boards.js";
5
+ import { taskSessions } from "../core/board-wake.js";
6
+ import { initContext as prepare } from "./routines.js";
7
+ import { fmtAgo } from "./workspaces.js";
8
+ /**
9
+ * `kraftwerk boards` and `kraftwerk tasks` — the boards (columns of tasks)
10
+ * of a project or of the workspace, and the tasks on them. The same module
11
+ * the inspector uses, so a task created here is on the board at once, and
12
+ * the agent that is assigned is woken by the server. An agent inside a
13
+ * session needs no --as: KRAFTWERK_AGENT names it, KRAFTWERK_ACTOR signs
14
+ * its log lines.
15
+ *
16
+ * kraftwerk boards [--project <p>] [--all] list boards (the workspace's, a project's, every one)
17
+ * kraftwerk boards show <board> [--project <p>] columns, rules, the tasks per column
18
+ * kraftwerk boards create <title> [--project <p>] [--agent] new board with the default columns
19
+ * kraftwerk boards set <board> --agent <slug> | --notes-file change default agent, stale_after, rules
20
+ * kraftwerk boards remove <board> move the board to the trash
21
+ *
22
+ * kraftwerk tasks [--board <b>] [--project <p>] [--status] [--assignee] [--mine]
23
+ * kraftwerk tasks show <n> task.md, folder path, files, log
24
+ * kraftwerk tasks create <title> [--status todo] [--assign <agent>] [--file task.md] [-m]
25
+ * kraftwerk tasks set <n> [--title] [--due] [--tags] [--refs] [--notes-file]
26
+ * kraftwerk tasks move <n> <column> [-m "<why>"]
27
+ * kraftwerk tasks assign <n> <agent> [-m "<hand-over note>"] | --to human
28
+ * kraftwerk tasks log <n> "<what you did>" [--tail <k>]
29
+ * kraftwerk tasks pick [--as <agent>] [--column todo]
30
+ * kraftwerk tasks remove <n>
31
+ *
32
+ * --board defaults to the project's `tasks` board (or the only board); the
33
+ * number alone names a task.
34
+ */
35
+ const die = (msg) => {
36
+ console.error(chalk.red(msg));
37
+ process.exit(1);
38
+ };
39
+ /** Who writes: --actor, else the session's KRAFTWERK_ACTOR, else human:user. */
40
+ const actorOf = (opt) => (opt ?? process.env.KRAFTWERK_ACTOR ?? "").trim() || "human:user";
41
+ /** Which agent acts: --as, else the session's KRAFTWERK_AGENT. */
42
+ const agentOf = (opt) => (opt ?? process.env.KRAFTWERK_AGENT ?? "").trim();
43
+ /** The board a command means: --board, else the project's default board when it exists, else the only board. */
44
+ async function whereOf(w, create = false) {
45
+ const project = w.project?.trim() || undefined;
46
+ if (w.board)
47
+ return { ...(project ? { project } : {}), board: w.board.trim() };
48
+ const view = await listBoards(project);
49
+ if (!view.enabled)
50
+ die(view.error ?? "boards are off");
51
+ if (view.error)
52
+ die(view.error);
53
+ const dflt = view.boards.find((b) => b.board === DEFAULT_BOARD);
54
+ if (dflt)
55
+ return { ...(project ? { project } : {}), board: DEFAULT_BOARD };
56
+ if (view.boards.length === 1)
57
+ return { ...(project ? { project } : {}), board: view.boards[0].board };
58
+ if (create && view.boards.length === 0)
59
+ return ensureDefaultBoard(project);
60
+ if (view.boards.length === 0)
61
+ die(`no board ${project ? `in project ${project}` : "in the workspace"} yet — \`kraftwerk boards create "<title>"${project ? ` --project ${project}` : ""}\` makes one`);
62
+ return die(`several boards here (${view.boards.map((b) => b.board).join(", ")}) — say which with --board`);
63
+ }
64
+ const flags = (ref) => (ref.project ? `--project ${ref.project} --board ${ref.board}` : `--board ${ref.board}`);
65
+ function taskRow(t) {
66
+ return [
67
+ chalk.cyan(String(t.number)),
68
+ t.configError ? chalk.red(t.configError) : t.title,
69
+ t.status,
70
+ t.assignee ?? chalk.dim("—"),
71
+ t.due ?? chalk.dim("—"),
72
+ chalk.dim(t.lastLog ?? ""),
73
+ chalk.dim(fmtAgo(t.updatedAt)),
74
+ ];
75
+ }
76
+ function printBoard(b) {
77
+ console.log(`${chalk.bold(b.title)} ${chalk.dim(`(${b.project ? `${b.project}/` : ""}${b.board}${b.agent ? `, default agent ${b.agent}` : ""})`)}`);
78
+ if (b.configError)
79
+ console.log(chalk.red(`board.md: ${b.configError}`));
80
+ if (b.notes.trim())
81
+ console.log(b.notes.trim());
82
+ console.log(chalk.dim(b.path));
83
+ for (const c of b.columns) {
84
+ const items = b.items.filter((t) => t.status === c.slug);
85
+ console.log(`\n${chalk.bold(c.label)} ${chalk.dim(`(${items.length}${c.limit ? `/${c.limit}` : ""}${c.review ? ", review" : c.done ? ", done" : ""})`)}`);
86
+ for (const t of items)
87
+ console.log(` ${chalk.cyan(String(t.number).padStart(3))} ${t.title}${t.assignee ? chalk.dim(` · ${t.assignee}`) : ""}${t.due ? chalk.dim(` · due ${t.due}`) : ""}${t.stale ? chalk.yellow(" · stale") : ""}`);
88
+ }
89
+ const lost = b.items.filter((t) => !b.columns.some((c) => c.slug === t.status));
90
+ if (lost.length) {
91
+ console.log(`\n${chalk.bold("no column")}`);
92
+ for (const t of lost)
93
+ console.log(` ${chalk.cyan(String(t.number).padStart(3))} ${t.title} ${chalk.dim(`(status "${t.status}")`)}`);
94
+ }
95
+ }
96
+ function printTask(t) {
97
+ console.log(`${chalk.cyan(String(t.number))} ${chalk.bold(t.title)} ${chalk.dim(`(${t.ref})`)}`);
98
+ console.log(`${t.status}${t.assignee ? ` · ${t.assignee}` : " · unassigned"}${t.due ? ` · due ${t.due}` : ""}${t.tags.length ? ` · ${t.tags.join(", ")}` : ""}${t.stale ? chalk.yellow(" · stale") : ""}`);
99
+ if (t.refs.length)
100
+ console.log(chalk.dim(`refs: ${t.refs.join(", ")}`));
101
+ console.log(chalk.dim(`folder: ${t.path}`));
102
+ const sessions = t.sessions ?? [];
103
+ if (sessions.length)
104
+ console.log(chalk.dim(`sessions: ${sessions.map((s) => `${s.emoji ? `${s.emoji} ` : ""}${s.name ?? s.agent} ${s.id}${s.busy ? " (working)" : ""}`).join(", ")}`));
105
+ if (t.configError)
106
+ console.log(chalk.red(`task.md: ${t.configError}`));
107
+ if (t.notes.trim())
108
+ console.log(`\n${t.notes.trim()}`);
109
+ console.log(`\n${chalk.bold("files")}${t.fileList.length ? "" : chalk.dim(" (none)")}`);
110
+ for (const f of t.fileList)
111
+ console.log(` ${f}`);
112
+ if (t.log.trim())
113
+ console.log(`\n${chalk.bold("log")}\n${t.log.trim()}`);
114
+ }
115
+ export function registerBoardCommands(program) {
116
+ const boards = program.command("boards").description("Task boards of a project or the workspace: list, show, create, set, remove");
117
+ boards
118
+ .command("list", { isDefault: true })
119
+ .description("List the boards: the workspace's, a project's (--project), or every one (--all)")
120
+ .option("--project <slug>", "A project's boards")
121
+ .option("--all", "Every board of the workspace and its projects")
122
+ .option("--json", "Machine-readable output")
123
+ .action(async (opts) => {
124
+ await prepare();
125
+ const view = opts.all ? { enabled: true, boards: await listAllBoards() } : await listBoards(opts.project);
126
+ if (opts.json) {
127
+ console.log(JSON.stringify(view, null, 2));
128
+ return;
129
+ }
130
+ if (!view.enabled)
131
+ die(("error" in view && view.error) || "boards are off");
132
+ if ("error" in view && view.error)
133
+ die(view.error);
134
+ if (view.boards.length === 0) {
135
+ console.log(chalk.dim(`No boards ${opts.project ? `in project ${opts.project}` : opts.all ? "anywhere" : "in the workspace"} yet — \`kraftwerk boards create "<title>"${opts.project ? ` --project ${opts.project}` : ""}\` starts one.`));
136
+ return;
137
+ }
138
+ const table = new Table({ head: ["board", "where", "tasks", "review", "columns", "changed"].map((h) => chalk.bold(h)) });
139
+ for (const b of view.boards) {
140
+ table.push([
141
+ `${chalk.cyan(b.title)}\n${chalk.dim(b.board)}`,
142
+ b.project ? `project ${b.project}` : chalk.dim("workspace"),
143
+ String(b.tasks),
144
+ b.review ? chalk.yellow(String(b.review)) : chalk.dim("0"),
145
+ chalk.dim(b.configError ? chalk.red(b.configError) : b.columns.map((c) => `${c.label} ${b.counts[c.slug] ?? 0}`).join(" · ")),
146
+ chalk.dim(fmtAgo(b.updatedAt)),
147
+ ]);
148
+ }
149
+ console.log(table.toString());
150
+ });
151
+ boards
152
+ .command("show")
153
+ .description("One board: columns, rules, every task per column")
154
+ .argument("<board>", "Board slug")
155
+ .option("--project <slug>", "The project the board belongs to")
156
+ .option("--json", "Machine-readable output")
157
+ .action(async (board, opts) => {
158
+ await prepare();
159
+ try {
160
+ const b = await getBoard({ ...(opts.project ? { project: opts.project } : {}), board });
161
+ if (!b)
162
+ die(`no board "${board}"`);
163
+ if (opts.json)
164
+ console.log(JSON.stringify(b, null, 2));
165
+ else
166
+ printBoard(b);
167
+ }
168
+ catch (err) {
169
+ die(err.message);
170
+ }
171
+ });
172
+ boards
173
+ .command("create")
174
+ .description("Create a board with the default columns (draft, todo, progress, review, done) unless --columns is given")
175
+ .argument("<title>", "Display title; the folder name is derived from it unless --slug is given")
176
+ .option("--project <slug>", "Create it inside this project (projects/<slug>/boards/)")
177
+ .option("--slug <slug>", "Folder name (lowercase, digits, dashes)")
178
+ .option("--columns <list>", "Comma-separated column slugs")
179
+ .option("--agent <slug>", "Default agent: tasks entering todo unassigned go to it")
180
+ .option("--notes <text>", "The board's rules (the body of board.md)")
181
+ .option("--json", "Print the board as JSON")
182
+ .action(async (title, opts) => {
183
+ await prepare();
184
+ try {
185
+ const b = await createBoard({ project: opts.project, title, slug: opts.slug, columns: opts.columns ? opts.columns.split(",").map((s) => s.trim()).filter(Boolean) : undefined, agent: opts.agent, notes: opts.notes });
186
+ if (opts.json)
187
+ console.log(JSON.stringify(b, null, 2));
188
+ else
189
+ console.log(`${chalk.green("✔")} ${chalk.cyan(b.board)} → ${b.path} ${chalk.dim(`(columns: ${b.columns.map((c) => c.slug).join(", ")})`)}`);
190
+ }
191
+ catch (err) {
192
+ die(err.message);
193
+ }
194
+ });
195
+ boards
196
+ .command("set")
197
+ .description("Change a board's title, default agent, stale_after or rules")
198
+ .argument("<board>", "Board slug")
199
+ .option("--project <slug>", "The project the board belongs to")
200
+ .option("--title <text>", "Display title")
201
+ .option("--agent <slug>", 'Default agent; "" clears it')
202
+ .option("--stale-after <duration>", 'Mark a worked task stale after e.g. 12h, 2d; "" for the default (1d)')
203
+ .option("--columns <list>", "Comma-separated column slugs (replaces the list)")
204
+ .option("--notes-file <path>", "Replace the rules with this file's text")
205
+ .option("--json", "Print the board as JSON")
206
+ .action(async (board, opts) => {
207
+ await prepare();
208
+ try {
209
+ const input = {
210
+ ...(opts.title !== undefined ? { title: opts.title } : {}),
211
+ ...(opts.agent !== undefined ? { agent: opts.agent || null } : {}),
212
+ ...(opts.staleAfter !== undefined ? { staleAfter: opts.staleAfter || null } : {}),
213
+ ...(opts.columns !== undefined ? { columns: opts.columns.split(",").map((s) => s.trim()).filter(Boolean) } : {}),
214
+ ...(opts.notesFile ? { notes: await readFile(opts.notesFile, "utf8") } : {}),
215
+ };
216
+ if (Object.keys(input).length === 0)
217
+ die("nothing to set — pass --title, --agent, --stale-after, --columns or --notes-file");
218
+ const b = await saveBoard({ ...(opts.project ? { project: opts.project } : {}), board }, input);
219
+ if (opts.json)
220
+ console.log(JSON.stringify(b, null, 2));
221
+ else
222
+ console.log(`${chalk.green("✔")} ${chalk.cyan(b.board)} saved`);
223
+ }
224
+ catch (err) {
225
+ die(err.message);
226
+ }
227
+ });
228
+ boards
229
+ .command("remove")
230
+ .description("Move a board, tasks and all, to the trash")
231
+ .argument("<board>", "Board slug")
232
+ .option("--project <slug>", "The project the board belongs to")
233
+ .action(async (board, opts) => {
234
+ await prepare();
235
+ try {
236
+ await deleteBoard({ ...(opts.project ? { project: opts.project } : {}), board });
237
+ console.log(`${chalk.green("✔")} moved ${chalk.cyan(board)} to the trash`);
238
+ }
239
+ catch (err) {
240
+ die(err.message);
241
+ }
242
+ });
243
+ const tasks = program.command("tasks").description("Tasks on a board: list, show, create, set, move, assign, log, pick, remove");
244
+ const where = (cmd) => cmd.option("--board <slug>", "The board (default: the project's `tasks` board, or the only one)").option("--project <slug>", "The project the board belongs to");
245
+ where(tasks.command("list", { isDefault: true }))
246
+ .description("List the tasks of a board, column by column")
247
+ .option("--status <column>", "Only this column")
248
+ .option("--assignee <agent>", 'Only this agent\'s ("" for unassigned)')
249
+ .option("--mine", "Only the tasks of the agent this session runs as (KRAFTWERK_AGENT)")
250
+ .option("--as <agent>", "With --mine: which agent")
251
+ .option("--json", "Machine-readable output")
252
+ .action(async (opts) => {
253
+ await prepare();
254
+ try {
255
+ const ref = await whereOf(opts);
256
+ const b = await getBoard(ref);
257
+ if (!b)
258
+ die(`no board "${ref.board}"`);
259
+ const me = opts.mine ? agentOf(opts.as) || die("--mine needs --as <agent> outside an agent session") : undefined;
260
+ const items = b.items.filter((t) => (opts.status === undefined || t.status === opts.status) && (opts.assignee === undefined || (t.assignee ?? "") === opts.assignee) && (me === undefined || t.assignee === me));
261
+ if (opts.json) {
262
+ console.log(JSON.stringify({ board: ref, columns: b.columns, items }, null, 2));
263
+ return;
264
+ }
265
+ if (items.length === 0) {
266
+ console.log(chalk.dim(`No tasks on ${b.title}${opts.status ? ` in ${opts.status}` : ""}.`));
267
+ return;
268
+ }
269
+ const table = new Table({ head: ["#", "task", "column", "assignee", "due", "last", "changed"].map((h) => chalk.bold(h)), wordWrap: true, colWidths: [5, 36, 11, 12, 12, 30, 12] });
270
+ for (const t of items)
271
+ table.push(taskRow(t));
272
+ console.log(table.toString());
273
+ console.log(chalk.dim(`${b.title} (${flags(ref)})`));
274
+ }
275
+ catch (err) {
276
+ die(err.message);
277
+ }
278
+ });
279
+ where(tasks.command("show"))
280
+ .description("One task: fields, markdown, folder path, files and log")
281
+ .argument("<task>", "Task number or folder name")
282
+ .option("--json", "Machine-readable output")
283
+ .action(async (task, opts) => {
284
+ await prepare();
285
+ try {
286
+ const found = await getTask(await whereOf(opts), task);
287
+ if (!found)
288
+ die(`no task "${task}"`);
289
+ const t = { ...found, sessions: await taskSessions(found.ref).catch(() => []) };
290
+ if (opts.json)
291
+ console.log(JSON.stringify(t, null, 2));
292
+ else
293
+ printTask(t);
294
+ }
295
+ catch (err) {
296
+ die(err.message);
297
+ }
298
+ });
299
+ where(tasks.command("create"))
300
+ .description("Create a task (a numbered folder with task.md and log.md); --assign with --status todo wakes the agent")
301
+ .argument("<title>", "The task's title")
302
+ .option("--status <column>", "The column it starts in (default: the first, draft)")
303
+ .option("--assign <agent>", "Assignee (an agent slug)")
304
+ .option("--due <date>", "Due date, 2026-11-20")
305
+ .option("--tags <list>", "Comma-separated tags")
306
+ .option("--refs <list>", "Comma-separated kind:slug references (contact:anna-meier, knowledge:brand-voice)")
307
+ .option("--file <path>", "The task's markdown (what, done-when, notes) from a file")
308
+ .option("--notes <text>", "The task's markdown inline")
309
+ .option("-m, --message <text>", "A first log line")
310
+ .option("--actor <actor>", "Who writes (default: KRAFTWERK_ACTOR, else human:user)")
311
+ .option("--json", "Print the task as JSON")
312
+ .action(async (title, opts) => {
313
+ await prepare();
314
+ try {
315
+ const ref = await whereOf(opts, true);
316
+ const t = await createTask(ref, {
317
+ title,
318
+ status: opts.status,
319
+ assignee: opts.assign,
320
+ due: opts.due,
321
+ tags: opts.tags,
322
+ refs: opts.refs,
323
+ notes: opts.file ? await readFile(opts.file, "utf8") : opts.notes,
324
+ actor: actorOf(opts.actor),
325
+ message: opts.message,
326
+ });
327
+ if (opts.json)
328
+ console.log(JSON.stringify(t, null, 2));
329
+ else
330
+ console.log(`${chalk.green("✔")} ${chalk.cyan(String(t.number))} ${t.title} ${chalk.dim(`(${t.status}${t.assignee ? `, ${t.assignee}` : ""}) → ${t.path}`)}`);
331
+ }
332
+ catch (err) {
333
+ die(err.message);
334
+ }
335
+ });
336
+ where(tasks.command("set"))
337
+ .description("Change a task's title, due date, tags, refs or markdown")
338
+ .argument("<task>", "Task number or folder name")
339
+ .option("--title <text>", "New title (the folder keeps its name)")
340
+ .option("--due <date>", 'Due date; "" clears it')
341
+ .option("--tags <list>", "Comma-separated tags (replaces)")
342
+ .option("--refs <list>", "Comma-separated kind:slug references (replaces)")
343
+ .option("--notes-file <path>", "Replace the markdown with this file's text")
344
+ .option("--actor <actor>", "Who writes")
345
+ .option("--json", "Print the task as JSON")
346
+ .action(async (task, opts) => {
347
+ await prepare();
348
+ try {
349
+ const input = {
350
+ ...(opts.title !== undefined ? { title: opts.title } : {}),
351
+ ...(opts.due !== undefined ? { due: opts.due || null } : {}),
352
+ ...(opts.tags !== undefined ? { tags: opts.tags } : {}),
353
+ ...(opts.refs !== undefined ? { refs: opts.refs } : {}),
354
+ ...(opts.notesFile ? { notes: await readFile(opts.notesFile, "utf8") } : {}),
355
+ };
356
+ if (Object.keys(input).length === 0)
357
+ die("nothing to set — pass --title, --due, --tags, --refs or --notes-file");
358
+ const t = await saveTask(await whereOf(opts), task, input, { actor: actorOf(opts.actor) });
359
+ if (opts.json)
360
+ console.log(JSON.stringify(t, null, 2));
361
+ else
362
+ console.log(`${chalk.green("✔")} ${chalk.cyan(String(t.number))} ${t.title} saved`);
363
+ }
364
+ catch (err) {
365
+ die(err.message);
366
+ }
367
+ });
368
+ where(tasks.command("move"))
369
+ .description("Put a task into a column; the move is logged with -m, and an assigned agent entering a work column is woken")
370
+ .argument("<task>", "Task number or folder name")
371
+ .argument("<column>", "Column slug")
372
+ .option("-m, --message <text>", "Why, for the log")
373
+ .option("--actor <actor>", "Who writes")
374
+ .option("--json", "Print the task as JSON")
375
+ .action(async (task, column, opts) => {
376
+ await prepare();
377
+ try {
378
+ const t = await moveTask(await whereOf(opts), task, column, { message: opts.message, actor: actorOf(opts.actor) });
379
+ if (opts.json)
380
+ console.log(JSON.stringify(t, null, 2));
381
+ else
382
+ console.log(`${chalk.green("✔")} ${chalk.cyan(String(t.number))} ${t.title} → ${t.status}`);
383
+ }
384
+ catch (err) {
385
+ die(err.message);
386
+ }
387
+ });
388
+ where(tasks.command("assign"))
389
+ .description('Hand a task to an agent (woken when the task is in a work column), or back to the humans with "human" (unassigned, into review)')
390
+ .argument("<task>", "Task number or folder name")
391
+ .argument("[agent]", 'Agent slug, or "human"; omit with --to')
392
+ .option("--to <agent>", 'Same as the argument: an agent slug or "human"')
393
+ .option("-m, --message <text>", "The hand-over note, for the log and the next agent")
394
+ .option("--actor <actor>", "Who writes")
395
+ .option("--json", "Print the task as JSON")
396
+ .action(async (task, agent, opts) => {
397
+ await prepare();
398
+ const to = (opts.to ?? agent ?? "").trim();
399
+ if (!to)
400
+ die('say who: `tasks assign <n> <agent>` or `--to human`');
401
+ try {
402
+ const t = await assignTask(await whereOf(opts), task, to === "human" ? null : to, { message: opts.message, actor: actorOf(opts.actor) });
403
+ if (opts.json)
404
+ console.log(JSON.stringify(t, null, 2));
405
+ else
406
+ console.log(`${chalk.green("✔")} ${chalk.cyan(String(t.number))} ${t.title} → ${t.assignee ?? `humans (${t.status})`}`);
407
+ }
408
+ catch (err) {
409
+ die(err.message);
410
+ }
411
+ });
412
+ where(tasks.command("log"))
413
+ .description("Append a stamped line to the task's log (what you did, a question, a decision), or print the last lines with --tail")
414
+ .argument("<task>", "Task number or folder name")
415
+ .argument("[entry]", "The line to record")
416
+ .option("--file <path>", "Read the entry from a file")
417
+ .option("--tail <n>", "Print the newest n lines instead of writing")
418
+ .option("--actor <actor>", "Who writes (default: KRAFTWERK_ACTOR, else human:user)")
419
+ .action(async (task, entry, opts) => {
420
+ await prepare();
421
+ try {
422
+ const ref = await whereOf(opts);
423
+ if (opts.tail !== undefined) {
424
+ const t = await getTask(ref, task);
425
+ if (!t)
426
+ die(`no task "${task}"`);
427
+ const n = Number(opts.tail) || 5;
428
+ console.log(t.log
429
+ .split("\n")
430
+ .filter((l) => l.startsWith("* ") || l.startsWith("## "))
431
+ .slice(0, n * 2)
432
+ .join("\n"));
433
+ return;
434
+ }
435
+ const text = opts.file ? await readFile(opts.file, "utf8") : entry ?? "";
436
+ const t = await appendTaskLog(ref, task, text, actorOf(opts.actor));
437
+ console.log(`${chalk.green("✔")} logged on ${chalk.cyan(String(t.number))} ${chalk.dim(`(${actorOf(opts.actor)})`)}`);
438
+ }
439
+ catch (err) {
440
+ die(err.message);
441
+ }
442
+ });
443
+ where(tasks.command("pick"))
444
+ .description("Take the next task of the pick column (todo): it becomes yours and moves to the work column; prints nothing and exits 3 when there is none")
445
+ .option("--as <agent>", "Which agent picks (default: KRAFTWERK_AGENT)")
446
+ .option("--column <slug>", "Pick from this column instead of todo")
447
+ .option("--json", "Print the task as JSON")
448
+ .action(async (opts) => {
449
+ await prepare();
450
+ const agent = agentOf(opts.as);
451
+ if (!agent)
452
+ die("who picks? pass --as <agent> (inside an agent session KRAFTWERK_AGENT is set)");
453
+ try {
454
+ const t = await pickTask(await whereOf(opts), agent, { column: opts.column, actor: process.env.KRAFTWERK_ACTOR || agent });
455
+ if (!t) {
456
+ if (opts.json)
457
+ console.log("null");
458
+ else
459
+ console.log(chalk.dim("nothing to pick"));
460
+ process.exit(3);
461
+ }
462
+ if (opts.json)
463
+ console.log(JSON.stringify(t, null, 2));
464
+ else {
465
+ console.log(`${chalk.green("✔")} picked ${chalk.cyan(String(t.number))} ${t.title} → ${t.status}`);
466
+ printTask(t);
467
+ }
468
+ }
469
+ catch (err) {
470
+ die(err.message);
471
+ }
472
+ });
473
+ where(tasks.command("remove"))
474
+ .description("Move a task folder, files and log included, to the trash")
475
+ .argument("<task>", "Task number or folder name")
476
+ .action(async (task, opts) => {
477
+ await prepare();
478
+ try {
479
+ await deleteTask(await whereOf(opts), task);
480
+ console.log(`${chalk.green("✔")} moved task ${chalk.cyan(task)} to the trash`);
481
+ }
482
+ catch (err) {
483
+ die(err.message);
484
+ }
485
+ });
486
+ }
package/dist/cli/init.js CHANGED
@@ -34,6 +34,7 @@ skills: ${DATA_DIR}/skills # workspace skills (shared instruction packag
34
34
  # contacts: # people and organisations, one Markdown file each under ${DATA_DIR}/contacts — referenced as contact:<slug> (uncomment to enable)
35
35
  # places: # addresses and points of interest, one Markdown file each under ${DATA_DIR}/places — referenced as place:<slug> (uncomment to enable)
36
36
  # events: # dated one-off entries with place, contacts and project, one Markdown file each under ${DATA_DIR}/events — referenced as event:<slug> (uncomment to enable)
37
+ # boards: # tasks as folders in columns (Draft, Todo, In progress, Review, Done) per project and under ${DATA_DIR}/boards — agents are woken when a task is assigned to them (uncomment to enable)
37
38
  # cloud: # the daemon registers this MACHINE with the kraftwerk cloud and lists this workspace (\`kraftwerk login\` claims the machine)
38
39
  # enabled: false # uncomment both lines to leave this workspace out (\`kraftwerk cloud off\` turns the cloud off for the whole machine)
39
40
  `;
@@ -13,6 +13,7 @@ import { runDoctor } from "./doctor.js";
13
13
  import { runInit } from "./init.js";
14
14
  import { registerKnowledgeCommands } from "./knowledge.js";
15
15
  import { registerProjectCommands } from "./projects.js";
16
+ import { registerBoardCommands } from "./boards.js";
16
17
  import { registerContactCommands, registerEventCommands, registerPlaceCommands, registerRefCommand } from "./entities.js";
17
18
  import { registerWorkspaceCommands } from "./workspaces.js";
18
19
  import { registerRepoCommands } from "./repos.js";
@@ -44,6 +45,7 @@ import { runUi } from "./ui.js";
44
45
  * kraftwerk ui start the inspector web UI (localhost:1981)
45
46
  * kraftwerk workspaces ... known workspaces on this machine (list/start/stop/forget)
46
47
  * kraftwerk projects ... goal-scoped project folders (list/create/show/link/log/remove)
48
+ * kraftwerk boards|tasks ... task boards per project or workspace: list/show/create; tasks list/show/create/move/assign/log/pick/remove
47
49
  * kraftwerk contacts|places|events who, where and when: one Markdown file each (list/show/create/set/remove)
48
50
  * kraftwerk ref <kind:slug> resolve a reference (contact:anna-meier) or every one in a file
49
51
  * kraftwerk vibeables ... small apps built live in a chat (list/create/remove)
@@ -304,6 +306,7 @@ registerKnowledgeCommands(program);
304
306
  registerRoutineCommands(program);
305
307
  registerWorkspaceCommands(program);
306
308
  registerProjectCommands(program);
309
+ registerBoardCommands(program);
307
310
  registerContactCommands(program);
308
311
  registerPlaceCommands(program);
309
312
  registerEventCommands(program);
@@ -20,6 +20,12 @@ export declare const ROUTES: {
20
20
  readonly "agents.skills": readonly ["GET", "/api/agents/:slug/skills"];
21
21
  readonly "agents.status": readonly ["GET", "/api/agent-status"];
22
22
  readonly "attention.list": readonly ["GET", "/api/attention"];
23
+ readonly "boards.create": readonly ["POST", "/api/boards"];
24
+ readonly "boards.delete": readonly ["DELETE", "/api/boards/:board"];
25
+ readonly "boards.ensure": readonly ["POST", "/api/boards/default"];
26
+ readonly "boards.get": readonly ["GET", "/api/boards/:board"];
27
+ readonly "boards.list": readonly ["GET", "/api/boards"];
28
+ readonly "boards.save": readonly ["PUT", "/api/boards/:board"];
23
29
  readonly "channels.create": readonly ["POST", "/api/channels"];
24
30
  readonly "channels.delete": readonly ["DELETE", "/api/channels/:slug"];
25
31
  readonly "channels.fromChat": readonly ["POST", "/api/channels/from-chat"];
@@ -136,6 +142,14 @@ export declare const ROUTES: {
136
142
  readonly "settings.save": readonly ["PUT", "/api/settings"];
137
143
  readonly "skills.get": readonly ["GET", "/api/skills/:name"];
138
144
  readonly "skills.list": readonly ["GET", "/api/skills"];
145
+ readonly "tasks.assign": readonly ["POST", "/api/boards/:board/tasks/:task/assign"];
146
+ readonly "tasks.create": readonly ["POST", "/api/boards/:board/tasks"];
147
+ readonly "tasks.delete": readonly ["DELETE", "/api/boards/:board/tasks/:task"];
148
+ readonly "tasks.get": readonly ["GET", "/api/boards/:board/tasks/:task"];
149
+ readonly "tasks.log": readonly ["POST", "/api/boards/:board/tasks/:task/log"];
150
+ readonly "tasks.move": readonly ["POST", "/api/boards/:board/tasks/:task/move"];
151
+ readonly "tasks.pick": readonly ["POST", "/api/boards/:board/pick"];
152
+ readonly "tasks.save": readonly ["PUT", "/api/boards/:board/tasks/:task"];
139
153
  readonly "temp.clean": readonly ["DELETE", "/api/temp"];
140
154
  readonly "temp.list": readonly ["GET", "/api/temp"];
141
155
  readonly "trash.empty": readonly ["DELETE", "/api/trash"];
@@ -21,6 +21,12 @@ export const ROUTES = {
21
21
  "agents.skills": ["GET", "/api/agents/:slug/skills"],
22
22
  "agents.status": ["GET", "/api/agent-status"],
23
23
  "attention.list": ["GET", "/api/attention"],
24
+ "boards.create": ["POST", "/api/boards"],
25
+ "boards.delete": ["DELETE", "/api/boards/:board"],
26
+ "boards.ensure": ["POST", "/api/boards/default"],
27
+ "boards.get": ["GET", "/api/boards/:board"],
28
+ "boards.list": ["GET", "/api/boards"],
29
+ "boards.save": ["PUT", "/api/boards/:board"],
24
30
  "channels.create": ["POST", "/api/channels"],
25
31
  "channels.delete": ["DELETE", "/api/channels/:slug"],
26
32
  "channels.fromChat": ["POST", "/api/channels/from-chat"],
@@ -137,6 +143,14 @@ export const ROUTES = {
137
143
  "settings.save": ["PUT", "/api/settings"],
138
144
  "skills.get": ["GET", "/api/skills/:name"],
139
145
  "skills.list": ["GET", "/api/skills"],
146
+ "tasks.assign": ["POST", "/api/boards/:board/tasks/:task/assign"],
147
+ "tasks.create": ["POST", "/api/boards/:board/tasks"],
148
+ "tasks.delete": ["DELETE", "/api/boards/:board/tasks/:task"],
149
+ "tasks.get": ["GET", "/api/boards/:board/tasks/:task"],
150
+ "tasks.log": ["POST", "/api/boards/:board/tasks/:task/log"],
151
+ "tasks.move": ["POST", "/api/boards/:board/tasks/:task/move"],
152
+ "tasks.pick": ["POST", "/api/boards/:board/pick"],
153
+ "tasks.save": ["PUT", "/api/boards/:board/tasks/:task"],
140
154
  "temp.clean": ["DELETE", "/api/temp"],
141
155
  "temp.list": ["GET", "/api/temp"],
142
156
  "trash.empty": ["DELETE", "/api/trash"],