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 +23 -1
- package/index.js +436 -4
- package/integrations/agent-teams/README.md +107 -0
- package/integrations/agent-teams/agent-teams-governance.md +117 -0
- package/integrations/agent-teams/example-claude-code-config.json +11 -0
- package/integrations/generic-mcp/README.md +121 -0
- package/integrations/generic-mcp/quick-start.sh +76 -0
- package/integrations/hivemind/README.md +98 -0
- package/integrations/hivemind/example-team.yaml +45 -0
- package/integrations/hivemind/hivemind-ns-connector.js +262 -0
- package/integrations/ruflo/README.md +80 -0
- package/integrations/ruflo/example-config.yaml +35 -0
- package/integrations/ruflo/ruflo-ns-plugin.js +228 -0
- package/package.json +8 -3
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.
|
|
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,
|
|
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 |
|