@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.
- package/README.md +57 -2
- package/dist/cli/boards.d.ts +2 -0
- package/dist/cli/boards.js +486 -0
- package/dist/cli/init.js +4 -2
- package/dist/cli/kraftwerk.js +6 -0
- package/dist/cli/temp.d.ts +2 -0
- package/dist/cli/temp.js +62 -0
- package/dist/client/routes.d.ts +16 -0
- package/dist/client/routes.js +16 -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 +3 -1
- package/dist/core/chat/backend.d.ts +13 -0
- package/dist/core/chat/backend.js +9 -1
- package/dist/core/chat/context.js +26 -9
- package/dist/core/chat/pi.d.ts +1 -1
- package/dist/core/chat/pi.js +3 -1
- package/dist/core/chat/seats.js +6 -2
- package/dist/core/chat/sessions.d.ts +2 -0
- package/dist/core/chat/sessions.js +4 -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 +20 -1
- package/dist/core/git.js +3 -1
- package/dist/core/notifications.d.ts +1 -1
- package/dist/core/projects.js +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/temp.d.ts +73 -0
- package/dist/core/temp.js +154 -0
- package/dist/core/trash.d.ts +1 -1
- package/dist/harness.d.ts +2 -0
- package/dist/harnesses/acp.js +1 -1
- package/dist/harnesses/claude.js +1 -0
- package/dist/harnesses/codex.js +1 -0
- package/dist/harnesses/pi.js +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.js +13 -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/files.d.ts +9 -1
- package/dist/server/api/files.js +13 -0
- package/dist/server/api/index.d.ts +113 -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/dist/yaml.js +1 -0
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +39 -0
- package/web/dist/assets/{dist-DlZPeneT.js → dist-BBFUIftm.js} +1 -1
- package/web/dist/assets/{dist-u_oAKIhZ.js → dist-Bie1ebP9.js} +1 -1
- package/web/dist/assets/{dist-NX4_yvho.js → dist-BuwdjX6z.js} +1 -1
- package/web/dist/assets/{dist-1PajaYPg.js → dist-C0ve4Tka.js} +1 -1
- package/web/dist/assets/{dist-DgKgeYFJ.js → dist-C16bXf8c.js} +1 -1
- package/web/dist/assets/{dist-DKduisGW.js → dist-CDUZdAAS.js} +1 -1
- package/web/dist/assets/{dist-B4dqwo7_.js → dist-CH73jsgO.js} +1 -1
- package/web/dist/assets/{dist-D-f0nszo.js → dist-C_rJjRj7.js} +1 -1
- package/web/dist/assets/dist-DYICNp09.js +1 -0
- package/web/dist/assets/{dist-K_3Y5Nbl.js → dist-DtUB2Uu2.js} +1 -1
- package/web/dist/assets/{dist-CK2u1OXb.js → dist-DvkrCSGL.js} +1 -1
- package/web/dist/assets/{editor-CE9TlKkJ.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-Bp1iMuQ7.js +0 -1
- package/web/dist/assets/index-Dvt-vE1h.css +0 -2
- package/web/dist/assets/index-IAuwD22j.js +0 -231
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,8 @@ 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";
|
|
7
|
+
import { tempEnv } from "../temp.js";
|
|
6
8
|
import { unattendedMode } from "./permissions.js";
|
|
7
9
|
/**
|
|
8
10
|
* ACP-backed chat: spawn an adapter (claude-agent-acp / codex-acp) as a
|
|
@@ -389,7 +391,7 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
|
389
391
|
};
|
|
390
392
|
const { child, conn } = await connectAcp(agent, {
|
|
391
393
|
cwd,
|
|
392
|
-
env: adapterEnv(agent, tuning),
|
|
394
|
+
env: { ...adapterEnv(agent, tuning), ...(tuning.temp ? tempEnv(tuning.temp) : {}), ...identityEnv(agent, tuning) },
|
|
393
395
|
client,
|
|
394
396
|
clientName: "kraftwerk-inspector",
|
|
395
397
|
elicitation: true,
|
|
@@ -37,6 +37,10 @@ export interface BackendTuning {
|
|
|
37
37
|
skillDirs?: string[];
|
|
38
38
|
/** claude: extra directories the session may work in (e.g. project root for run chats). */
|
|
39
39
|
addDirs?: string[];
|
|
40
|
+
/** The session's scratch folder (core/temp.ts): handed to the process as TMPDIR and KRAFTWERK_TEMP. */
|
|
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;
|
|
40
44
|
/**
|
|
41
45
|
* Nobody is watching this session live (routine runs). The backend selects
|
|
42
46
|
* the harness's own hands-off permission mode — the harness keeps deciding
|
|
@@ -46,6 +50,15 @@ export interface BackendTuning {
|
|
|
46
50
|
/** ACP agents: the session id to continue (from ChatMeta.sessions) instead of opening a new one. */
|
|
47
51
|
resume?: string;
|
|
48
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>;
|
|
49
62
|
/**
|
|
50
63
|
* How a turn ended. `failure` is the typed session failure the agent
|
|
51
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,10 +8,12 @@ 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";
|
|
14
15
|
import { filesSummary } from "../files.js";
|
|
16
|
+
import { sessionTemp, tempContext } from "../temp.js";
|
|
15
17
|
/**
|
|
16
18
|
* What an agent is told about where it works: the skills it may use and
|
|
17
19
|
* the scope's context block (the workspace, a run, a project, a channel,
|
|
@@ -85,8 +87,8 @@ async function agentKnowledgeContext(def) {
|
|
|
85
87
|
`part of your job. Consult them before answering questions in their domain, and keep them current ` +
|
|
86
88
|
`when you learn something durable:\n${lines}\n\n` +
|
|
87
89
|
`Read with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
88
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
89
|
-
`(write the markdown
|
|
90
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor ${def.slug}/${def.harness}\` ` +
|
|
91
|
+
`(write the markdown into your temp folder first; the CLI stamps provenance and maintains index.md/log.md). ` +
|
|
90
92
|
`Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
|
|
91
93
|
`click in the UI.\n\n`);
|
|
92
94
|
}
|
|
@@ -132,7 +134,8 @@ export async function agentFilesContext(def) {
|
|
|
132
134
|
return (`## Files\nThe workspace's shared files (any format: brand assets, templates, contracts, …) are in ${ws.root}/ (${ws.count} file${ws.count === 1 ? "" : "s"}). ` +
|
|
133
135
|
(linked ? `The folders that are part of your job:\n${linked}\n` : "") +
|
|
134
136
|
`Work you do for a project belongs in that project's files folder, not here. What you produce in a direct session goes to ${ws.root}/inbox/. ` +
|
|
135
|
-
`
|
|
137
|
+
`Only finished work and the user's own material belong in these folders — scratch, intermediate and downloaded files go to your ` +
|
|
138
|
+
`temp folder (below), never here. Delete nothing there unless asked.\n\n`);
|
|
136
139
|
}
|
|
137
140
|
/**
|
|
138
141
|
* "## Your vibeables" block: the apps an agent builds and maintains, linked
|
|
@@ -273,8 +276,9 @@ async function baseScopeContext(scope, agent) {
|
|
|
273
276
|
(await agentFilesContext(def)) +
|
|
274
277
|
`The working directory is the project root. Stay within your role; if a request is clearly outside it, ` +
|
|
275
278
|
`say so and suggest which agent or tool fits better.` +
|
|
276
|
-
(scope.
|
|
277
|
-
|
|
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`} — ` +
|
|
278
282
|
`nobody may be watching live. Complete the task autonomously. Edits inside the project ` +
|
|
279
283
|
`need no approval; anything your harness asks about (shell commands, network, files ` +
|
|
280
284
|
`outside the project) is shown to a human who may look later — say in one line why you ` +
|
|
@@ -310,7 +314,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
310
314
|
`- Favor structural markdown (headings, tables, lists) over prose.\n\n` +
|
|
311
315
|
`ALWAYS write through the kraftwerk CLI so provenance is stamped and the bundle log/index stay maintained:\n` +
|
|
312
316
|
`- \`npx kraftwerk knowledge init <bundle>\` — new bundle\n` +
|
|
313
|
-
`- \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
317
|
+
`- \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor kraftwerk-chat/${agent}\` — create/update a concept ` +
|
|
314
318
|
`(write the markdown to a temp file first; the CLI stamps generated.by/at, appends log.md, regenerates index.md)\n` +
|
|
315
319
|
`- \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`, \`validate\`\n` +
|
|
316
320
|
`Do not hand-edit index.md or log.md (derived/maintained), and do not add \`verified\` yourself — ` +
|
|
@@ -349,7 +353,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
349
353
|
`## Recent runs\n${runLines || "(none)"}\n\n` +
|
|
350
354
|
`## Knowledge\nOKF bundles in this project:\n${bundleLines || "(none)"}\n` +
|
|
351
355
|
`Read with \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
352
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
356
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor kraftwerk-chat/${agent}\` ` +
|
|
353
357
|
`(stamps provenance, maintains index.md/log.md — never hand-edit those).\n\n` +
|
|
354
358
|
`## Agents\n${agentLines || "(none)"}\n` +
|
|
355
359
|
`These are persistent agents (defined under agents/); the user talks to them on the ` +
|
|
@@ -392,7 +396,7 @@ export async function scopeContext(meta, seat) {
|
|
|
392
396
|
const agent = seat.harness;
|
|
393
397
|
// The repositories block reads every clone from git, so it runs alongside
|
|
394
398
|
// the rest instead of adding its spawns to the first prompt's latency.
|
|
395
|
-
const [base, repos, vibe, skills, channel, project, entities] = await Promise.all([
|
|
399
|
+
const [base, repos, vibe, skills, channel, project, entities, boards] = await Promise.all([
|
|
396
400
|
baseScopeContext(scope, agent),
|
|
397
401
|
scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
|
|
398
402
|
meta.vibeable ? vibeableContext(meta.vibeable, meta.cwd) : Promise.resolve(""),
|
|
@@ -402,8 +406,21 @@ export async function scopeContext(meta, seat) {
|
|
|
402
406
|
meta.scope.kind === "channel" && meta.project ? projectContext(meta.project, `${seat.key}/${agent}`, { member: true }) : Promise.resolve(""),
|
|
403
407
|
// Who, where and when: the workspace's contacts, places and events, wherever the chat works (a run discusses its own artifacts only).
|
|
404
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(""),
|
|
405
417
|
]);
|
|
406
|
-
|
|
418
|
+
// Every session has a scratch folder of its own (core/temp.ts), named by its absolute path: a run chat or a
|
|
419
|
+
// vibeable session works in another folder than the workspace root. The block comes after the files blocks it refers to.
|
|
420
|
+
const temp = await sessionTemp(meta.id).catch(() => null);
|
|
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");
|
|
407
424
|
}
|
|
408
425
|
/** The channel block a member agent gets once per process: who is here, how turns work, how to hand over. */
|
|
409
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,6 +1,8 @@
|
|
|
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";
|
|
5
|
+
import { tempEnv } from "../temp.js";
|
|
4
6
|
const clip = (raw) => {
|
|
5
7
|
const oneLine = String(raw ?? "").replace(/\s+/g, " ").trim();
|
|
6
8
|
return oneLine.length > 120 ? `${oneLine.slice(0, 120)}…` : oneLine;
|
|
@@ -24,7 +26,7 @@ export function startPiBackend(cwd, hooks, tuning = {}) {
|
|
|
24
26
|
const proc = spawn("pi", ["-p", "--mode", "json", "--session-id", sessionId, ...tuningArgs], {
|
|
25
27
|
cwd,
|
|
26
28
|
stdio: ["pipe", "pipe", "pipe"],
|
|
27
|
-
env: workspaceEnv(),
|
|
29
|
+
env: { ...workspaceEnv(), ...(tuning.temp ? tempEnv(tuning.temp) : {}), ...identityEnv("pi", tuning) },
|
|
28
30
|
});
|
|
29
31
|
child = proc;
|
|
30
32
|
proc.stdin.write(text);
|
package/dist/core/chat/seats.js
CHANGED
|
@@ -9,6 +9,7 @@ import { chatHref, dismissKey, pushNotification } from "../notifications.js";
|
|
|
9
9
|
import { writeMeta } from "./store.js";
|
|
10
10
|
import { availableSkills } from "./context.js";
|
|
11
11
|
import { effectiveCwd } from "./sessions.js";
|
|
12
|
+
import { sessionTemp } from "../temp.js";
|
|
12
13
|
import { emit, isUnattended, ownerLabel, seatAuthor } from "./state.js";
|
|
13
14
|
/**
|
|
14
15
|
* Starting an agent process for a seat: its harness, tuning (model,
|
|
@@ -22,6 +23,7 @@ async function backendTuning(meta, seat) {
|
|
|
22
23
|
const tuning = {};
|
|
23
24
|
const agentSlug = scope.kind === "agent" ? scope.slug : scope.kind === "channel" ? seat.key : undefined;
|
|
24
25
|
if (agentSlug) {
|
|
26
|
+
tuning.agent = agentSlug;
|
|
25
27
|
const def = await getAgent(agentSlug).catch(() => null);
|
|
26
28
|
if (def?.model)
|
|
27
29
|
tuning.model = def.model;
|
|
@@ -57,6 +59,8 @@ async function backendTuning(meta, seat) {
|
|
|
57
59
|
if (skills.length > 0)
|
|
58
60
|
tuning.skillDirs = skills.map((s) => s.dir);
|
|
59
61
|
}
|
|
62
|
+
// Every seat gets the chat's scratch folder as its temp dir (core/temp.ts), so nothing temporary lands in files/ or /tmp.
|
|
63
|
+
tuning.temp = (await sessionTemp(meta.id)).abs;
|
|
60
64
|
return tuning;
|
|
61
65
|
}
|
|
62
66
|
export async function ensureBackend(state, seat) {
|
|
@@ -103,7 +107,7 @@ export async function ensureBackend(state, seat) {
|
|
|
103
107
|
});
|
|
104
108
|
if (!state.pendingInfo.has(requestId))
|
|
105
109
|
await dismissKey(notifyKey);
|
|
106
|
-
});
|
|
110
|
+
}).catch(() => { });
|
|
107
111
|
if (isUnattended(state.meta.scope)) {
|
|
108
112
|
deadline = setTimeout(() => {
|
|
109
113
|
const pending = state.pendingPermissions.get(requestId);
|
|
@@ -154,7 +158,7 @@ export async function ensureBackend(state, seat) {
|
|
|
154
158
|
});
|
|
155
159
|
if (!state.pendingInfo.has(requestId))
|
|
156
160
|
await dismissKey(notifyKey);
|
|
157
|
-
});
|
|
161
|
+
}).catch(() => { });
|
|
158
162
|
if (isUnattended(state.meta.scope)) {
|
|
159
163
|
deadline = setTimeout(() => {
|
|
160
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). */
|
|
@@ -7,6 +7,7 @@ import { deleteAgentSession, listAgentSessions } from "./acp.js";
|
|
|
7
7
|
import { chatHref, pushNotification } from "../notifications.js";
|
|
8
8
|
import { readIdentity } from "../identity.js";
|
|
9
9
|
import { moveToTrash } from "../trash.js";
|
|
10
|
+
import { removeSessionTemp } from "../temp.js";
|
|
10
11
|
import { attachmentPath, copyChatFiles, listChatMetas, newChatId, readMetaAt, safeChatDir, writeMeta } from "./store.js";
|
|
11
12
|
import { postChannelMessage } from "./channel-turns.js";
|
|
12
13
|
import { expandSkillInvocation, scopeContext } from "./context.js";
|
|
@@ -54,6 +55,7 @@ export async function createChat(opts) {
|
|
|
54
55
|
cwd,
|
|
55
56
|
scope: opts.scope,
|
|
56
57
|
...(opts.project && opts.scope.kind === "agent" ? { project: opts.project } : {}),
|
|
58
|
+
...(opts.task ? { task: opts.task } : {}),
|
|
57
59
|
...(opts.resume && opts.agent !== "pi" ? { sessions: { [MAIN]: opts.resume } } : {}),
|
|
58
60
|
author,
|
|
59
61
|
...(opts.private ? { private: true } : {}),
|
|
@@ -526,6 +528,8 @@ export async function deleteChat(id) {
|
|
|
526
528
|
dropBackend(state);
|
|
527
529
|
chatStates().delete(id);
|
|
528
530
|
await moveToTrash("chats", id, safeChatDir(id));
|
|
531
|
+
// Scratch, not a deliverable: gone for good with the chat.
|
|
532
|
+
await removeSessionTemp(id).catch(() => { });
|
|
529
533
|
return {};
|
|
530
534
|
}
|
|
531
535
|
/** The agents' own transcripts of a chat folder (one in the trash), deleted for good. Best effort. */
|
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. */
|
|
@@ -312,5 +330,6 @@ export async function filesContext(scope, heading, intro) {
|
|
|
312
330
|
return (`## ${heading}\n${intro} ${s.root}/ — ${s.count} file${s.count === 1 ? "" : "s"}` +
|
|
313
331
|
(recent ? `, most recently changed:\n${recent}\n` : " (empty so far)\n") +
|
|
314
332
|
`Read and write them there like any folder; put what you produce for the user in ${s.root}/deliverables/. ` +
|
|
333
|
+
`Only finished work and the material the user gave you belong here — scratch, intermediate and downloaded files go to your temp folder (see Temporary files), never into this folder. ` +
|
|
315
334
|
`Files over 25 MB stay out of git automatically. Delete nothing there unless asked.`);
|
|
316
335
|
}
|
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/projects.js
CHANGED
|
@@ -441,7 +441,7 @@ export async function projectContext(slug, actor, opts = {}) {
|
|
|
441
441
|
? `## Knowledge\nOKF bundles linked to this project (markdown + YAML frontmatter under the knowledge root):\n` +
|
|
442
442
|
linkLines("knowledge", (l) => `- ${l.slug}${l.label ? ` (${l.label})` : ""}`) +
|
|
443
443
|
`\nRead with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
444
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
444
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor ${actor}\` ` +
|
|
445
445
|
`(stamps provenance, maintains index.md/log.md — never hand-edit those).`
|
|
446
446
|
: "",
|
|
447
447
|
p.repos.length > 0
|
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
|
}
|