monomind 2.1.9 → 2.3.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 (93) 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/skills/mastermind-skills/code-quality-reviewer-prompt.md +3 -3
  7. package/packages/@monomind/cli/.claude/skills/mastermind-skills/createorg.md +19 -24
  8. package/packages/@monomind/cli/.claude/skills/mastermind-skills/debug.md +49 -4
  9. package/packages/@monomind/cli/.claude/skills/mastermind-skills/design.md +11 -10
  10. package/packages/@monomind/cli/.claude/skills/mastermind-skills/final-reviewer-prompt.md +144 -0
  11. package/packages/@monomind/cli/.claude/skills/mastermind-skills/implementer-prompt.md +9 -4
  12. package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +20 -2
  13. package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgs.md +40 -19
  14. package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgstatus.md +90 -41
  15. package/packages/@monomind/cli/.claude/skills/mastermind-skills/plan.md +19 -0
  16. package/packages/@monomind/cli/.claude/skills/mastermind-skills/skill-builder.md +171 -0
  17. package/packages/@monomind/cli/.claude/skills/mastermind-skills/spec-reviewer-prompt.md +19 -2
  18. package/packages/@monomind/cli/.claude/skills/mastermind-skills/stoporg.md +7 -5
  19. package/packages/@monomind/cli/.claude/skills/mastermind-skills/taskdev.md +83 -15
  20. package/packages/@monomind/cli/.claude/skills/mastermind-skills/tdd.md +30 -0
  21. package/packages/@monomind/cli/dist/src/autopilot-state.js +6 -4
  22. package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +4 -1
  23. package/packages/@monomind/cli/dist/src/capabilities/manager.js +3 -1
  24. package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +26 -0
  25. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +28 -46
  26. package/packages/@monomind/cli/dist/src/commands/cleanup.d.ts +18 -0
  27. package/packages/@monomind/cli/dist/src/commands/cleanup.js +120 -0
  28. package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +13 -4
  29. package/packages/@monomind/cli/dist/src/commands/hooks-routing-commands.js +34 -18
  30. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +8 -2
  31. package/packages/@monomind/cli/dist/src/commands/hooks.js +1 -1
  32. package/packages/@monomind/cli/dist/src/commands/init-wizard.js +2 -2
  33. package/packages/@monomind/cli/dist/src/commands/init.js +54 -10
  34. package/packages/@monomind/cli/dist/src/commands/mcp.js +4 -1
  35. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +7 -1
  36. package/packages/@monomind/cli/dist/src/commands/org-observe.d.ts +18 -0
  37. package/packages/@monomind/cli/dist/src/commands/org-observe.js +295 -0
  38. package/packages/@monomind/cli/dist/src/commands/org.d.ts +9 -1
  39. package/packages/@monomind/cli/dist/src/commands/org.js +191 -9
  40. package/packages/@monomind/cli/dist/src/commands/performance.js +4 -1
  41. package/packages/@monomind/cli/dist/src/commands/platforms.js +4 -1
  42. package/packages/@monomind/cli/dist/src/commands/security-scan.js +8 -2
  43. package/packages/@monomind/cli/dist/src/commands/start.js +4 -1
  44. package/packages/@monomind/cli/dist/src/commands/swarm.js +5 -4
  45. package/packages/@monomind/cli/dist/src/index.js +13 -3
  46. package/packages/@monomind/cli/dist/src/init/executor.js +19 -5
  47. package/packages/@monomind/cli/dist/src/init/settings-generator.js +8 -1
  48. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +3 -2
  49. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +1 -0
  50. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +63 -20
  51. package/packages/@monomind/cli/dist/src/mcp-tools/claims-tools.js +3 -1
  52. package/packages/@monomind/cli/dist/src/mcp-tools/config-tools.js +3 -2
  53. package/packages/@monomind/cli/dist/src/mcp-tools/daa-tools.js +3 -2
  54. package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +6 -2
  55. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +35 -10
  56. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +24 -7
  57. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-embedding.js +13 -3
  58. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-intelligence.js +9 -3
  59. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-routing.d.ts +1 -0
  60. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-routing.js +112 -10
  61. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +2 -1
  62. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +8 -2
  63. package/packages/@monomind/cli/dist/src/mcp-tools/performance-tools.js +3 -2
  64. package/packages/@monomind/cli/dist/src/mcp-tools/session-tools.js +15 -5
  65. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +4 -1
  66. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +6 -4
  67. package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.d.ts +20 -0
  68. package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.js +38 -16
  69. package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.d.ts +2 -0
  70. package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.js +68 -24
  71. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.js +6 -2
  72. package/packages/@monomind/cli/dist/src/memory/intelligence.js +32 -10
  73. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +16 -4
  74. package/packages/@monomind/cli/dist/src/memory/memory-crud.js +26 -2
  75. package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +3 -2
  76. package/packages/@monomind/cli/dist/src/memory/memory-migrations.js +2 -0
  77. package/packages/@monomind/cli/dist/src/monovector/command-outcomes.js +4 -1
  78. package/packages/@monomind/cli/dist/src/orgrt/bus.js +4 -1
  79. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +74 -19
  80. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +3 -1
  81. package/packages/@monomind/cli/dist/src/orgrt/reporting.d.ts +40 -0
  82. package/packages/@monomind/cli/dist/src/orgrt/reporting.js +127 -0
  83. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +2 -0
  84. package/packages/@monomind/cli/dist/src/orgrt/session.js +10 -2
  85. package/packages/@monomind/cli/dist/src/orgrt/templates.d.ts +16 -0
  86. package/packages/@monomind/cli/dist/src/orgrt/templates.js +57 -0
  87. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -1
  88. package/packages/@monomind/cli/dist/src/transfer/storage/gcs.js +6 -2
  89. package/packages/@monomind/cli/dist/src/ui/orgs.html +32 -0
  90. package/packages/@monomind/cli/dist/src/ui/server.mjs +25 -0
  91. package/packages/@monomind/cli/dist/src/update/executor.js +3 -1
  92. package/packages/@monomind/cli/dist/src/update/rate-limiter.js +3 -1
  93. package/packages/@monomind/cli/package.json +2 -2
@@ -454,8 +454,10 @@ class LocalReasoningBank {
454
454
  // ReasoningBank via intelligence.ts's public API, so no separate
455
455
  // models.json bridge is needed.
456
456
  }
457
- catch {
457
+ catch (e) {
458
458
  // Ignore load errors, start fresh
459
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
460
+ console.error('[intelligence] failed to load patterns.json, starting fresh:', e);
459
461
  }
460
462
  }
461
463
  /**
@@ -680,8 +682,10 @@ function loadPersistedStats() {
680
682
  }
681
683
  }
682
684
  }
683
- catch {
685
+ catch (e) {
684
686
  // Ignore load errors, start fresh
687
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
688
+ console.error('[intelligence] failed to load stats.json, starting fresh:', e);
685
689
  }
686
690
  }
687
691
  /**
@@ -693,8 +697,10 @@ function savePersistedStats() {
693
697
  const path = getStatsPath();
694
698
  writeFileSync(path, JSON.stringify(globalStats, null, 2), 'utf-8');
695
699
  }
696
- catch {
700
+ catch (e) {
697
701
  // Ignore save errors
702
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
703
+ console.error('[intelligence] failed to save stats.json:', e);
698
704
  }
699
705
  }
700
706
  // ============================================================================
@@ -762,7 +768,11 @@ async function _doInitializeIntelligence(config) {
762
768
  }
763
769
  }
764
770
  }
765
- catch { /* neural patterns file unreadable — skip */ }
771
+ catch (e) {
772
+ /* neural patterns file unreadable — skip */
773
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
774
+ console.error('[intelligence] failed to seed patterns.json into ReasoningBank:', e);
775
+ }
766
776
  }
767
777
  // Seed SONA routing patterns into the ReasoningBank so keyword-based
768
778
  // routing knowledge from .swarm/sona-patterns.json participates in
@@ -890,7 +900,9 @@ export async function recordStep(step) {
890
900
  globalStats.trajectoriesRecorded++;
891
901
  return true;
892
902
  }
893
- catch {
903
+ catch (e) {
904
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
905
+ console.error('[intelligence] recordStep failed:', e);
894
906
  return false;
895
907
  }
896
908
  }
@@ -923,7 +935,9 @@ export async function recordTrajectory(steps, verdict) {
923
935
  savePersistedStats();
924
936
  return true;
925
937
  }
926
- catch {
938
+ catch (e) {
939
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
940
+ console.error('[intelligence] recordTrajectory failed:', e);
927
941
  return false;
928
942
  }
929
943
  }
@@ -980,7 +994,9 @@ export async function findSimilarPatterns(query, options) {
980
994
  similarity: r.similarity ?? r.confidence ?? 0.5
981
995
  }));
982
996
  }
983
- catch {
997
+ catch (e) {
998
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
999
+ console.error('[intelligence] findSimilarPatterns failed:', e);
984
1000
  return [];
985
1001
  }
986
1002
  }
@@ -1030,7 +1046,9 @@ export async function endTrajectoryWithVerdict(verdict) {
1030
1046
  savePersistedStats();
1031
1047
  return result;
1032
1048
  }
1033
- catch {
1049
+ catch (e) {
1050
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
1051
+ console.error('[intelligence] endTrajectoryWithVerdict failed:', e);
1034
1052
  return null;
1035
1053
  }
1036
1054
  }
@@ -1052,7 +1070,9 @@ export async function distillLearning() {
1052
1070
  savePersistedStats();
1053
1071
  return result;
1054
1072
  }
1055
- catch {
1073
+ catch (e) {
1074
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
1075
+ console.error('[intelligence] distillLearning failed:', e);
1056
1076
  return null;
1057
1077
  }
1058
1078
  }
@@ -1184,7 +1204,9 @@ function loadSonaRoutingPatterns() {
1184
1204
  }
1185
1205
  return results;
1186
1206
  }
1187
- catch {
1207
+ catch (e) {
1208
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
1209
+ console.error('[intelligence] failed to load .swarm/sona-patterns.json:', e);
1188
1210
  return [];
1189
1211
  }
1190
1212
  }
@@ -148,7 +148,10 @@ async function getBackend(dbPath) {
148
148
  };
149
149
  _embedder = embeddingGenerator;
150
150
  }
151
- catch { /* embeddings unavailable — store and search without vectors */ }
151
+ catch (e) {
152
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
153
+ console.error('[memory-bridge] embedding model failed to load — store and search without vectors:', e);
154
+ }
152
155
  const backend = new LanceDBBackend({
153
156
  // Always route through getDbPath's traversal guard — dbPath here can
154
157
  // originate from a caller/tool-supplied value, and getDbPath(undefined)
@@ -212,7 +215,10 @@ export async function bridgeStoreEntry(options) {
212
215
  embedding = await _embedder(value);
213
216
  embeddingInfo = { dimensions: embedding.length, model: BRIDGE_EMBEDDING_MODEL };
214
217
  }
215
- catch { /* store without embedding */ }
218
+ catch (e) {
219
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
220
+ console.error('[memory-bridge] embedding generation failed — storing entry without embedding:', e);
221
+ }
216
222
  }
217
223
  const mod = await import('@monoes/memory');
218
224
  const entry = mod.createDefaultEntry({
@@ -233,7 +239,10 @@ export async function bridgeStoreEntry(options) {
233
239
  if (existing)
234
240
  await backend.delete(existing.id);
235
241
  }
236
- catch { /* non-fatal */ }
242
+ catch (e) {
243
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
244
+ console.error('[memory-bridge] upsert failed to delete existing entry — may create a duplicate:', e);
245
+ }
237
246
  }
238
247
  // Dedup gate: skip if a near-duplicate already exists
239
248
  const automemCfg = getAutomemConfig();
@@ -644,7 +653,10 @@ export async function bridgeSessionEnd(options) {
644
653
  try {
645
654
  data = JSON.parse(existing.content);
646
655
  }
647
- catch { /* use empty */ }
656
+ catch (e) {
657
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
658
+ console.error('[memory-bridge] session content failed to parse — ending session with empty prior state:', e);
659
+ }
648
660
  await backend.update(existing.id, {
649
661
  content: JSON.stringify({
650
662
  ...data,
@@ -238,8 +238,32 @@ export async function storeEntry(options) {
238
238
  }
239
239
  const fileBuffer = fs.readFileSync(dbPath);
240
240
  const db = new SQL.Database(fileBuffer);
241
- const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
241
+ // memory_entries has no UNIQUE constraint on (key, namespace) — only
242
+ // `id TEXT PRIMARY KEY`. `INSERT OR REPLACE` only replaces a row when
243
+ // its PRIMARY KEY collides, so generating a fresh id on every call (as
244
+ // this used to do unconditionally) meant upsert never actually matched
245
+ // the existing row for a given key+namespace — it silently inserted a
246
+ // duplicate instead of replacing it. When upserting, look up the
247
+ // existing row's real id first and reuse it so the replace actually
248
+ // collides; only mint a new id when no existing row is found.
249
+ let id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
250
+ let existingCreatedAt = null;
251
+ if (upsert) {
252
+ // Only match active rows — a soft-deleted row's key+namespace should
253
+ // not be resurrected by an unrelated upsert; that would both restore
254
+ // history a delete intentionally cleared and mask the deletion.
255
+ const existingIdResult = db.exec("SELECT id, created_at FROM memory_entries WHERE key = ? AND namespace = ? AND status = 'active' LIMIT 1", [key, namespace]);
256
+ const existingRow = existingIdResult[0]?.values?.[0];
257
+ const existingId = existingRow?.[0];
258
+ if (typeof existingId === 'string') {
259
+ id = existingId;
260
+ const createdAt = existingRow?.[1];
261
+ if (typeof createdAt === 'number')
262
+ existingCreatedAt = createdAt;
263
+ }
264
+ }
242
265
  const now = Date.now();
266
+ const createdAt = existingCreatedAt ?? now;
243
267
  let embeddingJson = null;
244
268
  let embeddingDimensions = null;
245
269
  let embeddingModel = null;
@@ -270,7 +294,7 @@ export async function storeEntry(options) {
270
294
  embeddingModel,
271
295
  tags.length > 0 ? JSON.stringify(tags) : null,
272
296
  '{}',
273
- now,
297
+ createdAt,
274
298
  now,
275
299
  ttl ? now + (ttl * 1000) : null
276
300
  ]);
@@ -110,8 +110,9 @@ async function activateControllerRegistry(dbPath, verbose) {
110
110
  console.log(`ControllerRegistry: ${activated.length} controllers activated`);
111
111
  }
112
112
  }
113
- catch {
114
- // ControllerRegistry activation is best-effort
113
+ catch (e) {
114
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
115
+ console.error('[activateControllers] ControllerRegistry activation failed:', e);
115
116
  }
116
117
  return { activated, failed, initTimeMs: performance.now() - startTime };
117
118
  }
@@ -57,6 +57,8 @@ export async function ensureSchemaColumns(dbPath) {
57
57
  }
58
58
  catch (e) {
59
59
  // Column might already exist or other error - continue
60
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
61
+ console.error(`[ensureSchemaColumns] failed to add column '${col.name}':`, e);
60
62
  }
61
63
  }
62
64
  }
@@ -41,7 +41,10 @@ export async function recordCommand(baseDir, cmd) {
41
41
  await fs.writeFile(path, lines.slice(-MAX_COMMAND_RECORDS).join('\n') + '\n', 'utf8');
42
42
  }
43
43
  }
44
- catch { /* non-fatal */ }
44
+ catch (e) {
45
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
46
+ console.error('[recordCommand] failed to record command outcome:', e);
47
+ }
45
48
  }
46
49
  /**
47
50
  * Derive a measured success signal from recent command outcomes.
@@ -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