monomind 2.7.7 → 2.7.8
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/package.json +2 -1
- package/packages/@monomind/cli/.claude/agents/core/coder.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/coordinator.md +62 -0
- package/packages/@monomind/cli/.claude/agents/core/planner.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/reviewer.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/tester.md +8 -0
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +180 -47
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +32 -3
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +55 -3
- package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +3 -1
- package/packages/@monomind/cli/.claude/helpers/statusline.cjs +27 -3
- package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +104 -18
- package/packages/@monomind/cli/.claude/settings.json +1 -1
- package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +6 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.d.ts +0 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +5 -1
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/guidance.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/init.js +18 -4
- package/packages/@monomind/cli/dist/src/commands/memory-crud.js +7 -1
- package/packages/@monomind/cli/dist/src/commands/org-observe.js +17 -7
- package/packages/@monomind/cli/dist/src/commands/org.d.ts +11 -0
- package/packages/@monomind/cli/dist/src/commands/org.js +162 -15
- package/packages/@monomind/cli/dist/src/commands/security-scan.d.ts +30 -1
- package/packages/@monomind/cli/dist/src/commands/security-scan.js +182 -69
- package/packages/@monomind/cli/dist/src/commands/swarm.js +41 -14
- package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +11 -10
- package/packages/@monomind/cli/dist/src/init/executor.js +138 -22
- package/packages/@monomind/cli/dist/src/init/settings-generator.js +4 -1
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +17 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +62 -3
- package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +16 -8
- package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +27 -13
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +5 -1
- package/packages/@monomind/cli/dist/src/memory/memory-read.d.ts +7 -2
- package/packages/@monomind/cli/dist/src/memory/memory-read.js +10 -2
- package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +25 -6
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +21 -1
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +82 -11
- package/packages/@monomind/cli/dist/src/orgrt/inbox.js +53 -20
- package/packages/@monomind/cli/dist/src/parser.d.ts +4 -2
- package/packages/@monomind/cli/dist/src/parser.js +61 -25
- package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +10 -2
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -2
- package/packages/@monomind/cli/dist/src/ui/collector.mjs +43 -3
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +27 -8
- package/packages/@monomind/cli/dist/src/ui/server.mjs +144 -14
- package/packages/@monomind/cli/package.json +3 -3
- package/packages/@monomind/cli/dist/src/capabilities/watcher.d.ts +0 -18
- package/packages/@monomind/cli/dist/src/capabilities/watcher.js +0 -107
- package/packages/@monomind/cli/dist/src/config-adapter.d.ts +0 -16
- package/packages/@monomind/cli/dist/src/config-adapter.js +0 -220
|
@@ -8,19 +8,45 @@ import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { writeJsonFileAtomic } from '../utils/json-file.js';
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import { getMonomindDataRoot, getProjectCwd, migrateLegacyStoreFile } from '../mcp-tools/types.js';
|
|
12
|
+
// Canonical paths — resolved through getMonomindDataRoot() so the CLI and the MCP
|
|
13
|
+
// tools (agent-tools.ts / swarm-tools.ts / task-tools.ts) read and write the SAME
|
|
14
|
+
// physical files. These used to be `path.join(process.cwd(), '.monomind/...')`,
|
|
15
|
+
// which only coincides with the canonical root when there is no `.git` directory;
|
|
16
|
+
// inside a real repo the canonical root is `<repo>/.git/monomind`, so the CLI read
|
|
17
|
+
// an empty parallel store ("0 agents" with agents on disk) and `swarm init` wrote a
|
|
18
|
+
// second, divergent swarm-state.json no MCP tool ever read.
|
|
19
|
+
const SWARM_STATE_SUBDIR = 'swarm';
|
|
13
20
|
const SWARM_STATE_FILE = 'swarm-state.json';
|
|
14
|
-
const
|
|
21
|
+
const AGENT_STORE_SUBDIR = 'agents';
|
|
22
|
+
const AGENT_STORE_FILE = 'store.json';
|
|
23
|
+
function getSwarmDir() {
|
|
24
|
+
return path.join(getMonomindDataRoot(), SWARM_STATE_SUBDIR);
|
|
25
|
+
}
|
|
26
|
+
// Canonical swarm-state.json path. Also pulls a pre-existing legacy
|
|
27
|
+
// `<cwd>/.monomind/swarm/swarm-state.json` into place (copy, never move; no-op once
|
|
28
|
+
// the canonical file exists or when the two paths coincide) so state written by an
|
|
29
|
+
// older CLI isn't silently orphaned — same helper the MCP tools use.
|
|
30
|
+
function getSwarmStateFile() {
|
|
31
|
+
const file = path.join(getSwarmDir(), SWARM_STATE_FILE);
|
|
32
|
+
migrateLegacyStoreFile(file, path.join(SWARM_STATE_SUBDIR, SWARM_STATE_FILE));
|
|
33
|
+
return file;
|
|
34
|
+
}
|
|
35
|
+
function getAgentStoreFile() {
|
|
36
|
+
const file = path.join(getMonomindDataRoot(), AGENT_STORE_SUBDIR, AGENT_STORE_FILE);
|
|
37
|
+
migrateLegacyStoreFile(file, path.join(AGENT_STORE_SUBDIR, AGENT_STORE_FILE));
|
|
38
|
+
return file;
|
|
39
|
+
}
|
|
15
40
|
// Get dynamic swarm status from MCP-canonical state files
|
|
16
41
|
function getSwarmStatus(swarmId) {
|
|
17
|
-
const
|
|
42
|
+
const projectCwd = getProjectCwd();
|
|
43
|
+
const sessionDir = path.join(projectCwd, '.claude', 'sessions');
|
|
18
44
|
const memoryPaths = [
|
|
19
|
-
path.join(
|
|
20
|
-
path.join(
|
|
45
|
+
path.join(projectCwd, '.monomind', 'memory.db'),
|
|
46
|
+
path.join(projectCwd, '.claude', 'memory.db'),
|
|
21
47
|
];
|
|
22
|
-
// Read swarm state from MCP-canonical path
|
|
23
|
-
const swarmStateFile =
|
|
48
|
+
// Read swarm state from the MCP-canonical path
|
|
49
|
+
const swarmStateFile = getSwarmStateFile();
|
|
24
50
|
let swarmState = null;
|
|
25
51
|
if (fs.existsSync(swarmStateFile)) {
|
|
26
52
|
try {
|
|
@@ -52,10 +78,11 @@ function getSwarmStatus(swarmId) {
|
|
|
52
78
|
// Ignore parse errors
|
|
53
79
|
}
|
|
54
80
|
}
|
|
55
|
-
// Count agents from MCP-canonical agent store (
|
|
81
|
+
// Count agents from the MCP-canonical agent store (the same physical file
|
|
82
|
+
// agent_spawn writes to — see getAgentStoreFile()).
|
|
56
83
|
let activeAgents = 0;
|
|
57
84
|
let totalAgents = 0;
|
|
58
|
-
const agentStoreFile =
|
|
85
|
+
const agentStoreFile = getAgentStoreFile();
|
|
59
86
|
if (fs.existsSync(agentStoreFile)) {
|
|
60
87
|
try {
|
|
61
88
|
const agentSz = fs.statSync(agentStoreFile).size;
|
|
@@ -102,7 +129,7 @@ function getSwarmStatus(swarmId) {
|
|
|
102
129
|
let completedTasks = 0;
|
|
103
130
|
let inProgressTasks = 0;
|
|
104
131
|
let pendingTasks = 0;
|
|
105
|
-
const tasksDir = path.join(
|
|
132
|
+
const tasksDir = path.join(getSwarmDir(), 'tasks');
|
|
106
133
|
if (fs.existsSync(tasksDir)) {
|
|
107
134
|
try {
|
|
108
135
|
const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
|
|
@@ -311,7 +338,7 @@ const initCommand = {
|
|
|
311
338
|
output.printSuccess('Swarm initialized successfully');
|
|
312
339
|
// Save swarm state to MCP-canonical path so CLI and MCP share state
|
|
313
340
|
try {
|
|
314
|
-
const swarmDir2 =
|
|
341
|
+
const swarmDir2 = getSwarmDir();
|
|
315
342
|
if (!fs.existsSync(swarmDir2)) {
|
|
316
343
|
fs.mkdirSync(swarmDir2, { recursive: true });
|
|
317
344
|
}
|
|
@@ -466,7 +493,7 @@ const startCommand = {
|
|
|
466
493
|
output.writeln(output.dim(' Run with -v/--verbose for more detail, or `monomind doctor` to check config/permission issues.'));
|
|
467
494
|
}
|
|
468
495
|
// Persist swarm state to MCP-canonical path so CLI and MCP share state
|
|
469
|
-
const swarmDir =
|
|
496
|
+
const swarmDir = getSwarmDir();
|
|
470
497
|
if (!fs.existsSync(swarmDir))
|
|
471
498
|
fs.mkdirSync(swarmDir, { recursive: true });
|
|
472
499
|
// Read existing store to preserve other swarms
|
|
@@ -627,7 +654,7 @@ const stopCommand = {
|
|
|
627
654
|
}
|
|
628
655
|
// Only update persisted swarm state if the MCP call succeeded (#1423)
|
|
629
656
|
if (mcpStopped) {
|
|
630
|
-
const stopStateFile =
|
|
657
|
+
const stopStateFile = getSwarmStateFile();
|
|
631
658
|
if (fs.existsSync(stopStateFile)) {
|
|
632
659
|
try {
|
|
633
660
|
const stopStatSz = fs.statSync(stopStateFile).size;
|
|
@@ -177,19 +177,20 @@ export class AuditWriter {
|
|
|
177
177
|
appendFileSync(filePath, JSON.stringify(data) + '\n', 'utf-8');
|
|
178
178
|
}
|
|
179
179
|
readLines(filePath) {
|
|
180
|
+
// A missing log is a genuine empty trail. Everything else — an oversized
|
|
181
|
+
// log, a permissions error, an I/O failure — is an *unreadable* trail, and
|
|
182
|
+
// must not be reported as an empty one: this file is the tamper-evidence
|
|
183
|
+
// record, so "0 records" and "cannot read" have opposite meanings.
|
|
184
|
+
// Callers (hive-mind_audit_list / _verify) already surface a throw as
|
|
185
|
+
// `success: false` with the message.
|
|
180
186
|
if (!existsSync(filePath))
|
|
181
187
|
return [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
throw new Error(`Audit log ${filePath} exceeds 50MB — run rotation/cleanup`);
|
|
186
|
-
}
|
|
187
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
188
|
-
return parseJsonl(content);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return [];
|
|
188
|
+
const MAX_BYTES = 50 * 1024 * 1024;
|
|
189
|
+
if (statSync(filePath).size > MAX_BYTES) {
|
|
190
|
+
throw new Error(`Audit log ${filePath} exceeds 50MB — run rotation/cleanup`);
|
|
192
191
|
}
|
|
192
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
193
|
+
return parseJsonl(content);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
//# sourceMappingURL=audit-writer.js.map
|
|
@@ -410,17 +410,37 @@ try { await buildAsync(${JSON.stringify(targetDir)}); } finally {
|
|
|
410
410
|
* Non-fatal: best-effort health check and auto-install.
|
|
411
411
|
*/
|
|
412
412
|
async function runDoctorFix(targetDir, result) {
|
|
413
|
+
// Run the doctor THIS binary ships, in-process.
|
|
414
|
+
//
|
|
415
|
+
// This used to be `execSync('npx monomind@latest doctor --install')`, which
|
|
416
|
+
// was wrong in three ways at once: it downloaded and ran a DIFFERENT version
|
|
417
|
+
// than the one the user deliberately invoked (so `monomind@2.7.0 init` was
|
|
418
|
+
// finished by whatever `latest` happened to be), it silently required network
|
|
419
|
+
// — on a machine without it, the 120s timeout elapsed and init reported
|
|
420
|
+
// "skipped" with no explanation — and `stdio: 'ignore'` discarded everything
|
|
421
|
+
// it said, so a failed health check looked identical to a passing one.
|
|
413
422
|
try {
|
|
414
|
-
const {
|
|
415
|
-
|
|
423
|
+
const { doctorCommand } = await import('../commands/doctor.js');
|
|
424
|
+
if (!doctorCommand.action) {
|
|
425
|
+
result.skipped.push('doctor: auto-fix unavailable (run: monomind doctor --install)');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const res = await doctorCommand.action({
|
|
429
|
+
args: [],
|
|
430
|
+
flags: { install: true },
|
|
416
431
|
cwd: targetDir,
|
|
417
|
-
stdio: 'ignore',
|
|
418
|
-
timeout: 120000,
|
|
419
432
|
});
|
|
420
|
-
|
|
433
|
+
// Report what actually happened rather than asserting success either way.
|
|
434
|
+
if (res && res.success === false) {
|
|
435
|
+
result.skipped.push('doctor: reported issues (run: monomind doctor for details)');
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
result.created.files.push('doctor --install (health check + auto-fix)');
|
|
439
|
+
}
|
|
421
440
|
}
|
|
422
|
-
catch {
|
|
423
|
-
|
|
441
|
+
catch (err) {
|
|
442
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
443
|
+
result.skipped.push(`doctor: auto-fix failed (${detail}) — run: monomind doctor --install`);
|
|
424
444
|
}
|
|
425
445
|
}
|
|
426
446
|
/**
|
|
@@ -953,6 +973,73 @@ async function writeMCPConfig(targetDir, options, result) {
|
|
|
953
973
|
atomicWriteFile(mcpPath, content);
|
|
954
974
|
result.created.files.push('.mcp.json');
|
|
955
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Provenance manifest for generated .claude content.
|
|
978
|
+
*
|
|
979
|
+
* init used to "clean stale" entries by deleting every name under
|
|
980
|
+
* .claude/{skills,commands,agents} that was absent from the current version's
|
|
981
|
+
* SKILLS_MAP/COMMANDS_MAP/AGENTS_MAP. Every user-authored command and skill is
|
|
982
|
+
* absent from those maps, so that pass deleted user content on the very first
|
|
983
|
+
* run — unrecoverable data loss.
|
|
984
|
+
*
|
|
985
|
+
* The manifest records exactly which entries *this tool* wrote, so the stale
|
|
986
|
+
* sweep can be restricted to those. Anything init did not write is never
|
|
987
|
+
* removed. Projects initialised by an older version have no manifest, so their
|
|
988
|
+
* first run under the fix deletes nothing and seeds the manifest instead;
|
|
989
|
+
* stale generated content may survive one extra run, which is the correct
|
|
990
|
+
* trade (preserving stale generated content is recoverable, deleting user
|
|
991
|
+
* content is not).
|
|
992
|
+
*/
|
|
993
|
+
const INIT_MANIFEST_REL = path.join('.monomind', 'init-manifest.json');
|
|
994
|
+
/**
|
|
995
|
+
* Read the provenance manifest. Returns null when absent or unreadable —
|
|
996
|
+
* callers must treat that as "provenance unknown", i.e. delete nothing.
|
|
997
|
+
*/
|
|
998
|
+
function readInitManifest(targetDir) {
|
|
999
|
+
const manifestPath = path.join(targetDir, INIT_MANIFEST_REL);
|
|
1000
|
+
try {
|
|
1001
|
+
if (!fs.existsSync(manifestPath))
|
|
1002
|
+
return null;
|
|
1003
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1004
|
+
if (!parsed || typeof parsed !== 'object')
|
|
1005
|
+
return null;
|
|
1006
|
+
return {
|
|
1007
|
+
version: typeof parsed.version === 'number' ? parsed.version : 1,
|
|
1008
|
+
skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s) => typeof s === 'string') : [],
|
|
1009
|
+
commands: Array.isArray(parsed.commands) ? parsed.commands.filter((s) => typeof s === 'string') : [],
|
|
1010
|
+
agents: Array.isArray(parsed.agents) ? parsed.agents.filter((s) => typeof s === 'string') : [],
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Names init previously generated in one section. Empty set when no manifest
|
|
1019
|
+
* exists — which makes the stale sweep a no-op rather than a delete-everything.
|
|
1020
|
+
*/
|
|
1021
|
+
function previouslyGenerated(targetDir, section) {
|
|
1022
|
+
return new Set(readInitManifest(targetDir)?.[section] ?? []);
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Record the entries init just wrote for one section, merging into any
|
|
1026
|
+
* existing manifest so a partial run (e.g. --only-claude, or a section whose
|
|
1027
|
+
* source dir was missing) never drops provenance for the other sections.
|
|
1028
|
+
*/
|
|
1029
|
+
function recordGenerated(targetDir, section, entries) {
|
|
1030
|
+
const manifestPath = path.join(targetDir, INIT_MANIFEST_REL);
|
|
1031
|
+
const existing = readInitManifest(targetDir);
|
|
1032
|
+
const manifest = existing ?? { version: 1, skills: [], commands: [], agents: [] };
|
|
1033
|
+
manifest.version = 1;
|
|
1034
|
+
manifest[section] = [...new Set(entries)].sort();
|
|
1035
|
+
try {
|
|
1036
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
1037
|
+
atomicWriteFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
1038
|
+
}
|
|
1039
|
+
catch {
|
|
1040
|
+
// Non-fatal: without a manifest the next run simply deletes nothing.
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
956
1043
|
/**
|
|
957
1044
|
* Copy skills from source
|
|
958
1045
|
*/
|
|
@@ -992,11 +1079,14 @@ async function copySkills(targetDir, options, result) {
|
|
|
992
1079
|
skillsToCopy.push(...fs.readdirSync(sourceSkillsDir).filter(n => n.startsWith(prefix) &&
|
|
993
1080
|
fs.existsSync(path.join(sourceSkillsDir, n, 'SKILL.md'))));
|
|
994
1081
|
}
|
|
995
|
-
// Remove stale skill directories
|
|
1082
|
+
// Remove stale skill directories that a PREVIOUS init generated and this
|
|
1083
|
+
// version no longer ships. Entries init never wrote (user-authored skills,
|
|
1084
|
+
// skills installed by other tools) are left untouched — see readInitManifest.
|
|
996
1085
|
const knownSkills = new Set([...new Set(skillsToCopy)]);
|
|
1086
|
+
const priorSkills = previouslyGenerated(targetDir, 'skills');
|
|
997
1087
|
if (fs.existsSync(targetSkillsDir)) {
|
|
998
1088
|
for (const existing of fs.readdirSync(targetSkillsDir)) {
|
|
999
|
-
if (!knownSkills.has(existing)) {
|
|
1089
|
+
if (!knownSkills.has(existing) && priorSkills.has(existing)) {
|
|
1000
1090
|
const stalePath = path.join(targetSkillsDir, existing);
|
|
1001
1091
|
fs.rmSync(stalePath, { recursive: true, force: true });
|
|
1002
1092
|
result.created.files.push(`[cleaned] .claude/skills/${existing} (stale)`);
|
|
@@ -1004,14 +1094,19 @@ async function copySkills(targetDir, options, result) {
|
|
|
1004
1094
|
}
|
|
1005
1095
|
}
|
|
1006
1096
|
// Always copy/overwrite skills (never skip — ensures new version content lands)
|
|
1097
|
+
const writtenSkills = [];
|
|
1007
1098
|
for (const skillName of knownSkills) {
|
|
1008
1099
|
const sourcePath = path.join(sourceSkillsDir, skillName);
|
|
1009
1100
|
const targetPath = path.join(targetSkillsDir, skillName);
|
|
1010
1101
|
if (fs.existsSync(sourcePath)) {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1102
|
+
// Deliberately NOT rmSync'd first. copyDirRecursive overwrites every
|
|
1103
|
+
// file it ships, so wiping the directory adds nothing except destroying
|
|
1104
|
+
// anything the user put inside it — notes beside a shipped skill, an
|
|
1105
|
+
// extra command in a shipped folder. `init --force` did exactly that.
|
|
1106
|
+
// The cost of not wiping is that a file removed from a newer version
|
|
1107
|
+
// lingers; the cost of wiping is silent data loss, which is worse.
|
|
1014
1108
|
copyDirRecursive(sourcePath, targetPath);
|
|
1109
|
+
writtenSkills.push(skillName);
|
|
1015
1110
|
result.created.files.push(`.claude/skills/${skillName}`);
|
|
1016
1111
|
result.summary.skillsCount++;
|
|
1017
1112
|
}
|
|
@@ -1022,6 +1117,12 @@ async function copySkills(targetDir, options, result) {
|
|
|
1022
1117
|
result.errors.push(`Skill '${skillName}' listed in SKILLS_MAP has no source directory at ${sourcePath} — skipped`);
|
|
1023
1118
|
}
|
|
1024
1119
|
}
|
|
1120
|
+
// Record provenance so the next run can distinguish "we wrote this" from
|
|
1121
|
+
// "the user wrote this". Keep entries this run did not re-write but that a
|
|
1122
|
+
// previous run generated and that still exist, so provenance is not lost
|
|
1123
|
+
// when a section is partially skipped.
|
|
1124
|
+
const retainedSkills = [...priorSkills].filter(n => !writtenSkills.includes(n) && fs.existsSync(path.join(targetSkillsDir, n)));
|
|
1125
|
+
recordGenerated(targetDir, 'skills', [...writtenSkills, ...retainedSkills]);
|
|
1025
1126
|
}
|
|
1026
1127
|
/**
|
|
1027
1128
|
* Copy commands from source
|
|
@@ -1084,11 +1185,13 @@ async function copyCommands(targetDir, options, result) {
|
|
|
1084
1185
|
result.errors.push('Could not find source commands directory');
|
|
1085
1186
|
return;
|
|
1086
1187
|
}
|
|
1087
|
-
// Remove stale command files/directories
|
|
1188
|
+
// Remove stale command files/directories that a PREVIOUS init generated and
|
|
1189
|
+
// this version no longer ships. User-authored commands are never touched.
|
|
1088
1190
|
const knownCommands = new Set([...new Set(commandsToCopy)]);
|
|
1191
|
+
const priorCommands = previouslyGenerated(targetDir, 'commands');
|
|
1089
1192
|
if (fs.existsSync(targetCommandsDir)) {
|
|
1090
1193
|
for (const existing of fs.readdirSync(targetCommandsDir)) {
|
|
1091
|
-
if (!knownCommands.has(existing)) {
|
|
1194
|
+
if (!knownCommands.has(existing) && priorCommands.has(existing)) {
|
|
1092
1195
|
const stalePath = path.join(targetCommandsDir, existing);
|
|
1093
1196
|
fs.rmSync(stalePath, { recursive: true, force: true });
|
|
1094
1197
|
result.created.files.push(`[cleaned] .claude/commands/${existing} (stale)`);
|
|
@@ -1096,23 +1199,27 @@ async function copyCommands(targetDir, options, result) {
|
|
|
1096
1199
|
}
|
|
1097
1200
|
}
|
|
1098
1201
|
// Always copy/overwrite commands (never skip — ensures new version content lands)
|
|
1202
|
+
const writtenCommands = [];
|
|
1099
1203
|
for (const cmdName of knownCommands) {
|
|
1100
1204
|
const sourcePath = path.join(sourceCommandsDir, cmdName);
|
|
1101
1205
|
const targetPath = path.join(targetCommandsDir, cmdName);
|
|
1102
1206
|
if (fs.existsSync(sourcePath)) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1207
|
+
// No pre-copy rmSync — see the note in copySkills. Both branches below
|
|
1208
|
+
// overwrite what they ship, so wiping first only destroys files the user
|
|
1209
|
+
// added inside a shipped command directory.
|
|
1106
1210
|
if (fs.statSync(sourcePath).isDirectory()) {
|
|
1107
1211
|
copyDirRecursive(sourcePath, targetPath);
|
|
1108
1212
|
}
|
|
1109
1213
|
else {
|
|
1110
1214
|
fs.copyFileSync(sourcePath, targetPath);
|
|
1111
1215
|
}
|
|
1216
|
+
writtenCommands.push(cmdName);
|
|
1112
1217
|
result.created.files.push(`.claude/commands/${cmdName}`);
|
|
1113
1218
|
result.summary.commandsCount++;
|
|
1114
1219
|
}
|
|
1115
1220
|
}
|
|
1221
|
+
const retainedCommands = [...priorCommands].filter(n => !writtenCommands.includes(n) && fs.existsSync(path.join(targetCommandsDir, n)));
|
|
1222
|
+
recordGenerated(targetDir, 'commands', [...writtenCommands, ...retainedCommands]);
|
|
1116
1223
|
}
|
|
1117
1224
|
/**
|
|
1118
1225
|
* Copy agents from source
|
|
@@ -1147,11 +1254,13 @@ async function copyAgents(targetDir, options, result) {
|
|
|
1147
1254
|
result.errors.push('Could not find source agents directory');
|
|
1148
1255
|
return;
|
|
1149
1256
|
}
|
|
1150
|
-
// Remove stale agent category directories
|
|
1257
|
+
// Remove stale agent category directories that a PREVIOUS init generated and
|
|
1258
|
+
// this version no longer ships. User-authored agent dirs are never touched.
|
|
1151
1259
|
const knownAgents = new Set([...new Set(agentsToCopy)]);
|
|
1260
|
+
const priorAgents = previouslyGenerated(targetDir, 'agents');
|
|
1152
1261
|
if (fs.existsSync(targetAgentsDir)) {
|
|
1153
1262
|
for (const existing of fs.readdirSync(targetAgentsDir)) {
|
|
1154
|
-
if (!knownAgents.has(existing)) {
|
|
1263
|
+
if (!knownAgents.has(existing) && priorAgents.has(existing)) {
|
|
1155
1264
|
const stalePath = path.join(targetAgentsDir, existing);
|
|
1156
1265
|
fs.rmSync(stalePath, { recursive: true, force: true });
|
|
1157
1266
|
result.created.files.push(`[cleaned] .claude/agents/${existing} (stale)`);
|
|
@@ -1159,20 +1268,27 @@ async function copyAgents(targetDir, options, result) {
|
|
|
1159
1268
|
}
|
|
1160
1269
|
}
|
|
1161
1270
|
// Always copy/overwrite agents (never skip — ensures new version content lands)
|
|
1271
|
+
const writtenAgents = [];
|
|
1162
1272
|
for (const agentCategory of knownAgents) {
|
|
1163
1273
|
const sourcePath = path.join(sourceAgentsDir, agentCategory);
|
|
1164
1274
|
const targetPath = path.join(targetAgentsDir, agentCategory);
|
|
1165
1275
|
if (fs.existsSync(sourcePath)) {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1276
|
+
// Deliberately NOT rmSync'd first. copyDirRecursive overwrites every
|
|
1277
|
+
// file it ships, so wiping the directory adds nothing except destroying
|
|
1278
|
+
// anything the user put inside it — notes beside a shipped skill, an
|
|
1279
|
+
// extra command in a shipped folder. `init --force` did exactly that.
|
|
1280
|
+
// The cost of not wiping is that a file removed from a newer version
|
|
1281
|
+
// lingers; the cost of wiping is silent data loss, which is worse.
|
|
1169
1282
|
copyDirRecursive(sourcePath, targetPath);
|
|
1170
1283
|
// Count agent files (.md only — .yaml agents were migrated to .md)
|
|
1171
1284
|
const mdFiles = countFiles(sourcePath, '.md');
|
|
1172
1285
|
result.summary.agentsCount += mdFiles;
|
|
1286
|
+
writtenAgents.push(agentCategory);
|
|
1173
1287
|
result.created.files.push(`.claude/agents/${agentCategory}`);
|
|
1174
1288
|
}
|
|
1175
1289
|
}
|
|
1290
|
+
const retainedAgents = [...priorAgents].filter(n => !writtenAgents.includes(n) && fs.existsSync(path.join(targetAgentsDir, n)));
|
|
1291
|
+
recordGenerated(targetDir, 'agents', [...writtenAgents, ...retainedAgents]);
|
|
1176
1292
|
}
|
|
1177
1293
|
/**
|
|
1178
1294
|
* Find source helpers directory.
|
|
@@ -226,7 +226,10 @@ function generateHooksConfig(config, graphify = true) {
|
|
|
226
226
|
],
|
|
227
227
|
},
|
|
228
228
|
{
|
|
229
|
-
|
|
229
|
+
// NotebookEdit is listed explicitly: its content field (`new_source`)
|
|
230
|
+
// is scanned by the same secrets gate as Write/Edit/MultiEdit, so it
|
|
231
|
+
// must not depend on `Edit` happening to substring-match.
|
|
232
|
+
matcher: 'Write|Edit|MultiEdit|NotebookEdit',
|
|
230
233
|
hooks: [
|
|
231
234
|
{
|
|
232
235
|
// Was 'pre-edit' — not a registered hook-handler.cjs dispatch
|
|
@@ -26,6 +26,10 @@ export interface KnowledgeExcerpt {
|
|
|
26
26
|
similarity: number;
|
|
27
27
|
chunkIndex: number;
|
|
28
28
|
scope: string;
|
|
29
|
+
/** True when this chunk belongs to a document version that has since been
|
|
30
|
+
* re-ingested (its contentHash is no longer the file's current one). Only
|
|
31
|
+
* ever set when the caller opted into `includeSuperseded`. */
|
|
32
|
+
superseded?: boolean;
|
|
29
33
|
}
|
|
30
34
|
export interface DocumentMeta {
|
|
31
35
|
filePath: string;
|
|
@@ -40,6 +44,16 @@ export declare function ingestDirectory(dirPath: string, scope?: string, opts?:
|
|
|
40
44
|
rootDir?: string;
|
|
41
45
|
onProgress?: (file: string, done: number, total: number) => void;
|
|
42
46
|
}): Promise<BatchIngestResult>;
|
|
47
|
+
/** Content hashes of the documents currently indexed under `rootDir`. */
|
|
48
|
+
export declare function liveContentHashes(rootDir: string): Set<string>;
|
|
49
|
+
/**
|
|
50
|
+
* True when `key` is a document chunk whose version is no longer current.
|
|
51
|
+
* Non-`doc:` keys are never superseded, and an empty `live` set means the
|
|
52
|
+
* metadata log is missing/unreadable — in that case nothing is filtered,
|
|
53
|
+
* because "no metadata" must not read as "everything is stale".
|
|
54
|
+
*/
|
|
55
|
+
export declare function isSupersededKey(key: string, live: Set<string>): boolean;
|
|
56
|
+
export declare function supersededOverfetchLimit(limit: number, live: Set<string>): number;
|
|
43
57
|
export declare function searchKnowledge(query: string, opts?: {
|
|
44
58
|
scope?: string;
|
|
45
59
|
limit?: number;
|
|
@@ -47,6 +61,9 @@ export declare function searchKnowledge(query: string, opts?: {
|
|
|
47
61
|
rootDir?: string;
|
|
48
62
|
/** which store(s): project-only, global-only, or both (default). */
|
|
49
63
|
store?: 'project' | 'global' | 'all';
|
|
64
|
+
/** Return chunks from superseded document versions too, flagged
|
|
65
|
+
* `superseded: true`. Default false — see the note above `liveContentHashes`. */
|
|
66
|
+
includeSuperseded?: boolean;
|
|
50
67
|
}): Promise<KnowledgeExcerpt[]>;
|
|
51
68
|
export declare function listDocuments(rootDir?: string, scope?: string): DocumentMeta[];
|
|
52
69
|
export declare function removeDocument(filePath: string, scope?: string, rootDir?: string): Promise<void>;
|
|
@@ -358,6 +358,52 @@ export async function ingestDirectory(dirPath, scope = 'shared', opts) {
|
|
|
358
358
|
/** Small additive boost so project knowledge wins ties against the global
|
|
359
359
|
* brain — local context is more likely to be what the user means. */
|
|
360
360
|
const PROJECT_SCOPE_BOOST = 0.05;
|
|
361
|
+
// ── Superseded-version filtering ───────────────────────────────────
|
|
362
|
+
//
|
|
363
|
+
// Chunk keys are `doc:<contentHash>:<chunkIndex>`. Re-ingesting a changed file
|
|
364
|
+
// produces a NEW contentHash, so its chunks land under new keys — the previous
|
|
365
|
+
// version's rows are never touched (`removeDocument` only tombstones metadata;
|
|
366
|
+
// the bridge exposes no delete-by-prefix). The store therefore accumulates every
|
|
367
|
+
// version a document has ever had, and all of them stay searchable.
|
|
368
|
+
//
|
|
369
|
+
// Measured on this repo's own store (2026-07-26): 9,067 `doc:`-keyed rows in
|
|
370
|
+
// `knowledge:shared` spanning 798 distinct content hashes, of which only 139
|
|
371
|
+
// are current — 8,542 rows (94.2%) are orphaned older versions.
|
|
372
|
+
//
|
|
373
|
+
// Nothing is deleted here. The current-hash set from doc-metadata.jsonl is used
|
|
374
|
+
// to decide what search RETURNS; `includeSuperseded` puts the old versions back
|
|
375
|
+
// (flagged `superseded: true`) for anyone who wants document history.
|
|
376
|
+
/** Content hashes of the documents currently indexed under `rootDir`. */
|
|
377
|
+
export function liveContentHashes(rootDir) {
|
|
378
|
+
const live = new Set();
|
|
379
|
+
for (const m of readMetadata(rootDir))
|
|
380
|
+
if (m.contentHash)
|
|
381
|
+
live.add(m.contentHash);
|
|
382
|
+
return live;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* True when `key` is a document chunk whose version is no longer current.
|
|
386
|
+
* Non-`doc:` keys are never superseded, and an empty `live` set means the
|
|
387
|
+
* metadata log is missing/unreadable — in that case nothing is filtered,
|
|
388
|
+
* because "no metadata" must not read as "everything is stale".
|
|
389
|
+
*/
|
|
390
|
+
export function isSupersededKey(key, live) {
|
|
391
|
+
if (!key || !key.startsWith('doc:'))
|
|
392
|
+
return false;
|
|
393
|
+
if (live.size === 0)
|
|
394
|
+
return false;
|
|
395
|
+
return !live.has(key.split(':')[1] ?? '');
|
|
396
|
+
}
|
|
397
|
+
/** How many rows to ask the backend for per requested result when superseded
|
|
398
|
+
* filtering is active — most rows in a long-lived store are old versions, so
|
|
399
|
+
* a 1:1 fetch would return an almost-empty page. */
|
|
400
|
+
const SUPERSEDED_OVERFETCH = 20;
|
|
401
|
+
const SUPERSEDED_OVERFETCH_CAP = 300;
|
|
402
|
+
export function supersededOverfetchLimit(limit, live) {
|
|
403
|
+
if (live.size === 0)
|
|
404
|
+
return limit;
|
|
405
|
+
return Math.min(Math.max(limit * SUPERSEDED_OVERFETCH, limit), SUPERSEDED_OVERFETCH_CAP);
|
|
406
|
+
}
|
|
361
407
|
export async function searchKnowledge(query, opts) {
|
|
362
408
|
const bridge = await getBridge();
|
|
363
409
|
if (!bridge)
|
|
@@ -373,17 +419,28 @@ export async function searchKnowledge(query, opts) {
|
|
|
373
419
|
if (store !== 'project') {
|
|
374
420
|
targets.push({ ns: namespace('global'), dbPath: GLOBAL_BRAIN_SENTINEL, root: globalBrainRoot(), label: 'global', boost: 0 });
|
|
375
421
|
}
|
|
422
|
+
const includeSuperseded = opts?.includeSuperseded === true;
|
|
376
423
|
const perTarget = await Promise.all(targets.map(async (t) => {
|
|
424
|
+
const meta = readMetadata(t.root);
|
|
425
|
+
const live = new Set();
|
|
426
|
+
for (const m of meta)
|
|
427
|
+
if (m.contentHash)
|
|
428
|
+
live.add(m.contentHash);
|
|
429
|
+
// Old versions dominate a long-lived store, so a 1:1 fetch would come back
|
|
430
|
+
// nearly empty once they are filtered out. Over-fetch, then trim.
|
|
431
|
+
const fetchLimit = includeSuperseded ? limit : supersededOverfetchLimit(limit, live);
|
|
377
432
|
const result = await bridge.bridgeSearchEntries({
|
|
378
|
-
query, namespace: t.ns, limit, threshold: minScore, dbPath: t.dbPath,
|
|
433
|
+
query, namespace: t.ns, limit: fetchLimit, threshold: minScore, dbPath: t.dbPath,
|
|
379
434
|
}).catch(() => null);
|
|
380
435
|
if (!result?.success || !result.results.length)
|
|
381
436
|
return [];
|
|
382
|
-
const meta = readMetadata(t.root);
|
|
383
437
|
const hashToFile = new Map();
|
|
384
438
|
for (const m of meta)
|
|
385
439
|
hashToFile.set(m.contentHash, m.filePath);
|
|
386
|
-
|
|
440
|
+
const kept = includeSuperseded
|
|
441
|
+
? result.results
|
|
442
|
+
: result.results.filter((r) => !isSupersededKey(String(r.key ?? ''), live));
|
|
443
|
+
return kept.slice(0, limit).map((r) => {
|
|
387
444
|
const parts = r.key.startsWith('doc:') ? r.key.split(':') : [];
|
|
388
445
|
const hash = parts[1] ?? '';
|
|
389
446
|
const idx = parseInt(parts[2] ?? '0', 10);
|
|
@@ -391,6 +448,7 @@ export async function searchKnowledge(query, opts) {
|
|
|
391
448
|
// hash→file map can misattribute when two documents share identical
|
|
392
449
|
// content, and goes empty when a re-ingested file's hash changed.
|
|
393
450
|
const srcTag = (r.tags ?? []).find((tag) => tag.startsWith('src:'));
|
|
451
|
+
const superseded = includeSuperseded && isSupersededKey(String(r.key ?? ''), live);
|
|
394
452
|
return {
|
|
395
453
|
id: r.id,
|
|
396
454
|
filePath: srcTag ? srcTag.slice(4) : hashToFile.get(hash) ?? '',
|
|
@@ -398,6 +456,7 @@ export async function searchKnowledge(query, opts) {
|
|
|
398
456
|
similarity: r.score + t.boost,
|
|
399
457
|
chunkIndex: isNaN(idx) ? 0 : idx,
|
|
400
458
|
scope: t.label,
|
|
459
|
+
...(superseded ? { superseded: true } : {}),
|
|
401
460
|
};
|
|
402
461
|
});
|
|
403
462
|
}));
|
|
@@ -544,9 +544,13 @@ export const allEmbeddingsTools = [
|
|
|
544
544
|
message: 'Embedding substrate initialized',
|
|
545
545
|
};
|
|
546
546
|
case 'drift':
|
|
547
|
-
// Get real drift metrics if available
|
|
547
|
+
// Get real drift metrics if available.
|
|
548
|
+
// initializeIntelligence() MUST run first: getIntelligenceStats()
|
|
549
|
+
// reads module singletons (sonaCoordinator / reasoningBank) that are
|
|
550
|
+
// null until init, so a populated store otherwise reports 0 patterns.
|
|
548
551
|
try {
|
|
549
|
-
const { getIntelligenceStats } = await import('../memory/intelligence.js');
|
|
552
|
+
const { getIntelligenceStats, initializeIntelligence } = await import('../memory/intelligence.js');
|
|
553
|
+
await initializeIntelligence();
|
|
550
554
|
const stats = getIntelligenceStats();
|
|
551
555
|
return {
|
|
552
556
|
success: true,
|
|
@@ -564,17 +568,20 @@ export const allEmbeddingsTools = [
|
|
|
564
568
|
: 'No patterns stored yet - drift detection inactive',
|
|
565
569
|
};
|
|
566
570
|
}
|
|
567
|
-
catch {
|
|
571
|
+
catch (e) {
|
|
572
|
+
// Failing to read the store is NOT a drift report of zero.
|
|
568
573
|
return {
|
|
569
|
-
success:
|
|
574
|
+
success: false,
|
|
570
575
|
action: 'drift',
|
|
576
|
+
error: `Intelligence store unavailable — drift status unknown: ${e.message}`,
|
|
571
577
|
status: { semanticDrift: { enabled: false, reason: 'Intelligence module unavailable' } },
|
|
572
578
|
};
|
|
573
579
|
}
|
|
574
580
|
case 'consolidate':
|
|
575
|
-
// Get real consolidation metrics
|
|
581
|
+
// Get real consolidation metrics — same init requirement as 'drift'.
|
|
576
582
|
try {
|
|
577
|
-
const { getIntelligenceStats } = await import('../memory/intelligence.js');
|
|
583
|
+
const { getIntelligenceStats, initializeIntelligence } = await import('../memory/intelligence.js');
|
|
584
|
+
await initializeIntelligence();
|
|
578
585
|
const stats = getIntelligenceStats();
|
|
579
586
|
return {
|
|
580
587
|
success: true,
|
|
@@ -590,10 +597,11 @@ export const allEmbeddingsTools = [
|
|
|
590
597
|
message: `ReasoningBank: ${stats.reasoningBankSize} patterns, ${stats.trajectoriesRecorded} trajectories`,
|
|
591
598
|
};
|
|
592
599
|
}
|
|
593
|
-
catch {
|
|
600
|
+
catch (e) {
|
|
594
601
|
return {
|
|
595
|
-
success:
|
|
602
|
+
success: false,
|
|
596
603
|
action: 'consolidate',
|
|
604
|
+
error: `Intelligence store unavailable — consolidation status unknown: ${e.message}`,
|
|
597
605
|
status: { memoryPhysics: { enabled: false, reason: 'Intelligence module unavailable' } },
|
|
598
606
|
};
|
|
599
607
|
}
|