@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.
- package/README.md +52 -0
- package/dist/cli/boards.d.ts +2 -0
- package/dist/cli/boards.js +486 -0
- package/dist/cli/init.js +1 -0
- package/dist/cli/kraftwerk.js +3 -0
- package/dist/client/routes.d.ts +14 -0
- package/dist/client/routes.js +14 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +24 -1
- package/dist/core/attention.d.ts +2 -2
- package/dist/core/attention.js +22 -2
- package/dist/core/board-wake.d.ts +21 -0
- package/dist/core/board-wake.js +221 -0
- package/dist/core/boards.d.ts +256 -0
- package/dist/core/boards.js +936 -0
- package/dist/core/chat/acp.d.ts +1 -1
- package/dist/core/chat/acp.js +2 -1
- package/dist/core/chat/backend.d.ts +11 -0
- package/dist/core/chat/backend.js +9 -1
- package/dist/core/chat/context.js +16 -4
- package/dist/core/chat/pi.d.ts +1 -1
- package/dist/core/chat/pi.js +2 -1
- package/dist/core/chat/seats.js +3 -2
- package/dist/core/chat/sessions.d.ts +2 -0
- package/dist/core/chat/sessions.js +1 -0
- package/dist/core/chat/state.js +1 -1
- package/dist/core/chat/types.d.ts +11 -1
- package/dist/core/files.d.ts +1 -1
- package/dist/core/files.js +19 -1
- package/dist/core/git.js +3 -1
- package/dist/core/notifications.d.ts +1 -1
- package/dist/core/refs.d.ts +1 -1
- package/dist/core/refs.js +25 -1
- package/dist/core/search.d.ts +12 -0
- package/dist/core/search.js +19 -0
- package/dist/core/settings.d.ts +2 -0
- package/dist/core/settings.js +13 -1
- package/dist/core/trash.d.ts +1 -1
- package/dist/server/api/boards.d.ts +105 -0
- package/dist/server/api/boards.js +160 -0
- package/dist/server/api/chats.d.ts +2 -0
- package/dist/server/api/chats.js +9 -3
- package/dist/server/api/index.d.ts +105 -2
- package/dist/server/api/index.js +2 -0
- package/dist/server/api/workspace.d.ts +5 -0
- package/dist/server/api/workspace.js +2 -0
- package/dist/server/push.js +1 -1
- package/dist/server/server.js +3 -0
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +39 -0
- package/web/dist/assets/{dist-Dx3aajX1.js → dist-BBFUIftm.js} +1 -1
- package/web/dist/assets/{dist-ENZYo2jZ.js → dist-Bie1ebP9.js} +1 -1
- package/web/dist/assets/{dist-BiU4F98O.js → dist-BuwdjX6z.js} +1 -1
- package/web/dist/assets/{dist-8IOFzZx-.js → dist-C0ve4Tka.js} +1 -1
- package/web/dist/assets/{dist-FfKYtQxT.js → dist-C16bXf8c.js} +1 -1
- package/web/dist/assets/{dist-cewmjxFi.js → dist-CDUZdAAS.js} +1 -1
- package/web/dist/assets/{dist-BK0VhAPQ.js → dist-CH73jsgO.js} +1 -1
- package/web/dist/assets/{dist-Dy1SvhKC.js → dist-C_rJjRj7.js} +1 -1
- package/web/dist/assets/dist-DYICNp09.js +1 -0
- package/web/dist/assets/{dist-AhnSw4hy.js → dist-DtUB2Uu2.js} +1 -1
- package/web/dist/assets/{dist-Ba6WSVOB.js → dist-DvkrCSGL.js} +1 -1
- package/web/dist/assets/{editor-CaRa1NMC.js → editor-3z92i0tV.js} +3 -3
- package/web/dist/assets/{editor-CZMrhTTL.css → editor-C0_Snnzk.css} +1 -1
- package/web/dist/assets/index-DRhNELK_.js +232 -0
- package/web/dist/assets/index-unGEux3Y.css +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/dist-lgUK2Fjt.js +0 -1
- package/web/dist/assets/index-Dpzux3OC.js +0 -231
- package/web/dist/assets/index-Dvt-vE1h.css +0 -2
package/dist/core/chat/acp.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ContentBlock, type SessionConfigOption, type SessionNotification } from "@agentclientprotocol/sdk";
|
|
2
2
|
import { type AcpAgent, type AcpExtensionNotification } from "../../acp.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type BackendHooks, type BackendTuning, type ChatBackend, type PromptFile } from "./backend.js";
|
|
4
4
|
import type { AuthStatus, ChatEvent, ConfigOption, ElicitationField, SessionFailure } from "./types.js";
|
|
5
5
|
/** The session failure a `session_info_update` carries, if it is one (AIR `sessionFailure` in `_meta`). */
|
|
6
6
|
export declare function failureOf(meta: unknown): SessionFailure | undefined;
|
package/dist/core/chat/acp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { RequestError, } from "@agentclientprotocol/sdk";
|
|
5
5
|
import { adapterEnv, airMeta, connectAcp, EXT_METHODS, EXT_NOTIFICATIONS, fileChangeReportMeta, openSession, withAdapter, } from "../../acp.js";
|
|
6
|
+
import { identityEnv } from "./backend.js";
|
|
6
7
|
import { tempEnv } from "../temp.js";
|
|
7
8
|
import { unattendedMode } from "./permissions.js";
|
|
8
9
|
/**
|
|
@@ -390,7 +391,7 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
|
390
391
|
};
|
|
391
392
|
const { child, conn } = await connectAcp(agent, {
|
|
392
393
|
cwd,
|
|
393
|
-
env: { ...adapterEnv(agent, tuning), ...(tuning.temp ? tempEnv(tuning.temp) : {}) },
|
|
394
|
+
env: { ...adapterEnv(agent, tuning), ...(tuning.temp ? tempEnv(tuning.temp) : {}), ...identityEnv(agent, tuning) },
|
|
394
395
|
client,
|
|
395
396
|
clientName: "kraftwerk-inspector",
|
|
396
397
|
elicitation: true,
|
|
@@ -39,6 +39,8 @@ export interface BackendTuning {
|
|
|
39
39
|
addDirs?: string[];
|
|
40
40
|
/** The session's scratch folder (core/temp.ts): handed to the process as TMPDIR and KRAFTWERK_TEMP. */
|
|
41
41
|
temp?: string;
|
|
42
|
+
/** The agent this seat is (its slug), handed to the process as KRAFTWERK_AGENT, with KRAFTWERK_ACTOR = <slug>/<harness>; absent for a project assistant or Ralv. */
|
|
43
|
+
agent?: string;
|
|
42
44
|
/**
|
|
43
45
|
* Nobody is watching this session live (routine runs). The backend selects
|
|
44
46
|
* the harness's own hands-off permission mode — the harness keeps deciding
|
|
@@ -48,6 +50,15 @@ export interface BackendTuning {
|
|
|
48
50
|
/** ACP agents: the session id to continue (from ChatMeta.sessions) instead of opening a new one. */
|
|
49
51
|
resume?: string;
|
|
50
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Who the process is, for the CLI it runs: `kraftwerk tasks pick`, `tasks log`,
|
|
55
|
+
* `journal` and `knowledge put` read KRAFTWERK_AGENT / KRAFTWERK_ACTOR so an
|
|
56
|
+
* agent never has to name itself. A seat that is no agent (a project
|
|
57
|
+
* assistant, Ralv) is the actor `kraftwerk-chat/<harness>` — the name its
|
|
58
|
+
* context already tells it to write with — and has no KRAFTWERK_AGENT, so
|
|
59
|
+
* `tasks pick` still asks who picks.
|
|
60
|
+
*/
|
|
61
|
+
export declare const identityEnv: (harness: string, tuning: BackendTuning) => Record<string, string>;
|
|
51
62
|
/**
|
|
52
63
|
* How a turn ended. `failure` is the typed session failure the agent
|
|
53
64
|
* attached to the turn's own outcome (a usage limit, a rate limit, a
|
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Who the process is, for the CLI it runs: `kraftwerk tasks pick`, `tasks log`,
|
|
3
|
+
* `journal` and `knowledge put` read KRAFTWERK_AGENT / KRAFTWERK_ACTOR so an
|
|
4
|
+
* agent never has to name itself. A seat that is no agent (a project
|
|
5
|
+
* assistant, Ralv) is the actor `kraftwerk-chat/<harness>` — the name its
|
|
6
|
+
* context already tells it to write with — and has no KRAFTWERK_AGENT, so
|
|
7
|
+
* `tasks pick` still asks who picks.
|
|
8
|
+
*/
|
|
9
|
+
export const identityEnv = (harness, tuning) => tuning.agent ? { KRAFTWERK_AGENT: tuning.agent, KRAFTWERK_ACTOR: `${tuning.agent}/${harness}` } : { KRAFTWERK_ACTOR: `kraftwerk-chat/${harness}` };
|
|
@@ -8,6 +8,7 @@ import { listRepos } from "../repos.js";
|
|
|
8
8
|
import { listVibeables, vibeableStatus, VIBEABLE_CONFIG_FILE } from "../vibeables.js";
|
|
9
9
|
import { projectContext } from "../projects.js";
|
|
10
10
|
import { entitiesContext } from "../entities.js";
|
|
11
|
+
import { boardsContext, taskContext } from "../boards.js";
|
|
11
12
|
import { unattendedTimeoutLabel } from "./permissions.js";
|
|
12
13
|
import { getChannel } from "../channels.js";
|
|
13
14
|
import { JOURNAL_FILE, journalExcerpt, readJournal } from "../journal.js";
|
|
@@ -275,8 +276,9 @@ async function baseScopeContext(scope, agent) {
|
|
|
275
276
|
(await agentFilesContext(def)) +
|
|
276
277
|
`The working directory is the project root. Stay within your role; if a request is clearly outside it, ` +
|
|
277
278
|
`say so and suggest which agent or tool fits better.` +
|
|
278
|
-
(scope.
|
|
279
|
-
|
|
279
|
+
(scope.task ? `\n\n${await taskContext(scope.task)}` : "") +
|
|
280
|
+
(scope.routine || scope.task
|
|
281
|
+
? `\n\nThis session was started automatically by ${scope.routine ? `your scheduled routine "${scope.routine}"` : `the board: task ${scope.task} was given to you`} — ` +
|
|
280
282
|
`nobody may be watching live. Complete the task autonomously. Edits inside the project ` +
|
|
281
283
|
`need no approval; anything your harness asks about (shell commands, network, files ` +
|
|
282
284
|
`outside the project) is shown to a human who may look later — say in one line why you ` +
|
|
@@ -394,7 +396,7 @@ export async function scopeContext(meta, seat) {
|
|
|
394
396
|
const agent = seat.harness;
|
|
395
397
|
// The repositories block reads every clone from git, so it runs alongside
|
|
396
398
|
// the rest instead of adding its spawns to the first prompt's latency.
|
|
397
|
-
const [base, repos, vibe, skills, channel, project, entities] = await Promise.all([
|
|
399
|
+
const [base, repos, vibe, skills, channel, project, entities, boards] = await Promise.all([
|
|
398
400
|
baseScopeContext(scope, agent),
|
|
399
401
|
scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
|
|
400
402
|
meta.vibeable ? vibeableContext(meta.vibeable, meta.cwd) : Promise.resolve(""),
|
|
@@ -404,11 +406,21 @@ export async function scopeContext(meta, seat) {
|
|
|
404
406
|
meta.scope.kind === "channel" && meta.project ? projectContext(meta.project, `${seat.key}/${agent}`, { member: true }) : Promise.resolve(""),
|
|
405
407
|
// Who, where and when: the workspace's contacts, places and events, wherever the chat works (a run discusses its own artifacts only).
|
|
406
408
|
scope.kind === "run" ? Promise.resolve("") : entitiesContext().catch(() => ""),
|
|
409
|
+
// Boards: a project chat sees the project's boards; an agent sees the workspace boards narrowed to its tasks, plus its project's when filed under one.
|
|
410
|
+
scope.kind === "project"
|
|
411
|
+
? boardsContext(scope.slug, `kraftwerk-chat/${agent}`).catch(() => "")
|
|
412
|
+
: scope.kind === "agent"
|
|
413
|
+
? Promise.all([boardsContext(undefined, `${scope.slug}/${agent}`, scope.slug), meta.project ? boardsContext(meta.project, `${scope.slug}/${agent}`, scope.slug) : Promise.resolve("")])
|
|
414
|
+
.then((xs) => xs.filter(Boolean).join("\n\n"))
|
|
415
|
+
.catch(() => "")
|
|
416
|
+
: Promise.resolve(""),
|
|
407
417
|
]);
|
|
408
418
|
// Every session has a scratch folder of its own (core/temp.ts), named by its absolute path: a run chat or a
|
|
409
419
|
// vibeable session works in another folder than the workspace root. The block comes after the files blocks it refers to.
|
|
410
420
|
const temp = await sessionTemp(meta.id).catch(() => null);
|
|
411
|
-
|
|
421
|
+
// "Work on this" from a task page: the chat is tagged with the task (meta.task) and reads it like the agent's own task session would.
|
|
422
|
+
const task = meta.task && !(scope.kind === "agent" && scope.task === meta.task) ? await taskContext(meta.task).catch(() => "") : "";
|
|
423
|
+
return [base, channel, project, boards, task, repos, entities, vibe, temp ? tempContext(temp.abs) : "", RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
|
|
412
424
|
}
|
|
413
425
|
/** The channel block a member agent gets once per process: who is here, how turns work, how to hand over. */
|
|
414
426
|
async function channelContext(slug, self) {
|
package/dist/core/chat/pi.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BackendHooks, type BackendTuning, type ChatBackend } from "./backend.js";
|
|
2
2
|
export declare function startPiBackend(cwd: string, hooks: BackendHooks, tuning?: BackendTuning): ChatBackend;
|
package/dist/core/chat/pi.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { identityEnv } from "./backend.js";
|
|
3
4
|
import { workspaceEnv } from "../env.js";
|
|
4
5
|
import { tempEnv } from "../temp.js";
|
|
5
6
|
const clip = (raw) => {
|
|
@@ -25,7 +26,7 @@ export function startPiBackend(cwd, hooks, tuning = {}) {
|
|
|
25
26
|
const proc = spawn("pi", ["-p", "--mode", "json", "--session-id", sessionId, ...tuningArgs], {
|
|
26
27
|
cwd,
|
|
27
28
|
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
-
env: { ...workspaceEnv(), ...(tuning.temp ? tempEnv(tuning.temp) : {}) },
|
|
29
|
+
env: { ...workspaceEnv(), ...(tuning.temp ? tempEnv(tuning.temp) : {}), ...identityEnv("pi", tuning) },
|
|
29
30
|
});
|
|
30
31
|
child = proc;
|
|
31
32
|
proc.stdin.write(text);
|
package/dist/core/chat/seats.js
CHANGED
|
@@ -23,6 +23,7 @@ async function backendTuning(meta, seat) {
|
|
|
23
23
|
const tuning = {};
|
|
24
24
|
const agentSlug = scope.kind === "agent" ? scope.slug : scope.kind === "channel" ? seat.key : undefined;
|
|
25
25
|
if (agentSlug) {
|
|
26
|
+
tuning.agent = agentSlug;
|
|
26
27
|
const def = await getAgent(agentSlug).catch(() => null);
|
|
27
28
|
if (def?.model)
|
|
28
29
|
tuning.model = def.model;
|
|
@@ -106,7 +107,7 @@ export async function ensureBackend(state, seat) {
|
|
|
106
107
|
});
|
|
107
108
|
if (!state.pendingInfo.has(requestId))
|
|
108
109
|
await dismissKey(notifyKey);
|
|
109
|
-
});
|
|
110
|
+
}).catch(() => { });
|
|
110
111
|
if (isUnattended(state.meta.scope)) {
|
|
111
112
|
deadline = setTimeout(() => {
|
|
112
113
|
const pending = state.pendingPermissions.get(requestId);
|
|
@@ -157,7 +158,7 @@ export async function ensureBackend(state, seat) {
|
|
|
157
158
|
});
|
|
158
159
|
if (!state.pendingInfo.has(requestId))
|
|
159
160
|
await dismissKey(notifyKey);
|
|
160
|
-
});
|
|
161
|
+
}).catch(() => { });
|
|
161
162
|
if (isUnattended(state.meta.scope)) {
|
|
162
163
|
deadline = setTimeout(() => {
|
|
163
164
|
const pending = state.pendingElicitations.get(requestId);
|
|
@@ -30,6 +30,8 @@ export declare function createChat(opts: {
|
|
|
30
30
|
resume?: string;
|
|
31
31
|
/** The project an agent's chat works in (meta.project); the caller checks that it exists. */
|
|
32
32
|
project?: string;
|
|
33
|
+
/** The board task the chat works on (meta.task); the caller checks that it exists. */
|
|
34
|
+
task?: string;
|
|
33
35
|
/** Who starts it; default this machine's person. */
|
|
34
36
|
author?: ChatPerson;
|
|
35
37
|
/** A private routine's run: hidden from remote callers (meta.private). */
|
|
@@ -55,6 +55,7 @@ export async function createChat(opts) {
|
|
|
55
55
|
cwd,
|
|
56
56
|
scope: opts.scope,
|
|
57
57
|
...(opts.project && opts.scope.kind === "agent" ? { project: opts.project } : {}),
|
|
58
|
+
...(opts.task ? { task: opts.task } : {}),
|
|
58
59
|
...(opts.resume && opts.agent !== "pi" ? { sessions: { [MAIN]: opts.resume } } : {}),
|
|
59
60
|
author,
|
|
60
61
|
...(opts.private ? { private: true } : {}),
|
package/dist/core/chat/state.js
CHANGED
|
@@ -29,7 +29,7 @@ export const isBusy = (state) => [...state.seats.values()].some((s) => s.busy);
|
|
|
29
29
|
/** The event author for a seat: channel members sign their events, the lone agent of a chat does not. */
|
|
30
30
|
export const seatAuthor = (seat) => (seat.key === MAIN ? undefined : { kind: "agent", slug: seat.key });
|
|
31
31
|
/** Routine-fired sessions: nobody is expected to be watching live. */
|
|
32
|
-
export const isUnattended = (scope) => scope.kind === "agent" && !!scope.routine;
|
|
32
|
+
export const isUnattended = (scope) => scope.kind === "agent" && (!!scope.routine || !!scope.task);
|
|
33
33
|
/** Diagnose handle for a failed routine session (only called for unattended agent scopes). */
|
|
34
34
|
export function routineRef(meta) {
|
|
35
35
|
const scope = meta.scope;
|
|
@@ -16,10 +16,13 @@ export type ChatScope = {
|
|
|
16
16
|
} | {
|
|
17
17
|
kind: "knowledge";
|
|
18
18
|
bundle?: string;
|
|
19
|
-
}
|
|
19
|
+
}
|
|
20
|
+
/** `routine`: a scheduled run; `task`: opened for a task (<project>/<board>/<task>) the agent was assigned — both unattended. */
|
|
21
|
+
| {
|
|
20
22
|
kind: "agent";
|
|
21
23
|
slug: string;
|
|
22
24
|
routine?: string;
|
|
25
|
+
task?: string;
|
|
23
26
|
}
|
|
24
27
|
/** A project: the chat carries its brief, state, systems of record and links; see projects.ts. */
|
|
25
28
|
| {
|
|
@@ -64,6 +67,13 @@ export interface ChatMeta {
|
|
|
64
67
|
* the project and hands every seat the project's context.
|
|
65
68
|
*/
|
|
66
69
|
project?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The board task this chat works on (<project>/<board>/<task> or
|
|
72
|
+
* <board>/<task>) when the scope is not the agent's task session itself:
|
|
73
|
+
* "work on this" from the task page. The task page lists the chat as one
|
|
74
|
+
* of its sessions, and every seat gets the task block in its context.
|
|
75
|
+
*/
|
|
76
|
+
task?: string;
|
|
67
77
|
/**
|
|
68
78
|
* The agent's own session id per seat ("main", or the agent slug in a
|
|
69
79
|
* channel): after a restart or the idle reaper, the next backend resumes
|
package/dist/core/files.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const PROJECT_FILES_DIR = "files";
|
|
|
19
19
|
export declare const LOCAL_OVER_BYTES: number;
|
|
20
20
|
/** Upload cap: a guard against a runaway request, not a policy. */
|
|
21
21
|
export declare const MAX_UPLOAD_BYTES: number;
|
|
22
|
-
/** "workspace"
|
|
22
|
+
/** "workspace", "project:<slug>" or "task:<board>/<task>" / "task:<project>/<board>/<task>" (a task's folder). */
|
|
23
23
|
export type FilesScope = string;
|
|
24
24
|
export interface FileEntry {
|
|
25
25
|
name: string;
|
package/dist/core/files.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { pipeline } from "node:stream/promises";
|
|
4
4
|
import { resolveWorkspace } from "../config.js";
|
|
5
5
|
import { workspaceRoot } from "./context.js";
|
|
6
|
+
import { findTask, noteTaskFile, TASK_FILE, TASK_LOG } from "./boards.js";
|
|
6
7
|
import { listProjects, openProjects, safeProjectSlug } from "./projects.js";
|
|
7
8
|
import { moveToTrash } from "./trash.js";
|
|
8
9
|
/**
|
|
@@ -66,6 +67,13 @@ export async function scopeRoot(scope) {
|
|
|
66
67
|
const abs = path.join(workspace, WORKSPACE_FILES_DIR);
|
|
67
68
|
return { abs, rel: WORKSPACE_FILES_DIR, label: "Workspace", workspace };
|
|
68
69
|
}
|
|
70
|
+
const task = /^task:(.+)$/.exec(scope);
|
|
71
|
+
if (task) {
|
|
72
|
+
const t = await findTask(task[1]);
|
|
73
|
+
if (!t)
|
|
74
|
+
throw new Error(`no task "${task[1]}"`);
|
|
75
|
+
return { abs: t.path, rel: path.relative(workspace, t.path).split(path.sep).join("/"), label: `${t.number} ${t.title}`, workspace };
|
|
76
|
+
}
|
|
69
77
|
const m = /^project:(.+)$/.exec(scope);
|
|
70
78
|
if (!m)
|
|
71
79
|
throw new Error(`invalid files scope "${scope}"`);
|
|
@@ -153,9 +161,13 @@ export async function listFiles(scope, rel = "") {
|
|
|
153
161
|
const local = new Set(await readIgnore(root.abs));
|
|
154
162
|
const kids = st ? await fs.readdir(dir, { withFileTypes: true }) : [];
|
|
155
163
|
const entries = [];
|
|
164
|
+
const taskScope = scope.startsWith("task:");
|
|
156
165
|
for (const k of kids) {
|
|
157
166
|
if (k.name.startsWith("."))
|
|
158
167
|
continue;
|
|
168
|
+
// A task's own markdown and log are the task, not its files.
|
|
169
|
+
if (taskScope && dir === root.abs && (k.name === TASK_FILE || k.name === TASK_LOG))
|
|
170
|
+
continue;
|
|
159
171
|
const abs = path.join(dir, k.name);
|
|
160
172
|
const s = await fs.stat(abs).catch(() => null);
|
|
161
173
|
if (!s)
|
|
@@ -228,6 +240,8 @@ export async function saveUpload(scope, dirRel, name, body) {
|
|
|
228
240
|
const p = toPosix(path.relative(root.abs, abs));
|
|
229
241
|
if (size > LOCAL_OVER_BYTES)
|
|
230
242
|
await markLocal(root.abs, p, true);
|
|
243
|
+
if (scope.startsWith("task:"))
|
|
244
|
+
await noteTaskFile(scope.slice(5), "added", p).catch(() => { });
|
|
231
245
|
return { name: final, path: p, type: "file", size, modifiedAt: new Date().toISOString(), mime: mimeOf(final), ...(size > LOCAL_OVER_BYTES ? { local: true } : {}) };
|
|
232
246
|
}
|
|
233
247
|
export async function makeFolder(scope, rel) {
|
|
@@ -271,10 +285,14 @@ export async function deleteFile(scope, rel) {
|
|
|
271
285
|
if (!st)
|
|
272
286
|
throw new Error(`no file "${rel}"`);
|
|
273
287
|
const p = toPosix(path.relative(root.abs, abs));
|
|
274
|
-
|
|
288
|
+
if (scope.startsWith("task:") && (p === TASK_FILE || p === TASK_LOG))
|
|
289
|
+
throw new Error("the task's own files cannot be removed");
|
|
290
|
+
const owner = scope === "workspace" ? "workspace" : scope.startsWith("task:") ? `tasks/${scope.slice(5)}` : `projects/${scope.slice("project:".length)}`;
|
|
275
291
|
await moveToTrash("files", `${owner}/${p}`, abs);
|
|
276
292
|
if (st.isFile() && st.size > LOCAL_OVER_BYTES)
|
|
277
293
|
await markLocal(root.abs, p, false);
|
|
294
|
+
if (scope.startsWith("task:"))
|
|
295
|
+
await noteTaskFile(scope.slice(5), "removed", p).catch(() => { });
|
|
278
296
|
}
|
|
279
297
|
/* ---------- context for agents ---------- */
|
|
280
298
|
/** "files/ (23 files; recent: designs/home-v3.png 2026-10-05, …)" — where the files are, not every file. */
|
package/dist/core/git.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { ENTITY_KINDS, entitiesRootFor, projectsRootFor, reposRootFor, resolveWorkspace, vibeablesRootFor, isSafeGitName } from "../config.js";
|
|
4
|
+
import { boardsRootFor, ENTITY_KINDS, entitiesRootFor, projectsRootFor, reposRootFor, resolveWorkspace, vibeablesRootFor, isSafeGitName } from "../config.js";
|
|
5
5
|
import { ENV_FILE } from "../runner/docker.js";
|
|
6
6
|
import { getOutputDir, workspaceRoot } from "./context.js";
|
|
7
7
|
import { perWorkspace, workspaceOpen } from "./workspace.js";
|
|
@@ -241,6 +241,8 @@ function scopeFor({ project, repoRoot }) {
|
|
|
241
241
|
projectsRootFor(project),
|
|
242
242
|
// Contacts, places and events: who, where and when — the workspace's records.
|
|
243
243
|
...ENTITY_KINDS.map((kind) => entitiesRootFor(project, kind)),
|
|
244
|
+
// Workspace boards: tasks, logs and their files (project boards sit inside the projects root).
|
|
245
|
+
boardsRootFor(project),
|
|
244
246
|
// Workspace files: shared material, versioned (files over 25 MB ignore themselves, see files.ts).
|
|
245
247
|
path.resolve(project.root, "kraftwerk-data/files"),
|
|
246
248
|
project.configPath,
|
|
@@ -10,7 +10,7 @@ import type { ChatMeta } from "./chat/types.js";
|
|
|
10
10
|
* Approval items carry a `key` so the same request is never listed twice
|
|
11
11
|
* and disappears again the moment someone answers (dismissKey).
|
|
12
12
|
*/
|
|
13
|
-
export type NotificationKind = "approval" | "routine_done" | "routine_failed" | "run_done" | "run_failed";
|
|
13
|
+
export type NotificationKind = "approval" | "routine_done" | "routine_failed" | "run_done" | "run_failed" | "task_woken" | "task_wake_failed";
|
|
14
14
|
/**
|
|
15
15
|
* What a "diagnose" on a failure item should look at: a workflow run (chat
|
|
16
16
|
* in the run folder) or a routine (its agent, and the session that failed
|
package/dist/core/refs.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* like never do. A target that does not exist is reported (found: false),
|
|
14
14
|
* never an error — the same rule project links have always followed.
|
|
15
15
|
*/
|
|
16
|
-
export declare const REF_KIND_NAMES: readonly ["knowledge", "vibeables", "repos", "workflows", "agents", "projects", "contacts", "places", "events"];
|
|
16
|
+
export declare const REF_KIND_NAMES: readonly ["knowledge", "vibeables", "repos", "workflows", "agents", "projects", "contacts", "places", "events", "boards", "tasks"];
|
|
17
17
|
/** The kind's plural: the link-list name in project.yml and the folder/route name. */
|
|
18
18
|
export type RefKind = (typeof REF_KIND_NAMES)[number];
|
|
19
19
|
/** One existing target. */
|
package/dist/core/refs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listAgents } from "./agents.js";
|
|
2
2
|
import { CONTACTS, EVENTS, listEntities, PLACES } from "./entities.js";
|
|
3
|
+
import { boardKey, listAllBoards } from "./boards.js";
|
|
3
4
|
import { knowledgeIndex } from "./knowledge.js";
|
|
4
5
|
import { listProjects } from "./projects.js";
|
|
5
6
|
import { listRepos } from "./repos.js";
|
|
@@ -20,7 +21,7 @@ import { listWorkflows } from "./workflows.js";
|
|
|
20
21
|
* like never do. A target that does not exist is reported (found: false),
|
|
21
22
|
* never an error — the same rule project links have always followed.
|
|
22
23
|
*/
|
|
23
|
-
export const REF_KIND_NAMES = ["knowledge", "vibeables", "repos", "workflows", "agents", "projects", "contacts", "places", "events"];
|
|
24
|
+
export const REF_KIND_NAMES = ["knowledge", "vibeables", "repos", "workflows", "agents", "projects", "contacts", "places", "events", "boards", "tasks"];
|
|
24
25
|
/** A list that fails to read is an empty list: a reference into it is "not found", never an error. */
|
|
25
26
|
const safe = async (p, fallback) => p.catch(() => fallback);
|
|
26
27
|
export const REF_KINDS = [
|
|
@@ -80,7 +81,30 @@ export const REF_KINDS = [
|
|
|
80
81
|
noun: "event",
|
|
81
82
|
list: async () => (await safe(listEntities(EVENTS), { items: [] })).items.map((e) => ({ slug: e.slug, label: EVENTS.describe(e), href: `/events/${e.slug}` })),
|
|
82
83
|
},
|
|
84
|
+
{
|
|
85
|
+
kind: "boards",
|
|
86
|
+
token: "board",
|
|
87
|
+
noun: "board",
|
|
88
|
+
// A board's slug is unique within its project; across the workspace the first match wins and the label names where it is.
|
|
89
|
+
list: async () => (await safe(listAllBoards(), [])).map((b) => ({ slug: b.board, label: b.project ? `${b.title} · project ${b.project}` : b.title, href: boardHref(b) })),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
kind: "tasks",
|
|
93
|
+
token: "task",
|
|
94
|
+
noun: "task",
|
|
95
|
+
list: async () => {
|
|
96
|
+
const { getBoard } = await import("./boards.js");
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const b of await safe(listAllBoards(), [])) {
|
|
99
|
+
const detail = await safe(getBoard(b), null);
|
|
100
|
+
for (const t of detail?.items ?? [])
|
|
101
|
+
out.push({ slug: t.slug, label: `${t.title} · ${boardKey(b)} (${t.status})`, href: `${boardHref(b)}/${t.slug}` });
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
83
106
|
];
|
|
107
|
+
const boardHref = (b) => (b.project ? `/projects/${b.project}/boards/${b.board}` : `/boards/${b.board}`);
|
|
84
108
|
const byKind = new Map(REF_KINDS.map((d) => [d.kind, d]));
|
|
85
109
|
const byToken = new Map(REF_KINDS.map((d) => [d.token, d]));
|
|
86
110
|
/** The kind registered under a plural name or a token; undefined for anything else. */
|
package/dist/core/search.d.ts
CHANGED
|
@@ -27,6 +27,18 @@ export interface WorkspaceAgents {
|
|
|
27
27
|
projects: ProjectHit[];
|
|
28
28
|
/** Contacts, places and events of this instance (the registry does not carry them); empty for other workspaces and while the features are off. */
|
|
29
29
|
entities: EntityHit[];
|
|
30
|
+
/** Open tasks of this instance's boards; empty for other workspaces and while boards are off. */
|
|
31
|
+
tasks: TaskHit[];
|
|
32
|
+
}
|
|
33
|
+
/** What the palette needs of a task. */
|
|
34
|
+
export interface TaskHit {
|
|
35
|
+
slug: string;
|
|
36
|
+
title: string;
|
|
37
|
+
board: string;
|
|
38
|
+
project?: string;
|
|
39
|
+
status: string;
|
|
40
|
+
/** Where it opens. */
|
|
41
|
+
href: string;
|
|
30
42
|
}
|
|
31
43
|
/** What the palette needs of a contact, place or event. */
|
|
32
44
|
export interface EntityHit {
|
package/dist/core/search.js
CHANGED
|
@@ -6,6 +6,23 @@ import { listChannels, toChannelSummary } from "./channels.js";
|
|
|
6
6
|
import { currentInstanceUrl, discoverWorkspaces, listWorkspaceRecords } from "./instances.js";
|
|
7
7
|
import { CONTACTS, EVENTS, listEntities, PLACES } from "./entities.js";
|
|
8
8
|
import { listProjects, toProjectHit } from "./projects.js";
|
|
9
|
+
import { getBoard, listAllBoards } from "./boards.js";
|
|
10
|
+
async function taskHits() {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const b of await listAllBoards().catch(() => [])) {
|
|
13
|
+
const detail = await getBoard(b).catch(() => null);
|
|
14
|
+
if (!detail)
|
|
15
|
+
continue;
|
|
16
|
+
const done = new Set(detail.columns.filter((c) => c.done).map((c) => c.slug));
|
|
17
|
+
for (const t of detail.items) {
|
|
18
|
+
if (t.configError || done.has(t.status))
|
|
19
|
+
continue;
|
|
20
|
+
const base = b.project ? `/projects/${b.project}/boards/${b.board}` : `/boards/${b.board}`;
|
|
21
|
+
out.push({ slug: t.slug, title: t.title, board: b.board, ...(b.project ? { project: b.project } : {}), status: t.status, href: `${base}/${t.slug}` });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
9
26
|
async function entityHits(def) {
|
|
10
27
|
const view = await listEntities(def).catch(() => ({ items: [] }));
|
|
11
28
|
return view.items
|
|
@@ -28,6 +45,7 @@ export async function searchAgents() {
|
|
|
28
45
|
channels: (await listChannels().catch(() => [])).map(toChannelSummary),
|
|
29
46
|
projects: (await listProjects().catch(() => ({ projects: [] }))).projects.filter((p) => p.status !== "archived").map(toProjectHit),
|
|
30
47
|
entities: (await Promise.all([CONTACTS, PLACES, EVENTS].map(entityHits))).flat(),
|
|
48
|
+
tasks: await taskHits(),
|
|
31
49
|
};
|
|
32
50
|
const [workspaces, known] = await Promise.all([discoverWorkspaces(), listWorkspaceRecords()]);
|
|
33
51
|
const records = new Map(known.map((p) => [p.root, p]));
|
|
@@ -45,6 +63,7 @@ export async function searchAgents() {
|
|
|
45
63
|
channels: records.get(w.root)?.channels ?? [],
|
|
46
64
|
projects: records.get(w.root)?.projects ?? [],
|
|
47
65
|
entities: [],
|
|
66
|
+
tasks: [],
|
|
48
67
|
}));
|
|
49
68
|
return { workspaces: [self, ...others] };
|
|
50
69
|
}
|
package/dist/core/settings.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface SaveSettingsInput {
|
|
|
36
36
|
contacts?: ReposSettingsInput;
|
|
37
37
|
places?: ReposSettingsInput;
|
|
38
38
|
events?: ReposSettingsInput;
|
|
39
|
+
/** Omitted = untouched. Same shape and rules as repos (boards.columns in the file is kept). */
|
|
40
|
+
boards?: ReposSettingsInput;
|
|
39
41
|
}
|
|
40
42
|
export interface ReposSettingsInput {
|
|
41
43
|
enabled: boolean;
|
package/dist/core/settings.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseDocument } from "yaml";
|
|
4
|
-
import { ENTITY_KINDS, entityDefaultRoot, ignoreEntryFor, PROJECTS_DEFAULT_ROOT, REPOS_DEFAULT_ROOT, resolveWorkspace, VIBEABLES_DEFAULT_ROOT, isSafeGitName } from "../config.js";
|
|
4
|
+
import { BOARDS_DEFAULT_ROOT, ENTITY_KINDS, entityDefaultRoot, ignoreEntryFor, PROJECTS_DEFAULT_ROOT, REPOS_DEFAULT_ROOT, resolveWorkspace, VIBEABLES_DEFAULT_ROOT, isSafeGitName } from "../config.js";
|
|
5
|
+
import { ensureBoardsRoot } from "./boards.js";
|
|
5
6
|
import { ensureEntitiesRoot } from "./entities.js";
|
|
6
7
|
import { ensureProjectsRoot } from "./projects.js";
|
|
7
8
|
import { disposeAllDevs, ensureVibeablesRoot } from "./vibeables.js";
|
|
@@ -84,6 +85,7 @@ const ROOT_DEFAULTS = {
|
|
|
84
85
|
contacts: entityDefaultRoot("contacts"),
|
|
85
86
|
places: entityDefaultRoot("places"),
|
|
86
87
|
events: entityDefaultRoot("events"),
|
|
88
|
+
boards: BOARDS_DEFAULT_ROOT,
|
|
87
89
|
};
|
|
88
90
|
/** The repos, vibeables, projects, contacts, places or events block to write; null when the feature is off and nothing else is set. */
|
|
89
91
|
function cleanRootBlock(block, value, projectRoot) {
|
|
@@ -181,6 +183,14 @@ export async function saveSettings(input) {
|
|
|
181
183
|
else
|
|
182
184
|
doc.set(kind, block);
|
|
183
185
|
}
|
|
186
|
+
if (input.boards !== undefined) {
|
|
187
|
+
const block = cleanRootBlock("boards", input.boards, project.root);
|
|
188
|
+
const columns = project.config.boards?.columns;
|
|
189
|
+
if (block === null && !columns)
|
|
190
|
+
doc.delete("boards");
|
|
191
|
+
else
|
|
192
|
+
doc.set("boards", { ...(block ?? { enabled: false }), ...(columns ? { columns } : {}) });
|
|
193
|
+
}
|
|
184
194
|
await fs.writeFile(configPath, doc.toString());
|
|
185
195
|
// Turning repositories on prepares the folder so the first clone and the
|
|
186
196
|
// workspace git both find it in the right state.
|
|
@@ -193,5 +203,7 @@ export async function saveSettings(input) {
|
|
|
193
203
|
for (const kind of ENTITY_KINDS)
|
|
194
204
|
if (input[kind]?.enabled)
|
|
195
205
|
await ensureEntitiesRoot(kind).catch(() => { });
|
|
206
|
+
if (input.boards?.enabled)
|
|
207
|
+
await ensureBoardsRoot().catch(() => { });
|
|
196
208
|
return getSettings();
|
|
197
209
|
}
|
package/dist/core/trash.d.ts
CHANGED
|
@@ -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;
|
|
@@ -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>>)[];
|