@netnodeag/kraftwerk 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +26 -19
  2. package/dist/cli/init.js +3 -3
  3. package/dist/cli/routines.js +9 -9
  4. package/dist/config.d.ts +2 -2
  5. package/dist/config.js +1 -1
  6. package/dist/inspector/{team.d.ts → agents.d.ts} +21 -12
  7. package/dist/inspector/{team.js → agents.js} +34 -19
  8. package/dist/inspector/chat/backend.d.ts +1 -1
  9. package/dist/inspector/chat/sessions.js +43 -43
  10. package/dist/inspector/chat/store.js +13 -1
  11. package/dist/inspector/chat/types.d.ts +2 -2
  12. package/dist/inspector/instances.d.ts +12 -0
  13. package/dist/inspector/instances.js +28 -0
  14. package/dist/inspector/routines.d.ts +4 -4
  15. package/dist/inspector/routines.js +11 -11
  16. package/dist/inspector/search.d.ts +25 -0
  17. package/dist/inspector/search.js +31 -0
  18. package/dist/inspector/server.js +34 -29
  19. package/dist/inspector/skills.d.ts +6 -6
  20. package/dist/inspector/skills.js +8 -8
  21. package/inspector/dist/assets/{editor-BABv2LmA.js → editor-7ImnDC3D.js} +3 -3
  22. package/inspector/dist/assets/{index-DbWUNbt-.js → index--2I7vpcS.js} +1 -1
  23. package/inspector/dist/assets/index-B2ThRS2S.js +217 -0
  24. package/inspector/dist/assets/{index-BXspF2KR.css → index-BA7nx4V5.css} +1 -1
  25. package/inspector/dist/assets/{index-CdwTmXTV.js → index-BC7aIJ-h.js} +1 -1
  26. package/inspector/dist/assets/{index-DuPp3MUc.js → index-BIXYBiw7.js} +1 -1
  27. package/inspector/dist/assets/{index-Bmm4zJNU.js → index-BZeYO5wT.js} +1 -1
  28. package/inspector/dist/assets/{index-sLxulPOm.js → index-B_CIhKa4.js} +1 -1
  29. package/inspector/dist/assets/{index-DCR7u4Xa.js → index-BuRajvjQ.js} +1 -1
  30. package/inspector/dist/assets/{index-Ca9Qs5LZ.js → index-C3Ob1O7W.js} +1 -1
  31. package/inspector/dist/assets/{index-HBdsdJhD.js → index-CIjv7yxE.js} +1 -1
  32. package/inspector/dist/assets/{index-wkYwz0Tl.js → index-CRAwmMfk.js} +1 -1
  33. package/inspector/dist/assets/{index-ByWpqzAI.js → index-Ct_jZSyD.js} +1 -1
  34. package/inspector/dist/assets/{index-hkVov8T4.js → index-DKrBjjGQ.js} +1 -1
  35. package/inspector/dist/assets/{index-Gx93brFq.js → index-DL_bJiOj.js} +1 -1
  36. package/inspector/dist/assets/{index-BbTer62d.js → index-DPBAv_kj.js} +1 -1
  37. package/inspector/dist/assets/{index-CCACy6uF.js → index-DbmSSHvr.js} +1 -1
  38. package/inspector/dist/assets/{index-DuwmzZAr.js → index-DjRBLY9N.js} +1 -1
  39. package/inspector/dist/assets/{index-CzPQygR7.js → index-YZtBJhWD.js} +1 -1
  40. package/inspector/dist/assets/{index-CjlJeW9z.js → index-fWUFhIGR.js} +1 -1
  41. package/inspector/dist/index.html +2 -2
  42. package/package.json +1 -1
  43. package/inspector/dist/assets/index-uEig4--d.js +0 -217
package/README.md CHANGED
@@ -179,7 +179,7 @@ different host.
179
179
  Optional, at the project root, and also the root marker for the walk-up. All
180
180
  fields are optional. `workflows:` sets the workflows root, `output:` the
181
181
  run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
182
- (default `knowledge/`), and `agents:` the team agent-definition root (default
182
+ (default `knowledge/`), and `agents:` the agent-definition root (default
183
183
  `agents/`).
184
184
 
185
185
  `switcher:` links other kraftwerk workspaces from the inspector header. The
@@ -192,6 +192,13 @@ switcher:
192
192
  icon: "🛰" # optional
193
193
  ```
194
194
 
195
+ **⌘K** (Ctrl K) anywhere in the inspector opens the agent palette: type a
196
+ few letters of an agent's name, description or workspace and hit enter to
197
+ jump to it. It lists the active agents of every workspace on this machine,
198
+ running or not (a stopped one is started on the way), read from the
199
+ project registry under `~/.kraftwerk/projects`, which every inspector keeps
200
+ current with its roster.
201
+
195
202
  ### Triggering from CI, cron, or webhooks
196
203
 
197
204
  `run --json` is the machine mode. It runs non-interactively, prints one JSON
@@ -249,15 +256,15 @@ repo. Agent logins made inside the container persist in the `agent-home`
249
256
  volume. This is a different image from the `kraftwerk-runner` sandbox
250
257
  (`runner/Dockerfile`) used by `run --sandbox`.
251
258
 
252
- ## Persistent team agents
259
+ ## Persistent agents
253
260
 
254
- The inspector's "team" screen turns chat agents into persistent teammates. A
255
- team member is one folder under the project's `agents/` root:
261
+ The inspector's "agents" screen turns chat agents into persistent teammates. An
262
+ agent is one folder under the project's `agents/` root:
256
263
 
257
264
  ```
258
265
  agents/max/
259
266
  agent.yml # name, emoji, description, harness, model, effort, workflows
260
- system.md # the member's system prompt (its role)
267
+ system.md # the agent's system prompt (its role)
261
268
  ```
262
269
 
263
270
  ```yaml
@@ -269,43 +276,43 @@ harness: claude # claude | codex | pi (which chat backend runs it)
269
276
  model: sonnet # optional; harness default when omitted
270
277
  effort: medium # optional: low | medium | high | xhigh | max
271
278
  workflows: [tagline, website-check]
272
- knowledge: [customer-support] # OKF bundles the member consults & maintains
279
+ knowledge: [customer-support] # OKF bundles the agent consults & maintains
273
280
  skills: [report-html] # optional allowlist; omit = all skills, [] = none
274
281
  ```
275
282
 
276
- Sessions with a member are ordinary chats scoped `{ kind: "team", member }`,
277
- listed per member in a second sidebar. On the first message the member gets
283
+ Sessions with an agent are ordinary chats scoped `{ kind: "agent", slug }`,
284
+ listed per agent in a second sidebar. On the first message the agent gets
278
285
  its role plus its connected workflows and knowledge bundles injected as
279
286
  context. That context includes how to run workflows
280
287
  (`KRAFTWERK_YES=1 npx kraftwerk run <workflow> "<request>"`) and how to read
281
288
  and write knowledge through `kraftwerk knowledge`, with writes stamped with
282
- the member's own actor, `<slug>/<harness>`. So the member triggers its own
289
+ the agent's own actor, `<slug>/<harness>`. So the agent triggers its own
283
290
  workflows when a request matches, and keeps its bundles current.
284
291
 
285
292
  Model and effort ride on backend-specific channels. The claude adapter takes
286
293
  the model via ACP session options and the thinking budget via
287
294
  `MAX_THINKING_TOKENS`. Codex gets a `CODEX_CONFIG` env override (`model`,
288
- `model_reasoning_effort`). Pi gets `--model` and `--thinking` flags. Members
295
+ `model_reasoning_effort`). Pi gets `--model` and `--thinking` flags. Agents
289
296
  are created and edited in the UI, or by editing the files, since the
290
297
  definition is read fresh for each new session.
291
298
 
292
299
  ### Skills in chat
293
300
 
294
- Chats, both general ones and team sessions, can use skills. These are
301
+ Chats, both general ones and agent sessions, can use skills. These are
295
302
  Claude-style instruction packages, one folder per skill with a `SKILL.md`
296
303
  holding YAML frontmatter (`name`, `description`) and then the instructions.
297
304
  Four roots are discovered, each shadowing same-named skills in the roots below
298
305
  it:
299
306
 
300
307
  ```
301
- <project>/agents/<slug>/skills/<name>/SKILL.md # private to that one team agent
308
+ <project>/agents/<slug>/skills/<name>/SKILL.md # private to that one agent
302
309
  <project>/<skills root>/<name>/SKILL.md # workspace, git-tracked (kraftwerk.yml `skills`, default skills/)
303
310
  <project>/.claude/skills/<name>/SKILL.md # git-tracked, per project
304
311
  ~/.claude/skills/<name>/SKILL.md # personal, per user
305
312
  ```
306
313
 
307
314
  Agent skills are visible only to that agent's sessions and always apply. A
308
- member's `skills:` allowlist narrows the shared roots only. Manage them in the
315
+ agent's `skills:` allowlist narrows the shared roots only. Manage them in the
309
316
  agent profile under "own skills", or by editing the files.
310
317
 
311
318
  Every chat lists its visible skills as context under "## Your skills", so the
@@ -313,16 +320,16 @@ agent reaches for one when the request matches. Typing `/` in the composer
313
320
  opens an autocomplete over them. Sending `/<name> <args>` expands the skill's
314
321
  SKILL.md into the prompt, which is what makes this work identically on claude,
315
322
  codex, and pi. On top of that, claude discovers `.claude/skills` natively (a
316
- team member's `skills:` allowlist narrows that via ACP session options) and pi
317
- loads each visible skill folder via `--skill`. Team members take an optional
323
+ agent's `skills:` allowlist narrows that via ACP session options) and pi
324
+ loads each visible skill folder via `--skill`. Agents take an optional
318
325
  `skills:` list in `agent.yml`. Omitted means all discovered skills, an empty
319
326
  list means none, otherwise it is the allowlist. `GET /api/skills` returns
320
327
  what's discovered.
321
328
 
322
329
  ### Routines, or scheduled prompts
323
330
 
324
- A member can have routines, cron-scheduled prompts that work like standing
325
- orders for an employee. Definitions live next to the member in
331
+ An agent can have routines, cron-scheduled prompts that work like standing
332
+ orders for an employee. Definitions live next to the agent in
326
333
  `agents/<slug>/routines.yml` and are git-tracked. Run state (last run, last
327
334
  session, errors) lives in `<output>/routines-state.json`.
328
335
 
@@ -338,12 +345,12 @@ session, errors) lives in `<output>/routines-state.json`.
338
345
  ```
339
346
 
340
347
  The inspector server runs the scheduler in-process, so there is no external
341
- cron to set up. Every due routine opens a fresh session for the member, posts
348
+ cron to set up. Every due routine opens a fresh session for the agent, posts
342
349
  the prompt, and shows up in the sessions sidebar titled "⏰ <name>". Routine
343
350
  sessions run unattended, so tool-permission requests are auto-approved, with
344
351
  the request and approval pair left in the thread as an audit trail. Give
345
352
  routine prompts the same trust you would give a `KRAFTWERK_YES=1` workflow
346
- run. Manage routines on the member page, where you can create, edit and
353
+ run. Manage routines on the agent page, where you can create, edit and
347
354
  delete them, toggle enabled, hit "run now", and jump to the last run's
348
355
  session. Schedules missed while the server is down are skipped, not replayed.
349
356
 
package/dist/cli/init.js CHANGED
@@ -6,7 +6,7 @@ import { initBundle, writeConcept } from "../okf.js";
6
6
  /**
7
7
  * `kraftwerk init` — make any repository a kraftwerk consumer in one
8
8
  * command: kraftwerk.yml (root marker + config), a kraftwerk-data/ tree with
9
- * a runnable example workflow, a starter team agent ("max"), a demo
9
+ * a runnable example workflow, a starter agent ("max"), a demo
10
10
  * knowledge bundle, and an output ignore entry. Everything is idempotent:
11
11
  * existing files are left untouched and reported.
12
12
  */
@@ -20,7 +20,7 @@ port: 1981 # port \`kraftwerk ui\` listens on
20
20
  workflows: ${DATA_DIR}/workflows # where workflows live
21
21
  output: ${DATA_DIR}/output # where run artifacts land (git-ignored)
22
22
  knowledge: ${DATA_DIR}/knowledge # OKF knowledge bundles
23
- agents: ${DATA_DIR}/agents # team agent definitions
23
+ agents: ${DATA_DIR}/agents # agent definitions
24
24
  skills: ${DATA_DIR}/skills # workspace skills (shared instruction packages)
25
25
  `;
26
26
  const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
@@ -107,7 +107,7 @@ export async function runInit(cwd) {
107
107
  await put(`${DATA_DIR}/workflows/hello/workflow.yml`, WORKFLOW_TEMPLATE);
108
108
  await put(`${DATA_DIR}/workflows/hello/prompts/assistant.md`, PERSONA_TEMPLATE);
109
109
  await put(`${DATA_DIR}/workflows/hello/prompts/answer.md`, PROMPT_TEMPLATE);
110
- // Team agent "max".
110
+ // Agent "max".
111
111
  await put(`${DATA_DIR}/agents/max/agent.yml`, AGENT_YML_TEMPLATE);
112
112
  await put(`${DATA_DIR}/agents/max/system.md`, AGENT_SYSTEM_TEMPLATE);
113
113
  // Example workspace skill.
@@ -4,22 +4,22 @@ import chalk from "chalk";
4
4
  import { resolveProject } from "../config.js";
5
5
  import { setOutputDir, setProjectRoot } from "../inspector/context.js";
6
6
  import { deleteRoutine, listRoutines, routineStatuses, saveRoutine, } from "../inspector/routines.js";
7
- import { getMember, listMembers } from "../inspector/team.js";
7
+ import { getAgent, listAgents } from "../inspector/agents.js";
8
8
  /**
9
9
  * `kraftwerk routines` — CLI surface over per-agent scheduled prompts
10
10
  * (agents/<slug>/routines.yml). Definition CRUD works directly on the
11
11
  * files; `run` fires through the running inspector server, because the
12
12
  * session (chat) and the scheduler live there.
13
13
  */
14
- /** Point the inspector context at this project so team/routine paths resolve. */
14
+ /** Point the inspector context at this project so agent/routine paths resolve. */
15
15
  async function initContext() {
16
16
  const project = await resolveProject(process.cwd());
17
17
  setProjectRoot(project.root);
18
18
  setOutputDir(project.outputDir);
19
19
  return { port: project.config.port ?? 1981 };
20
20
  }
21
- async function requireMember(slug) {
22
- if (!(await getMember(slug))) {
21
+ async function requireAgent(slug) {
22
+ if (!(await getAgent(slug))) {
23
23
  console.error(chalk.red(`Agent "${slug}" not found (expected agents/${slug}/agent.yml).`));
24
24
  process.exit(2);
25
25
  }
@@ -56,9 +56,9 @@ export function registerRoutineCommands(program) {
56
56
  .option("--json", "Machine-readable output")
57
57
  .action(async (agent, opts) => {
58
58
  await initContext();
59
- const slugs = agent ? [agent] : (await listMembers()).map((m) => m.slug);
59
+ const slugs = agent ? [agent] : (await listAgents()).map((m) => m.slug);
60
60
  if (agent)
61
- await requireMember(agent);
61
+ await requireAgent(agent);
62
62
  const all = {};
63
63
  for (const slug of slugs)
64
64
  all[slug] = await routineStatuses(slug);
@@ -89,7 +89,7 @@ export function registerRoutineCommands(program) {
89
89
  .option("--disabled", "Create it switched off")
90
90
  .action(async (agent, opts) => {
91
91
  await initContext();
92
- await requireMember(agent);
92
+ await requireAgent(agent);
93
93
  const prompt = opts.prompt ??
94
94
  (opts.file ? await readFile(path.resolve(opts.file), "utf8") : process.stdin.isTTY ? "" : await readStdin());
95
95
  if (!prompt.trim()) {
@@ -124,7 +124,7 @@ export function registerRoutineCommands(program) {
124
124
  .argument("<id>", "Routine id (see `kraftwerk routines <agent>`)")
125
125
  .action(async (agent, id) => {
126
126
  await initContext();
127
- await requireMember(agent);
127
+ await requireAgent(agent);
128
128
  if (!(await listRoutines(agent)).some((r) => r.id === id)) {
129
129
  console.error(chalk.red(`Routine "${agent}/${id}" not found.`));
130
130
  process.exit(2);
@@ -158,7 +158,7 @@ export function registerRoutineCommands(program) {
158
158
  .action(async (agent, id, opts) => {
159
159
  const { port } = await initContext();
160
160
  const p = opts.port ? Number(opts.port) : port;
161
- const url = `http://localhost:${p}/api/team/${encodeURIComponent(agent)}/routines/${encodeURIComponent(id)}/run`;
161
+ const url = `http://localhost:${p}/api/agents/${encodeURIComponent(agent)}/routines/${encodeURIComponent(id)}/run`;
162
162
  let body;
163
163
  try {
164
164
  body = (await (await fetch(url, { method: "POST" })).json());
package/dist/config.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * workflows: src/workflows # workflows root, relative to the file
16
16
  * output: output # run-artifact directory, relative to the file
17
17
  * knowledge: knowledge # OKF knowledge-bundle root, relative to the file
18
- * agents: agents # team agent-definition root, relative to the file
18
+ * agents: agents # agent-definition root, relative to the file
19
19
  * skills: skills # workspace skill root, relative to the file
20
20
  * switcher: # other kraftwerk workspaces, linked from the header dropdown
21
21
  * - name: other space
@@ -71,7 +71,7 @@ export interface ProjectConfig {
71
71
  output?: string;
72
72
  /** OKF knowledge-bundle root relative to the project root. Default: knowledge */
73
73
  knowledge?: string;
74
- /** Team agent-definition root relative to the project root. Default: agents */
74
+ /** Agent-definition root relative to the project root. Default: agents */
75
75
  agents?: string;
76
76
  /** Workspace skill root relative to the project root. Default: skills */
77
77
  skills?: string;
package/dist/config.js CHANGED
@@ -18,7 +18,7 @@ import { parse } from "yaml";
18
18
  * workflows: src/workflows # workflows root, relative to the file
19
19
  * output: output # run-artifact directory, relative to the file
20
20
  * knowledge: knowledge # OKF knowledge-bundle root, relative to the file
21
- * agents: agents # team agent-definition root, relative to the file
21
+ * agents: agents # agent-definition root, relative to the file
22
22
  * skills: skills # workspace skill root, relative to the file
23
23
  * switcher: # other kraftwerk workspaces, linked from the header dropdown
24
24
  * - name: other space
@@ -1,17 +1,17 @@
1
1
  import type { ChatAgentId } from "./chat/types.js";
2
2
  /**
3
- * Team: persistent agent teammates, defined on the filesystem. Each member
3
+ * Agents: persistent agents, defined on the filesystem. Each one
4
4
  * is one folder under the project's agents/ root:
5
5
  *
6
6
  * agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
7
- * agents/<slug>/system.md # the member's system prompt / role description
7
+ * agents/<slug>/system.md # the agent's system prompt / role description
8
8
  *
9
9
  * Definitions are git-tracked project config (like workflows/), not run
10
10
  * state — sessions with a member are ordinary chats scoped to it.
11
11
  */
12
12
  export declare const HARNESSES: ChatAgentId[];
13
13
  export declare const EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
14
- export interface TeamMember {
14
+ export interface Agent {
15
15
  slug: string;
16
16
  name: string;
17
17
  emoji: string;
@@ -33,17 +33,26 @@ export interface TeamMember {
33
33
  /** Archived: hidden from the active roster, restorable any time. */
34
34
  archived?: boolean;
35
35
  }
36
- export interface TeamMemberDetail extends TeamMember {
36
+ /** What the ⌘K palette needs to find and show an agent — kept in the project registry. */
37
+ export interface AgentSummary {
38
+ slug: string;
39
+ name: string;
40
+ emoji: string;
41
+ description?: string;
42
+ group?: string;
43
+ }
44
+ export declare const toSummary: (a: Agent) => AgentSummary;
45
+ export interface AgentDetail extends Agent {
37
46
  /** Contents of system.md — the member's system prompt. */
38
47
  system: string;
39
48
  }
40
- export declare function teamRoot(): Promise<string>;
41
- export declare function safeMemberSlug(slug: string): string;
49
+ export declare function agentsRoot(): Promise<string>;
50
+ export declare function safeAgentSlug(slug: string): string;
42
51
  /** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
43
52
  export declare function slugFromName(name: string): string;
44
- export declare function listMembers(): Promise<TeamMember[]>;
45
- export declare function getMember(slug: string): Promise<TeamMemberDetail | null>;
46
- export interface SaveMemberInput {
53
+ export declare function listAgents(): Promise<Agent[]>;
54
+ export declare function getAgent(slug: string): Promise<AgentDetail | null>;
55
+ export interface SaveAgentInput {
47
56
  slug?: string;
48
57
  name: string;
49
58
  emoji?: string;
@@ -59,7 +68,7 @@ export interface SaveMemberInput {
59
68
  skills?: string[];
60
69
  system?: string;
61
70
  }
62
- export declare function saveMember(input: SaveMemberInput): Promise<TeamMemberDetail>;
71
+ export declare function saveAgent(input: SaveAgentInput): Promise<AgentDetail>;
63
72
  /** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
64
- export declare function setMemberArchived(slug: string, archived: boolean): Promise<TeamMemberDetail>;
65
- export declare function deleteMember(slug: string): Promise<void>;
73
+ export declare function setAgentArchived(slug: string, archived: boolean): Promise<AgentDetail>;
74
+ export declare function deleteAgent(slug: string): Promise<void>;
@@ -3,24 +3,32 @@ import path from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
4
  import { resolveProject } from "../config.js";
5
5
  import { getProjectRoot } from "./context.js";
6
+ import { syncProjectAgents } from "./instances.js";
6
7
  /**
7
- * Team: persistent agent teammates, defined on the filesystem. Each member
8
+ * Agents: persistent agents, defined on the filesystem. Each one
8
9
  * is one folder under the project's agents/ root:
9
10
  *
10
11
  * agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
11
- * agents/<slug>/system.md # the member's system prompt / role description
12
+ * agents/<slug>/system.md # the agent's system prompt / role description
12
13
  *
13
14
  * Definitions are git-tracked project config (like workflows/), not run
14
15
  * state — sessions with a member are ordinary chats scoped to it.
15
16
  */
16
17
  export const HARNESSES = ["claude", "codex", "pi"];
17
18
  export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
18
- export async function teamRoot() {
19
+ export const toSummary = (a) => ({
20
+ slug: a.slug,
21
+ name: a.name,
22
+ emoji: a.emoji,
23
+ ...(a.description ? { description: a.description } : {}),
24
+ ...(a.group ? { group: a.group } : {}),
25
+ });
26
+ export async function agentsRoot() {
19
27
  const project = await resolveProject(getProjectRoot());
20
28
  return path.resolve(project.root, project.config.agents ?? "agents");
21
29
  }
22
30
  const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
23
- export function safeMemberSlug(slug) {
31
+ export function safeAgentSlug(slug) {
24
32
  if (!SLUG_RE.test(slug))
25
33
  throw new Error(`invalid agent slug "${slug}"`);
26
34
  return slug;
@@ -55,8 +63,8 @@ function normalize(slug, raw) {
55
63
  ...(raw.archived === true ? { archived: true } : {}),
56
64
  };
57
65
  }
58
- export async function listMembers() {
59
- const root = await teamRoot();
66
+ export async function listAgents() {
67
+ const root = await agentsRoot();
60
68
  let entries;
61
69
  try {
62
70
  entries = (await fs.readdir(root, { withFileTypes: true }))
@@ -66,7 +74,7 @@ export async function listMembers() {
66
74
  catch {
67
75
  return [];
68
76
  }
69
- const members = await Promise.all(entries.map(async (slug) => {
77
+ const agents = await Promise.all(entries.map(async (slug) => {
70
78
  try {
71
79
  const raw = parse(await fs.readFile(path.join(root, slug, "agent.yml"), "utf8"));
72
80
  return normalize(slug, (raw ?? {}));
@@ -75,10 +83,17 @@ export async function listMembers() {
75
83
  return null;
76
84
  }
77
85
  }));
78
- return members.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
86
+ const list = agents.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
87
+ // Keep the registry's copy of the roster current for the other instances'
88
+ // palettes. Awaited so a read that follows a save sees the record updated;
89
+ // it only writes when the roster changed.
90
+ await resolveProject(getProjectRoot())
91
+ .then((p) => syncProjectAgents(p.root, list.filter((a) => !a.archived).map(toSummary)))
92
+ .catch(() => { });
93
+ return list;
79
94
  }
80
- export async function getMember(slug) {
81
- const dir = path.join(await teamRoot(), safeMemberSlug(slug));
95
+ export async function getAgent(slug) {
96
+ const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
82
97
  let raw;
83
98
  try {
84
99
  raw = parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8"));
@@ -89,7 +104,7 @@ export async function getMember(slug) {
89
104
  const system = await fs.readFile(path.join(dir, "system.md"), "utf8").catch(() => "");
90
105
  return { ...normalize(slug, (raw ?? {})), system: system.trim() };
91
106
  }
92
- export async function saveMember(input) {
107
+ export async function saveAgent(input) {
93
108
  const name = input.name?.trim();
94
109
  if (!name)
95
110
  throw new Error("name is required");
@@ -99,8 +114,8 @@ export async function saveMember(input) {
99
114
  if (input.effort && !EFFORTS.includes(input.effort)) {
100
115
  throw new Error(`effort must be one of: ${EFFORTS.join(", ")}`);
101
116
  }
102
- const slug = input.slug ? safeMemberSlug(input.slug) : slugFromName(name);
103
- const dir = path.join(await teamRoot(), slug);
117
+ const slug = input.slug ? safeAgentSlug(input.slug) : slugFromName(name);
118
+ const dir = path.join(await agentsRoot(), slug);
104
119
  await fs.mkdir(dir, { recursive: true });
105
120
  // Profile edits must not silently unarchive: carry the flag over.
106
121
  const existing = await fs
@@ -122,11 +137,11 @@ export async function saveMember(input) {
122
137
  };
123
138
  await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
124
139
  await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
125
- return (await getMember(slug));
140
+ return (await getAgent(slug));
126
141
  }
127
142
  /** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
128
- export async function setMemberArchived(slug, archived) {
129
- const dir = path.join(await teamRoot(), safeMemberSlug(slug));
143
+ export async function setAgentArchived(slug, archived) {
144
+ const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
130
145
  let raw;
131
146
  try {
132
147
  raw = (parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8")) ?? {});
@@ -140,9 +155,9 @@ export async function setMemberArchived(slug, archived) {
140
155
  else
141
156
  delete yml.archived;
142
157
  await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
143
- return (await getMember(slug));
158
+ return (await getAgent(slug));
144
159
  }
145
- export async function deleteMember(slug) {
146
- const dir = path.join(await teamRoot(), safeMemberSlug(slug));
160
+ export async function deleteAgent(slug) {
161
+ const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
147
162
  await fs.rm(dir, { recursive: true, force: true });
148
163
  }
@@ -19,7 +19,7 @@ export interface BackendHooks {
19
19
  }>): Promise<string | null>;
20
20
  }
21
21
  /**
22
- * Optional per-chat model/effort overrides (from a team member definition).
22
+ * Optional per-chat model/effort overrides (from an agent definition).
23
23
  * Each backend maps them onto whatever its agent actually supports.
24
24
  */
25
25
  export interface BackendTuning {
@@ -5,7 +5,7 @@ import { knowledgeIndex } from "../knowledge.js";
5
5
  import { getRun, listRuns, safeRunDir } from "../runs.js";
6
6
  import { listSkills, readSkill } from "../skills.js";
7
7
  import { listWorkflows } from "../workflows.js";
8
- import { getMember, listMembers } from "../team.js";
8
+ import { getAgent, listAgents } from "../agents.js";
9
9
  import { startAcpBackend } from "./acp.js";
10
10
  import { startPiBackend } from "./pi.js";
11
11
  import { appendEvent, listChatMetas, newChatId, readEvents, readMeta, safeChatDir, writeMeta, } from "./store.js";
@@ -74,19 +74,19 @@ function emit(state, ev) {
74
74
  /* ---------- skills ---------- */
75
75
  /**
76
76
  * Skills visible to a chat: every discovered skill (workspace skills root
77
- * + .claude/skills roots, plus the member's own agents/<slug>/skills for team
78
- * sessions), narrowed by the team member's allowlist when the chat is a team
77
+ * + .claude/skills roots, plus the agent's own agents/<slug>/skills for agent
78
+ * sessions), narrowed by the agent's allowlist when the chat is an agent
79
79
  * session (absent allowlist = all, empty = none). The agent's own skills
80
80
  * always apply — the allowlist narrows shared skills only.
81
81
  */
82
82
  async function availableSkills(scope) {
83
- const all = await listSkills(scope.kind === "team" ? scope.member : undefined).catch(() => []);
84
- if (scope.kind !== "team")
83
+ const all = await listSkills(scope.kind === "agent" ? scope.slug : undefined).catch(() => []);
84
+ if (scope.kind !== "agent")
85
85
  return all;
86
- const member = await getMember(scope.member).catch(() => null);
87
- if (!member || member.skills === undefined)
86
+ const def = await getAgent(scope.slug).catch(() => null);
87
+ if (!def || def.skills === undefined)
88
88
  return all;
89
- const allowed = new Set(member.skills.map((n) => n.toLowerCase()));
89
+ const allowed = new Set(def.skills.map((n) => n.toLowerCase()));
90
90
  return all.filter((s) => s.source === "agent" || allowed.has(s.name.toLowerCase()));
91
91
  }
92
92
  /** "## Your skills" context block (empty when no skills are visible). */
@@ -122,12 +122,12 @@ async function expandSkillInvocation(scope, text) {
122
122
  `Arguments the user passed to the skill: ${args || "(none)"}`);
123
123
  }
124
124
  /* ---------- scope context ---------- */
125
- /** "## Your knowledge" block for a team member's connected OKF bundles. */
126
- async function teamKnowledgeContext(member) {
127
- if (member.knowledge.length === 0)
125
+ /** "## Your knowledge" block for a agent's connected OKF bundles. */
126
+ async function agentKnowledgeContext(def) {
127
+ if (def.knowledge.length === 0)
128
128
  return "";
129
129
  const { bundles } = await knowledgeIndex().catch(() => ({ bundles: [] }));
130
- const lines = member.knowledge
130
+ const lines = def.knowledge
131
131
  .map((name) => {
132
132
  const b = bundles.find((x) => x.name === name);
133
133
  return b
@@ -139,30 +139,30 @@ async function teamKnowledgeContext(member) {
139
139
  `part of your job. Consult them before answering questions in their domain, and keep them current ` +
140
140
  `when you learn something durable:\n${lines}\n\n` +
141
141
  `Read with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
142
- `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${member.slug}/${member.harness}\` ` +
142
+ `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${def.slug}/${def.harness}\` ` +
143
143
  `(write the markdown to a temp file first; the CLI stamps provenance and maintains index.md/log.md). ` +
144
144
  `Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
145
145
  `click in the UI.\n\n`);
146
146
  }
147
147
  /** Base context per scope; scopeContext() appends the shared skills block. */
148
148
  async function baseScopeContext(scope, agent) {
149
- if (scope.kind === "team") {
150
- const member = await getMember(scope.member).catch(() => null);
151
- if (!member) {
152
- return `You were opened as team agent "${scope.member}", but its definition under agents/ is missing. Tell the user and ask them to recreate it.`;
149
+ if (scope.kind === "agent") {
150
+ const def = await getAgent(scope.slug).catch(() => null);
151
+ if (!def) {
152
+ return `You were opened as agent "${scope.slug}", but its definition under agents/ is missing. Tell the user and ask them to recreate it.`;
153
153
  }
154
154
  const { workflows } = await listWorkflows().catch(() => ({ workflows: [] }));
155
- const connected = workflows.filter((w) => member.workflows.includes(w.slug));
156
- const missing = member.workflows.filter((slug) => !connected.some((w) => w.slug === slug));
155
+ const connected = workflows.filter((w) => def.workflows.includes(w.slug));
156
+ const missing = def.workflows.filter((slug) => !connected.some((w) => w.slug === slug));
157
157
  const wfLines = connected
158
158
  .map((w) => `- ${w.slug}: ${w.description ?? w.name ?? ""} (${w.steps} steps)`)
159
159
  .join("\n");
160
- return (`You are ${member.emoji} ${member.name}, a persistent agent teammate on this project's team ` +
161
- `(defined in agents/${member.slug}/). The user works with you like with a colleague: every session ` +
162
- `is a conversation with the same ${member.name}, so keep this role consistently. You are an AI agent, ` +
160
+ return (`You are ${def.emoji} ${def.name}, a persistent agent of this project ` +
161
+ `(defined in agents/${def.slug}/). The user works with you like with a colleague: every session ` +
162
+ `is a conversation with the same ${def.name}, so keep this role consistently. You are an AI agent, ` +
163
163
  `not a human — never pretend otherwise, but do own your role.\n\n` +
164
- `## Your role\n${member.system || "(no system prompt written yet — ask the user what your job should be)"}\n\n` +
165
- (member.workflows.length > 0
164
+ `## Your role\n${def.system || "(no system prompt written yet — ask the user what your job should be)"}\n\n` +
165
+ (def.workflows.length > 0
166
166
  ? `## Your workflows\nThese kraftwerk workflows are part of your job. When the user's request matches one, ` +
167
167
  `run it yourself instead of doing the work by hand:\n${wfLines || "(none found)"}\n` +
168
168
  (missing.length > 0
@@ -174,9 +174,9 @@ async function baseScopeContext(scope, agent) {
174
174
  `run id and summarize the result. Confirm with the user before starting a long or expensive run ` +
175
175
  `unless they clearly asked for it.\n\n`
176
176
  : "") +
177
- (await teamKnowledgeContext(member)) +
177
+ (await agentKnowledgeContext(def)) +
178
178
  `The working directory is the project root. Stay within your role; if a request is clearly outside it, ` +
179
- `say so and suggest which teammate or tool fits better.` +
179
+ `say so and suggest which agent or tool fits better.` +
180
180
  (scope.routine
181
181
  ? `\n\nThis session was started automatically by your scheduled routine "${scope.routine}" — ` +
182
182
  `nobody may be watching live. Complete the task autonomously (tool permissions are ` +
@@ -212,11 +212,11 @@ async function baseScopeContext(scope, agent) {
212
212
  `verification is the human's click in the UI. Ask the user what knowledge to capture, then author concise, well-typed concepts.`);
213
213
  }
214
214
  if (scope.kind === "kraftwerk") {
215
- const [{ workflows }, runs, knowledge, members] = await Promise.all([
215
+ const [{ workflows }, runs, knowledge, agents] = await Promise.all([
216
216
  listWorkflows(),
217
217
  listRuns(),
218
218
  knowledgeIndex().catch(() => ({ bundles: [] })),
219
- listMembers().catch(() => []),
219
+ listAgents().catch(() => []),
220
220
  ]);
221
221
  const wfLines = workflows
222
222
  .map((w) => `- ${w.slug}: ${w.description ?? w.name ?? ""} (${w.steps} steps, ${w.agents} agents)`)
@@ -228,7 +228,7 @@ async function baseScopeContext(scope, agent) {
228
228
  const bundleLines = knowledge.bundles
229
229
  .map((b) => `- ${b.name} (${b.concepts} concepts${b.updatedAt ? `, updated ${b.updatedAt.slice(0, 10)}` : ""})`)
230
230
  .join("\n");
231
- const memberLines = members
231
+ const agentLines = agents
232
232
  .map((m) => `- ${m.emoji} ${m.name} (${m.slug})${m.description ? `: ${m.description}` : ""}`)
233
233
  .join("\n");
234
234
  return (`You are the assistant inside the kraftwerk inspector, a UI for a workflow-as-code agent framework. ` +
@@ -244,8 +244,8 @@ async function baseScopeContext(scope, agent) {
244
244
  `Read with \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
245
245
  `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor kraftwerk-chat/${agent}\` ` +
246
246
  `(stamps provenance, maintains index.md/log.md — never hand-edit those).\n\n` +
247
- `## Team agents\n${memberLines || "(none)"}\n` +
248
- `These are persistent agent teammates (defined under agents/); the user talks to them on the ` +
247
+ `## Agents\n${agentLines || "(none)"}\n` +
248
+ `These are persistent agents (defined under agents/); the user talks to them on the ` +
249
249
  `Agents screen. Point the user there when a request clearly belongs to one of them.\n\n` +
250
250
  `Answer questions about workflows and runs by reading the files above. Do not modify run outputs unless asked.`);
251
251
  }
@@ -285,18 +285,18 @@ async function scopeContext(scope, agent) {
285
285
  return [base, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
286
286
  }
287
287
  /* ---------- backend lifecycle ---------- */
288
- /** Team chats carry the member's model/effort; resolved live so edits apply to new backends. */
288
+ /** Agent sessions carry the agent's model/effort; resolved live so edits apply to new backends. */
289
289
  async function backendTuning(scope, agent) {
290
290
  const tuning = {};
291
- if (scope.kind === "team") {
292
- const member = await getMember(scope.member).catch(() => null);
293
- if (member?.model)
294
- tuning.model = member.model;
295
- if (member?.effort)
296
- tuning.effort = member.effort;
297
- // Claude discovers skills natively; a defined allowlist narrows that.
298
- if (agent === "claude" && member?.skills)
299
- tuning.skills = member.skills;
291
+ if (scope.kind === "agent") {
292
+ const def = await getAgent(scope.slug).catch(() => null);
293
+ if (def?.model)
294
+ tuning.model = def.model;
295
+ if (def?.effort)
296
+ tuning.effort = def.effort;
297
+ // Claude discovers skills natively; an agentined allowlist narrows that.
298
+ if (agent === "claude" && def?.skills)
299
+ tuning.skills = def.skills;
300
300
  }
301
301
  // Run chats live in the run folder — grant claude the project root so
302
302
  // project-level skills and files stay reachable.
@@ -319,7 +319,7 @@ async function ensureBackend(state) {
319
319
  // Routine-triggered sessions run unattended: auto-approve, but keep
320
320
  // the request/resolution pair in the thread as an audit trail.
321
321
  const scope = state.meta.scope;
322
- if (scope.kind === "team" && scope.routine) {
322
+ if (scope.kind === "agent" && scope.routine) {
323
323
  const allow = options.find((o) => o.kind?.startsWith("allow")) ?? options[0];
324
324
  const requestId = randomUUID();
325
325
  emit(state, { type: "permission_request", requestId, title, options });
@@ -418,7 +418,7 @@ export async function postMessage(id, text) {
418
418
  // Routine sessions are one-shot and unattended: release the agent
419
419
  // process as soon as the turn ends instead of waiting for the reaper.
420
420
  const scope = state.meta.scope;
421
- if (scope.kind === "team" && scope.routine)
421
+ if (scope.kind === "agent" && scope.routine)
422
422
  dropBackend(state);
423
423
  }
424
424
  catch (err) {