mcp-nervous-system 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +10 -16
  2. package/index.js +590 -7
  3. package/package.json +2 -2
  4. package/stdio.js +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  7 mechanically enforced rules that prevent the most common failure modes when LLMs have access to real infrastructure: context loss, silent failures, file damage, goal drift, and overreach.
6
6
 
7
- Built by [Arthur Palyan](https://www.levelsofself.com) at Palyan AI. 14 tools including task complexity classification and user intent parsing. Battle-tested on an 11-member AI family running 28 processes 24/7 on a single VPS. 58+ violations logged, 0 bypassed.
7
+ Built by [Arthur Palyan](https://www.levelsofself.com) at Palyan Family AI System. 12 tools including configuration drift detection and emergency kill switch. Battle-tested on an 11-member AI family running 28 processes 24/7 on a single VPS. 58+ violations logged, 0 bypassed.
8
8
 
9
9
  ## The Problem
10
10
 
@@ -60,17 +60,12 @@ Protocol: MCP 2024-11-05 (Streamable HTTP + SSE)
60
60
  Authentication: None required
61
61
  ```
62
62
 
63
- ## NEW in v1.3.0
63
+ ## NEW in v1.4.0
64
64
 
65
- **classify_task_complexity** (free tier)
66
- Analyzes any task across 6 dimensions (scope, judgment, risk, context depth, ambiguity, verification difficulty) and recommends the optimal model tier. Routes simple tasks to Haiku (~90% savings), standard tasks to Sonnet (~60% savings), and complex reasoning to Opus. Pass optional structured hints for higher accuracy.
65
+ **drift_audit** (free tier)
66
+ Configuration drift detection across 5 scopes: roles, versions, files, processes, and website. Scans source-of-truth files (family-roles.json, package.json, UNTOUCHABLE_FILES.txt) against all downstream references - HTML pages, JSON configs, markdown docs, and running PM2 processes. Change one file, drift_audit tells you everywhere else that needs updating. The closed loop that keeps your entire system consistent.
67
67
 
68
- **parse_user_intent** (free tier)
69
- Decomposes user requests into numbered deliverables with confidence scoring. Below 80% confidence: the model should clarify before executing, not guess. Detects ambiguity, unresolved pronouns, hedging language, and implicit expectations. The 80% rule: understand first, execute second.
70
-
71
- These tools work together. parse_user_intent breaks down what the person wants. classify_task_complexity routes each piece to the right model. The Nervous System enforces the rules while execution happens.
72
-
73
- **Positioning: Auto mode (launching March 12) decides what Claude CAN do. The Nervous System governs HOW it behaves while doing it.**
68
+ **Positioning: Auto mode decides what Claude CAN do. The Nervous System governs HOW it behaves while doing it.**
74
69
 
75
70
  ## The 7 Rules
76
71
 
@@ -84,7 +79,7 @@ These tools work together. parse_user_intent breaks down what the person wants.
84
79
  | 6 | **Ask Before Touching** | Unauthorized changes. Logic changes need human approval. |
85
80
  | 7 | **Hand Off** | Context loss. Written handoffs every 3-4 exchanges. |
86
81
 
87
- ## MCP Tools (14)
82
+ ## MCP Tools (12)
88
83
 
89
84
  | Tool | Description |
90
85
  |------|------------|
@@ -99,9 +94,7 @@ These tools work together. parse_user_intent breaks down what the person wants.
99
94
  | `emergency_kill_switch` | Emergency shutdown of all PM2 processes. Requires kill switch secret. Logs to tamper-evident audit trail |
100
95
  | `verify_audit_chain` | Walks the SHA-256 hash-chained audit log and verifies every entry. Returns chain integrity status |
101
96
  | `dispatch_to_llm` | Spawns a background LLM agent to handle a task. Checks RAM, enforces max 2 concurrent dispatches |
102
- | `classify_task_complexity` | Analyzes task across 6 dimensions, recommends optimal model tier (Haiku/Sonnet/Opus) |
103
- | `parse_user_intent` | Decomposes requests into numbered deliverables with confidence scoring. 80% gate |
104
- | `get_positioning` | Returns competitive positioning and differentiation messaging |
97
+ | `drift_audit` | Configuration drift detection across roles, versions, files, processes, and website. Finds stale values everywhere. |
105
98
 
106
99
  ## Kill Switch
107
100
 
@@ -134,16 +127,17 @@ The Nervous System provides practical compliance tools for the EU AI Act. See th
134
127
 
135
128
  https://api.100levelup.com/family/eu-ai-act.html
136
129
 
137
- ## Resources (4)
130
+ ## Resources (5)
138
131
 
139
132
  - `nervous-system://framework` - The complete framework
140
133
  - `nervous-system://quick-start` - Quick start guide
141
134
  - `nervous-system://rules` - The 7 core rules
142
135
  - `nervous-system://templates` - Templates for handoffs, worklogs, preflight
136
+ - `nervous-system://drift-audit` - Configuration drift detection
143
137
 
144
138
  ## Production Stats
145
139
 
146
- From the live Palyan AI deployment (Feb 28 - Mar 5, 2026):
140
+ From the live Palyan Family AI System deployment (Feb 28 - Mar 5, 2026):
147
141
 
148
142
  - **58+** violations caught
149
143
  - **29** edits blocked by preflight
package/index.js CHANGED
@@ -129,7 +129,7 @@ const MCP_VERSION = '2024-11-05';
129
129
  // Server info
130
130
  const SERVER_INFO = {
131
131
  name: 'nervous-system',
132
- version: '1.1.0'
132
+ version: '1.4.0'
133
133
  };
134
134
 
135
135
  // ============================================================
@@ -138,7 +138,7 @@ const SERVER_INFO = {
138
138
 
139
139
  const FRAMEWORK = {
140
140
  name: 'The Nervous System',
141
- version: '1.1.0',
141
+ version: '1.4.0',
142
142
  author: 'Arthur Palyan',
143
143
  tagline: 'Anthropic built the brain. Arthur built the nervous system that keeps it from hurting itself.',
144
144
  problem: 'LLMs lose context between sessions, loop on problems instead of dispatching, silently fail without progress notes, edit protected files, drift from the real problem, and solve instead of asking.',
@@ -600,6 +600,22 @@ const TOOLS = [
600
600
  },
601
601
  required: ['task']
602
602
  }
603
+ },
604
+ // NEW: Drift Audit
605
+ {
606
+ name: 'drift_audit',
607
+ annotations: { title: 'Configuration Drift Audit', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
608
+ description: 'Scans for configuration drift - finds files, docs, and configs that reference outdated values. Detects when a file is renamed but references are not updated, when roles change but downstream docs still show old values, or when running processes do not match documentation.',
609
+ inputSchema: {
610
+ type: 'object',
611
+ properties: {
612
+ scope: {
613
+ type: 'string',
614
+ enum: ['full', 'roles', 'versions', 'files', 'processes', 'website'],
615
+ description: 'What to audit. full=everything, roles=family role consistency, versions=NS version numbers, files=file reference integrity, processes=PM2 vs docs, website=HTML pages and configs for stale values'
616
+ }
617
+ }
618
+ }
603
619
  }
604
620
  ];
605
621
 
@@ -608,9 +624,566 @@ const RESOURCES = [
608
624
  { uri: 'nervous-system://framework', name: 'The Nervous System Framework', description: 'Complete behavioral enforcement framework for LLM management', mimeType: 'text/plain' },
609
625
  { uri: 'nervous-system://quick-start', name: 'Quick Start Guide', description: 'How to implement the nervous system in your own LLM deployment', mimeType: 'text/plain' },
610
626
  { uri: 'nervous-system://rules', name: 'The 7 Core Rules', description: 'All 7 behavioral rules with explanations and enforcement', mimeType: 'text/plain' },
611
- { uri: 'nervous-system://templates', name: 'Templates', description: 'Ready-to-use templates for handoffs, worklogs, preflight, and untouchable lists', mimeType: 'text/plain' }
627
+ { uri: 'nervous-system://templates', name: 'Templates', description: 'Ready-to-use templates for handoffs, worklogs, preflight, and untouchable lists', mimeType: 'text/plain' },
628
+ { uri: 'nervous-system://drift-audit', name: 'Drift Audit', description: 'Configuration drift detection - checks roles, versions, file references, and running processes against source-of-truth files', mimeType: 'text/plain' }
612
629
  ];
613
630
 
631
+ // ============================================================
632
+ // DRIFT AUDIT ENGINE
633
+ // ============================================================
634
+
635
+ const { execSync } = require('child_process');
636
+
637
+ function safeReadJSON(filePath) {
638
+ try {
639
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
640
+ } catch (e) {
641
+ return null;
642
+ }
643
+ }
644
+
645
+ function safeReadFile(filePath) {
646
+ try {
647
+ return fs.readFileSync(filePath, 'utf8');
648
+ } catch (e) {
649
+ return null;
650
+ }
651
+ }
652
+
653
+ function auditRoles() {
654
+ const drifts = [];
655
+ let cleanChecks = 0;
656
+ const rolesFile = '/root/family-data/family-roles.json';
657
+ const roles = safeReadJSON(rolesFile);
658
+ if (!roles || !roles.members) {
659
+ drifts.push({ type: 'missing_source', source: rolesFile, target: '', field: '', expected: 'valid JSON with members array', found: 'missing or invalid' });
660
+ return { drifts, cleanChecks };
661
+ }
662
+
663
+ const sourceRoles = {};
664
+ for (const m of roles.members) {
665
+ sourceRoles[m.id] = { name: m.name, aka: m.aka, role: m.role };
666
+ }
667
+
668
+ // Check family-status.json
669
+ const statusFile = '/root/family-data/family-status.json';
670
+ const status = safeReadJSON(statusFile);
671
+ if (status && status.members) {
672
+ for (const m of status.members) {
673
+ const src = sourceRoles[m.id];
674
+ if (!src) continue;
675
+ if (m.aka && m.aka !== src.aka) {
676
+ drifts.push({ type: 'role_mismatch', source: 'family-roles.json', target: 'family-status.json', field: `${m.id}.aka`, expected: src.aka, found: m.aka });
677
+ } else { cleanChecks++; }
678
+ if (m.role && m.role !== src.role) {
679
+ drifts.push({ type: 'role_mismatch', source: 'family-roles.json', target: 'family-status.json', field: `${m.id}.role`, expected: src.role, found: m.role });
680
+ } else { cleanChecks++; }
681
+ }
682
+ }
683
+
684
+ // Check system-config.json
685
+ const configFile = '/root/family-data/system-config.json';
686
+ const config = safeReadJSON(configFile);
687
+ if (config && config.family_members) {
688
+ for (const m of config.family_members) {
689
+ const src = sourceRoles[m.id];
690
+ if (!src) continue;
691
+ if (m.aka && m.aka !== src.aka) {
692
+ drifts.push({ type: 'role_mismatch', source: 'family-roles.json', target: 'system-config.json', field: `${m.id}.aka`, expected: src.aka, found: m.aka });
693
+ } else { cleanChecks++; }
694
+ if (m.role && m.role !== src.role) {
695
+ drifts.push({ type: 'role_mismatch', source: 'family-roles.json', target: 'system-config.json', field: `${m.id}.role`, expected: src.role, found: m.role });
696
+ } else { cleanChecks++; }
697
+ }
698
+ }
699
+
700
+ // Check family-guide.json
701
+ const guideFile = '/root/family-data/family-guide.json';
702
+ const guide = safeReadJSON(guideFile);
703
+ if (guide && guide.members) {
704
+ for (const m of guide.members) {
705
+ const src = sourceRoles[m.id];
706
+ if (!src) continue;
707
+ if (m.aka && m.aka !== src.aka) {
708
+ drifts.push({ type: 'role_mismatch', source: 'family-roles.json', target: 'family-guide.json', field: `${m.id}.aka`, expected: src.aka, found: m.aka });
709
+ } else { cleanChecks++; }
710
+ if (m.role && m.role !== src.role) {
711
+ drifts.push({ type: 'role_mismatch', source: 'family-roles.json', target: 'family-guide.json', field: `${m.id}.role`, expected: src.role, found: m.role });
712
+ } else { cleanChecks++; }
713
+ }
714
+ }
715
+
716
+ // Check HTML files for role references
717
+ const htmlFiles = [
718
+ { path: '/root/family-home/index.html', name: 'index.html' },
719
+ { path: '/root/family-home/explorer.html', name: 'explorer.html' },
720
+ { path: '/root/family-home/meet.html', name: 'meet.html' }
721
+ ];
722
+ for (const hf of htmlFiles) {
723
+ const content = safeReadFile(hf.path);
724
+ if (!content) continue;
725
+ for (const [id, src] of Object.entries(sourceRoles)) {
726
+ if (content.includes(src.name)) { cleanChecks++; }
727
+ }
728
+ }
729
+
730
+ // Check mcp-ops-server.js
731
+ const opsContent = safeReadFile('/root/mcp-ops-server.js');
732
+ if (opsContent) {
733
+ for (const [id, src] of Object.entries(sourceRoles)) {
734
+ if (opsContent.includes(`"${src.aka}"`) || opsContent.includes(`'${src.aka}'`)) {
735
+ cleanChecks++;
736
+ }
737
+ }
738
+ }
739
+
740
+ return { drifts, cleanChecks };
741
+ }
742
+
743
+ function auditVersions() {
744
+ const drifts = [];
745
+ let cleanChecks = 0;
746
+ const pkgFile = '/root/github-repos/mcp-nervous-system/package.json';
747
+ const pkg = safeReadJSON(pkgFile);
748
+ const expectedVersion = pkg ? pkg.version : null;
749
+ if (!expectedVersion) {
750
+ drifts.push({ type: 'missing_source', source: pkgFile, target: '', field: 'version', expected: 'valid version', found: 'missing' });
751
+ return { drifts, cleanChecks };
752
+ }
753
+
754
+ // Check SERVER_INFO.version and FRAMEWORK.version in index.js
755
+ const indexContent = safeReadFile('/root/github-repos/mcp-nervous-system/index.js');
756
+ if (indexContent) {
757
+ const siMatch = indexContent.match(/SERVER_INFO\s*=\s*\{[^}]*version:\s*'([^']+)'/);
758
+ if (siMatch) {
759
+ if (siMatch[1] !== expectedVersion) {
760
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'index.js SERVER_INFO', field: 'version', expected: expectedVersion, found: siMatch[1] });
761
+ } else { cleanChecks++; }
762
+ }
763
+ const fwMatch = indexContent.match(/FRAMEWORK\s*=\s*\{[^}]*version:\s*'([^']+)'/);
764
+ if (fwMatch) {
765
+ if (fwMatch[1] !== expectedVersion) {
766
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'index.js FRAMEWORK', field: 'version', expected: expectedVersion, found: fwMatch[1] });
767
+ } else { cleanChecks++; }
768
+ }
769
+ // Check health endpoint version
770
+ const healthMatch = indexContent.match(/version:\s*'([^']+)'.*?service:\s*'nervous-system/);
771
+ if (!healthMatch) {
772
+ const healthMatch2 = indexContent.match(/service:\s*'nervous-system-mcp',\s*version:\s*'([^']+)'/);
773
+ if (healthMatch2) {
774
+ if (healthMatch2[1] !== expectedVersion) {
775
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'index.js health endpoint', field: 'version', expected: expectedVersion, found: healthMatch2[1] });
776
+ } else { cleanChecks++; }
777
+ }
778
+ }
779
+ // Check startup log version
780
+ const startupMatch = indexContent.match(/Nervous System v([0-9.]+) running/);
781
+ if (startupMatch) {
782
+ if (startupMatch[1] !== expectedVersion) {
783
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'index.js startup log', field: 'version', expected: expectedVersion, found: startupMatch[1] });
784
+ } else { cleanChecks++; }
785
+ }
786
+ // Check root endpoint version
787
+ const rootMatch = indexContent.match(/name:\s*'The Nervous System MCP Server'[\s\S]*?version:\s*'([^']+)'/);
788
+ if (rootMatch) {
789
+ if (rootMatch[1] !== expectedVersion) {
790
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'index.js root endpoint', field: 'version', expected: expectedVersion, found: rootMatch[1] });
791
+ } else { cleanChecks++; }
792
+ }
793
+ }
794
+
795
+ // Check BUSINESS_BUILDER.md
796
+ const bbContent = safeReadFile('/root/family-data/BUSINESS_BUILDER.md');
797
+ if (bbContent) {
798
+ const bbMatch = bbContent.match(/[Nn]ervous [Ss]ystem.*?v?(\d+\.\d+\.\d+)/);
799
+ if (bbMatch && bbMatch[1] !== expectedVersion) {
800
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'BUSINESS_BUILDER.md', field: 'ns_version', expected: expectedVersion, found: bbMatch[1] });
801
+ } else if (bbMatch) { cleanChecks++; }
802
+ }
803
+
804
+ // Check gateway.html
805
+ const gwContent = safeReadFile('/root/family-home/gateway.html');
806
+ if (gwContent) {
807
+ const gwMatch = gwContent.match(/[Vv]ersion[:\s]*v?(\d+\.\d+\.\d+)/);
808
+ if (gwMatch && gwMatch[1] !== expectedVersion) {
809
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'gateway.html', field: 'version', expected: expectedVersion, found: gwMatch[1] });
810
+ } else if (gwMatch) { cleanChecks++; }
811
+ }
812
+
813
+ // Check README.md
814
+ const readmeContent = safeReadFile('/root/github-repos/mcp-nervous-system/README.md');
815
+ if (readmeContent) {
816
+ const rmMatch = readmeContent.match(/[Vv]ersion[:\s]*v?(\d+\.\d+\.\d+)/);
817
+ if (rmMatch && rmMatch[1] !== expectedVersion) {
818
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'README.md', field: 'version', expected: expectedVersion, found: rmMatch[1] });
819
+ } else if (rmMatch) { cleanChecks++; }
820
+ }
821
+
822
+ // Check family-roles.json stats
823
+ const roles = safeReadJSON('/root/family-data/family-roles.json');
824
+ if (roles && roles.stats) {
825
+ if (roles.stats.ns_version && roles.stats.ns_version !== expectedVersion) {
826
+ drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'family-roles.json', field: 'stats.ns_version', expected: expectedVersion, found: roles.stats.ns_version });
827
+ } else if (roles.stats.ns_version) { cleanChecks++; }
828
+
829
+ // Check tool count
830
+ const actualToolCount = TOOLS.length;
831
+ if (roles.stats.ns_tools && roles.stats.ns_tools !== actualToolCount) {
832
+ drifts.push({ type: 'tool_count_mismatch', source: 'TOOLS array', target: 'family-roles.json', field: 'stats.ns_tools', expected: String(actualToolCount), found: String(roles.stats.ns_tools) });
833
+ } else if (roles.stats.ns_tools) { cleanChecks++; }
834
+ }
835
+
836
+ return { drifts, cleanChecks };
837
+ }
838
+
839
+ function auditFiles() {
840
+ const drifts = [];
841
+ let cleanChecks = 0;
842
+
843
+ // Check UNTOUCHABLE_FILES.txt - verify each file exists
844
+ const untouchableContent = safeReadFile('/root/family-data/UNTOUCHABLE_FILES.txt');
845
+ if (untouchableContent) {
846
+ const lines = untouchableContent.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
847
+ for (const filePath of lines) {
848
+ if (fs.existsSync(filePath)) {
849
+ cleanChecks++;
850
+ } else {
851
+ drifts.push({ type: 'missing_file', source: 'UNTOUCHABLE_FILES.txt', target: filePath, field: 'exists', expected: 'true', found: 'false' });
852
+ }
853
+ }
854
+ }
855
+
856
+ // Check LLM_STARTUP.md and BUSINESS_BUILDER.md for file references
857
+ const docsToCheck = [
858
+ { path: '/root/family-data/LLM_STARTUP.md', name: 'LLM_STARTUP.md' },
859
+ { path: '/root/family-data/BUSINESS_BUILDER.md', name: 'BUSINESS_BUILDER.md' }
860
+ ];
861
+
862
+ // Get PM2 running scripts
863
+ let pm2Scripts = {};
864
+ try {
865
+ const pm2Output = execSync('pm2 jlist', { timeout: 10000 }).toString();
866
+ const pm2List = JSON.parse(pm2Output);
867
+ for (const proc of pm2List) {
868
+ pm2Scripts[proc.name] = proc.pm2_env ? proc.pm2_env.pm_exec_path : (proc.script || '');
869
+ }
870
+ } catch (e) {}
871
+
872
+ for (const doc of docsToCheck) {
873
+ const content = safeReadFile(doc.path);
874
+ if (!content) continue;
875
+ // Look for .js file references
876
+ const jsRefs = content.match(/\/root\/[^\s)]+\.js/g) || [];
877
+ for (const ref of jsRefs) {
878
+ if (fs.existsSync(ref)) {
879
+ cleanChecks++;
880
+ // Check if PM2 is running something different
881
+ const basename = ref.split('/').pop();
882
+ for (const [procName, scriptPath] of Object.entries(pm2Scripts)) {
883
+ const procBasename = scriptPath.split('/').pop();
884
+ // If the doc references a versioned file like tamara-v5.js but PM2 runs tamara-v6.js
885
+ const refBase = basename.replace(/-v\d+/, '');
886
+ const procBase = procBasename.replace(/-v\d+/, '');
887
+ if (refBase === procBase && basename !== procBasename && ref !== scriptPath) {
888
+ drifts.push({ type: 'file_version_mismatch', source: doc.name, target: `PM2 process ${procName}`, field: refBase, expected: basename, found: procBasename });
889
+ }
890
+ }
891
+ } else {
892
+ drifts.push({ type: 'missing_file_ref', source: doc.name, target: ref, field: 'exists', expected: 'true', found: 'false' });
893
+ }
894
+ }
895
+ }
896
+
897
+ // Check system-config.json syntax_check_scripts
898
+ const config = safeReadJSON('/root/family-data/system-config.json');
899
+ if (config && config.syntax_check_scripts) {
900
+ for (const script of config.syntax_check_scripts) {
901
+ if (fs.existsSync(script)) {
902
+ cleanChecks++;
903
+ } else {
904
+ drifts.push({ type: 'missing_file', source: 'system-config.json syntax_check_scripts', target: script, field: 'exists', expected: 'true', found: 'false' });
905
+ }
906
+ }
907
+ }
908
+
909
+ return { drifts, cleanChecks };
910
+ }
911
+
912
+ function auditProcesses() {
913
+ const drifts = [];
914
+ let cleanChecks = 0;
915
+
916
+ let pm2Procs = [];
917
+ try {
918
+ const pm2Output = execSync('pm2 jlist', { timeout: 10000 }).toString();
919
+ pm2Procs = JSON.parse(pm2Output);
920
+ } catch (e) {
921
+ drifts.push({ type: 'pm2_error', source: 'pm2 jlist', target: '', field: '', expected: 'valid pm2 output', found: e.message });
922
+ return { drifts, cleanChecks };
923
+ }
924
+
925
+ const config = safeReadJSON('/root/family-data/system-config.json');
926
+ if (!config || !config.processes) {
927
+ // Try to compare against family-roles.json procs
928
+ const roles = safeReadJSON('/root/family-data/family-roles.json');
929
+ if (roles && roles.members) {
930
+ const expectedProcs = [];
931
+ for (const m of roles.members) {
932
+ if (m.procs) expectedProcs.push(...m.procs);
933
+ }
934
+ const runningNames = pm2Procs.map(p => p.name);
935
+ for (const ep of expectedProcs) {
936
+ if (runningNames.includes(ep)) {
937
+ cleanChecks++;
938
+ } else {
939
+ drifts.push({ type: 'missing_process', source: 'family-roles.json', target: 'pm2', field: ep, expected: 'running', found: 'not found in pm2' });
940
+ }
941
+ }
942
+ }
943
+ return { drifts, cleanChecks };
944
+ }
945
+
946
+ // Compare config.processes against pm2
947
+ const runningNames = pm2Procs.map(p => p.name);
948
+ if (Array.isArray(config.processes)) {
949
+ for (const ep of config.processes) {
950
+ const procName = typeof ep === 'string' ? ep : ep.name;
951
+ if (runningNames.includes(procName)) {
952
+ cleanChecks++;
953
+ } else {
954
+ drifts.push({ type: 'missing_process', source: 'system-config.json', target: 'pm2', field: procName, expected: 'running', found: 'not found in pm2' });
955
+ }
956
+ }
957
+ }
958
+
959
+ // Check script paths match
960
+ for (const proc of pm2Procs) {
961
+ const scriptPath = proc.pm2_env ? proc.pm2_env.pm_exec_path : '';
962
+ if (scriptPath && !fs.existsSync(scriptPath)) {
963
+ drifts.push({ type: 'broken_script_path', source: `pm2 process ${proc.name}`, target: scriptPath, field: 'exists', expected: 'true', found: 'false' });
964
+ } else if (scriptPath) {
965
+ cleanChecks++;
966
+ }
967
+ }
968
+
969
+ return { drifts, cleanChecks };
970
+ }
971
+
972
+ function auditWebsite() {
973
+ const drifts = [];
974
+ let cleanChecks = 0;
975
+
976
+ // Source of truth values
977
+ const pkgFile = '/root/github-repos/mcp-nervous-system/package.json';
978
+ const pkg = safeReadJSON(pkgFile);
979
+ const expectedVersion = pkg ? pkg.version : '1.4.0';
980
+ const actualToolCount = TOOLS.length;
981
+ const actualResourceCount = RESOURCES.length;
982
+
983
+ const roles = safeReadJSON('/root/family-data/family-roles.json');
984
+ const expectedMemberCount = roles && roles.stats ? roles.stats.member_count : 11;
985
+ const expectedProcessCount = roles && roles.stats ? roles.stats.process_count : 28;
986
+
987
+ // Count protected files (non-comment, non-blank lines starting with /)
988
+ const untouchableContent = safeReadFile('/root/family-data/UNTOUCHABLE_FILES.txt');
989
+ let protectedFileCount = 0;
990
+ if (untouchableContent) {
991
+ protectedFileCount = untouchableContent.split('\n').filter(l => l.trim() && !l.trim().startsWith('#') && l.trim().startsWith('/')).length;
992
+ }
993
+
994
+ // Get role names from family-roles.json
995
+ const roleNames = roles && roles.members ? roles.members.map(m => m.name) : [];
996
+
997
+ // 1. Check all .html files in /root/family-home/
998
+ const familyHomeDir = '/root/family-home/';
999
+ let htmlFiles = [];
1000
+ try {
1001
+ htmlFiles = fs.readdirSync(familyHomeDir).filter(f => f.endsWith('.html')).map(f => familyHomeDir + f);
1002
+ } catch (e) {}
1003
+
1004
+ for (const htmlFile of htmlFiles) {
1005
+ const content = safeReadFile(htmlFile);
1006
+ if (!content) continue;
1007
+ const fname = htmlFile.split('/').pop();
1008
+
1009
+ // Check for old version references (not matching expected)
1010
+ const versionMatches = content.match(/v(\d+\.\d+\.\d+)/g) || [];
1011
+ for (const vm of versionMatches) {
1012
+ const ver = vm.substring(1);
1013
+ if (ver !== expectedVersion && /^1\.\d+\.\d+$/.test(ver)) {
1014
+ drifts.push({ type: 'stale_version', source: 'package.json', target: fname, field: 'version', expected: expectedVersion, found: ver });
1015
+ }
1016
+ }
1017
+
1018
+ // Check for stale tool count references
1019
+ const toolCountMatches = content.match(/(\d+)\s*(?:MCP\s+)?tools/gi) || [];
1020
+ for (const tcm of toolCountMatches) {
1021
+ const num = parseInt(tcm);
1022
+ if (num > 0 && num !== actualToolCount && num < 50) {
1023
+ drifts.push({ type: 'stale_tool_count', source: 'TOOLS array', target: fname, field: 'tool_count', expected: String(actualToolCount), found: String(num) });
1024
+ }
1025
+ }
1026
+
1027
+ // Check for stale agent/member count
1028
+ const agentMatches = content.match(/(\d+)\s*(?:AI\s+)?(?:family\s+)?(?:members|agents)/gi) || [];
1029
+ for (const am of agentMatches) {
1030
+ const num = parseInt(am);
1031
+ if (num > 0 && num !== expectedMemberCount && num < 50) {
1032
+ drifts.push({ type: 'stale_agent_count', source: 'family-roles.json', target: fname, field: 'member_count', expected: String(expectedMemberCount), found: String(num) });
1033
+ }
1034
+ }
1035
+
1036
+ // Check for stale protected file count
1037
+ const protMatches = content.match(/(\d+)\s*protected\s*files/gi) || [];
1038
+ for (const pm of protMatches) {
1039
+ const num = parseInt(pm);
1040
+ if (num > 0 && num !== protectedFileCount) {
1041
+ drifts.push({ type: 'stale_protected_count', source: 'UNTOUCHABLE_FILES.txt', target: fname, field: 'protected_files', expected: String(protectedFileCount), found: String(num) });
1042
+ }
1043
+ }
1044
+
1045
+ // Check for stale process count
1046
+ const procMatches = content.match(/(\d+)\s*(?:live\s+)?processes/gi) || [];
1047
+ for (const pcm of procMatches) {
1048
+ const num = parseInt(pcm);
1049
+ if (num > 0 && num !== expectedProcessCount && num < 100) {
1050
+ drifts.push({ type: 'stale_process_count', source: 'family-roles.json', target: fname, field: 'process_count', expected: String(expectedProcessCount), found: String(num) });
1051
+ }
1052
+ }
1053
+
1054
+ // If no drifts found for this file, count as clean
1055
+ if (!drifts.some(d => d.target === fname)) {
1056
+ cleanChecks++;
1057
+ }
1058
+ }
1059
+
1060
+ // 2. Check family-guide.json
1061
+ const guide = safeReadJSON('/root/family-data/family-guide.json');
1062
+ if (guide) {
1063
+ const guideStr = JSON.stringify(guide);
1064
+ // Check version refs
1065
+ const guideVersions = guideStr.match(/v(\d+\.\d+\.\d+)/g) || [];
1066
+ for (const gv of guideVersions) {
1067
+ const ver = gv.substring(1);
1068
+ if (ver !== expectedVersion && /^1\.\d+\.\d+$/.test(ver)) {
1069
+ drifts.push({ type: 'stale_version', source: 'package.json', target: 'family-guide.json', field: 'version', expected: expectedVersion, found: ver });
1070
+ }
1071
+ }
1072
+ // Check tool count refs
1073
+ const guideToolMatches = guideStr.match(/(\d+)\s*(?:MCP\s+)?tools/gi) || [];
1074
+ for (const gtm of guideToolMatches) {
1075
+ const num = parseInt(gtm);
1076
+ if (num > 0 && num !== actualToolCount && num < 50) {
1077
+ drifts.push({ type: 'stale_tool_count', source: 'TOOLS array', target: 'family-guide.json', field: 'tool_count', expected: String(actualToolCount), found: String(num) });
1078
+ }
1079
+ }
1080
+ // Check for references to removed tools
1081
+ if (guideStr.includes('classify_task_complexity') || guideStr.includes('parse_user_intent')) {
1082
+ drifts.push({ type: 'stale_tool_reference', source: 'TOOLS array', target: 'family-guide.json', field: 'removed_tools', expected: 'drift_audit', found: 'classify_task_complexity/parse_user_intent' });
1083
+ }
1084
+ } else { cleanChecks++; }
1085
+
1086
+ // 3. Check mcp-stripe-checkout.js for version refs
1087
+ const checkoutContent = safeReadFile('/root/mcp-stripe-checkout.js');
1088
+ if (checkoutContent) {
1089
+ const checkoutVersions = checkoutContent.match(/v(\d+\.\d+\.\d+)/g) || [];
1090
+ for (const cv of checkoutVersions) {
1091
+ const ver = cv.substring(1);
1092
+ if (ver !== expectedVersion && /^1\.\d+\.\d+$/.test(ver)) {
1093
+ drifts.push({ type: 'stale_version', source: 'package.json', target: 'mcp-stripe-checkout.js', field: 'version', expected: expectedVersion, found: ver });
1094
+ }
1095
+ }
1096
+ if (!checkoutVersions.length) cleanChecks++;
1097
+ }
1098
+
1099
+ // 4. Check system-config.json for version/tool counts
1100
+ const sysConfig = safeReadJSON('/root/family-data/system-config.json');
1101
+ if (sysConfig) {
1102
+ const scStr = JSON.stringify(sysConfig);
1103
+ const scVersions = scStr.match(/v(\d+\.\d+\.\d+)/g) || [];
1104
+ for (const sv of scVersions) {
1105
+ const ver = sv.substring(1);
1106
+ if (ver !== expectedVersion && /^1\.\d+\.\d+$/.test(ver)) {
1107
+ drifts.push({ type: 'stale_version', source: 'package.json', target: 'system-config.json', field: 'version', expected: expectedVersion, found: ver });
1108
+ }
1109
+ }
1110
+ if (!scVersions.length) cleanChecks++;
1111
+ }
1112
+
1113
+ // 5. Check FREE_TOOLS in mcp-api-middleware.js match actual tool names
1114
+ const middlewareContent = safeReadFile('/root/mcp-api-middleware.js');
1115
+ if (middlewareContent) {
1116
+ const freeToolsMatch = middlewareContent.match(/'nervous-system':\s*\[([^\]]+)\]/);
1117
+ if (freeToolsMatch) {
1118
+ const freeToolNames = freeToolsMatch[1].match(/'([^']+)'/g);
1119
+ if (freeToolNames) {
1120
+ const actualToolNames = TOOLS.map(t => t.name);
1121
+ for (const ft of freeToolNames) {
1122
+ const toolName = ft.replace(/'/g, '');
1123
+ if (actualToolNames.includes(toolName)) {
1124
+ cleanChecks++;
1125
+ } else {
1126
+ drifts.push({ type: 'invalid_free_tool', source: 'TOOLS array', target: 'mcp-api-middleware.js', field: 'FREE_TOOLS', expected: 'valid tool name', found: toolName });
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ // 6. Check sitemap.xml has all public pages
1134
+ const sitemapContent = safeReadFile('/root/family-home/sitemap.xml');
1135
+ if (sitemapContent && htmlFiles.length > 0) {
1136
+ const publicPages = htmlFiles.filter(f => {
1137
+ const name = f.split('/').pop();
1138
+ return !['404.html', 'arthur.html', 'aram-consent.html', 'explorer.html', 'checklist.html'].includes(name);
1139
+ });
1140
+ for (const page of publicPages) {
1141
+ const pageName = page.split('/').pop();
1142
+ if (pageName === 'index.html') {
1143
+ if (sitemapContent.includes('/family/')) cleanChecks++;
1144
+ } else {
1145
+ if (sitemapContent.includes(pageName)) {
1146
+ cleanChecks++;
1147
+ } else {
1148
+ drifts.push({ type: 'missing_from_sitemap', source: 'sitemap.xml', target: pageName, field: 'listed', expected: 'true', found: 'false' });
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ return { drifts, cleanChecks };
1155
+ }
1156
+
1157
+ function runDriftAudit(scope) {
1158
+ const timestamp = new Date().toISOString();
1159
+ const allDrifts = [];
1160
+ let totalClean = 0;
1161
+ const scopes = scope === 'full' ? ['roles', 'versions', 'files', 'processes', 'website'] : [scope];
1162
+
1163
+ for (const s of scopes) {
1164
+ let result;
1165
+ switch (s) {
1166
+ case 'roles': result = auditRoles(); break;
1167
+ case 'versions': result = auditVersions(); break;
1168
+ case 'files': result = auditFiles(); break;
1169
+ case 'processes': result = auditProcesses(); break;
1170
+ case 'website': result = auditWebsite(); break;
1171
+ default: result = { drifts: [{ type: 'unknown_scope', source: '', target: '', field: s, expected: 'valid scope', found: 'unknown' }], cleanChecks: 0 };
1172
+ }
1173
+ allDrifts.push(...result.drifts);
1174
+ totalClean += result.cleanChecks;
1175
+ }
1176
+
1177
+ return {
1178
+ scope,
1179
+ timestamp,
1180
+ status: allDrifts.length === 0 ? 'clean' : 'drift_detected',
1181
+ drift_count: allDrifts.length,
1182
+ drifts: allDrifts,
1183
+ clean_checks: totalClean
1184
+ };
1185
+ }
1186
+
614
1187
  // ============================================================
615
1188
  // Handle tool calls
616
1189
  // ============================================================
@@ -708,6 +1281,11 @@ function handleToolCall(name, args) {
708
1281
  return dispatchToLLM(args.task, args.max_turns);
709
1282
  }
710
1283
 
1284
+ case 'drift_audit': {
1285
+ const scope = args.scope || 'full';
1286
+ return runDriftAudit(scope);
1287
+ }
1288
+
711
1289
  default:
712
1290
  return { error: 'Unknown tool' };
713
1291
  }
@@ -746,6 +1324,11 @@ ${FRAMEWORK.before_any_change.map(s => `- ${s}`).join('\n')}`;
746
1324
  case 'nervous-system://templates':
747
1325
  return `## SESSION HANDOFF TEMPLATE\n${SESSION_HANDOFF_TEMPLATE.template}\n\n---\n\n## WORKLOG FORMAT\n${WORKLOG_TEMPLATE.format}\n\n---\n\n## PREFLIGHT SCRIPT\n${PREFLIGHT_PATTERN.script_template}\n\n---\n\n## UNTOUCHABLE FILES TEMPLATE\n${PREFLIGHT_PATTERN.untouchable_template}`;
748
1326
 
1327
+ case 'nervous-system://drift-audit': {
1328
+ const result = runDriftAudit('full');
1329
+ return `## Drift Audit Report\nTimestamp: ${result.timestamp}\nStatus: ${result.status}\nDrifts found: ${result.drift_count}\nClean checks: ${result.clean_checks}\n\n${result.drifts.map(d => `- [${d.type}] ${d.source} -> ${d.target}: ${d.field} expected="${d.expected}" found="${d.found}"`).join('\n') || 'No drifts detected.'}`;
1330
+ }
1331
+
749
1332
  default:
750
1333
  return null;
751
1334
  }
@@ -824,7 +1407,7 @@ const server = http.createServer((req, res) => {
824
1407
  // Health check
825
1408
  if (req.method === 'GET' && url.pathname === '/health') {
826
1409
  res.writeHead(200, { 'Content-Type': 'application/json' });
827
- res.end(JSON.stringify({ status: 'ok', service: 'nervous-system-mcp', version: '1.1.0', protocol: MCP_VERSION }));
1410
+ res.end(JSON.stringify({ status: 'ok', service: 'nervous-system-mcp', version: '1.4.0', protocol: MCP_VERSION }));
828
1411
  return;
829
1412
  }
830
1413
 
@@ -941,7 +1524,7 @@ const server = http.createServer((req, res) => {
941
1524
  res.writeHead(200, { 'Content-Type': 'application/json' });
942
1525
  res.end(JSON.stringify({
943
1526
  name: 'The Nervous System MCP Server',
944
- version: '1.1.0',
1527
+ version: '1.4.0',
945
1528
  protocol: MCP_VERSION,
946
1529
  description: 'LLM behavioral enforcement framework. 7 core rules, preflight checks, session handoffs, worklogs, violation logging, kill switch, hash-chained audit, and forced reflection cycles. Built by Arthur Palyan.',
947
1530
  endpoints: {
@@ -963,8 +1546,8 @@ const server = http.createServer((req, res) => {
963
1546
  migrateExistingViolations();
964
1547
 
965
1548
  server.listen(PORT, '127.0.0.1', () => {
966
- console.error(`[MCP Server] Nervous System v1.1.0 running on port ${PORT}`);
1549
+ console.error(`[MCP Server] Nervous System v1.4.0 running on port ${PORT}`);
967
1550
  console.error(`[MCP Server] SSE: /sse | HTTP: /mcp | Health: /health | Kill: POST /kill | Audit: GET /audit/verify | Dispatches: GET /dispatches`);
968
1551
  console.error(`[MCP Server] Protocol: ${MCP_VERSION}`);
969
- console.error(`[MCP Server] Tools: ${TOOLS.length} (including kill switch, audit chain, dispatch)`);
1552
+ console.error(`[MCP Server] Tools: ${TOOLS.length} (including kill switch, audit chain, dispatch, drift audit)`);
970
1553
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-nervous-system",
3
- "version": "1.3.0",
4
- "description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules, 14 tools including task complexity classification and user intent parsing. MCP server for Claude Desktop, Claude Code, and any MCP-compatible client.",
3
+ "version": "1.4.0",
4
+ "description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules, 12 tools including kill switch, audit chain, dispatch, and drift audit. MCP server for Claude Desktop, Claude Code, and any MCP-compatible client.",
5
5
  "main": "server.js",
6
6
  "bin": {
7
7
  "mcp-nervous-system": "./stdio.js"
package/stdio.js CHANGED
@@ -51,7 +51,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
51
51
  return { content: [{ type: "text", text: JSON.stringify({ name: "The Nervous System", version: "1.1.1", description: "LLM Behavioral Enforcement Framework", rules: RULES, total_tools: 11, production_stats: { violations_caught: 56, rules_bypassed: 0, edits_blocked: 32, processes_monitored: 22 } }, null, 2) }] };
52
52
 
53
53
  case "get_nervous_system_info":
54
- return { content: [{ type: "text", text: JSON.stringify({ name: "The Nervous System", version: "1.1.1", author: "Arthur Palyan", company: "Palyan AI / Levels of Self LLC", website: "https://www.levelsofself.com", demo: "https://api.100levelup.com/family/arthur.html?guest=1", github: "https://github.com/levelsofself/mcp-nervous-system", npm: "https://www.npmjs.com/package/mcp-nervous-system", tools: 11, rules: 7, production_stats: { violations: 56, bypasses: 0, blocked_edits: 32, uptime_days: 25, monthly_cost: "$12" } }, null, 2) }] };
54
+ return { content: [{ type: "text", text: JSON.stringify({ name: "The Nervous System", version: "1.1.1", author: "Arthur Palyan", company: "Palyan Family AI System / Levels of Self LLC", website: "https://www.levelsofself.com", demo: "https://api.100levelup.com/family/arthur.html?guest=1", github: "https://github.com/levelsofself/mcp-nervous-system", npm: "https://www.npmjs.com/package/mcp-nervous-system", tools: 11, rules: 7, production_stats: { violations: 56, bypasses: 0, blocked_edits: 32, uptime_days: 25, monthly_cost: "$12" } }, null, 2) }] };
55
55
 
56
56
  case "check_preflight":
57
57
  const fp = args?.file_path || "unknown";