monomind 2.2.0 → 2.3.1

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 (91) hide show
  1. package/package.json +2 -2
  2. package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +24 -16
  3. package/packages/@monomind/cli/.claude/commands/mastermind/master.md +23 -7
  4. package/packages/@monomind/cli/.claude/commands/mastermind/runorg.md +3 -1
  5. package/packages/@monomind/cli/.claude/commands/mastermind/stoporg.md +6 -3
  6. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +6 -1
  7. package/packages/@monomind/cli/.claude/skills/mastermind-skills/code-quality-reviewer-prompt.md +3 -3
  8. package/packages/@monomind/cli/.claude/skills/mastermind-skills/createorg.md +19 -24
  9. package/packages/@monomind/cli/.claude/skills/mastermind-skills/debug.md +49 -4
  10. package/packages/@monomind/cli/.claude/skills/mastermind-skills/design.md +11 -10
  11. package/packages/@monomind/cli/.claude/skills/mastermind-skills/final-reviewer-prompt.md +144 -0
  12. package/packages/@monomind/cli/.claude/skills/mastermind-skills/implementer-prompt.md +9 -4
  13. package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +20 -2
  14. package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgs.md +40 -19
  15. package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgstatus.md +90 -41
  16. package/packages/@monomind/cli/.claude/skills/mastermind-skills/plan.md +19 -0
  17. package/packages/@monomind/cli/.claude/skills/mastermind-skills/skill-builder.md +171 -0
  18. package/packages/@monomind/cli/.claude/skills/mastermind-skills/spec-reviewer-prompt.md +19 -2
  19. package/packages/@monomind/cli/.claude/skills/mastermind-skills/stoporg.md +7 -5
  20. package/packages/@monomind/cli/.claude/skills/mastermind-skills/taskdev.md +85 -15
  21. package/packages/@monomind/cli/.claude/skills/mastermind-skills/tdd.md +30 -0
  22. package/packages/@monomind/cli/dist/src/autopilot-state.js +6 -4
  23. package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +4 -1
  24. package/packages/@monomind/cli/dist/src/capabilities/manager.js +3 -1
  25. package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +26 -0
  26. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +28 -46
  27. package/packages/@monomind/cli/dist/src/commands/cleanup.d.ts +18 -0
  28. package/packages/@monomind/cli/dist/src/commands/cleanup.js +121 -0
  29. package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +13 -4
  30. package/packages/@monomind/cli/dist/src/commands/hooks-routing-commands.js +34 -18
  31. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +8 -2
  32. package/packages/@monomind/cli/dist/src/commands/hooks.js +1 -1
  33. package/packages/@monomind/cli/dist/src/commands/mcp.js +4 -1
  34. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +7 -1
  35. package/packages/@monomind/cli/dist/src/commands/org-observe.d.ts +18 -0
  36. package/packages/@monomind/cli/dist/src/commands/org-observe.js +295 -0
  37. package/packages/@monomind/cli/dist/src/commands/org.d.ts +9 -1
  38. package/packages/@monomind/cli/dist/src/commands/org.js +191 -9
  39. package/packages/@monomind/cli/dist/src/commands/performance.js +4 -1
  40. package/packages/@monomind/cli/dist/src/commands/platforms.js +4 -1
  41. package/packages/@monomind/cli/dist/src/commands/security-scan.js +8 -2
  42. package/packages/@monomind/cli/dist/src/commands/start.js +4 -1
  43. package/packages/@monomind/cli/dist/src/commands/swarm.js +3 -2
  44. package/packages/@monomind/cli/dist/src/index.js +13 -3
  45. package/packages/@monomind/cli/dist/src/init/executor.js +19 -5
  46. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +3 -2
  47. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +1 -0
  48. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +63 -20
  49. package/packages/@monomind/cli/dist/src/mcp-tools/claims-tools.js +3 -1
  50. package/packages/@monomind/cli/dist/src/mcp-tools/config-tools.js +3 -2
  51. package/packages/@monomind/cli/dist/src/mcp-tools/daa-tools.js +3 -2
  52. package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +6 -2
  53. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +35 -10
  54. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +24 -7
  55. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-embedding.js +13 -3
  56. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-intelligence.js +9 -3
  57. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-routing.d.ts +1 -0
  58. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-routing.js +112 -10
  59. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +2 -1
  60. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +8 -2
  61. package/packages/@monomind/cli/dist/src/mcp-tools/performance-tools.js +3 -2
  62. package/packages/@monomind/cli/dist/src/mcp-tools/session-tools.js +15 -5
  63. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +4 -1
  64. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +6 -4
  65. package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.d.ts +20 -0
  66. package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.js +38 -16
  67. package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.d.ts +2 -0
  68. package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.js +41 -24
  69. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.js +6 -2
  70. package/packages/@monomind/cli/dist/src/memory/intelligence.js +32 -10
  71. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +81 -31
  72. package/packages/@monomind/cli/dist/src/memory/memory-crud.js +26 -2
  73. package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +3 -2
  74. package/packages/@monomind/cli/dist/src/memory/memory-migrations.js +2 -0
  75. package/packages/@monomind/cli/dist/src/monovector/command-outcomes.js +4 -1
  76. package/packages/@monomind/cli/dist/src/orgrt/bus.js +4 -1
  77. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +74 -19
  78. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +3 -1
  79. package/packages/@monomind/cli/dist/src/orgrt/reporting.d.ts +40 -0
  80. package/packages/@monomind/cli/dist/src/orgrt/reporting.js +127 -0
  81. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +2 -0
  82. package/packages/@monomind/cli/dist/src/orgrt/session.js +10 -2
  83. package/packages/@monomind/cli/dist/src/orgrt/templates.d.ts +16 -0
  84. package/packages/@monomind/cli/dist/src/orgrt/templates.js +57 -0
  85. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -1
  86. package/packages/@monomind/cli/dist/src/transfer/storage/gcs.js +6 -2
  87. package/packages/@monomind/cli/dist/src/ui/orgs.html +32 -0
  88. package/packages/@monomind/cli/dist/src/ui/server.mjs +25 -0
  89. package/packages/@monomind/cli/dist/src/update/executor.js +3 -1
  90. package/packages/@monomind/cli/dist/src/update/rate-limiter.js +3 -1
  91. package/packages/@monomind/cli/package.json +3 -3
@@ -36,7 +36,10 @@ export class OrgBus {
36
36
  this.pending = this.pending.then(async () => {
37
37
  await mkdir(this.dir, { recursive: true });
38
38
  await appendFile(this.file, JSON.stringify(e) + '\n', 'utf8');
39
- }).catch(() => { });
39
+ }).catch((err) => {
40
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
41
+ console.error('[org-bus] event write failed:', err);
42
+ });
40
43
  for (const fn of this.listeners) {
41
44
  try {
42
45
  fn(e);
@@ -10,6 +10,7 @@ import { attachForwarder } from './forwarder.js';
10
10
  import { BrokerLease, lookupOrg } from './broker.js';
11
11
  import { queueMessage, drainInbox } from './inbox.js';
12
12
  import { OrgDefSchema, ORG_DIR } from './types.js';
13
+ import { summarizeRun, readRunEvents, readHistory, historyFile } from './reporting.js';
13
14
  export class OrgDaemon {
14
15
  root;
15
16
  opts;
@@ -66,33 +67,74 @@ export class OrgDaemon {
66
67
  const mailbox = new Mailbox();
67
68
  const policy = new PolicyEngine(role.id, { maxTokens: perRoleBudget, ...(role.policy ?? {}) }, bus, cwd);
68
69
  const runtime = { mailbox, policy, status: 'running', done: Promise.resolve() };
69
- runtime.done = runAgentSession({
70
+ const sessionOpts = {
70
71
  org: name, role, bus, policy, mailbox, cwd, def,
71
72
  maxTurns: def.run_config.max_turns_per_message,
72
73
  deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
73
- askHuman: (role, question) => this.askHuman(name, role, question),
74
+ askHuman: (r, question) => this.askHuman(name, r, question),
75
+ onComplete: (r, outcome, summary) => {
76
+ bus.emit({ type: 'status', from: r, reason: 'org-complete', msg: `run outcome: ${outcome}`, data: { outcome, summary } });
77
+ },
74
78
  queryFn: this.opts.queryFn,
75
- }).then(() => { runtime.status = 'ended'; })
76
- .catch((err) => {
77
- const message = err instanceof Error ? err.message : String(err);
78
- runtime.status = 'crashed';
79
- runtime.error = message;
80
- // runAgentSession already emits a 'status' event for the raw error; emit an
81
- // 'audit' event too so dashboards/alerts that filter on actionable failures
82
- // (not routine status chatter) can surface a dead agent instead of a run that
83
- // silently never progresses.
84
- bus.emit({
85
- type: 'audit', from: role.id,
86
- msg: `agent "${role.id}" crashed: ${message}`,
87
- reason: 'agent-session-crash',
88
- data: { agentId: role.id, error: message },
89
- });
90
- });
79
+ };
80
+ // Supervised session: transient crashes (provider blips, network) restart
81
+ // with backoff; a crash with the mailbox already closed, or one that
82
+ // exhausts the retry budget, is terminal. runAgentSession already emits a
83
+ // 'status' event for the raw error; the terminal 'audit' event is for
84
+ // dashboards/alerts that filter on actionable failures (not routine
85
+ // status chatter) so a dead agent surfaces instead of a run that
86
+ // silently never progresses.
87
+ const BACKOFFS_MS = [1000, 5000, 15000];
88
+ runtime.done = (async () => {
89
+ for (let attempt = 0;; attempt++) {
90
+ try {
91
+ await runAgentSession(sessionOpts);
92
+ runtime.status = 'ended';
93
+ return;
94
+ }
95
+ catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ const crash = () => {
98
+ runtime.status = 'crashed';
99
+ runtime.error = message;
100
+ bus.emit({
101
+ type: 'audit', from: role.id,
102
+ msg: `agent "${role.id}" crashed: ${message}`,
103
+ reason: 'agent-session-crash',
104
+ data: { agentId: role.id, error: message, restarts: attempt },
105
+ });
106
+ };
107
+ if (mailbox.isClosed || attempt >= BACKOFFS_MS.length) {
108
+ crash();
109
+ return;
110
+ }
111
+ bus.emit({
112
+ type: 'status', from: role.id, reason: 'agent-restart',
113
+ msg: `agent "${role.id}" crashed (${message}) — restarting in ${BACKOFFS_MS[attempt]}ms (attempt ${attempt + 1}/${BACKOFFS_MS.length})`,
114
+ });
115
+ await new Promise(r => { const t = setTimeout(r, BACKOFFS_MS[attempt]); t.unref?.(); });
116
+ if (mailbox.isClosed) {
117
+ crash();
118
+ return;
119
+ } // org stopped during backoff — never recovered
120
+ }
121
+ }
122
+ })();
91
123
  running.agents.set(role.id, runtime);
92
124
  }
93
125
  const boss = def.roles.find(r => r.type === 'boss' || r.reports_to === null) ?? def.roles[0];
126
+ // Cross-run memory: brief the coordinator on the previous run so scheduled
127
+ // orgs accumulate instead of starting cold every interval.
128
+ const prev = readHistory(this.root, name).at(-1);
129
+ const prevBrief = prev
130
+ ? `\n\nPrevious run (${prev.run}${prev.endedAt ? `, ${new Date(prev.endedAt).toISOString()}` : ''}): ` +
131
+ (prev.outcome
132
+ ? `outcome "${prev.outcome.status}" — ${prev.outcome.summary}`
133
+ : `no recorded outcome (${prev.messages} messages, ${prev.assets.length} assets${prev.crashes.length ? `, ${prev.crashes.length} crashed agent(s)` : ''})`) +
134
+ `\nBuild on that work — do not redo what is already done.`
135
+ : '';
94
136
  running.agents.get(boss.id).mailbox.push(`Org "${name}" started (run ${run}).\nGoal: ${taskOverride ?? def.goal}\n` +
95
- `Coordinate your team via org_send. Report completion by ending your turn.`);
137
+ `Coordinate your team via org_send. When the goal is achieved (or clearly can't be), record it with org_complete, then end your turn.${prevBrief}`);
96
138
  bus.emit({ type: 'status', msg: `org started (${def.roles.length} agents)`, data: { goal: taskOverride ?? def.goal } });
97
139
  this.persistState(name, 'running', run);
98
140
  if (this.opts.crossProcess && this.opts.inboxUrl) {
@@ -336,6 +378,19 @@ export class OrgDaemon {
336
378
  }
337
379
  org.bus.emit({ type: 'status', msg: 'org stopped' });
338
380
  await org.bus.flush();
381
+ // Append this run's summary to <org>/history.jsonl — read back from the
382
+ // flushed bus.jsonl (the full durable record) rather than the bounded
383
+ // in-memory buffer, so long runs summarize completely.
384
+ try {
385
+ const events = readRunEvents(this.root, name, org.run);
386
+ if (events.length) {
387
+ const { appendFileSync } = await import('node:fs');
388
+ appendFileSync(historyFile(this.root, name), JSON.stringify(summarizeRun(events)) + '\n', 'utf8');
389
+ }
390
+ }
391
+ catch (err) {
392
+ console.error(`org ${name}: could not write run history:`, err instanceof Error ? err.message : err);
393
+ }
339
394
  // the "org stopped" event above triggers the forwarder's final org:complete /
340
395
  // session:complete POST — without waiting for it here, the CLI process can exit
341
396
  // (and kill the in-flight fetch) before that last event reaches the dashboard,
@@ -23,7 +23,9 @@ export function drainInbox(root, orgName) {
23
23
  try {
24
24
  renameSync(path, draining);
25
25
  }
26
- catch {
26
+ catch (e) {
27
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
28
+ console.error('[inbox] drainInbox rename failed:', e);
27
29
  return [];
28
30
  }
29
31
  const raw = readFileSync(draining, 'utf8').trim();
@@ -0,0 +1,40 @@
1
+ import type { BusEvent } from './types.js';
2
+ export interface RoleStats {
3
+ messagesSent: number;
4
+ toolsAllowed: number;
5
+ toolsDenied: number;
6
+ tokens: number;
7
+ costUsd: number;
8
+ crashed: boolean;
9
+ }
10
+ export interface RunSummary {
11
+ org: string;
12
+ run: string;
13
+ startedAt: number | null;
14
+ endedAt: number | null;
15
+ durationMs: number | null;
16
+ events: number;
17
+ messages: number;
18
+ xorgMessages: number;
19
+ assets: string[];
20
+ crashes: string[];
21
+ outcome: {
22
+ status: string;
23
+ summary: string;
24
+ by: string;
25
+ } | null;
26
+ roles: Record<string, RoleStats>;
27
+ totalTokens: number;
28
+ totalCostUsd: number;
29
+ }
30
+ /** Aggregate one run's bus events into a summary. */
31
+ export declare function summarizeRun(events: BusEvent[]): RunSummary;
32
+ /** run directories for an org, newest first (by name — run-YYYYMMDDHHMMSS-xxxx sorts naturally). */
33
+ export declare function listRunDirs(cwd: string, org: string): string[];
34
+ export declare function readRunEvents(cwd: string, org: string, run: string): BusEvent[];
35
+ /** history.jsonl — one RunSummary line per completed run, appended by the daemon at stopOrg. */
36
+ export declare function historyFile(cwd: string, org: string): string;
37
+ export declare function readHistory(cwd: string, org: string): RunSummary[];
38
+ /** One bus event as a compact human-readable log line. */
39
+ export declare function formatEvent(e: BusEvent): string;
40
+ //# sourceMappingURL=reporting.d.ts.map
@@ -0,0 +1,127 @@
1
+ // packages/@monomind/cli/src/orgrt/reporting.ts
2
+ // Read-side aggregation over an org run's bus.jsonl — powers `org report`,
3
+ // `org logs`, and the per-run summary line appended to <org>/history.jsonl.
4
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { ORG_DIR } from './types.js';
7
+ const roleStats = () => ({ messagesSent: 0, toolsAllowed: 0, toolsDenied: 0, tokens: 0, costUsd: 0, crashed: false });
8
+ /** Aggregate one run's bus events into a summary. */
9
+ export function summarizeRun(events) {
10
+ const s = {
11
+ org: events[0]?.org ?? '', run: events[0]?.run ?? '',
12
+ startedAt: events.length ? events[0].ts : null,
13
+ endedAt: events.length ? events[events.length - 1].ts : null,
14
+ durationMs: null, events: events.length,
15
+ messages: 0, xorgMessages: 0, assets: [], crashes: [], outcome: null,
16
+ roles: {}, totalTokens: 0, totalCostUsd: 0,
17
+ };
18
+ if (s.startedAt !== null && s.endedAt !== null)
19
+ s.durationMs = s.endedAt - s.startedAt;
20
+ const role = (id) => {
21
+ const key = id ?? '(system)';
22
+ return (s.roles[key] ??= roleStats());
23
+ };
24
+ for (const e of events) {
25
+ switch (e.type) {
26
+ case 'message':
27
+ s.messages++;
28
+ role(e.from).messagesSent++;
29
+ break;
30
+ case 'xorg':
31
+ s.xorgMessages++;
32
+ role(e.from?.includes(':') ? e.from.split(':')[1] : e.from).messagesSent++;
33
+ break;
34
+ case 'tool':
35
+ if (e.decision === 'deny')
36
+ role(e.from).toolsDenied++;
37
+ else
38
+ role(e.from).toolsAllowed++;
39
+ break;
40
+ case 'asset':
41
+ if (e.path && !s.assets.includes(e.path))
42
+ s.assets.push(e.path);
43
+ break;
44
+ case 'usage': {
45
+ const tokens = Number(e.data?.tokens ?? 0);
46
+ const cost = Number(e.data?.cost_usd ?? 0);
47
+ const r = role(e.from);
48
+ r.tokens += tokens;
49
+ s.totalTokens += tokens;
50
+ if (Number.isFinite(cost)) {
51
+ r.costUsd += cost;
52
+ s.totalCostUsd += cost;
53
+ }
54
+ break;
55
+ }
56
+ case 'audit':
57
+ if (e.reason === 'agent-session-crash' && e.from) {
58
+ s.crashes.push(e.from);
59
+ role(e.from).crashed = true;
60
+ }
61
+ break;
62
+ case 'status': {
63
+ const d = e.data;
64
+ if (e.reason === 'org-complete' && d?.outcome)
65
+ s.outcome = { status: d.outcome, summary: d.summary ?? '', by: e.from ?? '' };
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ return s;
71
+ }
72
+ /** run directories for an org, newest first (by name — run-YYYYMMDDHHMMSS-xxxx sorts naturally). */
73
+ export function listRunDirs(cwd, org) {
74
+ const base = join(cwd, ORG_DIR, org);
75
+ if (!existsSync(base))
76
+ return [];
77
+ return readdirSync(base).filter(d => d.startsWith('run-')).sort().reverse();
78
+ }
79
+ export function readRunEvents(cwd, org, run) {
80
+ const f = join(cwd, ORG_DIR, org, run, 'bus.jsonl');
81
+ if (!existsSync(f))
82
+ return [];
83
+ return readFileSync(f, 'utf8').split('\n').filter(Boolean)
84
+ .map(l => { try {
85
+ return JSON.parse(l);
86
+ }
87
+ catch {
88
+ return null;
89
+ } })
90
+ .filter((e) => e !== null);
91
+ }
92
+ /** history.jsonl — one RunSummary line per completed run, appended by the daemon at stopOrg. */
93
+ export function historyFile(cwd, org) {
94
+ return join(cwd, ORG_DIR, org, 'history.jsonl');
95
+ }
96
+ export function readHistory(cwd, org) {
97
+ const f = historyFile(cwd, org);
98
+ if (!existsSync(f))
99
+ return [];
100
+ return readFileSync(f, 'utf8').split('\n').filter(Boolean)
101
+ .map(l => { try {
102
+ return JSON.parse(l);
103
+ }
104
+ catch {
105
+ return null;
106
+ } })
107
+ .filter((s) => s !== null);
108
+ }
109
+ /** One bus event as a compact human-readable log line. */
110
+ export function formatEvent(e) {
111
+ const t = new Date(e.ts).toISOString().slice(11, 19);
112
+ const from = e.from ?? '·';
113
+ switch (e.type) {
114
+ case 'message':
115
+ case 'xorg':
116
+ return `${t} ${e.type === 'xorg' ? '⇄' : '→'} ${from} → ${e.to}: [${e.subject ?? ''}] ${trim(e.msg)}`;
117
+ case 'chat': return `${t} 💬 ${from}: ${trim(e.msg)}`;
118
+ case 'tool': return `${t} 🔧 ${from} ${e.tool} ${e.decision === 'deny' ? `DENIED (${e.reason})` : 'ok'}`;
119
+ case 'asset': return `${t} 📄 ${from} wrote ${e.path}`;
120
+ case 'usage': return `${t} 🪙 ${from} +${e.data?.tokens ?? 0} tokens`;
121
+ case 'audit': return `${t} ⚠️ ${from} ${e.msg ?? e.reason ?? ''}`;
122
+ case 'question': return `${t} ❓ ${from}: ${trim(e.msg)}`;
123
+ default: return `${t} ▪ ${from} ${e.type}: ${trim(e.msg ?? '')}`;
124
+ }
125
+ }
126
+ const trim = (s, n = 120) => !s ? '' : s.length > n ? `${s.slice(0, n - 1)}…` : s.replace(/\n/g, ' ');
127
+ //# sourceMappingURL=reporting.js.map
@@ -13,6 +13,8 @@ export interface SessionOpts {
13
13
  cwd: string;
14
14
  deliver: DeliverFn;
15
15
  askHuman?: (role: string, question: string) => Promise<string>;
16
+ /** Coordinator-only: records the run's outcome (daemon persists it to run history). */
17
+ onComplete?: (role: string, outcome: 'achieved' | 'partial' | 'failed', summary: string) => void;
16
18
  def?: OrgDef;
17
19
  maxTurns?: number;
18
20
  queryFn?: typeof query;
@@ -4,17 +4,20 @@ import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'
4
4
  import { resolveProviderEnv } from './provider.js';
5
5
  /** Role briefing given to each agent session (SDK systemPrompt option). */
6
6
  export function buildRolePrompt(role, def, roster) {
7
+ const isCoordinator = role.reports_to == null;
7
8
  return [
8
9
  `You are agent "${role.id}" (${role.title || role.type}) in the org "${def.name}".`,
9
10
  `Org goal: ${def.goal}`,
10
- role.reports_to ? `You report to "${role.reports_to}".` : `You are the coordinator of this org.`,
11
+ isCoordinator ? `You are the coordinator of this org.` : `You report to "${role.reports_to}".`,
11
12
  role.responsibilities?.length ? `Your responsibilities:\n- ${role.responsibilities.join('\n- ')}` : '',
12
13
  `## Communication protocol`,
13
14
  `The ONLY way to communicate with other agents is the org_send tool.`,
14
15
  `Roster: ${roster.join(', ')}. Address another org's agent as "<org-name>:<role-id>".`,
15
16
  `If you need a human decision, call ask_human with your question, then end your turn — you'll receive the human's answer as a new message when it arrives. Do not call ask_human for anything you can resolve yourself.`,
16
17
  `When you receive a message, act on it, then org_send your result to the requester.`,
17
- `When your current work is complete and no reply is needed, end your turn without further tool calls.`,
18
+ isCoordinator
19
+ ? `When the org's goal for this run is achieved (or clearly can't be), call org_complete exactly once with the outcome and a concise summary of what was done — it is recorded in the org's run history and briefed to the next run. Then end your turn.`
20
+ : `When your current work is complete and no reply is needed, end your turn without further tool calls.`,
18
21
  ].filter(Boolean).join('\n\n');
19
22
  }
20
23
  /**
@@ -44,10 +47,15 @@ export async function runAgentSession(opts) {
44
47
  async function runOneSession(opts) {
45
48
  const { org, role, bus, policy, mailbox, cwd, deliver } = opts;
46
49
  const queryFn = opts.queryFn ?? query;
50
+ const isCoordinator = role.reports_to == null;
47
51
  const orgServer = createSdkMcpServer({
48
52
  name: 'org',
49
53
  version: '1.0.0',
50
54
  tools: [
55
+ ...(isCoordinator && opts.onComplete ? [tool('org_complete', 'Record the outcome of this run. Call exactly once, when the goal is achieved or clearly cannot be. The outcome and summary are persisted to the org run history and briefed to the next run.', { outcome: z.enum(['achieved', 'partial', 'failed']), summary: z.string() }, async (args) => {
56
+ opts.onComplete(role.id, args.outcome, args.summary);
57
+ return { content: [{ type: 'text', text: `outcome "${args.outcome}" recorded` }] };
58
+ })] : []),
51
59
  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) => {
52
60
  const receipt = await deliver(role.id, args.to, args.subject, args.message);
53
61
  return { content: [{ type: 'text', text: receipt }] };
@@ -0,0 +1,16 @@
1
+ import { type OrgDef } from './types.js';
2
+ interface TemplateRole {
3
+ id: string;
4
+ title: string;
5
+ type: string;
6
+ reports_to: string | null;
7
+ responsibilities: string[];
8
+ }
9
+ interface Template {
10
+ goal: string;
11
+ roles: TemplateRole[];
12
+ }
13
+ export declare const ORG_TEMPLATES: Record<string, Template>;
14
+ export declare function buildFromTemplate(templateName: string, orgName: string, goal?: string): OrgDef | null;
15
+ export {};
16
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1,57 @@
1
+ // packages/@monomind/cli/src/orgrt/templates.ts
2
+ // Starter org configs for `monomind org create <name> --template <t>`.
3
+ // Each validates against OrgDefSchema; goal is a placeholder the user edits
4
+ // (or supplies via --goal).
5
+ import { OrgDefSchema } from './types.js';
6
+ export const ORG_TEMPLATES = {
7
+ 'content-team': {
8
+ goal: 'Produce and publish high-quality content on a steady cadence',
9
+ roles: [
10
+ { id: 'editor-in-chief', title: 'Editor in Chief', type: 'boss', reports_to: null,
11
+ responsibilities: ['Own the content calendar and quality bar', 'Assign pieces to the writer and route drafts to review', 'Approve final drafts for publishing'] },
12
+ { id: 'writer', title: 'Staff Writer', type: 'specialist', reports_to: 'editor-in-chief',
13
+ responsibilities: ['Draft articles from assigned briefs', 'Incorporate review feedback', 'Hand finished drafts to the reviewer'] },
14
+ { id: 'reviewer', title: 'Content Reviewer', type: 'reviewer', reports_to: 'editor-in-chief',
15
+ responsibilities: ['Review drafts for accuracy, clarity, and tone', 'Return actionable feedback to the writer', 'Flag anything needing human judgment via ask_human'] },
16
+ ],
17
+ },
18
+ 'dev-team': {
19
+ goal: 'Deliver features and fixes from a continuously groomed backlog',
20
+ roles: [
21
+ { id: 'tech-lead', title: 'Tech Lead', type: 'boss', reports_to: null,
22
+ responsibilities: ['Break the goal into concrete tasks and assign them', 'Arbitrate design questions', 'Merge only work that passed review and tests'] },
23
+ { id: 'developer', title: 'Developer', type: 'specialist', reports_to: 'tech-lead',
24
+ responsibilities: ['Implement assigned tasks with tests', 'Keep changes small and focused', 'Send diffs to the reviewer before reporting done'] },
25
+ { id: 'code-reviewer', title: 'Code Reviewer', type: 'reviewer', reports_to: 'tech-lead',
26
+ responsibilities: ['Review diffs for correctness and maintainability', 'Verify claimed test results', 'Reject unverified work'] },
27
+ { id: 'qa', title: 'QA Engineer', type: 'specialist', reports_to: 'tech-lead',
28
+ responsibilities: ['Exercise delivered features end-to-end', 'File precise reproduction steps for defects'] },
29
+ ],
30
+ },
31
+ 'research-pod': {
32
+ goal: 'Produce a recurring intelligence brief on a defined topic',
33
+ roles: [
34
+ { id: 'lead-analyst', title: 'Lead Analyst', type: 'boss', reports_to: null,
35
+ responsibilities: ['Define research questions for each cycle', 'Synthesize findings into the final brief', 'Decide what merits deeper follow-up'] },
36
+ { id: 'researcher', title: 'Researcher', type: 'researcher', reports_to: 'lead-analyst',
37
+ responsibilities: ['Gather primary sources on assigned questions', 'Separate observed facts from inference', 'Deliver structured findings with citations'] },
38
+ { id: 'fact-checker', title: 'Fact Checker', type: 'reviewer', reports_to: 'lead-analyst',
39
+ responsibilities: ['Verify claims in draft briefs against sources', 'Flag unverifiable claims for removal or hedging'] },
40
+ ],
41
+ },
42
+ };
43
+ export function buildFromTemplate(templateName, orgName, goal) {
44
+ const t = ORG_TEMPLATES[templateName];
45
+ if (!t)
46
+ return null;
47
+ // Parse through the schema so a template can never produce an unrunnable config.
48
+ return OrgDefSchema.parse({
49
+ name: orgName,
50
+ goal: goal ?? t.goal,
51
+ status: 'stopped',
52
+ schedule: null,
53
+ run_config: { max_concurrent_agents: 4, budget_tokens: 1_000_000, memory_namespace: `org:${orgName}`, max_turns_per_message: 30 },
54
+ roles: t.roles,
55
+ });
56
+ }
57
+ //# sourceMappingURL=templates.js.map
@@ -194,7 +194,16 @@ export class ConfigFileManager {
194
194
  const parsed = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
195
195
  onDisk = sanitizeConfigObject(parsed);
196
196
  }
197
- catch { /* fall through to defaults */ }
197
+ catch (e) {
198
+ // The file exists but failed to parse — do NOT fall through to
199
+ // cloneDefaultConfig() and write that back. This config can hold
200
+ // provider API keys (see this method's doc comment above); silently
201
+ // "recovering" a momentarily-corrupt file by overwriting it with
202
+ // defaults-plus-the-new-key discards every real credential in it.
203
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
204
+ console.error('[config-file-manager] failed to parse existing config file:', e);
205
+ throw new Error(`Config file exists but is unreadable/corrupt: ${targetPath}. Refusing to overwrite it — fix or remove the file manually, then retry.`);
206
+ }
198
207
  }
199
208
  setNestedValue(onDisk, key, sanitisedValue);
200
209
  this.writeAtomic(targetPath, onDisk);
@@ -257,7 +257,9 @@ export async function listGCSObjects(prefix, config) {
257
257
  updated: obj.updated || new Date().toISOString(),
258
258
  }));
259
259
  }
260
- catch {
260
+ catch (e) {
261
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
262
+ console.error('[GCS] listGCSObjects failed:', e);
261
263
  return [];
262
264
  }
263
265
  }
@@ -275,7 +277,9 @@ export async function deleteFromGCS(uri, config) {
275
277
  execFileSync('gcloud', rmArgs, { encoding: 'utf-8', stdio: 'pipe' });
276
278
  return true;
277
279
  }
278
- catch {
280
+ catch (e) {
281
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
282
+ console.error('[GCS] deleteFromGCS failed:', e);
279
283
  return false;
280
284
  }
281
285
  }
@@ -975,6 +975,7 @@ html, body {
975
975
  <div id="health-pane">
976
976
  <div id="health-grid" class="health-grid"></div>
977
977
  <div id="health-detail"></div>
978
+ <div id="health-history"></div>
978
979
  </div>
979
980
  </div>
980
981
 
@@ -1759,6 +1760,37 @@ function renderHealth() {
1759
1760
  HEALTH DATA UNAVAILABLE
1760
1761
  </div>`;
1761
1762
  }
1763
+
1764
+ loadRunHistory();
1765
+ }
1766
+
1767
+ // Run history (history.jsonl, written by the daemon at each stopOrg): outcome,
1768
+ // duration, tokens per completed run — rendered under the health grid.
1769
+ async function loadRunHistory() {
1770
+ const el = document.getElementById('health-history');
1771
+ if (!el || !selectedOrg) return;
1772
+ try {
1773
+ const listOrg = allOrgs.find(o => o.name === selectedOrg) || {};
1774
+ const r = await fetch(`/api/orgs/${encodeURIComponent(selectedOrg)}/history?dir=${encodeURIComponent(listOrg.projectDir || '')}`);
1775
+ if (!r.ok) { el.innerHTML = ''; return; }
1776
+ const runs = (await r.json()).runs || [];
1777
+ if (!runs.length) { el.innerHTML = ''; return; }
1778
+ const outcomeColor = s => s === 'achieved' ? 'var(--teal)' : s === 'partial' ? 'var(--amber, #d90)' : 'var(--red)';
1779
+ el.innerHTML = `
1780
+ <div style="margin-top:16px">
1781
+ <div style="font-size:8px;letter-spacing:2px;color:var(--dim);margin-bottom:8px">RUN HISTORY (${runs.length})</div>
1782
+ ${runs.slice(0, 10).map(h => `
1783
+ <div style="font-size:9px;padding:5px 0;border-bottom:1px solid oklch(50% 0 0 / 0.1);display:flex;gap:10px;align-items:baseline">
1784
+ <span style="color:var(--dim);white-space:nowrap">${esc(h.run || '?')}</span>
1785
+ <span style="color:var(--muted);white-space:nowrap">${h.durationMs != null ? Math.round(h.durationMs / 1000) + 's' : '—'} · ${h.totalTokens || 0} tok</span>
1786
+ ${h.outcome
1787
+ ? `<span style="color:${outcomeColor(h.outcome.status)}">${esc(h.outcome.status.toUpperCase())}</span>
1788
+ <span style="color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(h.outcome.summary || '')}</span>`
1789
+ : `<span style="color:var(--dim)">NO OUTCOME</span>`}
1790
+ ${h.crashes && h.crashes.length ? `<span style="color:var(--red)">✗ ${h.crashes.length} crash</span>` : ''}
1791
+ </div>`).join('')}
1792
+ </div>`;
1793
+ } catch (_) { el.innerHTML = ''; }
1762
1794
  }
1763
1795
 
1764
1796
  // ── CHAT TAB ──────────────────────────────────────────────────────
@@ -5863,6 +5863,31 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5863
5863
  return;
5864
5864
  }
5865
5865
 
5866
+ // GET /api/orgs/:name/history — per-run outcome summaries (history.jsonl, newest first)
5867
+ if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/history$/.test(url)) {
5868
+ try {
5869
+ const orgName = decodeURIComponent(url.split('/')[3]);
5870
+ const _histQs = new URL(req.url, 'http://localhost').searchParams;
5871
+ const root = path.resolve(_histQs.get('dir') || projectDir || process.cwd());
5872
+ if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
5873
+ res.writeHead(400); res.end('{"error":"invalid org name"}'); return;
5874
+ }
5875
+ const monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
5876
+ const histFile = path.join(monoDir, 'orgs', orgName, 'history.jsonl');
5877
+ let runs = [];
5878
+ if (fs.existsSync(histFile)) {
5879
+ runs = fs.readFileSync(histFile, 'utf8').split('\n').filter(l => l.trim())
5880
+ .map(l => { try { return JSON.parse(l); } catch(_) { return null; } })
5881
+ .filter(Boolean).reverse().slice(0, 50);
5882
+ }
5883
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5884
+ res.end(JSON.stringify({ runs }));
5885
+ } catch(err) {
5886
+ res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
5887
+ }
5888
+ return;
5889
+ }
5890
+
5866
5891
  // GET /api/mastermind/metrics — aggregate system metrics from token-summary and swarm-activity
5867
5892
  if (req.method === 'GET' && url === '/api/mastermind/metrics') {
5868
5893
  try {
@@ -68,8 +68,10 @@ export function loadHistory() {
68
68
  });
69
69
  }
70
70
  }
71
- catch {
71
+ catch (e) {
72
72
  // Corrupted file
73
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
74
+ console.error('[update-executor] loadHistory failed:', e);
73
75
  }
74
76
  return [];
75
77
  }
@@ -70,8 +70,10 @@ export function loadState() {
70
70
  return state;
71
71
  }
72
72
  }
73
- catch {
73
+ catch (e) {
74
74
  // Corrupted file, reset
75
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
76
+ console.error('[rate-limiter] update-state.json read/parse failed:', e);
75
77
  }
76
78
  return getDefaultState();
77
79
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "type": "module",
5
5
  "description": "CLI engine for Monomind — an open-source MCP server that extends Claude Code with a codebase knowledge graph (tree-sitter + SQLite), persistent memory, multi-agent task coordination, and session hooks. MIT licensed, fully local.",
6
6
  "main": "dist/src/index.js",
@@ -105,7 +105,7 @@
105
105
  },
106
106
  "optionalDependencies": {
107
107
  "@huggingface/transformers": "^3.8.1",
108
- "@monoes/memory": "^1.0.5",
108
+ "@monoes/memory": "^1.0.6",
109
109
  "@monomind/hooks": "*",
110
110
  "@monomind/mcp": "*",
111
111
  "@monomind/routing": "*",
@@ -119,4 +119,4 @@
119
119
  "overrides": {
120
120
  "onnxruntime-node": ">=1.27.0"
121
121
  }
122
- }
122
+ }