monomind 2.3.3 → 2.4.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.
- package/README.md +37 -4
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/commands/mastermind/{approve.md → approvev1.md} +12 -9
- package/packages/@monomind/cli/.claude/commands/mastermind/help.md +18 -0
- package/packages/@monomind/cli/.claude/commands/mastermind/master.md +3 -3
- package/packages/@monomind/cli/.claude/commands/mastermind/runorg.md +19 -107
- package/packages/@monomind/cli/.claude/commands/mastermind/runorgv1.md +159 -0
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +50 -4
- package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +25 -18
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/agents.md +5 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/{approve.md → approvev1.md} +8 -5
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/env.md +7 -2
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/{heartbeat.md → heartbeatv1.md} +7 -4
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/inbox.md +14 -2
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/instance.md +3 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +4 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgs.md +14 -13
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/orgstatus.md +19 -8
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/projects.md +3 -0
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +37 -725
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorgv1.md +731 -0
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/stoporg.md +10 -3
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/tasks.md +4 -1
- package/packages/@monomind/cli/README.md +37 -4
- package/packages/@monomind/cli/dist/src/commands/cleanup.js +30 -8
- package/packages/@monomind/cli/dist/src/commands/doc.js +36 -8
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +23 -10
- package/packages/@monomind/cli/dist/src/commands/org-observe.js +30 -26
- package/packages/@monomind/cli/dist/src/commands/org.js +52 -2
- package/packages/@monomind/cli/dist/src/index.js +7 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +2 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +148 -45
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +115 -68
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +45 -9
- package/packages/@monomind/cli/dist/src/orgrt/mailbox.d.ts +23 -0
- package/packages/@monomind/cli/dist/src/orgrt/mailbox.js +33 -1
- package/packages/@monomind/cli/dist/src/orgrt/migrate.d.ts +26 -0
- package/packages/@monomind/cli/dist/src/orgrt/migrate.js +111 -0
- package/packages/@monomind/cli/dist/src/orgrt/reporting.js +8 -1
- package/packages/@monomind/cli/dist/src/orgrt/session.js +8 -2
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +44 -0
- package/packages/@monomind/cli/dist/src/ui/orgs.html +2 -2
- package/packages/@monomind/cli/dist/src/ui/server.mjs +25 -4
- package/packages/@monomind/cli/package.json +2 -2
|
@@ -57,13 +57,17 @@ echo "Stop requested for org '${org_name}' (v2 daemon exits within 2s)."
|
|
|
57
57
|
```
|
|
58
58
|
- Exit.
|
|
59
59
|
|
|
60
|
+
<!-- LEGACY-ORG-V1: remove this branch when v1 orgs are gone -->
|
|
61
|
+
Everything below this point only runs for v1 orgs — v2 orgs already exited above.
|
|
62
|
+
|
|
60
63
|
If `current_status == "stopped"`:
|
|
61
64
|
- Print: "Org '<org_name>' is already stopped."
|
|
62
65
|
- Exit.
|
|
63
66
|
|
|
64
67
|
---
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
<!-- LEGACY-ORG-V1: remove this step when v1 orgs are gone -->
|
|
70
|
+
## Step 2 — Set Status to Stopped and Clear next_run (v1 only)
|
|
67
71
|
|
|
68
72
|
```bash
|
|
69
73
|
orgFile=".monomind/orgs/${org_name}.json"
|
|
@@ -74,7 +78,8 @@ jq '.status = "stopped" | if .loop then .loop.next_run = null else . end' "$orgF
|
|
|
74
78
|
|
|
75
79
|
---
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
<!-- LEGACY-ORG-V1: remove this step when v1 orgs are gone -->
|
|
82
|
+
## Step 3 — Write Stop File (for any running boss agents, v1 only)
|
|
78
83
|
|
|
79
84
|
Any persistent boss agent checks for this file at the start of each loop iteration:
|
|
80
85
|
|
|
@@ -85,6 +90,7 @@ touch ".monomind/orgs/.stops/${org_name}.stop"
|
|
|
85
90
|
|
|
86
91
|
---
|
|
87
92
|
|
|
93
|
+
<!-- LEGACY-ORG-V1: remove this step when v1 orgs are gone -->
|
|
88
94
|
## Step 4 — Emit Dashboard Events
|
|
89
95
|
|
|
90
96
|
```bash
|
|
@@ -103,7 +109,8 @@ curl -s -X POST "${CTRL_URL}/api/mastermind/event" -H "x-monomind-token: $(cat "
|
|
|
103
109
|
|
|
104
110
|
---
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
<!-- LEGACY-ORG-V1: remove this step when v1 orgs are gone -->
|
|
113
|
+
## Step 5 — Report to User (v1 only)
|
|
107
114
|
|
|
108
115
|
```
|
|
109
116
|
✓ Org "<org_name>" stopped.
|
|
@@ -35,13 +35,16 @@ If `caller` is not "command", load brain context following _protocol.md Brain Lo
|
|
|
35
35
|
## Step 1 — Load Org Config
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
+
# LEGACY-ORG-V1: this whole board/column lookup is the pre-v2 board-backed
|
|
39
|
+
# task model — boards belong to the legacy v1 runner, see runorgv1.
|
|
38
40
|
orgFile=".monomind/orgs/${org_name}.json"
|
|
39
41
|
board_id=$(jq -r '.board_id // empty' "$orgFile")
|
|
40
42
|
todo_col=$(jq -r '.todo_col_id // empty' "$orgFile")
|
|
41
43
|
doing_col=$(jq -r '.doing_col_id // empty' "$orgFile")
|
|
42
44
|
done_col=$(jq -r '.done_col_id // empty' "$orgFile")
|
|
43
45
|
|
|
44
|
-
[ -z "$board_id" ] && { echo "ERROR: org config missing board_id —
|
|
46
|
+
[ -z "$board_id" ] && { echo "ERROR: org config missing board_id — boards belong to the legacy v1 runner, see runorgv1."; exit 1; }
|
|
47
|
+
# end LEGACY-ORG-V1
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
---
|
|
@@ -102,11 +102,25 @@ monomind org run content-team --task "Build and publish 3 blog posts per week"
|
|
|
102
102
|
# assigns work to role agents, coordinates until the task completes
|
|
103
103
|
# or you stop it. Every event streams into the dashboard above.
|
|
104
104
|
|
|
105
|
-
monomind org status content-team # runtime state
|
|
105
|
+
monomind org status content-team # runtime state (detects crashed daemons)
|
|
106
106
|
monomind org stop content-team # request a graceful stop
|
|
107
|
-
monomind org list # every org + status
|
|
107
|
+
monomind org list # every org + roles, schedule, status
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Observe, steer, and let it learn
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
monomind org logs content-team --follow # live event stream in the terminal
|
|
114
|
+
monomind org report content-team # outcome, per-role tokens vs budget, assets
|
|
115
|
+
monomind org questions content-team # what agents asked via ask_human
|
|
116
|
+
monomind org answer content-team q-123 "yes" # answer live or queued — no dashboard needed
|
|
117
|
+
monomind org create blog --template content-team --goal "3 posts/week" # scaffold from a template
|
|
118
|
+
monomind org validate blog # schema + structural checks before running
|
|
119
|
+
monomind org run blog --dry-run # preview each role's exact briefing
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Orgs are self-improving: the coordinator records every run's outcome (`org_complete`), the next run is briefed on it, and all agents can query accumulated cross-run memory with `org_recall` — a scheduled org gets smarter every cycle instead of starting cold. Crashed agent sessions restart automatically with backoff.
|
|
123
|
+
|
|
110
124
|
### What runs under the hood
|
|
111
125
|
|
|
112
126
|
| What | How |
|
|
@@ -196,9 +210,28 @@ monomind org run my-team # run your first AI org (see .monomind/orgs/sa
|
|
|
196
210
|
|
|
197
211
|
---
|
|
198
212
|
|
|
213
|
+
## 📚 Second Brain — Your Documents, Retrieved by Meaning
|
|
214
|
+
|
|
215
|
+
Drop documents (Markdown, TXT, PDF, DOCX) anywhere in your project and run `monomind init` — the Second Brain activates itself. No flags, no configuration, no accounts. Everything runs on your machine: a local embedding model (MiniLM via transformers.js) and a local SQLite vector store. **Your notes never leave your computer.**
|
|
216
|
+
|
|
217
|
+
From then on, every substantive prompt you type in Claude Code is automatically answered *with your own knowledge in context* — a hook retrieves the most relevant excerpts semantically (the always-on dashboard keeps the model warm, ~60ms per lookup) and injects them before Claude starts thinking. Ask "when do new parents get time off" and the parental-leave section of your handbook is already on the table, even though you never used the word "leave".
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
monomind doc ingest ./notes # index documents (init + session-start do this automatically)
|
|
221
|
+
monomind doc search -q "pricing psychology in checkout" # semantic search, by meaning not keywords
|
|
222
|
+
monomind doc list # what's indexed
|
|
223
|
+
monomind doc export # portable OKF bundle — move your brain between machines
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Retrieval quality is a tested invariant, not a hope: a golden-set eval (paraphrase queries against notes written in different vocabulary) runs in CI with an 80% recall bar.
|
|
227
|
+
|
|
228
|
+
> **Privacy note:** the embedding model (~90MB) is fetched once from HuggingFace's CDN when your first document is indexed, then cached locally forever. That download is the only outbound request the Second Brain ever makes — your documents and queries never leave your machine. Offline at first index? Search degrades gracefully to keyword matching and `monomind doctor` tells you how to warm up later.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
199
232
|
## 🧠 Memory That Persists
|
|
200
233
|
|
|
201
|
-
Every session, every agent, every org writes to a persistent memory store — a
|
|
234
|
+
Every session, every agent, every org writes to a persistent memory store that survives across sessions — text plus embedding vectors in local SQLite (better-sqlite3, pure-WASM fallback), embedded by a local model. No cloud vector database, no API keys, no data transmission. The next time you run anything, Monomind already knows what was built, what failed, and which patterns work.
|
|
202
235
|
|
|
203
236
|
```mermaid
|
|
204
237
|
graph TD
|
|
@@ -217,7 +250,7 @@ graph TD
|
|
|
217
250
|
|
|
218
251
|
```bash
|
|
219
252
|
monomind memory store "key insight" --namespace my-project
|
|
220
|
-
monomind memory search "auth implementation" #
|
|
253
|
+
monomind memory search "auth implementation" # semantic (local embeddings) with keyword fallback
|
|
221
254
|
```
|
|
222
255
|
|
|
223
256
|
---
|
|
@@ -57,16 +57,23 @@ export function findOrphanedProjectData(baseDir, now, aggressive) {
|
|
|
57
57
|
if (name.startsWith('.'))
|
|
58
58
|
continue;
|
|
59
59
|
const dir = join(baseDir, name);
|
|
60
|
-
let mtime = 0;
|
|
61
60
|
try {
|
|
62
|
-
|
|
63
|
-
if (!st.isDirectory())
|
|
61
|
+
if (!lstatSync(dir).isDirectory())
|
|
64
62
|
continue;
|
|
65
|
-
mtime = st.mtimeMs;
|
|
66
63
|
}
|
|
67
64
|
catch {
|
|
68
65
|
continue;
|
|
69
66
|
}
|
|
67
|
+
// Staleness must consider the files writes actually touch: appends to
|
|
68
|
+
// lancedb/memory.db and origin.json refreshes do NOT bump the slug dir's
|
|
69
|
+
// own mtime, so an actively-used project would otherwise look 30d stale.
|
|
70
|
+
const mtimeOf = (p) => { try {
|
|
71
|
+
return lstatSync(p).mtimeMs;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return 0;
|
|
75
|
+
} };
|
|
76
|
+
const mtime = Math.max(mtimeOf(dir), mtimeOf(join(dir, 'origin.json')), mtimeOf(join(dir, 'lancedb', 'memory.db')), mtimeOf(join(dir, 'memory.db')));
|
|
70
77
|
const originFile = join(dir, 'origin.json');
|
|
71
78
|
let originPath = null;
|
|
72
79
|
let hasOrigin = false;
|
|
@@ -76,13 +83,28 @@ export function findOrphanedProjectData(baseDir, now, aggressive) {
|
|
|
76
83
|
}
|
|
77
84
|
catch { /* no/corrupt marker */ }
|
|
78
85
|
if (hasOrigin && originPath && existsSync(originPath)) {
|
|
86
|
+
// NOTE: the directory is *named* lancedb for historical reasons, but the
|
|
87
|
+
// current SQLite engine keeps its LIVE memory.db inside it. Only genuine
|
|
88
|
+
// LanceDB leftovers (*.lance datasets, no memory.db) are dead weight.
|
|
79
89
|
const lance = join(dir, 'lancedb');
|
|
80
|
-
if (existsSync(lance))
|
|
81
|
-
|
|
90
|
+
if (existsSync(lance) && !existsSync(join(lance, 'memory.db'))) {
|
|
91
|
+
let hasLanceData = false;
|
|
92
|
+
try {
|
|
93
|
+
hasLanceData = readdirSync(lance).some(f => f.endsWith('.lance') || f === '__manifest');
|
|
94
|
+
}
|
|
95
|
+
catch { /* unreadable — leave it */ }
|
|
96
|
+
if (hasLanceData)
|
|
97
|
+
out.push({ path: lance, description: `dead lancedb store (project: ${originPath})`, size: 0 });
|
|
98
|
+
}
|
|
82
99
|
continue;
|
|
83
100
|
}
|
|
84
|
-
if (hasOrigin) {
|
|
85
|
-
|
|
101
|
+
if (hasOrigin && originPath) {
|
|
102
|
+
// Only classify as orphaned when the origin's PARENT exists — an
|
|
103
|
+
// unmounted volume / disconnected network share makes the whole subtree
|
|
104
|
+
// vanish temporarily, and that must never count as "project deleted".
|
|
105
|
+
if (existsSync(dirname(originPath))) {
|
|
106
|
+
out.push({ path: dir, description: `orphaned project data (origin gone: ${originPath})`, size: 0 });
|
|
107
|
+
}
|
|
86
108
|
}
|
|
87
109
|
else if (aggressive || now - mtime > UNKNOWN_DIR_MAX_AGE_MS) {
|
|
88
110
|
out.push({ path: dir, description: aggressive ? 'unverifiable project data (no origin marker)' : 'unverifiable project data (untouched >30d)', size: 0 });
|
|
@@ -3,22 +3,41 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { output } from '../output.js';
|
|
6
|
+
import { getGlobalBrainDir } from '../memory/memory-bridge.js';
|
|
6
7
|
const ingestCommand = {
|
|
7
8
|
name: 'ingest',
|
|
8
9
|
description: 'Ingest documents into the knowledge base',
|
|
9
10
|
options: [
|
|
10
|
-
|
|
11
|
+
// No `default` on scope: the auto-global routing below must be able to
|
|
12
|
+
// tell "user typed --scope shared" apart from "user typed nothing" — a
|
|
13
|
+
// parser-injected default makes them indistinguishable.
|
|
14
|
+
{ name: 'scope', short: 's', description: 'Knowledge scope (default: shared; auto-routes to global for paths outside the project)', type: 'string' },
|
|
15
|
+
{ name: 'global', short: 'g', description: 'Ingest into the personal cross-project global brain (~/.monomind/global-brain)', type: 'boolean' },
|
|
11
16
|
],
|
|
12
17
|
examples: [
|
|
13
18
|
{ command: 'monomind doc ingest ./docs', description: 'Ingest all docs in a directory' },
|
|
19
|
+
{ command: 'monomind doc ingest ~/notes --global', description: 'Ingest into the global brain (auto-detected for paths outside the project)' },
|
|
14
20
|
{ command: 'monomind doc ingest report.pdf', description: 'Ingest a single file' },
|
|
15
21
|
],
|
|
16
22
|
action: async (ctx) => {
|
|
17
23
|
const target = ctx.args[0] || '.';
|
|
18
|
-
const scope = String(ctx.flags.scope || 'shared');
|
|
19
24
|
const { ingestDocument, ingestDirectory } = await import('../knowledge/document-pipeline.js');
|
|
20
25
|
const fs = await import('node:fs');
|
|
21
26
|
const resolved = path.resolve(target);
|
|
27
|
+
// Zero-decision routing: an explicit --global wins; otherwise paths OUTSIDE
|
|
28
|
+
// the current project belong to the personal brain (a project store keyed
|
|
29
|
+
// to this cwd would never see them again from another project).
|
|
30
|
+
let scope = String(ctx.flags.scope || 'shared');
|
|
31
|
+
if (ctx.flags.global === true) {
|
|
32
|
+
scope = 'global';
|
|
33
|
+
}
|
|
34
|
+
else if (!ctx.flags.scope) {
|
|
35
|
+
const relToCwd = path.relative(process.cwd(), resolved);
|
|
36
|
+
if (relToCwd.startsWith('..') || path.isAbsolute(relToCwd)) {
|
|
37
|
+
scope = 'global';
|
|
38
|
+
output.writeln(output.dim(` ${target} is outside this project — ingesting into the global brain (use --scope shared to force project scope)`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
22
41
|
const spinner = output.createSpinner({ text: 'Indexing documents...' });
|
|
23
42
|
spinner.start();
|
|
24
43
|
try {
|
|
@@ -68,9 +87,11 @@ const searchDocCommand = {
|
|
|
68
87
|
{ name: 'limit', short: 'l', description: 'Max results (default: 10)', type: 'number', default: 10 },
|
|
69
88
|
{ name: 'scope', short: 's', description: 'Knowledge scope (default: shared)', type: 'string', default: 'shared' },
|
|
70
89
|
{ name: 'min-score', description: 'Minimum similarity (default: 0.3)', type: 'number', default: 0.3 },
|
|
90
|
+
{ name: 'store', description: 'Which store(s): project | global | all (default: all — project results win ties)', type: 'string', default: 'all' },
|
|
71
91
|
],
|
|
72
92
|
examples: [
|
|
73
|
-
{ command: 'monomind doc search -q "authentication flow"', description: 'Search
|
|
93
|
+
{ command: 'monomind doc search -q "authentication flow"', description: 'Search project + global brain' },
|
|
94
|
+
{ command: 'monomind doc search -q "pricing notes" --store global', description: 'Search only the personal global brain' },
|
|
74
95
|
],
|
|
75
96
|
action: async (ctx) => {
|
|
76
97
|
const query = String(ctx.flags.query || ctx.args[0] || '');
|
|
@@ -79,10 +100,12 @@ const searchDocCommand = {
|
|
|
79
100
|
return { success: false, exitCode: 1 };
|
|
80
101
|
}
|
|
81
102
|
const { searchKnowledge } = await import('../knowledge/document-pipeline.js');
|
|
103
|
+
const storeFlag = String(ctx.flags.store || 'all');
|
|
82
104
|
const excerpts = await searchKnowledge(query, {
|
|
83
105
|
scope: String(ctx.flags.scope || 'shared'),
|
|
84
106
|
limit: Number(ctx.flags.limit || 10),
|
|
85
107
|
minScore: Number(ctx.flags['min-score'] || 0.3),
|
|
108
|
+
store: storeFlag === 'project' || storeFlag === 'global' ? storeFlag : 'all',
|
|
86
109
|
});
|
|
87
110
|
if (!excerpts.length) {
|
|
88
111
|
output.writeln(output.dim('No results found.'));
|
|
@@ -92,7 +115,8 @@ const searchDocCommand = {
|
|
|
92
115
|
output.writeln();
|
|
93
116
|
for (let i = 0; i < excerpts.length; i++) {
|
|
94
117
|
const ex = excerpts[i];
|
|
95
|
-
|
|
118
|
+
const origin = ex.scope === 'global' ? ` ${output.dim('[global]')}` : '';
|
|
119
|
+
output.writeln(`${output.highlight(`${i + 1}.`)} ${output.dim(`(${ex.similarity.toFixed(3)})`)} ${ex.filePath || 'unknown'}${origin}`);
|
|
96
120
|
const preview = ex.text.length > 200 ? ex.text.slice(0, 200) + '...' : ex.text;
|
|
97
121
|
output.writeln(` ${output.dim(preview)}`);
|
|
98
122
|
output.writeln();
|
|
@@ -105,11 +129,13 @@ const listDocCommand = {
|
|
|
105
129
|
description: 'List indexed documents',
|
|
106
130
|
options: [
|
|
107
131
|
{ name: 'scope', short: 's', description: 'Knowledge scope', type: 'string' },
|
|
132
|
+
{ name: 'global', short: 'g', description: 'List the personal cross-project global brain', type: 'boolean' },
|
|
108
133
|
],
|
|
109
134
|
action: async (ctx) => {
|
|
110
135
|
const { listDocuments } = await import('../knowledge/document-pipeline.js');
|
|
111
|
-
const
|
|
112
|
-
const
|
|
136
|
+
const isGlobal = ctx.flags.global === true;
|
|
137
|
+
const scope = isGlobal ? 'global' : ctx.flags.scope ? String(ctx.flags.scope) : undefined;
|
|
138
|
+
const docs = listDocuments(isGlobal ? getGlobalBrainDir() : process.cwd(), scope);
|
|
113
139
|
if (!docs.length) {
|
|
114
140
|
output.writeln(output.dim('No documents indexed. Run: monomind doc ingest <path>'));
|
|
115
141
|
return { success: true, data: [] };
|
|
@@ -133,15 +159,17 @@ const exportDocCommand = {
|
|
|
133
159
|
options: [
|
|
134
160
|
{ name: 'output', short: 'o', description: 'Output directory', type: 'string', default: '.monomind/knowledge-export' },
|
|
135
161
|
{ name: 'scope', short: 's', description: 'Knowledge scope (default: shared)', type: 'string', default: 'shared' },
|
|
162
|
+
{ name: 'global', short: 'g', description: 'Export the personal cross-project global brain (portable between machines)', type: 'boolean' },
|
|
136
163
|
],
|
|
137
164
|
action: async (ctx) => {
|
|
138
165
|
const { exportToOKF } = await import('../knowledge/document-pipeline.js');
|
|
139
166
|
const outDir = path.resolve(String(ctx.flags.output || '.monomind/knowledge-export'));
|
|
140
|
-
const
|
|
167
|
+
const isGlobal = ctx.flags.global === true;
|
|
168
|
+
const scope = isGlobal ? 'global' : String(ctx.flags.scope || 'shared');
|
|
141
169
|
const spinner = output.createSpinner({ text: 'Exporting to OKF...' });
|
|
142
170
|
spinner.start();
|
|
143
171
|
try {
|
|
144
|
-
const result = await exportToOKF(outDir, process.cwd(), scope);
|
|
172
|
+
const result = await exportToOKF(outDir, isGlobal ? getGlobalBrainDir() : process.cwd(), scope);
|
|
145
173
|
spinner.succeed(`Exported ${result.exported} documents to ${result.outputDir}`);
|
|
146
174
|
return { success: true, data: result };
|
|
147
175
|
}
|
|
@@ -52,19 +52,32 @@ export async function checkSecondBrainModel() {
|
|
|
52
52
|
if (!existsSync(join(process.cwd(), '.monomind', 'knowledge', 'chunks.jsonl'))) {
|
|
53
53
|
return { name, status: 'pass', message: 'No knowledge base in this project (nothing to check)' };
|
|
54
54
|
}
|
|
55
|
-
let
|
|
55
|
+
let entryPath = null;
|
|
56
56
|
try {
|
|
57
|
-
// exports map blocks package.json — resolve the entry file
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
// exports map blocks package.json — resolve the entry file, then walk UP
|
|
58
|
+
// with path ops (no separator-sensitive regex: '/dist/'.replace broke on
|
|
59
|
+
// Windows paths, and any install layout without a dist segment).
|
|
60
|
+
// import.meta.resolve is missing on Node 20.0–20.5; fall back to CJS resolve.
|
|
61
|
+
const viaEsm = import.meta.resolve?.('@huggingface/transformers');
|
|
62
|
+
if (viaEsm)
|
|
63
|
+
entryPath = fileURLToPath(await viaEsm);
|
|
64
|
+
}
|
|
65
|
+
catch { /* try CJS below */ }
|
|
66
|
+
if (!entryPath) {
|
|
67
|
+
try {
|
|
68
|
+
const { createRequire } = await import('node:module');
|
|
69
|
+
entryPath = createRequire(import.meta.url).resolve('@huggingface/transformers');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { name, status: 'warn', message: '@huggingface/transformers not installed — semantic search degraded to keyword matching', fix: 'reinstall monomind (the model dependency is optional and may have failed to build)' };
|
|
73
|
+
}
|
|
65
74
|
}
|
|
75
|
+
// Walk up from the entry file to the package root (the dir named 'transformers').
|
|
76
|
+
let pkgDir = dirname(entryPath);
|
|
77
|
+
while (pkgDir !== dirname(pkgDir) && !pkgDir.endsWith('transformers'))
|
|
78
|
+
pkgDir = dirname(pkgDir);
|
|
66
79
|
const modelCache = join(pkgDir, '.cache', 'Xenova');
|
|
67
|
-
if (existsSync(modelCache)) {
|
|
80
|
+
if (pkgDir.endsWith('transformers') && existsSync(modelCache)) {
|
|
68
81
|
return { name, status: 'pass', message: 'Local embedding model cached — semantic search active' };
|
|
69
82
|
}
|
|
70
83
|
return { name, status: 'warn', message: 'Embedding model not downloaded yet — searches use keyword matching until it is', fix: 'run once while online: monomind doc search -q "warmup" (downloads ~90MB locally, one time)' };
|
|
@@ -7,7 +7,7 @@ import { output } from '../output.js';
|
|
|
7
7
|
import { ORG_DIR, OrgDefSchema } from '../orgrt/types.js';
|
|
8
8
|
import { formatEvent, listRunDirs, readHistory, readRunEvents, summarizeRun } from '../orgrt/reporting.js';
|
|
9
9
|
import { ORG_TEMPLATES, buildFromTemplate } from '../orgrt/templates.js';
|
|
10
|
-
import {
|
|
10
|
+
import { checkOrgStructure } from '../orgrt/migrate.js';
|
|
11
11
|
import { listOrgConfigFiles, validateOrgName } from './org.js';
|
|
12
12
|
const log = (text) => { console.log(text); };
|
|
13
13
|
/** Validate org config(s) against OrgDefSchema — the exact parse `org run`/`org serve`
|
|
@@ -42,23 +42,7 @@ export const validateAction = async (ctx) => {
|
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
44
|
const def = OrgDefSchema.parse(JSON.parse(readFileSync(path, 'utf8')));
|
|
45
|
-
|
|
46
|
-
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
47
|
-
if (dupes.length)
|
|
48
|
-
errors.push(`duplicate role id(s): ${[...new Set(dupes)].join(', ')}`);
|
|
49
|
-
const roots = def.roles.filter(r => r.reports_to === null);
|
|
50
|
-
if (roots.length === 0)
|
|
51
|
-
errors.push('no root role — exactly one role must have reports_to: null');
|
|
52
|
-
if (roots.length > 1)
|
|
53
|
-
errors.push(`multiple root roles (${roots.map(r => r.id).join(', ')}) — exactly one may have reports_to: null`);
|
|
54
|
-
for (const r of def.roles) {
|
|
55
|
-
if (r.reports_to !== null && !ids.includes(r.reports_to))
|
|
56
|
-
errors.push(`role "${r.id}": reports_to "${r.reports_to}" matches no role id`);
|
|
57
|
-
if (r.reports_to === r.id)
|
|
58
|
-
errors.push(`role "${r.id}" reports to itself`);
|
|
59
|
-
}
|
|
60
|
-
if (def.schedule != null && parseSchedule(def.schedule) === null)
|
|
61
|
-
errors.push(`schedule "${def.schedule}" is not parseable — use "<N>s", "<N>m", or "<N>h"`);
|
|
45
|
+
errors.push(...checkOrgStructure(def));
|
|
62
46
|
if (def.name !== stem)
|
|
63
47
|
warnings.push(`def.name "${def.name}" differs from filename — the runtime addresses this org as "${stem}"`);
|
|
64
48
|
}
|
|
@@ -80,9 +64,13 @@ export const validateAction = async (ctx) => {
|
|
|
80
64
|
? { success: false, message: `${failed} of ${files.length} org config(s) failed validation` }
|
|
81
65
|
: { success: true, message: `${files.length} org config(s) valid` };
|
|
82
66
|
};
|
|
67
|
+
// Run ids are joined into filesystem paths — enforce the daemon's own id shape
|
|
68
|
+
// so a crafted --run can't traverse out of the org directory (same reason the
|
|
69
|
+
// org-name guard exists).
|
|
70
|
+
const RUN_ID_RE = /^run-[A-Za-z0-9-]+$/;
|
|
83
71
|
const resolveRun = (cwd, name, runFlag) => {
|
|
84
72
|
if (typeof runFlag === 'string' && runFlag)
|
|
85
|
-
return runFlag;
|
|
73
|
+
return RUN_ID_RE.test(runFlag) ? runFlag : null;
|
|
86
74
|
return listRunDirs(cwd, name)[0] ?? null;
|
|
87
75
|
};
|
|
88
76
|
/** `org logs <name> [--run id] [--role r] [--follow]` — formatted bus.jsonl tail. */
|
|
@@ -103,14 +91,19 @@ export const logsAction = async (ctx, name) => {
|
|
|
103
91
|
if (!existsSync(file))
|
|
104
92
|
return;
|
|
105
93
|
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
106
|
-
for (
|
|
94
|
+
for (let i = seenLines; i < lines.length; i++) {
|
|
107
95
|
try {
|
|
108
|
-
show(JSON.parse(
|
|
109
|
-
seenLines
|
|
96
|
+
show(JSON.parse(lines[i]));
|
|
97
|
+
seenLines = i + 1;
|
|
110
98
|
}
|
|
111
99
|
catch {
|
|
112
|
-
|
|
113
|
-
|
|
100
|
+
// Only the FINAL line can be a partial mid-append write worth
|
|
101
|
+
// retrying; a corrupt interior line would otherwise stall the tail
|
|
102
|
+
// forever — skip it and keep going.
|
|
103
|
+
if (i === lines.length - 1)
|
|
104
|
+
break;
|
|
105
|
+
seenLines = i + 1;
|
|
106
|
+
}
|
|
114
107
|
}
|
|
115
108
|
};
|
|
116
109
|
drain();
|
|
@@ -249,10 +242,21 @@ export const answerAction = async (ctx, name) => {
|
|
|
249
242
|
}
|
|
250
243
|
}
|
|
251
244
|
// Offline path: mirror daemon.answerQuestion's org-not-running branch.
|
|
252
|
-
|
|
245
|
+
// RE-READ and merge by questionId just before writing — the pre-fetch
|
|
246
|
+
// snapshot can be up to 10s stale (live-delivery timeout), and rewriting
|
|
247
|
+
// from it would delete questions the daemon appended meanwhile and revert
|
|
248
|
+
// answers it recorded (atomic rename prevents torn writes, not lost updates).
|
|
249
|
+
const fresh = readQuestions(ctx.cwd, name);
|
|
250
|
+
const freshQ = fresh.find(x => x.questionId === questionId);
|
|
251
|
+
if (freshQ && freshQ.answer !== null) {
|
|
252
|
+
return { success: false, message: `question "${questionId}" was answered while this command was running` };
|
|
253
|
+
}
|
|
254
|
+
const merged = fresh.some(x => x.questionId === questionId)
|
|
255
|
+
? fresh.map(x => x.questionId === questionId ? { ...x, answer, answeredAt: Date.now() } : x)
|
|
256
|
+
: [...fresh, { ...q, answer, answeredAt: Date.now() }];
|
|
253
257
|
const dest = join(ctx.cwd, ORG_DIR, name, 'questions.json');
|
|
254
258
|
const tmp = `${dest}.${process.pid}.tmp`;
|
|
255
|
-
writeFileSync(tmp, JSON.stringify({ questions:
|
|
259
|
+
writeFileSync(tmp, JSON.stringify({ questions: merged }, null, 2));
|
|
256
260
|
const { renameSync } = await import('node:fs');
|
|
257
261
|
renameSync(tmp, dest);
|
|
258
262
|
const { queueMessage } = await import('../orgrt/inbox.js');
|
|
@@ -5,6 +5,7 @@ import { output } from '../output.js';
|
|
|
5
5
|
import { OrgDaemon } from '../orgrt/daemon.js';
|
|
6
6
|
import { startOrgServer } from '../orgrt/server.js';
|
|
7
7
|
import { ORG_DIR, OrgDefSchema } from '../orgrt/types.js';
|
|
8
|
+
import { migrateOrgFile } from '../orgrt/migrate.js';
|
|
8
9
|
const log = (text) => { console.log(text); };
|
|
9
10
|
/** Org names are used to build filesystem paths under .monomind/orgs — reject
|
|
10
11
|
* anything that isn't a plain identifier to prevent path traversal (e.g.
|
|
@@ -31,8 +32,13 @@ const ORG_ARTIFACT_SUFFIXES = [
|
|
|
31
32
|
'-approval-comments', '-skills',
|
|
32
33
|
];
|
|
33
34
|
export function listOrgConfigFiles(orgsDir) {
|
|
35
|
+
// endsWith, not includes: substring matching hid legitimate orgs whose NAME
|
|
36
|
+
// merely contains an artifact suffix anywhere (e.g. "state-machine.json",
|
|
37
|
+
// "issues-triage.json") — and anything hidden here is also invisible to
|
|
38
|
+
// run/list/serve while `org delete <sibling>` would still remove its files.
|
|
34
39
|
return readdirSync(orgsDir)
|
|
35
|
-
.filter(f => f.endsWith('.json') && !f.startsWith('._') && !
|
|
40
|
+
.filter(f => f.endsWith('.json') && !f.startsWith('._') && !f.endsWith('.v1.json')
|
|
41
|
+
&& !ORG_ARTIFACT_SUFFIXES.some(suf => f.endsWith(`${suf}.json`)));
|
|
36
42
|
}
|
|
37
43
|
/** Remove a lingering stopfile so a fresh `org run` doesn't self-terminate. */
|
|
38
44
|
export const clearStopfile = (cwd, name) => {
|
|
@@ -152,6 +158,11 @@ const statusAction = async (ctx) => {
|
|
|
152
158
|
if (!validated.ok)
|
|
153
159
|
return validated.result;
|
|
154
160
|
name = validated.name;
|
|
161
|
+
// A named org that doesn't exist must error, not report "never run" with exit 0.
|
|
162
|
+
if (!existsSync(join(ctx.cwd, ORG_DIR, `${name}.json`))) {
|
|
163
|
+
log(output.error(`Org not found: ${name}`));
|
|
164
|
+
return { success: false, message: 'org not found' };
|
|
165
|
+
}
|
|
155
166
|
}
|
|
156
167
|
const orgDir = join(ctx.cwd, ORG_DIR);
|
|
157
168
|
const targets = name ? [name] : (existsSync(orgDir)
|
|
@@ -385,6 +396,40 @@ const markCompleteAction = async (ctx) => {
|
|
|
385
396
|
return { success: false, message: 'server unreachable' };
|
|
386
397
|
}
|
|
387
398
|
};
|
|
399
|
+
const migrateAction = async (ctx) => {
|
|
400
|
+
const validated = validateOrgName(ctx.args[0]);
|
|
401
|
+
if (!validated.ok)
|
|
402
|
+
return validated.result;
|
|
403
|
+
const name = validated.name;
|
|
404
|
+
const cwd = ctx.cwd;
|
|
405
|
+
const cfgPath = join(cwd, ORG_DIR, `${name}.json`);
|
|
406
|
+
if (!existsSync(cfgPath)) {
|
|
407
|
+
log(output.error(`Org not found: ${name}`));
|
|
408
|
+
return { success: false, message: 'org not found' };
|
|
409
|
+
}
|
|
410
|
+
if (isOrgRunning(cwd, name)) {
|
|
411
|
+
log(output.error(`Org "${name}" is currently running — stop it first, then migrate.`));
|
|
412
|
+
return { success: false, message: 'org is running' };
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const outcome = migrateOrgFile(cfgPath, join(cwd, ORG_DIR, `${name}.v1.json`));
|
|
416
|
+
if (outcome.status === 'already-v2') {
|
|
417
|
+
log(output.info(`${name}: already v2 — nothing to migrate.`));
|
|
418
|
+
return { success: true, message: 'already v2' };
|
|
419
|
+
}
|
|
420
|
+
log(output.success(`${name}: migrated to v2 (backup: ${name}.v1.json)`));
|
|
421
|
+
for (const d of outcome.dropped)
|
|
422
|
+
log(output.info(` dropped v1 field: ${d}`));
|
|
423
|
+
for (const n of outcome.notes)
|
|
424
|
+
log(output.info(` ${n}`));
|
|
425
|
+
log(output.info(` run it with: monomind org run ${name}`));
|
|
426
|
+
return { success: true, message: `migrated ${name}` };
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
log(output.error(`Cannot migrate ${name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
430
|
+
return { success: false, message: 'migration produced an invalid config' };
|
|
431
|
+
}
|
|
432
|
+
};
|
|
388
433
|
export const orgCommand = {
|
|
389
434
|
name: 'org',
|
|
390
435
|
description: 'SDK-based org runtime — run agent organizations as a controlled daemon',
|
|
@@ -495,6 +540,11 @@ export const orgCommand = {
|
|
|
495
540
|
return validateAction(ctx);
|
|
496
541
|
},
|
|
497
542
|
},
|
|
543
|
+
{
|
|
544
|
+
name: 'migrate', description: 'Convert a v1 org config (topology/board/loop) to the v2 daemon shape',
|
|
545
|
+
examples: [{ command: 'monomind org migrate growth', description: 'Migrate one org; original saved as growth.v1.json' }],
|
|
546
|
+
action: migrateAction,
|
|
547
|
+
},
|
|
498
548
|
{ name: 'list', description: 'List all orgs in the current project', action: listAction },
|
|
499
549
|
{
|
|
500
550
|
name: 'delete', description: 'Delete an org and all its data',
|
|
@@ -511,7 +561,7 @@ export const orgCommand = {
|
|
|
511
561
|
// index.ts's dispatcher never prints result.message on a failed action —
|
|
512
562
|
// it only exits with result.exitCode — so this must log itself or bare
|
|
513
563
|
// `monomind org` exits silently with code 1 and zero output.
|
|
514
|
-
const message = 'usage: monomind org <run|stop|status|serve|test-loop|logs|report|questions|answer|create|validate|list|delete|mark-complete>';
|
|
564
|
+
const message = 'usage: monomind org <run|stop|status|serve|test-loop|logs|report|questions|answer|create|validate|migrate|list|delete|mark-complete>';
|
|
515
565
|
log(output.error(message));
|
|
516
566
|
return { success: false, message };
|
|
517
567
|
},
|
|
@@ -220,6 +220,13 @@ export class CLI {
|
|
|
220
220
|
this.output.printDebug(`Completed in ${Date.now() - startTime}ms`);
|
|
221
221
|
}
|
|
222
222
|
if (result && !result.success) {
|
|
223
|
+
// Always surface the failure reason: many actions return
|
|
224
|
+
// { success: false, message } without logging, and exiting 1 with
|
|
225
|
+
// zero output left users guessing (swarm finding #12). Actions that
|
|
226
|
+
// already printed a rich error produce one extra terse summary line
|
|
227
|
+
// — acceptable; silence is not.
|
|
228
|
+
if (result.message)
|
|
229
|
+
this.output.printError(result.message);
|
|
223
230
|
process.exit(result.exitCode || 1);
|
|
224
231
|
}
|
|
225
232
|
}
|
|
@@ -43,6 +43,8 @@ export declare function searchKnowledge(query: string, opts?: {
|
|
|
43
43
|
limit?: number;
|
|
44
44
|
minScore?: number;
|
|
45
45
|
rootDir?: string;
|
|
46
|
+
/** which store(s): project-only, global-only, or both (default). */
|
|
47
|
+
store?: 'project' | 'global' | 'all';
|
|
46
48
|
}): Promise<KnowledgeExcerpt[]>;
|
|
47
49
|
export declare function listDocuments(rootDir?: string, scope?: string): DocumentMeta[];
|
|
48
50
|
export declare function removeDocument(filePath: string, scope?: string, rootDir?: string): Promise<void>;
|