From 506f532f88d9b9b9f971d9f668b542e17dd960ce Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 12 Apr 2026 06:51:38 +0200 Subject: [PATCH] feat(templates): add goal hierarchy tracker (Paperclip pattern) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session 4 step 15 — GOALS.md hierarchy (objectives > initiatives > tasks) and goal-tracker.sh for status/context/complete operations. Co-Authored-By: Claude Sonnet 4.6 --- scripts/templates/goals/GOALS.md | 23 +++++ scripts/templates/goals/README.md | 40 ++++++++ scripts/templates/goals/goal-tracker.sh | 126 ++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 scripts/templates/goals/GOALS.md create mode 100644 scripts/templates/goals/README.md create mode 100644 scripts/templates/goals/goal-tracker.sh diff --git a/scripts/templates/goals/GOALS.md b/scripts/templates/goals/GOALS.md new file mode 100644 index 0000000..926daf9 --- /dev/null +++ b/scripts/templates/goals/GOALS.md @@ -0,0 +1,23 @@ +# Goals: {{PROJECT_NAME}} + +## Company Goals +- [G1] {{COMPANY_GOAL_1}} +- [G2] {{COMPANY_GOAL_2}} + +## Project Goals +- [G1.1] {{PROJECT_GOAL_1}} (parent: G1) +- [G1.2] {{PROJECT_GOAL_2}} (parent: G1) +- [G2.1] {{PROJECT_GOAL_3}} (parent: G2) + +## Task Goals +- [G1.1.1] {{TASK_GOAL_1}} (parent: G1.1, owner: {{AGENT_NAME}}, status: active) +- [G1.1.2] {{TASK_GOAL_2}} (parent: G1.1, owner: {{AGENT_NAME}}, status: pending) + +## Notes + +Goal IDs use hierarchical dot notation. Each goal has: +- ID: unique identifier (e.g., G1.1.1) +- Description: what the goal is +- Parent: which goal this supports (simple parent reference, not recursive) +- Owner: which agent is responsible (task goals only) +- Status: active | pending | complete | blocked diff --git a/scripts/templates/goals/README.md b/scripts/templates/goals/README.md new file mode 100644 index 0000000..d721d73 --- /dev/null +++ b/scripts/templates/goals/README.md @@ -0,0 +1,40 @@ +# Goal Hierarchy + +File-based goal hierarchy inspired by Paperclip's goal system. + +## Design decisions + +- **Simple parent_id, not recursive**: Each goal references its parent by ID. + No recursive traversal at runtime — matching Paperclip's actual implementation + (not their aspirational docs which describe "full ancestry"). +- **Dot notation for hierarchy**: G1 → G1.1 → G1.1.1 makes the hierarchy visible + in the ID itself. +- **File-based, not database**: Human-editable, version-controlled, no dependencies. + +## Goal levels + +| Level | ID pattern | Example | Description | +|-------|-----------|---------|-------------| +| Company | G1, G2 | G1 | Top-level organizational goals | +| Project | G1.1, G1.2 | G1.1 | Goals that support a company goal | +| Task | G1.1.1 | G1.1.1 | Specific tasks assigned to agents | + +## Usage + +```bash +# View goal summary +./goal-tracker.sh + +# Mark a task as complete +./goal-tracker.sh complete G1.1.1 + +# Generate context for heartbeat injection +./goal-tracker.sh context +``` + +## Integration with heartbeat + +The `context` command produces a summary suitable for injection into +the heartbeat context packet (see `scripts/templates/heartbeat/context-packet.md`). +The heartbeat runner can call `./goal-tracker.sh context` and inject +the output as `{{ACTIVE_GOALS}}`. diff --git a/scripts/templates/goals/goal-tracker.sh b/scripts/templates/goals/goal-tracker.sh new file mode 100644 index 0000000..da157a7 --- /dev/null +++ b/scripts/templates/goals/goal-tracker.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Goal tracker: parse and manage GOALS.md +# Bash 3.2 compatible. Uses python3 for parsing. +# +# Usage: +# ./goal-tracker.sh # Show goal summary +# ./goal-tracker.sh complete G1.1.1 # Mark goal as complete +# ./goal-tracker.sh status # Show status counts +# ./goal-tracker.sh context # Generate context for heartbeat injection +# +# Placeholders: +# {{WORKING_DIR}} - absolute path to project directory + +WORKING_DIR="{{WORKING_DIR}}" +GOALS_FILE="$WORKING_DIR/GOALS.md" +ACTION="${1:-summary}" +GOAL_ID="${2:-}" + +if [ ! -f "$GOALS_FILE" ]; then + echo "Error: $GOALS_FILE not found" + exit 1 +fi + +case "$ACTION" in + summary|status) + python3 << PYEOF +import re + +goals = [] +with open("$GOALS_FILE") as f: + for line in f: + m = re.match(r'-\s*\[(\S+)\]\s+(.+)', line.strip()) + if m: + gid = m.group(1) + rest = m.group(2) + status_m = re.search(r'status:\s*(\w+)', rest) + parent_m = re.search(r'parent:\s*(\S+)', rest) + owner_m = re.search(r'owner:\s*(\S+)', rest) + status = status_m.group(1) if status_m else 'active' + parent = parent_m.group(1).rstrip(',)') if parent_m else None + owner = owner_m.group(1).rstrip(',)') if owner_m else None + desc = re.sub(r'\(.*\)', '', rest).strip() + goals.append({'id': gid, 'desc': desc, 'status': status, 'parent': parent, 'owner': owner}) + +# Status counts +counts = {} +for g in goals: + counts[g['status']] = counts.get(g['status'], 0) + 1 + +print("Goal Status Summary") +print("=" * 40) +for status, count in sorted(counts.items()): + print(f" {status}: {count}") +print(f" Total: {len(goals)}") + +# Check for orphans +all_ids = set(g['id'] for g in goals) +orphans = [g for g in goals if g['parent'] and g['parent'] not in all_ids] +if orphans: + print(f"\nOrphaned goals (parent not found):") + for g in orphans: + print(f" [{g['id']}] parent: {g['parent']}") + +# Goals without owners at task level +unowned = [g for g in goals if '.' in g['id'] and g['id'].count('.') >= 2 and not g['owner']] +if unowned: + print(f"\nTask goals without owners:") + for g in unowned: + print(f" [{g['id']}] {g['desc']}") +PYEOF + ;; + + complete) + if [ -z "$GOAL_ID" ]; then + echo "Usage: $0 complete " + exit 1 + fi + python3 -c " +import re +goal_id = '$GOAL_ID' +with open('$GOALS_FILE') as f: + content = f.read() +# Replace status for the specific goal +pattern = r'(\[' + re.escape(goal_id) + r'\][^)]*status:\s*)\w+' +if re.search(pattern, content): + content = re.sub(pattern, r'\1complete', content) + with open('$GOALS_FILE', 'w') as f: + f.write(content) + print(f'Goal {goal_id} marked as complete') +else: + print(f'Goal {goal_id} not found or has no status field') +" + ;; + + context) + # Generate a goal summary for heartbeat context injection + python3 << PYEOF +import re + +goals = [] +with open("$GOALS_FILE") as f: + for line in f: + m = re.match(r'-\s*\[(\S+)\]\s+(.+)', line.strip()) + if m: + gid = m.group(1) + rest = m.group(2) + status_m = re.search(r'status:\s*(\w+)', rest) + status = status_m.group(1) if status_m else 'active' + desc = re.sub(r'\(.*\)', '', rest).strip() + goals.append({'id': gid, 'desc': desc, 'status': status}) + +active = [g for g in goals if g['status'] == 'active'] +if active: + print("Active goals:") + for g in active: + print(f" [{g['id']}] {g['desc']}") +else: + print("No active goals.") +PYEOF + ;; + + *) + echo "Usage: $0 [summary|complete |status|context]" + exit 1 + ;; +esac