@clawhub-ckouder-f24ceb95dc
Lightweight task management CLI for multi-agent workflows. SQLite backend, no external dependencies or credentials. Status-change hooks emit agent instructio...
---
name: taskboard-cli
version: 3.0.1
description: "Lightweight task management CLI for multi-agent workflows. SQLite backend, no external dependencies or credentials. Status-change hooks emit agent instructions (message, session) but do not auto-execute. Use when managing tasks across agents, tracking work status, assigning tasks, generating board summaries, or orchestrating cross-agent handoffs. Triggers on \"create task\", \"task board\", \"taskboard\", \"list tasks\", \"assign task\", \"board summary\", \"project tasks\"."
---
# Taskboard CLI
SQLite-backed task management for multi-agent projects. No network calls, no credentials, no environment variables.
## Quick Start
```bash
# Create tasks
python3 scripts/taskboard.py create "Build auth" --assign code-engineer --priority high
python3 scripts/taskboard.py create "Design UI" --assign designer --criteria "Responsive, mobile-first"
# Manage tasks
python3 scripts/taskboard.py update 1 --status in_progress --author code-engineer
python3 scripts/taskboard.py comment 1 "PR #42 ready" --author code-engineer
python3 scripts/taskboard.py update 1 --status done --author code-engineer --note "Merged to main"
# View board
python3 scripts/taskboard.py list --status in_progress
python3 scripts/taskboard.py show 1
python3 scripts/taskboard.py show 1 --json
python3 scripts/taskboard.py summary
# Subtasks
python3 scripts/taskboard.py create "Write tests" --parent 1 --assign code-engineer
# Thread linking
python3 scripts/taskboard.py set-thread 1 1484268803994026085
python3 scripts/taskboard.py get-thread 1
# Change history
python3 scripts/taskboard.py history 1
```
## Custom Database Path
By default the database lives at `scripts/taskboard.db`. Override with `--db`:
```bash
python3 scripts/taskboard.py --db /path/to/my.db list
```
## Task Statuses
`todo` → `in_progress` → `done`
Also: `blocked`, `rejected`
No "review" status — use hooks to create follow-up tasks or notify agents.
## Hooks (Cross-Agent Orchestration)
Hooks fire when task status changes. They print instructions to stdout for the calling agent to execute — no auto-execution, no network calls.
```bash
# When task is started (ack'd), print a notification instruction
python3 scripts/taskboard.py create "Build auth" \
--on-ack "message:CHANNEL_ID:🔨 {task.title} started by {task.assigned_to}"
# When done, instruct the agent to create a review task
python3 scripts/taskboard.py create "Design UI" \
--on-done "session:tech-lead:Review {task.title} and create QA task"
# Add/update hooks on existing task
python3 scripts/taskboard.py update 1 --on-done "message:CHANNEL_ID:Done!"
```
Hook output format:
```
🔔 ON_ACK: message:CHANNEL_ID:🔨 Build auth started
🔔 ON_DONE: session:tech-lead:Review Build auth and create QA task
```
The agent reads these lines and decides how to act (send a message, spawn a session, create a task, etc.).
## Data Model
- **tasks** — id, title, description, acceptance_criteria, status, priority, assigned_to, created_by, parent_id, thread_id, on_ack, on_done, timestamps
- **task_comments** — per-task comment history
- **task_updates** — audit log of all field changes
Schema auto-initializes on first run. Upgrades from v1 (missing on_ack/on_done columns) are handled automatically.
## Reference
- `references/webhook-integration.md` — How to add Discord/webhook notifications on top of taskboard
- `references/github-backend.md` — Syncing tasks with GitHub Issues
- `references/taskboard-setup.md` — Task lifecycle, cross-agent handoff protocol, cron integration
FILE:references/github-backend.md
# GitHub Issues Backend (Optional)
Sync your taskboard with GitHub Issues. This is a reference guide — the agent reads this and implements the integration when asked.
## Prerequisites
- A GitHub Personal Access Token with `repo` scope
- Set as environment variable: `export GITHUB_TOKEN=ghp_...`
- Recommended: use a dedicated machine/service token, not your personal token
## How It Works
When GitHub sync is enabled, the agent should:
1. **On task create** → Create a GitHub Issue with matching title, labels, and assignee
2. **On status update** → Update Issue labels (e.g., `status:in-progress`) and close/reopen as needed
3. **On note add** → Post an Issue comment
4. **On assign** → Update Issue assignee
## Setup Steps
1. Create status labels in your GitHub repo:
```
status:backlog, status:in-progress, status:review, status:done, status:blocked
```
2. Map agent names to GitHub usernames (keep this in your project's config or AGENTS.md):
```
code-engineer → github-username-1
tech-lead → github-username-2
designer → github-username-3
```
3. When creating/updating tasks, the agent uses the GitHub API:
### Create Issue
```bash
curl -X POST https://api.github.com/repos/OWNER/REPO/issues \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
-d '{
"title": "TASK-001: Build auth module",
"body": "Description here",
"labels": ["status:backlog", "backend", "security"],
"assignees": ["github-username"]
}'
```
### Update Issue Status
```bash
# Update labels
curl -X PATCH https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER \
-H "Authorization: token $GITHUB_TOKEN" \
-d '{"labels": ["status:in-progress", "backend"]}'
# Close issue (when done)
curl -X PATCH https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER \
-H "Authorization: token $GITHUB_TOKEN" \
-d '{"state": "closed"}'
```
### Add Comment
```bash
curl -X POST https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER/comments \
-H "Authorization: token $GITHUB_TOKEN" \
-d '{"body": "PR #42 ready for review"}'
```
## Task Field Mapping
| Taskboard Field | GitHub Issue Field |
|---|---|
| title | title (prefixed with task ID) |
| description | body |
| status | labels (status:xxx) |
| assignee | assignees (mapped via config) |
| tags | labels |
| notes | comments |
| priority | labels (priority:xxx) |
## Tracking the Link
When a task is synced to GitHub, store the issue number in the task:
```json
{
"id": "PROJ-001",
"title": "Build auth",
"github_issue": 42,
"github_url": "https://github.com/owner/repo/issues/42"
}
```
The agent can add these fields to `taskboard.json` after creating the issue.
## Bidirectional Sync (Advanced)
For GitHub → Taskboard sync (e.g., someone closes an issue on GitHub), set up a GitHub webhook pointing to a Discord channel or agent endpoint. The agent can then update the local taskboard when notified.
FILE:references/taskboard-setup.md
# Taskboard CLI Setup
A lightweight, file-based task management system for multi-agent workflows.
## Task Schema
Tasks are stored in `taskboard.json`:
```json
{
"tasks": [
{
"id": "PROJ-001",
"title": "Implement user authentication",
"description": "Add JWT-based auth with refresh tokens",
"status": "backlog",
"assignee": "code-engineer",
"priority": "high",
"dependencies": [],
"tags": ["backend", "security"],
"created": "2026-03-18T00:00:00Z",
"updated": "2026-03-18T00:00:00Z",
"adr": "ADR-003",
"notes": []
}
],
"meta": {
"project": "project-name",
"prefix": "PROJ",
"nextId": 2
}
}
```
## CLI Commands
### Create a task
```bash
python3 scripts/taskboard.py create \
--title "Implement user auth" \
--assignee code-engineer \
--priority high \
--tags backend,security
```
### List tasks
```bash
# All tasks
python3 scripts/taskboard.py list
# Filter by status
python3 scripts/taskboard.py list --status in-progress
# Filter by assignee
python3 scripts/taskboard.py list --assignee tech-lead
# Filter by priority
python3 scripts/taskboard.py list --priority high
```
### Update task status
```bash
python3 scripts/taskboard.py update PROJ-001 --status in-progress
python3 scripts/taskboard.py update PROJ-001 --status review --note "Ready for Tech Lead review"
python3 scripts/taskboard.py update PROJ-001 --status done
```
### Assign task
```bash
python3 scripts/taskboard.py assign PROJ-001 --to code-engineer
```
### Add note to task
```bash
python3 scripts/taskboard.py note PROJ-001 "Found edge case with expired tokens, added test"
```
### View task detail
```bash
python3 scripts/taskboard.py show PROJ-001
```
### Board summary (for cron jobs / Discord posting)
```bash
python3 scripts/taskboard.py summary
```
Output:
```
📋 Project Board: project-name
━━━━━━━━━━━━━━━━━━━━━━━━━━━
📥 Backlog: 3 tasks
🔄 In Progress: 2 tasks (code-engineer: 1, tech-lead: 1)
👀 Review: 1 task
✅ Done (this week): 4 tasks
🚫 Blocked: 0 tasks
🔥 High Priority:
PROJ-001 [in-progress] Implement user auth (code-engineer)
PROJ-005 [backlog] Fix payment webhook (unassigned)
```
## Cross-Agent Handoff Protocol
### When Tech Lead completes architecture:
1. `taskboard.py update PROJ-001 --status review --note "ADR written, ready for implementation"`
2. `taskboard.py create --title "Implement: [feature]" --assignee code-engineer --deps PROJ-001`
3. Notify Code Engineer's Discord channel
### When Code Engineer completes implementation:
1. `taskboard.py update PROJ-002 --status review --note "PR #42 ready for review"`
2. Notify Tech Lead's Discord channel for code review
### When review passes:
1. `taskboard.py update PROJ-002 --status done`
2. Check if downstream tasks are unblocked
3. Notify relevant agents
## Cron Integration
### Daily board check (morning)
```
Check taskboard.json for:
1. Tasks in-progress for >3 days (flag as potentially stuck)
2. Blocked tasks (check if blockers are resolved)
3. High-priority unassigned tasks
4. Send summary to #traveler-home Discord channel
```
### Weekly board review (Sunday)
```
Generate weekly report:
1. Tasks completed this week
2. Tasks carried over
3. Velocity trend (tasks done per week)
4. Blocked items needing human decision
```
## Minimum Check Interval
Do not poll the taskboard more frequently than every **5 minutes**.
FILE:references/webhook-integration.md
# Webhook & Discord Integration
The taskboard CLI itself makes no network calls. To add notifications, the agent reads hook output and acts on it.
## Pattern 1: Agent-Driven Notifications
When a task status changes and a hook fires, the CLI prints:
```
🔔 ON_DONE: message:CHANNEL_ID:✅ Task #5 completed
```
The agent parses this and uses its own messaging tools (e.g., OpenClaw `message` tool, `sessions_send`, etc.) to deliver the notification.
## Pattern 2: Discord Webhook (External)
If you want a standalone webhook notifier outside the agent, create a wrapper script:
```python
#!/usr/bin/env python3
"""Wrapper: runs taskboard command, parses hook output, sends Discord webhook."""
import subprocess, json, urllib.request, sys
result = subprocess.run(
["python3", "scripts/taskboard.py"] + sys.argv[1:],
capture_output=True, text=True
)
print(result.stdout, end="")
# Parse hook lines
for line in result.stdout.splitlines():
if line.startswith("🔔 ON_"):
hook_type, _, instruction = line.partition(": ")
# Send to Discord webhook
# ... your webhook logic here
```
This wrapper lives **outside** the skill directory (not published to ClawHub).
## Pattern 3: OpenClaw Cron Integration
Use OpenClaw cron jobs to periodically check for task changes:
```
Schedule: every 5 minutes
Payload: "Check taskboard for recently completed tasks, notify #task-status channel"
```
## Webhook Config Example
For teams using the webhook pattern, store config in a separate file (not in the skill):
```json
{
"discord_webhook": "https://discord.com/api/webhooks/...",
"discord_status_thread": "1482856331923816650",
"gateway_url": "http://localhost:18789",
"hooks_token": "your-token",
"agent_map": {
"tech-lead": {"agentId": "shannon"},
"code-engineer": {"agentId": "linus"}
}
}
```
FILE:scripts/schema.sql
-- TaskBoard Schema v2
-- SQLite backend for multi-agent task coordination
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT DEFAULT '',
acceptance_criteria TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'todo'
CHECK(status IN ('todo','in_progress','done','blocked','rejected')),
priority TEXT NOT NULL DEFAULT 'normal'
CHECK(priority IN ('low','normal','high','urgent')),
assigned_to TEXT DEFAULT NULL,
created_by TEXT NOT NULL,
parent_id INTEGER DEFAULT NULL REFERENCES tasks(id),
thread_id TEXT DEFAULT NULL,
on_ack TEXT DEFAULT NULL,
on_done TEXT DEFAULT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS task_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL REFERENCES tasks(id),
author TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS task_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL REFERENCES tasks(id),
field TEXT NOT NULL,
old_value TEXT,
new_value TEXT,
changed_by TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to, status);
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id);
CREATE INDEX IF NOT EXISTS idx_updates_task ON task_updates(task_id);
FILE:scripts/taskboard.py
#!/usr/bin/env python3
"""TaskBoard CLI — lightweight SQLite task coordination for multi-agent workflows.
No network calls, no environment variables, no external dependencies.
Hooks print instructions to stdout for the agent to execute.
"""
import argparse
import sqlite3
import sys
import os
import json
from datetime import datetime
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
SCHEMA_PATH = SCRIPT_DIR / "schema.sql"
DEFAULT_DB = SCRIPT_DIR / "taskboard.db"
PRIORITY_EMOJI = {"urgent": "🔴", "high": "🟠", "normal": "🔵", "low": "⚪"}
STATUS_EMOJI = {"todo": "📋", "in_progress": "🔨", "done": "✅", "blocked": "🚫", "rejected": "❌"}
VALID_STATUSES = ("todo", "in_progress", "done", "blocked", "rejected")
def get_db(db_path=None):
path = db_path or str(DEFAULT_DB)
db = sqlite3.connect(path)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA foreign_keys=ON")
with open(SCHEMA_PATH) as f:
db.executescript(f.read())
# Migrate: add on_ack/on_done if missing (upgrade from v1)
cols = [r[1] for r in db.execute("PRAGMA table_info(tasks)").fetchall()]
if "on_ack" not in cols:
db.execute("ALTER TABLE tasks ADD COLUMN on_ack TEXT DEFAULT NULL")
if "on_done" not in cols:
db.execute("ALTER TABLE tasks ADD COLUMN on_done TEXT DEFAULT NULL")
return db
# ─── Commands ───────────────────────────────────────────────
def cmd_create(args):
db = get_db(args.db)
thread_id = args.thread_id
if not thread_id and args.parent:
parent = db.execute(
"SELECT thread_id FROM tasks WHERE id = ?", (args.parent,)
).fetchone()
if parent and parent["thread_id"]:
thread_id = parent["thread_id"]
on_ack = getattr(args, "on_ack", None)
on_done = getattr(args, "on_done", None)
cur = db.execute(
"""INSERT INTO tasks
(title, description, acceptance_criteria, assigned_to, created_by,
parent_id, priority, thread_id, on_ack, on_done)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
args.title,
args.desc or "",
args.criteria or "",
args.assign,
args.author,
args.parent,
args.priority or "normal",
thread_id,
on_ack,
on_done,
),
)
task_id = cur.lastrowid
db.commit()
print(f"✅ Task #{task_id} created: {args.title}")
if args.assign:
print(f" Assigned to: {args.assign}")
if args.parent:
print(f" Parent: #{args.parent}")
if thread_id:
print(f" Thread: {thread_id}")
if on_ack:
print(f" on_ack: {on_ack}")
if on_done:
print(f" on_done: {on_done}")
def cmd_list(args):
db = get_db(args.db)
conditions = []
params = []
if args.assigned_to:
conditions.append("assigned_to = ?")
params.append(args.assigned_to)
if args.status:
conditions.append("status = ?")
params.append(args.status)
if args.created_by:
conditions.append("created_by = ?")
params.append(args.created_by)
if args.parent is not None:
if args.parent == -1:
conditions.append("parent_id IS NULL")
else:
conditions.append("parent_id = ?")
params.append(args.parent)
where = " WHERE " + " AND ".join(conditions) if conditions else ""
if args.json:
rows = db.execute(
f"SELECT * FROM tasks{where} ORDER BY priority DESC, created_at DESC",
params,
).fetchall()
print(json.dumps([dict(r) for r in rows], indent=2))
return
rows = db.execute(
f"""SELECT id, title, status, priority, assigned_to, parent_id
FROM tasks{where}
ORDER BY priority DESC, created_at DESC""",
params,
).fetchall()
if not rows:
print("No tasks found.")
return
for r in rows:
p = PRIORITY_EMOJI.get(r["priority"], "·")
s = STATUS_EMOJI.get(r["status"], "·")
parent = f" (sub of #{r['parent_id']})" if r["parent_id"] else ""
assign = f" → {r['assigned_to']}" if r["assigned_to"] else " (unassigned)"
print(
f" {p} #{r['id']:>3} {s} [{r['status']:<11}] {r['title']}{assign}{parent}"
)
def cmd_show(args):
db = get_db(args.db)
task = db.execute("SELECT * FROM tasks WHERE id = ?", (args.id,)).fetchone()
if not task:
print(f"❌ Task #{args.id} not found")
sys.exit(1)
if args.json:
t = dict(task)
t["comments"] = [
dict(c)
for c in db.execute(
"SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at",
(args.id,),
).fetchall()
]
t["subtasks"] = [
dict(s)
for s in db.execute(
"SELECT id, title, status, assigned_to FROM tasks WHERE parent_id = ?",
(args.id,),
).fetchall()
]
print(json.dumps(t, indent=2))
return
print(f"\n{'='*50}")
print(f" Task #{task['id']}: {task['title']}")
print(f"{'='*50}")
print(
f" Status: {STATUS_EMOJI.get(task['status'],'')} {task['status']}"
)
print(
f" Priority: {PRIORITY_EMOJI.get(task['priority'],'')} {task['priority']}"
)
print(f" Assigned: {task['assigned_to'] or '(unassigned)'}")
print(f" Created by: {task['created_by']}")
if task["parent_id"]:
print(f" Parent: #{task['parent_id']}")
print(f" Created: {task['created_at']}")
print(f" Updated: {task['updated_at']}")
if task["thread_id"]:
print(f" Thread: {task['thread_id']}")
if task["description"]:
print(f"\n 📝 Description:\n {task['description']}")
if task["acceptance_criteria"]:
print(f"\n ✅ Acceptance Criteria:\n {task['acceptance_criteria']}")
on_ack = task["on_ack"] if "on_ack" in task.keys() else None
on_done = task["on_done"] if "on_done" in task.keys() else None
if on_ack:
print(f"\n 🔔 On Ack: {on_ack}")
if on_done:
print(f"\n 🔔 On Done: {on_done}")
subtasks = db.execute(
"SELECT id, title, status, assigned_to FROM tasks WHERE parent_id = ?",
(args.id,),
).fetchall()
if subtasks:
print(f"\n 📦 Subtasks:")
for s in subtasks:
se = STATUS_EMOJI.get(s["status"], "·")
print(
f" {se} #{s['id']} [{s['status']}] {s['title']} → {s['assigned_to'] or '?'}"
)
comments = db.execute(
"SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at",
(args.id,),
).fetchall()
if comments:
print(f"\n 💬 Comments:")
for c in comments:
print(f" [{c['created_at']}] {c['author']}: {c['content']}")
print()
def cmd_update(args):
db = get_db(args.db)
task = db.execute("SELECT * FROM tasks WHERE id = ?", (args.id,)).fetchone()
if not task:
print(f"❌ Task #{args.id} not found")
sys.exit(1)
updates = []
params = []
changes = []
if args.status:
changes.append(("status", task["status"], args.status))
updates.append("status = ?")
params.append(args.status)
if args.assign is not None:
new_assign = args.assign if args.assign != "none" else None
changes.append(("assigned_to", task["assigned_to"], new_assign))
updates.append("assigned_to = ?")
params.append(new_assign)
if args.priority:
changes.append(("priority", task["priority"], args.priority))
updates.append("priority = ?")
params.append(args.priority)
if args.title:
changes.append(("title", task["title"], args.title))
updates.append("title = ?")
params.append(args.title)
if args.desc is not None:
changes.append(("description", task["description"], args.desc))
updates.append("description = ?")
params.append(args.desc)
if args.criteria is not None:
changes.append(
("acceptance_criteria", task["acceptance_criteria"], args.criteria)
)
updates.append("acceptance_criteria = ?")
params.append(args.criteria)
if getattr(args, "on_ack", None) is not None:
old = task["on_ack"] if "on_ack" in task.keys() else None
changes.append(("on_ack", old, args.on_ack))
updates.append("on_ack = ?")
params.append(args.on_ack)
if getattr(args, "on_done", None) is not None:
old = task["on_done"] if "on_done" in task.keys() else None
changes.append(("on_done", old, args.on_done))
updates.append("on_done = ?")
params.append(args.on_done)
if not updates:
print("Nothing to update.")
return
updates.append("updated_at = datetime('now')")
params.append(args.id)
db.execute(f"UPDATE tasks SET {', '.join(updates)} WHERE id = ?", params)
author = args.author or "unknown"
for field, old, new in changes:
db.execute(
"INSERT INTO task_updates (task_id, field, old_value, new_value, changed_by) VALUES (?, ?, ?, ?, ?)",
(args.id, field, str(old), str(new), author),
)
db.commit()
print(f"✅ Task #{args.id} updated:")
for field, old, new in changes:
print(f" {field}: {old} → {new}")
if args.note:
db.execute(
"INSERT INTO task_comments (task_id, author, content) VALUES (?, ?, ?)",
(args.id, author, args.note),
)
db.commit()
print(f" 📝 Note added")
# Refresh and emit hooks
task = db.execute("SELECT * FROM tasks WHERE id = ?", (args.id,)).fetchone()
if args.status == "in_progress" and task["on_ack"]:
print(f"\n🔔 ON_ACK: {task['on_ack']}")
if args.status == "done" and task["on_done"]:
print(f"\n🔔 ON_DONE: {task['on_done']}")
def cmd_comment(args):
db = get_db(args.db)
task = db.execute("SELECT id FROM tasks WHERE id = ?", (args.id,)).fetchone()
if not task:
print(f"❌ Task #{args.id} not found")
sys.exit(1)
db.execute(
"INSERT INTO task_comments (task_id, author, content) VALUES (?, ?, ?)",
(args.id, args.author, args.content),
)
db.commit()
print(f"💬 Comment added to task #{args.id}")
def cmd_summary(args):
db = get_db(args.db)
rows = db.execute(
"SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status"
).fetchall()
if not rows:
print("No tasks yet.")
return
total = sum(r["cnt"] for r in rows)
print(f"\n📊 TaskBoard Summary ({total} total)")
print("─" * 30)
for r in rows:
e = STATUS_EMOJI.get(r["status"], "·")
print(f" {e} {r['status']:<12} {r['cnt']}")
agents = db.execute(
"""SELECT assigned_to, COUNT(*) as cnt,
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done_cnt
FROM tasks WHERE assigned_to IS NOT NULL
GROUP BY assigned_to"""
).fetchall()
if agents:
print(f"\n👥 By Agent:")
for a in agents:
print(f" {a['assigned_to']}: {a['done_cnt']}/{a['cnt']} done")
print()
def cmd_history(args):
db = get_db(args.db)
rows = db.execute(
"SELECT * FROM task_updates WHERE task_id = ? ORDER BY created_at",
(args.id,),
).fetchall()
if not rows:
print(f"No history for task #{args.id}")
return
print(f"\n📜 History for task #{args.id}:")
for r in rows:
print(
f" [{r['created_at']}] {r['changed_by']}: {r['field']} {r['old_value']} → {r['new_value']}"
)
def cmd_set_thread(args):
db = get_db(args.db)
task = db.execute("SELECT id FROM tasks WHERE id = ?", (args.id,)).fetchone()
if not task:
print(f"❌ Task #{args.id} not found")
sys.exit(1)
db.execute(
"UPDATE tasks SET thread_id = ?, updated_at = datetime('now') WHERE id = ?",
(args.thread_id, args.id),
)
db.commit()
print(f"🔗 Task #{args.id} linked to thread {args.thread_id}")
def cmd_get_thread(args):
db = get_db(args.db)
task = db.execute(
"SELECT thread_id FROM tasks WHERE id = ?", (args.id,)
).fetchone()
if not task:
print(f"❌ Task #{args.id} not found")
sys.exit(1)
if task["thread_id"]:
print(task["thread_id"])
else:
print(f"No thread linked to task #{args.id}")
sys.exit(1)
# ─── Main ───────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="TaskBoard — SQLite task coordination for multi-agent workflows"
)
parser.add_argument(
"--db", help="Path to SQLite database (default: scripts/taskboard.db)"
)
sub = parser.add_subparsers(dest="command", required=True)
# create
p = sub.add_parser("create", help="Create a new task")
p.add_argument("title", help="Task title")
p.add_argument("--desc", "-d", help="Description")
p.add_argument("--criteria", "-c", help="Acceptance criteria")
p.add_argument("--assign", "-a", help="Assign to agent")
p.add_argument("--author", default="paimon", help="Created by")
p.add_argument("--parent", "-p", type=int, help="Parent task ID")
p.add_argument(
"--priority",
choices=["low", "normal", "high", "urgent"],
default="normal",
)
p.add_argument("--thread-id", "-t", help="Discord thread ID")
p.add_argument("--on-ack", help="Hook: action when task is acknowledged/started")
p.add_argument("--on-done", help="Hook: action when task is completed")
# list
p = sub.add_parser("list", help="List tasks")
p.add_argument("--assigned-to", "--mine", help="Filter by assignee")
p.add_argument("--status", "-s", help="Filter by status")
p.add_argument("--created-by", help="Filter by creator")
p.add_argument("--parent", type=int, help="Filter by parent (-1 for top-level)")
p.add_argument("--json", action="store_true", help="JSON output")
# show
p = sub.add_parser("show", help="Show task details")
p.add_argument("id", type=int, help="Task ID")
p.add_argument("--json", action="store_true", help="JSON output")
# update
p = sub.add_parser("update", help="Update a task")
p.add_argument("id", type=int, help="Task ID")
p.add_argument(
"--status", "-s", choices=list(VALID_STATUSES)
)
p.add_argument("--assign", "-a", help='Reassign (use "none" to unassign)')
p.add_argument("--priority", choices=["low", "normal", "high", "urgent"])
p.add_argument("--title", help="New title")
p.add_argument("--desc", "-d", help="New description")
p.add_argument("--criteria", "-c", help="New acceptance criteria")
p.add_argument("--note", "-n", help="Add a note/comment with the update")
p.add_argument("--author", default="paimon", help="Updated by")
p.add_argument("--on-ack", help="Hook: action on ack")
p.add_argument("--on-done", help="Hook: action on done")
# comment
p = sub.add_parser("comment", help="Add a comment")
p.add_argument("id", type=int, help="Task ID")
p.add_argument("content", help="Comment text")
p.add_argument("--author", default="paimon", help="Comment author")
# summary
sub.add_parser("summary", help="Show board summary")
# history
p = sub.add_parser("history", help="Show task change history")
p.add_argument("id", type=int, help="Task ID")
# set-thread
p = sub.add_parser("set-thread", help="Link task to a Discord thread")
p.add_argument("id", type=int, help="Task ID")
p.add_argument("thread_id", help="Discord thread ID")
# get-thread
p = sub.add_parser("get-thread", help="Get linked thread ID")
p.add_argument("id", type=int, help="Task ID")
args = parser.parse_args()
commands = {
"create": cmd_create,
"list": cmd_list,
"show": cmd_show,
"update": cmd_update,
"comment": cmd_comment,
"summary": cmd_summary,
"history": cmd_history,
"set-thread": cmd_set_thread,
"get-thread": cmd_get_thread,
}
commands[args.command](args)
if __name__ == "__main__":
main()
Bootstrap a multi-agent software project from idea to running CI/CD. Use when starting a new project that needs agent team design, task management, GitHub re...
---
name: project-bootstrap
description: Bootstrap a multi-agent software project from idea to running CI/CD. Use when starting a new project that needs agent team design, task management, GitHub repo setup, TDD pipeline, and Discord notifications. Triggers on "new project", "bootstrap project", "set up agents for project", "create project pipeline", "start a new repo with CI/CD".
---
# Project Bootstrap
Turn a project idea into a running multi-agent development pipeline in one session.
## Overview
This skill codifies the workflow for:
1. **Agent Team Design** — break complex work into specialized agents
2. **Taskboard Setup** — CLI-based task management across agents
3. **GitHub Repo + CI/CD** — TDD pipeline with Discord notifications
## Phase 1: Agent Team Design
### Analyze the Project
Before creating agents, answer these questions:
- What are the 3-5 major workstreams? (e.g., frontend, backend, research, design)
- Which workstreams need different expertise or thinking styles?
- What's the dependency graph between workstreams?
### Design Agent Roles
For each workstream, create an agent with a SOUL.md following this structure:
```markdown
# Agent Name — Nickname Emoji
You are **Nickname**, the [Role] — [one-line mission].
## 🧠 Identity & Memory
- **Role**: [specific expertise]
- **Personality**: [3-4 traits that affect work style]
- **Memory**: [what context files they track]
- **Experience**: [what failure modes they've seen]
## 🎯 Core Mission
[2-4 responsibility groups with specifics]
## 🚨 Critical Rules
[Non-negotiable constraints — security, process, boundaries]
## 📋 Deliverables
[Concrete outputs this agent produces]
## 🎯 Success Metrics
[How to measure if the agent is doing well]
## 💬 Communication Style
[How the agent communicates — tone, format, language]
## 🔗 Workflow Position
[Where in the pipeline: who feeds input, who receives output]
```
### Register Agents in Config
For each agent, add to `openclaw.json`:
```json
{
"id": "agent-id",
"name": "agent-id",
"agentDir": "/path/to/workspace/agents/agent-id",
"model": "model-alias",
"tools": {
"profile": "full",
"deny": ["gateway"]
}
}
```
Key decisions:
- **Model selection**: Use cheaper models (Haiku/Sonnet) for routine work, expensive (Opus) for architecture/review
- **Tool access**: Deny `gateway` for all agents except main. Deny `message` for pure code agents.
- **Subagent allowlist**: Main agent lists which agents it can spawn
### Wire Discord Bindings (if multi-bot)
If agents have separate Discord bots, add bindings:
```json
{
"bindings": [
{ "agentId": "tech-lead", "match": { "channel": "discord", "accountId": "bot-name" } }
]
}
```
## Phase 2: Taskboard Setup
### Install Taskboard CLI
See `references/taskboard-setup.md` for the full taskboard CLI setup guide including:
- Task schema (id, title, status, assignee, priority, dependencies)
- CLI commands (create, list, assign, update, close)
- Cross-agent task handoff protocol
- Integration with cron jobs for status checks
### Task Lifecycle
```
📋 backlog → 🔄 in-progress → 👀 review → ✅ done
↓ ↓
🚫 blocked ❌ rejected → 🔄 in-progress
```
### Cross-Agent Handoff
When an agent completes a task that feeds into another agent's work:
1. Update task status to `review`
2. Create a new task for the downstream agent referencing the completed task
3. Send notification to the downstream agent's Discord channel
## Phase 3: GitHub Repo + CI/CD
### Repository Setup
```bash
# Initialize repo
gh repo create <org>/<project> --private --clone
cd <project>
# Branch protection
gh api repos/<org>/<project>/rulesets -X POST --input .github/ruleset.json
# Required structure
mkdir -p .github/workflows tests src docs/adr
```
### TDD Pipeline
See `references/ci-cd-templates.md` for GitHub Actions workflow templates:
- **test.yml**: Run tests on every PR and push to main
- **lint.yml**: Code style checks
- **deploy.yml**: Deploy on merge to main (if applicable)
### Discord Notifications
Add Discord webhook to GitHub repo:
```bash
# Create webhook in Discord channel (Server Settings → Integrations → Webhooks)
# Add to GitHub: Settings → Webhooks → Add webhook
# Or use GitHub Actions:
```
See `references/ci-cd-templates.md` for the Discord notification action template.
### ADR (Architecture Decision Records)
Every significant technical decision gets an ADR:
```markdown
# ADR-NNN: [Title]
## Status: [proposed | accepted | deprecated | superseded]
## Context: [Why this decision is needed]
## Decision: [What we decided]
## Consequences: [Trade-offs and implications]
```
## Execution Checklist
Run through this for every new project:
- [ ] Define project scope and 3-5 workstreams
- [ ] Design agent SOUL.md for each workstream
- [ ] Register agents in openclaw.json
- [ ] Set up Discord channels per agent/workstream
- [ ] Create GitHub repo with branch protection
- [ ] Add CI/CD workflows (test + lint + deploy)
- [ ] Add Discord webhook for CI/CD notifications
- [ ] Initialize taskboard with backlog items
- [ ] Create first ADR (ADR-001: Project Architecture)
- [ ] Assign initial tasks to agents
- [ ] Run a test cycle: create task → agent executes → review → merge
FILE:references/ci-cd-templates.md
# CI/CD Templates
GitHub Actions workflow templates for multi-agent projects.
## Test Pipeline (test.yml)
```yaml
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11', '3.12'] # adjust per project
steps:
- uses: actions/checkout@v4
- name: Set up Python { matrix.python-version}
uses: actions/setup-python@v5
with:
python-version: { matrix.python-version}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests
run: pytest tests/ -v --tb=short --cov=src --cov-report=term-missing
- name: Upload coverage
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
```
For TypeScript/Node.js projects:
```yaml
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
```
## Lint Pipeline (lint.yml)
```yaml
name: Lint
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install linters
run: pip install ruff mypy
- name: Ruff check
run: ruff check src/ tests/
- name: Ruff format check
run: ruff format --check src/ tests/
- name: Type check
run: mypy src/ --ignore-missing-imports
```
## Discord Notification Action
Add to any workflow to get Discord notifications on failure/success:
```yaml
notify:
needs: [test]
if: always()
runs-on: ubuntu-latest
steps:
- name: Discord Notification
uses: sarisia/actions-status-discord@v1
with:
webhook: { secrets.DISCORD_WEBHOOK}
status: { needs.test.result}
title: "{ github.repository} CI"
description: |
Commit: { github.event.head_commit.message}
Author: { github.event.head_commit.author.name}
color: { needs.test.result == 'success' && '0x00ff00' || '0xff0000'}
```
### Discord Webhook Setup
1. Discord Server Settings → Integrations → Webhooks → New Webhook
2. Choose the notification channel (e.g., #traveler-home or a dedicated #ci-cd)
3. Copy webhook URL
4. GitHub repo → Settings → Secrets → New repository secret
- Name: `DISCORD_WEBHOOK`
- Value: the webhook URL
### Alternative: GitHub Native Discord Integration
For simpler setup (no Actions required):
1. Discord Server Settings → Integrations → Webhooks → New Webhook
2. Append `/github` to the webhook URL
3. GitHub repo → Settings → Webhooks → Add webhook
- Payload URL: `https://discord.com/api/webhooks/<id>/<token>/github`
- Content type: `application/json`
- Events: Push, Pull Request, Workflow Run
## Branch Protection Rules
Create `.github/ruleset.json`:
```json
{
"name": "main-protection",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/heads/main"]
}
},
"rules": [
{ "type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": true,
"require_last_push_approval": false
}
},
{ "type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [
{ "context": "test" }
]
}
}
]
}
```
## TDD Workflow for Agents
When Code Engineer receives a task:
1. **Write the test first** — define expected behavior
2. **Run tests — see them fail** (red)
3. **Implement minimal code** to pass the test (green)
4. **Refactor** while tests stay green
5. **Commit** with message: `feat(scope): description` or `fix(scope): description`
6. **Push** — CI runs automatically
7. **If CI fails** — fix before moving on, never leave main broken
FILE:references/taskboard-setup.md
# Taskboard CLI Setup
A lightweight, file-based task management system for multi-agent workflows.
## Task Schema
Tasks are stored in `taskboard.json`:
```json
{
"tasks": [
{
"id": "PROJ-001",
"title": "Implement user authentication",
"description": "Add JWT-based auth with refresh tokens",
"status": "backlog",
"assignee": "code-engineer",
"priority": "high",
"dependencies": [],
"tags": ["backend", "security"],
"created": "2026-03-18T00:00:00Z",
"updated": "2026-03-18T00:00:00Z",
"adr": "ADR-003",
"notes": []
}
],
"meta": {
"project": "project-name",
"prefix": "PROJ",
"nextId": 2
}
}
```
## CLI Commands
### Create a task
```bash
python3 scripts/taskboard.py create \
--title "Implement user auth" \
--assignee code-engineer \
--priority high \
--tags backend,security
```
### List tasks
```bash
# All tasks
python3 scripts/taskboard.py list
# Filter by status
python3 scripts/taskboard.py list --status in-progress
# Filter by assignee
python3 scripts/taskboard.py list --assignee tech-lead
# Filter by priority
python3 scripts/taskboard.py list --priority high
```
### Update task status
```bash
python3 scripts/taskboard.py update PROJ-001 --status in-progress
python3 scripts/taskboard.py update PROJ-001 --status review --note "Ready for Tech Lead review"
python3 scripts/taskboard.py update PROJ-001 --status done
```
### Assign task
```bash
python3 scripts/taskboard.py assign PROJ-001 --to code-engineer
```
### Add note to task
```bash
python3 scripts/taskboard.py note PROJ-001 "Found edge case with expired tokens, added test"
```
### View task detail
```bash
python3 scripts/taskboard.py show PROJ-001
```
### Board summary (for cron jobs / Discord posting)
```bash
python3 scripts/taskboard.py summary
```
Output:
```
📋 Project Board: project-name
━━━━━━━━━━━━━━━━━━━━━━━━━━━
📥 Backlog: 3 tasks
🔄 In Progress: 2 tasks (code-engineer: 1, tech-lead: 1)
👀 Review: 1 task
✅ Done (this week): 4 tasks
🚫 Blocked: 0 tasks
🔥 High Priority:
PROJ-001 [in-progress] Implement user auth (code-engineer)
PROJ-005 [backlog] Fix payment webhook (unassigned)
```
## Cross-Agent Handoff Protocol
### When Tech Lead completes architecture:
1. `taskboard.py update PROJ-001 --status review --note "ADR written, ready for implementation"`
2. `taskboard.py create --title "Implement: [feature]" --assignee code-engineer --deps PROJ-001`
3. Notify Code Engineer's Discord channel
### When Code Engineer completes implementation:
1. `taskboard.py update PROJ-002 --status review --note "PR #42 ready for review"`
2. Notify Tech Lead's Discord channel for code review
### When review passes:
1. `taskboard.py update PROJ-002 --status done`
2. Check if downstream tasks are unblocked
3. Notify relevant agents
## Cron Integration
### Daily board check (morning)
```
Check taskboard.json for:
1. Tasks in-progress for >3 days (flag as potentially stuck)
2. Blocked tasks (check if blockers are resolved)
3. High-priority unassigned tasks
4. Send summary to #traveler-home Discord channel
```
### Weekly board review (Sunday)
```
Generate weekly report:
1. Tasks completed this week
2. Tasks carried over
3. Velocity trend (tasks done per week)
4. Blocked items needing human decision
```
## Minimum Check Interval
Do not poll the taskboard more frequently than every **5 minutes**.
FILE:scripts/taskboard.py
#!/usr/bin/env python3
"""Pluggable taskboard CLI for multi-agent projects.
Backends: local (JSON file), github (GitHub Issues).
Configure via taskboard.config.json or --config flag.
"""
import argparse
import json
import os
import sys
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError
STATUSES = ["backlog", "in-progress", "review", "done", "blocked", "rejected"]
PRIORITIES = ["low", "medium", "high", "critical"]
STATUS_EMOJI = {
"backlog": "📥", "in-progress": "🔄", "review": "👀",
"done": "✅", "blocked": "🚫", "rejected": "❌",
}
DEFAULT_CONFIG = "taskboard.config.json"
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
# ── Backend Interface ──────────────────────────────────────────────
class Backend(ABC):
@abstractmethod
def load(self) -> dict:
"""Return board dict with 'tasks' list and 'meta' dict."""
@abstractmethod
def save(self, board: dict):
"""Persist the board."""
@abstractmethod
def create_task(self, task: dict) -> dict:
"""Create a task, return it with any backend-assigned fields."""
@abstractmethod
def update_task(self, task_id: str, updates: dict) -> dict | None:
"""Update a task, return updated task or None if not found."""
@abstractmethod
def add_note(self, task_id: str, note: str) -> bool:
"""Add a note/comment to a task."""
# ── Local Backend ──────────────────────────────────────────────────
class LocalBackend(Backend):
def __init__(self, config: dict):
self.path = config.get("file", "taskboard.json")
def load(self) -> dict:
p = Path(self.path)
if not p.exists():
return {"tasks": [], "meta": {"project": "unnamed", "prefix": "TASK", "nextId": 1}}
with open(p) as f:
return json.load(f)
def save(self, board: dict):
with open(self.path, "w") as f:
json.dump(board, f, indent=2, ensure_ascii=False)
def create_task(self, task: dict) -> dict:
# ID assignment handled by caller
return task
def update_task(self, task_id: str, updates: dict) -> dict | None:
# Handled inline by caller for local
return updates
def add_note(self, task_id: str, note: str) -> bool:
return True
# ── GitHub Backend ─────────────────────────────────────────────────
class GitHubBackend(Backend):
"""Sync tasks with GitHub Issues. Local JSON is the cache."""
def __init__(self, config: dict):
self.repo = config["repo"] # "owner/repo"
self.token = os.environ.get(config.get("token_env", "GITHUB_TOKEN"), "")
self.label_map = config.get("label_mapping", {})
self.assignee_map = config.get("assignee_mapping", {})
self.cache_file = config.get("cache_file", "taskboard.json")
if not self.token:
print(f"⚠️ {config.get('token_env', 'GITHUB_TOKEN')} not set, GitHub sync disabled")
def _api(self, method: str, endpoint: str, data: dict = None) -> dict | list:
url = f"https://api.github.com/repos/{self.repo}/{endpoint}"
body = json.dumps(data).encode() if data else None
req = Request(url, data=body, method=method)
req.add_header("Authorization", f"token {self.token}")
req.add_header("Accept", "application/vnd.github.v3+json")
if body:
req.add_header("Content-Type", "application/json")
try:
with urlopen(req) as resp:
return json.loads(resp.read())
except HTTPError as e:
err = e.read().decode()
print(f"❌ GitHub API error ({e.code}): {err[:200]}")
return {}
def _status_to_labels(self, status: str) -> list[str]:
label = self.label_map.get(status, f"status:{status}")
return [label] if label else []
def _agent_to_gh_user(self, agent: str) -> str | None:
return self.assignee_map.get(agent)
def load(self) -> dict:
p = Path(self.cache_file)
if not p.exists():
return {"tasks": [], "meta": {"project": "unnamed", "prefix": "TASK", "nextId": 1}}
with open(p) as f:
return json.load(f)
def save(self, board: dict):
with open(self.cache_file, "w") as f:
json.dump(board, f, indent=2, ensure_ascii=False)
def create_task(self, task: dict) -> dict:
if not self.token:
return task
labels = self._status_to_labels(task.get("status", "backlog"))
labels += task.get("tags", [])
body = {
"title": task["title"],
"body": task.get("description", ""),
"labels": labels,
}
gh_user = self._agent_to_gh_user(task.get("assignee", ""))
if gh_user:
body["assignees"] = [gh_user]
result = self._api("POST", "issues", body)
if result.get("number"):
task["github_issue"] = result["number"]
task["github_url"] = result["html_url"]
print(f" 🔗 GitHub Issue #{result['number']}: {result['html_url']}")
return task
def update_task(self, task_id: str, updates: dict) -> dict | None:
if not self.token:
return updates
# Find the task to get github_issue number
board = self.load()
task = next((t for t in board["tasks"] if t["id"] == task_id), None)
if not task or not task.get("github_issue"):
return updates
issue_num = task["github_issue"]
body = {}
if "status" in updates:
new_labels = self._status_to_labels(updates["status"])
# Get current labels, remove old status labels, add new
current = self._api("GET", f"issues/{issue_num}")
old_labels = [l["name"] for l in current.get("labels", [])
if not l["name"].startswith("status:")]
body["labels"] = old_labels + new_labels
if updates["status"] == "done":
body["state"] = "closed"
elif updates["status"] != "done" and current.get("state") == "closed":
body["state"] = "open"
if "assignee" in updates:
gh_user = self._agent_to_gh_user(updates["assignee"])
if gh_user:
body["assignees"] = [gh_user]
if body:
self._api("PATCH", f"issues/{issue_num}", body)
print(f" 🔗 GitHub Issue #{issue_num} synced")
return updates
def add_note(self, task_id: str, note: str) -> bool:
if not self.token:
return True
board = self.load()
task = next((t for t in board["tasks"] if t["id"] == task_id), None)
if not task or not task.get("github_issue"):
return True
self._api("POST", f"issues/{task['github_issue']}/comments", {"body": note})
print(f" 🔗 Comment synced to GitHub Issue #{task['github_issue']}")
return True
# ── Config & Factory ───────────────────────────────────────────────
def load_config(path: str) -> dict:
p = Path(path)
if not p.exists():
return {"backend": "local", "local": {"file": "taskboard.json"}}
with open(p) as f:
return json.load(f)
def create_backend(config: dict) -> Backend:
backend_type = config.get("backend", "local")
if backend_type == "github":
return GitHubBackend(config.get("github", {}))
return LocalBackend(config.get("local", {"file": "taskboard.json"}))
# ── Commands ───────────────────────────────────────────────────────
def cmd_create(args, board: dict, backend: Backend):
meta = board["meta"]
task_id = f"{meta['prefix']}-{meta['nextId']:03d}"
meta["nextId"] += 1
task = {
"id": task_id,
"title": args.title,
"description": args.description or "",
"status": "backlog",
"assignee": args.assignee or None,
"priority": args.priority or "medium",
"dependencies": args.deps.split(",") if args.deps else [],
"tags": args.tags.split(",") if args.tags else [],
"created": now_iso(),
"updated": now_iso(),
"adr": args.adr or None,
"notes": [],
}
task = backend.create_task(task)
board["tasks"].append(task)
print(f"✅ Created {task_id}: {args.title}")
return board
def cmd_list(args, board: dict, backend: Backend):
tasks = board["tasks"]
if args.status:
tasks = [t for t in tasks if t["status"] == args.status]
if args.assignee:
tasks = [t for t in tasks if t.get("assignee") == args.assignee]
if args.priority:
tasks = [t for t in tasks if t.get("priority") == args.priority]
if not tasks:
print("No tasks found.")
return board
for t in tasks:
emoji = STATUS_EMOJI.get(t["status"], "❓")
pri = f" [{t.get('priority', 'medium')}]" if t.get("priority") != "medium" else ""
assignee = f" ({t['assignee']})" if t.get("assignee") else ""
gh = f" #{t['github_issue']}" if t.get("github_issue") else ""
print(f" {emoji} {t['id']}{pri} {t['title']}{assignee}{gh}")
return board
def cmd_update(args, board: dict, backend: Backend):
for t in board["tasks"]:
if t["id"] == args.task_id:
updates = {}
if args.status:
if args.status not in STATUSES:
print(f"❌ Invalid status. Choose from: {', '.join(STATUSES)}")
return board
t["status"] = args.status
updates["status"] = args.status
if args.assignee:
t["assignee"] = args.assignee
updates["assignee"] = args.assignee
if args.priority:
t["priority"] = args.priority
updates["priority"] = args.priority
if args.note:
t["notes"].append({"text": args.note, "at": now_iso()})
backend.add_note(args.task_id, args.note)
t["updated"] = now_iso()
backend.update_task(args.task_id, updates)
print(f"✅ Updated {args.task_id}")
return board
print(f"❌ Task {args.task_id} not found")
return board
def cmd_assign(args, board: dict, backend: Backend):
for t in board["tasks"]:
if t["id"] == args.task_id:
t["assignee"] = args.to
t["updated"] = now_iso()
backend.update_task(args.task_id, {"assignee": args.to})
print(f"✅ Assigned {args.task_id} → {args.to}")
return board
print(f"❌ Task {args.task_id} not found")
return board
def cmd_note(args, board: dict, backend: Backend):
for t in board["tasks"]:
if t["id"] == args.task_id:
t["notes"].append({"text": args.text, "at": now_iso()})
t["updated"] = now_iso()
backend.add_note(args.task_id, args.text)
print(f"✅ Note added to {args.task_id}")
return board
print(f"❌ Task {args.task_id} not found")
return board
def cmd_show(args, board: dict, backend: Backend):
for t in board["tasks"]:
if t["id"] == args.task_id:
print(f"{'─' * 40}")
print(f" ID: {t['id']}")
print(f" Title: {t['title']}")
print(f" Status: {t['status']}")
print(f" Priority: {t.get('priority', 'medium')}")
print(f" Assignee: {t.get('assignee') or 'unassigned'}")
if t.get("description"):
print(f" Desc: {t['description']}")
if t.get("dependencies"):
print(f" Deps: {', '.join(t['dependencies'])}")
if t.get("tags"):
print(f" Tags: {', '.join(t['tags'])}")
if t.get("adr"):
print(f" ADR: {t['adr']}")
if t.get("github_issue"):
print(f" GitHub: #{t['github_issue']} {t.get('github_url', '')}")
print(f" Created: {t['created']}")
print(f" Updated: {t['updated']}")
if t.get("notes"):
print(f" Notes:")
for n in t["notes"]:
print(f" [{n['at']}] {n['text']}")
print(f"{'─' * 40}")
return board
print(f"❌ Task {args.task_id} not found")
return board
def cmd_summary(args, board: dict, backend: Backend):
tasks = board["tasks"]
by_status = {}
for t in tasks:
by_status.setdefault(t["status"], []).append(t)
project = board["meta"].get("project", "unnamed")
print(f"📋 Project Board: {project}")
print("━" * 30)
for status in STATUSES:
items = by_status.get(status, [])
if items or status in ("backlog", "in-progress", "review", "done"):
print(f"{STATUS_EMOJI[status]} {status.title()}: {len(items)} tasks")
high = [t for t in tasks if t.get("priority") in ("high", "critical")
and t["status"] not in ("done", "rejected")]
if high:
print(f"\n🔥 High Priority:")
for t in high:
assignee = f" ({t['assignee']})" if t.get("assignee") else ""
print(f" {t['id']} [{t['status']}] {t['title']}{assignee}")
blocked = by_status.get("blocked", [])
if blocked:
print(f"\n🚫 Blocked:")
for t in blocked:
last_note = t["notes"][-1]["text"] if t.get("notes") else "no reason given"
print(f" {t['id']} {t['title']} — {last_note}")
return board
def cmd_init(args, board: dict, backend: Backend):
board["meta"]["project"] = args.project
board["meta"]["prefix"] = args.prefix or args.project.upper()[:4]
print(f"✅ Initialized board: {args.project} (prefix: {board['meta']['prefix']})")
return board
def cmd_config_init(args, board: dict, backend: Backend):
"""Generate a starter config file."""
config = {
"backend": args.backend,
"local": {"file": "taskboard.json"},
}
if args.backend == "github":
config["github"] = {
"repo": args.repo or "owner/repo",
"token_env": "GITHUB_TOKEN",
"cache_file": "taskboard.json",
"label_mapping": {
"backlog": "status:backlog",
"in-progress": "status:in-progress",
"review": "status:review",
"done": "status:done",
"blocked": "status:blocked",
},
"assignee_mapping": {},
}
path = args.output or DEFAULT_CONFIG
with open(path, "w") as f:
json.dump(config, f, indent=2)
print(f"✅ Config written to {path}")
if args.backend == "github":
print(f" Edit 'repo', 'assignee_mapping', and set config['github']['token_env']")
return board
# ── Main ───────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Taskboard CLI (pluggable backends)")
parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
sub = parser.add_subparsers(dest="command", required=True)
# config-init
p_cfg = sub.add_parser("config-init", help="Generate a config file")
p_cfg.add_argument("--backend", choices=["local", "github"], default="local")
p_cfg.add_argument("--repo", help="GitHub repo (owner/repo)")
p_cfg.add_argument("--output", help="Output config file path")
# init
p_init = sub.add_parser("init", help="Initialize a new board")
p_init.add_argument("project", help="Project name")
p_init.add_argument("--prefix", help="Task ID prefix")
# create
p_create = sub.add_parser("create", help="Create a task")
p_create.add_argument("--title", required=True)
p_create.add_argument("--description")
p_create.add_argument("--assignee")
p_create.add_argument("--priority", choices=PRIORITIES, default="medium")
p_create.add_argument("--deps", help="Comma-separated dependency task IDs")
p_create.add_argument("--tags", help="Comma-separated tags")
p_create.add_argument("--adr", help="Related ADR")
# list
p_list = sub.add_parser("list", help="List tasks")
p_list.add_argument("--status", choices=STATUSES)
p_list.add_argument("--assignee")
p_list.add_argument("--priority", choices=PRIORITIES)
# update
p_update = sub.add_parser("update", help="Update a task")
p_update.add_argument("task_id")
p_update.add_argument("--status", choices=STATUSES)
p_update.add_argument("--assignee")
p_update.add_argument("--priority", choices=PRIORITIES)
p_update.add_argument("--note")
# assign
p_assign = sub.add_parser("assign", help="Assign a task")
p_assign.add_argument("task_id")
p_assign.add_argument("--to", required=True)
# note
p_note = sub.add_parser("note", help="Add a note")
p_note.add_argument("task_id")
p_note.add_argument("text")
# show
p_show = sub.add_parser("show", help="Show task detail")
p_show.add_argument("task_id")
# summary
sub.add_parser("summary", help="Board summary")
args = parser.parse_args()
# Config-init doesn't need a backend
if args.command == "config-init":
cmd_config_init(args, {}, None)
return
config = load_config(args.config)
backend = create_backend(config)
board = backend.load()
commands = {
"init": cmd_init, "create": cmd_create, "list": cmd_list,
"update": cmd_update, "assign": cmd_assign, "note": cmd_note,
"show": cmd_show, "summary": cmd_summary,
}
board = commands[args.command](args, board, backend)
if args.command not in ("list", "show", "summary"):
backend.save(board)
if __name__ == "__main__":
main()