@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
@@ -0,0 +1,160 @@
1
+ import * as z from "zod";
2
+ import { appendTaskLog, assignTask, createBoard, createTask, deleteBoard, deleteTask, ensureDefaultBoard, getBoard, getTask, listAllBoards, listBoards, moveTask, openBoards, pickTask, saveBoard, saveTask, } from "../../core/boards.js";
3
+ import { taskSessions } from "../../core/board-wake.js";
4
+ import { fail, reply, route, statusByMessage } from "./router.js";
5
+ /**
6
+ * Boards and tasks over the API. A board is addressed by its slug and, for
7
+ * a project's board, `?project=<slug>`; a task by its folder name or its
8
+ * bare number. Writes that change a task's column or assignee are logged
9
+ * on the task and may wake the assignee (core/board-wake.ts).
10
+ */
11
+ export const boardErrors = statusByMessage(/^no (board|task|project)/);
12
+ const Columns = z.array(z.union([z.string(), z.object({ slug: z.string(), label: z.string().optional(), limit: z.number().optional(), done: z.boolean().optional(), review: z.boolean().optional() })]));
13
+ const BoardFields = z.object({
14
+ title: z.string().optional(),
15
+ columns: Columns.optional(),
16
+ agent: z.string().nullable().optional(),
17
+ staleAfter: z.string().nullable().optional(),
18
+ notes: z.string().optional(),
19
+ });
20
+ const TaskFields = z.object({
21
+ title: z.string().optional(),
22
+ status: z.string().optional(),
23
+ order: z.number().optional(),
24
+ assignee: z.string().nullable().optional(),
25
+ due: z.string().nullable().optional(),
26
+ tags: z.array(z.string()).optional(),
27
+ refs: z.array(z.string()).optional(),
28
+ notes: z.string().optional(),
29
+ /** Who writes (human:user by default) and a line for the log. */
30
+ actor: z.string().optional(),
31
+ message: z.string().optional(),
32
+ });
33
+ const refOf = (c) => {
34
+ const project = c.query.get("project");
35
+ return { ...(project ? { project } : {}), board: c.params.board };
36
+ };
37
+ const requireOn = async () => {
38
+ if ((await openBoards()).off)
39
+ fail(409, "boards are off");
40
+ };
41
+ export const boardRoutes = [
42
+ route({ name: "boards.list", method: "GET", path: "/api/boards", summary: "the boards: a project's with ?project=, every board with ?all=1, else the workspace's" }, async (c) => {
43
+ if (c.query.get("all") === "1") {
44
+ const opened = await openBoards();
45
+ return { enabled: !opened.off && !!opened.root, ...(opened.root ? { root: opened.root } : {}), boards: await listAllBoards(), ...(opened.error ? { error: opened.error } : {}) };
46
+ }
47
+ return listBoards(c.query.get("project") ?? undefined);
48
+ }),
49
+ route({
50
+ name: "boards.create",
51
+ method: "POST",
52
+ path: "/api/boards",
53
+ summary: "create a board {title, slug?, project?, columns?, agent?, notes?}",
54
+ errors: boardErrors,
55
+ body: z.object({ title: z.string(), slug: z.string().optional(), project: z.string().optional(), columns: Columns.optional(), agent: z.string().optional(), notes: z.string().optional() }),
56
+ }, async (c) => {
57
+ await requireOn();
58
+ return reply(201, await createBoard(await c.body()));
59
+ }),
60
+ route({ name: "boards.get", method: "GET", path: "/api/boards/:board", summary: "columns, rules, counts and every task (?project=)", errors: boardErrors }, async (c) => {
61
+ await requireOn();
62
+ const ref = refOf(c);
63
+ return (await getBoard(ref)) ?? fail(404, `no board "${ref.board}"`);
64
+ }),
65
+ route({ name: "boards.save", method: "PUT", path: "/api/boards/:board", summary: "save title, columns, default agent, stale_after, rules", errors: boardErrors, body: BoardFields }, async (c) => {
66
+ await requireOn();
67
+ return saveBoard(refOf(c), (await c.body()));
68
+ }),
69
+ route({ name: "boards.delete", method: "DELETE", path: "/api/boards/:board", summary: "move the board, tasks and all, to the trash", errors: boardErrors }, async (c) => {
70
+ await requireOn();
71
+ await deleteBoard(refOf(c));
72
+ return { ok: true };
73
+ }),
74
+ route({
75
+ name: "boards.ensure",
76
+ method: "POST",
77
+ path: "/api/boards/default",
78
+ summary: "the project's (or workspace's) default board `tasks`, created on first use {project?}",
79
+ errors: boardErrors,
80
+ body: z.object({ project: z.string().optional() }),
81
+ }, async (c) => {
82
+ await requireOn();
83
+ const ref = await ensureDefaultBoard((await c.body()).project);
84
+ return (await getBoard(ref));
85
+ }),
86
+ route({
87
+ name: "tasks.create",
88
+ method: "POST",
89
+ path: "/api/boards/:board/tasks",
90
+ summary: "create a task {title, status?, assignee?, due?, tags?, refs?, notes?, actor?, message?} (an assigned agent in a work column is woken)",
91
+ errors: boardErrors,
92
+ body: TaskFields.extend({ title: z.string() }),
93
+ }, async (c) => {
94
+ await requireOn();
95
+ return reply(201, await createTask(refOf(c), (await c.body())));
96
+ }),
97
+ route({ name: "tasks.get", method: "GET", path: "/api/boards/:board/tasks/:task", summary: "the task: fields, markdown, files, log and the sessions that worked on it (:task = folder name or number)", errors: boardErrors }, async (c) => {
98
+ await requireOn();
99
+ const task = (await getTask(refOf(c), c.params.task)) ?? fail(404, `no task "${c.params.task}"`);
100
+ return { ...task, sessions: await taskSessions(task.ref) };
101
+ }),
102
+ route({ name: "tasks.save", method: "PUT", path: "/api/boards/:board/tasks/:task", summary: "save fields and markdown; a status or assignee change is logged (message?)", errors: boardErrors, body: TaskFields }, async (c) => {
103
+ await requireOn();
104
+ const { actor, message, ...fields } = await c.body();
105
+ return saveTask(refOf(c), c.params.task, fields, { actor, message });
106
+ }),
107
+ route({
108
+ name: "tasks.move",
109
+ method: "POST",
110
+ path: "/api/boards/:board/tasks/:task/move",
111
+ summary: "put the task into a column {status, before?, order?, message?, actor?}",
112
+ errors: boardErrors,
113
+ body: z.object({ status: z.string(), before: z.string().optional(), order: z.number().optional(), message: z.string().optional(), actor: z.string().optional() }),
114
+ }, async (c) => {
115
+ await requireOn();
116
+ const { status, ...opts } = await c.body();
117
+ return moveTask(refOf(c), c.params.task, status, opts);
118
+ }),
119
+ route({
120
+ name: "tasks.assign",
121
+ method: "POST",
122
+ path: "/api/boards/:board/tasks/:task/assign",
123
+ summary: 'give the task to an agent {to, message?, actor?}; to = null or "human" hands it back to the review column',
124
+ errors: boardErrors,
125
+ body: z.object({ to: z.string().nullable(), message: z.string().optional(), actor: z.string().optional() }),
126
+ }, async (c) => {
127
+ await requireOn();
128
+ const { to, ...opts } = await c.body();
129
+ return assignTask(refOf(c), c.params.task, to, opts);
130
+ }),
131
+ route({
132
+ name: "tasks.log",
133
+ method: "POST",
134
+ path: "/api/boards/:board/tasks/:task/log",
135
+ summary: "append a stamped line {entry, actor?} to the task's log",
136
+ errors: boardErrors,
137
+ body: z.object({ entry: z.string(), actor: z.string().optional() }),
138
+ }, async (c) => {
139
+ await requireOn();
140
+ const { entry, actor } = await c.body();
141
+ return appendTaskLog(refOf(c), c.params.task, entry, actor);
142
+ }),
143
+ route({
144
+ name: "tasks.pick",
145
+ method: "POST",
146
+ path: "/api/boards/:board/pick",
147
+ summary: "an agent takes the next task of the pick column {agent, column?}; null when there is none",
148
+ errors: boardErrors,
149
+ body: z.object({ agent: z.string(), column: z.string().optional() }),
150
+ }, async (c) => {
151
+ await requireOn();
152
+ const { agent, column } = await c.body();
153
+ return { task: await pickTask(refOf(c), agent, { column }) };
154
+ }),
155
+ route({ name: "tasks.delete", method: "DELETE", path: "/api/boards/:board/tasks/:task", summary: "move the task folder to the trash", errors: boardErrors }, async (c) => {
156
+ await requireOn();
157
+ await deleteTask(refOf(c), c.params.task);
158
+ return { ok: true };
159
+ }),
160
+ ];
@@ -31,9 +31,11 @@ export declare const chatRoutes: (import("./router.js").Route<"agents.sessions",
31
31
  runId: z.ZodOptional<z.ZodString>;
32
32
  bundle: z.ZodOptional<z.ZodString>;
33
33
  slug: z.ZodOptional<z.ZodString>;
34
+ task: z.ZodOptional<z.ZodString>;
34
35
  }, z.core.$strip>>;
35
36
  resume: z.ZodOptional<z.ZodString>;
36
37
  project: z.ZodOptional<z.ZodString>;
38
+ task: z.ZodOptional<z.ZodString>;
37
39
  }, z.core.$strip>> | import("./router.js").Route<"chats.delete", import("./router.js").Reply<{
38
40
  error?: string;
39
41
  }>, "/api/chats/:id", undefined> | import("./router.js").Route<"chats.elicitation", import("./router.js").Reply<{
@@ -5,6 +5,7 @@ import { getAgent } from "../../core/agents.js";
5
5
  import { answerElicitation, cancelChat, createChat, deleteChat, forkChat, getChat, listAgentSessionsFor, listChats, localPerson, postMessage, renameChat, resetSession, resolvePermission, setChatConfig, setChatProject, setChatVibeable, steerChat, stopChatTask, subscribeChat, } from "../../core/chat/sessions.js";
6
6
  import { attachmentPath, readMeta, saveAttachment } from "../../core/chat/store.js";
7
7
  import { remoteCaller } from "../trust.js";
8
+ import { findTask } from "../../core/boards.js";
8
9
  import { getProject } from "../../core/projects.js";
9
10
  import { MIME } from "./mime.js";
10
11
  import { projectErrors } from "./projects.js";
@@ -24,10 +25,12 @@ const Said = z.object({
24
25
  /** Where a new chat runs; unknown kinds fall back to a general chat. */
25
26
  const NewChat = z.object({
26
27
  agent: z.string().optional(),
27
- scope: z.object({ kind: z.string(), runId: z.string().optional(), bundle: z.string().optional(), slug: z.string().optional() }).optional(),
28
+ scope: z.object({ kind: z.string(), runId: z.string().optional(), bundle: z.string().optional(), slug: z.string().optional(), task: z.string().optional() }).optional(),
28
29
  resume: z.string().optional(),
29
30
  /** An agent's chat started in a project: listed with the project (meta.project). */
30
31
  project: z.string().optional(),
32
+ /** The board task the chat works on (<project>/<board>/<task> or <board>/<task>): listed on the task's page (meta.task). */
33
+ task: z.string().optional(),
31
34
  });
32
35
  /** An action on a running chat: `{ok}` when it went through, 409 with the reason when the chat refused. */
33
36
  const act = (name, path, summary, body, run) => route({ name, method: "POST", path, summary, errors: 400, body }, async (c) => {
@@ -47,7 +50,8 @@ async function scopeOf(body) {
47
50
  const def = await getAgent(s.slug).catch(() => null);
48
51
  if (!def)
49
52
  fail(404, "agent not found");
50
- return { agent: def.harness, scope: { kind: "agent", slug: def.slug } };
53
+ // `task`: the session is opened for a board task (<project>/<board>/<task>): its context carries the task, and it runs unattended like a routine's.
54
+ return { agent: def.harness, scope: { kind: "agent", slug: def.slug, ...(s.task ? { task: s.task } : {}) } };
51
55
  }
52
56
  if (s?.kind === "project" && s.slug) {
53
57
  // A project chat needs its folder; a missing one is a 404 here, not a chat that apologizes.
@@ -109,7 +113,7 @@ export const chatRoutes = [
109
113
  const chats = await listChats();
110
114
  return { chats: remoteCaller(c.trust) ? chats.filter((x) => !x.private) : chats };
111
115
  }),
112
- route({ name: "chats.create", method: "POST", path: "/api/chats", summary: "start a chat {agent, scope, resume?, project?}", body: NewChat }, async (c) => {
116
+ route({ name: "chats.create", method: "POST", path: "/api/chats", summary: "start a chat {agent, scope, resume?, project?, task?}", errors: 404, body: NewChat }, async (c) => {
113
117
  // `resume` continues one of the agent's own sessions (see agents.sessions).
114
118
  const body = await c.body();
115
119
  const { agent, scope } = await scopeOf(body);
@@ -118,12 +122,14 @@ export const chatRoutes = [
118
122
  if (body.project && scope.kind !== "agent")
119
123
  fail(400, "only an agent's chat takes a project");
120
124
  const project = body.project ? await projectSlug(body.project) : undefined;
125
+ const task = body.task ? ((await findTask(body.task).catch(() => null)) ?? fail(404, `no task "${body.task}"`)).ref : undefined;
121
126
  try {
122
127
  return await createChat({
123
128
  agent: agent,
124
129
  scope,
125
130
  author: await personOf(c.trust),
126
131
  ...(project ? { project } : {}),
132
+ ...(task ? { task } : {}),
127
133
  ...(body.resume ? { resume: body.resume } : {}),
128
134
  });
129
135
  }
@@ -104,7 +104,36 @@ export declare const routes: (Route<"agents.addJournal", {
104
104
  content: string;
105
105
  }, "/api/agents/:slug/skills/:name", undefined> | Route<"agents.skills", {
106
106
  skills: import("../../core/skills.js").SkillInfo[];
107
- }, "/api/agents/:slug/skills", undefined> | Route<"agents.status", Record<string, import("../../core/agent-status.js").AgentStatus>, "/api/agent-status", undefined> | Route<"attention.list", import("../../core/attention.js").AttentionView, "/api/attention", undefined> | Route<"channels.create", {
107
+ }, "/api/agents/:slug/skills", undefined> | Route<"agents.status", Record<string, import("../../core/agent-status.js").AgentStatus>, "/api/agent-status", undefined> | Route<"attention.list", import("../../core/attention.js").AttentionView, "/api/attention", undefined> | Route<"boards.create", import("./router.js").Reply<import("../../core/boards.js").BoardDetail>, "/api/boards", z.ZodObject<{
108
+ title: z.ZodString;
109
+ slug: z.ZodOptional<z.ZodString>;
110
+ project: z.ZodOptional<z.ZodString>;
111
+ columns: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
112
+ slug: z.ZodString;
113
+ label: z.ZodOptional<z.ZodString>;
114
+ limit: z.ZodOptional<z.ZodNumber>;
115
+ done: z.ZodOptional<z.ZodBoolean>;
116
+ review: z.ZodOptional<z.ZodBoolean>;
117
+ }, z.core.$strip>]>>>;
118
+ agent: z.ZodOptional<z.ZodString>;
119
+ notes: z.ZodOptional<z.ZodString>;
120
+ }, z.core.$strip>> | Route<"boards.delete", {
121
+ ok: boolean;
122
+ }, "/api/boards/:board", undefined> | Route<"boards.ensure", import("../../core/boards.js").BoardDetail, "/api/boards/default", z.ZodObject<{
123
+ project: z.ZodOptional<z.ZodString>;
124
+ }, z.core.$strip>> | Route<"boards.get", import("../../core/boards.js").BoardDetail, "/api/boards/:board", undefined> | Route<"boards.list", import("../../core/boards.js").BoardsView, "/api/boards", undefined> | Route<"boards.save", import("../../core/boards.js").BoardDetail, "/api/boards/:board", z.ZodObject<{
125
+ title: z.ZodOptional<z.ZodString>;
126
+ columns: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
127
+ slug: z.ZodString;
128
+ label: z.ZodOptional<z.ZodString>;
129
+ limit: z.ZodOptional<z.ZodNumber>;
130
+ done: z.ZodOptional<z.ZodBoolean>;
131
+ review: z.ZodOptional<z.ZodBoolean>;
132
+ }, z.core.$strip>]>>>;
133
+ agent: z.ZodOptional<z.ZodNullable<z.ZodString>>;
134
+ staleAfter: z.ZodOptional<z.ZodNullable<z.ZodString>>;
135
+ notes: z.ZodOptional<z.ZodString>;
136
+ }, z.core.$strip>> | Route<"channels.create", {
108
137
  slug: string;
109
138
  name: string;
110
139
  purpose?: string;
@@ -220,9 +249,11 @@ export declare const routes: (Route<"agents.addJournal", {
220
249
  runId: z.ZodOptional<z.ZodString>;
221
250
  bundle: z.ZodOptional<z.ZodString>;
222
251
  slug: z.ZodOptional<z.ZodString>;
252
+ task: z.ZodOptional<z.ZodString>;
223
253
  }, z.core.$strip>>;
224
254
  resume: z.ZodOptional<z.ZodString>;
225
255
  project: z.ZodOptional<z.ZodString>;
256
+ task: z.ZodOptional<z.ZodString>;
226
257
  }, z.core.$strip>> | Route<"chats.delete", import("./router.js").Reply<{
227
258
  error?: string;
228
259
  }>, "/api/chats/:id", undefined> | Route<"chats.elicitation", import("./router.js").Reply<{
@@ -549,6 +580,7 @@ export declare const routes: (Route<"agents.addJournal", {
549
580
  contacts: boolean;
550
581
  places: boolean;
551
582
  events: boolean;
583
+ boards: boolean;
552
584
  publicUrl: string;
553
585
  cloud: {
554
586
  included: boolean;
@@ -725,10 +757,81 @@ export declare const routes: (Route<"agents.addJournal", {
725
757
  enabled: z.ZodBoolean;
726
758
  root: z.ZodOptional<z.ZodString>;
727
759
  }, z.core.$strip>>;
760
+ boards: z.ZodOptional<z.ZodObject<{
761
+ enabled: z.ZodBoolean;
762
+ root: z.ZodOptional<z.ZodString>;
763
+ }, z.core.$strip>>;
728
764
  }, z.core.$loose>> | Route<"skills.get", import("../../core/skills.js").SkillDetail, "/api/skills/:name", undefined> | Route<"skills.list", {
729
765
  root: string;
730
766
  skills: import("../../core/skills.js").SkillInfo[];
731
- }, "/api/skills", undefined> | Route<"temp.clean", {
767
+ }, "/api/skills", undefined> | Route<"tasks.assign", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task/assign", z.ZodObject<{
768
+ to: z.ZodNullable<z.ZodString>;
769
+ message: z.ZodOptional<z.ZodString>;
770
+ actor: z.ZodOptional<z.ZodString>;
771
+ }, z.core.$strip>> | Route<"tasks.create", import("./router.js").Reply<import("../../core/boards.js").TaskDetail>, "/api/boards/:board/tasks", z.ZodObject<{
772
+ status: z.ZodOptional<z.ZodString>;
773
+ order: z.ZodOptional<z.ZodNumber>;
774
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodString>>;
775
+ due: z.ZodOptional<z.ZodNullable<z.ZodString>>;
776
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
777
+ refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
778
+ notes: z.ZodOptional<z.ZodString>;
779
+ actor: z.ZodOptional<z.ZodString>;
780
+ message: z.ZodOptional<z.ZodString>;
781
+ title: z.ZodString;
782
+ }, z.core.$strip>> | Route<"tasks.delete", {
783
+ ok: boolean;
784
+ }, "/api/boards/:board/tasks/:task", undefined> | Route<"tasks.get", {
785
+ slug: string;
786
+ number: number;
787
+ title: string;
788
+ status: string;
789
+ order: number;
790
+ assignee?: string;
791
+ due?: string;
792
+ created: string;
793
+ tags: string[];
794
+ refs: string[];
795
+ extra?: Record<string, unknown>;
796
+ ref: string;
797
+ path: string;
798
+ updatedAt?: string;
799
+ configError?: string;
800
+ files: number;
801
+ lastLog?: string;
802
+ stale?: boolean;
803
+ notes: string;
804
+ log: string;
805
+ fileList: string[];
806
+ board: string;
807
+ project?: string;
808
+ sessions: import("../../core/board-wake.js").TaskSession[];
809
+ }, "/api/boards/:board/tasks/:task", undefined> | Route<"tasks.log", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task/log", z.ZodObject<{
810
+ entry: z.ZodString;
811
+ actor: z.ZodOptional<z.ZodString>;
812
+ }, z.core.$strip>> | Route<"tasks.move", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task/move", z.ZodObject<{
813
+ status: z.ZodString;
814
+ before: z.ZodOptional<z.ZodString>;
815
+ order: z.ZodOptional<z.ZodNumber>;
816
+ message: z.ZodOptional<z.ZodString>;
817
+ actor: z.ZodOptional<z.ZodString>;
818
+ }, z.core.$strip>> | Route<"tasks.pick", {
819
+ task: import("../../core/boards.js").TaskDetail | null;
820
+ }, "/api/boards/:board/pick", z.ZodObject<{
821
+ agent: z.ZodString;
822
+ column: z.ZodOptional<z.ZodString>;
823
+ }, z.core.$strip>> | Route<"tasks.save", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task", z.ZodObject<{
824
+ title: z.ZodOptional<z.ZodString>;
825
+ status: z.ZodOptional<z.ZodString>;
826
+ order: z.ZodOptional<z.ZodNumber>;
827
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodString>>;
828
+ due: z.ZodOptional<z.ZodNullable<z.ZodString>>;
829
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
830
+ refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
831
+ notes: z.ZodOptional<z.ZodString>;
832
+ actor: z.ZodOptional<z.ZodString>;
833
+ message: z.ZodOptional<z.ZodString>;
834
+ }, z.core.$strip>> | Route<"temp.clean", {
732
835
  ok: boolean;
733
836
  removed: string[];
734
837
  bytes: number;
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { agentRoutes } from "./agents.js";
3
3
  import { attentionRoutes } from "./attention.js";
4
+ import { boardRoutes } from "./boards.js";
4
5
  import { channelRoutes } from "./channels.js";
5
6
  import { chatRoutes } from "./chats.js";
6
7
  import { cloudRoutes } from "./cloud.js";
@@ -31,6 +32,7 @@ const domainRoutes = [
31
32
  ...chatRoutes,
32
33
  ...channelRoutes,
33
34
  ...projectRoutes,
35
+ ...boardRoutes,
34
36
  ...contactRoutes,
35
37
  ...placeRoutes,
36
38
  ...eventRoutes,
@@ -36,6 +36,7 @@ export declare const workspaceRoutes: (import("./router.js").Route<"identity.get
36
36
  contacts: boolean;
37
37
  places: boolean;
38
38
  events: boolean;
39
+ boards: boolean;
39
40
  /** @deprecated Always "": the Cloudflare Tunnel that set it was removed in 0.65. Removed with protocol 2. */
40
41
  publicUrl: string;
41
42
  cloud: {
@@ -101,6 +102,10 @@ export declare const workspaceRoutes: (import("./router.js").Route<"identity.get
101
102
  enabled: z.ZodBoolean;
102
103
  root: z.ZodOptional<z.ZodString>;
103
104
  }, z.core.$strip>>;
105
+ boards: z.ZodOptional<z.ZodObject<{
106
+ enabled: z.ZodBoolean;
107
+ root: z.ZodOptional<z.ZodString>;
108
+ }, z.core.$strip>>;
104
109
  }, z.core.$loose>> | import("./router.js").Route<"update.check", import("./router.js").Reply<{
105
110
  name: string;
106
111
  current: string;
@@ -36,6 +36,7 @@ const SettingsFields = z.looseObject({
36
36
  contacts: RootBlock.optional(),
37
37
  places: RootBlock.optional(),
38
38
  events: RootBlock.optional(),
39
+ boards: RootBlock.optional(),
39
40
  });
40
41
  /**
41
42
  * Mark the workspaces the hub answering this request serves — or, a daemon,
@@ -111,6 +112,7 @@ export const workspaceRoutes = [
111
112
  contacts: (ws?.config.contacts && ws.config.contacts.enabled !== false) === true,
112
113
  places: (ws?.config.places && ws.config.places.enabled !== false) === true,
113
114
  events: (ws?.config.events && ws.config.events.enabled !== false) === true,
115
+ boards: (ws?.config.boards && ws.config.boards.enabled !== false) === true,
114
116
  /** @deprecated Always "": the Cloudflare Tunnel that set it was removed in 0.65. Removed with protocol 2. */
115
117
  publicUrl: "",
116
118
  // The machine's cloud record (only the daemon registers one); `included`: this workspace rides along (no cloud.enabled: false veto).
@@ -11,7 +11,7 @@ export function sealPush(keyBase64, content) {
11
11
  const ct = Buffer.concat([cipher.update(JSON.stringify(content), "utf8"), cipher.final()]);
12
12
  return Buffer.concat([nonce, ct, cipher.getAuthTag()]).toString("base64");
13
13
  }
14
- const LABEL = { approval: "Approval", question: "Question", failed: "Failed", runner: "Routines" };
14
+ const LABEL = { approval: "Approval", question: "Question", failed: "Failed", runner: "Routines", review: "Review" };
15
15
  export function contentOf(item, workspace) {
16
16
  return {
17
17
  title: `${LABEL[item.kind] ?? "Needs you"} · ${workspace.name}`,
@@ -11,6 +11,7 @@ import { startGitSync, stopGitSync } from "../core/git.js";
11
11
  import { disposeAllDevs, disposeWorkspaceDevs } from "../core/vibeables.js";
12
12
  import { proxyUpgrade, serveVibeable } from "./preview.js";
13
13
  import { startRoutineScheduler, stopRoutineScheduler } from "../core/routines.js";
14
+ import { startBoardWake, stopBoardWake } from "../core/board-wake.js";
14
15
  import { readWorkspaceDotenv } from "../core/env.js";
15
16
  import { closeWatches } from "./live.js";
16
17
  import { MACHINE_PATHS, runInHub } from "./hub-context.js";
@@ -415,6 +416,7 @@ export async function startHub(opts) {
415
416
  fallback ??= entry;
416
417
  runInHub(hub, () => ws.run(() => {
417
418
  startRoutineScheduler();
419
+ startBoardWake();
418
420
  startGitSync();
419
421
  // Best-effort, in the background: none of them may hold up (or, failing, break) the open.
420
422
  entry.registered = registerInstance(entry.aliasPort ?? port, abs, opts.daemon ? { hub: true, slug, url: entry.url } : {}).catch(() => { });
@@ -440,6 +442,7 @@ export async function startHub(opts) {
440
442
  aliasServers.delete(abs);
441
443
  await runInHub(hub, () => entry.ws.run(async () => {
442
444
  stopRoutineScheduler();
445
+ stopBoardWake();
443
446
  stopGitSync();
444
447
  closeWatches();
445
448
  disposeWorkspaceBackends();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netnodeag/kraftwerk",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "description": "Open-source agentic workspace for teams \u2014 where people and AI agents get work done together: agents (Claude Code, Codex, Pi), skills, shared knowledge, repeatable workflows, and deterministic verification",
5
5
  "keywords": [
6
6
  "agents",
@@ -225,6 +225,45 @@
225
225
  }
226
226
  }
227
227
  },
228
+ "boards": {
229
+ "type": [
230
+ "object",
231
+ "null"
232
+ ],
233
+ "additionalProperties": false,
234
+ "description": "Boards: tasks as folders (task.md, log.md, files) in columns, per project (projects/<p>/boards/) and workspace-wide under the root; agents are woken when a task is assigned to them. Absent = feature off, a bare `boards:` = on with defaults.",
235
+ "properties": {
236
+ "enabled": {
237
+ "type": "boolean",
238
+ "description": "Turn the feature off while keeping the config. Default: true when this block exists"
239
+ },
240
+ "root": {
241
+ "type": "string",
242
+ "description": "Where the workspace boards live, relative to the project root. Default: kraftwerk-data/boards"
243
+ },
244
+ "columns": {
245
+ "type": "array",
246
+ "description": "Default columns for boards that name none: slugs or {slug, label, limit, done, review}. Default: draft, todo, progress, review (review: true), done (done: true)",
247
+ "items": {
248
+ "anyOf": [
249
+ { "type": "string" },
250
+ {
251
+ "type": "object",
252
+ "additionalProperties": false,
253
+ "required": ["slug"],
254
+ "properties": {
255
+ "slug": { "type": "string" },
256
+ "label": { "type": "string" },
257
+ "limit": { "type": "integer", "minimum": 1 },
258
+ "done": { "type": "boolean" },
259
+ "review": { "type": "boolean" }
260
+ }
261
+ }
262
+ ]
263
+ }
264
+ }
265
+ }
266
+ },
228
267
  "cloud": {
229
268
  "type": [
230
269
  "object",
@@ -1 +1 @@
1
- import{L as e,R as t,Ut as n,i as r,r as i,s as a,u as o}from"./dist-CYzOYk_e.js";import{n as s}from"./dist-8IOFzZx-.js";import{r as c}from"./dist-cewmjxFi.js";var l=i.deserialize({version:14,states:"%pOVOWOOObQPOOOpOSO'#C_OOOO'#Cp'#CpQVOWOOQxQPOOO!TQQOOQ!YQPOOOOOO,58y,58yO!_OSO,58yOOOO-E6n-E6nO!dQQO'#CqQ{QPOOO!iQPOOQ{QPOOO!qQPOOOOOO1G.e1G.eOOQO,59],59]OOQO-E6o-E6oO!yOpO'#CiO#RO`O'#CiQOQPOOO#ZO#tO'#CmO#fO!bO'#CmOOQO,59T,59TO#qOpO,59TO#vO`O,59TOOOO'#Cr'#CrO#{O#tO,59XOOQO,59X,59XOOOO'#Cs'#CsO$WO!bO,59XOOQO1G.o1G.oOOOO-E6p-E6pOOQO1G.s1G.sOOOO-E6q-E6q",stateData:"$g~OjOS~OQROUROkQO~OWTOXUOZUO`VO~OSXOTWO~OXUO[]OlZO~OY^O~O[_O~OT`O~OYaO~OmcOodO~OmfOogO~O^iOnhO~O_jOphO~ObkOqkOrmO~OcnOsnOtmO~OnpO~OppO~ObkOqkOrrO~OcnOsnOtrO~OWX`~",goto:`!^hPPPiPPPPPPPPPmPPPpPPsy!Q!WTROSRe]Re_QSORYSS[T^Rb[QlfRqlQogRso`,nodeNames:`⚠ Content Text Interpolation InterpolationContent }} Entity Attribute VueAttributeName : Identifier @ Is ScriptAttributeValue AttributeScript AttributeScript AttributeName AttributeValue Entity Entity`,maxTerm:36,nodeProps:[[`isolate`,-3,3,13,17,``]],skippedNodes:[0],repeatNodeCount:4,tokenData:"'y~RdXY!aYZ!a]^!apq!ars!rwx!w}!O!|!O!P#t!Q![#y![!]$s!_!`%g!b!c%l!c!}#y#R#S#y#T#j#y#j#k%q#k#o#y%W;'S#y;'S;:j$m<%lO#y~!fSj~XY!aYZ!a]^!apq!a~!wOm~~!|Oo~!b#RX`!b}!O!|!Q![!|![!]!|!c!}!|#R#S!|#T#o!|%W;'S!|;'S;:j#n<%lO!|!b#qP;=`<%l!|~#yOl~%W$QXY#t`!b}!O!|!Q![#y![!]!|!c!}#y#R#S#y#T#o#y%W;'S#y;'S;:j$m<%lO#y%W$pP;=`<%l#y~$zXX~`!b}!O!|!Q![!|![!]!|!c!}!|#R#S!|#T#o!|%W;'S!|;'S;:j#n<%lO!|~%lO[~~%qOZ~%W%xXY#t`!b}!O&e!Q![#y![!]!|!c!}#y#R#S#y#T#o#y%W;'S#y;'S;:j$m<%lO#y!b&jX`!b}!O!|!Q![!|![!]!|!c!}'V#R#S!|#T#o'V%W;'S!|;'S;:j#n<%lO!|!b'^XW!b`!b}!O!|!Q![!|![!]!|!c!}'V#R#S!|#T#o'V%W;'S!|;'S;:j#n<%lO!|",tokenizers:[6,7,new r(`b~RP#q#rU~XP#q#r[~aOT~~`,17,4),new r("!k~RQvwX#o#p!_~^TU~Opmq!]m!^;'Sm;'S;=`!X<%lOm~pUOpmq!]m!]!^!S!^;'Sm;'S;=`!X<%lOm~!XOU~~![P;=`<%lm~!bP#o#p!e~!jOk~~",72,2),new r(`[~RPwxU~ZOp~~`,11,15),new r(`[~RPrsU~ZOn~~`,11,14),new r("!e~RQvwXwx!_~^Tc~Opmq!]m!^;'Sm;'S;=`!X<%lOm~pUOpmq!]m!]!^!S!^;'Sm;'S;=`!X<%lOm~!XOc~~![P;=`<%lm~!dOt~~",66,35),new r("!e~RQrsXvw^~^Or~~cTb~Oprq!]r!^;'Sr;'S;=`!^<%lOr~uUOprq!]r!]!^!X!^;'Sr;'S;=`!^<%lOr~!^Ob~~!aP;=`<%lr~",66,33)],topRules:{Content:[0,1],Attribute:[1,7]},tokenPrec:157}),u=c.parser.configure({top:`SingleExpression`}),d=l.configure({props:[e({Text:t.content,Is:t.definitionOperator,AttributeName:t.attributeName,VueAttributeName:t.keyword,Identifier:t.variableName,"AttributeValue ScriptAttributeValue":t.attributeValue,Entity:t.character,"{{ }}":t.brace,"@ :":t.punctuation})]}),f={parser:u},p=d.configure({wrap:n((e,t)=>e.name==`InterpolationContent`?f:null)}),m=d.configure({wrap:n((e,t)=>e.name==`AttributeScript`?f:null),top:`Attribute`}),h={parser:p},g={parser:m},_=s();function v(e){return e.configure({dialect:`selfClosing`,wrap:n(b)},`vue`)}var y=v(_.language);function b(e,t){switch(e.name){case`Attribute`:return/^(@|:|v-)/.test(t.read(e.from,e.from+2))?g:null;case`Text`:return h}return null}function x(e={}){let t=_;if(e.base){if(e.base.language.name!=`html`||!(e.base.language instanceof a))throw RangeError(`The base option must be the result of calling html(...)`);t=e.base}return new o(t.language==_.language?y:v(t.language),[t.support,t.language.data.of({closeBrackets:{brackets:[`{`,`"`]}})])}export{x as vue};
1
+ import{L as e,R as t,Ut as n,i as r,r as i,s as a,u as o}from"./dist-CYzOYk_e.js";import{n as s}from"./dist-C0ve4Tka.js";import{r as c}from"./dist-CDUZdAAS.js";var l=i.deserialize({version:14,states:"%pOVOWOOObQPOOOpOSO'#C_OOOO'#Cp'#CpQVOWOOQxQPOOO!TQQOOQ!YQPOOOOOO,58y,58yO!_OSO,58yOOOO-E6n-E6nO!dQQO'#CqQ{QPOOO!iQPOOQ{QPOOO!qQPOOOOOO1G.e1G.eOOQO,59],59]OOQO-E6o-E6oO!yOpO'#CiO#RO`O'#CiQOQPOOO#ZO#tO'#CmO#fO!bO'#CmOOQO,59T,59TO#qOpO,59TO#vO`O,59TOOOO'#Cr'#CrO#{O#tO,59XOOQO,59X,59XOOOO'#Cs'#CsO$WO!bO,59XOOQO1G.o1G.oOOOO-E6p-E6pOOQO1G.s1G.sOOOO-E6q-E6q",stateData:"$g~OjOS~OQROUROkQO~OWTOXUOZUO`VO~OSXOTWO~OXUO[]OlZO~OY^O~O[_O~OT`O~OYaO~OmcOodO~OmfOogO~O^iOnhO~O_jOphO~ObkOqkOrmO~OcnOsnOtmO~OnpO~OppO~ObkOqkOrrO~OcnOsnOtrO~OWX`~",goto:`!^hPPPiPPPPPPPPPmPPPpPPsy!Q!WTROSRe]Re_QSORYSS[T^Rb[QlfRqlQogRso`,nodeNames:`⚠ Content Text Interpolation InterpolationContent }} Entity Attribute VueAttributeName : Identifier @ Is ScriptAttributeValue AttributeScript AttributeScript AttributeName AttributeValue Entity Entity`,maxTerm:36,nodeProps:[[`isolate`,-3,3,13,17,``]],skippedNodes:[0],repeatNodeCount:4,tokenData:"'y~RdXY!aYZ!a]^!apq!ars!rwx!w}!O!|!O!P#t!Q![#y![!]$s!_!`%g!b!c%l!c!}#y#R#S#y#T#j#y#j#k%q#k#o#y%W;'S#y;'S;:j$m<%lO#y~!fSj~XY!aYZ!a]^!apq!a~!wOm~~!|Oo~!b#RX`!b}!O!|!Q![!|![!]!|!c!}!|#R#S!|#T#o!|%W;'S!|;'S;:j#n<%lO!|!b#qP;=`<%l!|~#yOl~%W$QXY#t`!b}!O!|!Q![#y![!]!|!c!}#y#R#S#y#T#o#y%W;'S#y;'S;:j$m<%lO#y%W$pP;=`<%l#y~$zXX~`!b}!O!|!Q![!|![!]!|!c!}!|#R#S!|#T#o!|%W;'S!|;'S;:j#n<%lO!|~%lO[~~%qOZ~%W%xXY#t`!b}!O&e!Q![#y![!]!|!c!}#y#R#S#y#T#o#y%W;'S#y;'S;:j$m<%lO#y!b&jX`!b}!O!|!Q![!|![!]!|!c!}'V#R#S!|#T#o'V%W;'S!|;'S;:j#n<%lO!|!b'^XW!b`!b}!O!|!Q![!|![!]!|!c!}'V#R#S!|#T#o'V%W;'S!|;'S;:j#n<%lO!|",tokenizers:[6,7,new r(`b~RP#q#rU~XP#q#r[~aOT~~`,17,4),new r("!k~RQvwX#o#p!_~^TU~Opmq!]m!^;'Sm;'S;=`!X<%lOm~pUOpmq!]m!]!^!S!^;'Sm;'S;=`!X<%lOm~!XOU~~![P;=`<%lm~!bP#o#p!e~!jOk~~",72,2),new r(`[~RPwxU~ZOp~~`,11,15),new r(`[~RPrsU~ZOn~~`,11,14),new r("!e~RQvwXwx!_~^Tc~Opmq!]m!^;'Sm;'S;=`!X<%lOm~pUOpmq!]m!]!^!S!^;'Sm;'S;=`!X<%lOm~!XOc~~![P;=`<%lm~!dOt~~",66,35),new r("!e~RQrsXvw^~^Or~~cTb~Oprq!]r!^;'Sr;'S;=`!^<%lOr~uUOprq!]r!]!^!X!^;'Sr;'S;=`!^<%lOr~!^Ob~~!aP;=`<%lr~",66,33)],topRules:{Content:[0,1],Attribute:[1,7]},tokenPrec:157}),u=c.parser.configure({top:`SingleExpression`}),d=l.configure({props:[e({Text:t.content,Is:t.definitionOperator,AttributeName:t.attributeName,VueAttributeName:t.keyword,Identifier:t.variableName,"AttributeValue ScriptAttributeValue":t.attributeValue,Entity:t.character,"{{ }}":t.brace,"@ :":t.punctuation})]}),f={parser:u},p=d.configure({wrap:n((e,t)=>e.name==`InterpolationContent`?f:null)}),m=d.configure({wrap:n((e,t)=>e.name==`AttributeScript`?f:null),top:`Attribute`}),h={parser:p},g={parser:m},_=s();function v(e){return e.configure({dialect:`selfClosing`,wrap:n(b)},`vue`)}var y=v(_.language);function b(e,t){switch(e.name){case`Attribute`:return/^(@|:|v-)/.test(t.read(e.from,e.from+2))?g:null;case`Text`:return h}return null}function x(e={}){let t=_;if(e.base){if(e.base.language.name!=`html`||!(e.base.language instanceof a))throw RangeError(`The base option must be the result of calling html(...)`);t=e.base}return new o(t.language==_.language?y:v(t.language),[t.support,t.language.data.of({closeBrackets:{brackets:[`{`,`"`]}})])}export{x as vue};