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.
- package/README.md +37 -4
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/commands/mastermind/_repeat.md +9 -9
- package/packages/@monomind/cli/.claude/commands/mastermind/{approve.md → approvev1.md} +14 -11
- package/packages/@monomind/cli/.claude/commands/mastermind/architect.md +3 -3
- package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +4 -4
- package/packages/@monomind/cli/.claude/commands/mastermind/help.md +18 -0
- package/packages/@monomind/cli/.claude/commands/mastermind/master.md +6 -6
- package/packages/@monomind/cli/.claude/commands/mastermind/repeat.md +5 -5
- package/packages/@monomind/cli/.claude/commands/mastermind/runorg.md +20 -108
- package/packages/@monomind/cli/.claude/commands/mastermind/runorgv1.md +159 -0
- package/packages/@monomind/cli/.claude/commands/mastermind/stoporg.md +1 -1
- package/packages/@monomind/cli/.claude/helpers/control-start.cjs +52 -4
- package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +9 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +9 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +88 -15
- package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +25 -18
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/_protocol.md +1 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/_repeat.md +3 -3
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/adapters.md +1 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/agents.md +6 -2
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/{approve.md → approvev1.md} +8 -5
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/architect.md +8 -8
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/backup.md +1 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/bootstrap.md +1 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/costs.md +1 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/env.md +7 -2
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/{heartbeat.md → heartbeatv1.md} +9 -6
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/idea.md +1 -1
- 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/monitor.md +2 -2
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +5 -2
- 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/plugins.md +1 -1
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/projects.md +3 -0
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +37 -719
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorgv1.md +731 -0
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/stoporg.md +12 -5
- 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/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/knowledge/document-pipeline.js +0 -0
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +31 -12
- 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 +86 -0
- package/packages/@monomind/cli/package.json +2 -2
|
@@ -52,18 +52,22 @@ CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control
|
|
|
52
52
|
npx -y monomind@latest org stop "${org_name}" \
|
|
53
53
|
|| { mkdir -p ".monomind/orgs/${org_name}"; date -u +%Y-%m-%dT%H:%M:%SZ > ".monomind/orgs/${org_name}/stop"; }
|
|
54
54
|
# Also POST to the control server in case a dashboard-started instance is running
|
|
55
|
-
curl -s -X POST "${CTRL_URL}/api/orgs/${org_name}/stop" >/dev/null 2>&1 || true
|
|
55
|
+
curl -s -X POST -H "x-monomind-token: $(cat "${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.monomind/dashboard-token" 2>/dev/null || true)" "${CTRL_URL}/api/orgs/${org_name}/stop" >/dev/null 2>&1 || true
|
|
56
56
|
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
|
|
@@ -92,7 +98,7 @@ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
|
92
98
|
CTRL_URL=$(jq -r '.url // "http://localhost:4242"' "$REPO_ROOT/.monomind/control.json" 2>/dev/null || echo "http://localhost:4242")
|
|
93
99
|
|
|
94
100
|
# org:stop — signals dashboard to update status to stopped
|
|
95
|
-
curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
|
|
101
|
+
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)" \
|
|
96
102
|
-H "Content-Type: application/json" \
|
|
97
103
|
-d "$(jq -cn \
|
|
98
104
|
--arg session "${session_id:-manual}" \
|
|
@@ -103,7 +109,8 @@ curl -s -X POST "${CTRL_URL}/api/mastermind/event" \
|
|
|
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 });
|
|
@@ -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
|
},
|
|
Binary file
|
|
@@ -263,23 +263,26 @@ export async function bridgeStoreEntry(options) {
|
|
|
263
263
|
entry.id = id;
|
|
264
264
|
if (embedding)
|
|
265
265
|
entry.embedding = embedding;
|
|
266
|
-
// Upsert:
|
|
266
|
+
// Upsert: find any existing entry with the same key+namespace — deleted
|
|
267
|
+
// only AFTER the new entry stores successfully, so a failed store() can't
|
|
268
|
+
// destroy the existing data (old order was delete-then-store).
|
|
269
|
+
let upsertVictim = null;
|
|
267
270
|
if (options.upsert) {
|
|
268
271
|
try {
|
|
269
|
-
|
|
270
|
-
if (existing)
|
|
271
|
-
await backend.delete(existing.id);
|
|
272
|
-
}
|
|
273
|
-
catch (e) {
|
|
274
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
275
|
-
console.error('[memory-bridge] upsert failed to delete existing entry — may create a duplicate:', e);
|
|
272
|
+
upsertVictim = await backend.getByKey(namespace, key);
|
|
276
273
|
}
|
|
274
|
+
catch { /* treat as no existing entry */ }
|
|
277
275
|
}
|
|
278
|
-
// Dedup gate: skip if a near-duplicate already exists
|
|
276
|
+
// Dedup gate: skip if a near-duplicate already exists IN THIS NAMESPACE —
|
|
277
|
+
// an unscoped search let a similar entry in some other namespace swallow
|
|
278
|
+
// the store entirely (returned duplicate:true, nothing written where asked).
|
|
279
279
|
const automemCfg = getAutomemConfig();
|
|
280
280
|
if (embedding && !options.upsert) {
|
|
281
281
|
try {
|
|
282
|
-
const similar = await backend.search(embedding, {
|
|
282
|
+
const similar = await backend.search(embedding, {
|
|
283
|
+
k: 1, threshold: automemCfg.dedupThreshold,
|
|
284
|
+
filters: { type: 'exact', namespace },
|
|
285
|
+
});
|
|
283
286
|
if (similar.length > 0 && similar[0].score >= automemCfg.dedupThreshold) {
|
|
284
287
|
return { success: true, id: similar[0].entry.id, duplicate: true };
|
|
285
288
|
}
|
|
@@ -287,6 +290,15 @@ export async function bridgeStoreEntry(options) {
|
|
|
287
290
|
catch { /* non-fatal — store anyway */ }
|
|
288
291
|
}
|
|
289
292
|
await backend.store(entry);
|
|
293
|
+
if (upsertVictim && upsertVictim.id !== id) {
|
|
294
|
+
try {
|
|
295
|
+
await backend.delete(upsertVictim.id);
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
299
|
+
console.error('[memory-bridge] upsert stored new entry but failed to delete the old one — duplicate may remain:', e);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
290
302
|
await flushBackend(backend);
|
|
291
303
|
return { success: true, id, embedding: embeddingInfo };
|
|
292
304
|
}
|
|
@@ -329,9 +341,11 @@ export async function bridgeSearchEntries(options) {
|
|
|
329
341
|
// Keyword fallback — scan all entries in namespace (not just first 100)
|
|
330
342
|
// to avoid missing documents that were ingested later in the batch.
|
|
331
343
|
if (results.length === 0) {
|
|
344
|
+
// No namespace filter means ALL namespaces — collapsing to 'default'
|
|
345
|
+
// made "search everything" silently miss every non-default entry.
|
|
332
346
|
const entries = await backend.query({
|
|
333
347
|
type: 'exact',
|
|
334
|
-
namespace
|
|
348
|
+
...(namespace ? { namespace } : {}),
|
|
335
349
|
limit: 50000,
|
|
336
350
|
});
|
|
337
351
|
// Token-based matching, not whole-phrase substring: "semantic test" must
|
|
@@ -363,11 +377,16 @@ export async function bridgeSearchEntries(options) {
|
|
|
363
377
|
}
|
|
364
378
|
// Filter stale entries based on automem config — skip for knowledge
|
|
365
379
|
// namespaces (documents should remain searchable indefinitely)
|
|
380
|
+
// Stale filtering is per-RESULT namespace (documents stay searchable
|
|
381
|
+
// forever) — keying it on the query's namespace filter meant an
|
|
382
|
+
// all-namespace search silently dropped knowledge:* results past the
|
|
383
|
+
// stale cutoff.
|
|
366
384
|
const isKnowledgeNs = namespace?.startsWith('knowledge:');
|
|
367
385
|
if (!isKnowledgeNs) {
|
|
368
386
|
const { staleDays } = getAutomemConfig();
|
|
369
387
|
const staleCutoff = Date.now() - staleDays * 86400000;
|
|
370
|
-
results = results.filter((r) =>
|
|
388
|
+
results = results.filter((r) => String(r.namespace ?? '').startsWith('knowledge:')
|
|
389
|
+
|| !r._createdAt || r._createdAt > staleCutoff);
|
|
371
390
|
}
|
|
372
391
|
results.forEach((r) => delete r._createdAt);
|
|
373
392
|
return {
|