feat(templates): add goal hierarchy tracker (Paperclip pattern)

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 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-12 06:51:38 +02:00
commit 506f532f88
3 changed files with 189 additions and 0 deletions

View file

@ -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 <goal-id>"
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 <id>|status|context]"
exit 1
;;
esac