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:
parent
fa8bc86897
commit
506f532f88
3 changed files with 189 additions and 0 deletions
23
scripts/templates/goals/GOALS.md
Normal file
23
scripts/templates/goals/GOALS.md
Normal file
|
|
@ -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
|
||||||
40
scripts/templates/goals/README.md
Normal file
40
scripts/templates/goals/README.md
Normal file
|
|
@ -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}}`.
|
||||||
126
scripts/templates/goals/goal-tracker.sh
Normal file
126
scripts/templates/goals/goal-tracker.sh
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue