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>
This commit is contained in:
parent
912689f3c5
commit
9d24dc5c41
3 changed files with 369 additions and 0 deletions
279
scripts/templates/org-chart/org-manager.sh
Normal file
279
scripts/templates/org-chart/org-manager.sh
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
#!/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
|
||||
Loading…
Add table
Add a link
Reference in a new issue