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:
Kjell Tore Guttormsen 2026-04-12 06:55:33 +02:00
commit 9d24dc5c41
3 changed files with 369 additions and 0 deletions

View file

@ -0,0 +1,28 @@
# Organization: {{ORG_NAME}}
## Agents
| Agent | Role | Reports To | Status | Budget |
|-------|------|------------|--------|--------|
| {{AGENT_1}} | {{ROLE_1}} | (board) | active | {{BUDGET_1}} |
| {{AGENT_2}} | {{ROLE_2}} | {{AGENT_1}} | active | {{BUDGET_2}} |
| {{AGENT_3}} | {{ROLE_3}} | {{AGENT_1}} | active | {{BUDGET_3}} |
## Delegation Rules
- Board (human) → top-level agents: task assignment, goal setting
- Manager agents → direct reports: task decomposition, delegation
- Cross-team requests: route through common ancestor in org chart
- Escalation: up the reporting chain to the first agent with authority
## Human Override
The human operator is the "board of directors" with override authority
on all decisions. Any agent can be paused, redirected, or terminated
by the human at any time.
## Notes
Use `(board)` as the Reports To value for top-level agents that report
directly to the human operator. All other agents must reference a valid
agent name in the Reports To column.

View file

@ -0,0 +1,62 @@
# Org Chart
File-based agent hierarchy using Paperclip's simple `reportsTo` pattern.
## Design decisions
- **Simple reportsTo FK, not recursive**: Each agent has a single `Reports To`
field referencing its parent by name. No recursive traversal at runtime.
- **Board = human operator**: Top-level agents use `(board)` to indicate they
report directly to the human. The human always has override authority.
- **Markdown table**: Human-editable, version-controlled, no service dependency.
## Delegation flow
```
(board) [human]
└─ orchestrator-agent (manager)
├─ research-agent (worker)
└─ writer-agent (worker)
```
Task assignment flows down the tree. Escalation flows up.
Cross-team requests are routed through the nearest common ancestor.
## Human override authority
The human operator ("board") has unconditional override authority:
- Any agent can be paused, redirected, or terminated at any time
- No agent can block a human instruction
- Governance gates apply to agents, not to the human operator
## Usage
```bash
# Show org tree
./org-manager.sh
# Validate chart (check agent files exist, no circular chains)
./org-manager.sh validate
# Add an agent
./org-manager.sh add writer-agent "Content Writer" orchestrator-agent
# Remove an agent (direct reports reassigned to parent)
./org-manager.sh remove writer-agent
# List all agents
./org-manager.sh list
```
## Validation checks
- All agent names must have a corresponding `.claude/agents/<name>.md` file
- All `Reports To` values must be valid agent names or `(board)`
- No circular reporting chains
## Paperclip comparison
Paperclip stores the org chart in a `agents` database table with a
`reportsTo` foreign key. This implementation uses a markdown table —
equivalent structure, no database required, suitable for file-based
agent systems running on a single machine.

View 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