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
|
@@ -63,6 +63,12 @@ export class OrgDaemon {
|
|
|
63
63
|
const running = { def, run, bus, agents: new Map(), busEvents: () => [...collected] };
|
|
64
64
|
this.orgs.set(name, running);
|
|
65
65
|
const perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
|
|
66
|
+
// Single boss-selection rule for kickoff AND org_complete gating — the
|
|
67
|
+
// session layer previously keyed the tool on reports_to===null while the
|
|
68
|
+
// kickoff went to (type==='boss' || reports_to===null || roles[0]), so a
|
|
69
|
+
// fallback-selected boss could be told to call org_complete without having
|
|
70
|
+
// the tool.
|
|
71
|
+
const bossRole = def.roles.find(r => r.type === 'boss' || r.reports_to === null) ?? def.roles[0];
|
|
66
72
|
for (const role of def.roles) {
|
|
67
73
|
const mailbox = new Mailbox();
|
|
68
74
|
const policy = new PolicyEngine(role.id, { maxTokens: perRoleBudget, ...(role.policy ?? {}) }, bus, cwd);
|
|
@@ -72,9 +78,11 @@ export class OrgDaemon {
|
|
|
72
78
|
maxTurns: def.run_config.max_turns_per_message,
|
|
73
79
|
deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
|
|
74
80
|
askHuman: (r, question) => this.askHuman(name, r, question),
|
|
75
|
-
onComplete:
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
onComplete: role.id === bossRole.id
|
|
82
|
+
? (r, outcome, summary) => {
|
|
83
|
+
bus.emit({ type: 'status', from: r, reason: 'org-complete', msg: `run outcome: ${outcome}`, data: { outcome, summary } });
|
|
84
|
+
}
|
|
85
|
+
: undefined,
|
|
78
86
|
recall: async (r, q) => {
|
|
79
87
|
const answer = await this.recallOrgMemory(name, def, q);
|
|
80
88
|
bus.emit({ type: 'status', from: r, reason: 'org-recall', msg: `recall: ${q.slice(0, 80)}`, data: { hits: answer.hits } });
|
|
@@ -98,10 +106,18 @@ export class OrgDaemon {
|
|
|
98
106
|
return;
|
|
99
107
|
}
|
|
100
108
|
catch (err) {
|
|
109
|
+
// Drop the crashed session's stale waker immediately: a push()
|
|
110
|
+
// during the backoff window must queue for the NEXT session, not
|
|
111
|
+
// wake the dead generator to swallow it.
|
|
112
|
+
mailbox.detach();
|
|
101
113
|
const message = err instanceof Error ? err.message : String(err);
|
|
102
114
|
const crash = () => {
|
|
103
115
|
runtime.status = 'crashed';
|
|
104
116
|
runtime.error = message;
|
|
117
|
+
// Close the mailbox so deliver()/receiveRemote() report a real
|
|
118
|
+
// error instead of pushing into a queue no session will read
|
|
119
|
+
// (and returning a false "delivered" receipt to the sender).
|
|
120
|
+
mailbox.close();
|
|
105
121
|
bus.emit({
|
|
106
122
|
type: 'audit', from: role.id,
|
|
107
123
|
msg: `agent "${role.id}" crashed: ${message}`,
|
|
@@ -127,7 +143,7 @@ export class OrgDaemon {
|
|
|
127
143
|
})();
|
|
128
144
|
running.agents.set(role.id, runtime);
|
|
129
145
|
}
|
|
130
|
-
const boss =
|
|
146
|
+
const boss = bossRole;
|
|
131
147
|
// Cross-run memory: brief the coordinator on the previous run so scheduled
|
|
132
148
|
// orgs accumulate instead of starting cold every interval.
|
|
133
149
|
const prev = readHistory(this.root, name).at(-1);
|
|
@@ -195,6 +211,10 @@ export class OrgDaemon {
|
|
|
195
211
|
return `ERROR: unknown recipient "${toQualified}" (known: ${[...(targetOrg?.agents.keys() ?? this.orgs.keys())].join(', ')})`;
|
|
196
212
|
}
|
|
197
213
|
const targetAgent = targetOrg.agents.get(targetRole);
|
|
214
|
+
if (targetAgent.status === 'crashed') {
|
|
215
|
+
src?.bus.emit({ type: 'audit', from: fromRole, to: toQualified, msg: `undeliverable: ${subject}`, reason: 'recipient crashed (retry budget exhausted)' });
|
|
216
|
+
return `ERROR: recipient "${toQualified}" crashed and will not recover this run — message not delivered (${targetAgent.error ?? 'unknown error'})`;
|
|
217
|
+
}
|
|
198
218
|
if (targetAgent.mailbox.isClosed) {
|
|
199
219
|
// The org is mid-shutdown: mailboxes close before the org is removed
|
|
200
220
|
// from `this.orgs`, so a message can arrive in that window. push() would
|
|
@@ -262,6 +282,8 @@ export class OrgDaemon {
|
|
|
262
282
|
const agent = org.agents.get(toRole);
|
|
263
283
|
if (!agent)
|
|
264
284
|
return { ok: false, error: `role "${toRole}" not found in org "${toOrg}"` };
|
|
285
|
+
if (agent.status === 'crashed')
|
|
286
|
+
return { ok: false, error: `role "${toRole}" in org "${toOrg}" crashed and will not recover this run` };
|
|
265
287
|
if (agent.mailbox.isClosed)
|
|
266
288
|
return { ok: false, error: `role "${toRole}" in org "${toOrg}" is shutting down` };
|
|
267
289
|
org.bus.emit({ type: 'xorg', from: fromQualified, to: `${toOrg}:${toRole}`, subject, msg: body });
|
|
@@ -364,6 +386,11 @@ export class OrgDaemon {
|
|
|
364
386
|
// is already gone and no-ops instead of re-running the whole shutdown and
|
|
365
387
|
// double-emitting 'org stopped' (duplicate org:complete/session:complete).
|
|
366
388
|
this.orgs.delete(name);
|
|
389
|
+
// Capture THIS run's forwarder now: an autoWake-restart of the same org
|
|
390
|
+
// during the long tail below (agent wait, flush, history write) would
|
|
391
|
+
// register a NEW forwarder under the same name — settling/unsubscribing
|
|
392
|
+
// that one would sever the new run's dashboard stream.
|
|
393
|
+
const forwarder = this.forwarders.get(name);
|
|
367
394
|
this.leases.get(name)?.stop();
|
|
368
395
|
this.leases.delete(name);
|
|
369
396
|
for (const a of org.agents.values())
|
|
@@ -402,14 +429,23 @@ export class OrgDaemon {
|
|
|
402
429
|
// the "org stopped" event above triggers the forwarder's final org:complete /
|
|
403
430
|
// session:complete POST — without waiting for it here, the CLI process can exit
|
|
404
431
|
// (and kill the in-flight fetch) before that last event reaches the dashboard,
|
|
405
|
-
// leaving the run stuck showing "running" forever.
|
|
406
|
-
|
|
432
|
+
// leaving the run stuck showing "running" forever. Bounded: a stalled
|
|
433
|
+
// dashboard must not hang org shutdown indefinitely.
|
|
407
434
|
if (forwarder) {
|
|
408
|
-
await
|
|
435
|
+
await Promise.race([
|
|
436
|
+
forwarder.settle(),
|
|
437
|
+
new Promise(r => { const t = setTimeout(r, 5_000); t.unref?.(); }),
|
|
438
|
+
]);
|
|
409
439
|
forwarder.unsubscribe();
|
|
410
|
-
|
|
440
|
+
// Only remove from the map if it's still OURS — an autoWake-restart may
|
|
441
|
+
// have registered the new run's forwarder under this name meanwhile.
|
|
442
|
+
if (this.forwarders.get(name) === forwarder)
|
|
443
|
+
this.forwarders.delete(name);
|
|
411
444
|
}
|
|
412
|
-
|
|
445
|
+
// Same guard for runtime.json: if a new run started during shutdown, its
|
|
446
|
+
// 'running' record must not be overwritten with this old run's 'stopped'.
|
|
447
|
+
if (!this.orgs.has(name))
|
|
448
|
+
this.persistState(name, 'stopped', org.run);
|
|
413
449
|
}
|
|
414
450
|
async stopAll() {
|
|
415
451
|
await Promise.all([...this.orgs.keys()].map(n => this.stopOrg(n)));
|
|
@@ -17,9 +17,32 @@ export declare class Mailbox {
|
|
|
17
17
|
private queue;
|
|
18
18
|
private wake;
|
|
19
19
|
private closed;
|
|
20
|
+
/** Bumped when a new stream() starts; stale generators see the mismatch and exit. */
|
|
21
|
+
private generation;
|
|
20
22
|
push(text: string): void;
|
|
21
23
|
close(): void;
|
|
22
24
|
get isClosed(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Detach the current waker WITHOUT resolving it. Called between sessions
|
|
27
|
+
* (maxTurns restart, crash backoff): the dead session's generator may still
|
|
28
|
+
* be parked on `wake` inside an abandoned next() — if a push() during that
|
|
29
|
+
* window resolved it, the stale generator would shift() the message and
|
|
30
|
+
* yield it into a promise nobody reads (silent loss, after deliver()
|
|
31
|
+
* already returned a "delivered" receipt). With the waker dropped, such a
|
|
32
|
+
* push only queues; the replacement session's stream() drains it. The
|
|
33
|
+
* parked stale generator is never resumed and becomes garbage with its
|
|
34
|
+
* session.
|
|
35
|
+
*/
|
|
36
|
+
detach(): void;
|
|
37
|
+
/**
|
|
38
|
+
* One live generator at a time: each stream() call bumps `generation`, and
|
|
39
|
+
* a stale generator that ever resumes exits immediately without touching
|
|
40
|
+
* the queue. Values are shift()ed at yield time — once the consumer's
|
|
41
|
+
* next() resolves with a message it counts as delivered (matching SDK
|
|
42
|
+
* behavior: a session may consume a message and end without ever resuming
|
|
43
|
+
* the generator; redelivering would duplicate work and can livelock the
|
|
44
|
+
* restart loop).
|
|
45
|
+
*/
|
|
23
46
|
stream(sessionId?: string): AsyncGenerator<OrgUserMessage>;
|
|
24
47
|
}
|
|
25
48
|
//# sourceMappingURL=mailbox.d.ts.map
|
|
@@ -7,6 +7,8 @@ export class Mailbox {
|
|
|
7
7
|
queue = [];
|
|
8
8
|
wake = null;
|
|
9
9
|
closed = false;
|
|
10
|
+
/** Bumped when a new stream() starts; stale generators see the mismatch and exit. */
|
|
11
|
+
generation = 0;
|
|
10
12
|
push(text) {
|
|
11
13
|
if (this.closed)
|
|
12
14
|
return;
|
|
@@ -20,19 +22,49 @@ export class Mailbox {
|
|
|
20
22
|
this.wake = null;
|
|
21
23
|
}
|
|
22
24
|
get isClosed() { return this.closed; }
|
|
25
|
+
/**
|
|
26
|
+
* Detach the current waker WITHOUT resolving it. Called between sessions
|
|
27
|
+
* (maxTurns restart, crash backoff): the dead session's generator may still
|
|
28
|
+
* be parked on `wake` inside an abandoned next() — if a push() during that
|
|
29
|
+
* window resolved it, the stale generator would shift() the message and
|
|
30
|
+
* yield it into a promise nobody reads (silent loss, after deliver()
|
|
31
|
+
* already returned a "delivered" receipt). With the waker dropped, such a
|
|
32
|
+
* push only queues; the replacement session's stream() drains it. The
|
|
33
|
+
* parked stale generator is never resumed and becomes garbage with its
|
|
34
|
+
* session.
|
|
35
|
+
*/
|
|
36
|
+
detach() { this.wake = null; }
|
|
37
|
+
/**
|
|
38
|
+
* One live generator at a time: each stream() call bumps `generation`, and
|
|
39
|
+
* a stale generator that ever resumes exits immediately without touching
|
|
40
|
+
* the queue. Values are shift()ed at yield time — once the consumer's
|
|
41
|
+
* next() resolves with a message it counts as delivered (matching SDK
|
|
42
|
+
* behavior: a session may consume a message and end without ever resuming
|
|
43
|
+
* the generator; redelivering would duplicate work and can livelock the
|
|
44
|
+
* restart loop).
|
|
45
|
+
*/
|
|
23
46
|
async *stream(sessionId = '') {
|
|
47
|
+
const gen = ++this.generation;
|
|
48
|
+
// Drop (never resolve) any stale waker — see detach().
|
|
49
|
+
this.wake = null;
|
|
24
50
|
while (true) {
|
|
25
51
|
while (this.queue.length > 0) {
|
|
52
|
+
if (gen !== this.generation)
|
|
53
|
+
return; // superseded — leave the queue for the live generator
|
|
26
54
|
yield {
|
|
27
55
|
type: 'user',
|
|
28
56
|
message: { role: 'user', content: this.queue.shift() },
|
|
29
57
|
parent_tool_use_id: null,
|
|
30
58
|
session_id: sessionId,
|
|
31
59
|
};
|
|
60
|
+
if (gen !== this.generation)
|
|
61
|
+
return;
|
|
32
62
|
}
|
|
33
|
-
if (this.closed)
|
|
63
|
+
if (this.closed || gen !== this.generation)
|
|
34
64
|
return;
|
|
35
65
|
await new Promise(r => { this.wake = r; });
|
|
66
|
+
if (gen !== this.generation)
|
|
67
|
+
return;
|
|
36
68
|
}
|
|
37
69
|
}
|
|
38
70
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type OrgDef } from './types.js';
|
|
2
|
+
/** The structural invariants the runtime assumes but the Zod schema can't
|
|
3
|
+
* express: unique role ids, exactly one root (reports_to: null), every
|
|
4
|
+
* non-root's reports_to resolving to another role (and not itself), and a
|
|
5
|
+
* parseable schedule. Shared by `org validate` (validateAction) and
|
|
6
|
+
* `migrateOrgFile`, which refuses to write a migrated config that still
|
|
7
|
+
* fails these checks. */
|
|
8
|
+
export declare function checkOrgStructure(def: Pick<OrgDef, 'roles' | 'schedule'>): string[];
|
|
9
|
+
export declare function migrateOrgConfig(raw: Record<string, unknown>): {
|
|
10
|
+
def: Record<string, unknown>;
|
|
11
|
+
dropped: string[];
|
|
12
|
+
notes: string[];
|
|
13
|
+
};
|
|
14
|
+
/** Reads `cfgPath`, migrates it, and (unless already v2) backs up the
|
|
15
|
+
* original to `backupPath` and overwrites `cfgPath` with the migrated def.
|
|
16
|
+
* Throws on an unmigratable config (schema failure inside migrateOrgConfig,
|
|
17
|
+
* or a structural violation caught here) — caller decides how to report it.
|
|
18
|
+
* Refuses to write when the migrated def fails checkOrgStructure, so a v1
|
|
19
|
+
* config with e.g. all-null reports_to fails cleanly instead of silently
|
|
20
|
+
* writing a config the runtime can't start. */
|
|
21
|
+
export declare function migrateOrgFile(cfgPath: string, backupPath: string): {
|
|
22
|
+
status: 'migrated' | 'already-v2';
|
|
23
|
+
dropped: string[];
|
|
24
|
+
notes: string[];
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/migrate.ts
|
|
2
|
+
/** v1 → v2 org config conversion. Pure transform + validation, plus the file
|
|
3
|
+
* IO orchestration for the `org migrate` subcommand — kept out of org.ts to
|
|
4
|
+
* stay under the 500-line file cap; org.ts's migrateAction is a thin wrapper
|
|
5
|
+
* that only does the name-validation / isOrgRunning guard and delegates here. */
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
7
|
+
import { OrgDefSchema } from './types.js';
|
|
8
|
+
import { parseSchedule } from './scheduler.js';
|
|
9
|
+
const V1_TOP_LEVEL_KEYS = [
|
|
10
|
+
'topology', 'consensus', 'strategy', 'maxAgents', 'communication',
|
|
11
|
+
'board_id', 'todo_col_id', 'doing_col_id', 'done_col_id', 'loop',
|
|
12
|
+
'version', 'channels', 'differentiation', 'outputDir', 'runBehavior', 'created',
|
|
13
|
+
];
|
|
14
|
+
const V1_ROLE_KEYS = ['agent_type', 'delegates_to', 'board_id'];
|
|
15
|
+
/** The structural invariants the runtime assumes but the Zod schema can't
|
|
16
|
+
* express: unique role ids, exactly one root (reports_to: null), every
|
|
17
|
+
* non-root's reports_to resolving to another role (and not itself), and a
|
|
18
|
+
* parseable schedule. Shared by `org validate` (validateAction) and
|
|
19
|
+
* `migrateOrgFile`, which refuses to write a migrated config that still
|
|
20
|
+
* fails these checks. */
|
|
21
|
+
export function checkOrgStructure(def) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
const ids = def.roles.map(r => r.id);
|
|
24
|
+
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
25
|
+
if (dupes.length)
|
|
26
|
+
errors.push(`duplicate role id(s): ${[...new Set(dupes)].join(', ')}`);
|
|
27
|
+
const roots = def.roles.filter(r => r.reports_to === null);
|
|
28
|
+
if (roots.length === 0)
|
|
29
|
+
errors.push('no root role — exactly one role must have reports_to: null');
|
|
30
|
+
if (roots.length > 1)
|
|
31
|
+
errors.push(`multiple root roles (${roots.map(r => r.id).join(', ')}) — exactly one may have reports_to: null`);
|
|
32
|
+
for (const r of def.roles) {
|
|
33
|
+
if (r.reports_to !== null && !ids.includes(r.reports_to))
|
|
34
|
+
errors.push(`role "${r.id}": reports_to "${r.reports_to}" matches no role id`);
|
|
35
|
+
if (r.reports_to === r.id)
|
|
36
|
+
errors.push(`role "${r.id}" reports to itself`);
|
|
37
|
+
}
|
|
38
|
+
if (def.schedule != null && parseSchedule(def.schedule) === null)
|
|
39
|
+
errors.push(`schedule "${def.schedule}" is not parseable — use "<N>s", "<N>m", or "<N>h"`);
|
|
40
|
+
return errors;
|
|
41
|
+
}
|
|
42
|
+
export function migrateOrgConfig(raw) {
|
|
43
|
+
const def = { ...raw };
|
|
44
|
+
const dropped = [];
|
|
45
|
+
const notes = [];
|
|
46
|
+
// v1 loop → v2 schedule ("<N>m"); only when no v2 schedule already set
|
|
47
|
+
const loop = def['loop'];
|
|
48
|
+
if (loop && typeof loop.poll_interval_minutes === 'number' && def['schedule'] == null) {
|
|
49
|
+
def['schedule'] = `${loop.poll_interval_minutes}m`;
|
|
50
|
+
notes.push(`loop.poll_interval_minutes=${loop.poll_interval_minutes} → schedule "${def['schedule']}"`);
|
|
51
|
+
}
|
|
52
|
+
for (const k of V1_TOP_LEVEL_KEYS) {
|
|
53
|
+
if (k in def) {
|
|
54
|
+
delete def[k];
|
|
55
|
+
dropped.push(k);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(def['roles'])) {
|
|
59
|
+
const roleKeysDropped = new Set();
|
|
60
|
+
def['roles'] = def['roles'].map(role => {
|
|
61
|
+
const r = { ...role };
|
|
62
|
+
if (typeof r['agent_type'] === 'string') {
|
|
63
|
+
if (r['type'] == null || r['type'] === 'specialist') {
|
|
64
|
+
r['type'] = r['agent_type'];
|
|
65
|
+
notes.push(`role ${String(r['id'])}: agent_type → type`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const k of V1_ROLE_KEYS) {
|
|
69
|
+
if (k in r) {
|
|
70
|
+
delete r[k];
|
|
71
|
+
roleKeysDropped.add(`roles[].${k}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return r;
|
|
75
|
+
});
|
|
76
|
+
dropped.push(...roleKeysDropped);
|
|
77
|
+
}
|
|
78
|
+
if (def['schedule'] === undefined)
|
|
79
|
+
def['schedule'] = null;
|
|
80
|
+
if (typeof def['status'] !== 'string')
|
|
81
|
+
def['status'] = 'stopped';
|
|
82
|
+
// Throws ZodError on an unmigratable config — caller surfaces it.
|
|
83
|
+
OrgDefSchema.parse(def);
|
|
84
|
+
return { def, dropped, notes };
|
|
85
|
+
}
|
|
86
|
+
/** Reads `cfgPath`, migrates it, and (unless already v2) backs up the
|
|
87
|
+
* original to `backupPath` and overwrites `cfgPath` with the migrated def.
|
|
88
|
+
* Throws on an unmigratable config (schema failure inside migrateOrgConfig,
|
|
89
|
+
* or a structural violation caught here) — caller decides how to report it.
|
|
90
|
+
* Refuses to write when the migrated def fails checkOrgStructure, so a v1
|
|
91
|
+
* config with e.g. all-null reports_to fails cleanly instead of silently
|
|
92
|
+
* writing a config the runtime can't start. */
|
|
93
|
+
export function migrateOrgFile(cfgPath, backupPath) {
|
|
94
|
+
const raw = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
95
|
+
const result = migrateOrgConfig(raw);
|
|
96
|
+
const def = result.def;
|
|
97
|
+
const structuralErrors = checkOrgStructure(def);
|
|
98
|
+
if (structuralErrors.length) {
|
|
99
|
+
throw new Error(`migrated config still fails structural validation: ${structuralErrors.join('; ')}`);
|
|
100
|
+
}
|
|
101
|
+
if (result.dropped.length === 0 && result.notes.length === 0) {
|
|
102
|
+
return { status: 'already-v2', dropped: [], notes: [] };
|
|
103
|
+
}
|
|
104
|
+
if (!existsSync(backupPath))
|
|
105
|
+
writeFileSync(backupPath, JSON.stringify(raw, null, 2));
|
|
106
|
+
const tmpPath = `${cfgPath}.tmp`;
|
|
107
|
+
writeFileSync(tmpPath, JSON.stringify(result.def, null, 2));
|
|
108
|
+
renameSync(tmpPath, cfgPath);
|
|
109
|
+
return { status: 'migrated', dropped: result.dropped, notes: result.notes };
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -123,5 +123,12 @@ export function formatEvent(e) {
|
|
|
123
123
|
default: return `${t} ▪ ${from} ${e.type}: ${trim(e.msg ?? '')}`;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
// Newlines stripped BEFORE truncation — the truncated branch previously kept
|
|
127
|
+
// them, so long multi-line messages broke the one-line-per-event log format.
|
|
128
|
+
const trim = (s, n = 120) => {
|
|
129
|
+
if (!s)
|
|
130
|
+
return '';
|
|
131
|
+
const oneLine = s.replace(/\s*\n\s*/g, ' ');
|
|
132
|
+
return oneLine.length > n ? `${oneLine.slice(0, n - 1)}…` : oneLine;
|
|
133
|
+
};
|
|
127
134
|
//# sourceMappingURL=reporting.js.map
|
|
@@ -39,6 +39,10 @@ export async function runAgentSession(opts) {
|
|
|
39
39
|
// would skip that drain entirely.
|
|
40
40
|
while (true) {
|
|
41
41
|
await runOneSession(opts);
|
|
42
|
+
// The dead session's generator may still hold the waker — drop it so a
|
|
43
|
+
// push() before the next stream() starts only queues instead of being
|
|
44
|
+
// consumed by the abandoned generator (silent message loss).
|
|
45
|
+
mailbox.detach();
|
|
42
46
|
if (mailbox.isClosed)
|
|
43
47
|
return;
|
|
44
48
|
opts.bus.emit({ type: 'status', from: opts.role.id, msg: 'session restarting (turn limit reached, mailbox still open)' });
|
|
@@ -48,7 +52,6 @@ export async function runAgentSession(opts) {
|
|
|
48
52
|
async function runOneSession(opts) {
|
|
49
53
|
const { org, role, bus, policy, mailbox, cwd, deliver } = opts;
|
|
50
54
|
const queryFn = opts.queryFn ?? query;
|
|
51
|
-
const isCoordinator = role.reports_to == null;
|
|
52
55
|
const orgServer = createSdkMcpServer({
|
|
53
56
|
name: 'org',
|
|
54
57
|
version: '1.0.0',
|
|
@@ -57,7 +60,10 @@ async function runOneSession(opts) {
|
|
|
57
60
|
const text = await opts.recall(role.id, args.query);
|
|
58
61
|
return { content: [{ type: 'text', text }] };
|
|
59
62
|
})] : []),
|
|
60
|
-
|
|
63
|
+
// Gate purely on onComplete: the daemon passes it only to the role its
|
|
64
|
+
// boss-selection rule picked, so tool availability always matches the
|
|
65
|
+
// kickoff instruction (reports_to may be non-null for a fallback boss).
|
|
66
|
+
...(opts.onComplete ? [tool('org_complete', 'Record the outcome of this run. Call exactly once, when the goal is achieved or clearly cannot be. The outcome and summary are persisted to the org run history and briefed to the next run.', { outcome: z.enum(['achieved', 'partial', 'failed']), summary: z.string() }, async (args) => {
|
|
61
67
|
opts.onComplete(role.id, args.outcome, args.summary);
|
|
62
68
|
return { content: [{ type: 'text', text: `outcome "${args.outcome}" recorded` }] };
|
|
63
69
|
})] : []),
|
|
@@ -1914,6 +1914,12 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
1914
1914
|
<button class="btn chunk-build-btn" id="btn-build-docs" onclick="buildKnowledgeDocs()" title="Index .md/.mdx/.txt/.rst files">⊕ Build Docs</button>
|
|
1915
1915
|
<span class="chunk-build-status" id="chunk-build-status"></span>
|
|
1916
1916
|
</div>
|
|
1917
|
+
<div class="filter-bar" style="margin-bottom:6px">
|
|
1918
|
+
<input class="filter-input" id="sb-search-input" type="text" placeholder="Ask your Second Brain — semantic search by meaning…"
|
|
1919
|
+
onkeydown="if(event.key==='Enter')secondBrainSearch()">
|
|
1920
|
+
<button class="btn" onclick="secondBrainSearch()" title="Semantic search (warm local model)">⌕ Search</button>
|
|
1921
|
+
</div>
|
|
1922
|
+
<div id="sb-search-results" style="display:none;margin-bottom:12px"></div>
|
|
1917
1923
|
<div class="filter-bar">
|
|
1918
1924
|
<input class="filter-input" type="text" placeholder="Filter chunks…" oninput="filterChunks(this.value)">
|
|
1919
1925
|
<button class="btn" onclick="_chunksLoaded=false;loadChunksTab()" title="Refresh">↺</button>
|
|
@@ -13592,6 +13598,44 @@ async function cleanSwarmData() {
|
|
|
13592
13598
|
// ── chunks memory tab ──────────────────────────────────────
|
|
13593
13599
|
let _chunks = [], _chunksLoaded = false, _editChunkId = null;
|
|
13594
13600
|
|
|
13601
|
+
// Second Brain semantic search — hits the warm /api/knowledge/search endpoint
|
|
13602
|
+
// (the server holds the local embedding model hot; ~60ms per query).
|
|
13603
|
+
async function secondBrainSearch() {
|
|
13604
|
+
const input = document.getElementById('sb-search-input');
|
|
13605
|
+
const box = document.getElementById('sb-search-results');
|
|
13606
|
+
if (!input || !box) return;
|
|
13607
|
+
const q = input.value.trim();
|
|
13608
|
+
if (!q) { box.style.display = 'none'; return; }
|
|
13609
|
+
box.style.display = 'block';
|
|
13610
|
+
box.innerHTML = '<div class="loading-txt">Searching…</div>';
|
|
13611
|
+
try {
|
|
13612
|
+
const r = await fetch('/api/knowledge/search', {
|
|
13613
|
+
method: 'POST',
|
|
13614
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13615
|
+
body: JSON.stringify({ query: q, limit: 5 }),
|
|
13616
|
+
});
|
|
13617
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
13618
|
+
const data = await r.json();
|
|
13619
|
+
const results = data.results || [];
|
|
13620
|
+
if (!results.length) {
|
|
13621
|
+
box.innerHTML = '<div class="empty" style="padding:10px">No matches (' + esc(data.method || '?') + '). Ingest documents with: <code>monomind doc ingest ./path</code></div>';
|
|
13622
|
+
return;
|
|
13623
|
+
}
|
|
13624
|
+
box.innerHTML =
|
|
13625
|
+
'<div style="font-size:10px;letter-spacing:0.07em;text-transform:uppercase;color:var(--text-xs);margin-bottom:6px">' +
|
|
13626
|
+
results.length + ' result(s) · ' + esc(data.method || '') + '</div>' +
|
|
13627
|
+
results.map(function (res) {
|
|
13628
|
+
return '<div style="border:1px solid var(--border);border-radius:8px;padding:8px 10px;margin-bottom:6px">' +
|
|
13629
|
+
'<div style="font-size:10px;color:var(--text-lo);margin-bottom:3px">' + esc(res.key || '') +
|
|
13630
|
+
' <span style="color:var(--accent)">' + (typeof res.score === 'number' ? res.score.toFixed(2) : '') + '</span></div>' +
|
|
13631
|
+
'<div style="font-size:12px;white-space:pre-wrap;max-height:120px;overflow:hidden">' + esc(String(res.content || '').slice(0, 600)) + '</div>' +
|
|
13632
|
+
'</div>';
|
|
13633
|
+
}).join('');
|
|
13634
|
+
} catch (e) {
|
|
13635
|
+
box.innerHTML = '<div class="empty" style="padding:10px">Search failed: ' + esc(e.message) + '</div>';
|
|
13636
|
+
}
|
|
13637
|
+
}
|
|
13638
|
+
|
|
13595
13639
|
async function loadChunksTab() {
|
|
13596
13640
|
if (_chunksLoaded) return;
|
|
13597
13641
|
const grid = document.getElementById('chunks-grid');
|
|
@@ -1782,12 +1782,12 @@ async function loadRunHistory() {
|
|
|
1782
1782
|
${runs.slice(0, 10).map(h => `
|
|
1783
1783
|
<div style="font-size:9px;padding:5px 0;border-bottom:1px solid oklch(50% 0 0 / 0.1);display:flex;gap:10px;align-items:baseline">
|
|
1784
1784
|
<span style="color:var(--dim);white-space:nowrap">${esc(h.run || '?')}</span>
|
|
1785
|
-
<span style="color:var(--muted);white-space:nowrap">${h.durationMs != null ? Math.round(h.durationMs / 1000) + 's' : '—'} · ${h.totalTokens || 0} tok</span>
|
|
1785
|
+
<span style="color:var(--muted);white-space:nowrap">${Number.isFinite(Number(h.durationMs)) && h.durationMs != null ? Math.round(Number(h.durationMs) / 1000) + 's' : '—'} · ${Number(h.totalTokens) || 0} tok</span>
|
|
1786
1786
|
${h.outcome
|
|
1787
1787
|
? `<span style="color:${outcomeColor(h.outcome.status)}">${esc(h.outcome.status.toUpperCase())}</span>
|
|
1788
1788
|
<span style="color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(h.outcome.summary || '')}</span>`
|
|
1789
1789
|
: `<span style="color:var(--dim)">NO OUTCOME</span>`}
|
|
1790
|
-
${h.crashes && h.crashes.length ? `<span style="color:var(--red)">✗ ${h.crashes.length} crash</span>` : ''}
|
|
1790
|
+
${Array.isArray(h.crashes) && h.crashes.length ? `<span style="color:var(--red)">✗ ${Number(h.crashes.length)} crash</span>` : ''}
|
|
1791
1791
|
</div>`).join('')}
|
|
1792
1792
|
</div>`;
|
|
1793
1793
|
} catch (_) { el.innerHTML = ''; }
|
|
@@ -5920,8 +5920,19 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
5920
5920
|
// when this endpoint is cold/absent. Fully local — model + store on disk.
|
|
5921
5921
|
if (req.method === 'POST' && url === '/api/knowledge/search') {
|
|
5922
5922
|
let body = '';
|
|
5923
|
-
|
|
5923
|
+
let overLimit = false;
|
|
5924
|
+
req.on('data', c => {
|
|
5925
|
+
if (overLimit) return;
|
|
5926
|
+
body += c;
|
|
5927
|
+
if (body.length > 64 * 1024) { // queries are prompts, not documents
|
|
5928
|
+
overLimit = true;
|
|
5929
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
5930
|
+
res.end('{"error":"request body too large"}');
|
|
5931
|
+
req.destroy();
|
|
5932
|
+
}
|
|
5933
|
+
});
|
|
5924
5934
|
req.on('end', async () => {
|
|
5935
|
+
if (overLimit) return;
|
|
5925
5936
|
try {
|
|
5926
5937
|
const payload = JSON.parse(body || '{}');
|
|
5927
5938
|
const query = String(payload.query || '').slice(0, 2000);
|
|
@@ -5939,11 +5950,21 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
5939
5950
|
res.end('{"error":"knowledge bridge unavailable"}');
|
|
5940
5951
|
return;
|
|
5941
5952
|
}
|
|
5942
|
-
|
|
5953
|
+
// scope: project | global | all (default all) — project results get a
|
|
5954
|
+
// small tie boost; global hits are flagged so callers can show origin.
|
|
5955
|
+
const scope = payload.scope === 'project' || payload.scope === 'global' ? payload.scope : 'all';
|
|
5956
|
+
const [proj, glob] = await Promise.all([
|
|
5957
|
+
scope !== 'global' ? bridge.bridgeSearchEntries({ query, namespace, limit }).catch(() => null) : null,
|
|
5958
|
+
scope !== 'project' ? bridge.bridgeSearchEntries({ query, namespace: 'knowledge:global', limit, dbPath: '@global' }).catch(() => null) : null,
|
|
5959
|
+
]);
|
|
5960
|
+
const merged = [
|
|
5961
|
+
...(proj?.results || []).map(r => ({ key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score + 0.05, global: false, tags: r.tags })),
|
|
5962
|
+
...(glob?.results || []).map(r => ({ key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score, global: true, tags: r.tags })),
|
|
5963
|
+
].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5943
5964
|
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
5944
5965
|
res.end(JSON.stringify({
|
|
5945
|
-
method:
|
|
5946
|
-
results:
|
|
5966
|
+
method: proj?.searchMethod || glob?.searchMethod || 'none',
|
|
5967
|
+
results: merged,
|
|
5947
5968
|
}));
|
|
5948
5969
|
} catch (err) {
|
|
5949
5970
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI engine for Monomind — an open-source MCP server that extends Claude Code with a codebase knowledge graph (tree-sitter + SQLite), persistent memory, multi-agent task coordination, and session hooks. MIT licensed, fully local.",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
},
|
|
106
106
|
"optionalDependencies": {
|
|
107
107
|
"@huggingface/transformers": "^3.8.1",
|
|
108
|
-
"@monoes/memory": "^1.0.
|
|
108
|
+
"@monoes/memory": "^1.0.8",
|
|
109
109
|
"@monomind/hooks": "*",
|
|
110
110
|
"@monomind/mcp": "*",
|
|
111
111
|
"@monomind/routing": "*",
|