mcp-nervous-system 1.8.1 → 1.9.1

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
@@ -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 Family AI System. 19 tools including configuration drift detection and emergency kill switch. Battle-tested on a 13-agent AI family running 29 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. 21 tools including configuration drift detection and emergency kill switch. Battle-tested on a 13-agent AI family running 27 processes 24/7 on a single VPS. 58+ violations logged, 0 bypassed.
8
8
 
9
9
  ## The Problem
10
10
 
@@ -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.
@@ -84,7 +93,7 @@ Configuration drift detection across 5 scopes: roles, versions, files, processes
84
93
  | 6 | **Ask Before Touching** | Unauthorized changes. Logic changes need human approval. |
85
94
  | 7 | **Hand Off** | Context loss. Written handoffs every 3-4 exchanges. |
86
95
 
87
- ## MCP Tools (12)
96
+ ## MCP Tools (21)
88
97
 
89
98
  | Tool | Description |
90
99
  |------|------------|
@@ -134,7 +143,7 @@ https://api.100levelup.com/family/eu-ai-act.html
134
143
 
135
144
  ## Production Reference: Tamara
136
145
 
137
- Tamara is an autonomous AI operations manager built on the Nervous System framework. She manages 13 AI agents across 5 platforms, serving 175 countries from a $24/month VPS - without dedicated DevOps staff.
146
+ Tamara is an autonomous AI operations manager built on the Nervous System framework. She manages 13 AI agents across 5 platforms, serving 175+ countries accessible from a $12/month VPS - without dedicated DevOps staff.
138
147
 
139
148
  Tamara is the proof that the Nervous System works in production. She runs 60-minute autonomous check cycles, detects configuration drift, dispatches remediation agents, and reports to the human operator only when judgment is needed.
140
149
 
@@ -159,10 +168,10 @@ From the live Palyan Family AI System deployment (Feb 28 - Mar 5, 2026):
159
168
 
160
169
  - **58+** violations caught
161
170
  - **29** edits blocked by preflight
162
- - **13** unique files protected
171
+ - **100** files protected
163
172
  - **0** rules bypassed
164
- - **28** processes monitored
165
- - **7** days continuous operation
173
+ - **27** processes monitored
174
+ - **5** days continuous operation
166
175
 
167
176
  ## Live Demo
168
177
 
@@ -177,6 +186,25 @@ 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
+
202
+ ## Agent Skills
203
+
204
+ The Nervous System is also available as an Agent Skill following the open [Agent Skills standard](https://agentskills.io). Install the governance skill to teach any AI agent (Claude, Codex, Cursor, Copilot, VS Code) how to enforce behavioral guardrails in multi-agent systems.
205
+
206
+ See `skills/multi-agent-governance/SKILL.md` for the full skill definition.
207
+
180
208
  ## Philosophy
181
209
 
182
210
  > "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';