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 +35 -7
- 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 +10 -4
- package/server.js +1 -1
- package/skills/multi-agent-governance/SKILL.md +97 -0
- package/stdio.js +4 -4
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.
|
|
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.
|
|
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 (
|
|
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 $
|
|
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
|
-
- **
|
|
171
|
+
- **100** files protected
|
|
163
172
|
- **0** rules bypassed
|
|
164
|
-
- **
|
|
165
|
-
- **
|
|
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,
|
|
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';
|