@monoes/monomindcli 2.16.12 → 2.16.13
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/.claude/agents/core/coder.md +1 -1
- package/.claude/agents/engineering/engineering-devops-automator.md +1 -1
- package/.claude/agents/github/repo-architect.md +1 -1
- package/.claude/agents/testing/production-validator.md +1 -1
- package/.claude/agents/testing/testing-tool-evaluator.md +1 -1
- package/.claude/commands/github/pr-manager.md +1 -1
- package/.claude/helpers/pick-rank.cjs +34 -4
- package/.claude/skills/monolean-audit/SKILL.md +1 -1
- package/dist/src/commands/cleanup-data.d.ts +30 -0
- package/dist/src/commands/cleanup-data.d.ts.map +1 -0
- package/dist/src/commands/cleanup-data.js +138 -0
- package/dist/src/commands/cleanup-data.js.map +1 -0
- package/dist/src/commands/cleanup-scratch.d.ts +43 -0
- package/dist/src/commands/cleanup-scratch.d.ts.map +1 -0
- package/dist/src/commands/cleanup-scratch.js +125 -0
- package/dist/src/commands/cleanup-scratch.js.map +1 -0
- package/dist/src/commands/cleanup.d.ts +2 -39
- package/dist/src/commands/cleanup.d.ts.map +1 -1
- package/dist/src/commands/cleanup.js +5 -247
- package/dist/src/commands/cleanup.js.map +1 -1
- package/dist/src/commands/doctor-gitignore-checks.d.ts +10 -0
- package/dist/src/commands/doctor-gitignore-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-gitignore-checks.js +141 -0
- package/dist/src/commands/doctor-gitignore-checks.js.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts +10 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.js +292 -0
- package/dist/src/commands/doctor-helpers-checks.js.map +1 -0
- package/dist/src/commands/doctor-memory-checks.d.ts +47 -0
- package/dist/src/commands/doctor-memory-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-memory-checks.js +298 -0
- package/dist/src/commands/doctor-memory-checks.js.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts +9 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.js +331 -0
- package/dist/src/commands/doctor-monograph-checks.js.map +1 -0
- package/dist/src/commands/doctor-project-checks.d.ts +7 -66
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +10 -1331
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor-routing-checks.d.ts +10 -0
- package/dist/src/commands/doctor-routing-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-routing-checks.js +194 -0
- package/dist/src/commands/doctor-routing-checks.js.map +1 -0
- package/dist/src/commands/doctor-worker-checks.d.ts +17 -0
- package/dist/src/commands/doctor-worker-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-worker-checks.js +122 -0
- package/dist/src/commands/doctor-worker-checks.js.map +1 -0
- package/dist/src/commands/hooks-routing-commands.js +1 -1
- package/dist/src/commands/hooks-routing-commands.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +6 -15
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/never-commit.d.ts +3 -3
- package/dist/src/init/types.d.ts +1 -1
- package/dist/src/init/types.js +1 -1
- package/dist/src/knowledge/document-store.js +1 -1
- package/dist/src/knowledge/document-store.js.map +1 -1
- package/dist/src/mcp-tools/hooks-edit-command.d.ts +10 -0
- package/dist/src/mcp-tools/hooks-edit-command.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-edit-command.js +247 -0
- package/dist/src/mcp-tools/hooks-edit-command.js.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.js +408 -0
- package/dist/src/mcp-tools/hooks-learning.js.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts +8 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js +205 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js.map +1 -0
- package/dist/src/mcp-tools/hooks-route.d.ts +12 -0
- package/dist/src/mcp-tools/hooks-route.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-route.js +251 -0
- package/dist/src/mcp-tools/hooks-route.js.map +1 -0
- package/dist/src/mcp-tools/hooks-routing.d.ts +6 -27
- package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-routing.js +6 -1727
- package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
- package/dist/src/mcp-tools/hooks-session.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-session.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-session.js +243 -0
- package/dist/src/mcp-tools/hooks-session.js.map +1 -0
- package/dist/src/mcp-tools/hooks-task.d.ts +17 -0
- package/dist/src/mcp-tools/hooks-task.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-task.js +427 -0
- package/dist/src/mcp-tools/hooks-task.js.map +1 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts +2 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +2 -1
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +1 -1
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/memory/bm25-index.d.ts +2 -2
- package/dist/src/memory/bm25-index.js +2 -2
- package/dist/src/memory/embedding-operations.js +2 -2
- package/dist/src/memory/embedding-operations.js.map +1 -1
- package/dist/src/memory/entity-name-key.d.ts +1 -1
- package/dist/src/memory/entity-name-key.js +1 -1
- package/dist/src/memory/hnsw-operations.d.ts +1 -1
- package/dist/src/memory/hnsw-operations.js +1 -1
- package/dist/src/memory/memory-bridge-backend.d.ts +43 -0
- package/dist/src/memory/memory-bridge-backend.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-backend.js +363 -0
- package/dist/src/memory/memory-bridge-backend.js.map +1 -0
- package/dist/src/memory/memory-bridge-core.d.ts +42 -0
- package/dist/src/memory/memory-bridge-core.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-core.js +126 -0
- package/dist/src/memory/memory-bridge-core.js.map +1 -0
- package/dist/src/memory/memory-bridge-entries.d.ts +131 -0
- package/dist/src/memory/memory-bridge-entries.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-entries.js +227 -0
- package/dist/src/memory/memory-bridge-entries.js.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts +60 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.js +118 -0
- package/dist/src/memory/memory-bridge-feedback.js.map +1 -0
- package/dist/src/memory/memory-bridge-paths.d.ts +123 -0
- package/dist/src/memory/memory-bridge-paths.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-paths.js +335 -0
- package/dist/src/memory/memory-bridge-paths.js.map +1 -0
- package/dist/src/memory/memory-bridge-search.d.ts +48 -0
- package/dist/src/memory/memory-bridge-search.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-search.js +383 -0
- package/dist/src/memory/memory-bridge-search.js.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts +38 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.js +109 -0
- package/dist/src/memory/memory-bridge-sessions.js.map +1 -0
- package/dist/src/memory/memory-bridge-store.d.ts +58 -0
- package/dist/src/memory/memory-bridge-store.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-store.js +201 -0
- package/dist/src/memory/memory-bridge-store.js.map +1 -0
- package/dist/src/memory/memory-bridge.d.ts +10 -438
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +14 -1787
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-kg-claims.d.ts +52 -0
- package/dist/src/memory/memory-kg-claims.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-claims.js +99 -0
- package/dist/src/memory/memory-kg-claims.js.map +1 -0
- package/dist/src/memory/memory-kg-index.d.ts +86 -0
- package/dist/src/memory/memory-kg-index.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-index.js +256 -0
- package/dist/src/memory/memory-kg-index.js.map +1 -0
- package/dist/src/memory/memory-kg-ingest.d.ts +44 -0
- package/dist/src/memory/memory-kg-ingest.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-ingest.js +361 -0
- package/dist/src/memory/memory-kg-ingest.js.map +1 -0
- package/dist/src/memory/memory-kg-model.d.ts +168 -0
- package/dist/src/memory/memory-kg-model.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-model.js +158 -0
- package/dist/src/memory/memory-kg-model.js.map +1 -0
- package/dist/src/memory/memory-kg-names.d.ts +64 -0
- package/dist/src/memory/memory-kg-names.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-names.js +187 -0
- package/dist/src/memory/memory-kg-names.js.map +1 -0
- package/dist/src/memory/memory-kg-promote.d.ts +79 -0
- package/dist/src/memory/memory-kg-promote.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-promote.js +248 -0
- package/dist/src/memory/memory-kg-promote.js.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts +77 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.js +299 -0
- package/dist/src/memory/memory-kg-rebuild.js.map +1 -0
- package/dist/src/memory/memory-kg-rollback.d.ts +47 -0
- package/dist/src/memory/memory-kg-rollback.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rollback.js +193 -0
- package/dist/src/memory/memory-kg-rollback.js.map +1 -0
- package/dist/src/memory/memory-kg-rules.d.ts +64 -0
- package/dist/src/memory/memory-kg-rules.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rules.js +251 -0
- package/dist/src/memory/memory-kg-rules.js.map +1 -0
- package/dist/src/memory/memory-kg-scan.d.ts +118 -0
- package/dist/src/memory/memory-kg-scan.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-scan.js +209 -0
- package/dist/src/memory/memory-kg-scan.js.map +1 -0
- package/dist/src/memory/memory-kg-search.d.ts +80 -0
- package/dist/src/memory/memory-kg-search.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-search.js +239 -0
- package/dist/src/memory/memory-kg-search.js.map +1 -0
- package/dist/src/memory/memory-kg.d.ts +19 -547
- package/dist/src/memory/memory-kg.d.ts.map +1 -1
- package/dist/src/memory/memory-kg.js +10 -2389
- package/dist/src/memory/memory-kg.js.map +1 -1
- package/dist/src/ui/org-hil.mjs +1 -1
- package/dist/src/ui/routes-org-agents.mjs +318 -0
- package/dist/src/ui/routes-org-approvals.mjs +133 -0
- package/dist/src/ui/routes-org-config.mjs +459 -0
- package/dist/src/ui/routes-org-control.mjs +300 -0
- package/dist/src/ui/routes-org-files.mjs +172 -0
- package/dist/src/ui/routes-org-helpers.mjs +84 -0
- package/dist/src/ui/routes-org-knowledge.mjs +262 -0
- package/dist/src/ui/routes-org-lifecycle.mjs +224 -0
- package/dist/src/ui/routes-org-live.mjs +278 -0
- package/dist/src/ui/routes-org-mastermind.mjs +346 -0
- package/dist/src/ui/routes-org-planning.mjs +398 -0
- package/dist/src/ui/routes-org-runs.mjs +430 -0
- package/dist/src/ui/routes-org-status.mjs +342 -0
- package/dist/src/ui/routes-org-workflows.mjs +212 -0
- package/dist/src/ui/routes-org.mjs +26 -3847
- package/dist/src/ui/server.mjs +1 -1
- package/dist/src/utils/native-error.d.ts +1 -1
- package/dist/src/utils/native-error.js +1 -1
- package/dist/src/utils/paths.d.ts +2 -1
- package/dist/src/utils/paths.d.ts.map +1 -1
- package/dist/src/utils/paths.js +2 -1
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/org-skills/adversarial-reviewer/SKILL.md +1 -1
- package/org-skills/board-deck-builder/SKILL.md +1 -1
- package/org-skills/code-reviewer/SKILL.md +1 -1
- package/org-skills/debugging-wizard/SKILL.md +1 -1
- package/org-skills/scientific-thinking-literature-review/SKILL.md +1 -1
- package/org-skills/scientific-thinking-scholar-evaluation/SKILL.md +1 -1
- package/package.json +8 -8
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
* Config, memory, API keys, MCP, monograph, helpers, routing, gates, gitignore, worker metrics
|
|
4
4
|
*/
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
|
-
import {
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
8
7
|
import { homedir } from 'node:os';
|
|
9
|
-
import {
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { isKeptUserEdit } from '../init/file-guard.js';
|
|
12
|
-
import { DOCTOR_TRACKED_HELPERS, OBSOLETE_HELPER_NAMES } from '../init/helpers-generator.js';
|
|
13
|
-
import { MONOMIND_NEVER_COMMIT } from '../init/never-commit.js';
|
|
8
|
+
import { join } from 'node:path';
|
|
14
9
|
import { mcpAddHint } from '../platform-adapters/renderers/mcp.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
10
|
+
import { CONFIG_JSON_CANDIDATE_PATHS, CONFIG_YAML_CANDIDATE_PATHS } from '../utils/paths.js';
|
|
11
|
+
import { MAX_DOCTOR_CONFIG_BYTES } from './doctor-env-checks.js';
|
|
12
|
+
export { checkGitignoreCoverage, fixGitignoreCoverage } from './doctor-gitignore-checks.js';
|
|
13
|
+
export { checkGuidanceGates, checkHelpersFresh, fixStaleHelpers } from './doctor-helpers-checks.js';
|
|
14
|
+
export { checkDocumentExtractors, checkMemoryDatabase, checkMemoryKnowledgeGraph, checkMemoryProficiency, checkProjectRoot, checkSecondBrainModel, } from './doctor-memory-checks.js';
|
|
15
|
+
export { checkMonoesMemory, checkMonograph, checkMonographFreshness, } from './doctor-monograph-checks.js';
|
|
16
|
+
export { checkAgentRegistry, checkMonoesIntegration } from './doctor-routing-checks.js';
|
|
17
|
+
export { checkMetricsFreshness, checkSecurityAuditFindings } from './doctor-worker-checks.js';
|
|
18
18
|
export async function checkConfigFile() {
|
|
19
19
|
const jsonPaths = CONFIG_JSON_CANDIDATE_PATHS;
|
|
20
20
|
for (const configPath of jsonPaths) {
|
|
@@ -45,218 +45,6 @@ export async function checkConfigFile() {
|
|
|
45
45
|
fix: 'monomind config init',
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
/** o-16 (AC-6): the marker walk that resolves "this project" for the Memory
|
|
49
|
-
* & Knowledge store can silently adopt a wrong ancestor — the incident that
|
|
50
|
-
* motivated this check was a shared scratch parent's bare `.monomind`
|
|
51
|
-
* capturing an unrelated fixture, with no visible symptom until a search
|
|
52
|
-
* came back empty. Disclose the resolved root and why it was chosen, so a
|
|
53
|
-
* user can see which directory their brain is keyed to without reading code
|
|
54
|
-
* or debug logs. Always 'info': the resolution itself is never a health
|
|
55
|
-
* problem, only the failure to see it was.
|
|
56
|
-
*
|
|
57
|
-
* Named/scoped to "Memory Project Root", not a bare "Project Root" (o-16
|
|
58
|
-
* revision 1, reviewer MAJOR 2): `MONOMIND_PROJECT_ROOT` has a SECOND,
|
|
59
|
-
* independent consumer — `mcp-tools/guidance-tools.ts:findProjectRoot()`,
|
|
60
|
-
* which uses a different marker (`.claude`) and can legitimately resolve a
|
|
61
|
-
* different directory (see the divergence explained at
|
|
62
|
-
* `memory-bridge.ts`'s `getProjectRoot()` doc comment). A generic "Project
|
|
63
|
-
* Root" label would claim there is one true answer when this commit is
|
|
64
|
-
* what creates a state where two resolvers can disagree — this check must
|
|
65
|
-
* not be the thing that misleads about that. */
|
|
66
|
-
export async function checkProjectRoot() {
|
|
67
|
-
const name = 'Memory Project Root';
|
|
68
|
-
const { getProjectRootResolution } = await import('../memory/memory-bridge.js');
|
|
69
|
-
const { root, reason, ignoredBareMonomind, invalidAnchor } = getProjectRootResolution();
|
|
70
|
-
const reasonText = {
|
|
71
|
-
'explicit-anchor': 'MONOMIND_PROJECT_ROOT anchor',
|
|
72
|
-
git: 'nearest .git ancestor',
|
|
73
|
-
'monomind-at-start': 'starting directory carries .monomind',
|
|
74
|
-
'monomind-with-marker': '.monomind ancestor confirmed by a project manifest',
|
|
75
|
-
'start-fallback': 'no adoptable marker found — using the current directory',
|
|
76
|
-
};
|
|
77
|
-
// o-16 revision 1 (dev-lead Addition 1): the anchor's own disclosure must
|
|
78
|
-
// cover the ambiguous case it exists to resolve — a SET-but-INVALID
|
|
79
|
-
// anchor — not just the happy path, or a typo'd MONOMIND_PROJECT_ROOT
|
|
80
|
-
// silently does nothing and the user has no way to find out why. Appended
|
|
81
|
-
// regardless of which `reason` the walk ultimately fell back to.
|
|
82
|
-
const invalidAnchorNote = invalidAnchor
|
|
83
|
-
? ` [MONOMIND_PROJECT_ROOT="${invalidAnchor.value}" ignored — ${invalidAnchor.problem}]`
|
|
84
|
-
: '';
|
|
85
|
-
if (reason === 'start-fallback' && ignoredBareMonomind) {
|
|
86
|
-
return {
|
|
87
|
-
name,
|
|
88
|
-
status: 'info',
|
|
89
|
-
message: `${root} (${reasonText[reason]}; ignored bare .monomind at ${ignoredBareMonomind} — add a project marker there, or set MONOMIND_PROJECT_ROOT, to adopt it)${invalidAnchorNote}`,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return { name, status: 'info', message: `${root} (${reasonText[reason]})${invalidAnchorNote}` };
|
|
93
|
-
}
|
|
94
|
-
export async function checkMemoryDatabase() {
|
|
95
|
-
const dbPaths = MEMORY_DB_CANDIDATE_PATHS;
|
|
96
|
-
for (const dbPath of dbPaths) {
|
|
97
|
-
if (existsSync(dbPath)) {
|
|
98
|
-
try {
|
|
99
|
-
const sizeMB = (statSync(dbPath).size / 1024 / 1024).toFixed(2);
|
|
100
|
-
return { name: 'Memory Database', status: 'pass', message: `${dbPath} (${sizeMB} MB)` };
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return { name: 'Memory Database', status: 'warn', message: `${dbPath} (unable to stat)` };
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
name: 'Memory Database',
|
|
109
|
-
status: 'warn',
|
|
110
|
-
message: 'Not initialized',
|
|
111
|
-
fix: 'monomind memory init',
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
/** Memory knowledge graph (cognee port): entities/relations/rules distilled
|
|
115
|
-
* from sessions and org runs. Informational — an empty graph on a fresh
|
|
116
|
-
* project is normal, so it never fails. */
|
|
117
|
-
export async function checkMemoryKnowledgeGraph() {
|
|
118
|
-
const name = 'Memory Knowledge Graph';
|
|
119
|
-
try {
|
|
120
|
-
const kg = await import('../memory/memory-kg.js');
|
|
121
|
-
const bridge = await import('../memory/memory-bridge.js');
|
|
122
|
-
const project = await kg.kgStats();
|
|
123
|
-
// Org KG facts are namespaced per org (kg:nodes:org:<name> etc.), so an
|
|
124
|
-
// unscoped kgStats against the org store reads the now-unused flat
|
|
125
|
-
// namespaces and reports zero for every org. Sum each org's own scope
|
|
126
|
-
// instead — the org store holds all of them, keyed by scope.
|
|
127
|
-
const orgDb = join(process.cwd(), '.monomind', 'org-memory');
|
|
128
|
-
let org = null;
|
|
129
|
-
if (existsSync(orgDb)) {
|
|
130
|
-
const { orgKgScope } = await import('../orgrt/org-memory.js');
|
|
131
|
-
const { listOrgConfigFiles } = await import('./org.js');
|
|
132
|
-
const orgsDir = join(process.cwd(), '.monomind', 'orgs');
|
|
133
|
-
const orgNames = existsSync(orgsDir)
|
|
134
|
-
? listOrgConfigFiles(orgsDir).map((f) => f.replace(/\.json$/, ''))
|
|
135
|
-
: [];
|
|
136
|
-
org = { nodes: 0, edges: 0, rules: 0 };
|
|
137
|
-
for (const orgName of orgNames) {
|
|
138
|
-
const s = await kg.kgStats({ dbPath: orgDb, scope: orgKgScope(orgName) });
|
|
139
|
-
org.nodes += s.nodes;
|
|
140
|
-
org.edges += s.edges;
|
|
141
|
-
org.rules += s.rules;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
await bridge.shutdownBridge().catch(() => {
|
|
145
|
-
/* best effort */
|
|
146
|
-
});
|
|
147
|
-
const total = project.nodes + (org?.nodes ?? 0);
|
|
148
|
-
const fmt = (s) => `${s.nodes}n/${s.edges}e/${s.rules}r`;
|
|
149
|
-
if (total === 0) {
|
|
150
|
-
return {
|
|
151
|
-
name,
|
|
152
|
-
status: 'warn',
|
|
153
|
-
message: 'Empty — sessions/org runs have not distilled knowledge yet',
|
|
154
|
-
fix: 'org runs call org_learn automatically; sessions use memory_kg_ingest',
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
name,
|
|
159
|
-
status: 'pass',
|
|
160
|
-
message: `project ${fmt(project)}${org ? ` · org ${fmt(org)}` : ''}`,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
catch (err) {
|
|
164
|
-
return {
|
|
165
|
-
name,
|
|
166
|
-
status: 'warn',
|
|
167
|
-
message: `Check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/** Count live (non-tombstoned) ingested documents under a brain root, without
|
|
172
|
-
* creating the directory — `listDocuments` mkdir's its metadata dir, which
|
|
173
|
-
* would materialize an empty global brain just by running doctor. */
|
|
174
|
-
async function countBrainDocs(root) {
|
|
175
|
-
if (!existsSync(join(root, '.monomind', 'knowledge', 'doc-metadata.jsonl')))
|
|
176
|
-
return 0;
|
|
177
|
-
try {
|
|
178
|
-
const { listDocuments } = await import('../knowledge/document-pipeline.js');
|
|
179
|
-
return listDocuments(root).length;
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
return 0;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/** Second Brain: when there is any indexed knowledge — ingested docs in this
|
|
186
|
-
* project, the personal global brain, or the monograph god-node chunk —
|
|
187
|
-
* semantic search needs the local embedding model. Its absence is silent
|
|
188
|
-
* (search degrades to keyword matching), so surface it here instead. */
|
|
189
|
-
export async function checkSecondBrainModel() {
|
|
190
|
-
const name = 'Second Brain Model';
|
|
191
|
-
const { BRIDGE_EMBEDDING_MODEL, getGlobalBrainDir, getProjectRoot } = await import('../memory/memory-bridge.js');
|
|
192
|
-
const projectDocs = await countBrainDocs(getProjectRoot());
|
|
193
|
-
const globalDocs = await countBrainDocs(getGlobalBrainDir());
|
|
194
|
-
// chunks.jsonl holds the monograph god-node chunk — knowledge with no doc
|
|
195
|
-
// ingest. Unlike the document store it is written per-CWD by the session
|
|
196
|
-
// hook, not at the project root, so check both rather than assuming.
|
|
197
|
-
const chunksIn = (root) => existsSync(join(root, '.monomind', 'knowledge', 'chunks.jsonl'));
|
|
198
|
-
const hasChunks = chunksIn(getProjectRoot()) || chunksIn(process.cwd());
|
|
199
|
-
if (!projectDocs && !globalDocs && !hasChunks) {
|
|
200
|
-
return { name, status: 'pass', message: 'No knowledge indexed (nothing to check)' };
|
|
201
|
-
}
|
|
202
|
-
const indexed = [
|
|
203
|
-
projectDocs ? `${projectDocs} project doc${projectDocs === 1 ? '' : 's'}` : null,
|
|
204
|
-
globalDocs ? `${globalDocs} global doc${globalDocs === 1 ? '' : 's'}` : null,
|
|
205
|
-
!projectDocs && !globalDocs ? 'monograph god-nodes' : null,
|
|
206
|
-
]
|
|
207
|
-
.filter(Boolean)
|
|
208
|
-
.join(' · ');
|
|
209
|
-
let entryPath = null;
|
|
210
|
-
try {
|
|
211
|
-
// exports map blocks package.json — resolve the entry file, then walk UP
|
|
212
|
-
// with path ops (no separator-sensitive regex: '/dist/'.replace broke on
|
|
213
|
-
// Windows paths, and any install layout without a dist segment).
|
|
214
|
-
// import.meta.resolve is missing on Node 20.0–20.5; fall back to CJS resolve.
|
|
215
|
-
const viaEsm = import.meta.resolve?.('@huggingface/transformers');
|
|
216
|
-
if (viaEsm)
|
|
217
|
-
entryPath = fileURLToPath(await viaEsm);
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
/* try CJS below */
|
|
221
|
-
}
|
|
222
|
-
if (!entryPath) {
|
|
223
|
-
try {
|
|
224
|
-
const { createRequire } = await import('node:module');
|
|
225
|
-
entryPath = createRequire(import.meta.url).resolve('@huggingface/transformers');
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
return {
|
|
229
|
-
name,
|
|
230
|
-
status: 'warn',
|
|
231
|
-
message: '@huggingface/transformers not installed — semantic search degraded to keyword matching',
|
|
232
|
-
fix: 'reinstall monomind (the model dependency is optional and may have failed to build)',
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// Walk up from the entry file to the package root (the dir named 'transformers').
|
|
237
|
-
let pkgDir = dirname(entryPath);
|
|
238
|
-
while (pkgDir !== dirname(pkgDir) && !pkgDir.endsWith('transformers'))
|
|
239
|
-
pkgDir = dirname(pkgDir);
|
|
240
|
-
// The cached model is whichever one the bridge actually loads. This used to
|
|
241
|
-
// look for '.cache/Xenova', a model the bridge stopped using — so the check
|
|
242
|
-
// reported "not downloaded" with a fully provisioned cache sitting on disk.
|
|
243
|
-
const modelCache = join(pkgDir, '.cache', ...BRIDGE_EMBEDDING_MODEL.split('/'));
|
|
244
|
-
if (pkgDir.endsWith('transformers') && existsSync(modelCache)) {
|
|
245
|
-
return {
|
|
246
|
-
name,
|
|
247
|
-
status: 'pass',
|
|
248
|
-
message: `Local embedding model cached — semantic search active (${indexed})`,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
name,
|
|
253
|
-
status: 'warn',
|
|
254
|
-
message: 'Embedding model not downloaded yet — searches use keyword matching until it is',
|
|
255
|
-
// Query-time paths all pass local_files_only and never fetch; this is the
|
|
256
|
-
// one command that downloads the weights.
|
|
257
|
-
fix: 'run once while online: monomind doc eval --provision-model (downloads ~270MB locally, one time)',
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
48
|
export async function checkApiKeys() {
|
|
261
49
|
const keys = ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY', 'OPENAI_API_KEY'];
|
|
262
50
|
const found = keys.filter((k) => process.env[k]);
|
|
@@ -399,1070 +187,6 @@ async function probeConfiguredMcpServer(entry, count, configPath, timeoutMs) {
|
|
|
399
187
|
fix: `Run it by hand to confirm: ${commandLine} # a pinned scoped package needs: npx -y --package=@monoes/monomindcli@<version> monomind mcp start`,
|
|
400
188
|
};
|
|
401
189
|
}
|
|
402
|
-
// Resolve an installed package's package.json through Node's real ESM
|
|
403
|
-
// resolution algorithm instead of guessing relative node_modules layouts.
|
|
404
|
-
// The old candidate-path approach assumed a nested install shape that never
|
|
405
|
-
// matches how npm actually hoists dependencies (flat local installs, npx's
|
|
406
|
-
// isolated cache dir, and global installs all put @monoes/* packages as
|
|
407
|
-
// siblings, not nested under this package's dist/ output) — so it always
|
|
408
|
-
// reported "not found" even when the package was present and importable.
|
|
409
|
-
// import.meta.resolve() is used (rather than createRequire().resolve()())
|
|
410
|
-
// because @monoes/monograph and @monoes/memory are ESM-only (no "require"
|
|
411
|
-
// export condition), so a CJS require.resolve() fails outright even when the
|
|
412
|
-
// package is installed. Their exports maps also don't expose a "./package.json"
|
|
413
|
-
// subpath, so the entry file is resolved first and package.json is found by
|
|
414
|
-
// walking up from it.
|
|
415
|
-
function _findInstalledPackageJson(specifier) {
|
|
416
|
-
try {
|
|
417
|
-
const entryUrl = import.meta.resolve(specifier);
|
|
418
|
-
let dir = dirname(fileURLToPath(entryUrl));
|
|
419
|
-
for (;;) {
|
|
420
|
-
const candidate = join(dir, 'package.json');
|
|
421
|
-
if (existsSync(candidate) && statSync(candidate).size <= MAX_DOCTOR_PKG_BYTES) {
|
|
422
|
-
try {
|
|
423
|
-
const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
424
|
-
if (pkg.name === specifier)
|
|
425
|
-
return { path: candidate, pkg };
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
/* keep walking */
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const parent = dirname(dir);
|
|
432
|
-
if (parent === dir)
|
|
433
|
-
return null;
|
|
434
|
-
dir = parent;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
catch {
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
export async function checkMonograph() {
|
|
442
|
-
const found = _findInstalledPackageJson('@monoes/monograph');
|
|
443
|
-
if (found) {
|
|
444
|
-
return {
|
|
445
|
-
name: 'Monograph',
|
|
446
|
-
status: 'pass',
|
|
447
|
-
message: `v${found.pkg.version || '?'} available (knowledge graph engine)`,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
return {
|
|
451
|
-
name: 'Monograph',
|
|
452
|
-
status: 'warn',
|
|
453
|
-
message: 'Package not found (knowledge graph disabled)',
|
|
454
|
-
fix: 'npm install -g monomind@latest',
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
function formatAge(ms) {
|
|
458
|
-
// Filesystem mtime can round to whole seconds and land a hair ahead of
|
|
459
|
-
// Date.now() for a file written moments ago — clamp so that never reads
|
|
460
|
-
// as a negative, nonsensical age.
|
|
461
|
-
const minutes = Math.floor(Math.max(0, ms) / 60000);
|
|
462
|
-
return minutes < 60 ? `${minutes}m` : `${Math.floor(minutes / 60)}h`;
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Best-effort "when was this package's own install last touched" probe,
|
|
466
|
-
* used only to soften a build.log-based failure (see #244) — never to
|
|
467
|
-
* harden one, so a wrong or missing answer here is always safe. Resolves
|
|
468
|
-
* `pkgName` from `cwd` (works through pnpm's symlinked store too, since
|
|
469
|
-
* `require.resolve` follows symlinks to the real file) and returns the
|
|
470
|
-
* latest mtime among its package.json and any `build/`/`prebuilds/` binary
|
|
471
|
-
* output, or null if the package can't be resolved at all — the caller
|
|
472
|
-
* treats null as "no evidence either way," not as "confirmed still broken."
|
|
473
|
-
*/
|
|
474
|
-
function findNativeModuleLastModifiedMs(pkgName, cwd) {
|
|
475
|
-
try {
|
|
476
|
-
const require = createRequire(join(cwd, 'noop.js'));
|
|
477
|
-
const entry = require.resolve(pkgName, { paths: [cwd] });
|
|
478
|
-
let pkgDir = dirname(entry);
|
|
479
|
-
for (let i = 0; i < 12 && !existsSync(join(pkgDir, 'package.json')); i++) {
|
|
480
|
-
const parent = dirname(pkgDir);
|
|
481
|
-
if (parent === pkgDir)
|
|
482
|
-
return null;
|
|
483
|
-
pkgDir = parent;
|
|
484
|
-
}
|
|
485
|
-
let latestMs = statSync(join(pkgDir, 'package.json')).mtimeMs;
|
|
486
|
-
for (const sub of ['build', 'prebuilds']) {
|
|
487
|
-
const subDir = join(pkgDir, sub);
|
|
488
|
-
if (!existsSync(subDir))
|
|
489
|
-
continue;
|
|
490
|
-
const stack = [subDir];
|
|
491
|
-
while (stack.length > 0) {
|
|
492
|
-
const current = stack.pop();
|
|
493
|
-
for (const entryName of readdirSync(current)) {
|
|
494
|
-
const full = join(current, entryName);
|
|
495
|
-
const st = statSync(full);
|
|
496
|
-
if (st.isDirectory())
|
|
497
|
-
stack.push(full);
|
|
498
|
-
else if (st.mtimeMs > latestMs)
|
|
499
|
-
latestMs = st.mtimeMs;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
return latestMs;
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
export async function checkMonographFreshness() {
|
|
510
|
-
try {
|
|
511
|
-
const cwd = process.cwd();
|
|
512
|
-
const dbPath = join(cwd, '.monomind', 'monograph.db');
|
|
513
|
-
const lockPath = join(cwd, '.monomind', 'graph', 'build.lock');
|
|
514
|
-
const statsPath = join(cwd, '.monomind', 'graph', 'stats.json');
|
|
515
|
-
const logPath = join(cwd, '.monomind', 'graph', 'build.log');
|
|
516
|
-
const hasDb = existsSync(dbPath);
|
|
517
|
-
if (!hasDb && !existsSync(statsPath)) {
|
|
518
|
-
// No graph yet — but that's one of three very different situations: a
|
|
519
|
-
// build is genuinely still running, one already ran and crashed (e.g.
|
|
520
|
-
// executor.ts's initKnowledgeGraph or monograph-freshen.cjs's detached
|
|
521
|
-
// spawn hit a native-module ABI mismatch), or nothing was ever
|
|
522
|
-
// attempted. Both spawners share this same build.lock/build.log pair,
|
|
523
|
-
// so this one check covers either origin.
|
|
524
|
-
//
|
|
525
|
-
// Lock existence (not freshness) is the right signal here: both
|
|
526
|
-
// spawners write build.lock with their OWN pid (the short-lived parent
|
|
527
|
-
// that immediately exits after spawning a detached child — not the
|
|
528
|
-
// child actually doing the build, so checking that pid's liveness
|
|
529
|
-
// would be checking the wrong process) and unlink it in a `finally`
|
|
530
|
-
// that runs on both success and a thrown/rejected build — so it's only
|
|
531
|
-
// ever left behind while the child is still actually running, or in
|
|
532
|
-
// the rare case of a hard OS-level kill that bypassed the `finally`.
|
|
533
|
-
// A large monorepo's first index can legitimately run well past any
|
|
534
|
-
// fixed "freshness" window, so age must not turn a live build into a
|
|
535
|
-
// false failure report. Neither spawner passes buildAsync an
|
|
536
|
-
// onProgress callback, so a clean run writes nothing to build.log
|
|
537
|
-
// until it's done — there's no heartbeat to check the log against, so
|
|
538
|
-
// this is worded to cover both "still running" and "was killed
|
|
539
|
-
// without cleaning up" rather than asserting liveness that can't
|
|
540
|
-
// actually be confirmed from disk.
|
|
541
|
-
if (existsSync(lockPath)) {
|
|
542
|
-
const ageMs = (() => {
|
|
543
|
-
try {
|
|
544
|
-
return Date.now() - statSync(lockPath).mtimeMs;
|
|
545
|
-
}
|
|
546
|
-
catch {
|
|
547
|
-
return 0;
|
|
548
|
-
}
|
|
549
|
-
})();
|
|
550
|
-
return {
|
|
551
|
-
name: 'Graph freshness',
|
|
552
|
-
status: 'warn',
|
|
553
|
-
message: `Monograph build in progress, or was interrupted — started ${formatAge(ageMs)} ago`,
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
if (existsSync(logPath)) {
|
|
557
|
-
let logTail = '';
|
|
558
|
-
try {
|
|
559
|
-
const raw = readFileSync(logPath, 'utf8');
|
|
560
|
-
// Bound the scan to the last MAX_BUILD_LOG_SCAN_BYTES, not the last
|
|
561
|
-
// 4000 — that window was cutting the actual "Error:"/"Exception:"
|
|
562
|
-
// text out of real Node uncaught-exception dumps. A native-module
|
|
563
|
-
// load failure (e.g. `bindings` walking a dozen candidate .node
|
|
564
|
-
// paths before giving up) prints its message and stack FIRST, then
|
|
565
|
-
// a long list of tried paths — a single such dump easily runs
|
|
566
|
-
// 7-8KB, so a 4000-char tail can land entirely inside the path
|
|
567
|
-
// list and see no error keyword at all, even though the log is
|
|
568
|
-
// nothing but a crash report. 64KB comfortably fits many realistic
|
|
569
|
-
// crash dumps (including chained causes) while still bounding this
|
|
570
|
-
// append-only, never-truncated file for a long-lived project that
|
|
571
|
-
// has failed and retried many times.
|
|
572
|
-
logTail =
|
|
573
|
-
raw.length > MAX_DOCTOR_BUILD_LOG_SCAN_BYTES
|
|
574
|
-
? raw.slice(-MAX_DOCTOR_BUILD_LOG_SCAN_BYTES)
|
|
575
|
-
: raw;
|
|
576
|
-
}
|
|
577
|
-
catch {
|
|
578
|
-
/* unreadable log — treat as no evidence, fall through below */
|
|
579
|
-
}
|
|
580
|
-
// build.log only proves a build ran at some point — a *successful*
|
|
581
|
-
// build also writes into it before producing monograph.db, so its
|
|
582
|
-
// mere existence isn't evidence of failure (e.g. monograph.db was
|
|
583
|
-
// deleted afterward, leaving a clean run's log behind). Only treat
|
|
584
|
-
// this as a failure when the tail actually looks error-shaped;
|
|
585
|
-
// otherwise this isn't distinguishable from "never built" and
|
|
586
|
-
// should read that way rather than as an unsupported "fail".
|
|
587
|
-
const classified = logTail ? classifyNativeModuleError(logTail) : null;
|
|
588
|
-
const looksLikeFailure = classified !== null || /error|exception|traceback/i.test(logTail);
|
|
589
|
-
if (looksLikeFailure) {
|
|
590
|
-
let logMtimeMs = 0;
|
|
591
|
-
let logAgeStr = '';
|
|
592
|
-
try {
|
|
593
|
-
logMtimeMs = statSync(logPath).mtimeMs;
|
|
594
|
-
logAgeStr = ` (${formatAge(Date.now() - logMtimeMs)} ago)`;
|
|
595
|
-
}
|
|
596
|
-
catch {
|
|
597
|
-
/* ignore */
|
|
598
|
-
}
|
|
599
|
-
if (classified) {
|
|
600
|
-
// build.log is append-only (see the comment above), so this
|
|
601
|
-
// failure might be historical — already fixed by a `npm
|
|
602
|
-
// rebuild` that ran after this log entry, just not yet
|
|
603
|
-
// confirmed by a fresh `monograph build`. When the implicated
|
|
604
|
-
// module's own files are demonstrably newer than the log entry,
|
|
605
|
-
// that's concrete on-disk evidence it was touched since, so
|
|
606
|
-
// soften to a warning instead of asserting it's still broken.
|
|
607
|
-
// No evidence either way (can't resolve the module, or its
|
|
608
|
-
// files are no newer) falls through to the unchanged 'fail'.
|
|
609
|
-
const pkgName = extractNativeModulePackageName(logTail);
|
|
610
|
-
const moduleMs = pkgName ? findNativeModuleLastModifiedMs(pkgName, cwd) : null;
|
|
611
|
-
if (pkgName && moduleMs !== null && moduleMs > logMtimeMs) {
|
|
612
|
-
return {
|
|
613
|
-
name: 'Graph freshness',
|
|
614
|
-
status: 'warn',
|
|
615
|
-
message: `${classified}${logAgeStr} — but \`${pkgName}\` was modified more recently ` +
|
|
616
|
-
`(${formatAge(Date.now() - moduleMs)} ago) than this log entry, so it may ` +
|
|
617
|
-
'already be fixed. Run monograph build to confirm.',
|
|
618
|
-
fix: 'mcp__monomind__monograph_build codeOnly:true',
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
return classified
|
|
623
|
-
? { name: 'Graph freshness', status: 'fail', message: `${classified}${logAgeStr}` }
|
|
624
|
-
: {
|
|
625
|
-
name: 'Graph freshness',
|
|
626
|
-
status: 'fail',
|
|
627
|
-
message: `Monograph build failed${logAgeStr} — see .monomind/graph/build.log for details`,
|
|
628
|
-
fix: 'cat .monomind/graph/build.log',
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
return {
|
|
633
|
-
name: 'Graph freshness',
|
|
634
|
-
status: 'warn',
|
|
635
|
-
message: 'No monograph graph built yet',
|
|
636
|
-
fix: 'mcp__monomind__monograph_build codeOnly:true',
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
// Deliberately NOT folding build.lock's mtime into this — a lock only
|
|
640
|
-
// means "a build started," not "the graph was built at this time," and
|
|
641
|
-
// counting it here would report a stale graph as freshly-built the
|
|
642
|
-
// moment a rebuild kicks off, before it's actually produced anything.
|
|
643
|
-
let buildMs = 0;
|
|
644
|
-
if (hasDb) {
|
|
645
|
-
try {
|
|
646
|
-
buildMs = Math.max(buildMs, statSync(dbPath).mtimeMs);
|
|
647
|
-
}
|
|
648
|
-
catch {
|
|
649
|
-
/* ignore */
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
try {
|
|
653
|
-
buildMs = Math.max(buildMs, statSync(statsPath).mtimeMs);
|
|
654
|
-
}
|
|
655
|
-
catch {
|
|
656
|
-
/* ignore */
|
|
657
|
-
}
|
|
658
|
-
if (buildMs === 0)
|
|
659
|
-
return {
|
|
660
|
-
name: 'Graph freshness',
|
|
661
|
-
status: 'warn',
|
|
662
|
-
message: 'Graph exists but build time unknown',
|
|
663
|
-
};
|
|
664
|
-
const buildIso = new Date(buildMs).toISOString();
|
|
665
|
-
let commitsBehind = 0;
|
|
666
|
-
try {
|
|
667
|
-
const out = execSync(`git rev-list --count --since='${buildIso}' HEAD 2>/dev/null`, {
|
|
668
|
-
encoding: 'utf8',
|
|
669
|
-
timeout: 2000,
|
|
670
|
-
cwd,
|
|
671
|
-
}).trim();
|
|
672
|
-
commitsBehind = parseInt(out, 10) || 0;
|
|
673
|
-
}
|
|
674
|
-
catch {
|
|
675
|
-
/* git unavailable */
|
|
676
|
-
}
|
|
677
|
-
const ageStr = `${formatAge(Date.now() - buildMs)} ago`;
|
|
678
|
-
if (commitsBehind === 0)
|
|
679
|
-
return {
|
|
680
|
-
name: 'Graph freshness',
|
|
681
|
-
status: 'pass',
|
|
682
|
-
message: `FRESH — built ${ageStr}, 0 commits behind`,
|
|
683
|
-
};
|
|
684
|
-
if (commitsBehind <= 5)
|
|
685
|
-
return {
|
|
686
|
-
name: 'Graph freshness',
|
|
687
|
-
status: 'warn',
|
|
688
|
-
message: `${commitsBehind} commit(s) behind — built ${ageStr}`,
|
|
689
|
-
fix: 'mcp__monomind__monograph_build codeOnly:true',
|
|
690
|
-
};
|
|
691
|
-
return {
|
|
692
|
-
name: 'Graph freshness',
|
|
693
|
-
status: 'fail',
|
|
694
|
-
message: `STALE — ${commitsBehind} commits behind (built ${ageStr})`,
|
|
695
|
-
fix: 'mcp__monomind__monograph_build codeOnly:true',
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
catch {
|
|
699
|
-
return { name: 'Graph freshness', status: 'warn', message: 'Could not check graph freshness' };
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
export async function checkMonoesMemory() {
|
|
703
|
-
// See _findInstalledPackageJson() above checkMonograph(): resolve through
|
|
704
|
-
// Node's ESM module resolution instead of guessing a node_modules layout
|
|
705
|
-
// that doesn't match how npm actually hoists dependencies.
|
|
706
|
-
const found = _findInstalledPackageJson('@monoes/memory');
|
|
707
|
-
if (found) {
|
|
708
|
-
return {
|
|
709
|
-
name: 'Vector Memory',
|
|
710
|
-
status: 'pass',
|
|
711
|
-
message: `@monoes/memory v${found.pkg.version || '?'} (HNSW search enabled)`,
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
return {
|
|
715
|
-
name: 'Vector Memory',
|
|
716
|
-
status: 'warn',
|
|
717
|
-
message: '@monoes/memory not installed (vector search disabled)',
|
|
718
|
-
fix: 'npm install @monoes/memory',
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
function _resolveBundledHelper(relativePath) {
|
|
722
|
-
try {
|
|
723
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
724
|
-
let dir = dirname(thisFile);
|
|
725
|
-
for (;;) {
|
|
726
|
-
const candidate = join(dir, 'package.json');
|
|
727
|
-
if (existsSync(candidate) && statSync(candidate).size <= MAX_DOCTOR_PKG_BYTES) {
|
|
728
|
-
try {
|
|
729
|
-
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
730
|
-
if (pkg.name === '@monomind/cli' ||
|
|
731
|
-
pkg.name === 'monomind' ||
|
|
732
|
-
pkg.name === '@monoes/monomindcli') {
|
|
733
|
-
const helperPath = join(dir, relativePath);
|
|
734
|
-
return existsSync(helperPath) ? helperPath : null;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
catch {
|
|
738
|
-
/* keep walking */
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
const parent = dirname(dir);
|
|
742
|
-
if (parent === dir)
|
|
743
|
-
return null;
|
|
744
|
-
dir = parent;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
catch {
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
// Top-level critical helpers, plus every file bundled under handlers/ and utils/
|
|
752
|
-
// (capture-handler.cjs, gates-handler.cjs, route-handler.cjs, session-handler.cjs,
|
|
753
|
-
// etc. — where most actual hook-behavior fixes live). Subdir membership is
|
|
754
|
-
// discovered from the bundled package itself, not hardcoded, so a helper added
|
|
755
|
-
// upstream is picked up automatically instead of silently never syncing.
|
|
756
|
-
function _allTrackedHelperNames() {
|
|
757
|
-
const names = [...DOCTOR_TRACKED_HELPERS];
|
|
758
|
-
for (const sub of ['handlers', 'utils']) {
|
|
759
|
-
const bundledSubDir = _resolveBundledHelper(join('.claude', 'helpers', sub));
|
|
760
|
-
if (!bundledSubDir)
|
|
761
|
-
continue;
|
|
762
|
-
try {
|
|
763
|
-
for (const f of readdirSync(bundledSubDir)) {
|
|
764
|
-
if (f.startsWith('._'))
|
|
765
|
-
continue;
|
|
766
|
-
if (statSync(join(bundledSubDir, f)).isDirectory())
|
|
767
|
-
continue;
|
|
768
|
-
names.push(join(sub, f));
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
catch {
|
|
772
|
-
/* skip */
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return names;
|
|
776
|
-
}
|
|
777
|
-
// Top-level helpers the bundle ships. Their mere ABSENCE is a defect no
|
|
778
|
-
// matter which one it is — handlers/ scripts require() some of them at module
|
|
779
|
-
// load, and a missing one fails the hook closed (issue #225: doctor reported
|
|
780
|
-
// "helpers match bundled version" while audit-log-writer.cjs was absent and
|
|
781
|
-
// every PreToolUse hook was crashing). Existence is checked for all of them;
|
|
782
|
-
// content is still only hash-compared for _allTrackedHelperNames(), because
|
|
783
|
-
// the rest are user-editable scaffolds (memory.cjs, session.cjs) whose local
|
|
784
|
-
// edits are legitimate and must not be reported as staleness.
|
|
785
|
-
function _bundledTopLevelHelperNames() {
|
|
786
|
-
const bundledDir = _resolveBundledHelper(join('.claude', 'helpers'));
|
|
787
|
-
if (!bundledDir)
|
|
788
|
-
return [];
|
|
789
|
-
try {
|
|
790
|
-
return readdirSync(bundledDir, { withFileTypes: true })
|
|
791
|
-
.filter((e) => e.isFile() && !e.name.startsWith('._'))
|
|
792
|
-
.map((e) => e.name);
|
|
793
|
-
}
|
|
794
|
-
catch {
|
|
795
|
-
return [];
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
// The installed helper copies doctor compares against the bundle: the Claude
|
|
799
|
-
// Code tree, plus the copy init writes to .gemini/helpers (Antigravity's status
|
|
800
|
-
// bar runs .gemini/helpers/statusline.cjs), which is only checked when present.
|
|
801
|
-
const CLAUDE_HELPERS = join('.claude', 'helpers');
|
|
802
|
-
const GEMINI_HELPERS = join('.gemini', 'helpers');
|
|
803
|
-
async function _detectStaleHelpers(helpersDir = CLAUDE_HELPERS) {
|
|
804
|
-
const stale = [];
|
|
805
|
-
const missing = [];
|
|
806
|
-
// Helpers this product shipped under a name it has since renamed away from
|
|
807
|
-
// (e.g. graphify-freshen.cjs -> monograph-freshen.cjs). Reported separately
|
|
808
|
-
// from `stale`/`missing`: unlike those, doctor --fix does not delete these
|
|
809
|
-
// on its own — settings.json may still have a hook command pointing at one,
|
|
810
|
-
// and only `init --force` (which also refreshes settings.json in the same
|
|
811
|
-
// pass) can safely remove it. See writeHelpers/writeSettings.
|
|
812
|
-
const orphaned = OBSOLETE_HELPER_NAMES.filter((name) => existsSync(join(process.cwd(), '.claude', 'helpers', name)));
|
|
813
|
-
const crypto = await import('node:crypto');
|
|
814
|
-
const hashChecked = new Set(_allTrackedHelperNames());
|
|
815
|
-
for (const name of new Set([..._allTrackedHelperNames(), ..._bundledTopLevelHelperNames()])) {
|
|
816
|
-
const bundled = _resolveBundledHelper(join('.claude', 'helpers', name));
|
|
817
|
-
// Oversized-bundled is reported the same as missing-bundled: doctor can't
|
|
818
|
-
// verify freshness either way, and silently excluding it from both `stale`
|
|
819
|
-
// and `missing` would let checkHelpersFresh() report "pass" without ever
|
|
820
|
-
// having actually compared this file.
|
|
821
|
-
if (!bundled || statSync(bundled).size > MAX_DOCTOR_HELPER_BYTES) {
|
|
822
|
-
missing.push(name);
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
const local = join(process.cwd(), helpersDir, name);
|
|
826
|
-
if (!existsSync(local)) {
|
|
827
|
-
stale.push(name);
|
|
828
|
-
continue;
|
|
829
|
-
} // bundled has it, project doesn't — needs creating
|
|
830
|
-
if (!hashChecked.has(name))
|
|
831
|
-
continue; // existence-only (see above)
|
|
832
|
-
if (statSync(local).size > MAX_DOCTOR_HELPER_BYTES)
|
|
833
|
-
continue;
|
|
834
|
-
try {
|
|
835
|
-
const hashLocal = crypto.createHash('sha256').update(readFileSync(local)).digest('hex');
|
|
836
|
-
const hashBundled = crypto.createHash('sha256').update(readFileSync(bundled)).digest('hex');
|
|
837
|
-
if (hashLocal !== hashBundled)
|
|
838
|
-
stale.push(name);
|
|
839
|
-
}
|
|
840
|
-
catch {
|
|
841
|
-
/* skip */
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return { stale, missing, orphaned };
|
|
845
|
-
}
|
|
846
|
-
/** Stale helpers in the Gemini copy, as `.gemini/helpers/<name>`; none if not installed. */
|
|
847
|
-
async function _detectStaleGeminiHelpers() {
|
|
848
|
-
if (!existsSync(join(process.cwd(), GEMINI_HELPERS)))
|
|
849
|
-
return [];
|
|
850
|
-
const { stale } = await _detectStaleHelpers(GEMINI_HELPERS);
|
|
851
|
-
return stale.map((name) => `.gemini/helpers/${name.split(sep).join('/')}`);
|
|
852
|
-
}
|
|
853
|
-
export async function fixStaleHelpers() {
|
|
854
|
-
const { stale } = await _detectStaleHelpers();
|
|
855
|
-
const targets = stale.map((name) => ({ name, local: join(process.cwd(), CLAUDE_HELPERS, name) }));
|
|
856
|
-
if (existsSync(join(process.cwd(), GEMINI_HELPERS))) {
|
|
857
|
-
for (const name of (await _detectStaleHelpers(GEMINI_HELPERS)).stale) {
|
|
858
|
-
targets.push({ name, local: join(process.cwd(), GEMINI_HELPERS, name) });
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
let fixed = 0;
|
|
862
|
-
for (const { name, local } of targets) {
|
|
863
|
-
// A helper `monomind init` kept because the user edited it is theirs.
|
|
864
|
-
if (isKeptUserEdit(process.cwd(), local))
|
|
865
|
-
continue;
|
|
866
|
-
const bundled = _resolveBundledHelper(join('.claude', 'helpers', name));
|
|
867
|
-
if (bundled) {
|
|
868
|
-
try {
|
|
869
|
-
mkdirSync(dirname(local), { recursive: true });
|
|
870
|
-
copyFileSync(bundled, local);
|
|
871
|
-
fixed++;
|
|
872
|
-
}
|
|
873
|
-
catch (e) {
|
|
874
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
875
|
-
console.error(`[fixStaleHelpers] failed to copy ${name}:`, e);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return fixed > 0;
|
|
880
|
-
}
|
|
881
|
-
export async function checkHelpersFresh() {
|
|
882
|
-
try {
|
|
883
|
-
const { stale: claudeStale, missing, orphaned } = await _detectStaleHelpers();
|
|
884
|
-
const stale = [...claudeStale, ...(await _detectStaleGeminiHelpers())];
|
|
885
|
-
if (orphaned.length > 0) {
|
|
886
|
-
return {
|
|
887
|
-
name: 'Helper Files',
|
|
888
|
-
status: 'warn',
|
|
889
|
-
message: `Found ${orphaned.length} hook(s) from before a rename, still on disk: ${orphaned.join(', ')}. Run \`monomind init --force\` to update your hooks and rebuild the Monograph graph.`,
|
|
890
|
-
fix: 'monomind init --force',
|
|
891
|
-
// `--fix` only copies stale helpers; it never removes these.
|
|
892
|
-
fixSafety: 'manual',
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
if (stale.length === 0 && missing.length === 0) {
|
|
896
|
-
return {
|
|
897
|
-
name: 'Helper Files',
|
|
898
|
-
status: 'pass',
|
|
899
|
-
message: 'Project helpers match bundled version',
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
if (stale.length > 0) {
|
|
903
|
-
return {
|
|
904
|
-
name: 'Helper Files',
|
|
905
|
-
status: 'warn',
|
|
906
|
-
message: `${stale.length} stale helper(s): ${stale.join(', ')}`,
|
|
907
|
-
fix: 'monomind init upgrade',
|
|
908
|
-
fixSafety: 'auto',
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
return {
|
|
912
|
-
name: 'Helper Files',
|
|
913
|
-
status: 'warn',
|
|
914
|
-
message: `Could not locate bundled copies of: ${missing.join(', ')}`,
|
|
915
|
-
fix: 'Reinstall monomind or run `monomind init upgrade`',
|
|
916
|
-
fixSafety: 'manual',
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
catch (e) {
|
|
920
|
-
return {
|
|
921
|
-
name: 'Helper Files',
|
|
922
|
-
status: 'warn',
|
|
923
|
-
message: `check failed: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
function fmtPct(v) {
|
|
928
|
-
return v === null ? 'n/a' : `${Math.round(v * 100)}%`;
|
|
929
|
-
}
|
|
930
|
-
async function routingAccuracyLine() {
|
|
931
|
-
try {
|
|
932
|
-
const { computeRoutingAccuracy, computeAdherence } = await import('../monovector/route-outcomes.js');
|
|
933
|
-
const baseDir = join(process.cwd(), '.monomind');
|
|
934
|
-
const acc = await computeRoutingAccuracy(baseDir, 100);
|
|
935
|
-
const adh = await computeAdherence(baseDir);
|
|
936
|
-
const adhStr = ` | adherence ${fmtPct(adh.adherence)} (n=${adh.sample})`;
|
|
937
|
-
if (acc.accuracy === null)
|
|
938
|
-
return `routing accuracy (last 100): no outcome data yet${adhStr}`;
|
|
939
|
-
const trend = acc.recentVsPrior === null
|
|
940
|
-
? ''
|
|
941
|
-
: ` trend ${acc.recentVsPrior >= 0 ? '+' : ''}${Math.round(acc.recentVsPrior * 100)}%`;
|
|
942
|
-
return `routing accuracy (last ${acc.window}): ${fmtPct(acc.accuracy)} [native ${fmtPct(acc.byMode.native)} / js ${fmtPct(acc.byMode.js)}]${trend}${adhStr}`;
|
|
943
|
-
}
|
|
944
|
-
catch (e) {
|
|
945
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
946
|
-
console.error('[routingAccuracyLine] compute failed:', e);
|
|
947
|
-
return 'routing accuracy (last 100): no outcome data yet';
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
/** Fallback when route-outcomes.jsonl has no data: summarize .monomind/routing-feedback.jsonl honestly. */
|
|
951
|
-
function routingFeedbackFallback() {
|
|
952
|
-
try {
|
|
953
|
-
const p = join(process.cwd(), '.monomind', 'routing-feedback.jsonl');
|
|
954
|
-
if (!existsSync(p) || statSync(p).size > 512 * 1024)
|
|
955
|
-
return null;
|
|
956
|
-
const records = readFileSync(p, 'utf-8')
|
|
957
|
-
.trim()
|
|
958
|
-
.split('\n')
|
|
959
|
-
.filter(Boolean)
|
|
960
|
-
.map((l) => {
|
|
961
|
-
try {
|
|
962
|
-
return JSON.parse(l);
|
|
963
|
-
}
|
|
964
|
-
catch {
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
967
|
-
})
|
|
968
|
-
.filter((r) => r !== null);
|
|
969
|
-
if (records.length === 0)
|
|
970
|
-
return null;
|
|
971
|
-
const sessions = new Set(records.map((r) => r.sessionId).filter(Boolean));
|
|
972
|
-
const byAgent = new Map();
|
|
973
|
-
for (const r of records) {
|
|
974
|
-
const a = typeof r.suggestedAgent === 'string' ? r.suggestedAgent : 'unknown';
|
|
975
|
-
byAgent.set(a, (byAgent.get(a) ?? 0) + 1);
|
|
976
|
-
}
|
|
977
|
-
const top = [...byAgent.entries()]
|
|
978
|
-
.sort((a, b) => b[1] - a[1])
|
|
979
|
-
.slice(0, 3)
|
|
980
|
-
.map(([a, n]) => `${a}=${n}`)
|
|
981
|
-
.join(', ');
|
|
982
|
-
const flags = records
|
|
983
|
-
.map((r) => r.intelligenceFeedback)
|
|
984
|
-
.filter((v) => typeof v === 'boolean');
|
|
985
|
-
const trueCount = flags.filter(Boolean).length;
|
|
986
|
-
const base = `routing feedback (fallback): ${records.length} decisions across ${sessions.size} sessions; top agents: ${top}`;
|
|
987
|
-
if (flags.length > 0 && trueCount === flags.length) {
|
|
988
|
-
return {
|
|
989
|
-
message: `${base}; success flag is degenerate (100% true — sessionSuccess heuristic never records failure)`,
|
|
990
|
-
degenerate: true,
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
if (flags.length === 0)
|
|
994
|
-
return { message: `${base}; no evidence-based success flags yet`, degenerate: true };
|
|
995
|
-
return {
|
|
996
|
-
message: `${base}; success rate ${Math.round((trueCount / flags.length) * 100)}% (n=${flags.length})`,
|
|
997
|
-
degenerate: false,
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
catch (e) {
|
|
1001
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
1002
|
-
console.error('[routingFeedbackFallback] parse failed:', e);
|
|
1003
|
-
return null;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
export async function checkMonoesIntegration() {
|
|
1007
|
-
try {
|
|
1008
|
-
const line = await routingAccuracyLine();
|
|
1009
|
-
if (line.startsWith('routing accuracy (last 100): no outcome data yet')) {
|
|
1010
|
-
const fb = routingFeedbackFallback();
|
|
1011
|
-
if (fb)
|
|
1012
|
-
return {
|
|
1013
|
-
name: 'Routing Learning',
|
|
1014
|
-
status: fb.degenerate ? 'warn' : 'pass',
|
|
1015
|
-
message: fb.message,
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
return { name: 'Routing Learning', status: 'pass', message: line };
|
|
1019
|
-
}
|
|
1020
|
-
catch (err) {
|
|
1021
|
-
return {
|
|
1022
|
-
name: 'Routing Learning',
|
|
1023
|
-
status: 'warn',
|
|
1024
|
-
message: `Could not compute routing accuracy: ${err instanceof Error ? err.message : String(err)}`,
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
// i-052: derived from MONOMIND_NEVER_COMMIT (write-runtime-config.ts), the
|
|
1029
|
-
// single source of truth for files that must never be committed — this was
|
|
1030
|
-
// the THIRD independently-maintained list that omitted `dashboard-token`,
|
|
1031
|
-
// and it is the one written specifically to catch gitignore gaps. Consuming
|
|
1032
|
-
// the shared list here means `monomind doctor --fix` (fixGitignoreCoverage
|
|
1033
|
-
// below) appends any missing entry for existing users for free.
|
|
1034
|
-
const REQUIRED_GITIGNORE_PATTERNS = [
|
|
1035
|
-
{ pattern: '.monomind/sessions/', reason: 'session files contain cwd and machine paths' },
|
|
1036
|
-
{ pattern: '.monomind/data/', reason: 'intelligence data with edit file paths' },
|
|
1037
|
-
{ pattern: '.monomind/metrics/', reason: 'metrics with file path references' },
|
|
1038
|
-
{ pattern: '.monomind/knowledge/', reason: 'knowledge chunks with local file content' },
|
|
1039
|
-
{ pattern: '.monomind/*.json', reason: 'root-level runtime JSON (control, registry, routing)' },
|
|
1040
|
-
{ pattern: '.monomind/*.jsonl', reason: 'root-level event logs (decisions, routing-feedback)' },
|
|
1041
|
-
{ pattern: '**/.monomind/sessions/', reason: 'nested session files in sub-packages' },
|
|
1042
|
-
{ pattern: '**/.monomind/*.json', reason: 'nested runtime JSON in sub-packages' },
|
|
1043
|
-
{ pattern: 'data/sessions/', reason: 'session files with machine paths' },
|
|
1044
|
-
{ pattern: 'data/mastermind-*.json', reason: 'mastermind session data' },
|
|
1045
|
-
{ pattern: 'data/mastermind-*.jsonl', reason: 'mastermind event logs' },
|
|
1046
|
-
{ pattern: '**/.claude-flow/', reason: 'claude-flow runtime data with paths' },
|
|
1047
|
-
{ pattern: '.monomind/monoswarm/', reason: 'monoswarm state files' },
|
|
1048
|
-
...MONOMIND_NEVER_COMMIT.map(({ file, reason }) => ({ pattern: `.monomind/${file}`, reason })),
|
|
1049
|
-
];
|
|
1050
|
-
/** Strip the parts that don't change what a pattern matches, so `**/.monomind/`,
|
|
1051
|
-
* `.monomind/` and `.monomind` all compare equal. A gitignore pattern with no
|
|
1052
|
-
* leading slash already matches at every depth, so `**/` is redundant. */
|
|
1053
|
-
function normalizeGitignorePattern(p) {
|
|
1054
|
-
return p
|
|
1055
|
-
.replace(/^\/+/, '')
|
|
1056
|
-
.replace(/^\*\*\//, '')
|
|
1057
|
-
.replace(/\/+$/, '');
|
|
1058
|
-
}
|
|
1059
|
-
/** True when ignoring `line` already protects everything `required` covers.
|
|
1060
|
-
* Beyond exact matches this understands directory containment: a repo with a
|
|
1061
|
-
* blanket `.monomind/` is fully covered for `.monomind/sessions/`,
|
|
1062
|
-
* `.monomind/*.json` and friends, and should not be nagged to add each one. */
|
|
1063
|
-
function gitignoreLineCovers(line, required) {
|
|
1064
|
-
const l = normalizeGitignorePattern(line);
|
|
1065
|
-
const r = normalizeGitignorePattern(required);
|
|
1066
|
-
if (!l)
|
|
1067
|
-
return false;
|
|
1068
|
-
if (l === r)
|
|
1069
|
-
return true;
|
|
1070
|
-
// A bare directory entry covers every path beneath it, but only when the
|
|
1071
|
-
// entry itself is literal — `.monomind/*.json` must not be treated as an
|
|
1072
|
-
// ancestor of `.monomind/sessions`.
|
|
1073
|
-
return !l.includes('*') && r.startsWith(`${l}/`);
|
|
1074
|
-
}
|
|
1075
|
-
export async function checkGitignoreCoverage(root = process.cwd()) {
|
|
1076
|
-
const gitignorePath = join(root, '.gitignore');
|
|
1077
|
-
if (!existsSync(gitignorePath)) {
|
|
1078
|
-
// The old hint was `echo ".monomind/\n**/.monomind/" >> .gitignore`, which
|
|
1079
|
-
// writes a literal backslash-n (sh/bash echo does not interpret escapes
|
|
1080
|
-
// without -e) — one junk line matching nothing. `--fix` now writes the file
|
|
1081
|
-
// directly, so point at that rather than at a command to copy-paste.
|
|
1082
|
-
return {
|
|
1083
|
-
name: 'Gitignore Coverage',
|
|
1084
|
-
status: 'warn',
|
|
1085
|
-
message: 'No .gitignore found — all monomind runtime paths are unprotected',
|
|
1086
|
-
fix: 'monomind doctor --fix (creates .gitignore covering .monomind/ and secrets)',
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
if (statSync(gitignorePath).size > MAX_DOCTOR_GITIGNORE_BYTES) {
|
|
1090
|
-
return { name: 'Gitignore Coverage', status: 'warn', message: '.gitignore too large to parse' };
|
|
1091
|
-
}
|
|
1092
|
-
const lines = readFileSync(gitignorePath, 'utf-8')
|
|
1093
|
-
.split('\n')
|
|
1094
|
-
.map((l) => l.trim())
|
|
1095
|
-
.filter((l) => l && !l.startsWith('#'));
|
|
1096
|
-
const missing = REQUIRED_GITIGNORE_PATTERNS.filter(({ pattern }) => !lines.some((l) => gitignoreLineCovers(l, pattern)));
|
|
1097
|
-
if (missing.length === 0)
|
|
1098
|
-
return {
|
|
1099
|
-
name: 'Gitignore Coverage',
|
|
1100
|
-
status: 'pass',
|
|
1101
|
-
message: 'All monomind runtime paths are gitignored',
|
|
1102
|
-
};
|
|
1103
|
-
const missingList = missing.map((m) => m.pattern).join(', ');
|
|
1104
|
-
return {
|
|
1105
|
-
name: 'Gitignore Coverage',
|
|
1106
|
-
status: 'warn',
|
|
1107
|
-
message: `${missing.length} runtime path(s) not in .gitignore: ${missingList}`,
|
|
1108
|
-
fix: 'monomind doctor --fix (appends the missing entries)',
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
/** Patterns written by `doctor --fix` when there is no .gitignore at all.
|
|
1112
|
-
* Covers monomind runtime state plus the standard secrets files, since a repo
|
|
1113
|
-
* with no .gitignore has nothing protecting those either. The runtime paths
|
|
1114
|
-
* are the specific ones the check requires, never a blanket `.monomind/`:
|
|
1115
|
-
* git cannot re-include a file below an ignored directory, so a blanket line
|
|
1116
|
-
* disables init's own `.monomind/.gitignore` allow-list (config.yaml,
|
|
1117
|
-
* CAPABILITIES.md, org definitions). */
|
|
1118
|
-
const GITIGNORE_FIX_PATTERNS = [
|
|
1119
|
-
'# monomind runtime state',
|
|
1120
|
-
...REQUIRED_GITIGNORE_PATTERNS.map(({ pattern }) => pattern),
|
|
1121
|
-
'',
|
|
1122
|
-
'# secrets / local env',
|
|
1123
|
-
'.env',
|
|
1124
|
-
'.env.*',
|
|
1125
|
-
'!.env.example',
|
|
1126
|
-
'*.pem',
|
|
1127
|
-
'*.key',
|
|
1128
|
-
'',
|
|
1129
|
-
'# dependencies / build output',
|
|
1130
|
-
'node_modules/',
|
|
1131
|
-
'dist/',
|
|
1132
|
-
];
|
|
1133
|
-
/** Creates or appends the missing gitignore entries. Returns true if it wrote.
|
|
1134
|
-
* Append-only and idempotent: never rewrites lines the user already has. */
|
|
1135
|
-
export async function fixGitignoreCoverage(root = process.cwd()) {
|
|
1136
|
-
const gitignorePath = join(root, '.gitignore');
|
|
1137
|
-
try {
|
|
1138
|
-
if (!existsSync(gitignorePath)) {
|
|
1139
|
-
writeFileSync(gitignorePath, `${GITIGNORE_FIX_PATTERNS.join('\n')}\n`, 'utf-8');
|
|
1140
|
-
return true;
|
|
1141
|
-
}
|
|
1142
|
-
if (statSync(gitignorePath).size > MAX_DOCTOR_GITIGNORE_BYTES)
|
|
1143
|
-
return false;
|
|
1144
|
-
const existing = readFileSync(gitignorePath, 'utf-8');
|
|
1145
|
-
const lines = existing
|
|
1146
|
-
.split('\n')
|
|
1147
|
-
.map((l) => l.trim())
|
|
1148
|
-
.filter((l) => l && !l.startsWith('#'));
|
|
1149
|
-
const missing = REQUIRED_GITIGNORE_PATTERNS.map((m) => m.pattern).filter((pattern) => !lines.some((l) => gitignoreLineCovers(l, pattern)));
|
|
1150
|
-
if (missing.length === 0)
|
|
1151
|
-
return false;
|
|
1152
|
-
const prefix = existing.endsWith('\n') || existing === '' ? '' : '\n';
|
|
1153
|
-
appendFileSync(gitignorePath, `${prefix}\n# monomind runtime state\n${missing.join('\n')}\n`, 'utf-8');
|
|
1154
|
-
return true;
|
|
1155
|
-
}
|
|
1156
|
-
catch {
|
|
1157
|
-
return false;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
export async function checkAgentRegistry(opts = {}) {
|
|
1161
|
-
try {
|
|
1162
|
-
const { buildUnifiedRegistry, computeAgentRoots } = await import('../agents/registry-builder.js');
|
|
1163
|
-
const { findProjectRoot, registryPath } = await import('../agents/registry-freshness.js');
|
|
1164
|
-
const project = findProjectRoot(process.cwd());
|
|
1165
|
-
const cwd = project ?? process.cwd();
|
|
1166
|
-
const roots = computeAgentRoots(cwd);
|
|
1167
|
-
const write = project !== null && !opts.readOnly;
|
|
1168
|
-
if (write)
|
|
1169
|
-
mkdirSync(join(cwd, '.monomind'), { recursive: true });
|
|
1170
|
-
// Rebuilds fresh in-memory (and, inside a project, refreshes
|
|
1171
|
-
// .monomind/registry.json on disk, never replacing a non-empty registry
|
|
1172
|
-
// with an empty one) rather than reading a file a separate startup task
|
|
1173
|
-
// may also be writing. Outside a project, and in read-only mode (issue
|
|
1174
|
-
// #335), nothing is written.
|
|
1175
|
-
const registry = buildUnifiedRegistry(roots, write ? registryPath(cwd) : undefined, {
|
|
1176
|
-
base: cwd,
|
|
1177
|
-
});
|
|
1178
|
-
const entries = registry.agents;
|
|
1179
|
-
// An extra root (MONOMIND_EXTRA_AGENT_PATHS or a sibling agency-agents dir)
|
|
1180
|
-
// wins slug conflicts over .claude/agents — say so instead of hiding it.
|
|
1181
|
-
const extras = roots.filter((r) => r.origin === 'extra').map((r) => r.dir);
|
|
1182
|
-
// User agents (~/.claude/agents) are indexed too; project agents win.
|
|
1183
|
-
const shadowed = registry.shadowed.length;
|
|
1184
|
-
const extraNote = `; ${registry.counts.user} from ~/.claude/agents` +
|
|
1185
|
-
(shadowed ? ` (${shadowed} shadowed by a project agent)` : '') +
|
|
1186
|
-
(extras.length ? `; extra agent roots (win on conflict): ${extras.join(', ')}` : '');
|
|
1187
|
-
if (entries.length === 0) {
|
|
1188
|
-
return {
|
|
1189
|
-
name: 'Agent Registry',
|
|
1190
|
-
status: 'warn',
|
|
1191
|
-
message: `No agents found under .claude/agents or ~/.claude/agents${extraNote}`,
|
|
1192
|
-
fix: 'monomind init (installs agent definitions)',
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
let missingSlug = 0, missingName = 0, missingDescription = 0, missingWhenToUse = 0;
|
|
1196
|
-
for (const agent of entries) {
|
|
1197
|
-
if (!agent.slug)
|
|
1198
|
-
missingSlug++;
|
|
1199
|
-
if (!agent.name)
|
|
1200
|
-
missingName++;
|
|
1201
|
-
if (!agent.description)
|
|
1202
|
-
missingDescription++;
|
|
1203
|
-
// The pick index leads with when_to_use; older installs lack it.
|
|
1204
|
-
if (!agent.whenToUse)
|
|
1205
|
-
missingWhenToUse++;
|
|
1206
|
-
}
|
|
1207
|
-
const dupes = registry.duplicates;
|
|
1208
|
-
const total = missingSlug + missingName + missingDescription + missingWhenToUse + dupes.length;
|
|
1209
|
-
if (total === 0) {
|
|
1210
|
-
return {
|
|
1211
|
-
name: 'Agent Registry',
|
|
1212
|
-
status: 'pass',
|
|
1213
|
-
message: `${entries.length} agent(s), all metadata complete${extraNote}`,
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
const parts = [
|
|
1217
|
-
missingSlug > 0 ? `${missingSlug} missing slug` : null,
|
|
1218
|
-
missingName > 0 ? `${missingName} missing name` : null,
|
|
1219
|
-
missingDescription > 0 ? `${missingDescription} missing description` : null,
|
|
1220
|
-
missingWhenToUse > 0 ? `${missingWhenToUse} missing when_to_use` : null,
|
|
1221
|
-
dupes.length > 0
|
|
1222
|
-
? `duplicate slug ${dupes.map((d) => `"${d.slug}" (kept ${d.kept}, dropped ${d.dropped.join(', ')})`).join('; ')}`
|
|
1223
|
-
: null,
|
|
1224
|
-
]
|
|
1225
|
-
.filter(Boolean)
|
|
1226
|
-
.join(', ');
|
|
1227
|
-
return {
|
|
1228
|
-
name: 'Agent Registry',
|
|
1229
|
-
status: 'warn',
|
|
1230
|
-
message: `${total} metadata issue(s) across ${entries.length} agent(s): ${parts}${extraNote}`,
|
|
1231
|
-
fix: dupes.length
|
|
1232
|
-
? 'Give each duplicated agent a unique `slug:` in its frontmatter'
|
|
1233
|
-
: missingWhenToUse > 0
|
|
1234
|
-
? 'monomind init upgrade (refreshes unedited bundled agents; add `when_to_use:` to your own agents in .claude/agents/*.md)'
|
|
1235
|
-
: 'Add the missing field(s) to frontmatter in .claude/agents/*.md',
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
catch {
|
|
1239
|
-
return {
|
|
1240
|
-
name: 'Agent Registry',
|
|
1241
|
-
status: 'warn',
|
|
1242
|
-
message: 'Could not build/parse agent registry',
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
export async function checkGuidanceGates() {
|
|
1247
|
-
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
1248
|
-
const gatesHandlerPath = join(process.cwd(), '.claude', 'helpers', 'handlers', 'gates-handler.cjs');
|
|
1249
|
-
if (!existsSync(gatesHandlerPath)) {
|
|
1250
|
-
return {
|
|
1251
|
-
name: 'Guidance Gates',
|
|
1252
|
-
status: 'warn',
|
|
1253
|
-
message: 'gates-handler.cjs not found — enforcement gates not installed',
|
|
1254
|
-
fix: 'monomind init (then monomind guidance setup)',
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
if (!existsSync(settingsPath)) {
|
|
1258
|
-
return {
|
|
1259
|
-
name: 'Guidance Gates',
|
|
1260
|
-
status: 'warn',
|
|
1261
|
-
message: 'gates-handler.cjs present but .claude/settings.json missing',
|
|
1262
|
-
fix: 'monomind guidance setup',
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1265
|
-
try {
|
|
1266
|
-
if (statSync(settingsPath).size > MAX_DOCTOR_CONFIG_BYTES) {
|
|
1267
|
-
return {
|
|
1268
|
-
name: 'Guidance Gates',
|
|
1269
|
-
status: 'warn',
|
|
1270
|
-
message: 'settings.json too large to parse',
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
1274
|
-
const preToolUse = settings?.hooks?.PreToolUse ?? [];
|
|
1275
|
-
// Match on capability, not on an exact matcher string. The matcher is a
|
|
1276
|
-
// regex alternation that legitimately changes as tools are added — it
|
|
1277
|
-
// gained `|NotebookEdit` when that tool turned out to bypass the secret
|
|
1278
|
-
// gate — and an `=== 'Write|Edit|MultiEdit'` test reported the gate as
|
|
1279
|
-
// INACTIVE the moment it did, telling users their secrets gate was off
|
|
1280
|
-
// while it was demonstrably blocking secrets, and sending them to
|
|
1281
|
-
// `guidance setup`, which would then add a duplicate entry.
|
|
1282
|
-
const hasPreWrite = preToolUse.some((e) => /\bWrite\b/.test(e.matcher ?? '') && e.hooks.some((h) => h.command?.includes('pre-write')));
|
|
1283
|
-
const hasPreBash = preToolUse.some((e) => e.matcher === 'Bash' && e.hooks.some((h) => h.command?.includes('pre-bash')));
|
|
1284
|
-
if (!hasPreWrite && !hasPreBash)
|
|
1285
|
-
return {
|
|
1286
|
-
name: 'Guidance Gates',
|
|
1287
|
-
status: 'warn',
|
|
1288
|
-
message: 'gates-handler.cjs present but no gates registered',
|
|
1289
|
-
fix: 'monomind guidance setup',
|
|
1290
|
-
};
|
|
1291
|
-
if (!hasPreWrite)
|
|
1292
|
-
return {
|
|
1293
|
-
name: 'Guidance Gates',
|
|
1294
|
-
status: 'warn',
|
|
1295
|
-
message: 'pre-write hook not registered — secrets gate inactive',
|
|
1296
|
-
fix: 'monomind guidance setup',
|
|
1297
|
-
};
|
|
1298
|
-
if (!hasPreBash)
|
|
1299
|
-
return {
|
|
1300
|
-
name: 'Guidance Gates',
|
|
1301
|
-
status: 'warn',
|
|
1302
|
-
message: 'pre-bash hook not registered — destructive-ops gate inactive',
|
|
1303
|
-
fix: 'monomind guidance setup',
|
|
1304
|
-
};
|
|
1305
|
-
return {
|
|
1306
|
-
name: 'Guidance Gates',
|
|
1307
|
-
status: 'pass',
|
|
1308
|
-
message: 'destructive-ops (pre-bash) + secrets (pre-write) gates active',
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
catch {
|
|
1312
|
-
return {
|
|
1313
|
-
name: 'Guidance Gates',
|
|
1314
|
-
status: 'warn',
|
|
1315
|
-
message: 'Could not parse .claude/settings.json',
|
|
1316
|
-
fix: 'monomind guidance setup --force',
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
// Workers refresh at session start when output is >6h old — allow a grace
|
|
1321
|
-
// window beyond that before flagging staleness.
|
|
1322
|
-
const METRICS_FRESHNESS_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
1323
|
-
const MAX_DOCTOR_METRICS_BYTES = 5 * 1024 * 1024;
|
|
1324
|
-
function readMetricsJSON(name) {
|
|
1325
|
-
try {
|
|
1326
|
-
const p = join(process.cwd(), '.monomind', 'metrics', name);
|
|
1327
|
-
if (!existsSync(p) || statSync(p).size > MAX_DOCTOR_METRICS_BYTES)
|
|
1328
|
-
return null;
|
|
1329
|
-
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
1330
|
-
}
|
|
1331
|
-
catch (e) {
|
|
1332
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
1333
|
-
console.error(`[readMetricsJSON] failed to read ${name}:`, e);
|
|
1334
|
-
return null;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
/**
|
|
1338
|
-
* Worker metrics freshness — reports the age of the @monoes/hooks worker
|
|
1339
|
-
* output files (written at session start with a 6h staleness gate, or on
|
|
1340
|
-
* demand via `monomind hooks worker run <name>`), so missing/stale worker
|
|
1341
|
-
* output is visible without digging through .monomind/metrics.
|
|
1342
|
-
*/
|
|
1343
|
-
export async function checkMetricsFreshness() {
|
|
1344
|
-
const metricsDir = join(process.cwd(), '.monomind', 'metrics');
|
|
1345
|
-
const knownOutputs = [
|
|
1346
|
-
'codebase-map.json',
|
|
1347
|
-
'security-audit.json',
|
|
1348
|
-
'performance.json',
|
|
1349
|
-
'consolidation.json',
|
|
1350
|
-
'ddd-progress.json',
|
|
1351
|
-
];
|
|
1352
|
-
if (!existsSync(metricsDir)) {
|
|
1353
|
-
return {
|
|
1354
|
-
name: 'Worker Metrics',
|
|
1355
|
-
status: 'warn',
|
|
1356
|
-
message: 'No .monomind/metrics — workers have not run yet (they run at session start)',
|
|
1357
|
-
fix: 'monomind hooks worker run map',
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
const now = Date.now();
|
|
1361
|
-
const fresh = [];
|
|
1362
|
-
const stale = [];
|
|
1363
|
-
for (const name of knownOutputs) {
|
|
1364
|
-
const p = join(metricsDir, name);
|
|
1365
|
-
if (!existsSync(p))
|
|
1366
|
-
continue;
|
|
1367
|
-
try {
|
|
1368
|
-
const ageMs = now - statSync(p).mtimeMs;
|
|
1369
|
-
if (ageMs <= METRICS_FRESHNESS_MS)
|
|
1370
|
-
fresh.push(name);
|
|
1371
|
-
else
|
|
1372
|
-
stale.push(name);
|
|
1373
|
-
}
|
|
1374
|
-
catch {
|
|
1375
|
-
/* skip unreadable */
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
if (fresh.length === 0 && stale.length === 0) {
|
|
1379
|
-
return {
|
|
1380
|
-
name: 'Worker Metrics',
|
|
1381
|
-
status: 'warn',
|
|
1382
|
-
message: 'No worker output files found yet (they run at session start)',
|
|
1383
|
-
fix: 'monomind hooks worker run map',
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
if (stale.length === 0) {
|
|
1387
|
-
return {
|
|
1388
|
-
name: 'Worker Metrics',
|
|
1389
|
-
status: 'pass',
|
|
1390
|
-
message: `${fresh.length} metrics file(s) fresh (<12h)`,
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
return {
|
|
1394
|
-
name: 'Worker Metrics',
|
|
1395
|
-
status: 'warn',
|
|
1396
|
-
message: `${stale.length} stale (>12h): ${stale.join(', ')}${fresh.length > 0 ? ` — ${fresh.length} fresh` : ''}`,
|
|
1397
|
-
fix: 'monomind hooks worker run <name> # map, audit, consolidate, ddd',
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
/**
|
|
1401
|
-
* Surfaces critical findings from the security-audit worker output.
|
|
1402
|
-
*/
|
|
1403
|
-
export async function checkSecurityAuditFindings() {
|
|
1404
|
-
const audit = readMetricsJSON('security-audit.json');
|
|
1405
|
-
if (!audit) {
|
|
1406
|
-
return {
|
|
1407
|
-
name: 'Security Audit',
|
|
1408
|
-
status: 'warn',
|
|
1409
|
-
message: 'No security-audit.json yet',
|
|
1410
|
-
fix: 'monomind hooks worker run audit',
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
const riskLevel = (audit.riskLevel || 'low').toLowerCase();
|
|
1414
|
-
const recommendations = audit.recommendations || [];
|
|
1415
|
-
const priorityTargets = audit.priorityScanTargets || [];
|
|
1416
|
-
const criticalCount = recommendations.length +
|
|
1417
|
-
(riskLevel === 'high' || riskLevel === 'critical' ? priorityTargets.length : 0);
|
|
1418
|
-
if (riskLevel === 'critical' || riskLevel === 'high') {
|
|
1419
|
-
return {
|
|
1420
|
-
name: 'Security Audit',
|
|
1421
|
-
status: 'fail',
|
|
1422
|
-
message: `risk=${riskLevel}, ${criticalCount} critical finding(s) — ${priorityTargets.length} priority scan target(s)`,
|
|
1423
|
-
fix: 'Review .monomind/metrics/security-audit.json priorityScanTargets and recommendations',
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
if (recommendations.length > 0) {
|
|
1427
|
-
return {
|
|
1428
|
-
name: 'Security Audit',
|
|
1429
|
-
status: 'warn',
|
|
1430
|
-
message: `risk=${riskLevel}, ${recommendations.length} recommendation(s)`,
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
return { name: 'Security Audit', status: 'pass', message: `risk=${riskLevel}, no open findings` };
|
|
1434
|
-
}
|
|
1435
|
-
/**
|
|
1436
|
-
* AutoMem proficiency check — reports memory learning health
|
|
1437
|
-
*/
|
|
1438
|
-
export async function checkMemoryProficiency() {
|
|
1439
|
-
try {
|
|
1440
|
-
const intelligenceMod = await import('../memory/intelligence.js');
|
|
1441
|
-
const stats = intelligenceMod.getMemoryProficiencyStats();
|
|
1442
|
-
const bankStats = intelligenceMod.getIntelligenceStats();
|
|
1443
|
-
if (stats.totalDecisions === 0) {
|
|
1444
|
-
return {
|
|
1445
|
-
name: 'Memory Proficiency',
|
|
1446
|
-
status: 'pass',
|
|
1447
|
-
message: 'AutoMem active — no decisions recorded yet (builds up over sessions)',
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
const successPct = Math.round(stats.successRate * 100);
|
|
1451
|
-
const hitPct = Math.round(stats.patternHitRate * 100);
|
|
1452
|
-
const msg = `${stats.totalDecisions} decisions, ${successPct}% success, ${stats.proficiencyPatterns} patterns, ${hitPct}% hit rate (bank: ${bankStats.patternsLearned} total)`;
|
|
1453
|
-
if (stats.successRate < 0.5) {
|
|
1454
|
-
return { name: 'Memory Proficiency', status: 'warn', message: msg };
|
|
1455
|
-
}
|
|
1456
|
-
return { name: 'Memory Proficiency', status: 'pass', message: msg };
|
|
1457
|
-
}
|
|
1458
|
-
catch {
|
|
1459
|
-
return {
|
|
1460
|
-
name: 'Memory Proficiency',
|
|
1461
|
-
status: 'warn',
|
|
1462
|
-
message: 'Could not load intelligence module',
|
|
1463
|
-
};
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
190
|
/** AppleDouble sidecars (`._name`) under `.claude/`.
|
|
1467
191
|
*
|
|
1468
192
|
* macOS writes these whenever a file carrying extended attributes is copied to
|
|
@@ -1543,49 +267,4 @@ export function fixAppleDoubleSidecars(cwd) {
|
|
|
1543
267
|
}
|
|
1544
268
|
return removed;
|
|
1545
269
|
}
|
|
1546
|
-
/** Second Brain document ingestion: whether the "documents" capability is
|
|
1547
|
-
* active for this project. Read directly from capabilities.json rather than
|
|
1548
|
-
* re-running the directory scan — doctor should report on what's actually
|
|
1549
|
-
* activated, not re-detect. */
|
|
1550
|
-
function isDocumentsCapabilityActive(cwd) {
|
|
1551
|
-
try {
|
|
1552
|
-
const capsPath = join(cwd, '.monomind', 'capabilities.json');
|
|
1553
|
-
if (!existsSync(capsPath))
|
|
1554
|
-
return false;
|
|
1555
|
-
const parsed = JSON.parse(readFileSync(capsPath, 'utf8'));
|
|
1556
|
-
return Array.isArray(parsed.active) && parsed.active.includes('documents');
|
|
1557
|
-
}
|
|
1558
|
-
catch {
|
|
1559
|
-
return false;
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
/** Per-extractor health for Second Brain document ingestion (MEM-8/MEM-9):
|
|
1563
|
-
* one check per format family (PDF, DOCX, XLSX/XLS/ODS, PPTX/ODT/ODP/EPUB,
|
|
1564
|
-
* legacy DOC/PPT/Pages) instead of a single blanket "documents" check, so
|
|
1565
|
-
* `doctor` can report exactly which formats are functional vs broken.
|
|
1566
|
-
* No-op (empty array) when the documents capability isn't active — nothing
|
|
1567
|
-
* to report if this project doesn't ingest documents. */
|
|
1568
|
-
export async function checkDocumentExtractors() {
|
|
1569
|
-
if (!isDocumentsCapabilityActive(process.cwd()))
|
|
1570
|
-
return [];
|
|
1571
|
-
try {
|
|
1572
|
-
const { documentsCapability } = await import('../capabilities/cap-documents.js');
|
|
1573
|
-
const checks = (await documentsCapability.healthChecks?.()) ?? [];
|
|
1574
|
-
return checks.map((c) => ({
|
|
1575
|
-
name: c.name,
|
|
1576
|
-
status: c.status,
|
|
1577
|
-
message: c.message,
|
|
1578
|
-
fix: c.hint ?? c.fix,
|
|
1579
|
-
}));
|
|
1580
|
-
}
|
|
1581
|
-
catch (err) {
|
|
1582
|
-
return [
|
|
1583
|
-
{
|
|
1584
|
-
name: 'Document Extractors',
|
|
1585
|
-
status: 'warn',
|
|
1586
|
-
message: `Check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1587
|
-
},
|
|
1588
|
-
];
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
270
|
//# sourceMappingURL=doctor-project-checks.js.map
|