fix: promote CLAUDE.md to main template when group becomes main

When a non-main group is re-registered with --is-main, the existing
CLAUDE.md (copied from global template) lacked admin context. Now
register.ts detects this promotion case and replaces it with the main
template. Files that already contain "## Admin Context" are preserved.

Adds tests for:
- promoting non-main to main upgrades the template
- cross-channel promotion (e.g. Telegram non-main → main)
- promotion with custom assistant name
- re-registration preserves user-modified main CLAUDE.md
- re-registration preserves user-modified non-main CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
glifocat
2026-03-24 12:44:24 +01:00
parent 07dc8c977c
commit 3207c35e50
2 changed files with 88 additions and 20 deletions

View File

@@ -272,18 +272,24 @@ 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 (register.ts lines 119-138) // Template copy + promotion (register.ts lines 119-148)
const dest = path.join(folderDir, 'CLAUDE.md'); const dest = path.join(folderDir, 'CLAUDE.md');
if (!fs.existsSync(dest)) { const templatePath = isMain
const templatePath = isMain ? path.join(groupsDir, 'main', 'CLAUDE.md')
? path.join(groupsDir, 'main', 'CLAUDE.md') : path.join(groupsDir, 'global', 'CLAUDE.md');
: path.join(groupsDir, 'global', 'CLAUDE.md'); const fileExists = fs.existsSync(dest);
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 140-165) // Name update across all groups (register.ts lines 150-175)
if (assistantName !== 'Andy') { if (assistantName !== 'Andy') {
const mdFiles = fs const mdFiles = fs
.readdirSync(groupsDir) .readdirSync(groupsDir)
@@ -401,19 +407,66 @@ describe('CLAUDE.md template copy', () => {
} }
}); });
it('does not overwrite user-modified CLAUDE.md', () => { it('does not overwrite main CLAUDE.md that already has admin context', () => {
simulateRegister('slack_main', true); simulateRegister('slack_main', true);
// User customizes the file // User appends custom content to the main template
fs.writeFileSync( const mdPath = path.join(groupsDir, 'slack_main', 'CLAUDE.md');
path.join(groupsDir, 'slack_main', 'CLAUDE.md'), fs.appendFileSync(mdPath, '\n\n## My Custom Section\n\nUser notes here.');
'# Custom\n\nUser-modified content.',
);
// 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('Admin Context');
expect(content).toContain('My Custom Section');
});
it('does not overwrite non-main CLAUDE.md on re-registration', () => {
simulateRegister('telegram_friends', false);
// User customizes the file
const mdPath = path.join(groupsDir, 'telegram_friends', 'CLAUDE.md');
fs.writeFileSync(mdPath, '# Custom\n\nUser-modified content.');
// Re-registering same folder as non-main
simulateRegister('telegram_friends', false);
const content = readGroupMd('telegram_friends');
expect(content).toContain('User-modified content'); expect(content).toContain('User-modified content');
expect(content).not.toContain('Admin Context'); });
it('promotes non-main group to main when re-registered with isMain', () => {
// Initially registered as non-main (gets global template)
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('telegram_control', false);
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', () => {

View File

@@ -116,7 +116,7 @@ export async function run(args: string[]): Promise<void> {
recursive: true, recursive: true,
}); });
// Create CLAUDE.md in the new group folder from template if it doesn't exist. // Create or upgrade CLAUDE.md in the group folder from the appropriate template.
// 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.
const groupClaudeMdPath = path.join( const groupClaudeMdPath = path.join(
projectRoot, projectRoot,
@@ -124,15 +124,30 @@ export async function run(args: string[]): Promise<void> {
parsed.folder, parsed.folder,
'CLAUDE.md', 'CLAUDE.md',
); );
if (!fs.existsSync(groupClaudeMdPath)) { const mainTemplatePath = path.join(projectRoot, 'groups', 'main', 'CLAUDE.md');
const templatePath = parsed.isMain const globalTemplatePath = path.join(projectRoot, 'groups', 'global', 'CLAUDE.md');
? path.join(projectRoot, 'groups', 'main', 'CLAUDE.md') const templatePath = parsed.isMain ? mainTemplatePath : globalTemplatePath;
: path.join(projectRoot, 'groups', 'global', 'CLAUDE.md'); const fileExists = fs.existsSync(groupClaudeMdPath);
// 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 }, {
'Created CLAUDE.md from template', file: groupClaudeMdPath,
template: templatePath,
promoted: needsPromotion,
},
needsPromotion
? 'Promoted CLAUDE.md to main template'
: 'Created CLAUDE.md from template',
); );
} }
} }