monomind 2.3.2 → 2.3.4

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 (59) hide show
  1. package/README.md +37 -4
  2. package/package.json +1 -1
  3. package/packages/@monomind/cli/.claude/commands/mastermind/_repeat.md +9 -9
  4. package/packages/@monomind/cli/.claude/commands/mastermind/{approve.md → approvev1.md} +14 -11
  5. package/packages/@monomind/cli/.claude/commands/mastermind/architect.md +3 -3
  6. package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +4 -4
  7. package/packages/@monomind/cli/.claude/commands/mastermind/help.md +18 -0
  8. package/packages/@monomind/cli/.claude/commands/mastermind/master.md +6 -6
  9. package/packages/@monomind/cli/.claude/commands/mastermind/repeat.md +5 -5
  10. package/packages/@monomind/cli/.claude/commands/mastermind/runorg.md +20 -108
  11. package/packages/@monomind/cli/.claude/commands/mastermind/runorgv1.md +159 -0
  12. package/packages/@monomind/cli/.claude/commands/mastermind/stoporg.md +1 -1
  13. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +52 -4
  14. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +9 -1
  15. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +9 -1
  16. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +88 -15
  17. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +25 -18
  18. package/packages/@monomind/cli/.claude/skills/mastermind-skills/_protocol.md +1 -1
  19. package/packages/@monomind/cli/.claude/skills/mastermind-skills/_repeat.md +3 -3
  20. package/packages/@monomind/cli/.claude/skills/mastermind-skills/adapters.md +1 -1
  21. package/packages/@monomind/cli/.claude/skills/mastermind-skills/agents.md +6 -2
  22. package/packages/@monomind/cli/.claude/skills/mastermind-skills/{approve.md → approvev1.md} +8 -5
  23. package/packages/@monomind/cli/.claude/skills/mastermind-skills/architect.md +8 -8
  24. package/packages/@monomind/cli/.claude/skills/mastermind-skills/backup.md +1 -1
  25. package/packages/@monomind/cli/.claude/skills/mastermind-skills/bootstrap.md +1 -1
  26. package/packages/@monomind/cli/.claude/skills/mastermind-skills/costs.md +1 -1
  27. package/packages/@monomind/cli/.claude/skills/mastermind-skills/env.md +7 -2
  28. package/packages/@monomind/cli/.claude/skills/mastermind-skills/{heartbeat.md → heartbeatv1.md} +9 -6
  29. package/packages/@monomind/cli/.claude/skills/mastermind-skills/idea.md +1 -1
  30. package/packages/@monomind/cli/.claude/skills/mastermind-skills/inbox.md +14 -2
  31. package/packages/@monomind/cli/.claude/skills/mastermind-skills/instance.md +3 -1
  32. package/packages/@monomind/cli/.claude/skills/mastermind-skills/monitor.md +2 -2
  33. package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +5 -2
  34. package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgs.md +14 -13
  35. package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgstatus.md +19 -8
  36. package/packages/@monomind/cli/.claude/skills/mastermind-skills/plugins.md +1 -1
  37. package/packages/@monomind/cli/.claude/skills/mastermind-skills/projects.md +3 -0
  38. package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +37 -719
  39. package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorgv1.md +731 -0
  40. package/packages/@monomind/cli/.claude/skills/mastermind-skills/stoporg.md +12 -5
  41. package/packages/@monomind/cli/.claude/skills/mastermind-skills/tasks.md +4 -1
  42. package/packages/@monomind/cli/README.md +37 -4
  43. package/packages/@monomind/cli/dist/src/commands/cleanup.js +30 -8
  44. package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +23 -10
  45. package/packages/@monomind/cli/dist/src/commands/org-observe.js +30 -26
  46. package/packages/@monomind/cli/dist/src/commands/org.js +52 -2
  47. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +0 -0
  48. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +31 -12
  49. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +45 -9
  50. package/packages/@monomind/cli/dist/src/orgrt/mailbox.d.ts +23 -0
  51. package/packages/@monomind/cli/dist/src/orgrt/mailbox.js +33 -1
  52. package/packages/@monomind/cli/dist/src/orgrt/migrate.d.ts +26 -0
  53. package/packages/@monomind/cli/dist/src/orgrt/migrate.js +111 -0
  54. package/packages/@monomind/cli/dist/src/orgrt/reporting.js +8 -1
  55. package/packages/@monomind/cli/dist/src/orgrt/session.js +8 -2
  56. package/packages/@monomind/cli/dist/src/ui/dashboard.html +44 -0
  57. package/packages/@monomind/cli/dist/src/ui/orgs.html +2 -2
  58. package/packages/@monomind/cli/dist/src/ui/server.mjs +86 -0
  59. package/packages/@monomind/cli/package.json +2 -2
@@ -0,0 +1,159 @@
1
+ <!-- LEGACY-ORG-V1 — Start a saved org as a persistent, prompt-orchestrated agent organization. The boss agent coordinates all roles; agents pick up tasks from a shared board and run until stopped. Reach it only via /mastermind:runorgv1. -->
2
+
3
+ > **LEGACY-ORG-V1 (2026-07): superseded by `monomind org run <name>` (SDK org runtime v2), delegated to by default via `/mastermind:runorg`.**
4
+ > This prompt-orchestrated path has no delivery guarantees and no ground-truth event stream.
5
+ > It remains only for orgs not yet migrated. New orgs MUST use the daemon. Reach this legacy path only via `/mastermind:runorgv1`.
6
+
7
+ **If $ARGUMENTS is empty:** List saved orgs and display the following.
8
+
9
+ ---
10
+
11
+ **MASTERMIND: RUN ORG**
12
+
13
+ Running an org starts an autonomous agent organization. There are two modes:
14
+
15
+ **Persistent org** (no schedule): a boss agent loads the org definition, assigns work to specialists from a shared board, and loops indefinitely. Stop with `/mastermind:stoporg --org <name>`.
16
+
17
+ **Scheduled org** (created with `--schedule`): sets the org to `active` and runs the first iteration immediately. Subsequent iterations are self-scheduled via ScheduleWakeup. The loop stops cleanly within one interval after `/mastermind:stoporg --org <name>`.
18
+
19
+ **Your saved orgs:**
20
+
21
+ ```bash
22
+ npx -y monomind@latest org list 2>/dev/null \
23
+ || ls .monomind/orgs/*.json 2>/dev/null | grep -vE -- '-(state|goals|threads|activity|approvals|members|secrets|budgets|routines|issues|projects|workspaces|worktrees|environments|plugins|adapters|join-requests|bootstrap|project-workspaces|approval-comments|skills)\.json$' | xargs -I{} basename {} .json 2>/dev/null \
24
+ || echo "(none — run /mastermind:createorg to define one)"
25
+ ```
26
+
27
+ **Usage:**
28
+
29
+ ```
30
+ /mastermind:runorgv1 --org <name>
31
+
32
+ /mastermind:runorgv1 --org content-team --task "Publish the Q2 product roundup post by Friday"
33
+
34
+ /mastermind:runorgv1 --org research-pod "Focus this week on competitor pricing changes"
35
+ ```
36
+
37
+ **Options:**
38
+ `--org <name>` — which saved org to start (required; prompted if omitted)
39
+ `--task <task>` — override the org's default goal for this run
40
+ Any remaining text is passed as additional context to the boss agent.
41
+
42
+ No orgs yet? Run `/mastermind:createorg` to define one.
43
+
44
+ ---
45
+
46
+ **If $ARGUMENTS is non-empty:** Execute the flow below.
47
+
48
+ ---
49
+
50
+ **STEP 0 — Extract loop-control flags (do this FIRST, before any other parsing)**
51
+
52
+ Scan `$ARGUMENTS` for these flags and store their values. Remove them from the argument string before Step 1:
53
+
54
+ | Flag | Variable | Default |
55
+ |---|---|---|
56
+ | `--rep <N>` | `current_rep = N` | absent |
57
+ | `--loop <id>` | `LOOP_ID = id` | absent |
58
+ | `--tillend` | `tillend_mode = true` | false |
59
+ | `--maxruns <N>` | `tillend_maxruns = N` | 50 |
60
+ | `--wait <N>` | `wait_seconds = N` | 60 |
61
+ | `--repeat <N>` | `repeat_count = N` | 0 |
62
+
63
+ ⚠️ **CRITICAL — CONTINUATION RUNS DO NOT SKIP WORK.** When `--rep N` is present, this is a scheduled continuation triggered by ScheduleWakeup. The org's FULL work cycle MUST still execute every time: session variables → session:start event → Skill("mastermind-skills:runorgv1") → session:complete event. NEVER short-circuit or skip the org work because `--rep` is present. The `--rep` / `--loop` flags are only consumed by `Skill("mastermind-skills:_repeat")` at the end.
64
+
65
+ ---
66
+
67
+ **STEP 1 — Parse org-specific flags**
68
+
69
+ From the remaining `$ARGUMENTS` (after loop flags removed in Step 0):
70
+ - `--org <name>` → org_name = <name>
71
+ - `--task <task>` → task_override = <task> (if omitted, task_override = null — the skill uses org's stored goal)
72
+ - Remaining text = additional context passed to the boss agent
73
+
74
+ If `--org` is not provided, list saved orgs and ask which to run:
75
+ ```bash
76
+ orgs=$(ls .monomind/orgs/*.json 2>/dev/null | xargs -I{} basename {} .json 2>/dev/null)
77
+ if [ -z "$orgs" ]; then
78
+ echo "No saved orgs found. Run /mastermind:createorg first."
79
+ exit 1
80
+ fi
81
+ echo "$orgs"
82
+ ```
83
+ Ask: "Which org would you like to start? Available: <list from above>"
84
+
85
+ Verify the org file exists before proceeding:
86
+ ```bash
87
+ [ -f ".monomind/orgs/${org_name}.json" ] || { echo "Org '${org_name}' not found."; exit 1; }
88
+ ```
89
+ If the file does not exist, stop and suggest running `/mastermind:createorg --name ${org_name}`.
90
+
91
+ Load brain context for the `ops` domain (follow _protocol.md Brain Load Procedure, namespace: `ops`).
92
+
93
+ Resolve session ID and project root:
94
+ ```bash
95
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
96
+ CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
97
+ # Reuse the loop's original sessionId for continuation runs (keeps all reps under one session)
98
+ if [ -n "${LOOP_ID:-}" ] && [ -f ".monomind/loops/${LOOP_ID}.json" ]; then
99
+ session_id=$(jq -r '.sessionId // empty' ".monomind/loops/${LOOP_ID}.json" 2>/dev/null)
100
+ fi
101
+ session_id="${session_id:-mm-$(date -u +%Y%m%dT%H%M%S)}"
102
+ ```
103
+
104
+ Emit `session:start` to dashboard:
105
+ ```bash
106
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
107
+ -H "Content-Type: application/json" \
108
+ -d "$(jq -cn \
109
+ --arg session "$session_id" \
110
+ --arg org "$org_name" \
111
+ --arg proj "$REPO_ROOT" \
112
+ '{type:"session:start",session:$session,domain:"ops",prompt:("Running org: "+$org),mode:"auto",project:$proj,ts:(now*1000|floor)}')" || true
113
+ ```
114
+
115
+ Emit `domain:dispatch`:
116
+ ```bash
117
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
118
+ -H "Content-Type: application/json" \
119
+ -d "$(jq -cn \
120
+ --arg session "$session_id" \
121
+ --arg org "$org_name" \
122
+ --arg proj "$REPO_ROOT" \
123
+ '{type:"domain:dispatch",session:$session,domain:"ops",cmd:("Starting org "+$org+" as persistent daemon"),project:$proj,ts:(now*1000|floor)}')" || true
124
+ ```
125
+
126
+ Invoke `Skill("mastermind-skills:runorgv1")` passing: brain_context, org_name: `$org_name`, session_id: `$session_id`, task: task_override, caller: "command".
127
+
128
+ After the skill spawns the boss agent and returns: note the status. Emit `session:complete`:
129
+ ```bash
130
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
131
+ -H "Content-Type: application/json" \
132
+ -d "$(jq -cn \
133
+ --arg session "$session_id" \
134
+ --arg status "<status>" \
135
+ --arg proj "$REPO_ROOT" \
136
+ '{type:"session:complete",session:$session,domain:"ops",status:$status,domains:["ops"],project:$proj,ts:(now*1000|floor)}')" || true
137
+ ```
138
+
139
+ Follow _protocol.md Brain Write Procedure for domain `ops`.
140
+
141
+
142
+ **MANDATORY — invoke `Skill("mastermind-skills:_repeat")` now.** This is required regardless of how the skill above completed, regardless of whether you think the work is done, regardless of whether you plan to end your response. For `--repeat N`: the count is non-negotiable — all N runs must happen. For `--tillend`: only a verified empty round (confirmed by git diff) stops the loop. Do not end your response without invoking this skill.
143
+
144
+ After the REPEAT POSTAMBLE completes, if a loop was started or continued (LOOP_ID is set), write the org name into the loop state file so the dashboard can detect running status:
145
+ ```bash
146
+ if [ -n "${LOOP_ID:-}" ]; then
147
+ LOOP_FILE=".monomind/loops/${LOOP_ID}.json"
148
+ if [ -f "$LOOP_FILE" ]; then
149
+ python3 -c "
150
+ import json, sys
151
+ f = sys.argv[1]; org = sys.argv[2]
152
+ d = json.load(open(f))
153
+ if 'orgName' not in d:
154
+ d['orgName'] = org
155
+ open(f, 'w').write(json.dumps(d, indent=2))
156
+ " "$LOOP_FILE" "${org_name}" 2>/dev/null || true
157
+ fi
158
+ fi
159
+ ```
@@ -66,7 +66,7 @@ Invoke `Skill("mastermind-skills:stoporg")` passing: org_name: `$org_name`, sess
66
66
 
67
67
  After skill returns: emit `session:complete`:
68
68
  ```bash
69
- curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
69
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
70
70
  -H "Content-Type: application/json" \
71
71
  -d "$(jq -cn \
72
72
  --arg session "$session_id" \
@@ -172,11 +172,16 @@ async function main() {
172
172
  ? [...args, String(DEFAULT_PORT)]
173
173
  : [...args, 'ui', '--no-open', '--port', String(DEFAULT_PORT)];
174
174
 
175
+ // The child writes its ACTUAL bound port here — the only identity-proof
176
+ // signal. An HTTP probe alone can be answered by another project's server
177
+ // already holding the port (which then leaves control.json lying about
178
+ // where THIS project's events should go).
179
+ const BOUND_REPORT = path.join(CWD, '.monomind', `.bound-report-${Date.now()}.json`);
175
180
  const child = spawn(cmd, allArgs, {
176
181
  detached: true,
177
182
  stdio: 'ignore',
178
183
  cwd: CWD,
179
- env: { ...process.env, CLAUDE_PROJECT_DIR: CWD },
184
+ env: { ...process.env, CLAUDE_PROJECT_DIR: CWD, MONOMIND_BOUND_REPORT: BOUND_REPORT },
180
185
  });
181
186
 
182
187
  child.unref();
@@ -189,11 +194,20 @@ async function main() {
189
194
  // If port 4242 was in use, server.mjs auto-increments (up to +10).
190
195
  // Poll a few ports to find where it actually bound and update control.json.
191
196
  const http = require('http');
197
+ // Resolves to the responder's pid (number), null for "answers but pid
198
+ // unreadable" (auth-walled or old server), or false for "no answer".
192
199
  function probePort(p) {
193
200
  return new Promise((resolve) => {
194
201
  const req = http.get({ hostname: 'localhost', port: p, path: '/api/status', timeout: 1000 }, (res) => {
195
- res.resume();
196
- resolve(res.statusCode < 500);
202
+ let body = '';
203
+ res.on('data', (c) => { if (body.length < 4096) body += c; });
204
+ res.on('end', () => {
205
+ if (res.statusCode >= 500) return resolve(false);
206
+ try {
207
+ const pid = JSON.parse(body).pid;
208
+ resolve(typeof pid === 'number' ? pid : null);
209
+ } catch { resolve(null); }
210
+ });
197
211
  });
198
212
  req.on('error', () => resolve(false));
199
213
  req.on('timeout', () => { req.destroy(); resolve(false); });
@@ -201,20 +215,54 @@ async function main() {
201
215
  }
202
216
 
203
217
  // Give the server up to ~10 s to start, then confirm the actual port.
218
+ // Identity-first: the child's bound-report file (written by server.mjs) is
219
+ // authoritative; the HTTP probe only confirms a port when the responder's
220
+ // pid matches our child. A foreign server answering on DEFAULT_PORT must
221
+ // NOT be mistaken for ours — that lie misroutes every event emitter in
222
+ // this project (root cause of orgs running invisibly).
204
223
  async function confirmPort() {
224
+ let sawForeignOnDefault = false;
205
225
  for (let attempt = 0; attempt < 20; attempt++) {
206
226
  await new Promise(r => setTimeout(r, 500));
227
+ // 1) Authoritative: child self-reported its bound port
228
+ try {
229
+ const rep = JSON.parse(fs.readFileSync(BOUND_REPORT, 'utf8'));
230
+ if (rep && rep.pid === child.pid && typeof rep.port === 'number') {
231
+ try { fs.unlinkSync(BOUND_REPORT); } catch { /* ignore */ }
232
+ if (rep.port !== DEFAULT_PORT) {
233
+ writeStatus(child.pid, rep.port);
234
+ process.stdout.write(`[control] server bound to port ${rep.port} (updated control.json)\n`);
235
+ }
236
+ return;
237
+ }
238
+ } catch { /* not written yet or old server — fall through to probe */ }
239
+ // 2) Fallback: pid-matched HTTP probe (old servers without report support)
207
240
  for (let delta = 0; delta <= 10; delta++) {
208
241
  const p = DEFAULT_PORT + delta;
209
- if (await probePort(p)) {
242
+ const responderPid = await probePort(p);
243
+ if (responderPid === false) continue;
244
+ if (responderPid === child.pid) {
210
245
  if (p !== DEFAULT_PORT) {
211
246
  writeStatus(child.pid, p);
212
247
  process.stdout.write(`[control] server bound to port ${p} (updated control.json)\n`);
213
248
  }
214
249
  return;
215
250
  }
251
+ if (p === DEFAULT_PORT && typeof responderPid === 'number') sawForeignOnDefault = true;
252
+ // pid mismatch or unreadable — not provably ours, keep scanning
216
253
  }
217
254
  }
255
+ try { fs.unlinkSync(BOUND_REPORT); } catch { /* ignore */ }
256
+ if (sawForeignOnDefault) {
257
+ // Another project's server owns DEFAULT_PORT and our child never proved
258
+ // its own port — point control.json at the live server instead of lying,
259
+ // and kill our redundant child.
260
+ try { process.kill(child.pid, 'SIGTERM'); } catch { /* already gone */ }
261
+ process.stdout.write(`[control] port ${DEFAULT_PORT} is served by another project's control server — reusing it (killed redundant child)\n`);
262
+ const foreignPid = await probePort(DEFAULT_PORT);
263
+ writeStatus(typeof foreignPid === 'number' ? foreignPid : 0, DEFAULT_PORT);
264
+ return;
265
+ }
218
266
  // Server never became reachable on any expected port — kill the child
219
267
  // rather than leave an orphan bound to some port nothing will ever read.
220
268
  // The next session-start simply retries.
@@ -66,12 +66,20 @@ function forwardToDashboard(entry) {
66
66
  const rawPort = Number(ctrl.port);
67
67
  const port = (Number.isInteger(rawPort) && rawPort >= 1024 && rawPort <= 65535) ? rawPort : 4242;
68
68
  const body = JSON.stringify({ ...entry, hookCaptured: true });
69
+ // Ingest is default-deny: attach the dashboard credential (written by
70
+ // server.mjs next to control.json) or the POST is silently rejected.
71
+ let authHdr = '';
72
+ try { authHdr = fs.readFileSync(path.join(CWD, '.monomind', 'dashboard-token'), 'utf8').trim(); } catch {}
69
73
  const req = http.request({
70
74
  method: 'POST',
71
75
  hostname: 'localhost',
72
76
  port,
73
77
  path: '/api/mastermind/event',
74
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ 'Content-Length': Buffer.byteLength(body),
81
+ ...(authHdr ? { 'x-monomind-token': authHdr } : {}),
82
+ },
75
83
  timeout: 400,
76
84
  });
77
85
  req.on('error', () => {});
@@ -201,12 +201,20 @@ function emitEvent(event) {
201
201
  }
202
202
  const u = new URL(baseUrl);
203
203
  const body = JSON.stringify(event);
204
+ // Ingest is default-deny: attach the dashboard credential (written by
205
+ // server.mjs next to control.json) or the POST is silently rejected.
206
+ let authHdr = '';
207
+ try { authHdr = fs.readFileSync(path.join(CWD, '.monomind', 'dashboard-token'), 'utf8').trim(); } catch {}
204
208
  const req = http.request({
205
209
  hostname: u.hostname,
206
210
  port: parseInt(u.port || '4242', 10),
207
211
  path: '/api/mastermind/event',
208
212
  method: 'POST',
209
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
213
+ headers: {
214
+ 'Content-Type': 'application/json',
215
+ 'Content-Length': Buffer.byteLength(body),
216
+ ...(authHdr ? { 'x-monomind-token': authHdr } : {}),
217
+ },
210
218
  }, () => {
211
219
  try { fs.unlinkSync(spoolFile); } catch {}
212
220
  resolve();
@@ -319,32 +319,105 @@ module.exports = {
319
319
  // ── Second Brain: per-request knowledge injection ──────────────────
320
320
  // When the project has an indexed knowledge base, surface the most
321
321
  // relevant excerpts for THIS prompt so Claude has them without having
322
- // to decide to call knowledge_search. Keyword-scored over chunks.jsonl
323
- // (a hook subprocess can't afford a 1-3s embedding-model load per
324
- // prompt; full semantic search stays behind the knowledge_search MCP
325
- // tool / `monomind doc search`).
326
- // monolean: [keyword-only injection] upgrade path: warm control-server
327
- // endpoint holding the embedding model for semantic per-prompt retrieval.
322
+ // to decide to call knowledge_search. Semantic-first via the dashboard
323
+ // server's warm /api/knowledge/search (it holds the local embedding
324
+ // model hot a hook subprocess can't afford a 1-3s model load per
325
+ // prompt); tokenized keyword scoring over chunks.jsonl as the fallback
326
+ // when the server is down or still warming.
328
327
  try {
329
328
  var sbPrompt = String(prompt || '');
330
- // Skip slash commands and trivial prompts — injection would be noise.
331
- if (sbPrompt.length >= 12 && sbPrompt.charAt(0) !== '/') {
329
+ // Skip slash commands and low-content prompts — injection would be
330
+ // noise. A prompt earns injection only when it carries at least two
331
+ // substantive terms ("what is next" / "ok go ahead" / "thanks" carry
332
+ // zero-to-one and must stay quiet; a real question about the project
333
+ // clears this easily).
334
+ var _SB_FILLER = new Set(['what', 'whats', 'is', 'are', 'was', 'were', 'the', 'this', 'that', 'these', 'those',
335
+ 'next', 'now', 'then', 'and', 'but', 'for', 'not', 'you', 'your', 'can', 'could', 'should', 'would', 'will',
336
+ 'lets', 'let', 'make', 'made', 'making', 'please', 'okay', 'yes', 'yeah', 'sure', 'thanks', 'thank',
337
+ 'ahead', 'continue', 'proceed', 'more', 'again', 'how', 'why', 'when', 'where', 'who', 'which',
338
+ 'with', 'about', 'from', 'into', 'onto', 'over', 'under', 'all', 'any', 'some', 'one', 'two', 'just',
339
+ 'like', 'want', 'need', 'get', 'got', 'here', 'there', 'still', 'also', 'too', 'very', 'really',
340
+ 'them', 'they', 'their', 'theirs', 'its', 'our', 'ours', 'him', 'her', 'his', 'hers',
341
+ 'order', 'done', 'doing', 'did', 'does', 'goes', 'going', 'went', 'come', 'came', 'good', 'great', 'nice', 'fine']);
342
+ var _sbSubstantive = sbPrompt.toLowerCase().split(/[^a-z0-9]+/).filter(function(t) {
343
+ return t.length >= 3 && !_SB_FILLER.has(t);
344
+ });
345
+ if (_sbSubstantive.length >= 2 && sbPrompt.charAt(0) !== '/') {
332
346
  var sbKnowledgeDir = path.join(CWD, '.monomind', 'knowledge');
333
347
  if (fs.existsSync(path.join(sbKnowledgeDir, 'chunks.jsonl'))) {
334
- var sbSearch = hCtx._buildKnowledgeSearchFn(sbKnowledgeDir);
335
- var sbHits = await hCtx.runWithTimeout(
336
- function() { return sbSearch(sbPrompt, { namespace: 'knowledge:shared', limit: 3, minScore: 0.45 }); },
337
- 'second-brain-inject'
338
- );
348
+ var sbHits = null;
349
+ var sbMethod = 'keyword';
350
+
351
+ // Semantic path: warm control-server endpoint (strictly local traffic;
352
+ // sbAuth is the local dashboard session value from .monomind, same one
353
+ // every other hook event POST attaches — not a checked-in secret).
354
+ try {
355
+ var sbCtrlUrl = 'http://localhost:4242';
356
+ try {
357
+ var sbCtl = JSON.parse(fs.readFileSync(path.join(CWD, '.monomind', 'control.json'), 'utf-8'));
358
+ if (sbCtl.url) sbCtrlUrl = sbCtl.url;
359
+ } catch (_) {}
360
+ var sbAuth = '';
361
+ try { sbAuth = fs.readFileSync(path.join(CWD, '.monomind', 'dashboard-token'), 'utf-8').trim(); } catch (_) {}
362
+ var sbResp = await fetch(sbCtrlUrl + '/api/knowledge/search', {
363
+ method: 'POST',
364
+ headers: { 'Content-Type': 'application/json', 'x-monomind-token': sbAuth },
365
+ body: JSON.stringify({ query: sbPrompt, namespace: 'knowledge:shared', limit: 3 }),
366
+ signal: AbortSignal.timeout(900),
367
+ });
368
+ if (sbResp.ok) {
369
+ var sbData = await sbResp.json();
370
+ if (sbData && Array.isArray(sbData.results) && sbData.results.length > 0) {
371
+ sbHits = sbData.results.map(function(r) { return { key: r.key, value: r.content, score: r.score, metadata: {} }; });
372
+ sbMethod = sbData.method === 'semantic' ? 'semantic' : 'keyword';
373
+ }
374
+ }
375
+ } catch (_) { /* server down or warming — fall back below */ }
376
+
377
+ if (!sbHits) {
378
+ var sbSearch = hCtx._buildKnowledgeSearchFn(sbKnowledgeDir);
379
+ sbHits = await hCtx.runWithTimeout(
380
+ function() { return sbSearch(sbPrompt, { namespace: 'knowledge:shared', limit: 3, minScore: 0.45 }); },
381
+ 'second-brain-inject'
382
+ );
383
+ }
384
+ // Relevance floor: injecting weak matches pollutes every prompt's
385
+ // context — below 0.35 the excerpt is more likely noise than help.
386
+ if (sbHits) sbHits = sbHits.filter(function(h) { return (h.score || 0) >= 0.35; });
387
+
388
+ // Telemetry: append one JSONL line per evaluated prompt so the
389
+ // thresholds above can be tuned from real usage (and misses can
390
+ // seed the golden-set eval). Prompt text is NOT logged.
391
+ try {
392
+ var sbMetricsDir = path.join(CWD, '.monomind', 'metrics');
393
+ fs.mkdirSync(sbMetricsDir, { recursive: true });
394
+ var sbMetricsFile = path.join(sbMetricsDir, 'second-brain.jsonl');
395
+ try {
396
+ if (fs.existsSync(sbMetricsFile) && fs.statSync(sbMetricsFile).size > 512 * 1024) {
397
+ var sbOld = fs.readFileSync(sbMetricsFile, 'utf-8').trim().split('\n');
398
+ fs.writeFileSync(sbMetricsFile, sbOld.slice(-500).join('\n') + '\n', 'utf-8');
399
+ }
400
+ } catch (_) {}
401
+ fs.appendFileSync(sbMetricsFile, JSON.stringify({
402
+ ts: Date.now(),
403
+ method: sbMethod,
404
+ hits: sbHits ? sbHits.length : 0,
405
+ topScore: sbHits && sbHits[0] ? Number((sbHits[0].score || 0).toFixed(3)) : null,
406
+ promptLen: sbPrompt.length,
407
+ terms: _sbSubstantive.length,
408
+ injected: !!(sbHits && sbHits.length > 0),
409
+ }) + '\n', 'utf-8');
410
+ } catch (_) { /* telemetry never blocks */ }
411
+
339
412
  if (sbHits && sbHits.length > 0) {
340
- var sbLines = ['[SECOND_BRAIN] ' + sbHits.length + ' relevant excerpt(s) from the project knowledge base:'];
413
+ var sbLines = ['[SECOND_BRAIN] ' + sbHits.length + ' relevant excerpt(s) (' + sbMethod + ') from the project knowledge base:'];
341
414
  for (var sbI = 0; sbI < sbHits.length; sbI++) {
342
415
  var sbH = sbHits[sbI];
343
416
  var sbSrc = (sbH.metadata && sbH.metadata.filePath) ? String(sbH.metadata.filePath).split('/').slice(-2).join('/') : sbH.key;
344
417
  var sbText = String(sbH.value || '').replace(/\s+/g, ' ').slice(0, 240);
345
418
  sbLines.push(' • [' + sbSrc + '] ' + sbText);
346
419
  }
347
- sbLines.push(' (deeper/semantic lookup: mcp__monomind__knowledge_search or `monomind doc search -q "..."`)');
420
+ sbLines.push(' (deeper lookup: mcp__monomind__knowledge_search or `monomind doc search -q "..."`)');
348
421
  console.log(sbLines.join('\n'));
349
422
  }
350
423
  }
@@ -358,6 +358,17 @@ module.exports = {
358
358
  var _kbLastCheck = 0;
359
359
  try { _kbLastCheck = JSON.parse(fs.readFileSync(_kbMarkerPath, 'utf-8')).ts || 0; } catch (_) {}
360
360
  if (Date.now() - _kbLastCheck > 30 * 60 * 1000) {
361
+ // Marker BEFORE the scan (not only when dirty): the 2000-stat walk
362
+ // itself is what needs rate-limiting — on a clean tree it used to
363
+ // re-run on every single session start. Fail-closed: skip when the
364
+ // marker can't be written, so an unwritable disk can't cause
365
+ // per-session scans either.
366
+ var _kbMarkerOk = false;
367
+ try {
368
+ fs.writeFileSync(_kbMarkerPath, JSON.stringify({ ts: Date.now() }), 'utf-8');
369
+ _kbMarkerOk = true;
370
+ } catch (_) {}
371
+ if (!_kbMarkerOk) throw new Error('reindex marker unwritable — skipping scan');
361
372
  // Cheap bounded scan: any document changed since the last ingest?
362
373
  var _DOC_EXT = { '.md': 1, '.txt': 1, '.pdf': 1, '.docx': 1 };
363
374
  var _kbDirty = false;
@@ -380,24 +391,20 @@ module.exports = {
380
391
  };
381
392
  _kbWalk(CWD, 0);
382
393
  if (_kbDirty) {
383
- var _kbMarkerOk = false;
384
- try {
385
- fs.writeFileSync(_kbMarkerPath, JSON.stringify({ ts: Date.now() }), 'utf-8');
386
- _kbMarkerOk = true;
387
- } catch (_) {}
388
- if (_kbMarkerOk) {
389
- var _kbSpawn = require('child_process').spawn;
390
- var _kbChild = _kbSpawn('npx', ['-y', 'monomind@latest', 'doc', 'ingest', '.'], {
391
- cwd: CWD,
392
- detached: true,
393
- stdio: 'ignore',
394
- env: process.env,
395
- shell: process.platform === 'win32',
396
- windowsHide: true,
397
- });
398
- _kbChild.unref();
399
- console.log('[KNOWLEDGE_REINDEX] changed documents detected — re-ingesting in background');
400
- }
394
+ var _kbSpawn = require('child_process').spawn;
395
+ var _kbChild = _kbSpawn('npx', ['-y', 'monomind@latest', 'doc', 'ingest', '.'], {
396
+ cwd: CWD,
397
+ detached: true,
398
+ stdio: 'ignore',
399
+ env: process.env,
400
+ shell: process.platform === 'win32',
401
+ windowsHide: true,
402
+ });
403
+ // Without an error listener, a missing npx binary emits an
404
+ // unhandled 'error' event and crashes the whole session-start hook.
405
+ _kbChild.on('error', function () { /* npx unavailable — reindex on a later session */ });
406
+ _kbChild.unref();
407
+ console.log('[KNOWLEDGE_REINDEX] changed documents detected — re-ingesting in background');
401
408
  }
402
409
  }
403
410
  }
@@ -230,7 +230,7 @@ Resolve `CTRL_URL` at runtime: `jq -r '.url // "http://localhost:4242"' "$REPO_R
230
230
  ```bash
231
231
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
232
232
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
233
- curl -s -o /dev/null -X POST "${CTRL_URL}/api/mastermind/event" \
233
+ curl -s -o /dev/null -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
234
234
  -H "Content-Type: application/json" \
235
235
  -d "$(jq -cn \
236
236
  --arg sid "$SESSION_ID" \
@@ -73,7 +73,7 @@ Set `TILLEND_EMPTY=true` **only if git shows no changes AND your self-assessment
73
73
  ```bash
74
74
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
75
75
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
76
- curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
76
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
77
77
  -H "Content-Type: application/json" \
78
78
  -d "{\"type\":\"loop:complete\",\"loopId\":\"${LOOP_ID}\",\"command\":\"/<command>\",\"mode\":\"tillend\",\"ranReps\":<current_rep>,\"reason\":\"empty-round\",\"project\":\"${REPO_ROOT}\",\"ts\":$(date +%s)000}" || true
79
79
  ```
@@ -103,7 +103,7 @@ If `current_rep >= repeat_count`:
103
103
  ```bash
104
104
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
105
105
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
106
- curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
106
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
107
107
  -H "Content-Type: application/json" \
108
108
  -d "{\"type\":\"loop:complete\",\"loopId\":\"${LOOP_ID}\",\"command\":\"/<command>\",\"ranReps\":<repeat_count>,\"project\":\"${REPO_ROOT}\",\"ts\":$(date +%s)000}" || true
109
109
  ```
@@ -151,7 +151,7 @@ Emit `loop:tick`:
151
151
  ```bash
152
152
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
153
153
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
154
- curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
154
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
155
155
  -H "Content-Type: application/json" \
156
156
  -d "{\"type\":\"loop:tick\",\"loopId\":\"${LOOP_ID}\",\"command\":\"/<command>\",\"completedRep\":<current_rep>,\"nextRep\":<next_rep>,\"nextAt\":${NEXT_AT},\"project\":\"${REPO_ROOT}\",\"ts\":$(date +%s)000}" || true
157
157
  ```
@@ -139,7 +139,7 @@ Emit `org:adapter:installed` event:
139
139
  ```bash
140
140
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
141
141
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
142
- curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
142
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
143
143
  -H "Content-Type: application/json" \
144
144
  -d "$(jq -cn --arg org "$org_name" --arg type "$adapter_type" --arg pkg "$package_name" \
145
145
  '{type:"org:adapter:installed",org:$org,adapter:$type,package:$pkg,ts:(now*1000|floor)}')" || true
@@ -51,12 +51,15 @@ stateFile=".monomind/orgs/${org_name}-state.json"
51
51
 
52
52
  jq -r '(.roles // [])[] | "• [\(.id)] \(.title) agent=\(.agent_type) reports_to=\(.reports_to // "none")"' "$orgFile"
53
53
 
54
+ # LEGACY-ORG-V1: the state file's per-agent heartbeat tracking is a v1 concept —
55
+ # v2 orgs report live status via `monomind org status`/`org report` instead.
54
56
  # Overlay runtime status from state file if present
55
57
  if [ -f "$stateFile" ]; then
56
58
  echo ""
57
59
  echo "RUNTIME STATUS:"
58
60
  jq -r '.agents // {} | to_entries[] | " \(.key): \(.value.status // "unknown") last_beat=\(.value.last_heartbeat // "never")"' "$stateFile" 2>/dev/null || true
59
61
  fi
62
+ # end LEGACY-ORG-V1
60
63
  ```
61
64
 
62
65
  Render as table:
@@ -126,7 +129,7 @@ Then emit `org:agent:hired` event to dashboard:
126
129
  ```bash
127
130
  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
128
131
  CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
129
- curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
132
+ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" \
130
133
  -H "Content-Type: application/json" \
131
134
  -d "$(jq -cn --arg org "$org_name" --arg role "$agent_id" --arg title "$title" \
132
135
  '{type:"org:agent:hired",org:$org,role:$role,title:$title,ts:(now*1000|floor)}')" || true
@@ -134,6 +137,7 @@ curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
134
137
 
135
138
  ### pause / resume
136
139
 
140
+ <!-- LEGACY-ORG-V1: the agent state/heartbeat file this pauses is a v1 concept. -->
137
141
  Update state file:
138
142
 
139
143
  ```bash
@@ -169,7 +173,7 @@ org: <org_name>
169
173
  agents_count: <N>
170
174
  ```
171
175
 
172
- Print summary and any suggested next actions (e.g. "Run /mastermind:heartbeat to trigger a manual heartbeat for this agent").
176
+ Print summary and any suggested next actions (e.g. "Run /mastermind:heartbeatv1 to trigger a manual heartbeat for this agent").
173
177
 
174
178
  ---
175
179
 
@@ -1,13 +1,16 @@
1
1
  ---
2
- name: mastermind-approve
3
- description: Mastermind approve — review and action pending approval requests from agents in a running org. Agents can request human approval before proceeding with sensitive actions.
2
+ name: mastermind-approvev1
3
+ description: "LEGACY-ORG-V1 (2026-07): v2 approvals arrive as question events in the dashboard Human Input tab — no skill needed. Mastermind approvev1 — review and action pending approval requests from agents in a running v1 (prompt-orchestrated) org. Agents can request human approval before proceeding with sensitive actions. Reach it only via `/mastermind:approvev1`."
4
4
  type: domain-skill
5
5
  default_mode: confirm
6
6
  ---
7
7
 
8
- # Mastermind Approve
8
+ # Mastermind Approve (v1, legacy)
9
9
 
10
- This skill is invoked by `mastermind:approve` or directly via `/mastermind:approve`.
10
+ > **LEGACY-ORG-V1 file-based approval queue for prompt-orchestrated v1 orgs. Reach it only via `/mastermind:approvev1`.**
11
+ > v2 approvals arrive as `question` events in the dashboard Human Input tab — there is no v2 approve skill, the dashboard UI is the approval surface. This skill remains only for orgs not yet migrated off the v1 `board_id`/`topology` config shape.
12
+
13
+ This skill is invoked by `mastermind:approvev1` or directly via `/mastermind:approvev1`.
11
14
 
12
15
  ---
13
16
 
@@ -150,7 +153,7 @@ jq --arg id "$approval_id" \
150
153
 
151
154
  2. Emit `org:approval:requested` event to dashboard (so the human is notified).
152
155
 
153
- 3. Poll for the approval decision — check the approvals file first (authoritative, updated by both the dashboard UI and `/mastermind:approve`), then fall back to memory:
156
+ 3. Poll for the approval decision — check the approvals file first (authoritative, updated by both the dashboard UI and `/mastermind:approvev1`), then fall back to memory:
154
157
  ```bash
155
158
  approvalsFile=".monomind/orgs/${orgName}-approvals.json"
156
159
  while true; do