mu-harness 0.16.7 → 0.16.8

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 (51) hide show
  1. package/esm/harness/npm/src/agents/index.d.ts +2 -2
  2. package/esm/harness/npm/src/agents/index.js +1 -1
  3. package/esm/harness/npm/src/agents/parser.js +18 -6
  4. package/esm/harness/npm/src/agents/registry.d.ts +3 -1
  5. package/esm/harness/npm/src/agents/registry.js +21 -0
  6. package/esm/harness/npm/src/agents/types.d.ts +3 -1
  7. package/esm/harness/npm/src/harness/create.js +18 -4
  8. package/esm/harness/npm/src/harness/types.d.ts +10 -1
  9. package/esm/harness/npm/src/permissions/approval-manager.d.ts +31 -0
  10. package/esm/harness/npm/src/permissions/approval-manager.js +55 -0
  11. package/esm/harness/npm/src/permissions/index.d.ts +1 -0
  12. package/esm/harness/npm/src/permissions/index.js +1 -0
  13. package/esm/harness/npm/src/plugin/import-ts.js +1 -1
  14. package/esm/harness/npm/src/scheduler/command.js +1 -1
  15. package/esm/harness/npm/src/scheduler/engine/index.d.ts +2 -1
  16. package/esm/harness/npm/src/scheduler/engine/index.js +1 -0
  17. package/esm/harness/npm/src/scheduler/engine/memory-store.d.ts +2 -0
  18. package/esm/harness/npm/src/scheduler/engine/memory-store.js +23 -0
  19. package/esm/harness/npm/src/scheduler/engine/scheduler.d.ts +2 -1
  20. package/esm/harness/npm/src/scheduler/engine/scheduler.js +9 -1
  21. package/esm/harness/npm/src/scheduler/engine/types.d.ts +19 -1
  22. package/esm/harness/npm/src/scheduler/index.d.ts +2 -2
  23. package/esm/harness/npm/src/scheduler/index.js +1 -1
  24. package/esm/harness/npm/src/subAgents/tool.js +35 -28
  25. package/esm/tui/src/layout/ansi.js +12 -5
  26. package/package.json +3 -3
  27. package/script/harness/npm/src/agents/index.d.ts +2 -2
  28. package/script/harness/npm/src/agents/index.js +3 -1
  29. package/script/harness/npm/src/agents/parser.js +18 -6
  30. package/script/harness/npm/src/agents/registry.d.ts +3 -1
  31. package/script/harness/npm/src/agents/registry.js +24 -1
  32. package/script/harness/npm/src/agents/types.d.ts +3 -1
  33. package/script/harness/npm/src/harness/create.js +17 -3
  34. package/script/harness/npm/src/harness/types.d.ts +10 -1
  35. package/script/harness/npm/src/permissions/approval-manager.d.ts +31 -0
  36. package/script/harness/npm/src/permissions/approval-manager.js +59 -0
  37. package/script/harness/npm/src/permissions/index.d.ts +1 -0
  38. package/script/harness/npm/src/permissions/index.js +3 -1
  39. package/script/harness/npm/src/plugin/import-ts.js +3 -3
  40. package/script/harness/npm/src/scheduler/command.js +1 -1
  41. package/script/harness/npm/src/scheduler/engine/index.d.ts +2 -1
  42. package/script/harness/npm/src/scheduler/engine/index.js +3 -1
  43. package/script/harness/npm/src/scheduler/engine/memory-store.d.ts +2 -0
  44. package/script/harness/npm/src/scheduler/engine/memory-store.js +27 -0
  45. package/script/harness/npm/src/scheduler/engine/scheduler.d.ts +2 -1
  46. package/script/harness/npm/src/scheduler/engine/scheduler.js +9 -1
  47. package/script/harness/npm/src/scheduler/engine/types.d.ts +19 -1
  48. package/script/harness/npm/src/scheduler/index.d.ts +2 -2
  49. package/script/harness/npm/src/scheduler/index.js +2 -1
  50. package/script/harness/npm/src/subAgents/tool.js +35 -28
  51. package/script/tui/src/layout/ansi.js +12 -5
@@ -1,4 +1,4 @@
1
- export type { Agent } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry } from './registry.js';
1
+ export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,3 +1,3 @@
1
- export { createAgentRegistry } from './registry.js';
1
+ export { createAgentRegistry, toolDecision, toolNames } from './registry.js';
2
2
  export { parseAgent } from './parser.js';
3
3
  export { loadAgents } from './loader.js';
@@ -1,10 +1,22 @@
1
1
  import { parseFrontmatter, str } from '../common/index.js';
2
- const parseToolList = (raw) => {
3
- if (Array.isArray(raw)) {
4
- return raw.filter((entry) => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
2
+ const DECISIONS = new Set(['allow', 'ask', 'deny']);
3
+ const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
4
+ .filter((entry) => typeof entry === 'string')
5
+ .map((entry) => entry.trim())
6
+ .filter(Boolean);
7
+ const parseTools = (raw) => {
8
+ if (Array.isArray(raw) || typeof raw === 'string') {
9
+ const list = parseStringList(raw);
10
+ return list.length > 0 ? list : undefined;
11
+ }
12
+ if (raw && typeof raw === 'object') {
13
+ const out = {};
14
+ for (const [tool, decision] of Object.entries(raw)) {
15
+ if (typeof decision === 'string' && DECISIONS.has(decision))
16
+ out[tool] = decision;
17
+ }
18
+ return Object.keys(out).length > 0 ? out : undefined;
5
19
  }
6
- if (typeof raw === 'string')
7
- return raw.split(',').map((part) => part.trim()).filter(Boolean);
8
20
  return undefined;
9
21
  };
10
22
  export const parseAgent = (source, fallbackName) => {
@@ -13,7 +25,7 @@ export const parseAgent = (source, fallbackName) => {
13
25
  name: str(fields.name) ?? fallbackName,
14
26
  description: str(fields.description) ?? '',
15
27
  prompt: body,
16
- tools: parseToolList(fields.tools),
28
+ tools: parseTools(fields.tools),
17
29
  model: str(fields.model),
18
30
  color: str(fields.color),
19
31
  extends: str(fields.extends),
@@ -1,6 +1,8 @@
1
- import type { Agent } from './types.js';
1
+ import type { Agent, ToolDecision } from './types.js';
2
2
  export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
5
  }
6
+ export declare const toolDecision: (agent: Agent, tool: string) => ToolDecision;
7
+ export declare const toolNames: (agent: Agent) => string[] | undefined;
6
8
  export declare const createAgentRegistry: (agents?: Agent[]) => AgentRegistry;
@@ -1,3 +1,24 @@
1
+ const asMap = (tools) => {
2
+ if (!tools)
3
+ return undefined;
4
+ if (Array.isArray(tools))
5
+ return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
6
+ return tools;
7
+ };
8
+ export const toolDecision = (agent, tool) => {
9
+ const map = asMap(agent.tools);
10
+ if (!map)
11
+ return 'allow';
12
+ return map[tool] ?? map['*'] ?? 'deny';
13
+ };
14
+ export const toolNames = (agent) => {
15
+ const map = asMap(agent.tools);
16
+ if (!map)
17
+ return undefined;
18
+ if (map['*'] && map['*'] !== 'deny')
19
+ return ['*'];
20
+ return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
21
+ };
1
22
  const merge = (base, child) => ({
2
23
  name: child.name,
3
24
  description: child.description || base.description,
@@ -1,8 +1,10 @@
1
+ export type ToolDecision = 'allow' | 'ask' | 'deny';
2
+ export type ToolGrants = string[] | Record<string, ToolDecision>;
1
3
  export interface Agent {
2
4
  name: string;
3
5
  description: string;
4
6
  prompt: string;
5
- tools?: string[];
7
+ tools?: ToolGrants;
6
8
  model?: string;
7
9
  color?: string;
8
10
  extends?: string;
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import process from 'node:process';
3
- import { createAgentRegistry, loadAgents } from '../agents/index.js';
3
+ import { createAgentRegistry, loadAgents, toolDecision, toolNames } from '../agents/index.js';
4
4
  import { createAgentsCommand, createCommandRegistry, createHelpCommand, createSessionsCommand, createSkillsCommand, } from '../commands/index.js';
5
5
  import { createHarnessConfig } from '../config/index.js';
6
6
  import { mergeHooks } from '../hooks/index.js';
@@ -18,7 +18,7 @@ const TITLE_AGENT = {
18
18
  tools: [],
19
19
  };
20
20
  export const createHarness = async (options) => {
21
- const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, ...sessionDefaults } = options;
21
+ const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
22
22
  const cwd = options.cwd ?? process.cwd();
23
23
  const config = createHarnessConfig({ hostName, xdg });
24
24
  const models = createModelRegistry({ providers, default: model });
@@ -42,17 +42,28 @@ export const createHarness = async (options) => {
42
42
  const runs = createSubAgentRegistry();
43
43
  const store = createSessionStore({ dir: join(config.dataDir, 'sessions') });
44
44
  const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
45
+ const approvalHook = (getAgent) => approvals
46
+ ? approvals.manager.hooksFor({
47
+ decide: (call) => {
48
+ const agent = getAgent();
49
+ if (!agent)
50
+ return 'allow';
51
+ return approvals.decide ? approvals.decide(agent, call) : toolDecision(agent, call.name);
52
+ },
53
+ agent: () => getAgent()?.name,
54
+ })
55
+ : undefined;
45
56
  const persona = (agent, opts) => createAgentSession({
46
57
  tools: opts.tools,
47
58
  plugins: sessionDefaults.plugins,
48
59
  system: agent.prompt,
49
- hooks: opts.hooks ?? allowList(agent.tools),
60
+ hooks: opts.hooks ?? allowList(toolNames(agent)),
50
61
  ...models.resolve(opts.model ?? agent.model),
51
62
  id: newId(),
52
63
  });
53
64
  const spawn = (agent) => persistTo(store, persona(agent, {
54
65
  tools: sessionTools(),
55
- hooks: mergeHooks([sessionDefaults.hooks, allowList(agent.tools)]),
66
+ hooks: mergeHooks([sessionDefaults.hooks, allowList(toolNames(agent)), approvalHook(() => agent)]),
56
67
  }));
57
68
  const scheduler = enableScheduler && tasks
58
69
  ? createScheduler({
@@ -61,6 +72,8 @@ export const createHarness = async (options) => {
61
72
  try {
62
73
  if (!task.agent)
63
74
  throw new Error('scheduled task is missing an agent');
75
+ if (!task.skill)
76
+ throw new Error('scheduled task is missing a skill');
64
77
  const output = await runSkill({ skills, agents, spawn, runs, parentId: task.id }, {
65
78
  skill: task.skill,
66
79
  task: task.prompt,
@@ -98,6 +111,7 @@ export const createHarness = async (options) => {
98
111
  },
99
112
  revive: ({ id, model: ref, messages }) => createAgentSession({
100
113
  ...sessionDefaults,
114
+ hooks: mergeHooks([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
101
115
  tools: sessionTools([createSubAgentTool({ registry: agents, spawn, runs, parentId: id })]),
102
116
  ...models.resolve(ref),
103
117
  id,
@@ -1,7 +1,8 @@
1
1
  import type { Provider } from 'mu-core';
2
- import type { Agent, AgentRegistry } from '../agents/index.js';
2
+ import type { Agent, AgentRegistry, ToolDecision } from '../agents/index.js';
3
3
  import type { CommandRegistry } from '../commands/index.js';
4
4
  import type { HarnessConfig, HarnessConfigOptions } from '../config/index.js';
5
+ import type { ApprovalManager } from '../permissions/index.js';
5
6
  import type { PluginStore } from '../plugin/index.js';
6
7
  import type { Scheduler, TaskStore } from '../scheduler/index.js';
7
8
  import type { AgentSessionConfig, SessionManager } from '../session/index.js';
@@ -17,6 +18,14 @@ export type HarnessOptions = HarnessConfigOptions & Omit<AgentSessionConfig, 'pr
17
18
  titleModel?: string;
18
19
  cwd?: string;
19
20
  scheduler?: boolean;
21
+ approvals?: {
22
+ manager: ApprovalManager;
23
+ activeAgent: () => Agent | undefined;
24
+ decide?: (agent: Agent, call: {
25
+ name: string;
26
+ input: unknown;
27
+ }) => ToolDecision;
28
+ };
20
29
  };
21
30
  export interface Harness {
22
31
  readonly config: HarnessConfig;
@@ -0,0 +1,31 @@
1
+ import type { AgentSessionHooks } from '../hooks/index.js';
2
+ export type ApprovalAction = 'approve' | 'approve_always' | 'deny';
3
+ export type ApprovalDecision = 'allow' | 'ask' | 'deny';
4
+ export interface PendingApproval {
5
+ id: string;
6
+ name: string;
7
+ input: unknown;
8
+ agent?: string;
9
+ }
10
+ export interface ApprovalManager {
11
+ hooks: AgentSessionHooks;
12
+ hooksFor(opts: {
13
+ decide(call: {
14
+ name: string;
15
+ input: unknown;
16
+ }): ApprovalDecision;
17
+ agent?(): string | undefined;
18
+ }): AgentSessionHooks;
19
+ pending(): PendingApproval[];
20
+ resolve(id: string, action: ApprovalAction): boolean;
21
+ subscribe(listener: (req: PendingApproval) => void): () => void;
22
+ }
23
+ export interface ApprovalManagerOptions {
24
+ needsApproval?: (call: {
25
+ name: string;
26
+ input: unknown;
27
+ }) => boolean;
28
+ askTools?: string[];
29
+ newId?: () => string;
30
+ }
31
+ export declare const createApprovalManager: (options?: ApprovalManagerOptions) => ApprovalManager;
@@ -0,0 +1,55 @@
1
+ import { requireApproval } from './approval.js';
2
+ const denied = (name) => [{ type: 'text', text: `Denied: ${name}` }];
3
+ export const createApprovalManager = (options = {}) => {
4
+ const askTools = options.askTools ? new Set(options.askTools) : undefined;
5
+ const alwaysAllow = new Set();
6
+ const newId = options.newId ?? (() => crypto.randomUUID());
7
+ const keyOf = (agent, tool) => `${agent ?? ''}:${tool}`;
8
+ const waiters = new Map();
9
+ const listeners = new Set();
10
+ const request = (id, name, input, agent) => new Promise((resolve) => {
11
+ const req = { id, name, input, agent };
12
+ waiters.set(id, { resolve, req, key: keyOf(agent, name) });
13
+ for (const listener of listeners)
14
+ listener(req);
15
+ });
16
+ const defaultNeeds = options.needsApproval ?? (({ name }) => (askTools ? askTools.has(name) : true));
17
+ const hooks = requireApproval({
18
+ needsApproval: (call) => defaultNeeds(call) && !alwaysAllow.has(keyOf(undefined, call.name)),
19
+ newId,
20
+ prompt: (call) => request(call.id, call.name, call.input, undefined),
21
+ });
22
+ const hooksFor = ({ decide, agent }) => ({
23
+ beforeToolCall: async (call) => {
24
+ const decision = decide(call);
25
+ if (decision === 'allow')
26
+ return;
27
+ const agentName = agent?.();
28
+ if (decision === 'deny')
29
+ return denied(call.name);
30
+ if (alwaysAllow.has(keyOf(agentName, call.name)))
31
+ return;
32
+ const allow = await request(newId(), call.name, call.input, agentName);
33
+ return allow ? undefined : denied(call.name);
34
+ },
35
+ });
36
+ return {
37
+ hooks,
38
+ hooksFor,
39
+ pending: () => [...waiters.values()].map((w) => w.req),
40
+ resolve: (id, action) => {
41
+ const waiter = waiters.get(id);
42
+ if (!waiter)
43
+ return false;
44
+ waiters.delete(id);
45
+ if (action === 'approve_always')
46
+ alwaysAllow.add(waiter.key);
47
+ waiter.resolve(action !== 'deny');
48
+ return true;
49
+ },
50
+ subscribe: (listener) => {
51
+ listeners.add(listener);
52
+ return () => listeners.delete(listener);
53
+ },
54
+ };
55
+ };
@@ -1,3 +1,4 @@
1
1
  export { allowList, filterTools } from './allow-list.js';
2
2
  export { type ApprovalCall, requireApproval, type RequireApprovalOptions } from './approval.js';
3
+ export { type ApprovalAction, type ApprovalDecision, type ApprovalManager, type ApprovalManagerOptions, createApprovalManager, type PendingApproval, } from './approval-manager.js';
3
4
  export { matchesAnyGlob } from './glob.js';
@@ -1,3 +1,4 @@
1
1
  export { allowList, filterTools } from './allow-list.js';
2
2
  export { requireApproval } from './approval.js';
3
+ export { createApprovalManager, } from './approval-manager.js';
3
4
  export { matchesAnyGlob } from './glob.js';
@@ -1,4 +1,3 @@
1
- import { transformSync } from '@swc/wasm-typescript';
2
1
  import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
3
2
  import { tmpdir } from 'node:os';
4
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
@@ -62,6 +61,7 @@ const commonBase = (files) => {
62
61
  const outName = (path) => path.replace(TS_EXT, '.mjs');
63
62
  const rewriteSpecifiers = (code) => code.replace(SPECIFIER, (whole, quote, spec) => whole.replace(`${quote}${spec}${quote}`, `${quote}${outName(spec)}${quote}`));
64
63
  const transpileTree = async (entry) => {
64
+ const { transformSync } = await import('@swc/wasm-typescript');
65
65
  const sources = await collect(entry);
66
66
  if (sources.size === 1) {
67
67
  const { code } = transformSync(sources.get(entry), { mode: 'transform', module: true });
@@ -12,7 +12,7 @@ export const createTasksCommand = (tasks) => ({
12
12
  ? `every ${t.schedule.ms}ms`
13
13
  : 'once';
14
14
  const state = t.enabled ? when : `${when} (disabled)`;
15
- return `- ${t.id} — ${t.skill} [${state}]`;
15
+ return `- ${t.id} — ${t.skill ?? t.agent ?? 'task'} [${state}]`;
16
16
  };
17
17
  return { ok: true, output: list.map(describe).join('\n') };
18
18
  },
@@ -1,3 +1,4 @@
1
- export type { Schedule, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './types.js';
1
+ export type { Schedule, SchedulerEvent, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './types.js';
2
2
  export { createTaskStore } from './store.js';
3
+ export { createMemoryTaskStore } from './memory-store.js';
3
4
  export { createScheduler, type Scheduler } from './scheduler.js';
@@ -1,2 +1,3 @@
1
1
  export { createTaskStore } from './store.js';
2
+ export { createMemoryTaskStore } from './memory-store.js';
2
3
  export { createScheduler } from './scheduler.js';
@@ -0,0 +1,2 @@
1
+ import type { Task, TaskStore } from './types.js';
2
+ export declare const createMemoryTaskStore: (initial?: Task[]) => TaskStore;
@@ -0,0 +1,23 @@
1
+ export const createMemoryTaskStore = (initial = []) => {
2
+ const tasks = new Map(initial.map((task) => [task.id, task]));
3
+ return {
4
+ list: async () => [...tasks.values()],
5
+ get: async (id) => tasks.get(id),
6
+ create: async (input) => {
7
+ const task = { ...input, id: crypto.randomUUID(), enabled: input.enabled ?? true, createdAt: Date.now() };
8
+ tasks.set(task.id, task);
9
+ return task;
10
+ },
11
+ update: async (id, patch) => {
12
+ const task = tasks.get(id);
13
+ if (!task)
14
+ return undefined;
15
+ const next = { ...task, ...patch, id: task.id };
16
+ tasks.set(id, next);
17
+ return next;
18
+ },
19
+ remove: async (id) => {
20
+ tasks.delete(id);
21
+ },
22
+ };
23
+ };
@@ -1,4 +1,4 @@
1
- import type { Task, TaskRunner, TaskStore } from './types.js';
1
+ import type { SchedulerEvent, Task, TaskRunner, TaskStore } from './types.js';
2
2
  export interface Scheduler {
3
3
  start(): Promise<void>;
4
4
  stop(): void;
@@ -8,5 +8,6 @@ export interface Scheduler {
8
8
  export declare const createScheduler: (deps: {
9
9
  store: TaskStore;
10
10
  run: TaskRunner;
11
+ onEvent?: (event: SchedulerEvent) => void;
11
12
  onError?: (error: unknown, task: Task) => void;
12
13
  }) => Scheduler;
@@ -8,14 +8,22 @@ export const createScheduler = (deps) => {
8
8
  const task = await store.get(id);
9
9
  if (!task || !task.enabled)
10
10
  return;
11
+ const at = Date.now();
12
+ deps.onEvent?.({ type: 'task_started', task, at });
11
13
  try {
12
14
  const result = await run(task);
13
15
  await store.update(id, { lastRun: Date.now(), lastResult: result });
16
+ const durationMs = Date.now() - at;
17
+ if (result.ok)
18
+ deps.onEvent?.({ type: 'task_completed', task, at: Date.now(), durationMs, result });
19
+ else
20
+ deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs, error: result.error ?? 'task failed' });
14
21
  }
15
22
  catch (error) {
16
23
  deps.onError?.(error, task);
17
24
  const message = error instanceof Error ? error.message : String(error);
18
25
  await store.update(id, { lastRun: Date.now(), lastResult: { ok: false, error: message } });
26
+ deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs: Date.now() - at, error: message });
19
27
  }
20
28
  if (task.schedule.kind === 'once')
21
29
  await store.update(id, { enabled: false });
@@ -33,7 +41,7 @@ export const createScheduler = (deps) => {
33
41
  return;
34
42
  const s = task.schedule;
35
43
  if (s.kind === 'cron') {
36
- crons.set(task.id, new Cron(s.expr, () => void fire(task.id)));
44
+ crons.set(task.id, new Cron(s.expr, s.timezone ? { timezone: s.timezone } : {}, () => void fire(task.id)));
37
45
  }
38
46
  else if (s.kind === 'interval') {
39
47
  timers.set(task.id, setInterval(() => void fire(task.id), s.ms));
@@ -1,6 +1,7 @@
1
1
  export type Schedule = {
2
2
  kind: 'cron';
3
3
  expr: string;
4
+ timezone?: string;
4
5
  } | {
5
6
  kind: 'interval';
6
7
  ms: number;
@@ -15,7 +16,7 @@ export interface TaskResult {
15
16
  }
16
17
  export interface Task {
17
18
  id: string;
18
- skill: string;
19
+ skill?: string;
19
20
  prompt: string;
20
21
  agent?: string;
21
22
  schedule: Schedule;
@@ -24,6 +25,23 @@ export interface Task {
24
25
  lastRun?: number;
25
26
  lastResult?: TaskResult;
26
27
  }
28
+ export type SchedulerEvent = {
29
+ type: 'task_started';
30
+ task: Task;
31
+ at: number;
32
+ } | {
33
+ type: 'task_completed';
34
+ task: Task;
35
+ at: number;
36
+ durationMs: number;
37
+ result: TaskResult;
38
+ } | {
39
+ type: 'task_failed';
40
+ task: Task;
41
+ at: number;
42
+ durationMs: number;
43
+ error: string;
44
+ };
27
45
  export type TaskInput = Omit<Task, 'id' | 'enabled' | 'createdAt' | 'lastRun' | 'lastResult'> & {
28
46
  enabled?: boolean;
29
47
  };
@@ -1,4 +1,4 @@
1
- export type { Schedule, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './engine/index.js';
2
- export { createScheduler, createTaskStore, type Scheduler } from './engine/index.js';
1
+ export type { Schedule, SchedulerEvent, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './engine/index.js';
2
+ export { createMemoryTaskStore, createScheduler, createTaskStore, type Scheduler } from './engine/index.js';
3
3
  export { createScheduleTaskTool } from './tool.js';
4
4
  export { createTasksCommand } from './command.js';
@@ -1,3 +1,3 @@
1
- export { createScheduler, createTaskStore } from './engine/index.js';
1
+ export { createMemoryTaskStore, createScheduler, createTaskStore } from './engine/index.js';
2
2
  export { createScheduleTaskTool } from './tool.js';
3
3
  export { createTasksCommand } from './command.js';
@@ -1,30 +1,37 @@
1
1
  import { runSubAgent } from './runner.js';
2
- export const createSubAgentTool = (deps) => ({
3
- name: 'subagent',
4
- description: 'Delegate an isolated task to a named sub-agent. Returns its final answer.',
5
- prompt: 'Delegate self-contained research or sub-tasks to a named sub-agent with `subagent`; treat its answer as research input.',
6
- parameters: {
7
- type: 'object',
8
- properties: {
9
- agent: { type: 'string', description: 'Sub-agent name.' },
10
- task: { type: 'string', description: 'The task to delegate.' },
2
+ const BASE_PROMPT = 'Delegate self-contained research or sub-tasks to a named sub-agent with `subagent`; treat its answer as research input.';
3
+ export const createSubAgentTool = (deps) => {
4
+ const roster = deps.registry.list()
5
+ .filter((agent) => agent.name !== 'title')
6
+ .map((agent) => `- ${agent.name}: ${agent.description}`)
7
+ .join('\n');
8
+ return {
9
+ name: 'subagent',
10
+ description: 'Delegate an isolated task to a named sub-agent. Returns its final answer.',
11
+ prompt: roster ? `${BASE_PROMPT}\n\nAvailable sub-agents:\n${roster}` : BASE_PROMPT,
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ agent: { type: 'string', description: 'Sub-agent name.' },
16
+ task: { type: 'string', description: 'The task to delegate.' },
17
+ },
18
+ required: ['agent', 'task'],
19
+ additionalProperties: false,
11
20
  },
12
- required: ['agent', 'task'],
13
- additionalProperties: false,
14
- },
15
- run: async (input, ctx) => {
16
- const { agent, task } = (input ?? {});
17
- if (!agent || !task)
18
- return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
19
- const def = deps.registry.get(agent);
20
- if (!def)
21
- return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
22
- const result = await runSubAgent(def, task, {
23
- spawn: deps.spawn,
24
- runs: deps.runs,
25
- parentId: deps.parentId,
26
- signal: ctx.signal,
27
- });
28
- return [{ type: 'text', text: result.text }];
29
- },
30
- });
21
+ run: async (input, ctx) => {
22
+ const { agent, task } = (input ?? {});
23
+ if (!agent || !task)
24
+ return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
25
+ const def = deps.registry.get(agent);
26
+ if (!def)
27
+ return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
28
+ const result = await runSubAgent(def, task, {
29
+ spawn: deps.spawn,
30
+ runs: deps.runs,
31
+ parentId: deps.parentId,
32
+ signal: ctx.signal,
33
+ });
34
+ return [{ type: 'text', text: result.text }];
35
+ },
36
+ };
37
+ };
@@ -288,12 +288,19 @@ function sgrDelta(prev, next) {
288
288
  return `\x1b[${params.join(';')}m`;
289
289
  }
290
290
  if (prev.bold !== next.bold || prev.dim !== next.dim) {
291
- if (!next.bold && !next.dim && (prev.bold || prev.dim))
291
+ if ((prev.bold && !next.bold) || (prev.dim && !next.dim)) {
292
292
  params.push('22');
293
- if (next.bold && !prev.bold)
294
- params.push('1');
295
- if (next.dim && !prev.dim)
296
- params.push('2');
293
+ if (next.bold)
294
+ params.push('1');
295
+ if (next.dim)
296
+ params.push('2');
297
+ }
298
+ else {
299
+ if (next.bold && !prev.bold)
300
+ params.push('1');
301
+ if (next.dim && !prev.dim)
302
+ params.push('2');
303
+ }
297
304
  }
298
305
  if (prev.italic !== next.italic)
299
306
  params.push(next.italic ? '3' : '23');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.16.7",
3
+ "version": "0.16.8",
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",
@@ -16,8 +16,8 @@
16
16
  "dependencies": {
17
17
  "@swc/wasm-typescript": "^1.15.0",
18
18
  "croner": "^9.0.0",
19
- "mu-core": "^0.16.7",
20
- "mu-tui": "^0.16.7"
19
+ "mu-core": "^0.16.8",
20
+ "mu-tui": "^0.16.8"
21
21
  },
22
22
  "_generatedBy": "dnt@dev"
23
23
  }
@@ -1,4 +1,4 @@
1
- export type { Agent } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry } from './registry.js';
1
+ export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadAgents = exports.parseAgent = exports.createAgentRegistry = void 0;
3
+ exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = 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
+ Object.defineProperty(exports, "toolDecision", { enumerable: true, get: function () { return registry_js_1.toolDecision; } });
7
+ Object.defineProperty(exports, "toolNames", { enumerable: true, get: function () { return registry_js_1.toolNames; } });
6
8
  var parser_js_1 = require("./parser.js");
7
9
  Object.defineProperty(exports, "parseAgent", { enumerable: true, get: function () { return parser_js_1.parseAgent; } });
8
10
  var loader_js_1 = require("./loader.js");
@@ -2,12 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseAgent = void 0;
4
4
  const index_js_1 = require("../common/index.js");
5
- const parseToolList = (raw) => {
6
- if (Array.isArray(raw)) {
7
- return raw.filter((entry) => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
5
+ const DECISIONS = new Set(['allow', 'ask', 'deny']);
6
+ const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
7
+ .filter((entry) => typeof entry === 'string')
8
+ .map((entry) => entry.trim())
9
+ .filter(Boolean);
10
+ const parseTools = (raw) => {
11
+ if (Array.isArray(raw) || typeof raw === 'string') {
12
+ const list = parseStringList(raw);
13
+ return list.length > 0 ? list : undefined;
14
+ }
15
+ if (raw && typeof raw === 'object') {
16
+ const out = {};
17
+ for (const [tool, decision] of Object.entries(raw)) {
18
+ if (typeof decision === 'string' && DECISIONS.has(decision))
19
+ out[tool] = decision;
20
+ }
21
+ return Object.keys(out).length > 0 ? out : undefined;
8
22
  }
9
- if (typeof raw === 'string')
10
- return raw.split(',').map((part) => part.trim()).filter(Boolean);
11
23
  return undefined;
12
24
  };
13
25
  const parseAgent = (source, fallbackName) => {
@@ -16,7 +28,7 @@ const parseAgent = (source, fallbackName) => {
16
28
  name: (0, index_js_1.str)(fields.name) ?? fallbackName,
17
29
  description: (0, index_js_1.str)(fields.description) ?? '',
18
30
  prompt: body,
19
- tools: parseToolList(fields.tools),
31
+ tools: parseTools(fields.tools),
20
32
  model: (0, index_js_1.str)(fields.model),
21
33
  color: (0, index_js_1.str)(fields.color),
22
34
  extends: (0, index_js_1.str)(fields.extends),
@@ -1,6 +1,8 @@
1
- import type { Agent } from './types.js';
1
+ import type { Agent, ToolDecision } from './types.js';
2
2
  export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
5
  }
6
+ export declare const toolDecision: (agent: Agent, tool: string) => ToolDecision;
7
+ export declare const toolNames: (agent: Agent) => string[] | undefined;
6
8
  export declare const createAgentRegistry: (agents?: Agent[]) => AgentRegistry;
@@ -1,6 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAgentRegistry = void 0;
3
+ exports.createAgentRegistry = exports.toolNames = exports.toolDecision = void 0;
4
+ const asMap = (tools) => {
5
+ if (!tools)
6
+ return undefined;
7
+ if (Array.isArray(tools))
8
+ return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
9
+ return tools;
10
+ };
11
+ const toolDecision = (agent, tool) => {
12
+ const map = asMap(agent.tools);
13
+ if (!map)
14
+ return 'allow';
15
+ return map[tool] ?? map['*'] ?? 'deny';
16
+ };
17
+ exports.toolDecision = toolDecision;
18
+ const toolNames = (agent) => {
19
+ const map = asMap(agent.tools);
20
+ if (!map)
21
+ return undefined;
22
+ if (map['*'] && map['*'] !== 'deny')
23
+ return ['*'];
24
+ return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
25
+ };
26
+ exports.toolNames = toolNames;
4
27
  const merge = (base, child) => ({
5
28
  name: child.name,
6
29
  description: child.description || base.description,
@@ -1,8 +1,10 @@
1
+ export type ToolDecision = 'allow' | 'ask' | 'deny';
2
+ export type ToolGrants = string[] | Record<string, ToolDecision>;
1
3
  export interface Agent {
2
4
  name: string;
3
5
  description: string;
4
6
  prompt: string;
5
- tools?: string[];
7
+ tools?: ToolGrants;
6
8
  model?: string;
7
9
  color?: string;
8
10
  extends?: string;
@@ -24,7 +24,7 @@ const TITLE_AGENT = {
24
24
  tools: [],
25
25
  };
26
26
  const createHarness = async (options) => {
27
- const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, ...sessionDefaults } = options;
27
+ const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
28
28
  const cwd = options.cwd ?? node_process_1.default.cwd();
29
29
  const config = (0, index_js_3.createHarnessConfig)({ hostName, xdg });
30
30
  const models = (0, models_js_1.createModelRegistry)({ providers, default: model });
@@ -48,17 +48,28 @@ const createHarness = async (options) => {
48
48
  const runs = (0, index_js_10.createSubAgentRegistry)();
49
49
  const store = (0, index_js_8.createSessionStore)({ dir: (0, node_path_1.join)(config.dataDir, 'sessions') });
50
50
  const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
51
+ const approvalHook = (getAgent) => approvals
52
+ ? approvals.manager.hooksFor({
53
+ decide: (call) => {
54
+ const agent = getAgent();
55
+ if (!agent)
56
+ return 'allow';
57
+ return approvals.decide ? approvals.decide(agent, call) : (0, index_js_1.toolDecision)(agent, call.name);
58
+ },
59
+ agent: () => getAgent()?.name,
60
+ })
61
+ : undefined;
51
62
  const persona = (agent, opts) => (0, index_js_8.createAgentSession)({
52
63
  tools: opts.tools,
53
64
  plugins: sessionDefaults.plugins,
54
65
  system: agent.prompt,
55
- hooks: opts.hooks ?? (0, index_js_5.allowList)(agent.tools),
66
+ hooks: opts.hooks ?? (0, index_js_5.allowList)((0, index_js_1.toolNames)(agent)),
56
67
  ...models.resolve(opts.model ?? agent.model),
57
68
  id: newId(),
58
69
  });
59
70
  const spawn = (agent) => (0, index_js_8.persistTo)(store, persona(agent, {
60
71
  tools: sessionTools(),
61
- hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, (0, index_js_5.allowList)(agent.tools)]),
72
+ hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, (0, index_js_5.allowList)((0, index_js_1.toolNames)(agent)), approvalHook(() => agent)]),
62
73
  }));
63
74
  const scheduler = enableScheduler && tasks
64
75
  ? (0, index_js_7.createScheduler)({
@@ -67,6 +78,8 @@ const createHarness = async (options) => {
67
78
  try {
68
79
  if (!task.agent)
69
80
  throw new Error('scheduled task is missing an agent');
81
+ if (!task.skill)
82
+ throw new Error('scheduled task is missing a skill');
70
83
  const output = await (0, index_js_9.runSkill)({ skills, agents, spawn, runs, parentId: task.id }, {
71
84
  skill: task.skill,
72
85
  task: task.prompt,
@@ -104,6 +117,7 @@ const createHarness = async (options) => {
104
117
  },
105
118
  revive: ({ id, model: ref, messages }) => (0, index_js_8.createAgentSession)({
106
119
  ...sessionDefaults,
120
+ hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
107
121
  tools: sessionTools([(0, index_js_10.createSubAgentTool)({ registry: agents, spawn, runs, parentId: id })]),
108
122
  ...models.resolve(ref),
109
123
  id,
@@ -1,7 +1,8 @@
1
1
  import type { Provider } from 'mu-core';
2
- import type { Agent, AgentRegistry } from '../agents/index.js';
2
+ import type { Agent, AgentRegistry, ToolDecision } from '../agents/index.js';
3
3
  import type { CommandRegistry } from '../commands/index.js';
4
4
  import type { HarnessConfig, HarnessConfigOptions } from '../config/index.js';
5
+ import type { ApprovalManager } from '../permissions/index.js';
5
6
  import type { PluginStore } from '../plugin/index.js';
6
7
  import type { Scheduler, TaskStore } from '../scheduler/index.js';
7
8
  import type { AgentSessionConfig, SessionManager } from '../session/index.js';
@@ -17,6 +18,14 @@ export type HarnessOptions = HarnessConfigOptions & Omit<AgentSessionConfig, 'pr
17
18
  titleModel?: string;
18
19
  cwd?: string;
19
20
  scheduler?: boolean;
21
+ approvals?: {
22
+ manager: ApprovalManager;
23
+ activeAgent: () => Agent | undefined;
24
+ decide?: (agent: Agent, call: {
25
+ name: string;
26
+ input: unknown;
27
+ }) => ToolDecision;
28
+ };
20
29
  };
21
30
  export interface Harness {
22
31
  readonly config: HarnessConfig;
@@ -0,0 +1,31 @@
1
+ import type { AgentSessionHooks } from '../hooks/index.js';
2
+ export type ApprovalAction = 'approve' | 'approve_always' | 'deny';
3
+ export type ApprovalDecision = 'allow' | 'ask' | 'deny';
4
+ export interface PendingApproval {
5
+ id: string;
6
+ name: string;
7
+ input: unknown;
8
+ agent?: string;
9
+ }
10
+ export interface ApprovalManager {
11
+ hooks: AgentSessionHooks;
12
+ hooksFor(opts: {
13
+ decide(call: {
14
+ name: string;
15
+ input: unknown;
16
+ }): ApprovalDecision;
17
+ agent?(): string | undefined;
18
+ }): AgentSessionHooks;
19
+ pending(): PendingApproval[];
20
+ resolve(id: string, action: ApprovalAction): boolean;
21
+ subscribe(listener: (req: PendingApproval) => void): () => void;
22
+ }
23
+ export interface ApprovalManagerOptions {
24
+ needsApproval?: (call: {
25
+ name: string;
26
+ input: unknown;
27
+ }) => boolean;
28
+ askTools?: string[];
29
+ newId?: () => string;
30
+ }
31
+ export declare const createApprovalManager: (options?: ApprovalManagerOptions) => ApprovalManager;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createApprovalManager = void 0;
4
+ const approval_js_1 = require("./approval.js");
5
+ const denied = (name) => [{ type: 'text', text: `Denied: ${name}` }];
6
+ const createApprovalManager = (options = {}) => {
7
+ const askTools = options.askTools ? new Set(options.askTools) : undefined;
8
+ const alwaysAllow = new Set();
9
+ const newId = options.newId ?? (() => crypto.randomUUID());
10
+ const keyOf = (agent, tool) => `${agent ?? ''}:${tool}`;
11
+ const waiters = new Map();
12
+ const listeners = new Set();
13
+ const request = (id, name, input, agent) => new Promise((resolve) => {
14
+ const req = { id, name, input, agent };
15
+ waiters.set(id, { resolve, req, key: keyOf(agent, name) });
16
+ for (const listener of listeners)
17
+ listener(req);
18
+ });
19
+ const defaultNeeds = options.needsApproval ?? (({ name }) => (askTools ? askTools.has(name) : true));
20
+ const hooks = (0, approval_js_1.requireApproval)({
21
+ needsApproval: (call) => defaultNeeds(call) && !alwaysAllow.has(keyOf(undefined, call.name)),
22
+ newId,
23
+ prompt: (call) => request(call.id, call.name, call.input, undefined),
24
+ });
25
+ const hooksFor = ({ decide, agent }) => ({
26
+ beforeToolCall: async (call) => {
27
+ const decision = decide(call);
28
+ if (decision === 'allow')
29
+ return;
30
+ const agentName = agent?.();
31
+ if (decision === 'deny')
32
+ return denied(call.name);
33
+ if (alwaysAllow.has(keyOf(agentName, call.name)))
34
+ return;
35
+ const allow = await request(newId(), call.name, call.input, agentName);
36
+ return allow ? undefined : denied(call.name);
37
+ },
38
+ });
39
+ return {
40
+ hooks,
41
+ hooksFor,
42
+ pending: () => [...waiters.values()].map((w) => w.req),
43
+ resolve: (id, action) => {
44
+ const waiter = waiters.get(id);
45
+ if (!waiter)
46
+ return false;
47
+ waiters.delete(id);
48
+ if (action === 'approve_always')
49
+ alwaysAllow.add(waiter.key);
50
+ waiter.resolve(action !== 'deny');
51
+ return true;
52
+ },
53
+ subscribe: (listener) => {
54
+ listeners.add(listener);
55
+ return () => listeners.delete(listener);
56
+ },
57
+ };
58
+ };
59
+ exports.createApprovalManager = createApprovalManager;
@@ -1,3 +1,4 @@
1
1
  export { allowList, filterTools } from './allow-list.js';
2
2
  export { type ApprovalCall, requireApproval, type RequireApprovalOptions } from './approval.js';
3
+ export { type ApprovalAction, type ApprovalDecision, type ApprovalManager, type ApprovalManagerOptions, createApprovalManager, type PendingApproval, } from './approval-manager.js';
3
4
  export { matchesAnyGlob } from './glob.js';
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.matchesAnyGlob = exports.requireApproval = exports.filterTools = exports.allowList = void 0;
3
+ exports.matchesAnyGlob = exports.createApprovalManager = exports.requireApproval = exports.filterTools = exports.allowList = void 0;
4
4
  var allow_list_js_1 = require("./allow-list.js");
5
5
  Object.defineProperty(exports, "allowList", { enumerable: true, get: function () { return allow_list_js_1.allowList; } });
6
6
  Object.defineProperty(exports, "filterTools", { enumerable: true, get: function () { return allow_list_js_1.filterTools; } });
7
7
  var approval_js_1 = require("./approval.js");
8
8
  Object.defineProperty(exports, "requireApproval", { enumerable: true, get: function () { return approval_js_1.requireApproval; } });
9
+ var approval_manager_js_1 = require("./approval-manager.js");
10
+ Object.defineProperty(exports, "createApprovalManager", { enumerable: true, get: function () { return approval_manager_js_1.createApprovalManager; } });
9
11
  var glob_js_1 = require("./glob.js");
10
12
  Object.defineProperty(exports, "matchesAnyGlob", { enumerable: true, get: function () { return glob_js_1.matchesAnyGlob; } });
@@ -34,7 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.importModule = void 0;
37
- const wasm_typescript_1 = require("@swc/wasm-typescript");
38
37
  const promises_1 = require("node:fs/promises");
39
38
  const node_os_1 = require("node:os");
40
39
  const node_path_1 = require("node:path");
@@ -98,15 +97,16 @@ const commonBase = (files) => {
98
97
  const outName = (path) => path.replace(TS_EXT, '.mjs');
99
98
  const rewriteSpecifiers = (code) => code.replace(SPECIFIER, (whole, quote, spec) => whole.replace(`${quote}${spec}${quote}`, `${quote}${outName(spec)}${quote}`));
100
99
  const transpileTree = async (entry) => {
100
+ const { transformSync } = await Promise.resolve().then(() => __importStar(require('@swc/wasm-typescript')));
101
101
  const sources = await collect(entry);
102
102
  if (sources.size === 1) {
103
- const { code } = (0, wasm_typescript_1.transformSync)(sources.get(entry), { mode: 'transform', module: true });
103
+ const { code } = transformSync(sources.get(entry), { mode: 'transform', module: true });
104
104
  return `data:text/javascript;base64,${btoa(unescape(encodeURIComponent(code)))}`;
105
105
  }
106
106
  const base = commonBase([...sources.keys()]);
107
107
  const outDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'mu-plugin-'));
108
108
  for (const [file, source] of sources) {
109
- const { code } = (0, wasm_typescript_1.transformSync)(source, { mode: 'transform', module: true });
109
+ const { code } = transformSync(source, { mode: 'transform', module: true });
110
110
  const out = (0, node_path_1.join)(outDir, outName((0, node_path_1.relative)(base, file)));
111
111
  await (0, promises_1.mkdir)((0, node_path_1.dirname)(out), { recursive: true });
112
112
  await (0, promises_1.writeFile)(out, rewriteSpecifiers(code), 'utf-8');
@@ -15,7 +15,7 @@ const createTasksCommand = (tasks) => ({
15
15
  ? `every ${t.schedule.ms}ms`
16
16
  : 'once';
17
17
  const state = t.enabled ? when : `${when} (disabled)`;
18
- return `- ${t.id} — ${t.skill} [${state}]`;
18
+ return `- ${t.id} — ${t.skill ?? t.agent ?? 'task'} [${state}]`;
19
19
  };
20
20
  return { ok: true, output: list.map(describe).join('\n') };
21
21
  },
@@ -1,3 +1,4 @@
1
- export type { Schedule, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './types.js';
1
+ export type { Schedule, SchedulerEvent, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './types.js';
2
2
  export { createTaskStore } from './store.js';
3
+ export { createMemoryTaskStore } from './memory-store.js';
3
4
  export { createScheduler, type Scheduler } from './scheduler.js';
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createScheduler = exports.createTaskStore = void 0;
3
+ exports.createScheduler = exports.createMemoryTaskStore = exports.createTaskStore = void 0;
4
4
  var store_js_1 = require("./store.js");
5
5
  Object.defineProperty(exports, "createTaskStore", { enumerable: true, get: function () { return store_js_1.createTaskStore; } });
6
+ var memory_store_js_1 = require("./memory-store.js");
7
+ Object.defineProperty(exports, "createMemoryTaskStore", { enumerable: true, get: function () { return memory_store_js_1.createMemoryTaskStore; } });
6
8
  var scheduler_js_1 = require("./scheduler.js");
7
9
  Object.defineProperty(exports, "createScheduler", { enumerable: true, get: function () { return scheduler_js_1.createScheduler; } });
@@ -0,0 +1,2 @@
1
+ import type { Task, TaskStore } from './types.js';
2
+ export declare const createMemoryTaskStore: (initial?: Task[]) => TaskStore;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMemoryTaskStore = void 0;
4
+ const createMemoryTaskStore = (initial = []) => {
5
+ const tasks = new Map(initial.map((task) => [task.id, task]));
6
+ return {
7
+ list: async () => [...tasks.values()],
8
+ get: async (id) => tasks.get(id),
9
+ create: async (input) => {
10
+ const task = { ...input, id: crypto.randomUUID(), enabled: input.enabled ?? true, createdAt: Date.now() };
11
+ tasks.set(task.id, task);
12
+ return task;
13
+ },
14
+ update: async (id, patch) => {
15
+ const task = tasks.get(id);
16
+ if (!task)
17
+ return undefined;
18
+ const next = { ...task, ...patch, id: task.id };
19
+ tasks.set(id, next);
20
+ return next;
21
+ },
22
+ remove: async (id) => {
23
+ tasks.delete(id);
24
+ },
25
+ };
26
+ };
27
+ exports.createMemoryTaskStore = createMemoryTaskStore;
@@ -1,4 +1,4 @@
1
- import type { Task, TaskRunner, TaskStore } from './types.js';
1
+ import type { SchedulerEvent, Task, TaskRunner, TaskStore } from './types.js';
2
2
  export interface Scheduler {
3
3
  start(): Promise<void>;
4
4
  stop(): void;
@@ -8,5 +8,6 @@ export interface Scheduler {
8
8
  export declare const createScheduler: (deps: {
9
9
  store: TaskStore;
10
10
  run: TaskRunner;
11
+ onEvent?: (event: SchedulerEvent) => void;
11
12
  onError?: (error: unknown, task: Task) => void;
12
13
  }) => Scheduler;
@@ -11,14 +11,22 @@ const createScheduler = (deps) => {
11
11
  const task = await store.get(id);
12
12
  if (!task || !task.enabled)
13
13
  return;
14
+ const at = Date.now();
15
+ deps.onEvent?.({ type: 'task_started', task, at });
14
16
  try {
15
17
  const result = await run(task);
16
18
  await store.update(id, { lastRun: Date.now(), lastResult: result });
19
+ const durationMs = Date.now() - at;
20
+ if (result.ok)
21
+ deps.onEvent?.({ type: 'task_completed', task, at: Date.now(), durationMs, result });
22
+ else
23
+ deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs, error: result.error ?? 'task failed' });
17
24
  }
18
25
  catch (error) {
19
26
  deps.onError?.(error, task);
20
27
  const message = error instanceof Error ? error.message : String(error);
21
28
  await store.update(id, { lastRun: Date.now(), lastResult: { ok: false, error: message } });
29
+ deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs: Date.now() - at, error: message });
22
30
  }
23
31
  if (task.schedule.kind === 'once')
24
32
  await store.update(id, { enabled: false });
@@ -36,7 +44,7 @@ const createScheduler = (deps) => {
36
44
  return;
37
45
  const s = task.schedule;
38
46
  if (s.kind === 'cron') {
39
- crons.set(task.id, new croner_1.Cron(s.expr, () => void fire(task.id)));
47
+ crons.set(task.id, new croner_1.Cron(s.expr, s.timezone ? { timezone: s.timezone } : {}, () => void fire(task.id)));
40
48
  }
41
49
  else if (s.kind === 'interval') {
42
50
  timers.set(task.id, setInterval(() => void fire(task.id), s.ms));
@@ -1,6 +1,7 @@
1
1
  export type Schedule = {
2
2
  kind: 'cron';
3
3
  expr: string;
4
+ timezone?: string;
4
5
  } | {
5
6
  kind: 'interval';
6
7
  ms: number;
@@ -15,7 +16,7 @@ export interface TaskResult {
15
16
  }
16
17
  export interface Task {
17
18
  id: string;
18
- skill: string;
19
+ skill?: string;
19
20
  prompt: string;
20
21
  agent?: string;
21
22
  schedule: Schedule;
@@ -24,6 +25,23 @@ export interface Task {
24
25
  lastRun?: number;
25
26
  lastResult?: TaskResult;
26
27
  }
28
+ export type SchedulerEvent = {
29
+ type: 'task_started';
30
+ task: Task;
31
+ at: number;
32
+ } | {
33
+ type: 'task_completed';
34
+ task: Task;
35
+ at: number;
36
+ durationMs: number;
37
+ result: TaskResult;
38
+ } | {
39
+ type: 'task_failed';
40
+ task: Task;
41
+ at: number;
42
+ durationMs: number;
43
+ error: string;
44
+ };
27
45
  export type TaskInput = Omit<Task, 'id' | 'enabled' | 'createdAt' | 'lastRun' | 'lastResult'> & {
28
46
  enabled?: boolean;
29
47
  };
@@ -1,4 +1,4 @@
1
- export type { Schedule, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './engine/index.js';
2
- export { createScheduler, createTaskStore, type Scheduler } from './engine/index.js';
1
+ export type { Schedule, SchedulerEvent, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './engine/index.js';
2
+ export { createMemoryTaskStore, createScheduler, createTaskStore, type Scheduler } from './engine/index.js';
3
3
  export { createScheduleTaskTool } from './tool.js';
4
4
  export { createTasksCommand } from './command.js';
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createTasksCommand = exports.createScheduleTaskTool = exports.createTaskStore = exports.createScheduler = void 0;
3
+ exports.createTasksCommand = exports.createScheduleTaskTool = exports.createTaskStore = exports.createScheduler = exports.createMemoryTaskStore = void 0;
4
4
  var index_js_1 = require("./engine/index.js");
5
+ Object.defineProperty(exports, "createMemoryTaskStore", { enumerable: true, get: function () { return index_js_1.createMemoryTaskStore; } });
5
6
  Object.defineProperty(exports, "createScheduler", { enumerable: true, get: function () { return index_js_1.createScheduler; } });
6
7
  Object.defineProperty(exports, "createTaskStore", { enumerable: true, get: function () { return index_js_1.createTaskStore; } });
7
8
  var tool_js_1 = require("./tool.js");
@@ -2,33 +2,40 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSubAgentTool = void 0;
4
4
  const runner_js_1 = require("./runner.js");
5
- const createSubAgentTool = (deps) => ({
6
- name: 'subagent',
7
- description: 'Delegate an isolated task to a named sub-agent. Returns its final answer.',
8
- prompt: 'Delegate self-contained research or sub-tasks to a named sub-agent with `subagent`; treat its answer as research input.',
9
- parameters: {
10
- type: 'object',
11
- properties: {
12
- agent: { type: 'string', description: 'Sub-agent name.' },
13
- task: { type: 'string', description: 'The task to delegate.' },
5
+ const BASE_PROMPT = 'Delegate self-contained research or sub-tasks to a named sub-agent with `subagent`; treat its answer as research input.';
6
+ const createSubAgentTool = (deps) => {
7
+ const roster = deps.registry.list()
8
+ .filter((agent) => agent.name !== 'title')
9
+ .map((agent) => `- ${agent.name}: ${agent.description}`)
10
+ .join('\n');
11
+ return {
12
+ name: 'subagent',
13
+ description: 'Delegate an isolated task to a named sub-agent. Returns its final answer.',
14
+ prompt: roster ? `${BASE_PROMPT}\n\nAvailable sub-agents:\n${roster}` : BASE_PROMPT,
15
+ parameters: {
16
+ type: 'object',
17
+ properties: {
18
+ agent: { type: 'string', description: 'Sub-agent name.' },
19
+ task: { type: 'string', description: 'The task to delegate.' },
20
+ },
21
+ required: ['agent', 'task'],
22
+ additionalProperties: false,
14
23
  },
15
- required: ['agent', 'task'],
16
- additionalProperties: false,
17
- },
18
- run: async (input, ctx) => {
19
- const { agent, task } = (input ?? {});
20
- if (!agent || !task)
21
- return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
22
- const def = deps.registry.get(agent);
23
- if (!def)
24
- return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
25
- const result = await (0, runner_js_1.runSubAgent)(def, task, {
26
- spawn: deps.spawn,
27
- runs: deps.runs,
28
- parentId: deps.parentId,
29
- signal: ctx.signal,
30
- });
31
- return [{ type: 'text', text: result.text }];
32
- },
33
- });
24
+ run: async (input, ctx) => {
25
+ const { agent, task } = (input ?? {});
26
+ if (!agent || !task)
27
+ return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
28
+ const def = deps.registry.get(agent);
29
+ if (!def)
30
+ return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
31
+ const result = await (0, runner_js_1.runSubAgent)(def, task, {
32
+ spawn: deps.spawn,
33
+ runs: deps.runs,
34
+ parentId: deps.parentId,
35
+ signal: ctx.signal,
36
+ });
37
+ return [{ type: 'text', text: result.text }];
38
+ },
39
+ };
40
+ };
34
41
  exports.createSubAgentTool = createSubAgentTool;
@@ -292,12 +292,19 @@ function sgrDelta(prev, next) {
292
292
  return `\x1b[${params.join(';')}m`;
293
293
  }
294
294
  if (prev.bold !== next.bold || prev.dim !== next.dim) {
295
- if (!next.bold && !next.dim && (prev.bold || prev.dim))
295
+ if ((prev.bold && !next.bold) || (prev.dim && !next.dim)) {
296
296
  params.push('22');
297
- if (next.bold && !prev.bold)
298
- params.push('1');
299
- if (next.dim && !prev.dim)
300
- params.push('2');
297
+ if (next.bold)
298
+ params.push('1');
299
+ if (next.dim)
300
+ params.push('2');
301
+ }
302
+ else {
303
+ if (next.bold && !prev.bold)
304
+ params.push('1');
305
+ if (next.dim && !prev.dim)
306
+ params.push('2');
307
+ }
301
308
  }
302
309
  if (prev.italic !== next.italic)
303
310
  params.push(next.italic ? '3' : '23');