From 9d24dc5c4173241835f2c0cc21ab6f654b0fbb72 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 12 Apr 2026 06:55:33 +0200 Subject: [PATCH] feat(templates): add org-chart template (Paperclip pattern) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/templates/org-chart/ORG-CHART.md | 28 +++ scripts/templates/org-chart/README.md | 62 +++++ scripts/templates/org-chart/org-manager.sh | 279 +++++++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 scripts/templates/org-chart/ORG-CHART.md create mode 100644 scripts/templates/org-chart/README.md create mode 100644 scripts/templates/org-chart/org-manager.sh diff --git a/scripts/templates/org-chart/ORG-CHART.md b/scripts/templates/org-chart/ORG-CHART.md new file mode 100644 index 0000000..1b58630 --- /dev/null +++ b/scripts/templates/org-chart/ORG-CHART.md @@ -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. diff --git a/scripts/templates/org-chart/README.md b/scripts/templates/org-chart/README.md new file mode 100644 index 0000000..f081095 --- /dev/null +++ b/scripts/templates/org-chart/README.md @@ -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/.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. diff --git a/scripts/templates/org-chart/org-manager.sh b/scripts/templates/org-chart/org-manager.sh new file mode 100644 index 0000000..d20bb49 --- /dev/null +++ b/scripts/templates/org-chart/org-manager.sh @@ -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 "" # 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