monomind 2.0.0 → 2.0.2

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 (35) hide show
  1. package/README.md +11 -12
  2. package/package.json +2 -2
  3. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +10 -0
  4. package/packages/@monomind/cli/README.md +11 -12
  5. package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +71 -12
  6. package/packages/@monomind/cli/dist/src/browser/dashboard/ui.html +4 -4
  7. package/packages/@monomind/cli/dist/src/commands/browse-workflow.js +2 -2
  8. package/packages/@monomind/cli/dist/src/index.js +8 -7
  9. package/packages/@monomind/cli/dist/src/mcp-tools/coherence/types.d.ts +18 -18
  10. package/packages/@monomind/cli/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +12 -12
  11. package/packages/@monomind/cli/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
  12. package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +23 -0
  13. package/packages/@monomind/cli/dist/src/orgrt/bus.js +64 -0
  14. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +41 -0
  15. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +101 -0
  16. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +11 -0
  17. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +37 -0
  18. package/packages/@monomind/cli/dist/src/orgrt/mailbox.d.ts +25 -0
  19. package/packages/@monomind/cli/dist/src/orgrt/mailbox.js +39 -0
  20. package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +24 -0
  21. package/packages/@monomind/cli/dist/src/orgrt/policy.js +86 -0
  22. package/packages/@monomind/cli/dist/src/orgrt/provider.d.ts +9 -0
  23. package/packages/@monomind/cli/dist/src/orgrt/provider.js +54 -0
  24. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +23 -0
  25. package/packages/@monomind/cli/dist/src/orgrt/session.js +78 -0
  26. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +847 -0
  27. package/packages/@monomind/cli/dist/src/orgrt/types.js +51 -0
  28. package/packages/@monomind/cli/dist/src/parser.js +31 -5
  29. package/packages/@monomind/cli/dist/src/ui/collector.mjs +69 -44
  30. package/packages/@monomind/cli/dist/src/ui/dashboard.html +112 -25
  31. package/packages/@monomind/cli/dist/src/ui/orgs.html +54 -0
  32. package/packages/@monomind/cli/dist/src/ui/server.mjs +87 -12
  33. package/packages/@monomind/cli/package.json +3 -2
  34. package/packages/@monomind/cli/dist/src/consensus/vote-signer.d.ts +0 -36
  35. package/packages/@monomind/cli/dist/src/consensus/vote-signer.js +0 -88
@@ -0,0 +1,51 @@
1
+ // packages/@monomind/cli/src/orgrt/types.ts
2
+ import { z } from 'zod';
3
+ /** Per-role provider config. Default (absent) = subscription login of local Claude Code. */
4
+ export const ProviderSchema = z.object({
5
+ kind: z.enum(['subscription', 'api-key', 'base-url', 'bedrock', 'vertex']).default('subscription'),
6
+ /** env var NAME holding the API key (never the key itself) */
7
+ apiKeyEnv: z.string().optional(),
8
+ baseUrl: z.string().optional(),
9
+ /** env var NAME holding the auth token for base-url providers */
10
+ authTokenEnv: z.string().optional(),
11
+ }).strict();
12
+ export const RolePolicySchema = z.object({
13
+ allowTools: z.array(z.string()).optional(),
14
+ denyTools: z.array(z.string()).default([]),
15
+ /** glob patterns relative to org cwd */
16
+ fileWrite: z.array(z.string()).default(['**']),
17
+ fileRead: z.array(z.string()).default(['**']),
18
+ /** allowed domains for WebFetch/WebSearch; empty array = no web */
19
+ webAllow: z.array(z.string()).optional(),
20
+ maxTokens: z.number().int().positive().optional(),
21
+ }).partial().passthrough();
22
+ export const RoleSchema = z.object({
23
+ id: z.string().min(1),
24
+ title: z.string().default(''),
25
+ type: z.string().default('specialist'),
26
+ reports_to: z.string().nullable().default(null),
27
+ responsibilities: z.array(z.string()).default([]),
28
+ instructions_file: z.string().optional(),
29
+ adapter_config: z.object({
30
+ model: z.string().default('claude-sonnet-4-5'),
31
+ max_tokens: z.number().optional(),
32
+ }).partial().optional(),
33
+ provider: ProviderSchema.optional(),
34
+ policy: RolePolicySchema.optional(),
35
+ }).passthrough();
36
+ export const OrgDefSchema = z.object({
37
+ name: z.string().min(1),
38
+ goal: z.string().default(''),
39
+ status: z.string().default('stopped'),
40
+ schedule: z.union([z.string(), z.number(), z.null()]).default(null),
41
+ run_config: z.object({
42
+ max_concurrent_agents: z.number().int().positive().default(4),
43
+ budget_tokens: z.number().int().positive().default(1_000_000),
44
+ memory_namespace: z.string().optional(),
45
+ max_turns_per_message: z.number().int().positive().default(30),
46
+ }).partial().passthrough().default({})
47
+ .transform(rc => ({ max_concurrent_agents: 4, budget_tokens: 1_000_000, max_turns_per_message: 30, ...rc })),
48
+ roles: z.array(RoleSchema).min(1),
49
+ }).passthrough();
50
+ export const ORG_DIR = '.monomind/orgs';
51
+ //# sourceMappingURL=types.js.map
@@ -159,11 +159,13 @@ export class CommandParser {
159
159
  result.command.push(arg);
160
160
  // Check for subcommand (level 1)
161
161
  const cmd = this.commands.get(arg);
162
+ resolvedCmd = cmd;
162
163
  if (cmd?.subcommands && i + 1 < args.length) {
163
164
  const nextArg = args[i + 1];
164
165
  const subCmd = cmd.subcommands.find(sc => sc.name === nextArg || sc.aliases?.includes(nextArg));
165
166
  if (subCmd) {
166
167
  result.command.push(nextArg);
168
+ resolvedCmd = subCmd;
167
169
  i++;
168
170
  // Check for nested subcommand (level 2)
169
171
  if (subCmd.subcommands && i + 1 < args.length) {
@@ -171,6 +173,7 @@ export class CommandParser {
171
173
  const nestedCmd = subCmd.subcommands.find(sc => sc.name === nestedArg || sc.aliases?.includes(nestedArg));
172
174
  if (nestedCmd) {
173
175
  result.command.push(nestedArg);
176
+ resolvedCmd = nestedCmd;
174
177
  i++;
175
178
  // Check for deeply nested subcommand (level 3)
176
179
  if (nestedCmd.subcommands && i + 1 < args.length) {
@@ -178,6 +181,7 @@ export class CommandParser {
178
181
  const deepCmd = nestedCmd.subcommands.find(sc => sc.name === deepArg || sc.aliases?.includes(deepArg));
179
182
  if (deepCmd) {
180
183
  result.command.push(deepArg);
184
+ resolvedCmd = deepCmd;
181
185
  i++;
182
186
  }
183
187
  }
@@ -193,8 +197,10 @@ export class CommandParser {
193
197
  }
194
198
  i++;
195
199
  }
196
- // Apply defaults
197
- this.applyDefaults(result.flags);
200
+ // Apply defaults — the resolved (sub)command's own option definitions
201
+ // shadow same-name global options (e.g. `browse screenshot --format` is an
202
+ // image format, not the global text|json|table output format).
203
+ this.applyDefaults(result.flags, resolvedCmd);
198
204
  return result;
199
205
  }
200
206
  parseFlag(args, index, aliases, booleanFlags) {
@@ -398,10 +404,24 @@ export class CommandParser {
398
404
  }
399
405
  return flags;
400
406
  }
401
- applyDefaults(flags) {
407
+ applyDefaults(flags, resolvedCmd) {
408
+ // The resolved command's own options shadow same-name globals: apply the
409
+ // command's defaults and suppress the global default for those names.
410
+ const shadowed = new Set();
411
+ if (resolvedCmd?.options) {
412
+ for (const opt of resolvedCmd.options) {
413
+ const key = this.normalizeKey(opt.name);
414
+ shadowed.add(key);
415
+ if (flags[key] === undefined && opt.default !== undefined) {
416
+ flags[key] = opt.default;
417
+ }
418
+ }
419
+ }
402
420
  // Apply global option defaults
403
421
  for (const opt of this.globalOptions) {
404
422
  const key = this.normalizeKey(opt.name);
423
+ if (shadowed.has(key))
424
+ continue;
405
425
  if (flags[key] === undefined && opt.default !== undefined) {
406
426
  flags[key] = opt.default;
407
427
  }
@@ -418,10 +438,16 @@ export class CommandParser {
418
438
  }
419
439
  validateFlags(flags, command) {
420
440
  const errors = [];
421
- const allOptions = [...this.globalOptions];
441
+ // Command options shadow same-name globals — validate against the
442
+ // command's definition (its choices/validators), not the global's.
443
+ const byName = new Map();
444
+ for (const opt of this.globalOptions)
445
+ byName.set(opt.name, opt);
422
446
  if (command?.options) {
423
- allOptions.push(...command.options);
447
+ for (const opt of command.options)
448
+ byName.set(opt.name, opt);
424
449
  }
450
+ const allOptions = [...byName.values()];
425
451
  // Check required flags
426
452
  for (const opt of allOptions) {
427
453
  const key = this.normalizeKey(opt.name);
@@ -277,12 +277,17 @@ function collectAgents(projectDir) {
277
277
  // Single source-of-truth for all model pricing (canonical list from src/pricing/model-pricing.ts).
278
278
  // server.mjs imports _tokPrice and _tokCost from here instead of duplicating this table.
279
279
  const _TOK_PRICES = {
280
+ // Frontier (Fable/Mythos)
281
+ 'claude-fable-5': { in: 10e-6, out: 50e-6, cw: 12.5e-6, cr: 1e-6 },
282
+ 'claude-mythos-5': { in: 10e-6, out: 50e-6, cw: 12.5e-6, cr: 1e-6 },
280
283
  // Opus
281
284
  'claude-opus-4-8': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
285
+ 'claude-opus-4-7': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
282
286
  'claude-opus-4-6': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
283
287
  'claude-opus-4-5': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
284
288
  'claude-opus-4': { in: 15e-6, out: 75e-6, cw: 18.75e-6, cr: 1.5e-6 },
285
289
  // Sonnet
290
+ 'claude-sonnet-5': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
286
291
  'claude-sonnet-4-6': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
287
292
  'claude-sonnet-4-5': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
288
293
  'claude-sonnet-4': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
@@ -489,6 +494,15 @@ function collectMetrics(projectDir) {
489
494
  const avgConf = routingConfCount > 0 ? Math.round((routingConfSum / routingConfCount) * 100) : null;
490
495
  const topAgent = Object.entries(agentCounts).sort((a, b) => b[1] - a[1])[0];
491
496
 
497
+ // Worker freshness: stat the live worker output files so the frontend can
498
+ // render freshness pills. Shape: workers: [{name, exists, ageMs}]
499
+ const workerFiles = ['codebase-map', 'security-audit', 'performance', 'consolidation', 'ddd-progress'];
500
+ const now = Date.now();
501
+ const workers = workerFiles.map(name => {
502
+ const s = fileStat(path.join(base, 'metrics', name + '.json'));
503
+ return { name, exists: !!s, ageMs: s ? Math.max(0, now - s.mtimeMs) : null };
504
+ });
505
+
492
506
  return {
493
507
  routing: {
494
508
  total: routingTotal,
@@ -507,7 +521,9 @@ function collectMetrics(projectDir) {
507
521
  monthCost: tokenSummary.monthCost,
508
522
  monthCalls: tokenSummary.monthCalls,
509
523
  },
510
- security: readJSON(path.join(base, 'security', 'audit-status.json')) || {}
524
+ // Live worker output (audit worker) fields include riskLevel, recommendations
525
+ security: readJSON(path.join(base, 'metrics', 'security-audit.json')) || {},
526
+ workers,
511
527
  };
512
528
  }
513
529
 
@@ -554,43 +570,56 @@ function collectMemory(projectDir) {
554
570
  const d = path.resolve(projectDir);
555
571
  const monomindDir = path.join(d, '.monomind');
556
572
 
557
- // AgentDBcheck all known locations across different init styles
558
- const dbCandidates = [
559
- path.join(d, 'data', 'memory.db'), // custom --path ./data/memory.db
560
- path.join(d, '.swarm', 'memory.db'), // default init path
561
- path.join(monomindDir, 'memory.db'), // legacy .monomind path
562
- path.join(d, '.claude', 'memory.db'), // .claude dir (some setups)
563
- ];
564
- const dbHit = probeFile(...dbCandidates);
565
- const dbSize = dbHit ? dbHit.size : 0;
566
- const dbPath = dbHit ? dbHit.path : null;
567
-
568
- // HNSW — lives alongside the DB or at the .swarm default
569
- const hnswCandidates = dbPath ? [
570
- path.join(path.dirname(dbPath), 'memory.graph'), // alongside DB (hybrid backend)
571
- path.join(path.dirname(dbPath), 'hnsw.index'), // default init name
572
- ] : [
573
- path.join(d, 'data', 'memory.graph'),
574
- path.join(d, '.swarm', 'hnsw.index'),
575
- ];
576
- const hnswHit = probeFile(...hnswCandidates);
577
- const hnsw = !!hnswHit;
573
+ // Auto-memory pattern store array of pattern entries written by hooks
574
+ let patternCount = 0;
575
+ let patternsUpdated = null;
576
+ const storePath = path.join(monomindDir, 'data', 'auto-memory-store.json');
577
+ const store = readJSON(storePath);
578
+ if (Array.isArray(store)) {
579
+ patternCount = store.length;
580
+ for (const e of store) {
581
+ if (e && typeof e.ts === 'number' && (!patternsUpdated || e.ts > patternsUpdated)) patternsUpdated = e.ts;
582
+ }
583
+ }
584
+ if (!patternsUpdated) {
585
+ const s = fileStat(storePath);
586
+ if (s) patternsUpdated = s.mtimeMs;
587
+ }
578
588
 
579
- // MonoVector DB
580
- const monovectorHit = probeFile(
581
- path.join(monomindDir, 'data', 'monovector.db'),
582
- path.join(d, 'data', 'monovector.db'),
583
- );
584
- const monovectorSize = monovectorHit ? monovectorHit.size : 0;
585
- const monovectorExists = !!monovectorHit;
589
+ // Episodic memory — one JSON line per episode
590
+ let episodeCount = 0;
591
+ let lastEpisode = null;
592
+ try {
593
+ const raw = fs.readFileSync(path.join(monomindDir, 'episodic', 'episodes.jsonl'), 'utf8');
594
+ const lines = raw.split('\n').filter(Boolean);
595
+ episodeCount = lines.length;
596
+ if (lines.length) {
597
+ try {
598
+ const last = JSON.parse(lines[lines.length - 1]);
599
+ lastEpisode = last.ts || last.timestamp || null;
600
+ } catch {}
601
+ }
602
+ } catch {}
586
603
 
587
- let monovectorPatterns = 0;
588
- const ranked = readJSON(path.join(monomindDir, 'data', 'ranked-context.json'));
589
- if (ranked && ranked.entries) monovectorPatterns = ranked.entries.length;
604
+ // Last consolidation run (consolidate worker output)
605
+ let lastConsolidated = null;
606
+ const consolidation = readJSON(path.join(monomindDir, 'metrics', 'consolidation.json'));
607
+ if (consolidation && consolidation.timestamp) lastConsolidated = consolidation.timestamp;
590
608
 
591
609
  const files = collectMemoryFiles(projectDir);
592
610
 
593
- return { dbSize, dbPath, hnsw, monovectorSize, monovectorExists, monovectorPatterns, files, count: files.length };
611
+ return {
612
+ patternCount,
613
+ patternsUpdated,
614
+ episodeCount,
615
+ lastEpisode,
616
+ lastConsolidated,
617
+ files,
618
+ count: files.length,
619
+ // Legacy keys kept for frontend backward-safety (v1 backends no longer written in v2)
620
+ dbSize: 0, dbPath: null, hnsw: false,
621
+ monovectorSize: 0, monovectorExists: false, monovectorPatterns: 0,
622
+ };
594
623
  }
595
624
 
596
625
  function collectSystem() {
@@ -771,7 +800,10 @@ export function getWatchPaths(projectDir) {
771
800
  path.join(m, 'metrics', 'token-summary.json'),
772
801
  path.join(m, 'metrics', 'token-sessions.json'),
773
802
  path.join(m, 'metrics', 'ddd-progress.json'),
774
- path.join(m, 'metrics', 'learning.json'),
803
+ path.join(m, 'metrics', 'codebase-map.json'),
804
+ path.join(m, 'metrics', 'security-audit.json'),
805
+ path.join(m, 'metrics', 'performance.json'),
806
+ path.join(m, 'metrics', 'consolidation.json'),
775
807
  // Agents
776
808
  path.join(m, 'registry.json'),
777
809
  path.join(m, 'agents', 'registrations'),
@@ -782,17 +814,10 @@ export function getWatchPaths(projectDir) {
782
814
  // Knowledge
783
815
  path.join(m, 'knowledge', 'chunks.jsonl'),
784
816
  path.join(m, 'skills.jsonl'),
785
- // Security
786
- path.join(m, 'security', 'audit-status.json'),
787
- // Triggers & memory — watch all candidate locations
817
+ // Triggers & memory — live v2 sources
788
818
  path.join(m, 'trigger-index.json'),
789
- path.join(resolvedDir, 'data', 'memory.db'),
790
- path.join(resolvedDir, 'data', 'memory.graph'),
791
- path.join(resolvedDir, '.swarm', 'memory.db'),
792
- path.join(resolvedDir, '.swarm', 'hnsw.index'),
793
- path.join(m, 'memory.db'),
794
- path.join(m, 'data', 'monovector.db'),
795
- path.join(m, 'data', 'ranked-context.json'),
819
+ path.join(m, 'data', 'auto-memory-store.json'),
820
+ path.join(m, 'episodic', 'episodes.jsonl'),
796
821
  // Sessions
797
822
  path.join(c, 'sessions')
798
823
  ];
@@ -2663,6 +2663,7 @@ function switchProject(path) {
2663
2663
  // Reset Monograph cache so it reloads for the new project
2664
2664
  _mgLoaded = false;
2665
2665
  _mgGraph = null;
2666
+ window._mgRealTotals = null;
2666
2667
  document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
2667
2668
  document.getElementById('sb-path').textContent = path;
2668
2669
  _showNavProjectCtx(path);
@@ -3174,9 +3175,10 @@ async function loadStatusStrip() {
3174
3175
  const strip = document.getElementById('status-strip');
3175
3176
  if (!strip || !DIR) return;
3176
3177
  try {
3177
- const [statusRes, memRes] = await Promise.allSettled([
3178
+ const [statusRes, memRes, metricsRes] = await Promise.allSettled([
3178
3179
  apiFetch('/api/status?dir=' + enc(DIR)),
3179
3180
  apiFetch('/api/memory/stats?dir=' + enc(DIR)),
3181
+ apiFetch('/api/section?name=metrics&dir=' + enc(DIR)),
3180
3182
  ]);
3181
3183
  const checks = statusRes.status === 'fulfilled'
3182
3184
  ? (Array.isArray(statusRes.value) ? statusRes.value : (statusRes.value?.checks || []))
@@ -3193,15 +3195,27 @@ async function loadStatusStrip() {
3193
3195
  pills.push(`<span class="ss-pill ${cls}">${esc(c.name || c.label || c.key || '?')}</span>`);
3194
3196
  });
3195
3197
 
3196
- // HNSW status
3197
- const hnswOn = mem.hnsw === true || mem.hnswEnabled === true || mem.hnsw_enabled === true;
3198
- pills.push(`<span class="ss-pill ${hnswOn ? 'on' : ''}">HNSW ${hnswOn ? 'ON' : 'OFF'}</span>`);
3199
-
3200
3198
  // Patterns count
3201
3199
  if (mem.patterns != null) {
3202
3200
  pills.push(`<span class="ss-pill">PATTERNS ${Number(mem.patterns).toLocaleString()}</span>`);
3203
3201
  }
3204
3202
 
3203
+ // Worker freshness (metrics.workers = [{name, exists, ageMs}])
3204
+ const metricsSec = metricsRes.status === 'fulfilled'
3205
+ ? (metricsRes.value?.metrics || metricsRes.value || {})
3206
+ : {};
3207
+ const workers = Array.isArray(metricsSec.workers) ? metricsSec.workers : [];
3208
+ const fmtAge = ms => ms < 3600e3 ? Math.round(ms / 60e3) + 'm'
3209
+ : ms < 86400e3 ? Math.round(ms / 3600e3) + 'h'
3210
+ : Math.round(ms / 86400e3) + 'd';
3211
+ workers.forEach(w => {
3212
+ if (!w || !w.name) return;
3213
+ const fresh = w.exists && typeof w.ageMs === 'number' && w.ageMs < 12 * 3600e3;
3214
+ const cls = !w.exists ? '' : (fresh ? 'on' : 'warn');
3215
+ const ageTxt = !w.exists ? 'missing' : (typeof w.ageMs === 'number' ? fmtAge(w.ageMs) : '?');
3216
+ pills.push(`<span class="ss-pill ${cls}" title="worker ${esc(w.name)}: ${esc(ageTxt)}">⚙ ${esc(w.name)}</span>`);
3217
+ });
3218
+
3205
3219
  // Chunks count
3206
3220
  if (mem.chunks != null) {
3207
3221
  pills.push(`<span class="ss-pill">CHUNKS ${Number(mem.chunks).toLocaleString()}</span>`);
@@ -3506,8 +3520,20 @@ async function loadMemUsagePeriod(btn, period) {
3506
3520
  }
3507
3521
 
3508
3522
  const totalCost = typeof s.todayCost === 'number' ? s.todayCost : (typeof s.cost === 'number' ? s.cost : null);
3509
- const cacheEff = s.cacheTokens && s.totalTokensIn
3510
- ? Math.round(s.cacheTokens / s.totalTokensIn * 100) : null;
3523
+ // Cache efficiency = cacheRead / (cacheRead + input), capped at 100%.
3524
+ // (The old cacheTokens/totalTokensIn math exceeded 100% because cache
3525
+ // reads are not included in the input-token total.)
3526
+ const cacheEff = s.cacheTokens
3527
+ ? Math.min(100, Math.round(s.cacheTokens / (s.cacheTokens + (Number(s.totalTokensIn) || 0)) * 100))
3528
+ : null;
3529
+
3530
+ // Projects rows sometimes arrive without a usable name — fall back to
3531
+ // the directory basename, then 'unknown'.
3532
+ const projRows = projects.map(p => {
3533
+ const raw = (p.project && p.project !== '?') ? p.project : (p.name && p.name !== '?' ? p.name : '');
3534
+ const base = String(p.dir || p.path || '').split('/').filter(Boolean).pop() || '';
3535
+ return Object.assign({}, p, { project: raw || base || 'unknown' });
3536
+ });
3511
3537
 
3512
3538
  content.innerHTML = `
3513
3539
  <!-- Overview stats -->
@@ -3536,8 +3562,8 @@ async function loadMemUsagePeriod(btn, period) {
3536
3562
  <div style="margin-bottom:14px">${barChart(mcps, 'count', 'server', 'oklch(70% 0.18 60)', 8)}</div>` : ''}
3537
3563
 
3538
3564
  <!-- Projects -->
3539
- ${projects.length ? `<div class="m-group-title" style="margin-bottom:6px">Projects</div>
3540
- <div style="margin-bottom:14px">${barChart(projects, 'cost', 'project', 'oklch(65% 0.15 150)', 5)}</div>` : ''}
3565
+ ${projRows.length ? `<div class="m-group-title" style="margin-bottom:6px">Projects</div>
3566
+ <div style="margin-bottom:14px">${barChart(projRows, 'cost', 'project', 'oklch(65% 0.15 150)', 5)}</div>` : ''}
3541
3567
 
3542
3568
  <!-- Fallback: session breakdown table if no breakdown data -->
3543
3569
  ${!models.length && !tools.length && rows.length ? `
@@ -6274,7 +6300,6 @@ const _v2AvatarKnown = new Set([
6274
6300
  'quorum-manager','consensus-coordinator','perf-analyzer','benchmarker',
6275
6301
  'task-orchestrator','memory-coordinator','load-balancer','resource-allocator',
6276
6302
  'pr-manager','code-review-swarm','issue-tracker','release-manager','repo-architect',
6277
- 'workflow-automation','sparc-coord','sparc-coder','specification','pseudocode',
6278
6303
  'architecture','refinement','backend-dev','frontend-developer','mobile-dev',
6279
6304
  'ml-developer','cicd-engineer','system-architect','ai-engineer','model-qa',
6280
6305
  'data-engineer','analytics-reporter','experiment-tracker','data-consolidator',
@@ -8227,7 +8252,7 @@ function v2RenderOrgConfig() {
8227
8252
  const rc = d.run_config || {};
8228
8253
  const gov = (d.governance && typeof d.governance === 'object') ? d.governance : { policy: d.governance || 'auto' };
8229
8254
  const topos = ['hierarchical','hierarchical-mesh','mesh','star','ring','adaptive','hybrid'];
8230
- const modes = ['daemon','once','scheduled'];
8255
+ const modes = [{ v: 'daemon', l: 'persistent (daemon)' }, { v: 'once', l: 'once' }, { v: 'scheduled', l: 'scheduled' }];
8231
8256
  const statuses = ['active','paused','archived'];
8232
8257
  const govPolicies = ['auto','board','strict'];
8233
8258
  const inp = (id,val,type,extra) => '<input class="filter-input" id="'+id+'" type="'+(type||'text')+'" value="'+esc(String(val??''))+'" '+(extra||'')+' style="width:100%;box-sizing:border-box">';
@@ -8239,7 +8264,7 @@ function v2RenderOrgConfig() {
8239
8264
  fld('Name', inp('oc-name', d.name||'','text','readonly style="opacity:0.5"'))
8240
8265
  +fld('Status', sel('oc-status', statuses, d.status||'active'))
8241
8266
  +'<div style="grid-column:1/-1">'+fld('Goal', '<textarea id="oc-goal" class="filter-input" rows="3" style="width:100%;box-sizing:border-box;resize:vertical;line-height:1.5">'+esc(d.goal||'')+'</textarea>')+'</div>'
8242
- +fld('Mode', sel('oc-mode', modes, d.mode||'daemon'))
8267
+ +fld('Mode', '<select class="filter-input" id="oc-mode" style="width:100%;box-sizing:border-box;cursor:pointer">'+modes.map(o=>'<option value="'+esc(o.v)+'"'+((d.mode||'daemon')===o.v?' selected':'')+'>'+esc(o.l)+'</option>').join('')+'</select>')
8243
8268
  +fld('Topology', sel('oc-topology', topos, d.topology||'hierarchical'))
8244
8269
  +'<div style="grid-column:1/-1">'+fld('Schedule', inp('oc-schedule', d.schedule||''))+'</div>'
8245
8270
  )
@@ -9144,7 +9169,6 @@ const _MASTERMIND_SKILLS = [
9144
9169
  '/monomind:repeat','monomind:review','monomind:understand','monomind:adr',
9145
9170
  '/monomind:budget','monomind:graph-status','monomind:loops','monomind:swarm',
9146
9171
  '/swarm:development','swarm:analysis','swarm:testing','swarm:optimization',
9147
- '/sparc:architect','sparc:code','sparc:tdd','sparc:reviewer','sparc:security-review',
9148
9172
  '/github:pr-manager','github:issue-tracker','github:release-manager',
9149
9173
  ];
9150
9174
  window._allSkills = _MASTERMIND_SKILLS;
@@ -11871,8 +11895,16 @@ function renderMgOverview() {
11871
11895
  const E = g.edges.length;
11872
11896
  const avgDeg = N > 0 ? ((2 * E) / N).toFixed(1) : '0';
11873
11897
  const typeSet = new Set(g.nodes.map(n => n.type || n.kind || 'unknown'));
11874
- document.getElementById('mg-stat-nodes').textContent = N;
11875
- document.getElementById('mg-stat-edges').textContent = E;
11898
+ // The graph fetch is capped at 2000 nodes prefer real totals from the
11899
+ // monograph report (set by mgLoadReport) and relabel honestly when capped.
11900
+ const _capped = N >= 2000 || E >= 2000;
11901
+ const _rt = window._mgRealTotals || null;
11902
+ const _nEl = document.getElementById('mg-stat-nodes');
11903
+ const _eEl = document.getElementById('mg-stat-edges');
11904
+ _nEl.textContent = (_rt && _rt.nodes) ? Number(_rt.nodes).toLocaleString() : N;
11905
+ _eEl.textContent = (_rt && _rt.edges) ? Number(_rt.edges).toLocaleString() : E;
11906
+ if (_nEl.previousElementSibling) _nEl.previousElementSibling.textContent = ((_rt && _rt.nodes) || !_capped) ? 'Nodes' : 'Nodes (top 2000)';
11907
+ if (_eEl.previousElementSibling) _eEl.previousElementSibling.textContent = ((_rt && _rt.edges) || !_capped) ? 'Edges' : 'Edges (top 2000)';
11876
11908
  document.getElementById('mg-stat-avgdeg').textContent = avgDeg;
11877
11909
  document.getElementById('mg-stat-types').textContent = typeSet.size;
11878
11910
 
@@ -11918,7 +11950,14 @@ function renderMgOverview() {
11918
11950
  else {
11919
11951
  const show = dead.slice(0, 12);
11920
11952
  deadEl.innerHTML = `<table class="mg-table"><thead><tr><th>Name</th><th>Type</th></tr></thead><tbody>` +
11921
- show.map(n => `<tr><td title="${esc(n.id||n.name||n.label)}">${esc(mgShortLabel(n.label||n.name||n.id||''))}</td><td>${esc(n.type||n.kind||'')}</td></tr>`).join('') +
11953
+ show.map(n => {
11954
+ // Prefer a real name; if the label is just a generic type word
11955
+ // (e.g. "function"), fall back to the qualified id/path instead.
11956
+ const raw = n.label || n.name || '';
11957
+ const generic = !raw || /^(function|method|class|node|unknown|anonymous)$/i.test(String(raw).trim());
11958
+ const display = generic ? (n.id || n.name || raw || '') : raw;
11959
+ return `<tr><td title="${esc(n.id||n.name||n.label)}">${esc(mgShortLabel(display))}</td><td>${esc(n.type||n.kind||'')}</td></tr>`;
11960
+ }).join('') +
11922
11961
  (dead.length > 12 ? `<tr><td colspan="2" style="color:var(--text-xs);font-size:11px">…and ${dead.length-12} more</td></tr>` : '') +
11923
11962
  `</tbody></table>`;
11924
11963
  }
@@ -12820,6 +12859,18 @@ async function mgLoadReport() {
12820
12859
  // Extract markdown from JSON envelope {exists, report, stats}
12821
12860
  const markdown = (data && data.report) ? data.report : (typeof data === 'string' ? data : JSON.stringify(data, null, 2));
12822
12861
  const stats = (data && data.stats) || {};
12862
+ // Real graph totals — the overview cards otherwise show the capped fetch size
12863
+ const _mdN = (String(markdown).match(/\*\*Nodes\*\*:\s*([\d,]+)/i) || [])[1];
12864
+ const _mdE = (String(markdown).match(/\*\*Edges\*\*:\s*([\d,]+)/i) || [])[1];
12865
+ const _totN = stats.nodes || (_mdN ? Number(_mdN.replace(/,/g, '')) : null);
12866
+ const _totE = stats.edges || (_mdE ? Number(_mdE.replace(/,/g, '')) : null);
12867
+ if (_totN || _totE) {
12868
+ window._mgRealTotals = { nodes: _totN, edges: _totE };
12869
+ const nEl = document.getElementById('mg-stat-nodes');
12870
+ const eEl = document.getElementById('mg-stat-edges');
12871
+ if (_totN && nEl) { nEl.textContent = Number(_totN).toLocaleString(); if (nEl.previousElementSibling) nEl.previousElementSibling.textContent = 'Nodes'; }
12872
+ if (_totE && eEl) { eEl.textContent = Number(_totE).toLocaleString(); if (eEl.previousElementSibling) eEl.previousElementSibling.textContent = 'Edges'; }
12873
+ }
12823
12874
  mgRenderReport(markdown, el, stats);
12824
12875
  } catch (err) {
12825
12876
  el.innerHTML = `<div style="color:var(--red);font-size:12px">Error: ${esc(String(err))}</div>`;
@@ -13156,7 +13207,7 @@ function mgWikiFindRelated(nodeId) {
13156
13207
  }
13157
13208
 
13158
13209
  function mgWikiRefresh() {
13159
- _mgLoaded = false; _mgGraph = null;
13210
+ _mgLoaded = false; _mgGraph = null; window._mgRealTotals = null;
13160
13211
  document.getElementById('mg-wiki-list').innerHTML = '<div class="loading-txt">Loading…</div>';
13161
13212
  loadMonograph();
13162
13213
  }
@@ -13383,7 +13434,7 @@ async function selectSwarmRun(idx) {
13383
13434
  detail.innerHTML =
13384
13435
  '<div style="margin-bottom:10px">' +
13385
13436
  '<div style="font-size:13px;font-weight:600;color:var(--text-hi)">' + esc((run.swarmId || run.id || '—').toString().slice(0, 14)) + '</div>' +
13386
- '<div style="font-size:11px;color:var(--text-lo);margin-top:3px">' + esc(run.topology || '—') + ' · ' + esc(run.consensus || '—') + ' · ' + (run.agentCount || 0) + ' agents</div>' +
13437
+ '<div style="font-size:11px;color:var(--text-lo);margin-top:3px">' + esc(run.topology || '—') + ' · ' + (run.consensus ? esc(run.consensus) + ' vote-threshold' : '—') + ' · ' + (run.agentCount || 0) + ' agents</div>' +
13387
13438
  '</div>' +
13388
13439
  '<canvas id="swarm-topo-canvas" style="width:100%;max-width:380px;height:190px;display:block;border:1px solid var(--border);border-radius:6px;margin-bottom:12px"></canvas>' +
13389
13440
  '<div id="swarm-agent-list" style="margin-bottom:10px"></div>' +
@@ -13545,25 +13596,40 @@ function _renderChunks(list) {
13545
13596
  grid.innerHTML = '<div class="empty">No chunks indexed.<br><span style="font-size:11px;color:var(--text-xs)">Run /monomind:understand to build the index.</span></div>';
13546
13597
  return;
13547
13598
  }
13548
- grid.innerHTML = list.slice(0, 200).map(c => {
13599
+ // Keep the rendered slice around so button handlers can look chunks up by
13600
+ // index — interpolating chunk content into onclick attributes broke (and
13601
+ // leaked markup) whenever the text contained quotes.
13602
+ const shown = list.slice(0, 200);
13603
+ window._chunksShown = shown;
13604
+ grid.innerHTML = shown.map((c, i) => {
13549
13605
  const src = (c.source || c.file || '').split('/').slice(-2).join('/');
13550
13606
  const excerpt = (c.content || c.text || c.body || '').slice(0, 220);
13551
13607
  const ns = c.namespace || c.type || '';
13552
- const chunkId = JSON.stringify(c.id || c.path || c.source || '');
13553
- const chunkContent = JSON.stringify(c.content || c.text || c.body || '');
13554
- const chunkSrc = JSON.stringify(src);
13555
13608
  return '<div class="chunk-card" data-search="' + esc((src + ' ' + excerpt + ' ' + ns).toLowerCase()) + '">' +
13556
13609
  '<div class="chunk-src">' + esc(src || '—') + '</div>' +
13557
13610
  '<div class="chunk-excerpt">' + esc(excerpt) + '</div>' +
13558
13611
  '<div class="chunk-footer">' +
13559
13612
  (ns ? '<span class="chunk-ns">' + esc(ns) + '</span>' : '') +
13560
- '<button class="btn" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEdit(' + chunkId + ',' + chunkContent + ',' + chunkSrc + ')">✎ Edit</button>' +
13561
- '<button class="btn" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunk(' + chunkId + ')">✕</button>' +
13613
+ '<button class="btn" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEditAt(' + i + ')">✎ Edit</button>' +
13614
+ '<button class="btn" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunkAt(' + i + ')">✕</button>' +
13562
13615
  '</div>' +
13563
13616
  '</div>';
13564
13617
  }).join('');
13565
13618
  }
13566
13619
 
13620
+ function openChunkEditAt(i) {
13621
+ const c = (window._chunksShown || [])[i];
13622
+ if (!c) return;
13623
+ const src = (c.source || c.file || '').split('/').slice(-2).join('/');
13624
+ openChunkEdit(c.id || c.path || c.source || '', c.content || c.text || c.body || '', src);
13625
+ }
13626
+
13627
+ function deleteChunkAt(i) {
13628
+ const c = (window._chunksShown || [])[i];
13629
+ if (!c) return;
13630
+ deleteChunk(c.id || c.path || c.source || '');
13631
+ }
13632
+
13567
13633
  function openChunkEdit(id, content, srcLabel) {
13568
13634
  _editChunkId = id;
13569
13635
  document.getElementById('chunk-modal-title').textContent = 'Edit Chunk';
@@ -13665,7 +13731,28 @@ async function loadAgentGraphTab() {
13665
13731
  if (bar) bar.innerHTML = '<div class="loading-txt">Loading…</div>';
13666
13732
  try {
13667
13733
  const data = await apiFetch('/api/graph?dir=' + enc(DIR));
13668
- _agData = data;
13734
+ // /api/graph returns {nodes, edges} — session nodes + agenttype nodes.
13735
+ // Transform into the summary/sessions shape this tab renders.
13736
+ const nodes = Array.isArray(data && data.nodes) ? data.nodes : [];
13737
+ const sessNodes = nodes.filter(n => n.type === 'session').sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
13738
+ const agNodes = nodes.filter(n => n.type === 'agenttype');
13739
+ const sessions = sessNodes.map(n => ({
13740
+ id: n.id,
13741
+ turns: n.turns || 0,
13742
+ spawnCount: Object.values(n.agentSpawns || {}).reduce((a, b) => a + b, 0),
13743
+ toolCount: n.totalTools || 0,
13744
+ tools: n.toolCounts || {},
13745
+ agentTypes: n.agentSpawns || {},
13746
+ cost: n.cost,
13747
+ }));
13748
+ _agData = {
13749
+ sessions,
13750
+ sessionCount: sessions.length,
13751
+ agentTypes: agNodes.length,
13752
+ totalSpawns: agNodes.reduce((a, n) => a + (n.totalSpawns || 0), 0),
13753
+ totalToolCalls: sessions.reduce((a, s) => a + (s.toolCount || 0), 0),
13754
+ totalCost: sessions.reduce((a, s) => a + (Number(s.cost) || 0), 0),
13755
+ };
13669
13756
  _renderAgSummary();
13670
13757
  _renderAgSessList();
13671
13758
  } catch (e) {
@@ -841,6 +841,15 @@ html, body {
841
841
  <div class="meta-cell-value" id="meta-created" style="font-size:10px;color:var(--muted)">—</div>
842
842
  </div>
843
843
  </div>
844
+
845
+ <!-- Audit Coverage card (populated from /api/org-coverage; hidden when no coverage.json) -->
846
+ <div id="audit-coverage-card" style="display:none; background:var(--bg-panel); border:1px solid var(--border); border-radius:3px; padding:10px 14px;">
847
+ <div class="meta-cell-label" style="display:flex; align-items:center; gap:8px;">
848
+ AUDIT COVERAGE
849
+ <span id="acov-summary" style="letter-spacing:1px; color:var(--muted)"></span>
850
+ </div>
851
+ <div id="acov-areas" style="display:flex; flex-wrap:wrap; gap:6px; margin-top:2px;"></div>
852
+ </div>
844
853
  <div id="chart-svg-wrap">
845
854
  <svg id="org-chart-svg" viewBox="0 0 720 320">
846
855
  <defs>
@@ -1203,6 +1212,51 @@ async function selectOrg(name) {
1203
1212
  }
1204
1213
 
1205
1214
  renderTab(currentTab);
1215
+ loadAuditCoverage(listOrg);
1216
+ }
1217
+
1218
+ // ── Audit Coverage card (reads .monomind/audit/coverage.json via /api/org-coverage) ──
1219
+ async function loadAuditCoverage(listOrg) {
1220
+ const card = document.getElementById('audit-coverage-card');
1221
+ card.style.display = 'none';
1222
+ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
1223
+ try {
1224
+ const dir = listOrg.projectDir || window._orgDir;
1225
+ const url = '/api/org-coverage' + (dir ? `?dir=${encodeURIComponent(dir)}` : '');
1226
+ const r = await fetch(url);
1227
+ if (!r.ok) return; // 404 = no coverage.json — keep card hidden
1228
+ const cov = await r.json();
1229
+ const areas = cov.areas && typeof cov.areas === 'object' ? cov.areas : {};
1230
+ const names = Object.keys(areas);
1231
+ if (!names.length) return;
1232
+
1233
+ const summ = [];
1234
+ if (cov.areas_converged != null && cov.areas_total != null) summ.push(`${cov.areas_converged}/${cov.areas_total} CONVERGED`);
1235
+ if (cov.current_area) summ.push(`NOW: ${String(cov.current_area).toUpperCase()}`);
1236
+ document.getElementById('acov-summary').textContent = summ.join(' · ');
1237
+
1238
+ const verdictColor = v => {
1239
+ const s = String(v || '').toUpperCase();
1240
+ if (s.includes('CONVERGE') || s.includes('KEEP') || s.includes('PASS') || s.includes('DONE')) return 'var(--green, oklch(68% 0.2 150))';
1241
+ if (s.includes('ITERATE') || s.includes('WARN')) return 'oklch(78% 0.18 80)';
1242
+ if (s.includes('FAIL') || s.includes('REJECT')) return 'var(--red, oklch(62% 0.22 25))';
1243
+ return 'var(--muted, oklch(55% 0.02 186))';
1244
+ };
1245
+ document.getElementById('acov-areas').innerHTML = names.map(name => {
1246
+ const a = areas[name] || {};
1247
+ const verdict = a.verdict || a.status || '—';
1248
+ const value = a.value_delivered || a.valueDelivered || null;
1249
+ const c = verdictColor(verdict);
1250
+ return `<span style="display:inline-flex; align-items:center; gap:5px; font-size:9px; font-family:var(--mono);
1251
+ border:1px solid var(--border); border-radius:3px; padding:2px 7px; color:var(--text);"
1252
+ title="${esc(value || verdict)}">
1253
+ ${esc(name)}
1254
+ <span style="color:${c}; letter-spacing:1px;">${esc(String(verdict).toUpperCase())}</span>
1255
+ ${value ? `<span style="color:var(--dim); max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${esc(value)}</span>` : ''}
1256
+ </span>`;
1257
+ }).join('');
1258
+ card.style.display = '';
1259
+ } catch (_) { /* keep hidden */ }
1206
1260
  }
1207
1261
 
1208
1262
  function updateHeader(listOrg, data) {