diff --git a/data/goals.json b/data/goals.json
new file mode 100644
index 0000000..1775da4
--- /dev/null
+++ b/data/goals.json
@@ -0,0 +1,42 @@
+{
+ "goals": [
+ {
+ "id": 1,
+ "title": "Build out the sandbox into a daily driver",
+ "description": "",
+ "deadline": null,
+ "created": "2026-01-30T23:44:53.121855",
+ "status": "active",
+ "milestones": [
+ {
+ "id": 1,
+ "title": "Core tools built",
+ "done": true,
+ "created": "2026-01-30T23:44:53.147088",
+ "completed": "2026-01-30T23:44:53.248604"
+ },
+ {
+ "id": 2,
+ "title": "Projects created",
+ "done": true,
+ "created": "2026-01-30T23:44:53.172153",
+ "completed": "2026-01-30T23:44:53.273691"
+ },
+ {
+ "id": 3,
+ "title": "Git/Gitea integration",
+ "done": true,
+ "created": "2026-01-30T23:44:53.197877",
+ "completed": "2026-01-30T23:44:53.299278"
+ },
+ {
+ "id": 4,
+ "title": "Documentation complete",
+ "done": false,
+ "created": "2026-01-30T23:44:53.223477"
+ }
+ ],
+ "progress": 75
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/people.json b/data/people.json
new file mode 100644
index 0000000..34fed42
--- /dev/null
+++ b/data/people.json
@@ -0,0 +1,15 @@
+{
+ "d_j": {
+ "name": "D J",
+ "context": "My human, built this workspace together",
+ "added": "2026-01-30T23:45:35.014343",
+ "notes": [
+ {
+ "text": "Late night building session, very productive",
+ "date": "2026-01-30T23:46:25.644083"
+ }
+ ],
+ "tags": [],
+ "last_contact": "2026-01-30T23:46:25.644093"
+ }
+}
\ No newline at end of file
diff --git a/data/reading.json b/data/reading.json
new file mode 100644
index 0000000..e0f57c8
--- /dev/null
+++ b/data/reading.json
@@ -0,0 +1,22 @@
+[
+ {
+ "id": 1,
+ "title": "The Art of Doing Science and Engineering",
+ "url": null,
+ "type": "book",
+ "tags": [],
+ "added": "2026-01-30T23:45:14.470706",
+ "status": "unread",
+ "notes": null
+ },
+ {
+ "id": 2,
+ "title": "Attention Is All You Need",
+ "url": null,
+ "type": "paper",
+ "tags": [],
+ "added": "2026-01-30T23:45:14.495258",
+ "status": "unread",
+ "notes": null
+ }
+]
\ No newline at end of file
diff --git a/tools/goals.py b/tools/goals.py
new file mode 100755
index 0000000..17a929d
--- /dev/null
+++ b/tools/goals.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+"""
+goals - Goal and project tracking with milestones
+
+Track long-term goals and break them into actionable steps.
+"""
+
+import json
+import sys
+from datetime import datetime
+from pathlib import Path
+
+WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
+GOALS_FILE = WORKSPACE / "data" / "goals.json"
+
+def load_goals() -> dict:
+ """Load goals data."""
+ GOALS_FILE.parent.mkdir(parents=True, exist_ok=True)
+ if GOALS_FILE.exists():
+ with open(GOALS_FILE) as f:
+ return json.load(f)
+ return {'goals': []}
+
+def save_goals(data: dict):
+ """Save goals data."""
+ with open(GOALS_FILE, 'w') as f:
+ json.dump(data, f, indent=2)
+
+def add_goal(title: str, description: str = "", deadline: str = None):
+ """Add a new goal."""
+ data = load_goals()
+
+ goal = {
+ 'id': len(data['goals']) + 1,
+ 'title': title,
+ 'description': description,
+ 'deadline': deadline,
+ 'created': datetime.now().isoformat(),
+ 'status': 'active',
+ 'milestones': [],
+ 'progress': 0,
+ }
+
+ data['goals'].append(goal)
+ save_goals(data)
+ print(f"šÆ Goal #{goal['id']}: {title}")
+
+def add_milestone(goal_id: int, title: str):
+ """Add a milestone to a goal."""
+ data = load_goals()
+
+ for goal in data['goals']:
+ if goal['id'] == goal_id:
+ milestone = {
+ 'id': len(goal['milestones']) + 1,
+ 'title': title,
+ 'done': False,
+ 'created': datetime.now().isoformat(),
+ }
+ goal['milestones'].append(milestone)
+ save_goals(data)
+ print(f" ā Added milestone: {title}")
+ return
+
+ print(f"Goal not found: #{goal_id}")
+
+def complete_milestone(goal_id: int, milestone_id: int):
+ """Mark a milestone as complete."""
+ data = load_goals()
+
+ for goal in data['goals']:
+ if goal['id'] == goal_id:
+ for m in goal['milestones']:
+ if m['id'] == milestone_id:
+ m['done'] = True
+ m['completed'] = datetime.now().isoformat()
+
+ # Update progress
+ done = len([x for x in goal['milestones'] if x['done']])
+ total = len(goal['milestones'])
+ goal['progress'] = int(done / total * 100) if total > 0 else 0
+
+ save_goals(data)
+ print(f" ā Completed: {m['title']}")
+ print(f" Progress: {goal['progress']}%")
+ return
+
+ print("Milestone not found")
+
+def show_goals(show_all: bool = False):
+ """Display all goals."""
+ data = load_goals()
+
+ goals = data['goals']
+ if not show_all:
+ goals = [g for g in goals if g['status'] == 'active']
+
+ if not goals:
+ print("No active goals. Add one with: goals add
")
+ return
+
+ print(f"\nšÆ Goals ({len(goals)})")
+ print("=" * 50)
+
+ for goal in goals:
+ status = "ā
" if goal['status'] == 'completed' else "šµ"
+ bar = "ā" * (goal['progress'] // 10) + "ā" * (10 - goal['progress'] // 10)
+
+ print(f"\n{status} #{goal['id']} {goal['title']}")
+ print(f" [{bar}] {goal['progress']}%")
+
+ if goal.get('deadline'):
+ print(f" š
Deadline: {goal['deadline']}")
+
+ if goal['milestones']:
+ for m in goal['milestones']:
+ check = "ā" if m['done'] else "ā"
+ print(f" {check} {m['title']}")
+
+ print()
+
+def complete_goal(goal_id: int):
+ """Mark a goal as completed."""
+ data = load_goals()
+
+ for goal in data['goals']:
+ if goal['id'] == goal_id:
+ goal['status'] = 'completed'
+ goal['progress'] = 100
+ goal['completed'] = datetime.now().isoformat()
+ save_goals(data)
+ print(f"š Goal completed: {goal['title']}")
+ return
+
+ print(f"Goal not found: #{goal_id}")
+
+def main():
+ if len(sys.argv) < 2:
+ show_goals()
+ return
+
+ cmd = sys.argv[1]
+
+ if cmd == 'add' and len(sys.argv) > 2:
+ title = ' '.join(sys.argv[2:])
+ add_goal(title)
+
+ elif cmd == 'milestone' and len(sys.argv) > 3:
+ goal_id = int(sys.argv[2])
+ title = ' '.join(sys.argv[3:])
+ add_milestone(goal_id, title)
+
+ elif cmd == 'done' and len(sys.argv) > 3:
+ goal_id = int(sys.argv[2])
+ milestone_id = int(sys.argv[3])
+ complete_milestone(goal_id, milestone_id)
+
+ elif cmd == 'complete' and len(sys.argv) > 2:
+ complete_goal(int(sys.argv[2]))
+
+ elif cmd == 'list':
+ show_goals(show_all='--all' in sys.argv)
+
+ else:
+ print("Usage:")
+ print(" goals - Show active goals")
+ print(" goals add - Add new goal")
+ print(" goals milestone - Add milestone")
+ print(" goals done - Complete milestone")
+ print(" goals complete - Complete goal")
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/people.py b/tools/people.py
new file mode 100755
index 0000000..3eb297b
--- /dev/null
+++ b/tools/people.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+"""
+people - Personal CRM / people notes
+
+Keep notes about people you know.
+"""
+
+import json
+import sys
+from datetime import datetime
+from pathlib import Path
+
+WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
+PEOPLE_FILE = WORKSPACE / "data" / "people.json"
+
+def load_people() -> dict:
+ """Load people data."""
+ PEOPLE_FILE.parent.mkdir(parents=True, exist_ok=True)
+ if PEOPLE_FILE.exists():
+ with open(PEOPLE_FILE) as f:
+ return json.load(f)
+ return {}
+
+def save_people(data: dict):
+ """Save people data."""
+ with open(PEOPLE_FILE, 'w') as f:
+ json.dump(data, f, indent=2)
+
+def add_person(name: str, context: str = None):
+ """Add a new person."""
+ people = load_people()
+ key = name.lower().replace(' ', '_')
+
+ if key in people:
+ print(f"Person already exists: {name}")
+ return
+
+ people[key] = {
+ 'name': name,
+ 'context': context,
+ 'added': datetime.now().isoformat(),
+ 'notes': [],
+ 'tags': [],
+ 'last_contact': None,
+ }
+
+ save_people(people)
+ print(f"š¤ Added: {name}")
+
+def add_note(name: str, note: str):
+ """Add a note about someone."""
+ people = load_people()
+ search = name.lower().replace(' ', '')
+
+ # Find by partial match (normalize spaces)
+ matches = [k for k in people if search in k.replace('_', '') or search in people[k]['name'].lower().replace(' ', '')]
+ if not matches:
+ print(f"Person not found: {name}")
+ return
+
+ key = matches[0]
+ people[key]['notes'].append({
+ 'text': note,
+ 'date': datetime.now().isoformat(),
+ })
+ people[key]['last_contact'] = datetime.now().isoformat()
+
+ save_people(people)
+ print(f"š Note added for {people[key]['name']}")
+
+def show_person(name: str):
+ """Show info about a person."""
+ people = load_people()
+ search = name.lower().replace(' ', '')
+
+ # Find by partial match (normalize spaces)
+ matches = [k for k in people if search in k.replace('_', '') or search in people[k]['name'].lower().replace(' ', '')]
+ if not matches:
+ print(f"Person not found: {name}")
+ return
+
+ person = people[matches[0]]
+
+ print(f"\nš¤ {person['name']}")
+ print("=" * 40)
+
+ if person.get('context'):
+ print(f"Context: {person['context']}")
+
+ if person.get('last_contact'):
+ last = person['last_contact'][:10]
+ print(f"Last contact: {last}")
+
+ if person['notes']:
+ print(f"\nš Notes ({len(person['notes'])})")
+ for note in person['notes'][-5:]:
+ date = note['date'][:10]
+ print(f" [{date}] {note['text'][:50]}")
+
+ print()
+
+def list_people():
+ """List all people."""
+ people = load_people()
+
+ if not people:
+ print("No people yet. Add with: people add ")
+ return
+
+ print(f"\nš„ People ({len(people)})")
+ print("=" * 40)
+
+ for key, person in sorted(people.items()):
+ notes = len(person['notes'])
+ context = person.get('context', '')[:20]
+ print(f" {person['name']:20} {notes} notes {context}")
+
+ print()
+
+def main():
+ if len(sys.argv) < 2:
+ list_people()
+ return
+
+ cmd = sys.argv[1]
+
+ if cmd == 'add' and len(sys.argv) > 2:
+ name = sys.argv[2]
+ context = ' '.join(sys.argv[3:]) if len(sys.argv) > 3 else None
+ add_person(name, context)
+
+ elif cmd == 'note' and len(sys.argv) > 3:
+ name = sys.argv[2]
+ note = ' '.join(sys.argv[3:])
+ add_note(name, note)
+
+ elif cmd == 'show' and len(sys.argv) > 2:
+ show_person(sys.argv[2])
+
+ elif cmd == 'list':
+ list_people()
+
+ else:
+ print("Usage:")
+ print(" people - List all")
+ print(" people add [context] - Add person")
+ print(" people note - Add note")
+ print(" people show - Show person")
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/reading.py b/tools/reading.py
new file mode 100755
index 0000000..d16b8d6
--- /dev/null
+++ b/tools/reading.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python3
+"""
+reading - Reading list and article tracker
+
+Track articles, books, and things to read.
+"""
+
+import json
+import sys
+from datetime import datetime
+from pathlib import Path
+
+WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
+READING_FILE = WORKSPACE / "data" / "reading.json"
+
+def load_list() -> list:
+ """Load reading list."""
+ READING_FILE.parent.mkdir(parents=True, exist_ok=True)
+ if READING_FILE.exists():
+ with open(READING_FILE) as f:
+ return json.load(f)
+ return []
+
+def save_list(items: list):
+ """Save reading list."""
+ with open(READING_FILE, 'w') as f:
+ json.dump(items, f, indent=2)
+
+def add(title: str, url: str = None, item_type: str = 'article', tags: list = None):
+ """Add to reading list."""
+ items = load_list()
+
+ item = {
+ 'id': len(items) + 1,
+ 'title': title,
+ 'url': url,
+ 'type': item_type,
+ 'tags': tags or [],
+ 'added': datetime.now().isoformat(),
+ 'status': 'unread',
+ 'notes': None,
+ }
+
+ items.append(item)
+ save_list(items)
+
+ emoji = {'article': 'š', 'book': 'š', 'video': 'š¬', 'paper': 'š'}.get(item_type, 'š')
+ print(f"{emoji} Added: {title}")
+
+def show_list(status: str = None, limit: int = 20):
+ """Show reading list."""
+ items = load_list()
+
+ if status:
+ items = [i for i in items if i['status'] == status]
+
+ if not items:
+ print("Reading list is empty")
+ return
+
+ # Group by status
+ unread = [i for i in items if i['status'] == 'unread']
+ reading = [i for i in items if i['status'] == 'reading']
+ done = [i for i in items if i['status'] == 'done']
+
+ print(f"\nš Reading List")
+ print("=" * 50)
+
+ if reading:
+ print(f"\nš Currently Reading ({len(reading)})")
+ for item in reading:
+ print(f" #{item['id']} {item['title'][:40]}")
+
+ if unread:
+ print(f"\nš„ To Read ({len(unread)})")
+ for item in unread[:10]:
+ emoji = {'article': 'š', 'book': 'š', 'video': 'š¬'}.get(item['type'], 'š')
+ print(f" #{item['id']} {emoji} {item['title'][:40]}")
+ if len(unread) > 10:
+ print(f" ... and {len(unread) - 10} more")
+
+ if done:
+ print(f"\nā
Completed ({len(done)})")
+ for item in done[-5:]:
+ print(f" #{item['id']} {item['title'][:40]}")
+
+ print()
+
+def start_reading(item_id: int):
+ """Mark item as currently reading."""
+ items = load_list()
+ for item in items:
+ if item['id'] == item_id:
+ item['status'] = 'reading'
+ item['started'] = datetime.now().isoformat()
+ save_list(items)
+ print(f"š Started: {item['title']}")
+ return
+ print(f"Item not found: #{item_id}")
+
+def finish_reading(item_id: int, notes: str = None):
+ """Mark item as done."""
+ items = load_list()
+ for item in items:
+ if item['id'] == item_id:
+ item['status'] = 'done'
+ item['finished'] = datetime.now().isoformat()
+ if notes:
+ item['notes'] = notes
+ save_list(items)
+ print(f"ā
Finished: {item['title']}")
+ return
+ print(f"Item not found: #{item_id}")
+
+def main():
+ if len(sys.argv) < 2:
+ show_list()
+ return
+
+ cmd = sys.argv[1]
+
+ if cmd == 'add' and len(sys.argv) > 2:
+ # Parse: add [--url URL] [--type TYPE]
+ args = sys.argv[2:]
+ title_parts = []
+ url = None
+ item_type = 'article'
+
+ i = 0
+ while i < len(args):
+ if args[i] == '--url' and i + 1 < len(args):
+ url = args[i + 1]
+ i += 2
+ elif args[i] == '--type' and i + 1 < len(args):
+ item_type = args[i + 1]
+ i += 2
+ else:
+ title_parts.append(args[i])
+ i += 1
+
+ add(' '.join(title_parts), url, item_type)
+
+ elif cmd == 'start' and len(sys.argv) > 2:
+ start_reading(int(sys.argv[2]))
+
+ elif cmd == 'done' and len(sys.argv) > 2:
+ item_id = int(sys.argv[2])
+ notes = ' '.join(sys.argv[3:]) if len(sys.argv) > 3 else None
+ finish_reading(item_id, notes)
+
+ elif cmd == 'list':
+ show_list()
+
+ else:
+ print("Usage:")
+ print(" reading - Show list")
+ print(" reading add - Add article")
+ print(" reading add --type book --url URL")
+ print(" reading start - Start reading")
+ print(" reading done [notes] - Finish reading")
+
+if __name__ == "__main__":
+ main()
diff --git a/ws b/ws
index 5d724e2..84aadc5 100755
--- a/ws
+++ b/ws
@@ -34,6 +34,9 @@ COMMANDS = {
'sys': ('tools/sysmon.py', 'System monitor'),
'journal': ('tools/journal.py', 'Structured journaling'),
'backup': ('tools/backup.py', 'Backup and export'),
+ 'goals': ('tools/goals.py', 'Goal tracking'),
+ 'reading': ('tools/reading.py', 'Reading list'),
+ 'people': ('tools/people.py', 'Personal CRM'),
# Projects
'news': ('projects/news-feed/main.py', 'RSS news reader'),