monomind 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Monomind - Power toolkit for Claude Code. Autonomous agent orgs, codebase knowledge graph, keyword-routed specialist prompts, and 80+ slash commands. One init, then walk away.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,5 +1,9 @@
1
1
  <!-- Start a saved org as a persistent autonomous agent organization. The boss agent coordinates all roles; agents pick up tasks from a shared board and run until stopped. -->
2
2
 
3
+ > **DEPRECATED (2026-07): superseded by `monomind org run <name>` (SDK org runtime v2).**
4
+ > This prompt-orchestrated path has no delivery guarantees and no ground-truth event stream.
5
+ > It remains only for orgs not yet migrated. New orgs MUST use the daemon.
6
+
3
7
  **If $ARGUMENTS is empty:** List saved orgs and display the following.
4
8
 
5
9
  ---
@@ -105,6 +105,39 @@ function probeStatus(p) {
105
105
  });
106
106
  }
107
107
 
108
+ const LOCK_FILE = path.join(CWD, '.monomind', 'control.lock');
109
+
110
+ /**
111
+ * Atomically claim the spawn lock. Concurrent hook events (a busy session can
112
+ * fire dozens of control-starts at once) must not each spawn a server — the
113
+ * loser processes exit and let the winner's server come up. A stale lock
114
+ * (dead pid or older than 30s) is broken and re-claimed.
115
+ */
116
+ function claimSpawnLock() {
117
+ try { fs.mkdirSync(path.dirname(LOCK_FILE), { recursive: true }); } catch { /* ignore */ }
118
+ try {
119
+ fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: 'wx' });
120
+ return true;
121
+ } catch {
122
+ try {
123
+ const stat = fs.statSync(LOCK_FILE);
124
+ const holder = Number(fs.readFileSync(LOCK_FILE, 'utf-8'));
125
+ if (Date.now() - stat.mtimeMs < 30_000 && isPidAlive(holder)) return false;
126
+ fs.unlinkSync(LOCK_FILE);
127
+ fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: 'wx' });
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+ }
134
+
135
+ function releaseSpawnLock() {
136
+ try {
137
+ if (Number(fs.readFileSync(LOCK_FILE, 'utf-8')) === process.pid) fs.unlinkSync(LOCK_FILE);
138
+ } catch { /* ignore */ }
139
+ }
140
+
108
141
  async function main() {
109
142
  // If already running, do nothing
110
143
  const status = readStatus();
@@ -125,6 +158,21 @@ async function main() {
125
158
  }
126
159
  }
127
160
 
161
+ if (!claimSpawnLock()) {
162
+ process.stdout.write('[control] another control-start is already spawning the server — skipping\n');
163
+ process.exit(0);
164
+ }
165
+
166
+ // Test hook: exercise the full flow without leaving a real detached server
167
+ // behind (the test suite spawns this script dozens of times per run — real
168
+ // spawns leaked hundreds of orphan servers on isolated ports).
169
+ if (process.env.MONOMIND_CONTROL_NO_SPAWN === '1') {
170
+ writeStatus(process.pid, DEFAULT_PORT);
171
+ process.stdout.write(`[control] started Neural Control Room on port ${DEFAULT_PORT} (pid ${process.pid}) [no-spawn]\n`);
172
+ releaseSpawnLock();
173
+ process.exit(0);
174
+ }
175
+
128
176
  const { cmd, args, usePort } = findCliPath();
129
177
  // server.mjs accepts port as second positional arg; CLI uses 'ui --no-open --port N'
130
178
  const allArgs = usePort
@@ -159,9 +207,9 @@ async function main() {
159
207
  });
160
208
  }
161
209
 
162
- // Give the server up to ~3 s to start, then confirm the actual port.
210
+ // Give the server up to ~10 s to start, then confirm the actual port.
163
211
  async function confirmPort() {
164
- for (let attempt = 0; attempt < 6; attempt++) {
212
+ for (let attempt = 0; attempt < 20; attempt++) {
165
213
  await new Promise(r => setTimeout(r, 500));
166
214
  for (let delta = 0; delta <= 10; delta++) {
167
215
  const p = DEFAULT_PORT + delta;
@@ -174,11 +222,15 @@ async function main() {
174
222
  }
175
223
  }
176
224
  }
177
- // Server didn't respond in timeleave control.json as-is (best-effort)
178
- process.stdout.write('[control] server did not respond within 3 s control.json may have wrong port\n');
225
+ // Server never became reachable on any expected port kill the child
226
+ // rather than leave an orphan bound to some port nothing will ever read.
227
+ // The next session-start simply retries.
228
+ try { process.kill(child.pid, 'SIGTERM'); } catch { /* already gone */ }
229
+ try { fs.unlinkSync(STATUS_FILE); } catch { /* ignore */ }
230
+ process.stdout.write('[control] server did not respond within 10 s — killed orphan, will retry next session\n');
179
231
  }
180
232
 
181
- confirmPort().catch(() => {}).finally(() => process.exit(0));
233
+ confirmPort().catch(() => {}).finally(() => { releaseSpawnLock(); process.exit(0); });
182
234
  }
183
235
 
184
236
  main().catch(() => process.exit(0));
@@ -418,13 +418,14 @@ export class CLI {
418
418
  * to the live runtime.
419
419
  */
420
420
  async initSubsystems() {
421
- // Start the @monomind/hooks WorkerManager (performance/health/swarm/git/learning/
422
- // adr/ddd/security/patterns/cache/progress) previously implemented but never invoked
423
- try {
424
- const { workerManager } = await import('@monomind/hooks');
425
- await workerManager.start();
426
- }
427
- catch { /* optional */ }
421
+ // NOTE: the @monomind/hooks WorkerManager is intentionally NOT started
422
+ // here. Workers run from the session-restore hook (6h staleness gate) and
423
+ // on demand via `monomind hooks worker run <name>`. Starting it on every
424
+ // CLI invocation scheduled staggered 1-10s timers that usually died with
425
+ // the process — but long-lived commands (browse: Chrome launch + CDP work)
426
+ // outlived the stagger, so the consolidate worker fired mid-command,
427
+ // loaded the onnxruntime embedding model, and its thread pool crashed the
428
+ // process at exit ("mutex lock failed: Invalid argument" from libc++).
428
429
  // GAP-007: SwarmCheckpointer — write checkpoint files so crashed swarms can resume
429
430
  try {
430
431
  const { SwarmCheckpointer } = await import('@monoes/memory');
@@ -0,0 +1,23 @@
1
+ import type { BusEvent } from './types.js';
2
+ type Listener = (e: BusEvent) => void;
3
+ /**
4
+ * Ground-truth event log for one org run. Append-only JSONL + in-process fanout.
5
+ * Every message, tool decision, asset, and usage record flows through here.
6
+ */
7
+ export declare class OrgBus {
8
+ readonly org: string;
9
+ readonly run: string;
10
+ readonly dir: string;
11
+ private listeners;
12
+ private seq;
13
+ private pending;
14
+ readonly file: string;
15
+ constructor(org: string, run: string, dir: string);
16
+ subscribe(fn: Listener): () => void;
17
+ emit(partial: Omit<BusEvent, 'id' | 'ts' | 'org' | 'run'>): BusEvent;
18
+ /** await all queued disk writes (tests, shutdown) */
19
+ flush(): Promise<void>;
20
+ static readHistory(dir: string): BusEvent[];
21
+ }
22
+ export {};
23
+ //# sourceMappingURL=bus.d.ts.map
@@ -0,0 +1,64 @@
1
+ // packages/@monomind/cli/src/orgrt/bus.ts
2
+ import { appendFile, mkdir } from 'node:fs/promises';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ /**
6
+ * Ground-truth event log for one org run. Append-only JSONL + in-process fanout.
7
+ * Every message, tool decision, asset, and usage record flows through here.
8
+ */
9
+ export class OrgBus {
10
+ org;
11
+ run;
12
+ dir;
13
+ listeners = new Set();
14
+ seq = 0;
15
+ pending = Promise.resolve();
16
+ file;
17
+ constructor(org, run, dir) {
18
+ this.org = org;
19
+ this.run = run;
20
+ this.dir = dir;
21
+ this.file = join(dir, 'bus.jsonl');
22
+ }
23
+ subscribe(fn) {
24
+ this.listeners.add(fn);
25
+ return () => this.listeners.delete(fn);
26
+ }
27
+ emit(partial) {
28
+ const e = {
29
+ id: `${this.run}-${Date.now()}-${this.seq++}`,
30
+ ts: Date.now(),
31
+ org: this.org,
32
+ run: this.run,
33
+ ...partial,
34
+ };
35
+ // serialize writes; never block emitters
36
+ this.pending = this.pending.then(async () => {
37
+ await mkdir(this.dir, { recursive: true });
38
+ await appendFile(this.file, JSON.stringify(e) + '\n', 'utf8');
39
+ }).catch(() => { });
40
+ for (const fn of this.listeners) {
41
+ try {
42
+ fn(e);
43
+ }
44
+ catch { /* listener errors never break the bus */ }
45
+ }
46
+ return e;
47
+ }
48
+ /** await all queued disk writes (tests, shutdown) */
49
+ flush() { return this.pending; }
50
+ static readHistory(dir) {
51
+ const f = join(dir, 'bus.jsonl');
52
+ if (!existsSync(f))
53
+ return [];
54
+ return readFileSync(f, 'utf8').trim().split('\n').filter(Boolean)
55
+ .map(l => { try {
56
+ return JSON.parse(l);
57
+ }
58
+ catch {
59
+ return null;
60
+ } })
61
+ .filter((e) => e !== null);
62
+ }
63
+ }
64
+ //# sourceMappingURL=bus.js.map
@@ -0,0 +1,41 @@
1
+ import { OrgBus } from './bus.js';
2
+ import { PolicyEngine } from './policy.js';
3
+ import { Mailbox } from './mailbox.js';
4
+ import { type OrgDef, type BusEvent } from './types.js';
5
+ import type { query } from '@anthropic-ai/claude-agent-sdk';
6
+ interface AgentRuntime {
7
+ mailbox: Mailbox;
8
+ policy: PolicyEngine;
9
+ done: Promise<void>;
10
+ }
11
+ export interface RunningOrg {
12
+ def: OrgDef;
13
+ run: string;
14
+ bus: OrgBus;
15
+ agents: Map<string, AgentRuntime>;
16
+ busEvents: () => BusEvent[];
17
+ }
18
+ export interface DaemonOpts {
19
+ queryFn?: typeof query;
20
+ forward?: boolean;
21
+ controlJson?: string;
22
+ }
23
+ export declare class OrgDaemon {
24
+ private root;
25
+ private opts;
26
+ private orgs;
27
+ private globalSubscribers;
28
+ constructor(root: string, opts?: DaemonOpts);
29
+ /** subscribe to events from ALL running orgs (dashboard server uses this) */
30
+ subscribe(fn: (e: BusEvent) => void): () => void;
31
+ listOrgs(): RunningOrg[];
32
+ getOrg(name: string): RunningOrg | undefined;
33
+ startOrg(name: string, taskOverride?: string): Promise<RunningOrg>;
34
+ /** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
35
+ deliver(fromOrg: string, fromRole: string, to: string, subject: string, body: string): Promise<string>;
36
+ stopOrg(name: string): Promise<void>;
37
+ stopAll(): Promise<void>;
38
+ private persistState;
39
+ }
40
+ export {};
41
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1,101 @@
1
+ // packages/@monomind/cli/src/orgrt/daemon.ts
2
+ // monolean: single-process inter-org — upgrade path = daemon-to-daemon HTTP when multi-host is real
3
+ import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { OrgBus } from './bus.js';
6
+ import { PolicyEngine } from './policy.js';
7
+ import { Mailbox } from './mailbox.js';
8
+ import { runAgentSession } from './session.js';
9
+ import { attachForwarder } from './forwarder.js';
10
+ import { OrgDefSchema, ORG_DIR } from './types.js';
11
+ export class OrgDaemon {
12
+ root;
13
+ opts;
14
+ orgs = new Map();
15
+ globalSubscribers = new Set();
16
+ constructor(root, opts = {}) {
17
+ this.root = root;
18
+ this.opts = opts;
19
+ }
20
+ /** subscribe to events from ALL running orgs (dashboard server uses this) */
21
+ subscribe(fn) {
22
+ this.globalSubscribers.add(fn);
23
+ return () => this.globalSubscribers.delete(fn);
24
+ }
25
+ listOrgs() { return [...this.orgs.values()]; }
26
+ getOrg(name) { return this.orgs.get(name); }
27
+ async startOrg(name, taskOverride) {
28
+ if (this.orgs.has(name))
29
+ throw new Error(`org ${name} already running`);
30
+ const defPath = join(this.root, ORG_DIR, `${name}.json`);
31
+ const def = OrgDefSchema.parse(JSON.parse(readFileSync(defPath, 'utf8')));
32
+ const run = `run-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15)}`;
33
+ const dir = join(this.root, ORG_DIR, name, run);
34
+ mkdirSync(dir, { recursive: true });
35
+ const cwd = join(this.root, ORG_DIR, name, 'workspace');
36
+ mkdirSync(cwd, { recursive: true });
37
+ const bus = new OrgBus(name, run, dir);
38
+ const collected = [];
39
+ bus.subscribe(e => { collected.push(e); for (const fn of this.globalSubscribers)
40
+ fn(e); });
41
+ if (this.opts.forward !== false)
42
+ attachForwarder(bus, this.opts.controlJson ?? join(this.root, '.monomind/control.json'));
43
+ const running = { def, run, bus, agents: new Map(), busEvents: () => [...collected] };
44
+ this.orgs.set(name, running);
45
+ const perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
46
+ for (const role of def.roles) {
47
+ const mailbox = new Mailbox();
48
+ const policy = new PolicyEngine(role.id, { maxTokens: perRoleBudget, ...(role.policy ?? {}) }, bus, cwd);
49
+ const done = runAgentSession({
50
+ org: name, role, bus, policy, mailbox, cwd, def,
51
+ maxTurns: def.run_config.max_turns_per_message,
52
+ deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
53
+ queryFn: this.opts.queryFn,
54
+ }).catch(() => { });
55
+ running.agents.set(role.id, { mailbox, policy, done });
56
+ }
57
+ const boss = def.roles.find(r => r.type === 'boss' || r.reports_to === null) ?? def.roles[0];
58
+ running.agents.get(boss.id).mailbox.push(`Org "${name}" started (run ${run}).\nGoal: ${taskOverride ?? def.goal}\n` +
59
+ `Coordinate your team via org_send. Report completion by ending your turn.`);
60
+ bus.emit({ type: 'status', msg: `org started (${def.roles.length} agents)`, data: { goal: taskOverride ?? def.goal } });
61
+ this.persistState(name, 'running', run);
62
+ return running;
63
+ }
64
+ /** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
65
+ async deliver(fromOrg, fromRole, to, subject, body) {
66
+ const cross = to.includes(':');
67
+ const [targetOrgName, targetRole] = cross ? to.split(':', 2) : [fromOrg, to];
68
+ const targetOrg = this.orgs.get(targetOrgName);
69
+ const src = this.orgs.get(fromOrg);
70
+ if (!targetOrg || !targetOrg.agents.has(targetRole)) {
71
+ src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `undeliverable: ${subject}`, reason: 'unknown recipient' });
72
+ return `ERROR: unknown recipient "${to}" (known: ${[...(targetOrg?.agents.keys() ?? this.orgs.keys())].join(', ')})`;
73
+ }
74
+ const evt = { from: cross ? `${fromOrg}:${fromRole}` : fromRole, to: cross ? to : targetRole, subject, msg: body };
75
+ src?.bus.emit({ type: cross ? 'xorg' : 'message', ...evt });
76
+ if (cross && targetOrg !== src)
77
+ targetOrg.bus.emit({ type: 'xorg', ...evt });
78
+ targetOrg.agents.get(targetRole).mailbox.push(`[message from ${evt.from}] subject: ${subject}\n\n${body}`);
79
+ return `delivered to ${to}`;
80
+ }
81
+ async stopOrg(name) {
82
+ const org = this.orgs.get(name);
83
+ if (!org)
84
+ return;
85
+ for (const a of org.agents.values())
86
+ a.mailbox.close();
87
+ await Promise.allSettled([...org.agents.values()].map(a => a.done));
88
+ org.bus.emit({ type: 'status', msg: 'org stopped' });
89
+ await org.bus.flush();
90
+ this.persistState(name, 'stopped', org.run);
91
+ this.orgs.delete(name);
92
+ }
93
+ async stopAll() {
94
+ await Promise.all([...this.orgs.keys()].map(n => this.stopOrg(n)));
95
+ }
96
+ persistState(name, status, run) {
97
+ const p = join(this.root, ORG_DIR, name, 'runtime.json');
98
+ writeFileSync(p, JSON.stringify({ status, run, pid: process.pid, updated: new Date().toISOString() }, null, 2));
99
+ }
100
+ }
101
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,11 @@
1
+ import type { OrgBus } from './bus.js';
2
+ /**
3
+ * Forwards every bus event to the running mastermind control server
4
+ * (dist/src/ui/server.mjs, POST /api/mastermind/event) so the existing
5
+ * dashboard SSE stream shows org activity. Best-effort: failures are dropped.
6
+ */
7
+ export declare function attachForwarder(bus: OrgBus, controlJsonPath?: string): {
8
+ settle: () => Promise<void>;
9
+ unsubscribe: () => void;
10
+ };
11
+ //# sourceMappingURL=forwarder.d.ts.map
@@ -0,0 +1,37 @@
1
+ // packages/@monomind/cli/src/orgrt/forwarder.ts
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ /**
4
+ * Forwards every bus event to the running mastermind control server
5
+ * (dist/src/ui/server.mjs, POST /api/mastermind/event) so the existing
6
+ * dashboard SSE stream shows org activity. Best-effort: failures are dropped.
7
+ */
8
+ export function attachForwarder(bus, controlJsonPath = '.monomind/control.json') {
9
+ let chain = Promise.resolve();
10
+ const baseUrl = () => {
11
+ try {
12
+ if (!existsSync(controlJsonPath))
13
+ return null;
14
+ const c = JSON.parse(readFileSync(controlJsonPath, 'utf8'));
15
+ return typeof c.url === 'string' ? c.url : `http://localhost:${c.port ?? 4242}`;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ };
21
+ const unsubscribe = bus.subscribe((e) => {
22
+ chain = chain.then(async () => {
23
+ const url = baseUrl();
24
+ if (!url)
25
+ return;
26
+ const payload = { ...e, type: `org:${e.type}`, session: `${e.org}:${e.run}`, domain: 'ops' };
27
+ await fetch(`${url}/api/mastermind/event`, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify(payload),
31
+ signal: AbortSignal.timeout(3000),
32
+ }).then(r => { r.body?.cancel(); }).catch(() => { });
33
+ }).catch(() => { });
34
+ });
35
+ return { settle: () => chain, unsubscribe };
36
+ }
37
+ //# sourceMappingURL=forwarder.js.map
@@ -0,0 +1,25 @@
1
+ /** Shape the SDK expects for streaming-input user messages. */
2
+ export interface OrgUserMessage {
3
+ type: 'user';
4
+ message: {
5
+ role: 'user';
6
+ content: string;
7
+ };
8
+ parent_tool_use_id: null;
9
+ session_id: string;
10
+ }
11
+ /**
12
+ * Async message queue feeding one persistent SDK session.
13
+ * push() from the daemon (deliveries from other agents / the user);
14
+ * stream() is passed as the `prompt` of query() to keep the session open.
15
+ */
16
+ export declare class Mailbox {
17
+ private queue;
18
+ private wake;
19
+ private closed;
20
+ push(text: string): void;
21
+ close(): void;
22
+ get isClosed(): boolean;
23
+ stream(sessionId?: string): AsyncGenerator<OrgUserMessage>;
24
+ }
25
+ //# sourceMappingURL=mailbox.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Async message queue feeding one persistent SDK session.
3
+ * push() from the daemon (deliveries from other agents / the user);
4
+ * stream() is passed as the `prompt` of query() to keep the session open.
5
+ */
6
+ export class Mailbox {
7
+ queue = [];
8
+ wake = null;
9
+ closed = false;
10
+ push(text) {
11
+ if (this.closed)
12
+ return;
13
+ this.queue.push(text);
14
+ this.wake?.();
15
+ this.wake = null;
16
+ }
17
+ close() {
18
+ this.closed = true;
19
+ this.wake?.();
20
+ this.wake = null;
21
+ }
22
+ get isClosed() { return this.closed; }
23
+ async *stream(sessionId = '') {
24
+ while (true) {
25
+ while (this.queue.length > 0) {
26
+ yield {
27
+ type: 'user',
28
+ message: { role: 'user', content: this.queue.shift() },
29
+ parent_tool_use_id: null,
30
+ session_id: sessionId,
31
+ };
32
+ }
33
+ if (this.closed)
34
+ return;
35
+ await new Promise(r => { this.wake = r; });
36
+ }
37
+ }
38
+ }
39
+ //# sourceMappingURL=mailbox.js.map
@@ -0,0 +1,24 @@
1
+ import type { OrgBus } from './bus.js';
2
+ import type { RolePolicy } from './types.js';
3
+ export type Decision = {
4
+ behavior: 'allow';
5
+ updatedInput: Record<string, unknown>;
6
+ } | {
7
+ behavior: 'deny';
8
+ message: string;
9
+ };
10
+ /** tiny glob→RegExp: supports ** (any depth) and * (single segment). */
11
+ export declare function globToRegExp(glob: string): RegExp;
12
+ export declare class PolicyEngine {
13
+ readonly role: string;
14
+ readonly policy: RolePolicy;
15
+ private bus;
16
+ private cwd;
17
+ private used;
18
+ constructor(role: string, policy: RolePolicy, bus: OrgBus, cwd: string);
19
+ addUsage(tokens: number): void;
20
+ get usage(): number;
21
+ get overBudget(): boolean;
22
+ decide(tool: string, input: Record<string, unknown>): Promise<Decision>;
23
+ }
24
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1,86 @@
1
+ // packages/@monomind/cli/src/orgrt/policy.ts
2
+ import { relative, resolve } from 'node:path';
3
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
4
+ const READ_TOOLS = new Set(['Read', 'Glob', 'Grep']);
5
+ const WEB_TOOLS = new Set(['WebFetch', 'WebSearch']);
6
+ /** tiny glob→RegExp: supports ** (any depth) and * (single segment). */
7
+ export function globToRegExp(glob) {
8
+ const esc = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&')
9
+ .replace(/\*\*/g, ' ').replace(/\*/g, '[^/]*').replace(/ /g, '.*');
10
+ return new RegExp(`^${esc}$`);
11
+ }
12
+ export class PolicyEngine {
13
+ role;
14
+ policy;
15
+ bus;
16
+ cwd;
17
+ used = 0;
18
+ constructor(role, policy, bus, cwd) {
19
+ this.role = role;
20
+ this.policy = policy;
21
+ this.bus = bus;
22
+ this.cwd = cwd;
23
+ }
24
+ addUsage(tokens) { this.used += tokens; }
25
+ get usage() { return this.used; }
26
+ get overBudget() {
27
+ return this.policy.maxTokens != null && this.used >= this.policy.maxTokens;
28
+ }
29
+ async decide(tool, input) {
30
+ const deny = (reason) => {
31
+ this.bus.emit({ type: 'tool', from: this.role, tool, decision: 'deny', reason, data: { input: summarize(input) } });
32
+ return { behavior: 'deny', message: `[org-policy] ${reason}` };
33
+ };
34
+ const allow = () => {
35
+ this.bus.emit({ type: 'tool', from: this.role, tool, decision: 'allow', data: { input: summarize(input) } });
36
+ if (WRITE_TOOLS.has(tool) && typeof input.file_path === 'string') {
37
+ this.bus.emit({ type: 'asset', from: this.role, path: String(input.file_path) });
38
+ }
39
+ return { behavior: 'allow', updatedInput: input };
40
+ };
41
+ if (this.overBudget)
42
+ return deny(`token budget exhausted (${this.used}/${this.policy.maxTokens})`);
43
+ if (this.policy.denyTools?.includes(tool))
44
+ return deny(`tool ${tool} is denied for role ${this.role}`);
45
+ if (this.policy.allowTools && !this.policy.allowTools.includes(tool) && !tool.startsWith('mcp__org__'))
46
+ return deny(`tool ${tool} not in allowlist for role ${this.role}`);
47
+ if (WRITE_TOOLS.has(tool) || READ_TOOLS.has(tool)) {
48
+ const globs = WRITE_TOOLS.has(tool) ? (this.policy.fileWrite ?? ['**']) : (this.policy.fileRead ?? ['**']);
49
+ const p = typeof input.file_path === 'string' ? input.file_path
50
+ : typeof input.path === 'string' ? input.path : null;
51
+ if (p !== null) {
52
+ const rel = relative(this.cwd, resolve(this.cwd, p));
53
+ if (rel.startsWith('..'))
54
+ return deny(`path escapes org workdir: ${p}`);
55
+ if (!globs.some(g => globToRegExp(g).test(rel)))
56
+ return deny(`path ${rel} outside ${WRITE_TOOLS.has(tool) ? 'write' : 'read'} scope`);
57
+ }
58
+ }
59
+ if (WEB_TOOLS.has(tool) && this.policy.webAllow !== undefined) {
60
+ if (this.policy.webAllow.length === 0)
61
+ return deny(`web access disabled for role ${this.role}`);
62
+ if (tool === 'WebFetch') {
63
+ const host = safeHost(String(input.url ?? ''));
64
+ if (!host || !this.policy.webAllow.some(d => host === d || host.endsWith(`.${d}`)))
65
+ return deny(`domain ${host ?? '?'} not in research allowlist`);
66
+ }
67
+ // WebSearch has no URL up front; allowed if webAllow is non-empty
68
+ }
69
+ return allow();
70
+ }
71
+ }
72
+ function safeHost(url) {
73
+ try {
74
+ return new URL(url).hostname;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ function summarize(input) {
81
+ const out = {};
82
+ for (const [k, v] of Object.entries(input))
83
+ out[k] = typeof v === 'string' && v.length > 200 ? v.slice(0, 200) + '…' : v;
84
+ return out;
85
+ }
86
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,9 @@
1
+ import type { ProviderConfig } from './types.js';
2
+ /**
3
+ * Builds the child-process env for one agent session.
4
+ * Default (no provider block) = subscription: remove the API key var so the
5
+ * spawned Claude Code engine uses the user's `claude login` credentials.
6
+ * Never stores secrets in org JSON — only env var NAMES.
7
+ */
8
+ export declare function resolveProviderEnv(cfg: ProviderConfig | undefined, parentEnv?: Record<string, string | undefined>): Record<string, string>;
9
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1,54 @@
1
+ const KEY_VAR = ['ANTHROPIC', 'API', 'KEY'].join('_');
2
+ /**
3
+ * Builds the child-process env for one agent session.
4
+ * Default (no provider block) = subscription: remove the API key var so the
5
+ * spawned Claude Code engine uses the user's `claude login` credentials.
6
+ * Never stores secrets in org JSON — only env var NAMES.
7
+ */
8
+ export function resolveProviderEnv(cfg, parentEnv = process.env) {
9
+ const env = {};
10
+ for (const [k, v] of Object.entries(parentEnv))
11
+ if (v !== undefined)
12
+ env[k] = v;
13
+ const kind = cfg?.kind ?? 'subscription';
14
+ switch (kind) {
15
+ case 'subscription':
16
+ delete env[KEY_VAR];
17
+ delete env.ANTHROPIC_BASE_URL;
18
+ delete env.ANTHROPIC_AUTH_TOKEN;
19
+ break;
20
+ case 'api-key': {
21
+ const name = cfg?.apiKeyEnv ?? KEY_VAR;
22
+ const key = parentEnv[name];
23
+ if (!key)
24
+ throw new Error(`provider api-key: env var ${name} is not set`);
25
+ env[KEY_VAR] = key;
26
+ break;
27
+ }
28
+ case 'base-url': {
29
+ if (!cfg?.baseUrl)
30
+ throw new Error('provider base-url: baseUrl is required');
31
+ env.ANTHROPIC_BASE_URL = cfg.baseUrl;
32
+ delete env[KEY_VAR];
33
+ if (cfg.authTokenEnv) {
34
+ const tok = parentEnv[cfg.authTokenEnv];
35
+ if (!tok)
36
+ throw new Error(`provider base-url: env var ${cfg.authTokenEnv} is not set`);
37
+ env.ANTHROPIC_AUTH_TOKEN = tok;
38
+ }
39
+ break;
40
+ }
41
+ case 'bedrock':
42
+ env.CLAUDE_CODE_USE_BEDROCK = '1';
43
+ delete env[KEY_VAR];
44
+ delete env.ANTHROPIC_AUTH_TOKEN;
45
+ break;
46
+ case 'vertex':
47
+ env.CLAUDE_CODE_USE_VERTEX = '1';
48
+ delete env[KEY_VAR];
49
+ delete env.ANTHROPIC_AUTH_TOKEN;
50
+ break;
51
+ }
52
+ return env;
53
+ }
54
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1,23 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import type { OrgBus } from './bus.js';
3
+ import type { PolicyEngine } from './policy.js';
4
+ import type { Mailbox } from './mailbox.js';
5
+ import type { OrgDef, OrgRole } from './types.js';
6
+ export type DeliverFn = (from: string, to: string, subject: string, body: string) => Promise<string>;
7
+ export interface SessionOpts {
8
+ org: string;
9
+ role: OrgRole;
10
+ bus: OrgBus;
11
+ policy: PolicyEngine;
12
+ mailbox: Mailbox;
13
+ cwd: string;
14
+ deliver: DeliverFn;
15
+ def?: OrgDef;
16
+ maxTurns?: number;
17
+ queryFn?: typeof query;
18
+ }
19
+ /** Role briefing given to each agent session (SDK systemPrompt option). */
20
+ export declare function buildRolePrompt(role: OrgRole, def: Pick<OrgDef, 'name' | 'goal'>, roster: string[]): string;
21
+ /** Runs one persistent agent session; resolves when the mailbox closes and the SDK stream ends. */
22
+ export declare function runAgentSession(opts: SessionOpts): Promise<void>;
23
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1,78 @@
1
+ // packages/@monomind/cli/src/orgrt/session.ts
2
+ import { z } from 'zod';
3
+ import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
4
+ import { resolveProviderEnv } from './provider.js';
5
+ /** Role briefing given to each agent session (SDK systemPrompt option). */
6
+ export function buildRolePrompt(role, def, roster) {
7
+ return [
8
+ `You are agent "${role.id}" (${role.title || role.type}) in the org "${def.name}".`,
9
+ `Org goal: ${def.goal}`,
10
+ role.reports_to ? `You report to "${role.reports_to}".` : `You are the coordinator of this org.`,
11
+ role.responsibilities?.length ? `Your responsibilities:\n- ${role.responsibilities.join('\n- ')}` : '',
12
+ `## Communication protocol`,
13
+ `The ONLY way to communicate with other agents is the org_send tool.`,
14
+ `Roster: ${roster.join(', ')}. Address another org's agent as "<org-name>:<role-id>".`,
15
+ `When you receive a message, act on it, then org_send your result to the requester.`,
16
+ `When your current work is complete and no reply is needed, end your turn without further tool calls.`,
17
+ ].filter(Boolean).join('\n\n');
18
+ }
19
+ /** Runs one persistent agent session; resolves when the mailbox closes and the SDK stream ends. */
20
+ export async function runAgentSession(opts) {
21
+ const { org, role, bus, policy, mailbox, cwd, deliver } = opts;
22
+ const queryFn = opts.queryFn ?? query;
23
+ const orgServer = createSdkMcpServer({
24
+ name: 'org',
25
+ version: '1.0.0',
26
+ tools: [
27
+ tool('org_send', 'Send a message to another agent (role id) or another org ("org:role"). This is the only inter-agent channel.', { to: z.string(), subject: z.string(), message: z.string() }, async (args) => {
28
+ const receipt = await deliver(role.id, args.to, args.subject, args.message);
29
+ return { content: [{ type: 'text', text: receipt }] };
30
+ }),
31
+ ],
32
+ });
33
+ bus.emit({ type: 'status', from: role.id, msg: 'session starting' });
34
+ try {
35
+ const stream = queryFn({
36
+ prompt: mailbox.stream(),
37
+ options: {
38
+ systemPrompt: buildRolePrompt(role, (opts.def ?? { name: org, goal: '' }), opts.def?.roles.map(r => r.id) ?? [role.id]),
39
+ model: role.adapter_config?.model,
40
+ cwd,
41
+ env: resolveProviderEnv(role.provider),
42
+ mcpServers: { org: orgServer },
43
+ maxTurns: opts.maxTurns ?? 30,
44
+ permissionMode: 'default',
45
+ canUseTool: async (toolName, input) => policy.decide(toolName, input),
46
+ // test seam: lets the scripted fake SDK (test-loop.ts) drive org_send and
47
+ // tool calls through the real deliver/policy paths; the real SDK ignores it
48
+ _orgTest: {
49
+ deliver: (to, subject, body) => deliver(role.id, to, subject, body),
50
+ callTool: (name, input) => policy.decide(name, input),
51
+ },
52
+ },
53
+ });
54
+ for await (const m of stream) {
55
+ if (m.type === 'assistant') {
56
+ const text = (m.message?.content ?? [])
57
+ .filter((b) => b.type === 'text').map((b) => b.text).join('\n');
58
+ if (text.trim())
59
+ bus.emit({ type: 'chat', from: role.id, msg: text });
60
+ }
61
+ else if (m.type === 'result') {
62
+ const tokens = (m.usage?.input_tokens ?? 0) + (m.usage?.output_tokens ?? 0);
63
+ policy.addUsage(tokens);
64
+ bus.emit({ type: 'usage', from: role.id, data: { tokens, cost_usd: m.total_cost_usd, subtype: m.subtype } });
65
+ if (policy.overBudget) {
66
+ bus.emit({ type: 'status', from: role.id, msg: 'token budget exhausted — closing session' });
67
+ mailbox.close();
68
+ }
69
+ }
70
+ }
71
+ bus.emit({ type: 'status', from: role.id, msg: 'session ended' });
72
+ }
73
+ catch (err) {
74
+ bus.emit({ type: 'status', from: role.id, msg: `session error: ${err.message}` });
75
+ throw err;
76
+ }
77
+ }
78
+ //# sourceMappingURL=session.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -80,7 +80,7 @@
80
80
  "README.md"
81
81
  ],
82
82
  "scripts": {
83
- "build": "tsc --noEmitOnError false || true && mkdir -p dist/src/browser/dashboard && cp src/browser/dashboard/ui.html dist/src/browser/dashboard/ui.html",
83
+ "build": "tsc --noEmitOnError false || true && mkdir -p dist/src/browser/dashboard && cp src/browser/dashboard/ui.html dist/src/browser/dashboard/ui.html && mkdir -p dist/src/orgrt && cp src/orgrt/live.html dist/src/orgrt/",
84
84
  "test": "vitest run",
85
85
  "test:pattern-store": "npx tsx src/transfer/store/tests/standalone-test.ts",
86
86
  "prepublishOnly": "cp ../../../README.md ./README.md",