monomind 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +9 -5
  2. package/packages/@monomind/cli/.claude/commands/mastermind/runorg.md +4 -0
  3. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +50 -5
  4. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
  5. package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
  6. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  7. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +75 -18
  8. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
  9. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +16 -7
  10. package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  11. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +4 -4
  12. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +40 -22
  13. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  14. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
  15. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +54 -4
  16. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
  17. package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
  18. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
  19. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
  20. package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
  21. package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
  22. package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
  23. package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
  24. package/packages/@monomind/cli/bin/cli.js +10 -1
  25. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
  26. package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
  27. package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
  28. package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
  29. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
  30. package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
  31. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
  32. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
  33. package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
  34. package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
  35. package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
  36. package/packages/@monomind/cli/dist/src/commands/org.js +306 -129
  37. package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
  38. package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
  39. package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
  40. package/packages/@monomind/cli/dist/src/index.js +64 -28
  41. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
  42. package/packages/@monomind/cli/dist/src/init/executor.js +63 -49
  43. package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
  44. package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
  45. package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
  46. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  47. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
  48. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
  49. package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
  50. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +51 -34
  51. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
  52. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
  53. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +65 -43
  54. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
  55. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
  56. package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
  57. package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
  58. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
  59. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
  60. package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
  61. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
  62. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
  63. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +24 -0
  64. package/packages/@monomind/cli/dist/src/orgrt/broker.js +68 -0
  65. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +30 -0
  66. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +96 -7
  67. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
  68. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +105 -8
  69. package/packages/@monomind/cli/dist/src/orgrt/live.html +56 -0
  70. package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
  71. package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
  72. package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
  73. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +8 -0
  74. package/packages/@monomind/cli/dist/src/orgrt/server.js +72 -0
  75. package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
  76. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
  77. package/packages/@monomind/cli/dist/src/output.js +12 -5
  78. package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
  79. package/packages/@monomind/cli/dist/src/parser.js +130 -5
  80. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
  81. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
  82. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
  83. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
  84. package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
  85. package/packages/@monomind/cli/dist/src/ui/dashboard.html +40 -40
  86. package/packages/@monomind/cli/dist/src/ui/orgs.html +110 -11
  87. package/packages/@monomind/cli/dist/src/ui/server.mjs +304 -133
  88. package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
  89. package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
  90. package/packages/@monomind/cli/package.json +6 -2
@@ -0,0 +1,56 @@
1
+ <!-- packages/@monomind/cli/src/orgrt/live.html -->
2
+ <!doctype html>
3
+ <html><head><meta charset="utf-8"><title>org live</title>
4
+ <style>
5
+ :root { color-scheme: dark; }
6
+ body { margin:0; font:13px/1.5 ui-monospace,monospace; background:#0d1117; color:#e6edf3;
7
+ display:grid; grid-template-columns:220px 1fr 1fr; grid-template-rows:auto 1fr 1fr; gap:1px; height:100vh; }
8
+ header { grid-column:1/4; padding:8px 14px; background:#161b22; font-weight:700; }
9
+ section { background:#161b22; overflow:auto; padding:10px; }
10
+ h2 { font-size:11px; text-transform:uppercase; letter-spacing:.1em; color:#7d8590; margin:0 0 8px; }
11
+ #agents { grid-row:2/4; }
12
+ .agent { padding:6px 8px; border-left:3px solid #2f81f7; margin-bottom:6px; background:#0d1117; }
13
+ .agent small { color:#7d8590; display:block; }
14
+ .row { padding:4px 6px; border-bottom:1px solid #21262d; word-break:break-word; }
15
+ .from { color:#2f81f7; font-weight:700; } .to { color:#a371f7; }
16
+ .deny { color:#f85149; } .allow { color:#3fb950; } .xorg { color:#d29922; font-weight:700; }
17
+ .ts { color:#484f58; font-size:11px; margin-right:6px; }
18
+ </style></head>
19
+ <body>
20
+ <header>org live — <span id="conn">connecting…</span></header>
21
+ <section id="agents"><h2>Agents</h2><div id="agents-list"></div></section>
22
+ <section id="chat"><h2>Chats &amp; Messages</h2><div id="chat-list"></div></section>
23
+ <section id="tools"><h2>Tool Activity &amp; Assets</h2><div id="tools-list"></div></section>
24
+ <script>
25
+ const $ = id => document.getElementById(id);
26
+ const fmt = ts => new Date(ts).toLocaleTimeString();
27
+ const add = (listId, html, cap = 500) => {
28
+ const el = document.createElement('div'); el.className = 'row'; el.innerHTML = html;
29
+ const list = $(listId); list.prepend(el);
30
+ while (list.children.length > cap) list.lastChild.remove();
31
+ };
32
+ const esc = s => String(s ?? '').replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
33
+ const decisionClass = d => (d === 'allow' || d === 'deny') ? d : 'unknown';
34
+ function render(e) {
35
+ const t = `<span class="ts">${fmt(e.ts)}</span>`;
36
+ if (e.type === 'chat') add('chat-list', `${t}<span class="from">${esc(e.org)}/${esc(e.from)}</span>: ${esc(e.msg)}`);
37
+ else if (e.type === 'message') add('chat-list', `${t}<span class="from">${esc(e.from)}</span> → <span class="to">${esc(e.to)}</span> [${esc(e.subject||'')}] ${esc(e.msg)}`);
38
+ else if (e.type === 'xorg') add('chat-list', `${t}<span class="xorg">⇄ ${esc(e.from)} → ${esc(e.to)}</span> [${esc(e.subject||'')}] ${esc(e.msg)}`);
39
+ else if (e.type === 'tool') add('tools-list', `${t}<span class="from">${esc(e.from)}</span> ${esc(e.tool)} <span class="${decisionClass(e.decision)}">${esc(e.decision)}</span>${e.reason ? ' — ' + esc(e.reason) : ''}`);
40
+ else if (e.type === 'asset') add('tools-list', `${t}<span class="from">${esc(e.from)}</span> 📄 <b>${esc(e.path)}</b>`);
41
+ else if (e.type === 'status' || e.type === 'usage') add('tools-list', `${t}<i>${esc(e.org)}/${esc(e.from ?? 'org')}: ${esc(e.msg ?? JSON.stringify(e.data))}</i>`);
42
+ }
43
+ async function refreshAgents() {
44
+ const orgs = await fetch('/api/orgs').then(r => r.json()).catch(() => []);
45
+ $('agents-list').innerHTML = orgs.map(o =>
46
+ `<div class="agent"><b>${esc(o.name)}</b><small>${esc(o.run)}</small>` +
47
+ o.agents.map(a => `<div>${a.closed ? '⚫' : '🟢'} ${esc(a.id)} <small>${esc(a.usage)} tok</small></div>`).join('') +
48
+ `</div>`).join('') || '<i>no running orgs</i>';
49
+ }
50
+ const ws = new WebSocket(`ws://${location.host}/ws`);
51
+ ws.onopen = () => { $('conn').textContent = 'live'; refreshAgents(); };
52
+ ws.onclose = () => { $('conn').textContent = 'disconnected'; };
53
+ ws.onmessage = ev => { render(JSON.parse(ev.data)); };
54
+ setInterval(refreshAgents, 5000);
55
+ </script>
56
+ </body></html>
@@ -23,6 +23,7 @@ export function resolveProviderEnv(cfg, parentEnv = process.env) {
23
23
  if (!key)
24
24
  throw new Error(`provider api-key: env var ${name} is not set`);
25
25
  env[KEY_VAR] = key;
26
+ delete env.ANTHROPIC_AUTH_TOKEN; // leftover parent token would override the key in the engine
26
27
  break;
27
28
  }
28
29
  case 'base-url': {
@@ -0,0 +1,13 @@
1
+ /** "15m" | "2h" | "45s" | minutes as number | null → interval ms or null */
2
+ export declare function parseSchedule(s: string | number | null | undefined): number | null;
3
+ /** Fires runFn(name, intervalMs) every intervalMs per org. Real timer loop — no ScheduleWakeup, no prompts. */
4
+ export declare class OrgScheduler {
5
+ private runFn;
6
+ private timers;
7
+ private running;
8
+ constructor(runFn: (name: string, intervalMs: number) => Promise<void>);
9
+ add(name: string, intervalMs: number): void;
10
+ remove(name: string): void;
11
+ stop(): void;
12
+ }
13
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1,48 @@
1
+ // packages/@monomind/cli/src/orgrt/scheduler.ts
2
+ /** "15m" | "2h" | "45s" | minutes as number | null → interval ms or null */
3
+ export function parseSchedule(s) {
4
+ if (s == null)
5
+ return null;
6
+ if (typeof s === 'number')
7
+ return s * 60_000;
8
+ const m = /^(\d+)\s*(s|m|h)$/.exec(s.trim());
9
+ if (!m)
10
+ return null;
11
+ const n = Number(m[1]);
12
+ return m[2] === 's' ? n * 1000 : m[2] === 'm' ? n * 60_000 : n * 3_600_000;
13
+ }
14
+ /** Fires runFn(name, intervalMs) every intervalMs per org. Real timer loop — no ScheduleWakeup, no prompts. */
15
+ export class OrgScheduler {
16
+ runFn;
17
+ timers = new Map();
18
+ running = new Set();
19
+ constructor(runFn) {
20
+ this.runFn = runFn;
21
+ }
22
+ add(name, intervalMs) {
23
+ this.remove(name);
24
+ this.timers.set(name, setInterval(async () => {
25
+ if (this.running.has(name))
26
+ return; // skip if previous iteration still running
27
+ this.running.add(name);
28
+ try {
29
+ await this.runFn(name, intervalMs);
30
+ }
31
+ catch (err) {
32
+ console.error(`[org-scheduler] ${name}: scheduled run failed:`, err);
33
+ }
34
+ finally {
35
+ this.running.delete(name);
36
+ }
37
+ }, intervalMs));
38
+ }
39
+ remove(name) {
40
+ const t = this.timers.get(name);
41
+ if (t)
42
+ clearInterval(t);
43
+ this.timers.delete(name);
44
+ }
45
+ stop() { for (const name of [...this.timers.keys()])
46
+ this.remove(name); }
47
+ }
48
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1,8 @@
1
+ import type { OrgDaemon } from './daemon.js';
2
+ export interface OrgServer {
3
+ port: number;
4
+ close: () => void;
5
+ }
6
+ /** Daemon-owned live view: WS fanout of every bus event + tiny REST surface. */
7
+ export declare function startOrgServer(daemon: OrgDaemon, port: number): Promise<OrgServer>;
8
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,72 @@
1
+ // packages/@monomind/cli/src/orgrt/server.ts
2
+ import http from 'node:http';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { WebSocketServer, WebSocket } from 'ws';
7
+ /** Daemon-owned live view: WS fanout of every bus event + tiny REST surface. */
8
+ export async function startOrgServer(daemon, port) {
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const htmlPath = ['live.html', '../orgrt/live.html']
11
+ .map(p => join(here, p)).find(existsSync) ?? join(here, 'live.html');
12
+ const server = http.createServer((req, res) => {
13
+ const url = req.url ?? '/';
14
+ if (req.method === 'GET' && url === '/') {
15
+ res.writeHead(200, { 'Content-Type': 'text/html' });
16
+ res.end(readFileSync(htmlPath, 'utf8'));
17
+ }
18
+ else if (req.method === 'GET' && url === '/api/orgs') {
19
+ res.writeHead(200, { 'Content-Type': 'application/json' });
20
+ res.end(JSON.stringify(daemon.listOrgs().map(o => ({
21
+ name: o.def.name, run: o.run, goal: o.def.goal,
22
+ agents: [...o.agents.entries()].map(([id, a]) => ({
23
+ id, usage: a.policy.usage, closed: a.mailbox.isClosed,
24
+ status: a.status, error: a.error,
25
+ })),
26
+ }))));
27
+ }
28
+ else if (req.method === 'GET' && url.startsWith('/api/history/')) {
29
+ const name = decodeURIComponent(url.slice('/api/history/'.length));
30
+ const org = daemon.getOrg(name);
31
+ res.writeHead(200, { 'Content-Type': 'application/json' });
32
+ res.end(JSON.stringify(org ? org.busEvents() : []));
33
+ }
34
+ else if (req.method === 'POST' && url === '/api/xdeliver') {
35
+ // inbound cross-process delivery — see OrgDaemon.deliverRemote() on the sending side
36
+ let body = '';
37
+ req.on('data', c => { body += c; });
38
+ req.on('end', () => {
39
+ try {
40
+ const payload = JSON.parse(body || '{}');
41
+ if (!payload.toOrg || !payload.toRole || !payload.fromOrg || !payload.fromRole) {
42
+ res.writeHead(400, { 'Content-Type': 'application/json' });
43
+ res.end(JSON.stringify({ ok: false, error: 'toOrg, toRole, fromOrg, fromRole are required' }));
44
+ return;
45
+ }
46
+ const result = daemon.receiveRemote(payload.toOrg, payload.toRole, `${payload.fromOrg}:${payload.fromRole}`, payload.subject ?? '', payload.body ?? '');
47
+ res.writeHead(result.ok ? 200 : 404, { 'Content-Type': 'application/json' });
48
+ res.end(JSON.stringify(result));
49
+ }
50
+ catch (err) {
51
+ res.writeHead(400, { 'Content-Type': 'application/json' });
52
+ res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : 'bad request' }));
53
+ }
54
+ });
55
+ }
56
+ else {
57
+ res.writeHead(404);
58
+ res.end('not found');
59
+ }
60
+ });
61
+ const wss = new WebSocketServer({ server, path: '/ws' });
62
+ const unsubscribe = daemon.subscribe(e => {
63
+ const line = JSON.stringify(e);
64
+ for (const c of wss.clients)
65
+ if (c.readyState === WebSocket.OPEN)
66
+ c.send(line);
67
+ });
68
+ await new Promise(r => server.listen(port, r));
69
+ const actual = server.address().port;
70
+ return { port: actual, close: () => { unsubscribe(); wss.close(); server.close(); } };
71
+ }
72
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,12 @@
1
+ interface IterationResult {
2
+ checks: Record<string, boolean>;
3
+ events: number;
4
+ }
5
+ export interface LoopReport {
6
+ iterations: IterationResult[];
7
+ failed: number;
8
+ summary: string;
9
+ }
10
+ export declare function runTestLoop(root: string, times: number): Promise<LoopReport>;
11
+ export {};
12
+ //# sourceMappingURL=test-loop.d.ts.map
@@ -0,0 +1,108 @@
1
+ // packages/@monomind/cli/src/orgrt/test-loop.ts
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import WebSocket from 'ws';
5
+ import { OrgDaemon } from './daemon.js';
6
+ import { startOrgServer } from './server.js';
7
+ import { OrgBus } from './bus.js';
8
+ import { ORG_DIR } from './types.js';
9
+ /**
10
+ * Scripted fake SDK used by the verification loop (no API cost, deterministic).
11
+ * boss: on kickoff, delegates to coder, then pings the partner org's boss.
12
+ * coder: "writes" a report (Write allowed by policy), attempts Bash (denied), replies to boss.
13
+ * It drives the SAME production code paths via the _orgTest seam:
14
+ * callTool → policy.decide → bus; deliver → daemon.deliver → mailboxes + bus;
15
+ * assistant/result → chat/usage events.
16
+ */
17
+ const scriptedQuery = (roleId) => ({ prompt, options }) => (async function* () {
18
+ const seam = options._orgTest;
19
+ for await (const m of prompt) {
20
+ const text = String(m.message.content);
21
+ // Trigger ONLY on the daemon kickoff message (starts with `Org "`). A bare
22
+ // includes('started') also matches the xorg body "alpha started its run",
23
+ // making the partner boss re-deliver to itself forever — an unbroken
24
+ // microtask chain that starves the event loop (waitFor's timer never fires).
25
+ if (roleId === 'boss' && text.startsWith('Org "') && text.includes('started')) {
26
+ yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Kicking off: delegating to coder.' }] } };
27
+ await seam.deliver('coder', 'task', 'produce out/report.md');
28
+ await seam.deliver('partner:boss', 'fyi', 'alpha started its run');
29
+ }
30
+ else if (roleId === 'coder') {
31
+ await seam.callTool('Write', { file_path: join(options.cwd, 'out/report.md'), content: '# report' });
32
+ await seam.callTool('Bash', { command: 'echo should-be-denied' }); // policy MUST deny
33
+ yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Report written.' }] } };
34
+ await seam.deliver('boss', 're: task', 'done — out/report.md');
35
+ }
36
+ else {
37
+ yield { type: 'assistant', message: { content: [{ type: 'text', text: `ack: ${text.slice(0, 40)}` }] } };
38
+ }
39
+ yield { type: 'result', subtype: 'success', usage: { input_tokens: 5, output_tokens: 5 } };
40
+ }
41
+ })();
42
+ function writeFixtures(root) {
43
+ const dir = join(root, ORG_DIR);
44
+ mkdirSync(dir, { recursive: true });
45
+ writeFileSync(join(dir, 'alpha.json'), JSON.stringify({
46
+ name: 'alpha', goal: 'produce a report',
47
+ roles: [
48
+ { id: 'boss', title: 'Boss', type: 'boss', reports_to: null },
49
+ { id: 'coder', title: 'Coder', type: 'specialist', reports_to: 'boss',
50
+ policy: { denyTools: ['Bash'], fileWrite: ['out/**'] } },
51
+ ],
52
+ }));
53
+ writeFileSync(join(dir, 'partner.json'), JSON.stringify({
54
+ name: 'partner', goal: 'receive handoffs',
55
+ roles: [{ id: 'boss', title: 'Boss', type: 'boss', reports_to: null }],
56
+ }));
57
+ }
58
+ async function waitFor(pred, ms = 5000) {
59
+ const t0 = Date.now();
60
+ while (Date.now() - t0 < ms) {
61
+ if (pred())
62
+ return true;
63
+ await new Promise(r => setTimeout(r, 50));
64
+ }
65
+ return pred();
66
+ }
67
+ export async function runTestLoop(root, times) {
68
+ writeFixtures(root);
69
+ const iterations = [];
70
+ for (let i = 0; i < times; i++) {
71
+ const queryFn = (args) => {
72
+ const roleId = /You are agent "([^"]+)"/.exec(args.options.systemPrompt)?.[1] ?? 'unknown';
73
+ return scriptedQuery(roleId)(args);
74
+ };
75
+ const daemon = new OrgDaemon(root, { queryFn: queryFn, forward: false });
76
+ const srv = await startOrgServer(daemon, 0);
77
+ const wsEvents = [];
78
+ const ws = new WebSocket(`ws://127.0.0.1:${srv.port}/ws`);
79
+ ws.on('message', d => wsEvents.push(JSON.parse(d.toString())));
80
+ await new Promise(r => ws.on('open', r));
81
+ const alpha = await daemon.startOrg('alpha');
82
+ await daemon.startOrg('partner');
83
+ await waitFor(() => alpha.busEvents().some(e => e.type === 'message' && e.from === 'coder' && e.to === 'boss'));
84
+ await daemon.stopAll();
85
+ ws.close();
86
+ srv.close();
87
+ const evs = alpha.busEvents();
88
+ const has = (pred) => evs.some(pred);
89
+ const persistedCount = OrgBus.readHistory(join(root, ORG_DIR, 'alpha', alpha.run)).length;
90
+ const checks = {
91
+ chat: has(e => e.type === 'chat'),
92
+ message: has(e => e.type === 'message' && e.from === 'boss' && e.to === 'coder'),
93
+ tool: has(e => e.type === 'tool' && e.decision === 'allow' && e.tool === 'Write'),
94
+ policyDeny: has(e => e.type === 'tool' && e.decision === 'deny' && e.tool === 'Bash'),
95
+ asset: has(e => e.type === 'asset' && (e.path ?? '').endsWith('out/report.md')),
96
+ xorg: has(e => e.type === 'xorg' && e.to === 'partner:boss'),
97
+ usage: has(e => e.type === 'usage'),
98
+ wsDelivery: wsEvents.length > 0 && wsEvents.some(e => e.type === 'chat'),
99
+ persisted: persistedCount === evs.length,
100
+ };
101
+ iterations.push({ checks, events: evs.length });
102
+ }
103
+ const failed = iterations.filter(it => Object.values(it.checks).some(v => !v)).length;
104
+ const summary = `org e2e: ${times - failed}/${times} passed` +
105
+ (failed ? ` — failing checks: ${JSON.stringify(iterations.filter(it => Object.values(it.checks).some(v => !v)).map(it => it.checks))}` : '');
106
+ return { iterations, failed, summary };
107
+ }
108
+ //# sourceMappingURL=test-loop.js.map
@@ -77,9 +77,12 @@ export class OutputFormatter {
77
77
  // Check for NO_COLOR environment variable
78
78
  if (process.env.NO_COLOR !== undefined)
79
79
  return false;
80
- // Check for FORCE_COLOR environment variable
81
- if (process.env.FORCE_COLOR !== undefined)
82
- return true;
80
+ // Check for FORCE_COLOR environment variable.
81
+ // Per the supports-color convention, FORCE_COLOR=0 (or "false") forces
82
+ // color OFF; any other defined, non-empty value forces color ON.
83
+ if (process.env.FORCE_COLOR !== undefined) {
84
+ return process.env.FORCE_COLOR !== '0' && process.env.FORCE_COLOR !== 'false';
85
+ }
83
86
  // Check if stdout is a TTY
84
87
  return process.stdout.isTTY ?? false;
85
88
  }
@@ -149,14 +152,18 @@ export class OutputFormatter {
149
152
  if (this.verbosity === 'quiet')
150
153
  return;
151
154
  const icon = this.color('[WARN]', 'yellow', 'bold');
152
- this.writeln(`${icon} ${message}`);
155
+ // Write to stderr (not stdout) so it never interleaves with structured
156
+ // stdout output (e.g. --format json | jq .)
157
+ this.writeErrorln(`${icon} ${message}`);
153
158
  }
154
159
  printInfo(message) {
155
160
  // Info suppressed in quiet mode
156
161
  if (this.verbosity === 'quiet')
157
162
  return;
158
163
  const icon = this.color('[INFO]', 'blue', 'bold');
159
- this.writeln(`${icon} ${message}`);
164
+ // Write to stderr (not stdout) so it never interleaves with structured
165
+ // stdout output (e.g. --format json | jq .)
166
+ this.writeErrorln(`${icon} ${message}`);
160
167
  }
161
168
  printDebug(message) {
162
169
  // Debug only shows in verbose/debug mode
@@ -35,7 +35,35 @@ export declare class CommandParser {
35
35
  */
36
36
  private static readonly RESERVED_FLAG_KEYS;
37
37
  private setFlagSafe;
38
+ /**
39
+ * Merge a single parsed flag (or set of `_` positionals) into the
40
+ * accumulated result flags, collecting repeats into an array instead of
41
+ * overwriting. Declared `type: 'array'` options always end up as an array
42
+ * (even a single occurrence); any other repeated flag also becomes an
43
+ * array on its second occurrence rather than silently dropping the first
44
+ * value.
45
+ */
46
+ private mergeParsedFlags;
38
47
  parse(args: string[]): ParseResult;
48
+ /**
49
+ * Convert a camelCase key to kebab-case (inverse of normalizeKey).
50
+ */
51
+ private camelToKebab;
52
+ /**
53
+ * Ensure every flag is reachable under both its camelCase and
54
+ * kebab-case spelling. Runs once, after all flags (including
55
+ * defaults) have been merged into the result.
56
+ */
57
+ private mirrorFlagKeys;
58
+ /**
59
+ * True when `value` looks like a space-separated negative number
60
+ * (`-0.5`, `-42`) rather than a new flag. Scoped narrowly to "leading `-`
61
+ * immediately followed by a digit" — a legitimate flag name never starts
62
+ * with a digit, so this can't misfire on a genuine following flag while
63
+ * still letting `--threshold -0.5` consume `-0.5` as the value instead of
64
+ * being misparsed as a new (bogus) flag.
65
+ */
66
+ private looksLikeNegativeNumber;
39
67
  private parseFlag;
40
68
  private parseValue;
41
69
  private normalizeKey;
@@ -50,6 +78,10 @@ export declare class CommandParser {
50
78
  * Get boolean flags scoped to a specific command/subcommand.
51
79
  */
52
80
  private getScopedBooleanFlags;
81
+ /**
82
+ * Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
83
+ */
84
+ private getScopedArrayFlags;
53
85
  private getBooleanFlags;
54
86
  private applyDefaults;
55
87
  validateFlags(flags: ParsedFlags, command?: Command): string[];
@@ -72,6 +72,12 @@ export class CommandParser {
72
72
  description: 'Enable interactive mode',
73
73
  type: 'boolean',
74
74
  default: true
75
+ },
76
+ {
77
+ name: 'update',
78
+ description: 'Check for updates on startup (use --no-update to disable)',
79
+ type: 'boolean',
80
+ default: true
75
81
  }
76
82
  ];
77
83
  }
@@ -111,6 +117,48 @@ export class CommandParser {
111
117
  return;
112
118
  flags[key] = value;
113
119
  }
120
+ /**
121
+ * Merge a single parsed flag (or set of `_` positionals) into the
122
+ * accumulated result flags, collecting repeats into an array instead of
123
+ * overwriting. Declared `type: 'array'` options always end up as an array
124
+ * (even a single occurrence); any other repeated flag also becomes an
125
+ * array on its second occurrence rather than silently dropping the first
126
+ * value.
127
+ */
128
+ mergeParsedFlags(into, from, arrayFlags, booleanFlags) {
129
+ for (const key of Object.keys(from)) {
130
+ if (key === '_') {
131
+ into._.push(...from._);
132
+ continue;
133
+ }
134
+ if (CommandParser.RESERVED_FLAG_KEYS.has(key))
135
+ continue;
136
+ const incoming = from[key];
137
+ if (Array.isArray(into[key])) {
138
+ into[key].push(...[].concat(incoming).map(String));
139
+ }
140
+ else if (into[key] !== undefined && booleanFlags?.has(key)) {
141
+ // Declared boolean flag repeated (e.g. `--dry-run --dry-run`) — last
142
+ // value wins instead of promoting to an array. Callers throughout the
143
+ // codebase check booleans with `flags['x'] === true`; an array value
144
+ // would silently fail that check (e.g. guidance.ts's --dry-run,
145
+ // which guards a destructive write).
146
+ into[key] = incoming;
147
+ }
148
+ else if (into[key] !== undefined) {
149
+ // Repeated flag not previously an array — promote to array so the
150
+ // earlier value isn't lost.
151
+ into[key] = [String(into[key]), String(incoming)];
152
+ }
153
+ else if (arrayFlags.has(key)) {
154
+ // First occurrence of a declared array flag — still wrap in an array.
155
+ into[key] = [String(incoming)];
156
+ }
157
+ else {
158
+ into[key] = incoming;
159
+ }
160
+ }
161
+ }
114
162
  parse(args) {
115
163
  const result = {
116
164
  command: [],
@@ -135,6 +183,7 @@ export class CommandParser {
135
183
  // Subcommand-specific aliases take priority over global ones
136
184
  const aliases = this.buildScopedAliases(resolvedSub || resolvedCmd);
137
185
  const booleanFlags = this.getScopedBooleanFlags(resolvedSub || resolvedCmd);
186
+ const arrayFlags = this.getScopedArrayFlags(resolvedSub || resolvedCmd);
138
187
  let i = 0;
139
188
  let parsingFlags = true;
140
189
  while (i < args.length) {
@@ -148,8 +197,13 @@ export class CommandParser {
148
197
  // Handle flags
149
198
  if (parsingFlags && arg.startsWith('-')) {
150
199
  const parseResult = this.parseFlag(args, i, aliases, booleanFlags);
151
- // Apply to result flags
152
- Object.assign(result.flags, parseResult.flags);
200
+ // Merge into result flags. A plain Object.assign here would silently
201
+ // drop earlier values whenever the same flag is passed more than once
202
+ // (`-p a=1 -p b=2` kept only `b=2`) — options declared `type: 'array'`
203
+ // (e.g. browse-action.ts's --params/-p, swarm.ts) are meant to
204
+ // collect every occurrence, and even a flag NOT declared array
205
+ // shouldn't silently lose data on repetition.
206
+ this.mergeParsedFlags(result.flags, parseResult.flags, arrayFlags, booleanFlags);
153
207
  i = parseResult.nextIndex;
154
208
  continue;
155
209
  }
@@ -201,8 +255,55 @@ export class CommandParser {
201
255
  // shadow same-name global options (e.g. `browse screenshot --format` is an
202
256
  // image format, not the global text|json|table output format).
203
257
  this.applyDefaults(result.flags, resolvedCmd);
258
+ // Dual-write flag keys: every flag ends up stored under BOTH its
259
+ // camelCase form (`keepConfig`) and its original kebab-case form
260
+ // (`keep-config`). Historically only the camelCase key was ever set,
261
+ // but dozens of command action functions across the codebase read
262
+ // `ctx.flags['kebab-case']` directly — which was always `undefined`,
263
+ // silently disabling those flags (see AUDIT-BACKLOG P0-10). Running
264
+ // this as a single pass over the fully-merged flags object (long
265
+ // flags, `--no-x` negation, short-flag aliases, and defaults are all
266
+ // merged into `result.flags` by this point) covers every entry point
267
+ // in one place instead of touching each call site individually.
268
+ this.mirrorFlagKeys(result.flags);
204
269
  return result;
205
270
  }
271
+ /**
272
+ * Convert a camelCase key to kebab-case (inverse of normalizeKey).
273
+ */
274
+ camelToKebab(key) {
275
+ return key.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase());
276
+ }
277
+ /**
278
+ * Ensure every flag is reachable under both its camelCase and
279
+ * kebab-case spelling. Runs once, after all flags (including
280
+ * defaults) have been merged into the result.
281
+ */
282
+ mirrorFlagKeys(flags) {
283
+ for (const key of Object.keys(flags)) {
284
+ if (key === '_' || CommandParser.RESERVED_FLAG_KEYS.has(key))
285
+ continue;
286
+ const camelKey = this.normalizeKey(key);
287
+ if (camelKey !== key && flags[camelKey] === undefined && !CommandParser.RESERVED_FLAG_KEYS.has(camelKey)) {
288
+ flags[camelKey] = flags[key];
289
+ }
290
+ const kebabKey = this.camelToKebab(key);
291
+ if (kebabKey !== key && flags[kebabKey] === undefined && !CommandParser.RESERVED_FLAG_KEYS.has(kebabKey)) {
292
+ flags[kebabKey] = flags[key];
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * True when `value` looks like a space-separated negative number
298
+ * (`-0.5`, `-42`) rather than a new flag. Scoped narrowly to "leading `-`
299
+ * immediately followed by a digit" — a legitimate flag name never starts
300
+ * with a digit, so this can't misfire on a genuine following flag while
301
+ * still letting `--threshold -0.5` consume `-0.5` as the value instead of
302
+ * being misparsed as a new (bogus) flag.
303
+ */
304
+ looksLikeNegativeNumber(value) {
305
+ return /^-\d/.test(value);
306
+ }
206
307
  parseFlag(args, index, aliases, booleanFlags) {
207
308
  const flags = { _: [] };
208
309
  const arg = args[index];
@@ -239,7 +340,8 @@ export class CommandParser {
239
340
  if (booleanFlags.has(normalizedKey)) {
240
341
  this.setFlagSafe(flags, normalizedKey, true);
241
342
  }
242
- else if (nextIndex < args.length && !args[nextIndex].startsWith('-')) {
343
+ else if (nextIndex < args.length &&
344
+ (!args[nextIndex].startsWith('-') || this.looksLikeNegativeNumber(args[nextIndex]))) {
243
345
  this.setFlagSafe(flags, normalizedKey, this.parseValue(args[nextIndex]));
244
346
  nextIndex++;
245
347
  }
@@ -258,7 +360,8 @@ export class CommandParser {
258
360
  if (booleanFlags.has(normalizedKey)) {
259
361
  this.setFlagSafe(flags, normalizedKey, true);
260
362
  }
261
- else if (nextIndex < args.length && !args[nextIndex].startsWith('-')) {
363
+ else if (nextIndex < args.length &&
364
+ (!args[nextIndex].startsWith('-') || this.looksLikeNegativeNumber(args[nextIndex]))) {
262
365
  this.setFlagSafe(flags, normalizedKey, this.parseValue(args[nextIndex]));
263
366
  nextIndex++;
264
367
  }
@@ -368,6 +471,20 @@ export class CommandParser {
368
471
  }
369
472
  return flags;
370
473
  }
474
+ /**
475
+ * Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
476
+ */
477
+ getScopedArrayFlags(resolvedCmd) {
478
+ const flags = new Set();
479
+ if (resolvedCmd?.options) {
480
+ for (const opt of resolvedCmd.options) {
481
+ if (opt.type === 'array') {
482
+ flags.add(this.normalizeKey(opt.name));
483
+ }
484
+ }
485
+ }
486
+ return flags;
487
+ }
371
488
  getBooleanFlags() {
372
489
  const flags = new Set();
373
490
  for (const opt of this.globalOptions) {
@@ -471,7 +588,15 @@ export class CommandParser {
471
588
  }
472
589
  // Check for unknown flags if not allowed
473
590
  if (!this.options.allowUnknownFlags) {
474
- const knownFlags = new Set(allOptions.map(opt => this.normalizeKey(opt.name)));
591
+ // Include both the camelCase and original (kebab-case) spelling of
592
+ // each option name — parse() now mirrors flags under both forms, so
593
+ // validation must recognize both or the mirrored key would be
594
+ // flagged as an unknown option.
595
+ const knownFlags = new Set();
596
+ for (const opt of allOptions) {
597
+ knownFlags.add(this.normalizeKey(opt.name));
598
+ knownFlags.add(opt.name);
599
+ }
475
600
  knownFlags.add('_'); // Positional args
476
601
  for (const key of Object.keys(flags)) {
477
602
  if (!knownFlags.has(key) && key !== '_') {
@@ -50,7 +50,19 @@ async function main() {
50
50
  const result = await layer.route(task);
51
51
  // Emit on its own line, prefixed, so the parent can unambiguously locate the
52
52
  // result even if the model loader wrote stray bytes to stdout earlier.
53
- process.stdout.write(`\n__ROUTE_RESULT__${JSON.stringify(result)}\n`);
53
+ //
54
+ // Fix (P1-3): onnxruntime's native thread pool keeps the event loop alive
55
+ // even after this function returns, so the process never exits on its own
56
+ // and the parent's `close` listener waits until it force-kills us at the
57
+ // timeout — discarding the result we already wrote. Force-exit once the
58
+ // write is flushed (same onnx-teardown class as memory.ts's #1428 fix).
59
+ const wroteSynchronously = process.stdout.write(`\n__ROUTE_RESULT__${JSON.stringify(result)}\n`);
60
+ if (wroteSynchronously) {
61
+ process.exit(0);
62
+ }
63
+ else {
64
+ process.stdout.once('drain', () => process.exit(0));
65
+ }
54
66
  }
55
67
  main().catch((err) => {
56
68
  process.stderr.write(`embed-worker: ${err instanceof Error ? err.message : String(err)}\n`);