#!/bin/bash # Org chart manager: parse, validate, and visualize ORG-CHART.md. # Bash 3.2 compatible. Uses python3 for table parsing. # # Usage: # ./org-manager.sh # Show org tree # ./org-manager.sh validate # Validate chart integrity # ./org-manager.sh add "" # Add agent # ./org-manager.sh remove # Remove agent (reassigns reports) # ./org-manager.sh list # List all agents # # Placeholders: # {{WORKING_DIR}} - absolute path to project directory WORKING_DIR="{{WORKING_DIR}}" ORG_FILE="$WORKING_DIR/ORG-CHART.md" AGENTS_DIR="$WORKING_DIR/.claude/agents" ACTION="${1:-tree}" ARG1="${2:-}" ARG2="${3:-}" ARG3="${4:-}" if [ ! -f "$ORG_FILE" ]; then echo "Error: $ORG_FILE not found" exit 1 fi parse_org_table() { python3 -c " import re, sys, os org_file = '$ORG_FILE' rows = [] in_table = False with open(org_file) as f: for line in f: line = line.rstrip() if re.match(r'\|\s*Agent\s*\|', line): in_table = True continue if in_table and re.match(r'\|[-| ]+\|', line): continue if in_table and line.startswith('|'): parts = [p.strip() for p in line.strip('|').split('|')] if len(parts) >= 4: rows.append({ 'agent': parts[0], 'role': parts[1], 'reports_to': parts[2], 'status': parts[3], 'budget': parts[4] if len(parts) > 4 else '' }) elif in_table and not line.startswith('|'): in_table = False for r in rows: print(r['agent'] + '|' + r['role'] + '|' + r['reports_to'] + '|' + r['status'] + '|' + r['budget']) " } case "$ACTION" in tree) python3 -c " import re, sys, os org_file = '$ORG_FILE' rows = [] in_table = False with open(org_file) as f: for line in f: line = line.rstrip() if re.match(r'\|\s*Agent\s*\|', line): in_table = True continue if in_table and re.match(r'\|[-| ]+\|', line): continue if in_table and line.startswith('|'): parts = [p.strip() for p in line.strip('|').split('|')] if len(parts) >= 4: rows.append({ 'agent': parts[0], 'role': parts[1], 'reports_to': parts[2], 'status': parts[3] }) elif in_table and not line.startswith('|'): in_table = False # Build parent → children map children = {} for r in rows: parent = r['reports_to'] if parent not in children: children[parent] = [] children[parent].append(r) def print_tree(parent, indent): kids = children.get(parent, []) for k in kids: status_mark = '' if k['status'] == 'active' else ' [' + k['status'] + ']' print(indent + '- ' + k['agent'] + ' (' + k['role'] + ')' + status_mark) print_tree(k['agent'], indent + ' ') print('Organization Tree') print('=' * 40) print('(board) [human operator]') print_tree('(board)', '') " ;; validate) python3 -c " import re, sys, os org_file = '$ORG_FILE' agents_dir = '$AGENTS_DIR' rows = [] in_table = False with open(org_file) as f: for line in f: line = line.rstrip() if re.match(r'\|\s*Agent\s*\|', line): in_table = True continue if in_table and re.match(r'\|[-| ]+\|', line): continue if in_table and line.startswith('|'): parts = [p.strip() for p in line.strip('|').split('|')] if len(parts) >= 4: rows.append({ 'agent': parts[0], 'role': parts[1], 'reports_to': parts[2], 'status': parts[3] }) elif in_table and not line.startswith('|'): in_table = False errors = [] all_agents = set(r['agent'] for r in rows) # Check agent files exist for r in rows: agent_file = os.path.join(agents_dir, r['agent'] + '.md') if not os.path.exists(agent_file): errors.append('Missing agent file: .claude/agents/' + r['agent'] + '.md') # Check reports_to references are valid for r in rows: if r['reports_to'] not in all_agents and r['reports_to'] != '(board)': errors.append('Unknown reports_to: ' + r['agent'] + ' → ' + r['reports_to']) # Check for circular chains (simple: follow chain, max 50 steps) for r in rows: visited = set() current = r['reports_to'] while current and current != '(board)': if current in visited: errors.append('Circular chain detected involving: ' + r['agent']) break visited.add(current) parent_rows = [x for x in rows if x['agent'] == current] if not parent_rows: break current = parent_rows[0]['reports_to'] if errors: print('VALIDATION ERRORS:') for e in errors: print(' ' + e) sys.exit(1) else: print('Validation OK: ' + str(len(rows)) + ' agents, no issues found.') " ;; add) if [ -z "$ARG1" ] || [ -z "$ARG2" ] || [ -z "$ARG3" ]; then echo "Usage: $0 add \"\" " exit 1 fi AGENT_NAME="$ARG1" ROLE="$ARG2" REPORTS_TO="$ARG3" python3 -c " import re org_file = '$ORG_FILE' agent = '$AGENT_NAME' role = '$ROLE' reports_to = '$REPORTS_TO' with open(org_file) as f: content = f.read() new_row = '| ' + agent + ' | ' + role + ' | ' + reports_to + ' | active | |' # Insert before the first blank line after the table lines = content.split('\n') in_table = False insert_at = None for i, line in enumerate(lines): if re.match(r'\|\s*Agent\s*\|', line): in_table = True elif in_table and line.startswith('|'): insert_at = i + 1 elif in_table and not line.startswith('|'): break if insert_at is not None: lines.insert(insert_at, new_row) with open(org_file, 'w') as f: f.write('\n'.join(lines)) print('Added: ' + agent + ' (' + role + ') → ' + reports_to) else: print('Error: Could not find table in ' + org_file) exit(1) " ;; remove) if [ -z "$ARG1" ]; then echo "Usage: $0 remove " exit 1 fi AGENT_NAME="$ARG1" python3 -c " import re org_file = '$ORG_FILE' agent = '$AGENT_NAME' with open(org_file) as f: lines = f.readlines() # Find the agent row and its parent agent_parent = None new_lines = [] removed = False for line in lines: if line.startswith('|'): parts = [p.strip() for p in line.strip('|').split('|')] if len(parts) >= 3 and parts[0] == agent: agent_parent = parts[2] removed = True continue # skip this row # Reassign direct reports to agent's parent if len(parts) >= 3 and parts[2] == agent and agent_parent: parts[2] = agent_parent line = '| ' + ' | '.join(parts) + ' |\n' new_lines.append(line) if removed: with open(org_file, 'w') as f: f.writelines(new_lines) print('Removed: ' + agent + ' (reports reassigned to ' + str(agent_parent) + ')') else: print('Agent not found: ' + agent) exit(1) " ;; list) parse_org_table | python3 -c " import sys print('Agent | Role | Reports To | Status') print('-' * 60) for line in sys.stdin: parts = line.strip().split('|') if len(parts) >= 4: print(parts[0] + ' | ' + parts[1] + ' | ' + parts[2] + ' | ' + parts[3]) " ;; *) echo "Usage: $0 [tree|validate|add |remove |list]" exit 1 ;; esac