mu-harness 0.17.2 → 0.17.5

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.
@@ -2,3 +2,4 @@ export type { Agent, GrantValue, ToolDecision, ToolGrants } from './types.js';
2
2
  export { type AgentRegistry, createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
5
+ export { createAgentWriterTool } from './writer.js';
@@ -1,3 +1,4 @@
1
1
  export { createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
2
2
  export { parseAgent } from './parser.js';
3
3
  export { loadAgents } from './loader.js';
4
+ export { createAgentWriterTool } from './writer.js';
@@ -2,6 +2,18 @@ import type { Agent, ToolDecision } from './types.js';
2
2
  export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
+ /**
6
+ * Register (or replace) an agent at runtime — mirrors {@link SkillRegistry.add}.
7
+ * Lets tools like `create_agent` make a freshly-authored agent immediately
8
+ * delegatable without a restart. Agents that `extends` the added one are
9
+ * re-resolved so they pick up the change.
10
+ */
11
+ add(agent: Agent): void;
12
+ /**
13
+ * Replace the entire set in place (rebuild) — used by hot-reload to reflect
14
+ * created, edited, and deleted definitions. Existing references see the new set.
15
+ */
16
+ replaceAll(agents: Agent[]): void;
5
17
  }
6
18
  export declare const grantArg: (tool: string, input: unknown) => string | undefined;
7
19
  export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
@@ -64,9 +64,7 @@ const merge = (base, child) => ({
64
64
  });
65
65
  export const createAgentRegistry = (agents = []) => {
66
66
  const raw = new Map();
67
- for (const agent of agents)
68
- if (!raw.has(agent.name))
69
- raw.set(agent.name, agent);
67
+ const byName = new Map();
70
68
  const resolve = (name, seen) => {
71
69
  const agent = raw.get(name);
72
70
  if (!agent.extends)
@@ -78,11 +76,27 @@ export const createAgentRegistry = (agents = []) => {
78
76
  throw new Error(`AgentRegistry: "${name}" extends unknown agent "${agent.extends}"`);
79
77
  return merge(resolve(agent.extends, new Set(seen).add(name)), agent);
80
78
  };
81
- const byName = new Map();
82
- for (const name of raw.keys())
83
- byName.set(name, resolve(name, new Set()));
79
+ // Rebuild both maps in place so existing holders of this registry see the new set.
80
+ const load = (list) => {
81
+ raw.clear();
82
+ for (const agent of list)
83
+ if (!raw.has(agent.name))
84
+ raw.set(agent.name, agent);
85
+ byName.clear();
86
+ for (const name of raw.keys())
87
+ byName.set(name, resolve(name, new Set()));
88
+ };
89
+ load(agents);
84
90
  return {
85
91
  list: () => [...byName.values()],
86
92
  get: (name) => byName.get(name),
93
+ add: (agent) => {
94
+ raw.set(agent.name, agent);
95
+ byName.set(agent.name, resolve(agent.name, new Set()));
96
+ for (const [name, a] of raw)
97
+ if (a.extends === agent.name)
98
+ byName.set(name, resolve(name, new Set()));
99
+ },
100
+ replaceAll: (list) => load(list),
87
101
  };
88
102
  };
@@ -0,0 +1,14 @@
1
+ import type { Tool } from 'mu-core';
2
+ import type { Scope } from '../common/index.js';
3
+ import type { AgentRegistry } from './registry.js';
4
+ /**
5
+ * `create_agent` — authors a reusable agent definition (`.md` with frontmatter)
6
+ * and registers it live via {@link AgentRegistry.add}, so it can be delegated to
7
+ * through `subagent` without a restart. Mirrors {@link createSkillWriterTool}:
8
+ * the `scope` selects the save location, or a configured `forceScope` pins it.
9
+ */
10
+ export declare const createAgentWriterTool: (deps: {
11
+ dirs: Record<Scope, string>;
12
+ registry: AgentRegistry;
13
+ forceScope?: Scope;
14
+ }) => Tool;
@@ -0,0 +1,81 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { stringify as stringifyYaml } from '../deps/jsr.io/@std/yaml/1.1.0/mod.js';
5
+ import { parseAgent } from './parser.js';
6
+ const slug = (name) => name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
7
+ /**
8
+ * `create_agent` — authors a reusable agent definition (`.md` with frontmatter)
9
+ * and registers it live via {@link AgentRegistry.add}, so it can be delegated to
10
+ * through `subagent` without a restart. Mirrors {@link createSkillWriterTool}:
11
+ * the `scope` selects the save location, or a configured `forceScope` pins it.
12
+ */
13
+ export const createAgentWriterTool = (deps) => {
14
+ const { forceScope } = deps;
15
+ const scopeProp = forceScope ? {} : {
16
+ scope: {
17
+ type: 'string',
18
+ enum: ['local', 'config'],
19
+ description: 'Where to save it: "local" = this project (repo-first), "config" = global config dir. Defaults to "local".',
20
+ },
21
+ };
22
+ return {
23
+ name: 'create_agent',
24
+ description: forceScope
25
+ ? `Define a reusable agent (name, description, system prompt, optional per-tool grants) that can be delegated to via \`subagent\`. Always saved to the "${forceScope}" agents directory.`
26
+ : 'Define a reusable agent (name, description, system prompt, optional per-tool grants) that can be delegated to via `subagent`. `scope: "local"` saves it to this project, `scope: "config"` makes it available across all projects.',
27
+ parameters: {
28
+ type: 'object',
29
+ properties: {
30
+ name: { type: 'string', description: 'Short agent name (kebab-case); also the filename.' },
31
+ description: { type: 'string', description: 'One line describing what this agent is for.' },
32
+ prompt: { type: 'string', description: 'The system prompt that defines the agent.' },
33
+ tools: {
34
+ type: 'object',
35
+ description: 'Optional per-tool grants: map a tool name to "allow" | "ask" | "deny" (or a nested {glob: decision} map). Omitted tools are denied — be explicit about what it may use.',
36
+ additionalProperties: true,
37
+ },
38
+ model: { type: 'string', description: 'Optional model ref override.' },
39
+ color: { type: 'string', description: 'Optional hex color for the UI.' },
40
+ ...scopeProp,
41
+ },
42
+ required: ['name', 'description', 'prompt'],
43
+ additionalProperties: false,
44
+ },
45
+ run: async (input) => {
46
+ const { name, description, prompt, tools, model, color, scope } = (input ?? {});
47
+ if (!name || !description || !prompt) {
48
+ return [{ type: 'text', text: 'Error: create_agent requires `name`, `description`, and `prompt`.' }];
49
+ }
50
+ // A configured forceScope wins over whatever the model passed.
51
+ const resolved = forceScope ?? scope ?? 'local';
52
+ const base = deps.dirs[resolved];
53
+ if (!base)
54
+ return [{ type: 'text', text: `Error: unknown scope "${scope}".` }];
55
+ const id = slug(name);
56
+ if (!id)
57
+ return [{ type: 'text', text: `Error: invalid agent name "${name}".` }];
58
+ if (deps.registry.get(id)) {
59
+ return [{ type: 'text', text: `Error: an agent named "${id}" already exists.` }];
60
+ }
61
+ const file = join(base, `${id}.md`);
62
+ if (existsSync(file))
63
+ return [{ type: 'text', text: `Error: ${file} already exists.` }];
64
+ const frontmatter = { name: id, description };
65
+ if (model)
66
+ frontmatter.model = model;
67
+ if (color)
68
+ frontmatter.color = color;
69
+ if (tools && typeof tools === 'object')
70
+ frontmatter.tools = tools;
71
+ const source = `---\n${stringifyYaml(frontmatter).trimEnd()}\n---\n\n${prompt.trim()}\n`;
72
+ await mkdir(base, { recursive: true });
73
+ await writeFile(file, source, 'utf-8');
74
+ deps.registry.add(parseAgent(source, id));
75
+ return [{
76
+ type: 'text',
77
+ text: `Created agent "${id}" at ${file}. It can now be delegated to via the \`subagent\` tool.`,
78
+ }];
79
+ },
80
+ };
81
+ };
@@ -1,2 +1,3 @@
1
1
  export { createEmitter, type Emitter, strList } from './utils.js';
2
2
  export { type Frontmatter, parseFrontmatter, str } from './frontmatter.js';
3
+ export type { Scope } from './scope.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Where a host saves agent-authored definitions (skills, sub-agents, …):
3
+ * - `local` → the current project (repo-first, e.g. `<cwd>/skills`).
4
+ * - `config` → the global config dir, shared across every project.
5
+ *
6
+ * Generic across definition kinds so every writer tool speaks the same vocabulary.
7
+ */
8
+ export type Scope = 'local' | 'config';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,7 @@
1
1
  import os from 'node:os';
2
2
  import { join } from 'node:path';
3
3
  import process from 'node:process';
4
- import { createAgentRegistry, grantArg, loadAgents, toolDecision, toolNames } from '../agents/index.js';
4
+ import { createAgentRegistry, createAgentWriterTool, grantArg, loadAgents, toolDecision, toolNames } from '../agents/index.js';
5
5
  import { createAgentsCommand, createCommandRegistry, createHelpCommand, createSessionsCommand, createSkillsCommand, } from '../commands/index.js';
6
6
  import { createHarnessConfig } from '../config/index.js';
7
7
  import { mergeHooks } from '../hooks/index.js';
@@ -20,17 +20,25 @@ const TITLE_AGENT = {
20
20
  tools: [],
21
21
  };
22
22
  export const createHarness = async (options) => {
23
- const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], skillScope, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
23
+ const { hostName, xdg, providers, model, agents: hostAgents = [], defaultAgents = [], skills: hostSkills = [], skillScope, agentScope, agentDirs, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
24
24
  const cwd = options.cwd ?? process.cwd();
25
25
  const config = createHarnessConfig({ hostName, xdg });
26
26
  const models = createModelRegistry({ providers, default: model });
27
27
  const pluginsDir = join(config.configDir, 'plugins');
28
- const agentsDir = join(config.configDir, 'agents');
28
+ const agentsDir = agentDirs?.config ?? join(config.configDir, 'agents');
29
+ const localAgentsDir = agentDirs?.local ?? join(cwd, 'agents');
29
30
  const plugins = createPluginStore({ dir: pluginsDir });
30
31
  const newId = () => crypto.randomUUID();
31
32
  const pluginAgents = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.agents ?? []);
32
- const diskAgents = await loadAgents(agentsDir);
33
- const agents = createAgentRegistry([...hostAgents, ...pluginAgents, ...diskAgents]);
33
+ const loadDiskAgents = async () => [...await loadAgents(localAgentsDir), ...await loadAgents(agentsDir)];
34
+ // Priority (first wins): host > plugin > local dir > config dir > default fallback.
35
+ const mergedAgents = async () => [
36
+ ...hostAgents,
37
+ ...pluginAgents,
38
+ ...await loadDiskAgents(),
39
+ ...defaultAgents,
40
+ ];
41
+ const agents = createAgentRegistry(await mergedAgents());
34
42
  const skillsDir = join(config.configDir, 'skills');
35
43
  const cwdSkillsDir = join(cwd, 'skills');
36
44
  const envBlock = environmentBlock({
@@ -46,14 +54,23 @@ export const createHarness = async (options) => {
46
54
  prepareRequest: ({ system }) => ({ system: system ? `${system}\n\n${envBlock}` : envBlock }),
47
55
  };
48
56
  const pluginSkills = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.skills ?? []);
49
- const cwdSkills = await loadSkills(cwdSkillsDir);
50
- const diskSkills = await loadSkills(skillsDir);
51
- const skills = createSkillRegistry([...hostSkills, ...pluginSkills, ...cwdSkills, ...diskSkills]);
57
+ const mergedSkills = async () => [
58
+ ...hostSkills,
59
+ ...pluginSkills,
60
+ ...await loadSkills(cwdSkillsDir),
61
+ ...await loadSkills(skillsDir),
62
+ ];
63
+ const skills = createSkillRegistry(await mergedSkills());
52
64
  const skillWriterTool = createSkillWriterTool({
53
65
  dirs: { local: cwdSkillsDir, config: skillsDir },
54
66
  registry: skills,
55
67
  forceScope: skillScope,
56
68
  });
69
+ const agentWriterTool = createAgentWriterTool({
70
+ dirs: { local: localAgentsDir, config: agentsDir },
71
+ registry: agents,
72
+ forceScope: agentScope,
73
+ });
57
74
  const scopeSkills = (agent) => {
58
75
  if (!agent)
59
76
  return skills;
@@ -68,6 +85,7 @@ export const createHarness = async (options) => {
68
85
  ...extra,
69
86
  createSkillTool(scopeSkills(agent)),
70
87
  skillWriterTool,
88
+ agentWriterTool,
71
89
  ...schedulerTools,
72
90
  ];
73
91
  const approvalHook = (getAgent) => approvals
@@ -171,6 +189,10 @@ export const createHarness = async (options) => {
171
189
  throw new Error(`unknown sub-agent "${agent}"`);
172
190
  return await runSubAgent(def, task, { spawn, runs, parentId });
173
191
  },
192
+ reloadDefinitions: async () => {
193
+ agents.replaceAll(await mergedAgents());
194
+ skills.replaceAll(await mergedSkills());
195
+ },
174
196
  scheduler,
175
197
  tasks,
176
198
  commands,
@@ -1,25 +1,44 @@
1
1
  import type { Provider } from 'mu-core';
2
2
  import type { Agent, AgentRegistry, ToolDecision } from '../agents/index.js';
3
3
  import type { CommandRegistry } from '../commands/index.js';
4
+ import type { Scope } from '../common/index.js';
4
5
  import type { HarnessConfig, HarnessConfigOptions } from '../config/index.js';
5
6
  import type { ApprovalManager } from '../permissions/index.js';
6
7
  import type { PluginStore } from '../plugin/index.js';
7
8
  import type { Scheduler, TaskStore } from '../scheduler/index.js';
8
9
  import type { AgentSessionConfig, SessionManager } from '../session/index.js';
9
- import type { Skill, SkillRegistry, SkillScope } from '../skills/index.js';
10
+ import type { Skill, SkillRegistry } from '../skills/index.js';
10
11
  import type { SubAgentRegistry, SubAgentResult } from '../subAgents/index.js';
11
12
  import type { ModelRegistry } from './models.js';
12
13
  export type HarnessOptions = HarnessConfigOptions & Omit<AgentSessionConfig, 'provider' | 'model' | 'id' | 'messages'> & {
13
14
  providers: Record<string, Provider>;
14
15
  model: string;
15
16
  agents?: Agent[];
17
+ /**
18
+ * Lowest-priority fallback agents (e.g. a host's built-in primary). Merged
19
+ * last, so disk- or host-defined agents of the same name override them.
20
+ */
21
+ defaultAgents?: Agent[];
16
22
  skills?: Skill[];
17
23
  /**
18
24
  * Forces the save location of `create_skill`. When set, the model's `scope`
19
25
  * argument is overridden (and dropped from the tool schema). Unset → the
20
26
  * model chooses, defaulting to 'local'.
21
27
  */
22
- skillScope?: SkillScope;
28
+ skillScope?: Scope;
29
+ /**
30
+ * Forces the save location of `create_agent`. Same semantics as
31
+ * {@link skillScope}: set to pin the scope, leave unset to let the model choose.
32
+ */
33
+ agentScope?: Scope;
34
+ /**
35
+ * Overrides where `create_agent` writes (and which dirs are loaded at boot).
36
+ * Defaults to `{ local: <cwd>/agents, config: <configDir>/agents }`.
37
+ */
38
+ agentDirs?: {
39
+ local?: string;
40
+ config?: string;
41
+ };
23
42
  title?: boolean;
24
43
  titleModel?: string;
25
44
  cwd?: string;
@@ -43,6 +62,12 @@ export interface Harness {
43
62
  readonly skills: SkillRegistry;
44
63
  readonly subAgents: SubAgentRegistry;
45
64
  dispatchSubAgent(agent: string, task: string, parentId: string): Promise<SubAgentResult>;
65
+ /**
66
+ * Re-read the on-disk agent and skill directories and refresh the registries
67
+ * in place (create/edit/delete), preserving host/plugin/default contributions.
68
+ * Lets a host hot-reload definitions without a restart.
69
+ */
70
+ reloadDefinitions(): Promise<void>;
46
71
  readonly commands: CommandRegistry;
47
72
  readonly scheduler?: Scheduler;
48
73
  readonly tasks?: TaskStore;
@@ -4,5 +4,5 @@ export { parseSkill } from './parser.js';
4
4
  export { loadSkills } from './loader.js';
5
5
  export { skillMatchesPlatform } from './platform.js';
6
6
  export { createSkillTool } from './tool.js';
7
- export { createSkillWriterTool, type SkillScope } from './writer.js';
7
+ export { createSkillWriterTool } from './writer.js';
8
8
  export { createRunSkillTool, runSkill, type RunSkillDeps } from './run.js';
@@ -3,6 +3,11 @@ export interface SkillRegistry {
3
3
  list(): Skill[];
4
4
  get(name: string): Skill | undefined;
5
5
  add(skill: Skill): void;
6
+ /**
7
+ * Replace the entire set in place (rebuild) — used by hot-reload to reflect
8
+ * created, edited, and deleted skills. Existing references see the new set.
9
+ */
10
+ replaceAll(skills: Skill[]): void;
6
11
  select(names: string[]): SkillRegistry;
7
12
  }
8
13
  export declare const createSkillRegistry: (skills?: Skill[]) => SkillRegistry;
@@ -1,8 +1,12 @@
1
1
  export const createSkillRegistry = (skills = []) => {
2
2
  const byName = new Map();
3
- for (const skill of skills)
4
- if (!byName.has(skill.name))
5
- byName.set(skill.name, skill);
3
+ const load = (list) => {
4
+ byName.clear();
5
+ for (const skill of list)
6
+ if (!byName.has(skill.name))
7
+ byName.set(skill.name, skill);
8
+ };
9
+ load(skills);
6
10
  const view = (allow) => ({
7
11
  list: () => [...byName.values()].filter((skill) => !allow || allow.has(skill.name)),
8
12
  get: (name) => (!allow || allow.has(name) ? byName.get(name) : undefined),
@@ -10,6 +14,7 @@ export const createSkillRegistry = (skills = []) => {
10
14
  byName.set(skill.name, skill);
11
15
  allow?.add(skill.name);
12
16
  },
17
+ replaceAll: (list) => load(list),
13
18
  select: (names) => view(new Set(allow ? names.filter((name) => allow.has(name)) : names)),
14
19
  });
15
20
  return view();
@@ -1,8 +1,8 @@
1
1
  import type { Tool } from 'mu-core';
2
+ import type { Scope } from '../common/index.js';
2
3
  import type { SkillRegistry } from './registry.js';
3
- export type SkillScope = 'local' | 'config';
4
4
  export declare const createSkillWriterTool: (deps: {
5
- dirs: Record<SkillScope, string>;
5
+ dirs: Record<Scope, string>;
6
6
  registry: SkillRegistry;
7
- forceScope?: SkillScope;
7
+ forceScope?: Scope;
8
8
  }) => Tool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.17.2",
3
+ "version": "0.17.5",
4
4
  "description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
5
5
  "license": "MIT",
6
6
  "main": "./script/index.js",
@@ -23,8 +23,8 @@
23
23
  "@swc/wasm-typescript": "^1.15.0",
24
24
  "cli-highlight": "^2.1.11",
25
25
  "croner": "^9.0.0",
26
- "mu-core": "^0.17.2",
27
- "mu-tui": "^0.17.2"
26
+ "mu-core": "^0.17.5",
27
+ "mu-tui": "^0.17.5"
28
28
  },
29
29
  "_generatedBy": "dnt@dev",
30
30
  "types": "./esm/index.d.ts"
@@ -2,3 +2,4 @@ export type { Agent, GrantValue, ToolDecision, ToolGrants } from './types.js';
2
2
  export { type AgentRegistry, createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
5
+ export { createAgentWriterTool } from './writer.js';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = exports.grantArg = exports.createAgentRegistry = void 0;
3
+ exports.createAgentWriterTool = exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = exports.grantArg = exports.createAgentRegistry = void 0;
4
4
  var registry_js_1 = require("./registry.js");
5
5
  Object.defineProperty(exports, "createAgentRegistry", { enumerable: true, get: function () { return registry_js_1.createAgentRegistry; } });
6
6
  Object.defineProperty(exports, "grantArg", { enumerable: true, get: function () { return registry_js_1.grantArg; } });
@@ -10,3 +10,5 @@ var parser_js_1 = require("./parser.js");
10
10
  Object.defineProperty(exports, "parseAgent", { enumerable: true, get: function () { return parser_js_1.parseAgent; } });
11
11
  var loader_js_1 = require("./loader.js");
12
12
  Object.defineProperty(exports, "loadAgents", { enumerable: true, get: function () { return loader_js_1.loadAgents; } });
13
+ var writer_js_1 = require("./writer.js");
14
+ Object.defineProperty(exports, "createAgentWriterTool", { enumerable: true, get: function () { return writer_js_1.createAgentWriterTool; } });
@@ -2,6 +2,18 @@ import type { Agent, ToolDecision } from './types.js';
2
2
  export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
+ /**
6
+ * Register (or replace) an agent at runtime — mirrors {@link SkillRegistry.add}.
7
+ * Lets tools like `create_agent` make a freshly-authored agent immediately
8
+ * delegatable without a restart. Agents that `extends` the added one are
9
+ * re-resolved so they pick up the change.
10
+ */
11
+ add(agent: Agent): void;
12
+ /**
13
+ * Replace the entire set in place (rebuild) — used by hot-reload to reflect
14
+ * created, edited, and deleted definitions. Existing references see the new set.
15
+ */
16
+ replaceAll(agents: Agent[]): void;
5
17
  }
6
18
  export declare const grantArg: (tool: string, input: unknown) => string | undefined;
7
19
  export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
@@ -70,9 +70,7 @@ const merge = (base, child) => ({
70
70
  });
71
71
  const createAgentRegistry = (agents = []) => {
72
72
  const raw = new Map();
73
- for (const agent of agents)
74
- if (!raw.has(agent.name))
75
- raw.set(agent.name, agent);
73
+ const byName = new Map();
76
74
  const resolve = (name, seen) => {
77
75
  const agent = raw.get(name);
78
76
  if (!agent.extends)
@@ -84,12 +82,28 @@ const createAgentRegistry = (agents = []) => {
84
82
  throw new Error(`AgentRegistry: "${name}" extends unknown agent "${agent.extends}"`);
85
83
  return merge(resolve(agent.extends, new Set(seen).add(name)), agent);
86
84
  };
87
- const byName = new Map();
88
- for (const name of raw.keys())
89
- byName.set(name, resolve(name, new Set()));
85
+ // Rebuild both maps in place so existing holders of this registry see the new set.
86
+ const load = (list) => {
87
+ raw.clear();
88
+ for (const agent of list)
89
+ if (!raw.has(agent.name))
90
+ raw.set(agent.name, agent);
91
+ byName.clear();
92
+ for (const name of raw.keys())
93
+ byName.set(name, resolve(name, new Set()));
94
+ };
95
+ load(agents);
90
96
  return {
91
97
  list: () => [...byName.values()],
92
98
  get: (name) => byName.get(name),
99
+ add: (agent) => {
100
+ raw.set(agent.name, agent);
101
+ byName.set(agent.name, resolve(agent.name, new Set()));
102
+ for (const [name, a] of raw)
103
+ if (a.extends === agent.name)
104
+ byName.set(name, resolve(name, new Set()));
105
+ },
106
+ replaceAll: (list) => load(list),
93
107
  };
94
108
  };
95
109
  exports.createAgentRegistry = createAgentRegistry;
@@ -0,0 +1,14 @@
1
+ import type { Tool } from 'mu-core';
2
+ import type { Scope } from '../common/index.js';
3
+ import type { AgentRegistry } from './registry.js';
4
+ /**
5
+ * `create_agent` — authors a reusable agent definition (`.md` with frontmatter)
6
+ * and registers it live via {@link AgentRegistry.add}, so it can be delegated to
7
+ * through `subagent` without a restart. Mirrors {@link createSkillWriterTool}:
8
+ * the `scope` selects the save location, or a configured `forceScope` pins it.
9
+ */
10
+ export declare const createAgentWriterTool: (deps: {
11
+ dirs: Record<Scope, string>;
12
+ registry: AgentRegistry;
13
+ forceScope?: Scope;
14
+ }) => Tool;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAgentWriterTool = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ const mod_js_1 = require("../deps/jsr.io/@std/yaml/1.1.0/mod.js");
8
+ const parser_js_1 = require("./parser.js");
9
+ const slug = (name) => name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
10
+ /**
11
+ * `create_agent` — authors a reusable agent definition (`.md` with frontmatter)
12
+ * and registers it live via {@link AgentRegistry.add}, so it can be delegated to
13
+ * through `subagent` without a restart. Mirrors {@link createSkillWriterTool}:
14
+ * the `scope` selects the save location, or a configured `forceScope` pins it.
15
+ */
16
+ const createAgentWriterTool = (deps) => {
17
+ const { forceScope } = deps;
18
+ const scopeProp = forceScope ? {} : {
19
+ scope: {
20
+ type: 'string',
21
+ enum: ['local', 'config'],
22
+ description: 'Where to save it: "local" = this project (repo-first), "config" = global config dir. Defaults to "local".',
23
+ },
24
+ };
25
+ return {
26
+ name: 'create_agent',
27
+ description: forceScope
28
+ ? `Define a reusable agent (name, description, system prompt, optional per-tool grants) that can be delegated to via \`subagent\`. Always saved to the "${forceScope}" agents directory.`
29
+ : 'Define a reusable agent (name, description, system prompt, optional per-tool grants) that can be delegated to via `subagent`. `scope: "local"` saves it to this project, `scope: "config"` makes it available across all projects.',
30
+ parameters: {
31
+ type: 'object',
32
+ properties: {
33
+ name: { type: 'string', description: 'Short agent name (kebab-case); also the filename.' },
34
+ description: { type: 'string', description: 'One line describing what this agent is for.' },
35
+ prompt: { type: 'string', description: 'The system prompt that defines the agent.' },
36
+ tools: {
37
+ type: 'object',
38
+ description: 'Optional per-tool grants: map a tool name to "allow" | "ask" | "deny" (or a nested {glob: decision} map). Omitted tools are denied — be explicit about what it may use.',
39
+ additionalProperties: true,
40
+ },
41
+ model: { type: 'string', description: 'Optional model ref override.' },
42
+ color: { type: 'string', description: 'Optional hex color for the UI.' },
43
+ ...scopeProp,
44
+ },
45
+ required: ['name', 'description', 'prompt'],
46
+ additionalProperties: false,
47
+ },
48
+ run: async (input) => {
49
+ const { name, description, prompt, tools, model, color, scope } = (input ?? {});
50
+ if (!name || !description || !prompt) {
51
+ return [{ type: 'text', text: 'Error: create_agent requires `name`, `description`, and `prompt`.' }];
52
+ }
53
+ // A configured forceScope wins over whatever the model passed.
54
+ const resolved = forceScope ?? scope ?? 'local';
55
+ const base = deps.dirs[resolved];
56
+ if (!base)
57
+ return [{ type: 'text', text: `Error: unknown scope "${scope}".` }];
58
+ const id = slug(name);
59
+ if (!id)
60
+ return [{ type: 'text', text: `Error: invalid agent name "${name}".` }];
61
+ if (deps.registry.get(id)) {
62
+ return [{ type: 'text', text: `Error: an agent named "${id}" already exists.` }];
63
+ }
64
+ const file = (0, node_path_1.join)(base, `${id}.md`);
65
+ if ((0, node_fs_1.existsSync)(file))
66
+ return [{ type: 'text', text: `Error: ${file} already exists.` }];
67
+ const frontmatter = { name: id, description };
68
+ if (model)
69
+ frontmatter.model = model;
70
+ if (color)
71
+ frontmatter.color = color;
72
+ if (tools && typeof tools === 'object')
73
+ frontmatter.tools = tools;
74
+ const source = `---\n${(0, mod_js_1.stringify)(frontmatter).trimEnd()}\n---\n\n${prompt.trim()}\n`;
75
+ await (0, promises_1.mkdir)(base, { recursive: true });
76
+ await (0, promises_1.writeFile)(file, source, 'utf-8');
77
+ deps.registry.add((0, parser_js_1.parseAgent)(source, id));
78
+ return [{
79
+ type: 'text',
80
+ text: `Created agent "${id}" at ${file}. It can now be delegated to via the \`subagent\` tool.`,
81
+ }];
82
+ },
83
+ };
84
+ };
85
+ exports.createAgentWriterTool = createAgentWriterTool;
@@ -1,2 +1,3 @@
1
1
  export { createEmitter, type Emitter, strList } from './utils.js';
2
2
  export { type Frontmatter, parseFrontmatter, str } from './frontmatter.js';
3
+ export type { Scope } from './scope.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Where a host saves agent-authored definitions (skills, sub-agents, …):
3
+ * - `local` → the current project (repo-first, e.g. `<cwd>/skills`).
4
+ * - `config` → the global config dir, shared across every project.
5
+ *
6
+ * Generic across definition kinds so every writer tool speaks the same vocabulary.
7
+ */
8
+ export type Scope = 'local' | 'config';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -26,17 +26,25 @@ const TITLE_AGENT = {
26
26
  tools: [],
27
27
  };
28
28
  const createHarness = async (options) => {
29
- const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], skillScope, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
29
+ const { hostName, xdg, providers, model, agents: hostAgents = [], defaultAgents = [], skills: hostSkills = [], skillScope, agentScope, agentDirs, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
30
30
  const cwd = options.cwd ?? node_process_1.default.cwd();
31
31
  const config = (0, index_js_3.createHarnessConfig)({ hostName, xdg });
32
32
  const models = (0, models_js_1.createModelRegistry)({ providers, default: model });
33
33
  const pluginsDir = (0, node_path_1.join)(config.configDir, 'plugins');
34
- const agentsDir = (0, node_path_1.join)(config.configDir, 'agents');
34
+ const agentsDir = agentDirs?.config ?? (0, node_path_1.join)(config.configDir, 'agents');
35
+ const localAgentsDir = agentDirs?.local ?? (0, node_path_1.join)(cwd, 'agents');
35
36
  const plugins = (0, index_js_6.createPluginStore)({ dir: pluginsDir });
36
37
  const newId = () => crypto.randomUUID();
37
38
  const pluginAgents = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.agents ?? []);
38
- const diskAgents = await (0, index_js_1.loadAgents)(agentsDir);
39
- const agents = (0, index_js_1.createAgentRegistry)([...hostAgents, ...pluginAgents, ...diskAgents]);
39
+ const loadDiskAgents = async () => [...await (0, index_js_1.loadAgents)(localAgentsDir), ...await (0, index_js_1.loadAgents)(agentsDir)];
40
+ // Priority (first wins): host > plugin > local dir > config dir > default fallback.
41
+ const mergedAgents = async () => [
42
+ ...hostAgents,
43
+ ...pluginAgents,
44
+ ...await loadDiskAgents(),
45
+ ...defaultAgents,
46
+ ];
47
+ const agents = (0, index_js_1.createAgentRegistry)(await mergedAgents());
40
48
  const skillsDir = (0, node_path_1.join)(config.configDir, 'skills');
41
49
  const cwdSkillsDir = (0, node_path_1.join)(cwd, 'skills');
42
50
  const envBlock = (0, environment_js_1.environmentBlock)({
@@ -52,14 +60,23 @@ const createHarness = async (options) => {
52
60
  prepareRequest: ({ system }) => ({ system: system ? `${system}\n\n${envBlock}` : envBlock }),
53
61
  };
54
62
  const pluginSkills = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.skills ?? []);
55
- const cwdSkills = await (0, index_js_9.loadSkills)(cwdSkillsDir);
56
- const diskSkills = await (0, index_js_9.loadSkills)(skillsDir);
57
- const skills = (0, index_js_9.createSkillRegistry)([...hostSkills, ...pluginSkills, ...cwdSkills, ...diskSkills]);
63
+ const mergedSkills = async () => [
64
+ ...hostSkills,
65
+ ...pluginSkills,
66
+ ...await (0, index_js_9.loadSkills)(cwdSkillsDir),
67
+ ...await (0, index_js_9.loadSkills)(skillsDir),
68
+ ];
69
+ const skills = (0, index_js_9.createSkillRegistry)(await mergedSkills());
58
70
  const skillWriterTool = (0, index_js_9.createSkillWriterTool)({
59
71
  dirs: { local: cwdSkillsDir, config: skillsDir },
60
72
  registry: skills,
61
73
  forceScope: skillScope,
62
74
  });
75
+ const agentWriterTool = (0, index_js_1.createAgentWriterTool)({
76
+ dirs: { local: localAgentsDir, config: agentsDir },
77
+ registry: agents,
78
+ forceScope: agentScope,
79
+ });
63
80
  const scopeSkills = (agent) => {
64
81
  if (!agent)
65
82
  return skills;
@@ -74,6 +91,7 @@ const createHarness = async (options) => {
74
91
  ...extra,
75
92
  (0, index_js_9.createSkillTool)(scopeSkills(agent)),
76
93
  skillWriterTool,
94
+ agentWriterTool,
77
95
  ...schedulerTools,
78
96
  ];
79
97
  const approvalHook = (getAgent) => approvals
@@ -177,6 +195,10 @@ const createHarness = async (options) => {
177
195
  throw new Error(`unknown sub-agent "${agent}"`);
178
196
  return await (0, index_js_10.runSubAgent)(def, task, { spawn, runs, parentId });
179
197
  },
198
+ reloadDefinitions: async () => {
199
+ agents.replaceAll(await mergedAgents());
200
+ skills.replaceAll(await mergedSkills());
201
+ },
180
202
  scheduler,
181
203
  tasks,
182
204
  commands,
@@ -1,25 +1,44 @@
1
1
  import type { Provider } from 'mu-core';
2
2
  import type { Agent, AgentRegistry, ToolDecision } from '../agents/index.js';
3
3
  import type { CommandRegistry } from '../commands/index.js';
4
+ import type { Scope } from '../common/index.js';
4
5
  import type { HarnessConfig, HarnessConfigOptions } from '../config/index.js';
5
6
  import type { ApprovalManager } from '../permissions/index.js';
6
7
  import type { PluginStore } from '../plugin/index.js';
7
8
  import type { Scheduler, TaskStore } from '../scheduler/index.js';
8
9
  import type { AgentSessionConfig, SessionManager } from '../session/index.js';
9
- import type { Skill, SkillRegistry, SkillScope } from '../skills/index.js';
10
+ import type { Skill, SkillRegistry } from '../skills/index.js';
10
11
  import type { SubAgentRegistry, SubAgentResult } from '../subAgents/index.js';
11
12
  import type { ModelRegistry } from './models.js';
12
13
  export type HarnessOptions = HarnessConfigOptions & Omit<AgentSessionConfig, 'provider' | 'model' | 'id' | 'messages'> & {
13
14
  providers: Record<string, Provider>;
14
15
  model: string;
15
16
  agents?: Agent[];
17
+ /**
18
+ * Lowest-priority fallback agents (e.g. a host's built-in primary). Merged
19
+ * last, so disk- or host-defined agents of the same name override them.
20
+ */
21
+ defaultAgents?: Agent[];
16
22
  skills?: Skill[];
17
23
  /**
18
24
  * Forces the save location of `create_skill`. When set, the model's `scope`
19
25
  * argument is overridden (and dropped from the tool schema). Unset → the
20
26
  * model chooses, defaulting to 'local'.
21
27
  */
22
- skillScope?: SkillScope;
28
+ skillScope?: Scope;
29
+ /**
30
+ * Forces the save location of `create_agent`. Same semantics as
31
+ * {@link skillScope}: set to pin the scope, leave unset to let the model choose.
32
+ */
33
+ agentScope?: Scope;
34
+ /**
35
+ * Overrides where `create_agent` writes (and which dirs are loaded at boot).
36
+ * Defaults to `{ local: <cwd>/agents, config: <configDir>/agents }`.
37
+ */
38
+ agentDirs?: {
39
+ local?: string;
40
+ config?: string;
41
+ };
23
42
  title?: boolean;
24
43
  titleModel?: string;
25
44
  cwd?: string;
@@ -43,6 +62,12 @@ export interface Harness {
43
62
  readonly skills: SkillRegistry;
44
63
  readonly subAgents: SubAgentRegistry;
45
64
  dispatchSubAgent(agent: string, task: string, parentId: string): Promise<SubAgentResult>;
65
+ /**
66
+ * Re-read the on-disk agent and skill directories and refresh the registries
67
+ * in place (create/edit/delete), preserving host/plugin/default contributions.
68
+ * Lets a host hot-reload definitions without a restart.
69
+ */
70
+ reloadDefinitions(): Promise<void>;
46
71
  readonly commands: CommandRegistry;
47
72
  readonly scheduler?: Scheduler;
48
73
  readonly tasks?: TaskStore;
@@ -4,5 +4,5 @@ export { parseSkill } from './parser.js';
4
4
  export { loadSkills } from './loader.js';
5
5
  export { skillMatchesPlatform } from './platform.js';
6
6
  export { createSkillTool } from './tool.js';
7
- export { createSkillWriterTool, type SkillScope } from './writer.js';
7
+ export { createSkillWriterTool } from './writer.js';
8
8
  export { createRunSkillTool, runSkill, type RunSkillDeps } from './run.js';
@@ -3,6 +3,11 @@ export interface SkillRegistry {
3
3
  list(): Skill[];
4
4
  get(name: string): Skill | undefined;
5
5
  add(skill: Skill): void;
6
+ /**
7
+ * Replace the entire set in place (rebuild) — used by hot-reload to reflect
8
+ * created, edited, and deleted skills. Existing references see the new set.
9
+ */
10
+ replaceAll(skills: Skill[]): void;
6
11
  select(names: string[]): SkillRegistry;
7
12
  }
8
13
  export declare const createSkillRegistry: (skills?: Skill[]) => SkillRegistry;
@@ -3,9 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSkillRegistry = void 0;
4
4
  const createSkillRegistry = (skills = []) => {
5
5
  const byName = new Map();
6
- for (const skill of skills)
7
- if (!byName.has(skill.name))
8
- byName.set(skill.name, skill);
6
+ const load = (list) => {
7
+ byName.clear();
8
+ for (const skill of list)
9
+ if (!byName.has(skill.name))
10
+ byName.set(skill.name, skill);
11
+ };
12
+ load(skills);
9
13
  const view = (allow) => ({
10
14
  list: () => [...byName.values()].filter((skill) => !allow || allow.has(skill.name)),
11
15
  get: (name) => (!allow || allow.has(name) ? byName.get(name) : undefined),
@@ -13,6 +17,7 @@ const createSkillRegistry = (skills = []) => {
13
17
  byName.set(skill.name, skill);
14
18
  allow?.add(skill.name);
15
19
  },
20
+ replaceAll: (list) => load(list),
16
21
  select: (names) => view(new Set(allow ? names.filter((name) => allow.has(name)) : names)),
17
22
  });
18
23
  return view();
@@ -1,8 +1,8 @@
1
1
  import type { Tool } from 'mu-core';
2
+ import type { Scope } from '../common/index.js';
2
3
  import type { SkillRegistry } from './registry.js';
3
- export type SkillScope = 'local' | 'config';
4
4
  export declare const createSkillWriterTool: (deps: {
5
- dirs: Record<SkillScope, string>;
5
+ dirs: Record<Scope, string>;
6
6
  registry: SkillRegistry;
7
- forceScope?: SkillScope;
7
+ forceScope?: Scope;
8
8
  }) => Tool;