agent-builder/scripts/templates/org-chart/org-manager.sh
Kjell Tore Guttormsen 9d24dc5c41 feat(templates): add org-chart template (Paperclip pattern)
Session 4 step 18 — agent role/reports-to hierarchy with tree/validate/
add/remove/list commands, circular chain detection via python3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 06:55:33 +02:00

279 lines
7.6 KiB
Bash

#!/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 <agent> "<role>" <reports-to> # Add agent
# ./org-manager.sh remove <agent> # 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 <agent-name> \"<role>\" <reports-to>"
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 <agent-name>"
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 <agent> <role> <reports-to>|remove <agent>|list]"
exit 1
;;
esac