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
28
scripts/templates/org-chart/ORG-CHART.md
Normal file
28
scripts/templates/org-chart/ORG-CHART.md
Normal 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.
|
||||
62
scripts/templates/org-chart/README.md
Normal file
62
scripts/templates/org-chart/README.md
Normal 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.
|
||||
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