@monoes/monomindcli 1.12.0 → 1.14.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 (61) hide show
  1. package/.claude/agents/generated/churn-analyst.md +53 -0
  2. package/.claude/agents/generated/code-reviewer.md +55 -0
  3. package/.claude/agents/generated/code-validator.md +57 -0
  4. package/.claude/agents/generated/complexity-scanner.md +56 -0
  5. package/.claude/agents/generated/devbot-orchestrator.md +58 -0
  6. package/.claude/agents/generated/devbot-planner.md +63 -0
  7. package/.claude/agents/generated/impact-assessor.md +54 -0
  8. package/.claude/commands/mastermind/master.md +88 -24
  9. package/.claude/helpers/control-start.cjs +60 -1
  10. package/.claude/helpers/event-logger.cjs +43 -2
  11. package/.claude/helpers/handlers/capture-handler.cjs +336 -0
  12. package/.claude/helpers/handlers/route-handler.cjs +20 -13
  13. package/.claude/helpers/handlers/session-restore-handler.cjs +14 -8
  14. package/.claude/helpers/hook-handler.cjs +57 -1
  15. package/.claude/helpers/intelligence.cjs +129 -57
  16. package/.claude/helpers/memory-palace.cjs +461 -0
  17. package/.claude/helpers/memory.cjs +134 -15
  18. package/.claude/helpers/metrics-db.mjs +87 -0
  19. package/.claude/helpers/router.cjs +296 -41
  20. package/.claude/helpers/session.cjs +107 -32
  21. package/.claude/helpers/statusline.cjs +138 -2
  22. package/.claude/helpers/toggle-statusline.cjs +73 -0
  23. package/.claude/helpers/token-tracker.cjs +934 -0
  24. package/.claude/helpers/utils/monograph.cjs +39 -4
  25. package/.claude/helpers/utils/telemetry.cjs +3 -3
  26. package/.claude/skills/mastermind/createorg.md +227 -16
  27. package/.claude/skills/mastermind/idea.md +15 -3
  28. package/.claude/skills/mastermind/runorg.md +2 -1
  29. package/dist/src/commands/doctor.d.ts.map +1 -1
  30. package/dist/src/commands/doctor.js +96 -4
  31. package/dist/src/commands/doctor.js.map +1 -1
  32. package/dist/src/commands/index.js +2 -0
  33. package/dist/src/commands/org.d.ts +4 -0
  34. package/dist/src/commands/org.d.ts.map +1 -0
  35. package/dist/src/commands/org.js +93 -0
  36. package/dist/src/commands/org.js.map +1 -0
  37. package/dist/src/mcp-tools/memory-tools.js +6 -6
  38. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  39. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  40. package/dist/src/mcp-tools/monograph-tools.js +329 -37
  41. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  42. package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
  43. package/dist/src/mcp-tools/session-tools.js +9 -10
  44. package/dist/src/mcp-tools/session-tools.js.map +1 -1
  45. package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
  46. package/dist/src/mcp-tools/task-tools.js +7 -8
  47. package/dist/src/mcp-tools/task-tools.js.map +1 -1
  48. package/dist/src/mcp-tools/types.d.ts +1 -0
  49. package/dist/src/mcp-tools/types.d.ts.map +1 -1
  50. package/dist/src/mcp-tools/types.js +49 -0
  51. package/dist/src/mcp-tools/types.js.map +1 -1
  52. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  53. package/dist/src/services/worker-daemon.js +295 -5
  54. package/dist/src/services/worker-daemon.js.map +1 -1
  55. package/dist/src/transfer/serialization/cfp.js +1 -1
  56. package/dist/src/transfer/serialization/cfp.js.map +1 -1
  57. package/dist/src/ui/dashboard.html +2235 -178
  58. package/dist/src/ui/orgs.html +1 -0
  59. package/dist/src/ui/server.mjs +532 -133
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +1 -1
@@ -25,6 +25,30 @@ function _requireMonograph() {
25
25
  // Memoized at module scope — opening monograph.db can take 7-10s.
26
26
  // Callers MUST NOT close the returned handle.
27
27
  var _cachedMonographDb = undefined;
28
+
29
+ // LRU cache for getMonographSuggestions: avoids re-querying the DB for
30
+ // the same task text within a single hook execution process lifetime.
31
+ // Max 20 entries; evicts the least-recently-used on overflow.
32
+ var _suggestCache = { _map: Object.create(null), _order: [], _max: 20 };
33
+ function _suggestCacheGet(key) {
34
+ if (key in _suggestCache._map) {
35
+ // Move to end (most recently used)
36
+ var idx = _suggestCache._order.indexOf(key);
37
+ if (idx !== -1) { _suggestCache._order.splice(idx, 1); _suggestCache._order.push(key); }
38
+ return _suggestCache._map[key];
39
+ }
40
+ return undefined;
41
+ }
42
+ function _suggestCacheSet(key, value) {
43
+ if (!(key in _suggestCache._map)) {
44
+ if (_suggestCache._order.length >= _suggestCache._max) {
45
+ var evict = _suggestCache._order.shift();
46
+ delete _suggestCache._map[evict];
47
+ }
48
+ _suggestCache._order.push(key);
49
+ }
50
+ _suggestCache._map[key] = value;
51
+ }
28
52
  function _openMonographDb() {
29
53
  if (_cachedMonographDb !== undefined) return _cachedMonographDb;
30
54
  try {
@@ -39,6 +63,10 @@ function _openMonographDb() {
39
63
 
40
64
  function getMonographSuggestions(taskText, limit) {
41
65
  if (!taskText || typeof taskText !== 'string') return [];
66
+ // Fast path: return cached result for repeated identical queries.
67
+ var cacheKey = taskText.slice(0, 200) + '|' + (limit || 5);
68
+ var cached = _suggestCacheGet(cacheKey);
69
+ if (cached !== undefined) return cached;
42
70
  var db = _openMonographDb();
43
71
  if (!db) return [];
44
72
  try {
@@ -56,7 +84,7 @@ function getMonographSuggestions(taskText, limit) {
56
84
  var rows = [];
57
85
  try {
58
86
  rows = db.prepare(
59
- 'SELECT n.id, n.name, n.label, n.file_path AS file, ' +
87
+ 'SELECT n.id, n.name, n.label, n.file_path AS file, n.start_line AS startLine, ' +
60
88
  'bm25(nodes_fts) AS bm25_score, ' +
61
89
  '(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg, ' +
62
90
  'CASE n.label WHEN \'File\' THEN 3 WHEN \'Function\' THEN 3 WHEN \'Class\' THEN 3 ' +
@@ -72,7 +100,7 @@ function getMonographSuggestions(taskText, limit) {
72
100
  var likeFrag = keys.map(function(){ return 'lower(n.name) LIKE ?'; }).join(' OR ');
73
101
  var likeArgs = keys.map(function(k){ return '%' + k + '%'; });
74
102
  var stmt = db.prepare(
75
- 'SELECT n.id, n.name, n.label, n.file_path AS file, ' +
103
+ 'SELECT n.id, n.name, n.label, n.file_path AS file, n.start_line AS startLine, ' +
76
104
  '(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg ' +
77
105
  'FROM nodes n WHERE (' + likeFrag + ') AND n.file_path IS NOT NULL AND n.file_path != \'\' ' +
78
106
  'AND n.label NOT IN (\'Concept\') ' +
@@ -80,7 +108,9 @@ function getMonographSuggestions(taskText, limit) {
80
108
  );
81
109
  rows = stmt.all.apply(stmt, likeArgs.concat([lim]));
82
110
  }
83
- return rows || [];
111
+ var result = rows || [];
112
+ _suggestCacheSet(cacheKey, result);
113
+ return result;
84
114
  } catch (e) { return []; }
85
115
  finally { /* db is shared/cached; do not close */ }
86
116
  }
@@ -287,8 +317,13 @@ function injectGodNodesContext(CWD) {
287
317
  // Staleness indicator: compare stored commit hash with current HEAD.
288
318
  var staleIndicator = '';
289
319
  try {
320
+ // The orchestrator writes 'last_commit_hash'; fall back to legacy keys.
290
321
  var lastCommitRow = null;
291
- try { lastCommitRow = db.prepare("SELECT value FROM index_meta WHERE key='ua_last_commit'").get(); } catch (_) {}
322
+ try {
323
+ lastCommitRow = db.prepare("SELECT value FROM index_meta WHERE key='last_commit_hash'").get() ||
324
+ db.prepare("SELECT value FROM index_meta WHERE key='lastCommit'").get() ||
325
+ db.prepare("SELECT value FROM index_meta WHERE key='ua_last_commit'").get();
326
+ } catch (_) {}
292
327
  if (lastCommitRow && lastCommitRow.value) {
293
328
  var { execFileSync: execSync } = require('child_process');
294
329
  var currentHead = '';
@@ -10,14 +10,14 @@ const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
10
10
  function _recordRecentEdit(filePath) {
11
11
  if (!filePath) return;
12
12
  try {
13
- var relPath = path.isAbsolute(filePath) ? path.relative(CWD, filePath) : filePath;
13
+ var storedPath = filePath;
14
14
  var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
15
15
  fs.mkdirSync(path.dirname(f), { recursive: true });
16
16
  var d = { edits: [] };
17
17
  try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
18
18
  if (!Array.isArray(d.edits)) d.edits = [];
19
- d.edits = d.edits.filter(function(e) { return e.file !== relPath; });
20
- d.edits.unshift({ file: relPath, editedAt: Date.now() });
19
+ d.edits = d.edits.filter(function(e) { return e.file !== storedPath; });
20
+ d.edits.unshift({ file: storedPath, editedAt: Date.now() });
21
21
  if (d.edits.length > 10) d.edits = d.edits.slice(0, 10);
22
22
  fs.writeFileSync(f, JSON.stringify(d));
23
23
  } catch (e) { /* non-fatal */ }
@@ -67,12 +67,14 @@ Reject any `org_name` that does not match `^[a-z0-9][a-z0-9-]{0,63}$`.
67
67
 
68
68
  Parse `roles_desc` (if provided) into a list of role titles. If not provided, derive a set of roles from `prompt` by identifying the human functions needed to achieve the goal.
69
69
 
70
- **Required roles to always include** (if the prompt implies a team):
70
+ **Required roles to always include** (if the prompt implies a team — **skip this rule for persona-based orgs** where the characters themselves define the structure; do not inject a generic coordinator into a celebrity panel):
71
71
  - A coordinator/boss role that owns the goal and makes final decisions
72
72
  - At least one executor role that does the primary work
73
73
  - A reviewer or QA role if quality output is implied
74
74
  - A communication layer (middle manager) if team size ≥ 4
75
75
 
76
+ > ⚠ **Check Step 2.3 (Persona / Character Detection) BEFORE applying this table.** If the org is persona-based (roles are named real people, celebrities, or fictional characters), skip this table for those roles and follow Step 2.3 instead.
77
+
76
78
  **Role → Agent Type mapping table** (use exact `subagent_type` slug for Task tool):
77
79
 
78
80
  | User role keyword | Agent type slug | Specialty |
@@ -92,12 +94,56 @@ Parse `roles_desc` (if provided) into a list of role titles. If not provided, de
92
94
 
93
95
  If a role doesn't match any keyword **and the org's domain is far from software** (legal, medical, finance, creative, etc.), do NOT force a mismatched generic type whose instructions are about the wrong domain (e.g. a court reporter mapped to the code `reviewer`). Instead coin a role-specific `agent_type` slug from the role title (slugify: `Court Reporter` → `court-reporter`, `Prosecutor` → `prosecutor`) and generate a fitting definition for it in Step 2.5. Only fall back to `general-purpose` when no sensible slug applies.
94
96
 
97
+ **For technical/engineering orgs** (DevBot, code quality, CI/CD, data pipelines, etc.), coin a precise domain slug for every specialized role rather than forcing it into a generic category:
98
+ - `Churn Analyst` → `churn-analyst` (git churn analysis, not a financial analyst)
99
+ - `Complexity Scanner` → `complexity-scanner` (static analysis, cyclomatic complexity)
100
+ - `Impact Assessor` → `impact-assessor` (code change blast-radius scoring)
101
+ - `Validator` → `code-validator` (applies patches, runs tests, enforces kill switch)
102
+ - `Orchestrator` (in a devbot) → `devbot-orchestrator` (4-phase pipeline boss, not generic coordinator)
103
+
104
+ The coined slug + a generated definition at `.claude/agents/generated/<slug>.md` is always better than a generic type whose system prompt talks about a completely different job.
105
+
106
+ ---
107
+
108
+ ## Step 2.3 — Persona / Character Detection (run BEFORE the mapping table above)
109
+
110
+ **Before applying the mapping table above, detect whether this org is persona-based:**
111
+
112
+ A role is **persona-based** if its title is:
113
+ - A named real person (e.g. "Donald Trump", "Elon Musk", "Steve Jobs")
114
+ - A well-known fictional character (e.g. "Sherlock Holmes", "Tony Stark")
115
+ - A celebrity, historical figure, or public persona referred to by name
116
+
117
+ An org is **persona-based** if ≥ 50% of its roles are character names, OR the goal/prompt contains keywords like: `panel`, `debate`, `simulation`, `roleplay`, `celebrity`, `character`, `virtual [name]`, `impersonate`, `as [name]`.
118
+
119
+ **If persona-based:**
120
+
121
+ 1. **Ignore the mapping table entirely for these roles.** Do NOT map "Donald Trump" → `coder`, "Elon Musk" → `researcher`, etc.
122
+
123
+ 2. **Coin a character-specific `agent_type` slug** from the character's name:
124
+ - `Donald Trump` → `donald-trump`
125
+ - `Sherlock Holmes` → `sherlock-holmes`
126
+ - `Steve Jobs` → `steve-jobs`
127
+
128
+ 3. **Derive skills and expertise from what is publicly known about that person**, not from generic software roles. Use your knowledge of the person's career, public persona, communication style, known positions, and characteristic behaviors. For example:
129
+ - Donald Trump: negotiation, real-estate dealmaking, brand promotion, confrontational rhetoric, media manipulation, self-promotion, political populism
130
+ - Elon Musk: first-principles engineering, disruptive product vision, risk-taking, rapid iteration, social media provocation, space/EV technology
131
+ - A fictional detective: deductive reasoning, observation, criminal psychology, pattern recognition
132
+
133
+ 4. **The generated agent definition** (written in Step 2.5) must read as that character — their voice, stance, known views, communication style. It is a character simulation, not a generic role.
134
+
135
+ 5. **Non-character roles in the same org** (e.g. "Moderator", "Audience") should use the standard mapping table or a role-coined slug as appropriate.
136
+
137
+ **If NOT persona-based:** proceed normally with the mapping table.
138
+
95
139
  ---
96
140
 
97
141
  ## Step 2.5 — Complete Every Agent's Specification (generate what's missing)
98
142
 
99
143
  **This is the step that makes each created agent actually work.** A role is only usable if it has: skills, an instruction document (system prompt), an input contract, and an output contract. Most of these are missing from a bare role description — **generate them, tailored to the specific agent, rather than leaving them blank.**
100
144
 
145
+ **For persona/character roles** (identified in Step 2.3): the generated definition must embody the character. The system prompt should open with "You are [Character Name]" and describe their personality, known views, communication style, and behavioral quirks drawn from public knowledge. Skills must reflect their real-world expertise and traits, not generic software capabilities. If the character is a living public figure, base the portrayal on documented public behavior and statements — do not invent positions they haven't taken.
146
+
101
147
  For **each** role, do the following:
102
148
 
103
149
  **1. Check whether a usable agent definition already exists.**
@@ -159,6 +205,35 @@ The dashboard agent drawer and `runorg` both read these definitions (matched by
159
205
 
160
206
  ---
161
207
 
208
+ ## Step 2.6 — COMPLETENESS GATE (BLOCKING — do not proceed to Step 3 until all checks pass)
209
+
210
+ This gate prevents saving an org where the dashboard will show "No instruction document" or blank skills. **Run these checks now — one Bash call per role:**
211
+
212
+ For each role in the org:
213
+
214
+ ```bash
215
+ # Check 1 — agent_type is set
216
+ role_id="<role_id>"
217
+ agent_type="<agent_type>"
218
+ [ -z "$agent_type" ] && echo "FAIL: role '$role_id' has no agent_type" && exit 1
219
+
220
+ # Check 2 — definition file exists on disk
221
+ def_file=".claude/agents/generated/${agent_type}.md"
222
+ # Also check non-generated paths
223
+ fallback=$(find .claude/agents -iname "${agent_type}.md" 2>/dev/null | head -1)
224
+ if [ ! -f "$def_file" ] && [ -z "$fallback" ]; then
225
+ echo "FAIL: no agent definition file for '$agent_type' — generate it now before continuing"
226
+ exit 1
227
+ fi
228
+ echo "OK: $role_id → $agent_type ✓"
229
+ ```
230
+
231
+ If any check fails: go back to Step 2.5 and generate the missing file before proceeding. Do not skip this check.
232
+
233
+ **Also verify the `skills` array for each role is non-empty (≥3 items).** If `skills: []` for any role, pull the expertise from the generated definition file and populate it now.
234
+
235
+ ---
236
+
162
237
  ## Step 3 — Suggest Communication Topology
163
238
 
164
239
  Determine topology from team size:
@@ -201,6 +276,32 @@ A role that ends up with no inbound or no outbound edge is a bug — re-examine
201
276
 
202
277
  ---
203
278
 
279
+ ## Step 3.5 — TOPOLOGY VALIDATION GATE (BLOCKING — do not proceed to Step 4 until all checks pass)
280
+
281
+ Build the communication array now, then validate it with these checks:
282
+
283
+ ```bash
284
+ # Check 1 — at least one edge exists
285
+ edges='<communication_json_array>'
286
+ edge_count=$(echo "$edges" | jq 'length')
287
+ [ "$edge_count" -eq 0 ] && echo "FAIL: communication array is empty — define edges now" && exit 1
288
+
289
+ # Check 2 — every role has at least one inbound OR outbound edge
290
+ role_ids=("<id1>" "<id2>" ... ) # all role IDs
291
+ for rid in "${role_ids[@]}"; do
292
+ has_edge=$(echo "$edges" | jq --arg r "$rid" '[.[] | select(.from==$r or .to==$r)] | length')
293
+ [ "$has_edge" -eq 0 ] && echo "FAIL: role '$rid' has no communication edges — it is an orphan" && exit 1
294
+ done
295
+
296
+ # Check 3 — every non-boss executor role has at least one inbound edge (receives work)
297
+ # and at least one outbound edge (delivers output)
298
+ echo "OK: topology validated — $edge_count edges, all roles wired"
299
+ ```
300
+
301
+ If any check fails: go back to Step 3 and add the missing edges. **The `communication` array must be non-empty in the saved JSON** — the dashboard Chart tab draws its arrows from this array; an empty array means a blank chart with isolated nodes and no connection lines.
302
+
303
+ ---
304
+
204
305
  ## Step 4 — Build Org Config
205
306
 
206
307
  Produce an org config object using the resolved topology (not hardcoded to `hierarchical`).
@@ -222,7 +323,7 @@ Ask the user (or infer from prompt) for the optional Paperclip-style fields:
222
323
  {
223
324
  "id": "<slug>",
224
325
  "title": "<display title>",
225
- "agent_type": "<subagent_type slug from mapping table>",
326
+ "agent_type": "<subagent_type slug from Step 2.3 character slug for persona roles, or from mapping table for functional roles>",
226
327
  "responsibilities": ["<1-3 bullet responsibilities>"],
227
328
  "reports_to": "<role id or null>",
228
329
  "skills": ["<populated from the generated def's expertise in Step 2.5 — never left empty>"],
@@ -290,27 +391,32 @@ Render the org plan in a clear human-readable format:
290
391
  ║ GOAL: <goal> ║
291
392
  ╚══════════════════════════════════════════════════╝
292
393
 
293
- ROLES
394
+ ROLES (N roles — all must have agent_type + skills before "go")
294
395
  ─────
295
396
  • [boss] CEO / Boss
296
- Agent: coordinator
297
- Reports to: (none top of hierarchy)
397
+ Agent type: coordinator
398
+ Skills: strategic oversight, decision-making, org management
399
+ Reports to: (none — top of hierarchy)
298
400
  Responsibilities: Strategic oversight, final approval
401
+ Definition file: .claude/agents/coordinator.md ✓
299
402
 
300
403
  • [middle_manager] Middle Manager
301
- Agent: Project Shepherd
302
- Reports to: boss
404
+ Agent type: project-shepherd
405
+ Skills: sprint planning, cross-team coordination, status tracking
406
+ Reports to: boss
303
407
  Responsibilities: Sprint planning, cross-team coordination
408
+ Definition file: .claude/agents/generated/project-shepherd.md ✓
304
409
 
305
- ... (all roles)
410
+ ... (all roles — each showing agent_type, skills, and definition file status)
306
411
 
307
- COMMUNICATION TOPOLOGY
412
+ COMMUNICATION TOPOLOGY (N edges — must be non-zero)
308
413
  ──────────────────────
309
- boss → middle_manager (command)
310
- middle_manager → content_writer (command)
311
- content_writer → content_reviewer (handoff)
312
- content_reviewer → middle_manager (report)
313
- ... (all edges)
414
+ boss → middle_manager (command)
415
+ middle_manager → content_writer (command)
416
+ content_writer → content_reviewer (handoff)
417
+ content_reviewer → middle_manager (report)
418
+ middle_manager → boss (report)
419
+ ... (all edges — every role must appear here at least once)
314
420
 
315
421
  SETTINGS
316
422
  ────────
@@ -427,6 +533,39 @@ jq --arg board_id "$board_id" \
427
533
  "$orgJson" > "$tmp" && mv "$tmp" "$orgJson"
428
534
  ```
429
535
 
536
+ **POST-SAVE VALIDATION (run immediately after saving — abort if either check fails):**
537
+
538
+ ```bash
539
+ # Check 1 — communication array is non-empty (dashboard Chart tab requires this for arrows)
540
+ comm_count=$(jq '.communication | length' "$orgJson" 2>/dev/null || echo 0)
541
+ if [ "$comm_count" -eq 0 ]; then
542
+ echo "ERROR: Saved org has empty communication array — dashboard Chart will show isolated nodes with no arrows."
543
+ echo "Fix: go back to Step 3 and Step 3.5, define edges, then re-save."
544
+ exit 1
545
+ fi
546
+ echo "✓ Communication: $comm_count edges saved"
547
+
548
+ # Check 2 — every role has agent_type set
549
+ bad_roles=$(jq -r '.roles[] | select((.agent_type // "") == "") | .id' "$orgJson" 2>/dev/null)
550
+ if [ -n "$bad_roles" ]; then
551
+ echo "ERROR: These roles have no agent_type — dashboard will show '?' for each:"
552
+ echo "$bad_roles"
553
+ echo "Fix: go back to Step 2 / Step 2.5 and set agent_type for each role, then re-save."
554
+ exit 1
555
+ fi
556
+ echo "✓ All roles have agent_type set"
557
+
558
+ # Check 3 — every role has skills (non-empty array)
559
+ empty_skills=$(jq -r '.roles[] | select((.skills // []) | length == 0) | .id' "$orgJson" 2>/dev/null)
560
+ if [ -n "$empty_skills" ]; then
561
+ echo "WARNING: These roles have empty skills arrays — dashboard Skills tab will show nothing:"
562
+ echo "$empty_skills"
563
+ echo "Fix: populate skills from each role's generated agent definition."
564
+ fi
565
+
566
+ echo "✓ Org config validated — org is ready to run"
567
+ ```
568
+
430
569
  ---
431
570
 
432
571
  ## Step 6.7 — Generate Loop Prompt File (scheduled orgs only)
@@ -472,10 +611,33 @@ tmp="${ORG_FILE}.tmp"
472
611
  jq '.loop.last_run = (now|todate)' "$ORG_FILE" > "$tmp" && mv "$tmp" "$ORG_FILE"
473
612
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
474
613
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
614
+ # Unique run ID — used to thread all events in the Chat tab under one session
615
+ RUN_ID="run-$(date -u +%Y%m%dT%H%M%S)"
616
+ # Capture Claude project dir for per-agent token tracking
617
+ CLAUDE_PROJECT_DIR="$HOME/.claude/projects/$(echo "$REPO_ROOT" | tr '/' '-' | sed 's/^-//')"
618
+
619
+ # Comm helper — emits org:comms so Chat tab shows agent-to-agent messages
620
+ _comm() {
621
+ local _from="$1" _to="$2" _msg="$3"
622
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
623
+ -H "Content-Type: application/json" \
624
+ -d "$(jq -cn \
625
+ --arg org "<org_name>" \
626
+ --arg runId "$RUN_ID" \
627
+ --arg from "$_from" \
628
+ --arg to "$_to" \
629
+ --arg msg "$_msg" \
630
+ '{type:"org:comms",org:$org,runId:$runId,from:$from,to:$to,msg:$msg,ts:(now*1000|floor)}')" || true
631
+ }
632
+
633
+ # Register this run with the server — creates run file and enables Chat tab dropdown
475
634
  curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
476
635
  -H "Content-Type: application/json" \
477
- -d "$(jq -cn --arg org "<org_name>" --arg proj "$REPO_ROOT" \
478
- '{type:"org:loop:start",org:$org,project:$proj,ts:(now*1000|floor)}')" || true
636
+ -d "$(jq -cn \
637
+ --arg org "<org_name>" \
638
+ --arg runId "$RUN_ID" \
639
+ --arg goal "<goal>" \
640
+ '{type:"run:start",org:$org,runId:$runId,goal:$goal,ts:(now*1000|floor)}')" || true
479
641
  ```
480
642
 
481
643
  ---
@@ -488,6 +650,55 @@ curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
488
650
  <For a content org, these would be: check content calendar, assign writers, review drafts, publish.>
489
651
  <Derive from orgConfig.goal and orgConfig.roles[].responsibilities — be specific.>
490
652
 
653
+ **REQUIRED — include these patterns at every agent handoff:**
654
+
655
+ 1. **Before spawning each agent**, snapshot the Claude project JSONL files and emit a `_comm` from the dispatching role to the receiving role describing the task:
656
+ ```bash
657
+ JSONL_SNAP_<N>=$(ls -t "$CLAUDE_PROJECT_DIR"/*.jsonl 2>/dev/null | head -20 | sort)
658
+ _comm "<dispatcher-role-id>" "<agent-role-id>" "Task: <what the agent is being asked to do>"
659
+ ```
660
+
661
+ 2. **After the agent returns**, emit a `_comm` from that agent back to its caller with the result summary, then emit its token usage:
662
+ ```bash
663
+ JSONL_SNAP_<N+1>=$(ls -t "$CLAUDE_PROJECT_DIR"/*.jsonl 2>/dev/null | head -20 | sort)
664
+ NEW_JSONL=$(comm -13 <(echo "$JSONL_SNAP_<N>") <(echo "$JSONL_SNAP_<N+1>") | head -1)
665
+ _comm "<agent-role-id>" "<dispatcher-role-id>" "Result: <one-sentence summary of what the agent returned>"
666
+ if [ -n "$NEW_JSONL" ] && [ -f "$NEW_JSONL" ]; then
667
+ USAGE=$(python3 -c "
668
+ import json, sys
669
+ tin=tout=0
670
+ for l in open(sys.argv[1]):
671
+ try:
672
+ d=json.loads(l)
673
+ u=d.get('message',{}).get('usage',{})
674
+ tin+=u.get('input_tokens',0); tout+=u.get('output_tokens',0)
675
+ except: pass
676
+ cost=tin*3e-6+tout*15e-6
677
+ print(json.dumps({'tokens_in':tin,'tokens_out':tout,'cost_usd':round(cost,6)}))
678
+ " "$NEW_JSONL" 2>/dev/null || echo '{"tokens_in":0,"tokens_out":0,"cost_usd":0}')
679
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
680
+ -H "Content-Type: application/json" \
681
+ -d "$(echo "$USAGE" | jq \
682
+ --arg org "<org_name>" \
683
+ --arg role "<agent-role-id>" \
684
+ --arg runId "$RUN_ID" \
685
+ '. + {type:"agent:usage",org:$org,role:$role,runId:$runId,ts:(now*1000|floor|tostring|tonumber)}')" || true
686
+ fi
687
+ ```
688
+
689
+ 3. Use the actual content from the agent's return value in the `_comm` `msg` field — not a generic placeholder. The Chat tab shows this text verbatim.
690
+
691
+ 4. At cycle end (before Schedule Next), emit the completion comms and event:
692
+ ```bash
693
+ _comm "<boss-role-id>" "sys" "Cycle complete: <one-line summary of what was accomplished>"
694
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
695
+ -H "Content-Type: application/json" \
696
+ -d "$(jq -cn \
697
+ --arg org "<org_name>" \
698
+ --arg runId "$RUN_ID" \
699
+ '{type:"org:cycle:complete",org:$org,runId:$runId,ts:(now*1000|floor)}')" || true
700
+ ```
701
+
491
702
  ---
492
703
 
493
704
  ## Schedule Next (ONLY if status is active or paused)
@@ -1064,9 +1064,21 @@ Follow _protocol.md Brain Write Procedure (namespace: `idea`).
1064
1064
 
1065
1065
  ```bash
1066
1066
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
1067
- SESSION_ID=$(jq -r '.sessionId // empty' "$REPO_ROOT/.monomind/sessions/current.json" 2>/dev/null)
1067
+ _get_mono_dir() {
1068
+ local w="${1:-$(pwd)}"
1069
+ if [ -d "$w/.git" ]; then echo "$w/.git/monomind"; return; fi
1070
+ if [ -f "$w/.git" ]; then
1071
+ local m; m=$(grep '^gitdir:' "$w/.git" | sed 's/gitdir: *//')
1072
+ [ -z "$m" ] && { echo "$w/.monomind"; return; }
1073
+ [[ "$m" != /* ]] && m="$w/$m"
1074
+ echo "$(dirname "$(dirname "$m")")/monomind"; return
1075
+ fi
1076
+ echo "$w/.monomind"
1077
+ }
1078
+ MONO_DIR=$(_get_mono_dir "$REPO_ROOT")
1079
+ SESSION_ID=$(jq -r '.sessionId // empty' "$MONO_DIR/sessions/current.json" 2>/dev/null)
1068
1080
  if [ -n "$SESSION_ID" ]; then
1069
- mkdir -p "$REPO_ROOT/.monomind/sessions/${SESSION_ID}"
1081
+ mkdir -p "$MONO_DIR/sessions/${SESSION_ID}"
1070
1082
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
1071
1083
  # LLM: substitute <status>: complete (all steps ran), partial (some skipped), blocked (critical error)
1072
1084
  # LLM: substitute next_actions with actual suggestions derived from this run's top ideas
@@ -1076,7 +1088,7 @@ if [ -n "$SESSION_ID" ]; then
1076
1088
  --argjson artifacts '[]' \
1077
1089
  --argjson next_actions '["<next_action_1>","<next_action_2>"]' \
1078
1090
  '{domain:$domain,status:$status,artifacts:$artifacts,next_actions:$next_actions}' \
1079
- > "$REPO_ROOT/.monomind/sessions/${SESSION_ID}/idea.json"
1091
+ > "$MONO_DIR/sessions/${SESSION_ID}/idea.json"
1080
1092
  curl -s -o /dev/null -X POST "${CTRL_URL}/api/mastermind/event" \
1081
1093
  -H "Content-Type: application/json" \
1082
1094
  -d "$(jq -cn --arg sid "$SESSION_ID" --arg status "<status>" \
@@ -526,7 +526,8 @@ OPERATING LOOP:
526
526
  jq -cn --arg runId "${runId}" --arg org "${orgName}" --argjson pend "${pending_count:-0}" \
527
527
  '{type:"run:cycle:complete",runId:$runId,org:$org,pending:$pend,ts:(now*1000|floor)}' >> "${runFile}" || true
528
528
  activityFile=".monomind/orgs/${orgName}-activity.jsonl"
529
- echo "{\"type\":\"run:cycle:complete\",\"org\":\"${orgName}\",\"runId\":\"${runId}\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"pending\":${pending_count:-0}}" >> "$activityFile" 2>/dev/null || true
529
+ jq -cn --arg org "${orgName}" --arg runId "${runId}" --argjson pend "${pending_count:-0}" \
530
+ '{type:"run:cycle:complete",org:$org,runId:$runId,ts:(now*1000|floor),pending:$pend}' >> "$activityFile" 2>/dev/null || true
530
531
 
531
532
  8. REQUIRED — emit org:checkpoint with a one-sentence progress summary (include runId):
532
533
  curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "Content-Type: application/json" \
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAiC,MAAM,aAAa,CAAC;AA6xB1E,eAAO,MAAM,aAAa,EAAE,OA6M3B,CAAC;AAEF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAiC,MAAM,aAAa,CAAC;AAg2B1E,eAAO,MAAM,aAAa,EAAE,OA0N3B,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -436,6 +436,84 @@ async function checkMonograph() {
436
436
  };
437
437
  }
438
438
  }
439
+ // Check monograph graph freshness (is the graph built? how stale?)
440
+ async function checkMonographFreshness() {
441
+ try {
442
+ const cwd = process.cwd();
443
+ const dbPath = join(cwd, '.monomind', 'monograph.db');
444
+ const lockPath = join(cwd, '.monomind', 'graph', '.rebuild-lock');
445
+ const statsPath = join(cwd, '.monomind', 'graph', 'stats.json');
446
+ // Check if graph exists at all
447
+ const hasDb = existsSync(dbPath);
448
+ const hasLock = existsSync(lockPath);
449
+ const hasStats = existsSync(statsPath);
450
+ if (!hasDb && !hasStats) {
451
+ return {
452
+ name: 'Graph freshness',
453
+ status: 'warn',
454
+ message: 'No monograph graph built yet',
455
+ fix: 'mcp__monomind__monograph_build codeOnly:true — or run npx monomind@latest hooks graph-status',
456
+ };
457
+ }
458
+ // Determine last build time
459
+ let buildMs = 0;
460
+ if (hasDb) {
461
+ try {
462
+ buildMs = Math.max(buildMs, statSync(dbPath).mtimeMs);
463
+ }
464
+ catch { /* ignore */ }
465
+ }
466
+ if (hasLock) {
467
+ try {
468
+ buildMs = Math.max(buildMs, statSync(lockPath).mtimeMs);
469
+ }
470
+ catch { /* ignore */ }
471
+ }
472
+ if (hasStats) {
473
+ try {
474
+ buildMs = Math.max(buildMs, statSync(statsPath).mtimeMs);
475
+ }
476
+ catch { /* ignore */ }
477
+ }
478
+ if (buildMs === 0) {
479
+ return { name: 'Graph freshness', status: 'warn', message: 'Graph exists but build time unknown' };
480
+ }
481
+ // Count commits since last build
482
+ const buildIso = new Date(buildMs).toISOString();
483
+ let commitsBehind = 0;
484
+ try {
485
+ const out = execSync(`git rev-list --count --since='${buildIso}' HEAD 2>/dev/null`, {
486
+ encoding: 'utf8', timeout: 2000, cwd,
487
+ }).trim();
488
+ commitsBehind = parseInt(out, 10) || 0;
489
+ }
490
+ catch { /* git not available or not a git repo */ }
491
+ const ageMinutes = Math.floor((Date.now() - buildMs) / 60000);
492
+ const ageStr = ageMinutes < 60 ? `${ageMinutes}m ago` : `${Math.floor(ageMinutes / 60)}h ago`;
493
+ if (commitsBehind === 0) {
494
+ return { name: 'Graph freshness', status: 'pass', message: `FRESH — built ${ageStr}, 0 commits behind` };
495
+ }
496
+ else if (commitsBehind <= 5) {
497
+ return {
498
+ name: 'Graph freshness',
499
+ status: 'warn',
500
+ message: `${commitsBehind} commit(s) behind — built ${ageStr}`,
501
+ fix: 'mcp__monomind__monograph_build codeOnly:true',
502
+ };
503
+ }
504
+ else {
505
+ return {
506
+ name: 'Graph freshness',
507
+ status: 'fail',
508
+ message: `STALE — ${commitsBehind} commits behind (built ${ageStr})`,
509
+ fix: 'mcp__monomind__monograph_build codeOnly:true',
510
+ };
511
+ }
512
+ }
513
+ catch {
514
+ return { name: 'Graph freshness', status: 'warn', message: 'Could not check graph freshness' };
515
+ }
516
+ }
439
517
  // Check @monoes/memory (optional HNSW vector search package)
440
518
  async function checkMonoesMemory() {
441
519
  try {
@@ -798,7 +876,7 @@ export const doctorCommand = {
798
876
  {
799
877
  name: 'component',
800
878
  short: 'c',
801
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, monograph, memory-pkg, helpers, agentic-flow, monoes, gates, gitignore)',
879
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, monograph, graph-freshness, memory-pkg, helpers, agentic-flow, monoes, gates, gitignore)',
802
880
  type: 'string'
803
881
  },
804
882
  {
@@ -841,6 +919,7 @@ export const doctorCommand = {
841
919
  checkDiskSpace,
842
920
  checkBuildTools,
843
921
  checkMonograph,
922
+ checkMonographFreshness,
844
923
  checkMonoesMemory,
845
924
  checkHelpersFresh,
846
925
  checkAgenticFlow,
@@ -863,6 +942,7 @@ export const doctorCommand = {
863
942
  'disk': checkDiskSpace,
864
943
  'typescript': checkBuildTools,
865
944
  'monograph': checkMonograph,
945
+ 'graph-freshness': checkMonographFreshness,
866
946
  'memory-pkg': checkMonoesMemory,
867
947
  'helpers': checkHelpersFresh,
868
948
  'agentic-flow': checkAgenticFlow,
@@ -889,6 +969,14 @@ export const doctorCommand = {
889
969
  const result = settledResult.value;
890
970
  results.push(result);
891
971
  output.writeln(formatCheck(result));
972
+ if (result.fix && result.status === 'fail') {
973
+ // Always show fix inline for failures — no flag needed
974
+ output.writeln(output.dim(` Fix: ${result.fix}`));
975
+ }
976
+ else if (result.fix && result.status === 'warn') {
977
+ // Show fix inline for warnings too, so users don't need --fix for common issues
978
+ output.writeln(output.dim(` Hint: ${result.fix}`));
979
+ }
892
980
  if (result.fix && (result.status === 'fail' || result.status === 'warn')) {
893
981
  fixes.push(`${result.name}: ${result.fix}`);
894
982
  }
@@ -949,9 +1037,13 @@ export const doctorCommand = {
949
1037
  output.writeln(output.dim(` ${fix}`));
950
1038
  }
951
1039
  }
952
- else if (fixes.length > 0 && !showFix) {
953
- output.writeln();
954
- output.writeln(output.dim(`Run with --fix to see ${fixes.length} suggested fix${fixes.length > 1 ? 'es' : ''}`));
1040
+ else if (!showFix) {
1041
+ // Only nudge about --fix for warnings (failures already showed their fix inline)
1042
+ const warnFixes = results.filter(r => r.status === 'warn' && r.fix).length;
1043
+ if (warnFixes > 0) {
1044
+ output.writeln();
1045
+ output.writeln(output.dim(`Run with --fix to see ${warnFixes} suggested fix${warnFixes > 1 ? 'es' : ''} for warnings`));
1046
+ }
955
1047
  }
956
1048
  // Overall result
957
1049
  if (failed > 0) {