Skip to main content

scripts-update-component-activation

#!/usr/bin/env python3 """​

title: "Update Component Activation" component_type: script version: "1.0.0" audience: contributor status: stable summary: "Update Component Activation Status" keywords: ['activation', 'api', 'automation', 'component', 'git'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "update-component-activation.py" language: python executable: true usage: "python3 scripts/update-component-activation.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false​

Update Component Activation Status

Manages component activation/deactivation with git-tracked state changes. Implements atomic updates, timestamp tracking, and reason documentation.

Part of Phase 5.3: Activation Management Created: 2025-11-29

Usage: # Activate a component python3 scripts/update-component-activation.py activate agent codi-documentation-writer --reason "Testing activation"

# Deactivate a component
python3 scripts/update-component-activation.py deactivate skill git-workflow-automation --reason "Temporary disable"

# Check status
python3 scripts/update-component-activation.py status agent orchestrator

# List all activated components
python3 scripts/update-component-activation.py list --activated-only

# Dry run (no changes)
python3 scripts/update-component-activation.py activate agent test-agent --dry-run

"""

import sys import json import argparse import subprocess from pathlib import Path from datetime import datetime, timezone from typing import Dict, Any, Optional, List, Tuple

class ComponentActivationManager: """Manages component activation state with atomic updates and git tracking"""

def __init__(self, framework_root: Path, dry_run: bool = False):
"""
Initialize activation manager

Args:
framework_root: Root directory of CODITECT framework
dry_run: If True, don't make actual changes
"""
self.framework_root = framework_root
self.dry_run = dry_run

# Check multiple locations for activation status file
# Priority: .coditect/ (canonical) > config/ (fallback)
possible_paths = [
framework_root / ".coditect" / "component-activation-status.json",
framework_root / "config" / "component-activation-status.json",
]

self.status_file = None
for path in possible_paths:
if path.exists():
self.status_file = path
break

if self.status_file is None:
self.status_file = possible_paths[0] # Default to .coditect/ for creation

def load_status(self) -> Dict[str, Any]:
"""Load current activation status"""
if not self.status_file.exists():
print(f"❌ Error: Activation status file not found")
print(f" Checked locations:")
print(f" - {self.framework_root / '.coditect' / 'component-activation-status.json'}")
print(f" - {self.framework_root / 'config' / 'component-activation-status.json'}")
print(f" Run: python3 scripts/generate-activation-status.py")
sys.exit(1)

with open(self.status_file, 'r') as f:
return json.load(f)

def save_status(self, status: Dict[str, Any]) -> None:
"""
Save activation status with atomic write to both canonical and fallback locations.

Writes to:
- .coditect/component-activation-status.json (canonical)
- config/component-activation-status.json (fallback, kept in sync)

Args:
status: Complete activation status dict
"""
if self.dry_run:
print(f"[DRY RUN] Would save to: {self.status_file}")
return

# Update last_updated timestamp
status['last_updated'] = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')

# Both locations to keep in sync
locations = [
self.framework_root / ".coditect" / "component-activation-status.json",
self.framework_root / "config" / "component-activation-status.json",
]

try:
# Validate JSON before writing
json_str = json.dumps(status, indent=2)
json.loads(json_str) # Validate it's valid JSON

# Write to all locations
for target_file in locations:
if target_file.parent.exists():
temp_file = target_file.with_suffix('.tmp')
# Write to temp file
with open(temp_file, 'w') as f:
f.write(json_str)
f.write('\n')
# Atomic rename
temp_file.rename(target_file)

print(f"βœ… Activation status updated: {self.status_file}")
print(f" (synced to both .coditect/ and config/)")

except Exception as e:
# Cleanup temp files on error
for target_file in locations:
temp_file = target_file.with_suffix('.tmp')
if temp_file.exists():
temp_file.unlink()
raise Exception(f"Failed to save activation status: {e}")

def find_component(self, status: Dict[str, Any], component_type: str,
component_name: str) -> Tuple[Optional[Dict], Optional[int]]:
"""
Find component in activation status

Returns:
(component_dict, index) or (None, None) if not found
"""
for idx, component in enumerate(status.get('components', [])):
if (component.get('type') == component_type and
component.get('name') == component_name):
return component, idx

return None, None

def discover_component_path(self, component_type: str, component_name: str) -> Optional[Path]:
"""
Discover component file path from filesystem.

Args:
component_type: Type of component (agent, skill, command, hook, script, prompt)
component_name: Name of component

Returns:
Path to component file if found, None otherwise
"""
# Map component types to their directory and file patterns
type_mappings = {
'agent': ('agents', f'{component_name}.md'),
'command': ('commands', f'{component_name}.md'),
'skill': ('skills', f'{component_name}/SKILL.md'),
'hook': ('hooks', f'{component_name}.md'),
'script': ('scripts', f'{component_name}.py'),
'prompt': ('config/system-prompts', f'{component_name}.txt'),
'workflow': ('workflows', f'{component_name}.yaml'), # NEW: Executable workflow definitions
}

if component_type not in type_mappings:
return None

directory, filename = type_mappings[component_type]
component_path = self.framework_root / directory / filename

# For skills, also check alternate pattern (direct SKILL.md)
if component_type == 'skill' and not component_path.exists():
alt_path = self.framework_root / 'skills' / component_name / 'SKILL.md'
if alt_path.exists():
return alt_path

if component_path.exists():
return component_path

return None

def add_new_component(self, status: Dict[str, Any], component_type: str,
component_name: str, component_path: Path) -> int:
"""
Add a newly discovered component to the activation status.

Args:
status: Current status dict (modified in place)
component_type: Type of component
component_name: Name of component
component_path: Path to component file

Returns:
Index of newly added component
"""
# Create new component entry
new_component = {
"type": component_type,
"name": component_name,
"path": str(component_path.relative_to(self.framework_root)),
"activated": False,
"version": "1.0.0",
"status": "pending",
"reason": "Newly discovered - pending activation",
"discovered_at": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
}

# Add to components list
status['components'].append(new_component)

# Update summary
status['activation_summary']['total_components'] += 1
status['activation_summary']['deactivated'] += 1

# Return index of new component
return len(status['components']) - 1

def activate_component(self, component_type: str, component_name: str,
reason: str = "Manual activation") -> bool:
"""
Activate a component

Args:
component_type: Type of component (agent, skill, command, etc.)
component_name: Name of component
reason: Reason for activation

Returns:
True if successful, False otherwise
"""
print()
print("=" * 60)
print(f"πŸ”„ Activating Component")
print("=" * 60)
print(f"Type: {component_type}")
print(f"Name: {component_name}")
print(f"Reason: {reason}")
print()

# Load current status
status = self.load_status()

# Find component
component, idx = self.find_component(status, component_type, component_name)

if component is None:
# Try to discover the component from filesystem
print(f"⚠️ Component not in registry, checking filesystem...")
component_path = self.discover_component_path(component_type, component_name)

if component_path is None:
print(f"❌ Component not found: {component_type}/{component_name}")
print(f" Not in registry and no file found at expected location")
print(f" Available types: agent, skill, command, script, hook, prompt")
print()
print(f" Expected locations:")
print(f" - agents/{component_name}.md")
print(f" - commands/{component_name}.md")
print(f" - skills/{component_name}/SKILL.md")
print(f" - hooks/{component_name}.md")
print(f" - scripts/{component_name}.py")
return False

# Found on filesystem - add to registry
print(f"✨ Found component file: {component_path.relative_to(self.framework_root)}")
print(f" Adding to activation registry...")
idx = self.add_new_component(status, component_type, component_name, component_path)
component = status['components'][idx]

# Check if already activated
if component.get('activated', False):
print(f"⚠️ Component already activated")
print(f" Activated at: {component.get('activated_at', 'unknown')}")
print(f" Reason: {component.get('reason', 'unknown')}")
return True

# Update component
now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
status['components'][idx]['activated'] = True
status['components'][idx]['activated_at'] = now
status['components'][idx]['reason'] = reason

# Update summary counts
status['activation_summary']['activated'] += 1
status['activation_summary']['deactivated'] -= 1

# Save changes
self.save_status(status)

print(f"βœ… Component activated successfully")
print(f" Path: {component.get('path', 'unknown')}")
print(f" Activated at: {now}")
print()

return True

def deactivate_component(self, component_type: str, component_name: str,
reason: str = "Manual deactivation") -> bool:
"""
Deactivate a component

Args:
component_type: Type of component
component_name: Name of component
reason: Reason for deactivation

Returns:
True if successful, False otherwise
"""
print()
print("=" * 60)
print(f"πŸ”„ Deactivating Component")
print("=" * 60)
print(f"Type: {component_type}")
print(f"Name: {component_name}")
print(f"Reason: {reason}")
print()

# Load current status
status = self.load_status()

# Find component
component, idx = self.find_component(status, component_type, component_name)

if component is None:
print(f"❌ Component not found: {component_type}/{component_name}")
return False

# Check if already deactivated
if not component.get('activated', False):
print(f"⚠️ Component already deactivated")
return True

# Update component
now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
status['components'][idx]['activated'] = False
status['components'][idx]['deactivated_at'] = now
status['components'][idx]['reason'] = reason

# Remove activated_at timestamp
if 'activated_at' in status['components'][idx]:
del status['components'][idx]['activated_at']

# Update summary counts
status['activation_summary']['activated'] -= 1
status['activation_summary']['deactivated'] += 1

# Save changes
self.save_status(status)

print(f"βœ… Component deactivated successfully")
print(f" Path: {component.get('path', 'unknown')}")
print(f" Deactivated at: {now}")
print()

return True

def get_status(self, component_type: str, component_name: str) -> Optional[Dict]:
"""Get status of a specific component"""
status = self.load_status()
component, _ = self.find_component(status, component_type, component_name)
return component

def list_components(self, activated_only: bool = False,
component_type: Optional[str] = None) -> List[Dict]:
"""
List components with optional filtering

Args:
activated_only: If True, only show activated components
component_type: If specified, filter by component type

Returns:
List of component dicts
"""
status = self.load_status()
components = status.get('components', [])

# Filter by activation status
if activated_only:
components = [c for c in components if c.get('activated', False)]

# Filter by type
if component_type:
components = [c for c in components if c.get('type') == component_type]

return components

def sync_components(self, activate_new: bool = False) -> Dict[str, int]:
"""
Discover all components from filesystem and sync with registry.

Args:
activate_new: If True, also activate newly discovered components

Returns:
Dict with counts: {'discovered': n, 'added': n, 'activated': n}
"""
print()
print("=" * 60)
print("πŸ”„ Syncing Components from Filesystem")
print("=" * 60)
print()

status = self.load_status()
existing_names = {(c['type'], c['name']) for c in status.get('components', [])}

# Discover components from filesystem
discoveries = {
'agent': [],
'command': [],
'skill': [],
'hook': [],
'script': [],
'prompt': [],
}

# Scan agents/
agents_dir = self.framework_root / 'agents'
if agents_dir.exists():
for f in agents_dir.glob('*.md'):
if f.stem not in ['README', 'AGENT-INDEX']:
discoveries['agent'].append((f.stem, f))

# Scan commands/
commands_dir = self.framework_root / 'commands'
if commands_dir.exists():
for f in commands_dir.glob('*.md'):
if f.stem not in ['README', 'COMMAND-GUIDE']:
discoveries['command'].append((f.stem, f))

# Scan skills/
skills_dir = self.framework_root / 'skills'
if skills_dir.exists():
for d in skills_dir.iterdir():
if d.is_dir() and (d / 'SKILL.md').exists():
discoveries['skill'].append((d.name, d / 'SKILL.md'))

# Scan hooks/
hooks_dir = self.framework_root / 'hooks'
if hooks_dir.exists():
for f in hooks_dir.glob('*.md'):
if f.stem not in ['README', 'HOOKS-INDEX', 'PHASE2-3-ADVANCED-HOOKS']:
discoveries['hook'].append((f.stem, f))

# Scan scripts/
scripts_dir = self.framework_root / 'scripts'
if scripts_dir.exists():
for f in scripts_dir.glob('*.py'):
if f.stem not in ['__init__', 'README']:
discoveries['script'].append((f.stem, f))

# Scan prompts/
prompts_dir = self.framework_root / 'config' / 'system-prompts'
if prompts_dir.exists():
for f in prompts_dir.glob('*.txt'):
discoveries['prompt'].append((f.stem, f))

# Find new components not in registry
added = []
for component_type, items in discoveries.items():
for name, path in items:
if (component_type, name) not in existing_names:
idx = self.add_new_component(status, component_type, name, path)
added.append((component_type, name, path))
print(f" ✨ Added: {component_type}/{name}")

if activate_new:
# Activate immediately
now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
status['components'][idx]['activated'] = True
status['components'][idx]['activated_at'] = now
status['components'][idx]['status'] = 'operational'
status['components'][idx]['reason'] = 'Auto-activated during sync'
# Adjust counts (add_new_component incremented deactivated)
status['activation_summary']['activated'] += 1
status['activation_summary']['deactivated'] -= 1

# Calculate totals
total_discovered = sum(len(items) for items in discoveries.values())

print()
print(f"Filesystem scan complete:")
for ctype, items in discoveries.items():
print(f" {ctype}s: {len(items)}")

print()
if added:
print(f"βœ… Added {len(added)} new components to registry")
if activate_new:
print(f"βœ… Activated {len(added)} new components")

# Save changes
self.save_status(status)
else:
print(f"βœ… Registry is up to date - no new components found")

return {
'discovered': total_discovered,
'added': len(added),
'activated': len(added) if activate_new else 0
}

def git_commit_changes(self, message: str) -> bool:
"""
Commit activation changes to git

Args:
message: Commit message

Returns:
True if successful, False otherwise
"""
if self.dry_run:
print(f"[DRY RUN] Would commit with message: {message}")
return True

try:
# Stage activation status file
subprocess.run(
['git', 'add', str(self.status_file.relative_to(self.framework_root))],
cwd=self.framework_root,
check=True,
capture_output=True
)

# Commit changes
full_message = f"{message}\n\nπŸ€– Generated with Claude Code\n\nCo-Authored-By: Claude <noreply@anthropic.com>"

subprocess.run(
['git', 'commit', '-m', full_message],
cwd=self.framework_root,
check=True,
capture_output=True
)

print(f"βœ… Changes committed to git")
return True

except subprocess.CalledProcessError as e:
print(f"⚠️ Git commit failed: {e}")
print(f" Changes saved but not committed")
return False

def print_component_status(component: Dict) -> None: """Print formatted component status""" print() print("=" * 60) print(f"Component: {component.get('name', 'unknown')}") print("=" * 60) print(f"Type: {component.get('type', 'unknown')}") print(f"Path: {component.get('path', 'unknown')}") print(f"Status: {'βœ… Activated' if component.get('activated', False) else '⏸️ Deactivated'}") print(f"Version: {component.get('version', 'unknown')}")

if component.get('activated', False):
print(f"Activated: {component.get('activated_at', 'unknown')}")
else:
print(f"Deactivated: {component.get('deactivated_at', 'unknown')}")

print(f"Reason: {component.get('reason', 'unknown')}")
print()

def main(): """Main entry point""" parser = argparse.ArgumentParser( description='CODITECT Component Activation Manager', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples:

Activate a component

%(prog)s activate agent orchestrator --reason "Needed for project planning"

Deactivate a component

%(prog)s deactivate skill old-skill --reason "Deprecated"

Check status

%(prog)s status agent orchestrator

List all activated components

%(prog)s list --activated-only

Sync filesystem - discover and register new components

%(prog)s sync

Sync and auto-activate all new components

%(prog)s sync --activate

Dry run (preview changes)

%(prog)s activate agent test-agent --dry-run """ )

subparsers = parser.add_subparsers(dest='command', help='Command to execute')

# Activate command
activate_parser = subparsers.add_parser('activate', help='Activate a component')
activate_parser.add_argument('type', help='Component type (agent, skill, command, etc.)')
activate_parser.add_argument('name', help='Component name')
activate_parser.add_argument('--reason', default='Manual activation', help='Reason for activation')
activate_parser.add_argument('--dry-run', action='store_true', help='Preview changes without saving')
activate_parser.add_argument('--no-commit', action='store_true', help='Skip git commit')

# Deactivate command
deactivate_parser = subparsers.add_parser('deactivate', help='Deactivate a component')
deactivate_parser.add_argument('type', help='Component type')
deactivate_parser.add_argument('name', help='Component name')
deactivate_parser.add_argument('--reason', default='Manual deactivation', help='Reason for deactivation')
deactivate_parser.add_argument('--dry-run', action='store_true', help='Preview changes without saving')
deactivate_parser.add_argument('--no-commit', action='store_true', help='Skip git commit')

# Status command
status_parser = subparsers.add_parser('status', help='Show component status')
status_parser.add_argument('type', help='Component type')
status_parser.add_argument('name', help='Component name')

# List command
list_parser = subparsers.add_parser('list', help='List components')
list_parser.add_argument('--activated-only', action='store_true', help='Only show activated components')
list_parser.add_argument('--type', help='Filter by component type')

# Sync command - discover and register new components from filesystem
sync_parser = subparsers.add_parser('sync', help='Sync registry with filesystem (discover new components)')
sync_parser.add_argument('--activate', action='store_true', help='Also activate newly discovered components')
sync_parser.add_argument('--dry-run', action='store_true', help='Preview changes without saving')
sync_parser.add_argument('--no-commit', action='store_true', help='Skip git commit')

args = parser.parse_args()

# Get framework root
framework_root = Path(__file__).parent.parent

# Create manager
dry_run = getattr(args, 'dry_run', False)
manager = ComponentActivationManager(framework_root, dry_run=dry_run)

# Execute command
if args.command == 'activate':
success = manager.activate_component(args.type, args.name, args.reason)

if success and not dry_run and not args.no_commit:
commit_msg = f"chore: Activate {args.type}/{args.name}\n\nReason: {args.reason}"
manager.git_commit_changes(commit_msg)

sys.exit(0 if success else 1)

elif args.command == 'deactivate':
success = manager.deactivate_component(args.type, args.name, args.reason)

if success and not dry_run and not args.no_commit:
commit_msg = f"chore: Deactivate {args.type}/{args.name}\n\nReason: {args.reason}"
manager.git_commit_changes(commit_msg)

sys.exit(0 if success else 1)

elif args.command == 'status':
component = manager.get_status(args.type, args.name)

if component:
print_component_status(component)
sys.exit(0)
else:
print(f"❌ Component not found: {args.type}/{args.name}")
sys.exit(1)

elif args.command == 'list':
components = manager.list_components(
activated_only=args.activated_only,
component_type=args.type
)

print()
print("=" * 60)
print(f"CODITECT Components ({len(components)} found)")
print("=" * 60)
print()

# Group by type
by_type = {}
for c in components:
ctype = c.get('type', 'unknown')
if ctype not in by_type:
by_type[ctype] = []
by_type[ctype].append(c)

for ctype, items in sorted(by_type.items()):
print(f"{ctype.upper()} ({len(items)}):")
for c in sorted(items, key=lambda x: x.get('name', '')):
status_icon = 'βœ…' if c.get('activated', False) else '⏸️ '
print(f" {status_icon} {c.get('name')}")
print()

sys.exit(0)

elif args.command == 'sync':
results = manager.sync_components(activate_new=args.activate)

if results['added'] > 0 and not dry_run and not args.no_commit:
action = "sync and activate" if args.activate else "sync"
commit_msg = f"chore: {action.capitalize()} {results['added']} new components\n\nDiscovered from filesystem scan"
manager.git_commit_changes(commit_msg)

sys.exit(0)

else:
parser.print_help()
sys.exit(1)

if name == 'main': main()