@netnodeag/kraftwerk 0.80.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 (84) hide show
  1. package/README.md +57 -2
  2. package/dist/cli/boards.d.ts +2 -0
  3. package/dist/cli/boards.js +486 -0
  4. package/dist/cli/init.js +4 -2
  5. package/dist/cli/kraftwerk.js +6 -0
  6. package/dist/cli/temp.d.ts +2 -0
  7. package/dist/cli/temp.js +62 -0
  8. package/dist/client/routes.d.ts +16 -0
  9. package/dist/client/routes.js +16 -0
  10. package/dist/config.d.ts +19 -0
  11. package/dist/config.js +24 -1
  12. package/dist/core/attention.d.ts +2 -2
  13. package/dist/core/attention.js +22 -2
  14. package/dist/core/board-wake.d.ts +21 -0
  15. package/dist/core/board-wake.js +221 -0
  16. package/dist/core/boards.d.ts +256 -0
  17. package/dist/core/boards.js +936 -0
  18. package/dist/core/chat/acp.d.ts +1 -1
  19. package/dist/core/chat/acp.js +3 -1
  20. package/dist/core/chat/backend.d.ts +13 -0
  21. package/dist/core/chat/backend.js +9 -1
  22. package/dist/core/chat/context.js +26 -9
  23. package/dist/core/chat/pi.d.ts +1 -1
  24. package/dist/core/chat/pi.js +3 -1
  25. package/dist/core/chat/seats.js +6 -2
  26. package/dist/core/chat/sessions.d.ts +2 -0
  27. package/dist/core/chat/sessions.js +4 -0
  28. package/dist/core/chat/state.js +1 -1
  29. package/dist/core/chat/types.d.ts +11 -1
  30. package/dist/core/files.d.ts +1 -1
  31. package/dist/core/files.js +20 -1
  32. package/dist/core/git.js +3 -1
  33. package/dist/core/notifications.d.ts +1 -1
  34. package/dist/core/projects.js +1 -1
  35. package/dist/core/refs.d.ts +1 -1
  36. package/dist/core/refs.js +25 -1
  37. package/dist/core/search.d.ts +12 -0
  38. package/dist/core/search.js +19 -0
  39. package/dist/core/settings.d.ts +2 -0
  40. package/dist/core/settings.js +13 -1
  41. package/dist/core/temp.d.ts +73 -0
  42. package/dist/core/temp.js +154 -0
  43. package/dist/core/trash.d.ts +1 -1
  44. package/dist/harness.d.ts +2 -0
  45. package/dist/harnesses/acp.js +1 -1
  46. package/dist/harnesses/claude.js +1 -0
  47. package/dist/harnesses/codex.js +1 -0
  48. package/dist/harnesses/pi.js +1 -0
  49. package/dist/run.d.ts +7 -0
  50. package/dist/run.js +13 -1
  51. package/dist/server/api/boards.d.ts +105 -0
  52. package/dist/server/api/boards.js +160 -0
  53. package/dist/server/api/chats.d.ts +2 -0
  54. package/dist/server/api/chats.js +9 -3
  55. package/dist/server/api/files.d.ts +9 -1
  56. package/dist/server/api/files.js +13 -0
  57. package/dist/server/api/index.d.ts +113 -2
  58. package/dist/server/api/index.js +2 -0
  59. package/dist/server/api/workspace.d.ts +5 -0
  60. package/dist/server/api/workspace.js +2 -0
  61. package/dist/server/push.js +1 -1
  62. package/dist/server/server.js +3 -0
  63. package/dist/yaml.js +1 -0
  64. package/package.json +1 -1
  65. package/schema/kraftwerk.schema.json +39 -0
  66. package/web/dist/assets/{dist-DlZPeneT.js → dist-BBFUIftm.js} +1 -1
  67. package/web/dist/assets/{dist-u_oAKIhZ.js → dist-Bie1ebP9.js} +1 -1
  68. package/web/dist/assets/{dist-NX4_yvho.js → dist-BuwdjX6z.js} +1 -1
  69. package/web/dist/assets/{dist-1PajaYPg.js → dist-C0ve4Tka.js} +1 -1
  70. package/web/dist/assets/{dist-DgKgeYFJ.js → dist-C16bXf8c.js} +1 -1
  71. package/web/dist/assets/{dist-DKduisGW.js → dist-CDUZdAAS.js} +1 -1
  72. package/web/dist/assets/{dist-B4dqwo7_.js → dist-CH73jsgO.js} +1 -1
  73. package/web/dist/assets/{dist-D-f0nszo.js → dist-C_rJjRj7.js} +1 -1
  74. package/web/dist/assets/dist-DYICNp09.js +1 -0
  75. package/web/dist/assets/{dist-K_3Y5Nbl.js → dist-DtUB2Uu2.js} +1 -1
  76. package/web/dist/assets/{dist-CK2u1OXb.js → dist-DvkrCSGL.js} +1 -1
  77. package/web/dist/assets/{editor-CE9TlKkJ.js → editor-3z92i0tV.js} +3 -3
  78. package/web/dist/assets/{editor-CZMrhTTL.css → editor-C0_Snnzk.css} +1 -1
  79. package/web/dist/assets/index-DRhNELK_.js +232 -0
  80. package/web/dist/assets/index-unGEux3Y.css +2 -0
  81. package/web/dist/index.html +2 -2
  82. package/web/dist/assets/dist-Bp1iMuQ7.js +0 -1
  83. package/web/dist/assets/index-Dvt-vE1h.css +0 -2
  84. package/web/dist/assets/index-IAuwD22j.js +0 -231
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The temp folder: where a session's scratch work goes — intermediate
3
+ * results, downloads, build output, a markdown written for a CLI to read —
4
+ * instead of the system's temp dir or, worse, the workspace's files.
5
+ *
6
+ * kraftwerk-data/temp/<session-id>/ one folder per chat session
7
+ * <run dir>/temp/ a workflow run's (inside its mount)
8
+ *
9
+ * Three things make it stick, so it does not depend on an agent reading a
10
+ * sentence: the folder is handed to every spawned agent as TMPDIR (and
11
+ * TMP, TEMP, KRAFTWERK_TEMP), so mktemp, Python's tempfile, Node's
12
+ * os.tmpdir() and the agent's own scratchpad land there by themselves; it
13
+ * sits inside the workspace, so writes there need no approval from the
14
+ * harness; and the session prompt names it next to the files folders,
15
+ * which say in turn that nothing temporary belongs in them.
16
+ *
17
+ * The temp root is local: a .gitignore of its own keeps everything in it
18
+ * out of the workspace git (and `kraftwerk init` lists it too). Nothing in
19
+ * it is a deliverable, so nothing goes to the trash: a chat's folder is
20
+ * removed with the chat, and folders nobody touched for SWEEP_AFTER_MS are
21
+ * swept when a session starts (at most once an hour) or on demand
22
+ * (`kraftwerk temp clean`, DELETE /api/temp).
23
+ */
24
+ export declare const TEMP_DIR = "kraftwerk-data/temp";
25
+ /** A folder idle this long is swept. */
26
+ export declare const SWEEP_AFTER_MS: number;
27
+ export interface TempEntry {
28
+ /** The folder name: the session's id. */
29
+ id: string;
30
+ /** Relative to the workspace root, forward slashes. */
31
+ path: string;
32
+ files: number;
33
+ bytes: number;
34
+ /** The newest change inside (the folder's own when empty). */
35
+ modifiedAt: string;
36
+ }
37
+ export interface TempView {
38
+ root: string;
39
+ entries: TempEntry[];
40
+ bytes: number;
41
+ }
42
+ /** Absolute temp root of the current workspace. */
43
+ export declare function tempRoot(): Promise<string>;
44
+ /** The environment that points a process at `dir` for everything temporary. */
45
+ export declare function tempEnv(dir: string): Record<string, string>;
46
+ /** Create a folder outside the workspace's temp root (a workflow run's) and return the env for it. */
47
+ export declare function tempEnvFor(dir: string): Promise<Record<string, string>>;
48
+ /**
49
+ * A session's own folder, created: absolute path and the path relative to
50
+ * the workspace root (for the prompt). Starting a session is also when
51
+ * folders nobody uses any more are swept, at most once an hour.
52
+ */
53
+ export declare function sessionTemp(id: string): Promise<{
54
+ abs: string;
55
+ rel: string;
56
+ env: Record<string, string>;
57
+ }>;
58
+ /** Remove one session's folder for good (nothing there is a deliverable). */
59
+ export declare function removeSessionTemp(id: string): Promise<void>;
60
+ /** Every session folder in the temp root, newest first. */
61
+ export declare function listTemp(): Promise<TempView>;
62
+ /**
63
+ * Remove session folders: all of them, or those idle longer than
64
+ * `olderThanMs` (default SWEEP_AFTER_MS). `keep` names folders that stay
65
+ * whatever their age (the session being started). Returns what went.
66
+ */
67
+ export declare function cleanTemp(opts?: {
68
+ all?: boolean;
69
+ olderThanMs?: number;
70
+ keep?: string[];
71
+ }): Promise<TempEntry[]>;
72
+ /** The "## Temporary files" block of a session's context. */
73
+ export declare function tempContext(rel: string): string;
@@ -0,0 +1,154 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveWorkspace } from "../config.js";
4
+ import { workspaceRoot } from "./context.js";
5
+ import { perWorkspace } from "./workspace.js";
6
+ /**
7
+ * The temp folder: where a session's scratch work goes — intermediate
8
+ * results, downloads, build output, a markdown written for a CLI to read —
9
+ * instead of the system's temp dir or, worse, the workspace's files.
10
+ *
11
+ * kraftwerk-data/temp/<session-id>/ one folder per chat session
12
+ * <run dir>/temp/ a workflow run's (inside its mount)
13
+ *
14
+ * Three things make it stick, so it does not depend on an agent reading a
15
+ * sentence: the folder is handed to every spawned agent as TMPDIR (and
16
+ * TMP, TEMP, KRAFTWERK_TEMP), so mktemp, Python's tempfile, Node's
17
+ * os.tmpdir() and the agent's own scratchpad land there by themselves; it
18
+ * sits inside the workspace, so writes there need no approval from the
19
+ * harness; and the session prompt names it next to the files folders,
20
+ * which say in turn that nothing temporary belongs in them.
21
+ *
22
+ * The temp root is local: a .gitignore of its own keeps everything in it
23
+ * out of the workspace git (and `kraftwerk init` lists it too). Nothing in
24
+ * it is a deliverable, so nothing goes to the trash: a chat's folder is
25
+ * removed with the chat, and folders nobody touched for SWEEP_AFTER_MS are
26
+ * swept when a session starts (at most once an hour) or on demand
27
+ * (`kraftwerk temp clean`, DELETE /api/temp).
28
+ */
29
+ export const TEMP_DIR = "kraftwerk-data/temp";
30
+ /** A folder idle this long is swept. */
31
+ export const SWEEP_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
32
+ const SWEEP_EVERY_MS = 60 * 60 * 1000;
33
+ /** A session id as a folder name: what newChatId() produces, nothing that could leave the root. */
34
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,120}$/;
35
+ async function resolvedRoot() {
36
+ const ws = await resolveWorkspace(workspaceRoot()).catch(() => null);
37
+ return ws?.root ?? workspaceRoot();
38
+ }
39
+ /** Absolute temp root of the current workspace. */
40
+ export async function tempRoot() {
41
+ return path.join(await resolvedRoot(), TEMP_DIR);
42
+ }
43
+ /** Create the root and keep it out of the workspace git: a .gitignore of its own ignores everything in it, itself included. */
44
+ async function prepare(root) {
45
+ await fs.mkdir(root, { recursive: true });
46
+ await fs.writeFile(path.join(root, ".gitignore"), "*\n", { flag: "wx" }).catch(() => { });
47
+ }
48
+ /** The environment that points a process at `dir` for everything temporary. */
49
+ export function tempEnv(dir) {
50
+ return { KRAFTWERK_TEMP: dir, TMPDIR: dir, TMP: dir, TEMP: dir };
51
+ }
52
+ /** Create a folder outside the workspace's temp root (a workflow run's) and return the env for it. */
53
+ export async function tempEnvFor(dir) {
54
+ await fs.mkdir(dir, { recursive: true });
55
+ return tempEnv(dir);
56
+ }
57
+ const lastSweep = perWorkspace(() => ({ at: 0 }));
58
+ /**
59
+ * A session's own folder, created: absolute path and the path relative to
60
+ * the workspace root (for the prompt). Starting a session is also when
61
+ * folders nobody uses any more are swept, at most once an hour.
62
+ */
63
+ export async function sessionTemp(id) {
64
+ if (!ID_RE.test(id))
65
+ throw new Error("invalid session id");
66
+ const root = await tempRoot();
67
+ await prepare(root);
68
+ const abs = path.join(root, id);
69
+ await fs.mkdir(abs, { recursive: true });
70
+ const sweep = lastSweep();
71
+ if (Date.now() - sweep.at > SWEEP_EVERY_MS) {
72
+ sweep.at = Date.now();
73
+ void cleanTemp({ keep: [id] }).catch(() => { });
74
+ }
75
+ return { abs, rel: `${TEMP_DIR}/${id}`, env: tempEnv(abs) };
76
+ }
77
+ /** Remove one session's folder for good (nothing there is a deliverable). */
78
+ export async function removeSessionTemp(id) {
79
+ if (!ID_RE.test(id))
80
+ return;
81
+ await fs.rm(path.join(await tempRoot(), id), { recursive: true, force: true });
82
+ }
83
+ /** Files, bytes and the newest mtime below a folder. */
84
+ async function measure(dir) {
85
+ const st = await fs.stat(dir);
86
+ const out = { files: 0, bytes: 0, newest: st.mtimeMs };
87
+ const walk = async (d) => {
88
+ if (out.files > 20000)
89
+ return;
90
+ for (const k of await fs.readdir(d, { withFileTypes: true }).catch(() => [])) {
91
+ const abs = path.join(d, k.name);
92
+ if (k.isDirectory())
93
+ await walk(abs);
94
+ else if (k.isFile()) {
95
+ const s = await fs.stat(abs).catch(() => null);
96
+ if (!s)
97
+ continue;
98
+ out.files++;
99
+ out.bytes += s.size;
100
+ if (s.mtimeMs > out.newest)
101
+ out.newest = s.mtimeMs;
102
+ }
103
+ }
104
+ };
105
+ await walk(dir);
106
+ return out;
107
+ }
108
+ /** Every session folder in the temp root, newest first. */
109
+ export async function listTemp() {
110
+ const root = await tempRoot();
111
+ const kids = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
112
+ const entries = [];
113
+ for (const k of kids) {
114
+ if (!k.isDirectory() || k.name.startsWith("."))
115
+ continue;
116
+ const m = await measure(path.join(root, k.name)).catch(() => null);
117
+ if (!m)
118
+ continue;
119
+ entries.push({ id: k.name, path: `${TEMP_DIR}/${k.name}`, files: m.files, bytes: m.bytes, modifiedAt: new Date(m.newest).toISOString() });
120
+ }
121
+ entries.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
122
+ return { root, entries, bytes: entries.reduce((n, e) => n + e.bytes, 0) };
123
+ }
124
+ /**
125
+ * Remove session folders: all of them, or those idle longer than
126
+ * `olderThanMs` (default SWEEP_AFTER_MS). `keep` names folders that stay
127
+ * whatever their age (the session being started). Returns what went.
128
+ */
129
+ export async function cleanTemp(opts = {}) {
130
+ const view = await listTemp();
131
+ const cutoff = Date.now() - (opts.olderThanMs ?? SWEEP_AFTER_MS);
132
+ const keep = new Set(opts.keep ?? []);
133
+ const gone = [];
134
+ for (const e of view.entries) {
135
+ if (keep.has(e.id))
136
+ continue;
137
+ if (!opts.all && Date.parse(e.modifiedAt) > cutoff)
138
+ continue;
139
+ await fs.rm(path.join(view.root, e.id), { recursive: true, force: true });
140
+ gone.push(e);
141
+ }
142
+ return gone;
143
+ }
144
+ /** The "## Temporary files" block of a session's context. */
145
+ export function tempContext(rel) {
146
+ return (`## Temporary files\n` +
147
+ `Your scratch folder is ${rel}/ — TMPDIR (and KRAFTWERK_TEMP) point there, so \`mktemp\`, Python's tempfile, ` +
148
+ `os.tmpdir() and your own scratchpad land in it by themselves. Put everything that is not a result there: ` +
149
+ `intermediate output, downloads, extracted archives, build output, one-off scripts, a markdown written for a CLI ` +
150
+ `to read. It is private to this session, kept out of git, and removed when the session is deleted or after a week ` +
151
+ `unused — never store anything there you or the user want to keep. The files folders named above hold only what ` +
152
+ `the user keeps: what you produce for them, and the material they gave you. Never create temporary or ` +
153
+ `intermediate files there, and never in the system's temp dir.`);
154
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export declare const TRASH_DIR = "kraftwerk-data/trash";
10
10
  /** What can be in the trash; also the folder under trash/ it lands in. */
11
- export type TrashKind = "agents" | "agent-skills" | "projects" | "contacts" | "places" | "events" | "workflows" | "knowledge" | "vibeables" | "channels" | "repos" | "chats" | "runs" | "files";
11
+ export type TrashKind = "agents" | "agent-skills" | "projects" | "boards" | "tasks" | "contacts" | "places" | "events" | "workflows" | "knowledge" | "vibeables" | "channels" | "repos" | "chats" | "runs" | "files";
12
12
  export interface TrashEntry {
13
13
  /** Path inside the trash, forward slashes — the entry's handle. */
14
14
  id: string;
package/dist/harness.d.ts CHANGED
@@ -47,6 +47,8 @@ export interface AgentInvocation {
47
47
  clis?: string[];
48
48
  /** MCP servers available in this phase, keyed by server name. */
49
49
  mcpServers?: Record<string, McpServerConfig>;
50
+ /** Extra environment for the agent process (the run's temp folder as TMPDIR, see core/temp.ts); on top of the workspace's. */
51
+ env?: Record<string, string>;
50
52
  resume?: string;
51
53
  onToolUse?: (tool: string, target: string) => void;
52
54
  onText?: (text: string) => void;
@@ -83,7 +83,7 @@ async function open(agent, inv, resume) {
83
83
  };
84
84
  const { child, conn } = await connectAcp(agent, {
85
85
  cwd: inv.cwd,
86
- env: adapterEnv(agent, { model: inv.model, effort: inv.effort }),
86
+ env: { ...adapterEnv(agent, { model: inv.model, effort: inv.effort }), ...inv.env },
87
87
  client,
88
88
  clientName: "kraftwerk",
89
89
  onError: (err) => end(`could not start the ${agent} adapter: ${err.message}`),
@@ -38,6 +38,7 @@ function invokeClaude(inv) {
38
38
  const child = spawn("claude", args, {
39
39
  cwd: inv.cwd,
40
40
  stdio: ["pipe", "pipe", "pipe"],
41
+ env: { ...process.env, ...inv.env },
41
42
  });
42
43
  child.stdin.write(inv.prompt);
43
44
  child.stdin.end();
@@ -62,6 +62,7 @@ function invokeCodex(inv) {
62
62
  const child = spawn("codex", args, {
63
63
  cwd: inv.cwd,
64
64
  stdio: ["pipe", "pipe", "pipe"],
65
+ env: { ...process.env, ...inv.env },
65
66
  });
66
67
  child.stdin.write(prompt);
67
68
  child.stdin.end();
@@ -64,6 +64,7 @@ function invokePi(inv) {
64
64
  const child = spawn("pi", args, {
65
65
  cwd: inv.cwd,
66
66
  stdio: ["pipe", "pipe", "pipe"],
67
+ env: { ...process.env, ...inv.env },
67
68
  });
68
69
  child.stdin.write(inv.prompt);
69
70
  child.stdin.end();
package/dist/run.d.ts CHANGED
@@ -39,6 +39,13 @@ export declare class Run {
39
39
  private readonly verbose;
40
40
  private readonly maxGateRetries;
41
41
  constructor(options: RunOptions);
42
+ /**
43
+ * The run's scratch folder, <run dir>/temp, as TMPDIR for every phase
44
+ * (core/temp.ts): inside the run dir so a sandboxed run finds it in its
45
+ * mount, created when the first phase starts.
46
+ */
47
+ private temp?;
48
+ private tempEnv;
42
49
  trace(event: string, data?: Record<string, unknown>): Promise<void>;
43
50
  agentPhase(params: {
44
51
  name: string;
package/dist/run.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { harnessFor } from "./harnesses/registry.js";
5
5
  import { correctionPrompt, parseEnvelope } from "./envelope.js";
6
6
  import { fmtDuration, phaseStatsLine, summaryTable } from "./stats.js";
7
+ import { tempEnvFor } from "./core/temp.js";
7
8
  export class Run {
8
9
  /** Session per harness AND protocol: a CLI session id means nothing to the ACP adapter and vice versa. */
9
10
  sessions = new Map();
@@ -19,6 +20,15 @@ export class Run {
19
20
  this.verbose = options.verbose ?? false;
20
21
  this.maxGateRetries = options.maxGateRetries ?? 2;
21
22
  }
23
+ /**
24
+ * The run's scratch folder, <run dir>/temp, as TMPDIR for every phase
25
+ * (core/temp.ts): inside the run dir so a sandboxed run finds it in its
26
+ * mount, created when the first phase starts.
27
+ */
28
+ temp;
29
+ tempEnv() {
30
+ return (this.temp ??= tempEnvFor(path.join(this.runDir, "temp")));
31
+ }
22
32
  async trace(event, data = {}) {
23
33
  await appendFile(path.join(this.runDir, "trace.jsonl"), JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + "\n");
24
34
  }
@@ -78,6 +88,7 @@ export class Run {
78
88
  tools: agent.tools,
79
89
  clis: cliEntries.map(([name]) => name),
80
90
  mcpServers: agent.mcp,
91
+ env: await this.tempEnv(),
81
92
  resume: this.sessions.get(sessionKey),
82
93
  onToolUse: (tool, target) => {
83
94
  console.log(` ⚙ ${tool} ${target}`.trimEnd());
@@ -160,10 +171,11 @@ export class Run {
160
171
  console.log(`\n▸ Phase "${params.name}" — script (bash)`);
161
172
  await this.trace("phase_start", { phase: params.name, kind: "script" });
162
173
  const started = Date.now();
174
+ const temp = await this.tempEnv();
163
175
  const result = await new Promise((resolve, reject) => {
164
176
  const child = spawn("bash", ["-c", params.script], {
165
177
  cwd: this.runDir,
166
- env: { ...process.env, RUN_DIR: this.runDir, PHASE: params.name, ...params.env },
178
+ env: { ...process.env, ...temp, RUN_DIR: this.runDir, PHASE: params.name, ...params.env },
167
179
  });
168
180
  let stdout = "";
169
181
  let stderr = "";
@@ -0,0 +1,105 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * Boards and tasks over the API. A board is addressed by its slug and, for
4
+ * a project's board, `?project=<slug>`; a task by its folder name or its
5
+ * bare number. Writes that change a task's column or assignee are logged
6
+ * on the task and may wake the assignee (core/board-wake.ts).
7
+ */
8
+ export declare const boardErrors: (message: string) => number;
9
+ export declare const boardRoutes: (import("./router.js").Route<"boards.create", import("./router.js").Reply<import("../../core/boards.js").BoardDetail>, "/api/boards", z.ZodObject<{
10
+ title: z.ZodString;
11
+ slug: z.ZodOptional<z.ZodString>;
12
+ project: z.ZodOptional<z.ZodString>;
13
+ columns: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
14
+ slug: z.ZodString;
15
+ label: z.ZodOptional<z.ZodString>;
16
+ limit: z.ZodOptional<z.ZodNumber>;
17
+ done: z.ZodOptional<z.ZodBoolean>;
18
+ review: z.ZodOptional<z.ZodBoolean>;
19
+ }, z.core.$strip>]>>>;
20
+ agent: z.ZodOptional<z.ZodString>;
21
+ notes: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strip>> | import("./router.js").Route<"boards.delete", {
23
+ ok: boolean;
24
+ }, "/api/boards/:board", undefined> | import("./router.js").Route<"boards.ensure", import("../../core/boards.js").BoardDetail, "/api/boards/default", z.ZodObject<{
25
+ project: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>> | import("./router.js").Route<"boards.get", import("../../core/boards.js").BoardDetail, "/api/boards/:board", undefined> | import("./router.js").Route<"boards.list", import("../../core/boards.js").BoardsView, "/api/boards", undefined> | import("./router.js").Route<"boards.save", import("../../core/boards.js").BoardDetail, "/api/boards/:board", z.ZodObject<{
27
+ title: z.ZodOptional<z.ZodString>;
28
+ columns: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
29
+ slug: z.ZodString;
30
+ label: z.ZodOptional<z.ZodString>;
31
+ limit: z.ZodOptional<z.ZodNumber>;
32
+ done: z.ZodOptional<z.ZodBoolean>;
33
+ review: z.ZodOptional<z.ZodBoolean>;
34
+ }, z.core.$strip>]>>>;
35
+ agent: z.ZodOptional<z.ZodNullable<z.ZodString>>;
36
+ staleAfter: z.ZodOptional<z.ZodNullable<z.ZodString>>;
37
+ notes: z.ZodOptional<z.ZodString>;
38
+ }, z.core.$strip>> | import("./router.js").Route<"tasks.assign", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task/assign", z.ZodObject<{
39
+ to: z.ZodNullable<z.ZodString>;
40
+ message: z.ZodOptional<z.ZodString>;
41
+ actor: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>> | import("./router.js").Route<"tasks.create", import("./router.js").Reply<import("../../core/boards.js").TaskDetail>, "/api/boards/:board/tasks", z.ZodObject<{
43
+ status: z.ZodOptional<z.ZodString>;
44
+ order: z.ZodOptional<z.ZodNumber>;
45
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
+ due: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
48
+ refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
49
+ notes: z.ZodOptional<z.ZodString>;
50
+ actor: z.ZodOptional<z.ZodString>;
51
+ message: z.ZodOptional<z.ZodString>;
52
+ title: z.ZodString;
53
+ }, z.core.$strip>> | import("./router.js").Route<"tasks.delete", {
54
+ ok: boolean;
55
+ }, "/api/boards/:board/tasks/:task", undefined> | import("./router.js").Route<"tasks.get", {
56
+ slug: string;
57
+ number: number;
58
+ title: string;
59
+ status: string;
60
+ order: number;
61
+ assignee?: string;
62
+ due?: string;
63
+ created: string;
64
+ tags: string[];
65
+ refs: string[];
66
+ extra?: Record<string, unknown>;
67
+ ref: string;
68
+ path: string;
69
+ updatedAt?: string;
70
+ configError?: string;
71
+ files: number;
72
+ lastLog?: string;
73
+ stale?: boolean;
74
+ notes: string;
75
+ log: string;
76
+ fileList: string[];
77
+ board: string;
78
+ project?: string;
79
+ sessions: import("../../core/board-wake.js").TaskSession[];
80
+ }, "/api/boards/:board/tasks/:task", undefined> | import("./router.js").Route<"tasks.log", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task/log", z.ZodObject<{
81
+ entry: z.ZodString;
82
+ actor: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$strip>> | import("./router.js").Route<"tasks.move", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task/move", z.ZodObject<{
84
+ status: z.ZodString;
85
+ before: z.ZodOptional<z.ZodString>;
86
+ order: z.ZodOptional<z.ZodNumber>;
87
+ message: z.ZodOptional<z.ZodString>;
88
+ actor: z.ZodOptional<z.ZodString>;
89
+ }, z.core.$strip>> | import("./router.js").Route<"tasks.pick", {
90
+ task: import("../../core/boards.js").TaskDetail | null;
91
+ }, "/api/boards/:board/pick", z.ZodObject<{
92
+ agent: z.ZodString;
93
+ column: z.ZodOptional<z.ZodString>;
94
+ }, z.core.$strip>> | import("./router.js").Route<"tasks.save", import("../../core/boards.js").TaskDetail, "/api/boards/:board/tasks/:task", z.ZodObject<{
95
+ title: z.ZodOptional<z.ZodString>;
96
+ status: z.ZodOptional<z.ZodString>;
97
+ order: z.ZodOptional<z.ZodNumber>;
98
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodString>>;
99
+ due: z.ZodOptional<z.ZodNullable<z.ZodString>>;
100
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
101
+ refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
102
+ notes: z.ZodOptional<z.ZodString>;
103
+ actor: z.ZodOptional<z.ZodString>;
104
+ message: z.ZodOptional<z.ZodString>;
105
+ }, z.core.$strip>>)[];
@@ -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<{