mcp-nervous-system 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,7 +60,16 @@ Protocol: MCP 2024-11-05 (Streamable HTTP + SSE)
60
60
  Authentication: None required
61
61
  ```
62
62
 
63
- ## NEW in v1.8.0
63
+ ## NEW in v1.9.0
64
+
65
+ **Platform Integration Guides** - Working examples for governing multi-agent systems on the 3 biggest platforms plus any MCP client. Each guide gets you to governed agents in under 10 minutes.
66
+
67
+ - [Ruflo (claude-flow)](./integrations/ruflo/) - Queen-Worker hive mind governance with preflight checks, drift audits, and violation logging to swarm_state
68
+ - [Hivemind](./integrations/hivemind/) - Team chat agent governance with tool interception, heartbeat drift audits, and session handoffs
69
+ - [Anthropic Agent Teams](./integrations/agent-teams/) - Parallel agent governance via CLAUDE.md propagation with shared untouchable lists and unified audit trails
70
+ - [Generic MCP](./integrations/generic-mcp/) - 5-minute setup for any MCP client (Claude Desktop, Claude Code, Cursor, Windsurf, Cline)
71
+
72
+ ## v1.8.0
64
73
 
65
74
  **Tamara Reference Implementation + Case Study** (2 new resources)
66
75
  Production reference implementation of an autonomous AI operations manager built on the Nervous System. Includes full architecture documentation, build-your-own guide, and case study with real metrics from managing 13 agents across 5 platforms.
@@ -177,6 +186,19 @@ Try it yourself (no login required):
177
186
  - **[Incident Response](https://api.100levelup.com/family/incident-response.html)** - Detection, containment, resolution
178
187
  - **[EU AI Act Compliance](https://api.100levelup.com/family/eu-ai-act.html)** - Practical EU AI Act compliance tools
179
188
 
189
+ ## Integrations
190
+
191
+ Works with the major multi-agent platforms and any MCP client:
192
+
193
+ | Platform | Integration | Setup Time |
194
+ |----------|------------|------------|
195
+ | **[Ruflo (claude-flow)](./integrations/ruflo/)** | Plugin hooks into Queen-Worker pipeline | 10 min |
196
+ | **[Hivemind](./integrations/hivemind/)** | MCP connector with tool interception | 10 min |
197
+ | **[Anthropic Agent Teams](./integrations/agent-teams/)** | CLAUDE.md governance propagation | 10 min |
198
+ | **[Any MCP Client](./integrations/generic-mcp/)** | 3 lines of config | 5 min |
199
+
200
+ Each integration includes working code, example configs, and a step-by-step README.
201
+
180
202
  ## Philosophy
181
203
 
182
204
  > "LLMs can't reliably self-enforce promises. Guardrails work via preflight.sh, violation logs, and catching drift. Build enforcement systems, don't make promises."
package/index.js CHANGED
@@ -669,14 +669,14 @@ const TOOLS = [
669
669
  {
670
670
  name: 'drift_audit',
671
671
  annotations: { title: 'Configuration Drift Audit', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
672
- 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, when running processes do not match documentation, or when bots fail compliance with the 6 universal standards. Scopes: roles, versions, files, processes, website, platforms, docs, bots.',
672
+ 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, when running processes do not match documentation, when bots fail compliance with the 6 universal standards, or when family members in family-roles.json are missing from downstream locations. Scopes: roles, versions, files, processes, website, platforms, docs, bots, members.',
673
673
  inputSchema: {
674
674
  type: 'object',
675
675
  properties: {
676
676
  scope: {
677
677
  type: 'string',
678
- enum: ['full', 'roles', 'versions', 'files', 'processes', 'website', 'docs'],
679
- 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, docs=compares reality (pm2, ports, crons, dept folders) against BUSINESS_BUILDER.md, LLM_STARTUP.md, family-roles.json'
678
+ enum: ['full', 'roles', 'versions', 'files', 'processes', 'website', 'docs', 'members'],
679
+ 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, docs=compares reality (pm2, ports, crons, dept folders) against BUSINESS_BUILDER.md, LLM_STARTUP.md, family-roles.json, members=checks every downstream location for missing family members from family-roles.json'
680
680
  }
681
681
  }
682
682
  }
@@ -695,6 +695,18 @@ const TOOLS = [
695
695
  description: 'Runs all 3 propagators (role, version, content) and reports what changed vs what was already current. Ensures all downstream files match source-of-truth values.',
696
696
  inputSchema: { type: 'object', properties: {} }
697
697
  },
698
+ // NEW: Propagate Family Member
699
+ {
700
+ name: 'propagate_family_member',
701
+ annotations: { title: 'Propagate Family Member', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
702
+ description: 'Checks family-roles.json as source of truth, detects missing members in all downstream locations, and auto-fixes what it can (family-status.json, system-config.json, HTML counts). Flags UNTOUCHABLE files for manual fix. Run after adding/removing a family member.',
703
+ inputSchema: {
704
+ type: 'object',
705
+ properties: {
706
+ dry_run: { type: 'boolean', description: 'If true, only report what would change without making changes. Default: false' }
707
+ }
708
+ }
709
+ },
698
710
  // NEW: Session Close
699
711
  {
700
712
  name: 'session_close',
@@ -1696,11 +1708,419 @@ function auditDocs() {
1696
1708
  return { drifts, cleanChecks };
1697
1709
  }
1698
1710
 
1711
+ // ============================================================
1712
+ // MEMBER PROPAGATION AUDIT
1713
+ // Checks that every member in family-roles.json exists in all
1714
+ // downstream locations. Catches the Corona/Soriano problem.
1715
+ // ============================================================
1716
+
1717
+ function auditMembers() {
1718
+ const drifts = [];
1719
+ let cleanChecks = 0;
1720
+ const rolesFile = projectPath('roles_file');
1721
+ if (!rolesFile) return { drifts: [], cleanChecks: 0, skipped: 'roles_file not configured' };
1722
+ const roles = safeReadJSON(rolesFile);
1723
+ if (!roles || !roles.members) {
1724
+ drifts.push({ type: 'members_missing_source', source: rolesFile, target: '', field: '', expected: 'valid JSON with members array', found: 'missing or invalid' });
1725
+ return { drifts, cleanChecks };
1726
+ }
1727
+
1728
+ const sourceIds = roles.members.map(function(m) { return m.id; });
1729
+ const sourceCount = roles.members.length;
1730
+
1731
+ // Helper: check a downstream location for missing member IDs
1732
+ function checkDownstream(name, ids, filePath) {
1733
+ if (ids.length !== sourceCount) {
1734
+ drifts.push({ type: 'member_count_mismatch', source: 'family-roles.json', target: name, field: 'count', expected: String(sourceCount), found: String(ids.length) });
1735
+ }
1736
+ for (var i = 0; i < sourceIds.length; i++) {
1737
+ var sid = sourceIds[i];
1738
+ if (ids.indexOf(sid) === -1) {
1739
+ drifts.push({ type: 'member_missing', source: 'family-roles.json', target: name, field: sid, expected: 'present', found: 'missing' });
1740
+ }
1741
+ }
1742
+ // Check for extra IDs not in source
1743
+ for (var j = 0; j < ids.length; j++) {
1744
+ if (sourceIds.indexOf(ids[j]) === -1) {
1745
+ drifts.push({ type: 'member_extra', source: 'family-roles.json', target: name, field: ids[j], expected: 'not present', found: 'extra member in downstream' });
1746
+ }
1747
+ }
1748
+ if (!drifts.some(function(d) { return d.target === name; })) cleanChecks++;
1749
+ }
1750
+
1751
+ // 1. family-status.json (cache)
1752
+ var statusFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-status.json') : null;
1753
+ if (statusFile) {
1754
+ var status = safeReadJSON(statusFile);
1755
+ if (status && Array.isArray(status)) {
1756
+ var statusIds = status.filter(function(m) { return m.id !== 'papa'; }).map(function(m) { return m.id; });
1757
+ checkDownstream('family-status.json', statusIds, statusFile);
1758
+ } else if (status && status.members) {
1759
+ var statusIds2 = status.members.map(function(m) { return m.id; });
1760
+ checkDownstream('family-status.json', statusIds2, statusFile);
1761
+ }
1762
+ }
1763
+
1764
+ // 2. system-config.json family array
1765
+ var configFile = projectPath('config_file');
1766
+ if (configFile) {
1767
+ var config = safeReadJSON(configFile);
1768
+ if (config && config.family) {
1769
+ // system-config includes Arthur/papa as first entry
1770
+ var configNames = config.family.filter(function(m) { return m.name !== 'Arthur'; }).map(function(m) { return m.name; });
1771
+ var sourceNames = roles.members.map(function(m) { return m.name; });
1772
+ for (var k = 0; k < sourceNames.length; k++) {
1773
+ if (configNames.indexOf(sourceNames[k]) === -1) {
1774
+ drifts.push({ type: 'member_missing', source: 'family-roles.json', target: 'system-config.json', field: sourceNames[k], expected: 'present', found: 'missing' });
1775
+ }
1776
+ }
1777
+ if (!drifts.some(function(d) { return d.target === 'system-config.json' && d.type === 'member_missing'; })) cleanChecks++;
1778
+ }
1779
+ }
1780
+
1781
+ // 3. family-home/index.html PROFILES and MS objects
1782
+ var htmlDir = projectPath('html_dir');
1783
+ if (htmlDir) {
1784
+ var indexContent = safeReadFile(path.join(htmlDir, 'index.html'));
1785
+ if (indexContent) {
1786
+ // Check PROFILES object - extract top-level keys (lines like " name: {")
1787
+ var profileIds = [];
1788
+ var profileStart = indexContent.indexOf('PROFILES = {');
1789
+ if (profileStart === -1) profileStart = indexContent.indexOf('PROFILES={');
1790
+ if (profileStart !== -1) {
1791
+ var profileBlock = indexContent.substring(profileStart, indexContent.indexOf('};', profileStart) + 2);
1792
+ var profileLines = profileBlock.split('\n');
1793
+ for (var pl = 0; pl < profileLines.length; pl++) {
1794
+ var pmatch = profileLines[pl].match(/^\s+(\w+)\s*:/);
1795
+ if (pmatch && pmatch[1] !== 'papa') profileIds.push(pmatch[1]);
1796
+ }
1797
+ }
1798
+ checkDownstream('index.html PROFILES', profileIds, path.join(htmlDir, 'index.html'));
1799
+
1800
+ // Check MS colors object - same line-based approach
1801
+ var msIds = [];
1802
+ var msStart = indexContent.indexOf('const MS = {');
1803
+ if (msStart === -1) msStart = indexContent.indexOf('const MS={');
1804
+ if (msStart !== -1) {
1805
+ var msBlock = indexContent.substring(msStart, indexContent.indexOf('};', msStart) + 2);
1806
+ var msLines = msBlock.split('\n');
1807
+ for (var ml = 0; ml < msLines.length; ml++) {
1808
+ var mmatch = msLines[ml].match(/^\s+(\w+)\s*:/);
1809
+ if (mmatch && mmatch[1] !== 'papa') msIds.push(mmatch[1]);
1810
+ }
1811
+ }
1812
+ checkDownstream('index.html MS colors', msIds, path.join(htmlDir, 'index.html'));
1813
+ }
1814
+
1815
+ // 4. meet.html - check member count in text
1816
+ var meetContent = safeReadFile(path.join(htmlDir, 'meet.html'));
1817
+ if (meetContent) {
1818
+ var meetCountMatches = meetContent.match(/(\d+)\s*AI\s*family\s*members/gi) || [];
1819
+ for (var mc = 0; mc < meetCountMatches.length; mc++) {
1820
+ var num = parseInt(meetCountMatches[mc]);
1821
+ // Total includes papa (+1)
1822
+ if (num > 0 && num !== sourceCount + 1 && num !== sourceCount) {
1823
+ drifts.push({ type: 'member_count_mismatch', source: 'family-roles.json', target: 'meet.html', field: 'member_count_text', expected: String(sourceCount + 1), found: String(num) });
1824
+ }
1825
+ }
1826
+ if (!drifts.some(function(d) { return d.target === 'meet.html'; })) cleanChecks++;
1827
+ }
1828
+ }
1829
+
1830
+ // 5. family-home/server.js COLORS map, soulPaths, publicPersonas
1831
+ var serverFile = htmlDir ? path.join(htmlDir, 'server.js') : null;
1832
+ if (serverFile) {
1833
+ var serverContent = safeReadFile(serverFile);
1834
+ if (serverContent) {
1835
+ // COLORS map
1836
+ var colorsMatch = serverContent.match(/COLORS\s*=\s*\{([^}]+)\}/);
1837
+ if (colorsMatch) {
1838
+ var colorKeys = colorsMatch[1].match(/(\w+)\s*:/g) || [];
1839
+ var colorIds = colorKeys.map(function(k) { return k.replace(/\s*:/, '').trim(); }).filter(function(k) { return k !== 'papa'; });
1840
+ checkDownstream('server.js COLORS', colorIds, serverFile);
1841
+ }
1842
+
1843
+ // soulPaths map
1844
+ var soulMatch = serverContent.match(/soulPaths\s*=\s*\{([^}]+)\}/);
1845
+ if (soulMatch) {
1846
+ var soulKeys = soulMatch[1].match(/(\w+)\s*:/g) || [];
1847
+ var soulIds = soulKeys.map(function(k) { return k.replace(/\s*:/, '').trim(); });
1848
+ checkDownstream('server.js soulPaths', soulIds, serverFile);
1849
+ }
1850
+
1851
+ // publicPersonas object - multi-line, find keys
1852
+ var personaIds = [];
1853
+ var personaMatches = serverContent.match(/publicPersonas\s*=\s*\{/);
1854
+ if (personaMatches) {
1855
+ // Extract from publicPersonas to its closing - look for member keys
1856
+ var personaStart = serverContent.indexOf('publicPersonas = {');
1857
+ if (personaStart !== -1) {
1858
+ var personaBlock = serverContent.substring(personaStart, personaStart + 5000);
1859
+ var pKeyMatches = personaBlock.match(/^\s+(\w+)\s*:\s*`/gm) || [];
1860
+ personaIds = pKeyMatches.map(function(k) { return k.trim().replace(/\s*:\s*`$/, ''); });
1861
+ }
1862
+ }
1863
+ if (personaIds.length > 0) {
1864
+ checkDownstream('server.js publicPersonas', personaIds, serverFile);
1865
+ }
1866
+ }
1867
+ }
1868
+
1869
+ // 6. tamara-v6.js writeFamilyStatus array (UNTOUCHABLE - flag only)
1870
+ var tamaraFile = projectPath('project_root') ? path.join(projectPath('project_root') || process.cwd(), 'tamara-v6.js') : null;
1871
+ if (tamaraFile) {
1872
+ var tamaraContent = safeReadFile(tamaraFile);
1873
+ if (tamaraContent) {
1874
+ var wfsMatch = tamaraContent.match(/writeFamilyStatus/);
1875
+ if (wfsMatch) {
1876
+ // Check if each member ID appears in the file
1877
+ for (var ti = 0; ti < sourceIds.length; ti++) {
1878
+ if (tamaraContent.indexOf("'" + sourceIds[ti] + "'") === -1 && tamaraContent.indexOf('"' + sourceIds[ti] + '"') === -1) {
1879
+ drifts.push({ type: 'member_missing_untouchable', source: 'family-roles.json', target: 'tamara-v6.js (UNTOUCHABLE)', field: sourceIds[ti], expected: 'present in writeFamilyStatus', found: 'missing - FLAG ONLY, do not edit' });
1880
+ }
1881
+ }
1882
+ if (!drifts.some(function(d) { return d.target === 'tamara-v6.js (UNTOUCHABLE)'; })) cleanChecks++;
1883
+ }
1884
+ }
1885
+ }
1886
+
1887
+ // 7. bots-app/public/index.html - agent count in footer/text
1888
+ var botsAppIndex = projectPath('project_root') ? path.join(projectPath('project_root') || process.cwd(), 'bots-app', 'public', 'index.html') : null;
1889
+ if (botsAppIndex) {
1890
+ var botsContent = safeReadFile(botsAppIndex);
1891
+ if (botsContent) {
1892
+ var botsCountMatches = botsContent.match(/(\d+)\s*agents/gi) || [];
1893
+ for (var bc = 0; bc < botsCountMatches.length; bc++) {
1894
+ var bNum = parseInt(botsCountMatches[bc]);
1895
+ if (bNum > 0 && bNum !== sourceCount + 1 && bNum !== sourceCount) {
1896
+ drifts.push({ type: 'member_count_mismatch', source: 'family-roles.json', target: 'bots-app/index.html', field: 'agent_count', expected: String(sourceCount + 1), found: String(bNum) });
1897
+ }
1898
+ }
1899
+ if (!drifts.some(function(d) { return d.target === 'bots-app/index.html'; })) cleanChecks++;
1900
+ }
1901
+ }
1902
+
1903
+ return { drifts, cleanChecks };
1904
+ }
1905
+
1906
+ // ============================================================
1907
+ // MEMBER PROPAGATION ENGINE
1908
+ // Auto-fixes downstream files when family-roles.json changes.
1909
+ // ============================================================
1910
+
1911
+ function runMemberPropagation(dryRun) {
1912
+ var timestamp = new Date().toISOString();
1913
+ var actions = [];
1914
+ var flags = [];
1915
+
1916
+ var rolesFile = projectPath('roles_file');
1917
+ if (!rolesFile) return { timestamp: timestamp, status: 'skipped', reason: 'roles_file not configured' };
1918
+ var roles = safeReadJSON(rolesFile);
1919
+ if (!roles || !roles.members) return { timestamp: timestamp, status: 'error', reason: 'family-roles.json missing or invalid' };
1920
+
1921
+ var sourceIds = roles.members.map(function(m) { return m.id; });
1922
+ var sourceCount = roles.members.length;
1923
+
1924
+ // 1. family-status.json - just delete it, Tamara regenerates
1925
+ var statusFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-status.json') : null;
1926
+ if (statusFile && fs.existsSync(statusFile)) {
1927
+ var status = safeReadJSON(statusFile);
1928
+ if (status) {
1929
+ var statusArr = Array.isArray(status) ? status : (status.members || []);
1930
+ var statusIds = statusArr.filter(function(m) { return m.id !== 'papa'; }).map(function(m) { return m.id; });
1931
+ var missing = sourceIds.filter(function(id) { return statusIds.indexOf(id) === -1; });
1932
+ if (missing.length > 0) {
1933
+ if (!dryRun) {
1934
+ try { fs.unlinkSync(statusFile); } catch (e) {}
1935
+ }
1936
+ actions.push({ file: 'family-status.json', action: dryRun ? 'would_delete' : 'deleted', reason: 'Missing members: ' + missing.join(', ') + '. Tamara will regenerate.' });
1937
+ }
1938
+ }
1939
+ }
1940
+
1941
+ // 2. system-config.json - add missing members
1942
+ var configFile = projectPath('config_file');
1943
+ if (configFile) {
1944
+ var config = safeReadJSON(configFile);
1945
+ if (config && config.family) {
1946
+ var configNames = config.family.map(function(m) { return m.name; });
1947
+ var missingMembers = roles.members.filter(function(m) { return configNames.indexOf(m.name) === -1; });
1948
+ if (missingMembers.length > 0) {
1949
+ if (!dryRun) {
1950
+ for (var i = 0; i < missingMembers.length; i++) {
1951
+ var mm = missingMembers[i];
1952
+ config.family.push({
1953
+ name: mm.name,
1954
+ role: mm.role,
1955
+ procs: mm.procs || [],
1956
+ desc: mm.desc || ''
1957
+ });
1958
+ }
1959
+ try { fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); } catch (e) {}
1960
+ }
1961
+ actions.push({ file: 'system-config.json', action: dryRun ? 'would_add' : 'added', members: missingMembers.map(function(m) { return m.name; }) });
1962
+ }
1963
+ }
1964
+ }
1965
+
1966
+ // 3. HTML page counts - update agent/member count text
1967
+ var htmlDir = projectPath('html_dir');
1968
+ if (htmlDir) {
1969
+ var htmlFiles = [];
1970
+ try { htmlFiles = fs.readdirSync(htmlDir).filter(function(f) { return f.endsWith('.html'); }); } catch (e) {}
1971
+ var totalWithPapa = sourceCount + 1; // family-roles + papa
1972
+ for (var hi = 0; hi < htmlFiles.length; hi++) {
1973
+ var htmlPath = path.join(htmlDir, htmlFiles[hi]);
1974
+ var content = safeReadFile(htmlPath);
1975
+ if (!content) continue;
1976
+ var changed = false;
1977
+ // Replace stale agent/member counts
1978
+ var updated = content.replace(/(\d+)\s*(AI\s+)?(?:family\s+)?(?:members|agents)/gi, function(match, num) {
1979
+ var n = parseInt(num);
1980
+ if (n > 0 && n !== totalWithPapa && n !== sourceCount && n < 50) {
1981
+ changed = true;
1982
+ return match.replace(num, String(totalWithPapa));
1983
+ }
1984
+ return match;
1985
+ });
1986
+ if (changed) {
1987
+ if (!dryRun) {
1988
+ try { fs.writeFileSync(htmlPath, updated); } catch (e) {}
1989
+ }
1990
+ actions.push({ file: htmlFiles[hi], action: dryRun ? 'would_update_count' : 'updated_count', new_count: totalWithPapa });
1991
+ }
1992
+ }
1993
+
1994
+ // Also check bots-app
1995
+ var botsIndex = projectPath('project_root') ? path.join(projectPath('project_root') || process.cwd(), 'bots-app', 'public', 'index.html') : null;
1996
+ if (botsIndex && fs.existsSync(botsIndex)) {
1997
+ var botsContent = safeReadFile(botsIndex);
1998
+ if (botsContent) {
1999
+ var botsChanged = false;
2000
+ var botsUpdated = botsContent.replace(/(\d+)\s*agents/gi, function(match, num) {
2001
+ var n = parseInt(num);
2002
+ if (n > 0 && n !== totalWithPapa && n !== sourceCount && n < 50) {
2003
+ botsChanged = true;
2004
+ return match.replace(num, String(totalWithPapa));
2005
+ }
2006
+ return match;
2007
+ });
2008
+ if (botsChanged) {
2009
+ if (!dryRun) {
2010
+ try { fs.writeFileSync(botsIndex, botsUpdated); } catch (e) {}
2011
+ }
2012
+ actions.push({ file: 'bots-app/index.html', action: dryRun ? 'would_update_count' : 'updated_count', new_count: totalWithPapa });
2013
+ }
2014
+ }
2015
+ }
2016
+ }
2017
+
2018
+ // 4. Flag UNTOUCHABLE files that need manual updates
2019
+ // tamara-v6.js
2020
+ var tamaraFile = projectPath('project_root') ? path.join(projectPath('project_root') || process.cwd(), 'tamara-v6.js') : null;
2021
+ if (tamaraFile) {
2022
+ var tamaraContent = safeReadFile(tamaraFile);
2023
+ if (tamaraContent) {
2024
+ var tamaraMissing = sourceIds.filter(function(id) {
2025
+ return tamaraContent.indexOf("'" + id + "'") === -1 && tamaraContent.indexOf('"' + id + '"') === -1;
2026
+ });
2027
+ if (tamaraMissing.length > 0) {
2028
+ flags.push({ file: 'tamara-v6.js', status: 'UNTOUCHABLE', missing_members: tamaraMissing, action_needed: 'Add member IDs to writeFamilyStatus array' });
2029
+ }
2030
+ }
2031
+ }
2032
+
2033
+ // family-home/index.html PROFILES and MS (UNTOUCHABLE for auto-edit per task)
2034
+ if (htmlDir) {
2035
+ var indexContent = safeReadFile(path.join(htmlDir, 'index.html'));
2036
+ if (indexContent) {
2037
+ var profileIds2 = [];
2038
+ var pStart = indexContent.indexOf('PROFILES = {');
2039
+ if (pStart === -1) pStart = indexContent.indexOf('PROFILES={');
2040
+ if (pStart !== -1) {
2041
+ var pBlock = indexContent.substring(pStart, indexContent.indexOf('};', pStart) + 2);
2042
+ var pLines = pBlock.split('\n');
2043
+ for (var pli = 0; pli < pLines.length; pli++) {
2044
+ var pm = pLines[pli].match(/^\s+(\w+)\s*:/);
2045
+ if (pm && pm[1] !== 'papa') profileIds2.push(pm[1]);
2046
+ }
2047
+ }
2048
+ var profileMissing = sourceIds.filter(function(id) { return profileIds2.indexOf(id) === -1; });
2049
+ if (profileMissing.length > 0) {
2050
+ flags.push({ file: 'index.html PROFILES', status: 'NEEDS_UPDATE', missing_members: profileMissing, action_needed: 'Add PROFILES entries with avatar, role, desc, relation' });
2051
+ }
2052
+
2053
+ var msIds2 = [];
2054
+ var mStart = indexContent.indexOf('const MS = {');
2055
+ if (mStart === -1) mStart = indexContent.indexOf('const MS={');
2056
+ if (mStart !== -1) {
2057
+ var mBlock = indexContent.substring(mStart, indexContent.indexOf('};', mStart) + 2);
2058
+ var mLines = mBlock.split('\n');
2059
+ for (var mli = 0; mli < mLines.length; mli++) {
2060
+ var mm2 = mLines[mli].match(/^\s+(\w+)\s*:/);
2061
+ if (mm2 && mm2[1] !== 'papa') msIds2.push(mm2[1]);
2062
+ }
2063
+ }
2064
+ var msMissing = sourceIds.filter(function(id) { return msIds2.indexOf(id) === -1; });
2065
+ if (msMissing.length > 0) {
2066
+ flags.push({ file: 'index.html MS colors', status: 'NEEDS_UPDATE', missing_members: msMissing, action_needed: 'Add color scheme entries' });
2067
+ }
2068
+ }
2069
+ }
2070
+
2071
+ // server.js soulPaths, publicPersonas, COLORS
2072
+ var serverFile = htmlDir ? path.join(htmlDir, 'server.js') : null;
2073
+ if (serverFile) {
2074
+ var serverContent = safeReadFile(serverFile);
2075
+ if (serverContent) {
2076
+ var colorsMatch = serverContent.match(/COLORS\s*=\s*\{([^}]+)\}/);
2077
+ if (colorsMatch) {
2078
+ var colorKeys = colorsMatch[1].match(/(\w+)\s*:/g) || [];
2079
+ var colorIds = colorKeys.map(function(k) { return k.replace(/\s*:/, '').trim(); }).filter(function(k) { return k !== 'papa'; });
2080
+ var colorMissing = sourceIds.filter(function(id) { return colorIds.indexOf(id) === -1; });
2081
+ if (colorMissing.length > 0) {
2082
+ flags.push({ file: 'server.js COLORS', status: 'UNTOUCHABLE', missing_members: colorMissing, action_needed: 'Add color entries for missing members' });
2083
+ }
2084
+ }
2085
+ var soulMatch = serverContent.match(/soulPaths\s*=\s*\{([^}]+)\}/);
2086
+ if (soulMatch) {
2087
+ var soulKeys = soulMatch[1].match(/(\w+)\s*:/g) || [];
2088
+ var soulIds = soulKeys.map(function(k) { return k.replace(/\s*:/, '').trim(); });
2089
+ var soulMissing = sourceIds.filter(function(id) { return soulIds.indexOf(id) === -1; });
2090
+ if (soulMissing.length > 0) {
2091
+ flags.push({ file: 'server.js soulPaths', status: 'UNTOUCHABLE', missing_members: soulMissing, action_needed: 'Add SOUL.md path entries' });
2092
+ }
2093
+ }
2094
+ // publicPersonas
2095
+ var personaStart = serverContent.indexOf('publicPersonas = {');
2096
+ if (personaStart !== -1) {
2097
+ var personaBlock = serverContent.substring(personaStart, personaStart + 5000);
2098
+ var pKeyMatches = personaBlock.match(/^\s+(\w+)\s*:\s*`/gm) || [];
2099
+ var personaIds = pKeyMatches.map(function(k) { return k.trim().replace(/\s*:\s*`$/, ''); });
2100
+ var personaMissing = sourceIds.filter(function(id) { return personaIds.indexOf(id) === -1; });
2101
+ if (personaMissing.length > 0) {
2102
+ flags.push({ file: 'server.js publicPersonas', status: 'UNTOUCHABLE', missing_members: personaMissing, action_needed: 'Add public persona templates' });
2103
+ }
2104
+ }
2105
+ }
2106
+ }
2107
+
2108
+ return {
2109
+ timestamp: timestamp,
2110
+ dry_run: dryRun,
2111
+ status: (actions.length === 0 && flags.length === 0) ? 'all_current' : 'propagation_needed',
2112
+ auto_fixed: actions,
2113
+ manual_flags: flags,
2114
+ source_member_count: sourceCount,
2115
+ source_ids: sourceIds
2116
+ };
2117
+ }
2118
+
1699
2119
  function runDriftAudit(scope) {
1700
2120
  const timestamp = new Date().toISOString();
1701
2121
  const allDrifts = [];
1702
2122
  let totalClean = 0;
1703
- const scopes = scope === 'full' ? ['roles', 'versions', 'files', 'processes', 'website', 'platforms', 'docs', 'bots'] : [scope];
2123
+ const scopes = scope === 'full' ? ['roles', 'versions', 'files', 'processes', 'website', 'platforms', 'docs', 'bots', 'members'] : [scope];
1704
2124
 
1705
2125
  for (const s of scopes) {
1706
2126
  let result;
@@ -1713,6 +2133,7 @@ function runDriftAudit(scope) {
1713
2133
  case 'platforms': result = auditPlatforms(); break;
1714
2134
  case 'docs': result = auditDocs(); break;
1715
2135
  case 'bots': result = auditBotCompliance(); break;
2136
+ case 'members': result = auditMembers(); break;
1716
2137
  default: result = { drifts: [{ type: 'unknown_scope', source: '', target: '', field: s, expected: 'valid scope', found: 'unknown' }], cleanChecks: 0 };
1717
2138
  }
1718
2139
  allDrifts.push(...result.drifts);
@@ -2310,9 +2731,14 @@ function handleToolCall(name, args) {
2310
2731
  return runAutoPropagators();
2311
2732
  }
2312
2733
 
2734
+ case 'propagate_family_member': {
2735
+ return runMemberPropagation(args.dry_run || false);
2736
+ }
2737
+
2313
2738
  case 'session_close': {
2314
2739
  const driftResult = runDriftAudit('full');
2315
2740
  const propagateResult = runAutoPropagators();
2741
+ const memberPropResult = runMemberPropagation(false);
2316
2742
 
2317
2743
  // Platform parity warnings
2318
2744
  let platformWarnings = [];
@@ -2374,9 +2800,15 @@ function handleToolCall(name, args) {
2374
2800
  timestamp: new Date().toISOString(),
2375
2801
  drift_audit: driftResult,
2376
2802
  propagation: propagateResult,
2803
+ member_propagation: memberPropResult,
2377
2804
  summary: driftResult.drift_count === 0 ? 'Session clean - no drifts, propagators run' : `${driftResult.drift_count} drifts found - review before closing`
2378
2805
  };
2379
2806
 
2807
+ // Member propagation blocker
2808
+ if (memberPropResult.manual_flags && memberPropResult.manual_flags.length > 0) {
2809
+ result.member_propagation_blocker = 'MEMBER DRIFT - ' + memberPropResult.manual_flags.length + ' files need manual member updates';
2810
+ }
2811
+
2380
2812
  if (docDriftWarnings.length > 0) {
2381
2813
  result.doc_drift_warnings = docDriftWarnings;
2382
2814
  result.doc_drift_blocker = 'DOCS STALE - update before closing session';
@@ -0,0 +1,107 @@
1
+ # Nervous System + Anthropic Agent Teams (Claude Code)
2
+
3
+ Govern parallel agent teams in Claude Code with the Nervous System. Every sub-agent in the team operates under the same 7 rules, shares an untouchable files list, and contributes to a unified audit trail.
4
+
5
+ ## What You Get
6
+
7
+ - **Shared governance** across all agents in the team
8
+ - **Shared UNTOUCHABLE list** - no agent can edit protected files
9
+ - **Team-level drift_audit** after task completion
10
+ - **Audit trail** spanning all agents in the team session
11
+ - **Compatible with Tool Search** (defer_loading on less-used NS tools)
12
+
13
+ ## Setup (Under 10 Minutes)
14
+
15
+ ### 1. Add NS as an MCP Server in Claude Code
16
+
17
+ ```bash
18
+ claude mcp add nervous-system npx mcp-nervous-system
19
+ ```
20
+
21
+ ### 2. Create Your Config
22
+
23
+ ```bash
24
+ # In your project root:
25
+ cat > nervous-system.config.json << 'EOF'
26
+ {
27
+ "project_root": ".",
28
+ "data_dir": "./.ns-data",
29
+ "logs_dir": "./.ns-data/logs",
30
+ "protected_files_list": "./untouchable-files.txt",
31
+ "pm2_managed": false
32
+ }
33
+ EOF
34
+
35
+ mkdir -p .ns-data/logs
36
+ ```
37
+
38
+ ### 3. Create Your Untouchable Files List
39
+
40
+ ```bash
41
+ cat > untouchable-files.txt << 'EOF'
42
+ # Files no agent should edit
43
+ package-lock.json
44
+ .env
45
+ .env.production
46
+ database/migrations/
47
+ EOF
48
+ ```
49
+
50
+ ### 4. Add Governance to Your CLAUDE.md
51
+
52
+ Add this to your project's `CLAUDE.md` so every agent in the team inherits the rules:
53
+
54
+ ```markdown
55
+ ## GOVERNANCE (Nervous System)
56
+ - Before editing ANY file, call preflight_check with the file path
57
+ - If BLOCKED: do not edit. Report to team lead.
58
+ - If PROTECTED: do not edit. Ask human.
59
+ - Every 4 messages: call step_back_check
60
+ - Before ending session: call session_handoff
61
+ - After completing team task: call drift_audit with scope "full"
62
+ ```
63
+
64
+ ### 5. Use Agent Teams with Governance
65
+
66
+ When Claude Code spawns agent teams, every agent reads `CLAUDE.md` and inherits the governance rules. The NS MCP server is available to all agents in the team.
67
+
68
+ ```
69
+ Team Lead
70
+ |
71
+ +-- Agent 1 (developer)
72
+ | +-- preflight_check before edits
73
+ | +-- step_back every 4 turns
74
+ | +-- violations logged to shared audit
75
+ |
76
+ +-- Agent 2 (tester)
77
+ | +-- preflight_check before edits
78
+ | +-- step_back every 4 turns
79
+ | +-- violations logged to shared audit
80
+ |
81
+ +-- Agent 3 (reviewer)
82
+ | +-- preflight_check before edits
83
+ | +-- step_back every 4 turns
84
+ | +-- violations logged to shared audit
85
+ |
86
+ +-- After all agents complete:
87
+ +-- drift_audit(scope: "full")
88
+ +-- session_handoff
89
+ +-- verify_audit_chain
90
+ ```
91
+
92
+ ## Architecture
93
+
94
+ See `agent-teams-governance.md` for the full architecture guide covering:
95
+
96
+ - How governance flows through CLAUDE.md to every agent
97
+ - Tool Search compatibility with defer_loading
98
+ - Audit trail spanning multiple parallel agents
99
+ - Team-level vs agent-level governance patterns
100
+
101
+ ## Files
102
+
103
+ | File | Purpose |
104
+ |------|---------|
105
+ | `README.md` | This guide |
106
+ | `agent-teams-governance.md` | Architecture deep-dive |
107
+ | `example-claude-code-config.json` | Example MCP config for Claude Code |