fix: revert promotion logic — never overwrite existing CLAUDE.md
The promotion logic (overwriting CLAUDE.md when a group becomes main) is unsafe. Real-world setups use is_main for groups that intentionally lack admin context — e.g. a family chat (whatsapp_casa) with 144 lines of custom persona, PARA workspace, task management, and family context. Overwriting based on missing "## Admin Context" would destroy user work. register.ts now follows a simple rule: create template for new folders, never touch existing files. Tests updated to verify preservation across re-registration and main promotion scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -272,24 +272,18 @@ describe('CLAUDE.md template copy', () => {
|
|||||||
const folderDir = path.join(groupsDir, folder);
|
const folderDir = path.join(groupsDir, folder);
|
||||||
fs.mkdirSync(path.join(folderDir, 'logs'), { recursive: true });
|
fs.mkdirSync(path.join(folderDir, 'logs'), { recursive: true });
|
||||||
|
|
||||||
// Template copy + promotion (register.ts lines 119-148)
|
// Template copy — never overwrite existing (register.ts lines 119-135)
|
||||||
const dest = path.join(folderDir, 'CLAUDE.md');
|
const dest = path.join(folderDir, 'CLAUDE.md');
|
||||||
const templatePath = isMain
|
if (!fs.existsSync(dest)) {
|
||||||
? path.join(groupsDir, 'main', 'CLAUDE.md')
|
const templatePath = isMain
|
||||||
: path.join(groupsDir, 'global', 'CLAUDE.md');
|
? path.join(groupsDir, 'main', 'CLAUDE.md')
|
||||||
const fileExists = fs.existsSync(dest);
|
: path.join(groupsDir, 'global', 'CLAUDE.md');
|
||||||
const needsPromotion =
|
|
||||||
isMain &&
|
|
||||||
fileExists &&
|
|
||||||
!fs.readFileSync(dest, 'utf-8').includes('## Admin Context');
|
|
||||||
|
|
||||||
if (!fileExists || needsPromotion) {
|
|
||||||
if (fs.existsSync(templatePath)) {
|
if (fs.existsSync(templatePath)) {
|
||||||
fs.copyFileSync(templatePath, dest);
|
fs.copyFileSync(templatePath, dest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name update across all groups (register.ts lines 150-175)
|
// Name update across all groups (register.ts lines 140-165)
|
||||||
if (assistantName !== 'Andy') {
|
if (assistantName !== 'Andy') {
|
||||||
const mdFiles = fs
|
const mdFiles = fs
|
||||||
.readdirSync(groupsDir)
|
.readdirSync(groupsDir)
|
||||||
@@ -407,66 +401,54 @@ describe('CLAUDE.md template copy', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not overwrite main CLAUDE.md that already has admin context', () => {
|
it('never overwrites existing CLAUDE.md on re-registration', () => {
|
||||||
simulateRegister('slack_main', true);
|
simulateRegister('slack_main', true);
|
||||||
// User appends custom content to the main template
|
// User customizes the file extensively (persona, workspace, rules)
|
||||||
const mdPath = path.join(groupsDir, 'slack_main', 'CLAUDE.md');
|
const mdPath = path.join(groupsDir, 'slack_main', 'CLAUDE.md');
|
||||||
fs.appendFileSync(mdPath, '\n\n## My Custom Section\n\nUser notes here.');
|
fs.writeFileSync(
|
||||||
|
mdPath,
|
||||||
|
'# Gambi\n\nCustom persona with workspace rules and family context.',
|
||||||
|
);
|
||||||
// Re-registering same folder (e.g. re-running /add-slack)
|
// Re-registering same folder (e.g. re-running /add-slack)
|
||||||
simulateRegister('slack_main', true);
|
simulateRegister('slack_main', true);
|
||||||
|
|
||||||
const content = readGroupMd('slack_main');
|
const content = readGroupMd('slack_main');
|
||||||
// Preserved: has both admin context AND user additions
|
expect(content).toContain('Custom persona');
|
||||||
expect(content).toContain('Admin Context');
|
expect(content).not.toContain('Admin Context');
|
||||||
expect(content).toContain('My Custom Section');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not overwrite non-main CLAUDE.md on re-registration', () => {
|
it('never overwrites when non-main becomes main (isMain changes)', () => {
|
||||||
simulateRegister('telegram_friends', false);
|
// User registers a family group as non-main
|
||||||
// User customizes the file
|
simulateRegister('whatsapp_casa', false);
|
||||||
const mdPath = path.join(groupsDir, 'telegram_friends', 'CLAUDE.md');
|
// User extensively customizes it (PARA system, task management, etc.)
|
||||||
fs.writeFileSync(mdPath, '# Custom\n\nUser-modified content.');
|
const mdPath = path.join(groupsDir, 'whatsapp_casa', 'CLAUDE.md');
|
||||||
// Re-registering same folder as non-main
|
fs.writeFileSync(
|
||||||
simulateRegister('telegram_friends', false);
|
mdPath,
|
||||||
|
'# Casa\n\nFamily group with PARA system, task management, shopping lists.',
|
||||||
|
);
|
||||||
|
// Later, user promotes to main (no trigger required) — CLAUDE.md must be preserved
|
||||||
|
simulateRegister('whatsapp_casa', true);
|
||||||
|
|
||||||
const content = readGroupMd('telegram_friends');
|
const content = readGroupMd('whatsapp_casa');
|
||||||
expect(content).toContain('User-modified content');
|
expect(content).toContain('PARA system');
|
||||||
|
expect(content).not.toContain('Admin Context');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('promotes non-main group to main when re-registered with isMain', () => {
|
it('preserves custom CLAUDE.md across channels when changing main', () => {
|
||||||
// Initially registered as non-main (gets global template)
|
// Real-world scenario: WhatsApp main + customized Discord research channel
|
||||||
simulateRegister('telegram_main', false);
|
|
||||||
expect(readGroupMd('telegram_main')).not.toContain('Admin Context');
|
|
||||||
|
|
||||||
// User switches this channel to main
|
|
||||||
simulateRegister('telegram_main', true);
|
|
||||||
expect(readGroupMd('telegram_main')).toContain('Admin Context');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('promotes across channels — WhatsApp non-main to Telegram main', () => {
|
|
||||||
// Start with WhatsApp as main, Telegram as non-main
|
|
||||||
simulateRegister('whatsapp_main', true);
|
simulateRegister('whatsapp_main', true);
|
||||||
simulateRegister('telegram_control', false);
|
simulateRegister('discord_main', false);
|
||||||
|
const discordPath = path.join(groupsDir, 'discord_main', 'CLAUDE.md');
|
||||||
|
fs.writeFileSync(
|
||||||
|
discordPath,
|
||||||
|
'# Gambi HQ — Research Assistant\n\nResearch workflows for Laura and Ethan.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Discord becomes main too — custom content must survive
|
||||||
|
simulateRegister('discord_main', true);
|
||||||
|
expect(readGroupMd('discord_main')).toContain('Research Assistant');
|
||||||
|
// WhatsApp main also untouched
|
||||||
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
|
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
|
||||||
expect(readGroupMd('telegram_control')).not.toContain('Admin Context');
|
|
||||||
|
|
||||||
// User decides Telegram should be the new main
|
|
||||||
simulateRegister('telegram_control', true);
|
|
||||||
expect(readGroupMd('telegram_control')).toContain('Admin Context');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('promotion updates assistant name in promoted file', () => {
|
|
||||||
// Register as non-main with default name
|
|
||||||
simulateRegister('slack_ops', false);
|
|
||||||
expect(readGroupMd('slack_ops')).toContain('You are Andy');
|
|
||||||
|
|
||||||
// Promote to main with custom name
|
|
||||||
simulateRegister('slack_ops', true, 'Nova');
|
|
||||||
const content = readGroupMd('slack_ops');
|
|
||||||
expect(content).toContain('Admin Context');
|
|
||||||
expect(content).toContain('You are Nova');
|
|
||||||
expect(content).not.toContain('Andy');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles missing templates gracefully', () => {
|
it('handles missing templates gracefully', () => {
|
||||||
|
|||||||
@@ -116,38 +116,26 @@ export async function run(args: string[]): Promise<void> {
|
|||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create or upgrade CLAUDE.md in the group folder from the appropriate template.
|
// Create CLAUDE.md in the new group folder from template if it doesn't exist.
|
||||||
// The agent runs with CWD=/workspace/group and loads CLAUDE.md from there.
|
// The agent runs with CWD=/workspace/group and loads CLAUDE.md from there.
|
||||||
|
// Never overwrite an existing CLAUDE.md — users customize these extensively
|
||||||
|
// (persona, workspace structure, communication rules, family context, etc.)
|
||||||
|
// and a stock template replacement would destroy that work.
|
||||||
const groupClaudeMdPath = path.join(
|
const groupClaudeMdPath = path.join(
|
||||||
projectRoot,
|
projectRoot,
|
||||||
'groups',
|
'groups',
|
||||||
parsed.folder,
|
parsed.folder,
|
||||||
'CLAUDE.md',
|
'CLAUDE.md',
|
||||||
);
|
);
|
||||||
const mainTemplatePath = path.join(projectRoot, 'groups', 'main', 'CLAUDE.md');
|
if (!fs.existsSync(groupClaudeMdPath)) {
|
||||||
const globalTemplatePath = path.join(projectRoot, 'groups', 'global', 'CLAUDE.md');
|
const templatePath = parsed.isMain
|
||||||
const templatePath = parsed.isMain ? mainTemplatePath : globalTemplatePath;
|
? path.join(projectRoot, 'groups', 'main', 'CLAUDE.md')
|
||||||
const fileExists = fs.existsSync(groupClaudeMdPath);
|
: path.join(projectRoot, 'groups', 'global', 'CLAUDE.md');
|
||||||
|
|
||||||
// Promotion case: group was registered as non-main (got global template)
|
|
||||||
// and is now being re-registered as main. Replace with main template.
|
|
||||||
const needsPromotion =
|
|
||||||
parsed.isMain &&
|
|
||||||
fileExists &&
|
|
||||||
!fs.readFileSync(groupClaudeMdPath, 'utf-8').includes('## Admin Context');
|
|
||||||
|
|
||||||
if (!fileExists || needsPromotion) {
|
|
||||||
if (fs.existsSync(templatePath)) {
|
if (fs.existsSync(templatePath)) {
|
||||||
fs.copyFileSync(templatePath, groupClaudeMdPath);
|
fs.copyFileSync(templatePath, groupClaudeMdPath);
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{ file: groupClaudeMdPath, template: templatePath },
|
||||||
file: groupClaudeMdPath,
|
'Created CLAUDE.md from template',
|
||||||
template: templatePath,
|
|
||||||
promoted: needsPromotion,
|
|
||||||
},
|
|
||||||
needsPromotion
|
|
||||||
? 'Promoted CLAUDE.md to main template'
|
|
||||||
: 'Created CLAUDE.md from template',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user