@netnodeag/kraftwerk 0.57.4 → 0.58.1

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 (63) hide show
  1. package/README.md +85 -2
  2. package/dist/acp.js +2 -1
  3. package/dist/cli/journal.d.ts +10 -0
  4. package/dist/cli/journal.js +36 -0
  5. package/dist/cli/knowledge.js +17 -0
  6. package/dist/cli/kraftwerk.js +6 -0
  7. package/dist/cli/projects.js +3 -3
  8. package/dist/cli/repos.js +1 -1
  9. package/dist/cli/trash.d.ts +2 -0
  10. package/dist/cli/trash.js +70 -0
  11. package/dist/cli/vibeables.js +3 -3
  12. package/dist/inspector/agent-status.d.ts +21 -0
  13. package/dist/inspector/agent-status.js +25 -0
  14. package/dist/inspector/agents.d.ts +5 -0
  15. package/dist/inspector/agents.js +10 -1
  16. package/dist/inspector/attention.d.ts +48 -0
  17. package/dist/inspector/attention.js +66 -0
  18. package/dist/inspector/channels.js +2 -1
  19. package/dist/inspector/chat/sessions.d.ts +52 -1
  20. package/dist/inspector/chat/sessions.js +160 -27
  21. package/dist/inspector/chat/store.d.ts +2 -0
  22. package/dist/inspector/chat/store.js +5 -1
  23. package/dist/inspector/files.d.ts +87 -0
  24. package/dist/inspector/files.js +316 -0
  25. package/dist/inspector/git.js +3 -1
  26. package/dist/inspector/journal.d.ts +31 -0
  27. package/dist/inspector/journal.js +72 -0
  28. package/dist/inspector/knowledge.d.ts +2 -0
  29. package/dist/inspector/knowledge.js +7 -0
  30. package/dist/inspector/projects.d.ts +1 -1
  31. package/dist/inspector/projects.js +5 -2
  32. package/dist/inspector/repos.js +2 -1
  33. package/dist/inspector/runs.d.ts +1 -1
  34. package/dist/inspector/runs.js +3 -2
  35. package/dist/inspector/server.js +111 -1
  36. package/dist/inspector/skills.d.ts +1 -1
  37. package/dist/inspector/skills.js +3 -2
  38. package/dist/inspector/status-line.d.ts +14 -0
  39. package/dist/inspector/status-line.js +44 -0
  40. package/dist/inspector/trash.d.ts +37 -0
  41. package/dist/inspector/trash.js +148 -0
  42. package/dist/inspector/vibeables.d.ts +1 -1
  43. package/dist/inspector/vibeables.js +3 -2
  44. package/inspector/dist/assets/{dist-bpX7WAGx.js → dist-BOgAuM_m.js} +1 -1
  45. package/inspector/dist/assets/{dist-D5fobTtx.js → dist-BW2AWp_e.js} +1 -1
  46. package/inspector/dist/assets/{dist-BvDLxSg5.js → dist-BcBryE8Y.js} +1 -1
  47. package/inspector/dist/assets/{dist-slvUmm0x.js → dist-C5aV9g-B.js} +1 -1
  48. package/inspector/dist/assets/{dist-B53NsyKK.js → dist-CL5nzsfs.js} +1 -1
  49. package/inspector/dist/assets/dist-D8BaN0Ps.js +1 -0
  50. package/inspector/dist/assets/{dist-D9p_d6De.js → dist-D9EqytQ1.js} +1 -1
  51. package/inspector/dist/assets/{dist-q_nB-2jY.js → dist-DCbRyQBr.js} +1 -1
  52. package/inspector/dist/assets/{dist-DxkFf8wL.js → dist-DvLwKI0v.js} +1 -1
  53. package/inspector/dist/assets/{dist-D3FXw2rB.js → dist-DyuX-pWN.js} +1 -1
  54. package/inspector/dist/assets/{dist-DygUNvj6.js → dist-HArst0KY.js} +1 -1
  55. package/inspector/dist/assets/{editor-qZxomNcW.js → editor-C6eZc_Cr.js} +3 -3
  56. package/inspector/dist/assets/{editor-Cz0AMLr-.css → editor-CZMrhTTL.css} +1 -1
  57. package/inspector/dist/assets/index-Bb27CwDb.css +2 -0
  58. package/inspector/dist/assets/index-kyDjRVVU.js +229 -0
  59. package/inspector/dist/index.html +2 -2
  60. package/package.json +1 -1
  61. package/inspector/dist/assets/dist-BOZxRe-K.js +0 -1
  62. package/inspector/dist/assets/index-6Jx1rUdP.js +0 -227
  63. package/inspector/dist/assets/index-B2UPyYuE.css +0 -1
package/README.md CHANGED
@@ -158,6 +158,8 @@ kraftwerk knowledge # Knowledge: OKF bundles (list/get/put/v
158
158
  kraftwerk ui # inspector web UI on http://localhost:1981; --port, --output
159
159
  kraftwerk workspaces # every workspace on this machine; workspaces start|stop|forget <ref>
160
160
  kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
161
+ kraftwerk trash # what was deleted; trash restore|purge <id>, trash empty
162
+ kraftwerk journal <agent> ["<entry>"] # an agent's memory across sessions: print it, add a line (--kind)
161
163
  kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
162
164
  kraftwerk validate # all discovered: schema + semantics + files, exit 1 on failure
163
165
  kraftwerk validate src/workflows/pitch # specific paths
@@ -169,6 +171,15 @@ kraftwerk tunnel setup kw.example.com # Cloudflare Tunnel to the inspector: lo
169
171
  kraftwerk tunnel # run that tunnel alone (the UI runs elsewhere)
170
172
  ```
171
173
 
174
+ Nothing is deleted at once. Removing an agent, a project, a knowledge bundle,
175
+ a vibeable, a channel, a repository, a chat or a run, in the inspector or the
176
+ CLI, moves its folder to `kraftwerk-data/trash/`. The trash mirrors the
177
+ workspace (`trash/agents/<slug>`, `trash/knowledge/<bundle>`,
178
+ `trash/projects/<slug>`, `trash/chats/<id>`, …) and ignores itself in git.
179
+ The **trash** screen (`#/trash`) or `kraftwerk trash restore <id>` puts an entry
180
+ back where it came from. Deleting it there, or `kraftwerk trash empty`, makes it
181
+ final. A chat's agent transcripts are only removed then, so a restored chat can resume.
182
+
172
183
  The inspector binds `127.0.0.1` — it has no authentication of its own and
173
184
  its chat runs coding agents against the repo, so it stays off the LAN, and
174
185
  while bound there it only answers requests whose `Host` is a loopback name
@@ -510,6 +521,11 @@ directory stays the workspace root, so linked repositories and knowledge
510
521
  are reachable by path; opening a vibeable moves it into the app folder as
511
522
  in any chat.
512
523
 
524
+ **In the rail.** The agents linked to a project and its apps (the linked
525
+ vibeables) are listed under the project. Pin an app from the vibeables tab
526
+ of the project's context column, or unpin it there; clicking it in the rail
527
+ opens the project with the app beside its chat (`#/projects/<slug>?app=<vibeable>`).
528
+
513
529
  **Coworkers.** "Add coworker" on a project chat works as on an agent
514
530
  session: the chat becomes a channel whose members are the agents you pick,
515
531
  with everything said so far and the project's brief, state, records and
@@ -530,6 +546,37 @@ kraftwerk projects remove relaunch-netnode-ch
530
546
  The registry of workspaces on this machine, which answered to
531
547
  `kraftwerk projects` until 0.48, is `kraftwerk workspaces` now.
532
548
 
549
+ ## Files
550
+
551
+ Files are the material of the work, in any format: briefings, contracts,
552
+ designs, spreadsheets, deliverables. They are plain folders with two kinds
553
+ of owner, chosen by how long a file lives:
554
+
555
+ ```
556
+ kraftwerk-data/files/ # workspace files: shared, outlive any project
557
+ kraftwerk-data/projects/<slug>/files/ # a project's files: archived and trashed with the project
558
+ ```
559
+
560
+ Agents own no files. They work in the project's files when they work on
561
+ a project, and may link workspace folders in `agent.yml` (`files: [contracts]`)
562
+ the way they link knowledge. Every session's context names the folders, the
563
+ file count and the latest changes, not every file, and tells the agent to put
564
+ what it produces in `files/deliverables/` (or `files/inbox/` without a
565
+ project). Because they are plain folders, agents read and write them like any
566
+ directory, and you can drop files in with Finder.
567
+
568
+ The **files** page (`#/files`) has the workspace and every project on the
569
+ left, the folder in the middle, and a preview on the right for images, PDFs,
570
+ video, audio, markdown, CSV and text. Drop files anywhere on the list to
571
+ upload. A project's chat has the same browser in its context column. Files
572
+ are versioned in the workspace git; a file over 25 MB stays local, listed in
573
+ the files root's own `.gitignore` and marked "local only". Deleting goes to
574
+ the trash (`trash/files/…`, mirroring the folders).
575
+
576
+ Files are not knowledge. A knowledge bundle holds curated, verified concepts,
577
+ and a file becomes part of one as evidence, in the bundle's `references/`
578
+ folder, which concepts cite in `sources` (OKF §5.1, §6.3).
579
+
533
580
  ## Persistent agents
534
581
 
535
582
  The inspector's "agents" screen turns chat agents into persistent teammates. An
@@ -539,6 +586,7 @@ agent is one folder under the project's `agents/` root:
539
586
  agents/max/
540
587
  agent.yml # name, emoji, description, harness, model, effort, workflows, knowledge, vibeables
541
588
  system.md # the agent's system prompt (its role)
589
+ journal.md # the agent's own memory across sessions (written through `kraftwerk journal`)
542
590
  ```
543
591
 
544
592
  ```yaml
@@ -564,6 +612,40 @@ and write knowledge through `kraftwerk knowledge`, with writes stamped with
564
612
  the agent's own actor, `<slug>/<harness>`. So the agent triggers its own
565
613
  workflows when a request matches, and keeps its bundles current.
566
614
 
615
+ Every agent keeps a journal, `agents/<slug>/journal.md`: what it learned,
616
+ decided, promised and finished, one line per entry under a dated heading,
617
+ newest first. The recent days (about 4,000 characters) are part of every
618
+ new session's context, together with how to add to it:
619
+
620
+ ```bash
621
+ kraftwerk journal max # print it
622
+ kraftwerk journal max "send the summary on Friday" --kind promised # learned | decided | promised | done | note
623
+ ```
624
+
625
+ The agent's own lines are not signed; a line from anyone else is stamped
626
+ with its actor. In the inspector, the agent's context column has a
627
+ **journal** tab, where you can also add a note for the agent to remember
628
+ (`GET`/`POST /api/agents/:slug/journal`). That is what makes an agent the
629
+ same colleague next week, whichever chat you open.
630
+
631
+ **What needs you.** Everything that blocks an agent, an approval or a
632
+ question it asked, plus failures nobody has opened yet, is one list
633
+ (`GET /api/attention`), oldest first, each item with its owner: the agent,
634
+ the project or the channel. The rail puts a red count on every row that
635
+ leads there: the agent, the project (its own chats and its agents'), the
636
+ channel, Ralv, and the projects header while folded. The bell lists these
637
+ items under "needs you", grouped by owner, with finished runs and routines
638
+ below as "updates". **next** in the top bar (⌥N) opens the next waiting
639
+ item where it lives: the chat, scrolled to the card, which lights up, with
640
+ the project's or agent's context beside it. Answer, press next again.
641
+ Notification titles name the owner first ("🦊 Lisa needs approval").
642
+
643
+ Under each agent's name the rail shows what it is doing now: waiting for
644
+ you (an approval or a question, with a red light), working (in which chat,
645
+ for how long, with a pulsing light), the next routine due within a day and
646
+ a half, or when it was last active (`GET /api/agent-status`). Agents linked
647
+ to a project are listed under the project too.
648
+
567
649
  A chat keeps the agent's own session. The ACP session id is stored with the
568
650
  chat (`sessions` in `meta.json`), and the next process — after an inspector
569
651
  restart, or once the idle reaper released the agent — resumes it over
@@ -593,8 +675,8 @@ and cost plus the model and thinking settings it lets you change live
593
675
  (`POST /api/chats/:id/config`). The agent's slash commands join the
594
676
  skills in the `/` menu. Each turn ends with the files the agent says it
595
677
  changed. Background tasks that can be stopped have a stop button
596
- (`POST /api/chats/:id/task-stop`). Deleting a chat deletes the agent's own
597
- sessions with it, and "continue a session" on the new-chat screen lists
678
+ (`POST /api/chats/:id/task-stop`). Deleting a chat moves it to the trash; the
679
+ agent's own sessions go when it is deleted from there, and "continue a session" on the new-chat screen lists
598
680
  the agent's sessions in the project (`GET /api/agent-sessions`) to pick
599
681
  one up as a chat.
600
682
 
@@ -821,6 +903,7 @@ kraftwerk knowledge search "refund" # full-text across bundl
821
903
  kraftwerk knowledge verify customer-support/playbooks/refunds --by human:user
822
904
  kraftwerk knowledge validate # OKF conformance + warnings
823
905
  kraftwerk knowledge fsck --fix # heal out-of-band edits (reindex)
906
+ kraftwerk knowledge remove customer-support # move the bundle to the trash
824
907
  ```
825
908
 
826
909
  Actors follow the OKF convention: `<producer>/<version>` for agents,
package/dist/acp.js CHANGED
@@ -157,7 +157,8 @@ export function divertExtensionUpdates(source, onExtension) {
157
157
  * representation of that work.
158
158
  */
159
159
  export async function connectAcp(agent, opts) {
160
- const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
160
+ // KRAFTWERK_ACP_ADAPTER: a script standing in for the adapter (the tests' scripted fake agent).
161
+ const entry = process.env.KRAFTWERK_ACP_ADAPTER || fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
161
162
  const child = spawn(process.execPath, [entry], {
162
163
  cwd: opts.cwd,
163
164
  stdio: ["pipe", "pipe", "pipe"],
@@ -0,0 +1,10 @@
1
+ import type { Command } from "commander";
2
+ /**
3
+ * `kraftwerk journal` — an agent's own memory across sessions
4
+ * (agents/<slug>/journal.md). Agents write it from their sessions; every
5
+ * new session starts with the recent part of it:
6
+ *
7
+ * kraftwerk journal <agent> print the journal
8
+ * kraftwerk journal <agent> "<entry>" [--kind ..] add a line under today (learned|decided|promised|done|note)
9
+ */
10
+ export declare function registerJournalCommands(program: Command): void;
@@ -0,0 +1,36 @@
1
+ import chalk from "chalk";
2
+ import { appendJournal, JOURNAL_KINDS, readJournal } from "../inspector/journal.js";
3
+ import { initContext as prepare } from "./routines.js";
4
+ /**
5
+ * `kraftwerk journal` — an agent's own memory across sessions
6
+ * (agents/<slug>/journal.md). Agents write it from their sessions; every
7
+ * new session starts with the recent part of it:
8
+ *
9
+ * kraftwerk journal <agent> print the journal
10
+ * kraftwerk journal <agent> "<entry>" [--kind ..] add a line under today (learned|decided|promised|done|note)
11
+ */
12
+ export function registerJournalCommands(program) {
13
+ program
14
+ .command("journal")
15
+ .description("An agent's journal: print it, or add one line (what it learned, decided, promised, did)")
16
+ .argument("<agent>", "Agent slug (folder under agents/)")
17
+ .argument("[entry]", "One line to add under today's date")
18
+ .option("--kind <kind>", `Kind of entry: ${JOURNAL_KINDS.join(" | ")}`, "note")
19
+ .option("--actor <actor>", "Who writes (default: the agent itself; KRAFTWERK_ACTOR)")
20
+ .action(async (agent, entry, opts) => {
21
+ await prepare();
22
+ try {
23
+ if (!entry) {
24
+ const text = await readJournal(agent);
25
+ console.log(text.trim() || chalk.dim(`${agent}'s journal is empty.`));
26
+ return;
27
+ }
28
+ await appendJournal(agent, entry, { kind: opts.kind, actor: opts.actor ?? process.env.KRAFTWERK_ACTOR });
29
+ console.log(`${chalk.green("✔")} added to ${chalk.cyan(agent)}'s journal`);
30
+ }
31
+ catch (err) {
32
+ console.error(chalk.red(err.message));
33
+ process.exit(2);
34
+ }
35
+ });
36
+ }
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { resolveProject } from "../config.js";
5
5
  import { fsck, getConcept, initBundle, knowledgeRoot, listBundles, listConcepts, searchKnowledge, validateBundle, verifyConcept, writeConcept, } from "../okf.js";
6
+ import { deleteBundle } from "../inspector/knowledge.js";
7
+ import { initContext as prepare } from "./routines.js";
6
8
  /**
7
9
  * `kraftwerk knowledge` — the CLI surface over OKF bundles (the
8
10
  * inspector's Knowledge screen). This is the enforced write path: `put` stamps provenance
@@ -182,6 +184,21 @@ export function registerKnowledgeCommands(program) {
182
184
  process.exit(2);
183
185
  }
184
186
  });
187
+ knowledge
188
+ .command("remove")
189
+ .description("Move a bundle to the trash (`kraftwerk trash` puts it back)")
190
+ .argument("<bundle>", "Bundle name")
191
+ .action(async (bundle) => {
192
+ try {
193
+ await prepare();
194
+ await deleteBundle(bundle);
195
+ console.log(`${chalk.green("✔")} moved ${chalk.cyan(bundle)} to the trash`);
196
+ }
197
+ catch (err) {
198
+ console.error(chalk.red(err.message));
199
+ process.exit(2);
200
+ }
201
+ });
185
202
  knowledge
186
203
  .command("validate")
187
204
  .description("OKF conformance check (errors) + provenance/staleness warnings")
@@ -16,6 +16,8 @@ import { registerProjectCommands } from "./projects.js";
16
16
  import { registerWorkspaceCommands } from "./workspaces.js";
17
17
  import { registerRepoCommands } from "./repos.js";
18
18
  import { registerVibeableCommands } from "./vibeables.js";
19
+ import { registerTrashCommands } from "./trash.js";
20
+ import { registerJournalCommands } from "./journal.js";
19
21
  import { registerTunnelCommands } from "./tunnel.js";
20
22
  import { applyDotenv } from "../dotenv.js";
21
23
  import { resolveProject } from "../config.js";
@@ -36,6 +38,8 @@ import { runUi } from "./ui.js";
36
38
  * kraftwerk projects ... goal-scoped project folders (list/create/show/link/log/remove)
37
39
  * kraftwerk vibeables ... small apps built live in a chat (list/create/remove)
38
40
  * kraftwerk repos ... repositories the agents work on (list/add/update/remove)
41
+ * kraftwerk journal <agent> [entry] an agent's memory across sessions: print it, add a line
42
+ * kraftwerk trash ... what was deleted: list, restore, purge, empty
39
43
  * kraftwerk tunnel [setup <host>] Cloudflare Tunnel to the inspector: run it alone, or set one up
40
44
  * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
41
45
  * kraftwerk validate [paths...] validate without executing
@@ -286,6 +290,8 @@ registerWorkspaceCommands(program);
286
290
  registerProjectCommands(program);
287
291
  registerRepoCommands(program);
288
292
  registerVibeableCommands(program);
293
+ registerTrashCommands(program);
294
+ registerJournalCommands(program);
289
295
  registerTunnelCommands(program);
290
296
  program
291
297
  .command("ui")
@@ -19,7 +19,7 @@ import { fmtAgo } from "./workspaces.js";
19
19
  * kraftwerk projects link <slug> <kind> <name> add a knowledge|vibeables|repos|workflows|agents link
20
20
  * kraftwerk projects unlink <slug> <kind> <name> remove one
21
21
  * kraftwerk projects log <slug> "<entry>" append a stamped line to log.md (--actor)
22
- * kraftwerk projects remove <slug> delete the folder (its history stays in the workspace git)
22
+ * kraftwerk projects remove <slug> move the folder to the trash
23
23
  *
24
24
  * The workspace registry answered to this name until 0.48; it is
25
25
  * `kraftwerk workspaces` now (workspaces.ts).
@@ -208,13 +208,13 @@ export function registerProjectCommands(program) {
208
208
  });
209
209
  projects
210
210
  .command("remove")
211
- .description("Delete a project folder; its history stays in the workspace git")
211
+ .description("Move a project folder to the trash (`kraftwerk trash` puts it back)")
212
212
  .argument("<slug>", "Folder name under the root")
213
213
  .action(async (slug) => {
214
214
  await prepare();
215
215
  try {
216
216
  await deleteProject(slug);
217
- console.log(`${chalk.green("✔")} removed ${chalk.cyan(slug)}`);
217
+ console.log(`${chalk.green("✔")} moved ${chalk.cyan(slug)} to the trash`);
218
218
  }
219
219
  catch (err) {
220
220
  die(err.message);
package/dist/cli/repos.js CHANGED
@@ -110,6 +110,6 @@ export function registerRepoCommands(program) {
110
110
  const r = await removeRepo(name, !!opts.force).catch((err) => ({ ok: false, error: err.message }));
111
111
  if (!r.ok)
112
112
  die(r.error ?? "remove failed");
113
- console.log(`${chalk.green("✔")} removed ${chalk.cyan(name)}`);
113
+ console.log(`${chalk.green("✔")} moved ${chalk.cyan(name)} to the trash`);
114
114
  });
115
115
  }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerTrashCommands(program: Command): void;
@@ -0,0 +1,70 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { emptyTrash, listTrash, purgeFromTrash, restoreFromTrash, trashRoot } from "../inspector/trash.js";
4
+ import { fmtAgo } from "./workspaces.js";
5
+ import { initContext as prepare } from "./routines.js";
6
+ /**
7
+ * `kraftwerk trash` — what was deleted (agents, projects, knowledge bundles,
8
+ * vibeables, channels, repos, chats, runs), kept under kraftwerk-data/trash
9
+ * until it is deleted from there:
10
+ *
11
+ * kraftwerk trash list: id, what it was, when
12
+ * kraftwerk trash restore <id> put an entry back where it came from
13
+ * kraftwerk trash purge <id> delete one entry for good
14
+ * kraftwerk trash empty delete everything in the trash for good
15
+ */
16
+ const die = (msg) => {
17
+ console.error(chalk.red(msg));
18
+ process.exit(1);
19
+ };
20
+ export function registerTrashCommands(program) {
21
+ const trash = program.command("trash").description("What was deleted: list, restore, purge one, empty");
22
+ trash
23
+ .command("list", { isDefault: true })
24
+ .description("List the trash, newest first")
25
+ .option("--json", "Machine-readable output")
26
+ .action(async (opts) => {
27
+ await prepare();
28
+ const entries = await listTrash();
29
+ if (opts.json) {
30
+ console.log(JSON.stringify(entries, null, 2));
31
+ return;
32
+ }
33
+ if (entries.length === 0) {
34
+ console.log(chalk.dim("The trash is empty."));
35
+ return;
36
+ }
37
+ const table = new Table({ head: ["id", "was", "deleted"].map((h) => chalk.bold(h)), wordWrap: true });
38
+ for (const e of entries)
39
+ table.push([chalk.cyan(e.id), chalk.dim(e.from), chalk.dim(fmtAgo(e.trashedAt))]);
40
+ console.log(table.toString());
41
+ console.log(chalk.dim(await trashRoot()));
42
+ });
43
+ for (const [verb, fn, done] of [
44
+ ["restore", restoreFromTrash, "restored"],
45
+ ["purge", purgeFromTrash, "deleted for good:"],
46
+ ]) {
47
+ trash
48
+ .command(verb)
49
+ .description(verb === "restore" ? "Put an entry back where it came from" : "Delete one entry for good")
50
+ .argument("<id>", "Entry id as `kraftwerk trash` lists it, e.g. knowledge/customer-support")
51
+ .action(async (id) => {
52
+ await prepare();
53
+ try {
54
+ const e = await fn(id);
55
+ console.log(`${chalk.green("✔")} ${done} ${chalk.cyan(e.from)}`);
56
+ }
57
+ catch (err) {
58
+ die(err.message);
59
+ }
60
+ });
61
+ }
62
+ trash
63
+ .command("empty")
64
+ .description("Delete everything in the trash for good")
65
+ .action(async () => {
66
+ await prepare();
67
+ const n = await emptyTrash();
68
+ console.log(`${chalk.green("✔")} deleted ${n} entr${n === 1 ? "y" : "ies"} for good`);
69
+ });
70
+ }
@@ -11,7 +11,7 @@ import { initContext as prepare } from "./routines.js";
11
11
  *
12
12
  * kraftwerk vibeables list: name, mode, state
13
13
  * kraftwerk vibeables create <name> new app from the starter (index.html, vibeable.yml)
14
- * kraftwerk vibeables remove <name> delete the folder (its history stays in the workspace git)
14
+ * kraftwerk vibeables remove <name> move the folder to the trash
15
15
  */
16
16
  const die = (msg) => {
17
17
  console.error(chalk.red(msg));
@@ -74,13 +74,13 @@ export function registerVibeableCommands(program) {
74
74
  });
75
75
  vibeables
76
76
  .command("remove")
77
- .description("Delete an app folder; its history stays in the workspace git")
77
+ .description("Move an app folder to the trash (`kraftwerk trash` puts it back)")
78
78
  .argument("<name>", "Folder name under the root")
79
79
  .action(async (name) => {
80
80
  await prepare();
81
81
  try {
82
82
  await deleteVibeable(name);
83
- console.log(`${chalk.green("✔")} removed ${chalk.cyan(name)}`);
83
+ console.log(`${chalk.green("✔")} moved ${chalk.cyan(name)} to the trash`);
84
84
  }
85
85
  catch (err) {
86
86
  die(err.message);
@@ -0,0 +1,21 @@
1
+ import { type AgentActivity } from "./chat/sessions.js";
2
+ /**
3
+ * What each agent is up to, for the status line under its name: waiting on
4
+ * a human, working (where, since when), the next scheduled routine, and
5
+ * when it was last active. The UI picks the one line that matters most.
6
+ */
7
+ export interface AgentStatus {
8
+ /** Chats where it waits for an approval or an answer. */
9
+ waiting: AgentActivity[];
10
+ /** Chats where it is mid-turn. */
11
+ working: AgentActivity[];
12
+ /** The next enabled routine due. */
13
+ nextRoutine?: {
14
+ id: string;
15
+ name: string;
16
+ at: string;
17
+ };
18
+ /** Newest change in any of its sessions or channels. */
19
+ lastActiveAt?: string;
20
+ }
21
+ export declare function agentStatuses(): Promise<Record<string, AgentStatus>>;
@@ -0,0 +1,25 @@
1
+ import { listAgents } from "./agents.js";
2
+ import { agentActivity, listChats } from "./chat/sessions.js";
3
+ import { routineStatuses } from "./routines.js";
4
+ export async function agentStatuses() {
5
+ const [agents, chats] = await Promise.all([listAgents(), listChats()]);
6
+ const live = agentActivity();
7
+ const out = {};
8
+ await Promise.all(agents.map(async (a) => {
9
+ const act = live.get(a.slug);
10
+ const routines = await routineStatuses(a.slug).catch(() => []);
11
+ const next = routines
12
+ .filter((r) => r.nextRunAt)
13
+ .sort((x, y) => x.nextRunAt.localeCompare(y.nextRunAt))[0];
14
+ // A channel chat counts once the agent has a session there (sessions are keyed by agent slug).
15
+ const mine = chats.filter((c) => (c.scope.kind === "agent" && c.scope.slug === a.slug) || (c.scope.kind === "channel" && c.sessions?.[a.slug]));
16
+ const last = mine.reduce((m, c) => (!m || c.updatedAt > m ? c.updatedAt : m), undefined);
17
+ out[a.slug] = {
18
+ waiting: act?.waiting ?? [],
19
+ working: act?.working ?? [],
20
+ ...(next ? { nextRoutine: { id: next.id, name: next.name, at: next.nextRunAt } } : {}),
21
+ ...(last ? { lastActiveAt: last } : {}),
22
+ };
23
+ }));
24
+ return out;
25
+ }
@@ -27,6 +27,8 @@ export interface Agent {
27
27
  knowledge: string[];
28
28
  /** Vibeables (folder names under the vibeables root) this member builds and maintains. */
29
29
  vibeables: string[];
30
+ /** Workspace file folders (paths under kraftwerk-data/files) this agent works with; agents own no files. */
31
+ files?: string[];
30
32
  /**
31
33
  * Skill allowlist (names from .claude/skills, project or user level).
32
34
  * Absent = all discovered skills; empty list = no skills.
@@ -67,6 +69,8 @@ export interface SaveAgentInput {
67
69
  workflows?: string[];
68
70
  knowledge?: string[];
69
71
  vibeables?: string[];
72
+ /** Linked workspace file folders; omitted = keep what agent.yml has. */
73
+ files?: string[];
70
74
  /** Omit for "all skills"; a list (possibly empty) restricts to those names. */
71
75
  skills?: string[];
72
76
  system?: string;
@@ -74,4 +78,5 @@ export interface SaveAgentInput {
74
78
  export declare function saveAgent(input: SaveAgentInput): Promise<AgentDetail>;
75
79
  /** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
76
80
  export declare function setAgentArchived(slug: string, archived: boolean): Promise<AgentDetail>;
81
+ /** Move the agent's folder to the trash. */
77
82
  export declare function deleteAgent(slug: string): Promise<void>;
@@ -4,6 +4,7 @@ import { parse, stringify } from "yaml";
4
4
  import { resolveProject } from "../config.js";
5
5
  import { getProjectRoot } from "./context.js";
6
6
  import { syncWorkspaceAgents } from "./instances.js";
7
+ import { moveToTrash } from "./trash.js";
7
8
  /**
8
9
  * Agents: persistent agents, defined on the filesystem. Each one
9
10
  * is one folder under the project's agents/ root:
@@ -60,6 +61,7 @@ function normalize(slug, raw) {
60
61
  workflows: Array.isArray(raw.workflows) ? raw.workflows.map(String) : [],
61
62
  knowledge: Array.isArray(raw.knowledge) ? raw.knowledge.map(String) : [],
62
63
  vibeables: Array.isArray(raw.vibeables) ? raw.vibeables.map(String) : [],
64
+ ...(Array.isArray(raw.files) && raw.files.length ? { files: raw.files.map(String) } : {}),
63
65
  ...(Array.isArray(raw.skills) ? { skills: raw.skills.map(String) } : {}),
64
66
  ...(raw.archived === true ? { archived: true } : {}),
65
67
  };
@@ -135,6 +137,12 @@ export async function saveAgent(input) {
135
137
  knowledge: (input.knowledge ?? []).map(String),
136
138
  // Empty list left out: most agents build nothing, and agent.yml stays as short as before.
137
139
  ...(input.vibeables?.length ? { vibeables: [...new Set(input.vibeables.map((v) => String(v).trim()).filter(Boolean))] } : {}),
140
+ ...(() => {
141
+ // Linked folders survive an edit from a form that does not know them.
142
+ const files = input.files ?? (Array.isArray(existing?.files) ? existing.files.map(String) : []);
143
+ const clean = [...new Set(files.map((f) => String(f).trim().replace(/^\/+|\/+$/g, "")).filter(Boolean))];
144
+ return clean.length ? { files: clean } : {};
145
+ })(),
138
146
  ...(input.skills ? { skills: input.skills.map(String) } : {}),
139
147
  ...(existing?.archived === true ? { archived: true } : {}),
140
148
  };
@@ -160,7 +168,8 @@ export async function setAgentArchived(slug, archived) {
160
168
  await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
161
169
  return (await getAgent(slug));
162
170
  }
171
+ /** Move the agent's folder to the trash. */
163
172
  export async function deleteAgent(slug) {
164
173
  const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
165
- await fs.rm(dir, { recursive: true, force: true });
174
+ await moveToTrash("agents", slug, dir);
166
175
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What needs you: every place where work is stuck on a human, with whose it
3
+ * is. Two sources: requests waiting in a live chat (an approval, a question
4
+ * — the agent cannot go on until someone answers) and failures nobody has
5
+ * looked at yet (a routine or a workflow run that ended badly; unread
6
+ * failure notifications). Oldest first: what waits longest comes first.
7
+ *
8
+ * The inspector builds everything urgent from this one list: the counts on
9
+ * the rail, the "needs you" part of the bell, the tab title and "next",
10
+ * which walks through it and lands on each item with its context.
11
+ */
12
+ /** Who an item belongs to — what the rail lights up and the bell groups by. */
13
+ export interface AttentionOwner {
14
+ project?: {
15
+ slug: string;
16
+ title: string;
17
+ };
18
+ agent?: {
19
+ slug: string;
20
+ name: string;
21
+ emoji?: string;
22
+ };
23
+ channel?: string;
24
+ /** A chat with Ralv, the workspace's general assistant. */
25
+ general?: boolean;
26
+ /** A workflow run. */
27
+ workflow?: string;
28
+ }
29
+ export interface AttentionItem {
30
+ /** The request id (live requests) or the notification id (failures). */
31
+ id: string;
32
+ kind: "approval" | "question" | "failed";
33
+ /** What is asked, or what went wrong. */
34
+ title: string;
35
+ /** The session it sits in. */
36
+ chatId?: string;
37
+ chatTitle?: string;
38
+ /** Where to go: a chat route with ?focus=<request id>, or the failed run or routine. */
39
+ href: string;
40
+ since: string;
41
+ owner: AttentionOwner;
42
+ /** Failures: the notification to mark read once the item was opened. */
43
+ notificationId?: string;
44
+ }
45
+ export interface AttentionView {
46
+ items: AttentionItem[];
47
+ }
48
+ export declare function listAttention(): Promise<AttentionView>;
@@ -0,0 +1,66 @@
1
+ import { listAgents } from "./agents.js";
2
+ import { waitingRequests } from "./chat/sessions.js";
3
+ import { chatHref, listNotifications } from "./notifications.js";
4
+ import { listProjects } from "./projects.js";
5
+ export async function listAttention() {
6
+ const [agents, projects, notes] = await Promise.all([
7
+ listAgents().catch(() => []),
8
+ listProjects().catch(() => ({ projects: [] })),
9
+ listNotifications().catch(() => ({ items: [] })),
10
+ ]);
11
+ const agentOf = (slug) => {
12
+ const a = agents.find((x) => x.slug === slug);
13
+ return { slug, name: a?.name ?? slug, ...(a?.emoji ? { emoji: a.emoji } : {}) };
14
+ };
15
+ const projectOf = (slug) => ({
16
+ slug,
17
+ title: projects.projects.find((p) => p.slug === slug)?.title ?? slug,
18
+ });
19
+ const waiting = waitingRequests().map((w) => {
20
+ const { scope } = w.meta;
21
+ const owner = {};
22
+ if (scope.kind === "agent")
23
+ owner.agent = agentOf(scope.slug);
24
+ if (scope.kind === "project")
25
+ owner.project = projectOf(scope.slug);
26
+ if (scope.kind === "channel") {
27
+ owner.channel = scope.slug;
28
+ owner.agent = agentOf(w.seat);
29
+ if (w.meta.project)
30
+ owner.project = projectOf(w.meta.project);
31
+ }
32
+ if (scope.kind === "general" || scope.kind === "kraftwerk" || scope.kind === "knowledge")
33
+ owner.general = true;
34
+ return {
35
+ id: w.requestId,
36
+ kind: w.kind,
37
+ title: w.title,
38
+ chatId: w.meta.id,
39
+ ...(w.meta.title ? { chatTitle: w.meta.title } : {}),
40
+ href: `${chatHref(w.meta)}?focus=${encodeURIComponent(w.requestId)}`,
41
+ since: w.since,
42
+ owner,
43
+ };
44
+ });
45
+ const failed = notes.items
46
+ .filter((n) => !n.readAt && (n.kind === "routine_failed" || n.kind === "run_failed"))
47
+ .map((n) => {
48
+ const owner = {};
49
+ if (n.diagnose?.kind === "routine")
50
+ owner.agent = agentOf(n.diagnose.agent);
51
+ if (n.diagnose?.kind === "run")
52
+ owner.workflow = n.title.replace(/ run .*$/, "");
53
+ return {
54
+ id: n.id,
55
+ kind: "failed",
56
+ title: n.body ? `${n.title}: ${n.body.split("\n")[0]}` : n.title,
57
+ ...(n.diagnose?.kind === "routine" && n.diagnose.chatId ? { chatId: n.diagnose.chatId } : {}),
58
+ href: n.href,
59
+ since: n.at,
60
+ owner,
61
+ notificationId: n.id,
62
+ };
63
+ });
64
+ const items = [...waiting, ...failed].sort((a, b) => a.since.localeCompare(b.since));
65
+ return { items };
66
+ }
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
4
  import { getProjectRoot } from "./context.js";
5
+ import { moveToTrash } from "./trash.js";
5
6
  import { resolveProject } from "../config.js";
6
7
  import { listAgents, safeAgentSlug, slugFromName } from "./agents.js";
7
8
  import { syncWorkspaceChannels } from "./instances.js";
@@ -130,7 +131,7 @@ export async function saveChannel(input) {
130
131
  return channel;
131
132
  }
132
133
  export async function deleteChannel(slug) {
133
- await fs.rm(path.join(await channelsRoot(), safeChannelSlug(slug)), { recursive: true, force: true });
134
+ await moveToTrash("channels", slug, path.join(await channelsRoot(), safeChannelSlug(slug)));
134
135
  }
135
136
  /**
136
137
  * Who a channel message wakes. @mentions of members win (in text order, each