Add taskboard CLI tool for kanban orchestration (Phase 1 of plan 13)
This commit is contained in:
72
tools/taskboard/SKILL.md
Normal file
72
tools/taskboard/SKILL.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Taskboard — Kanban Task Management
|
||||||
|
|
||||||
|
Central orchestration tool for Atomizer HQ. All agents interact with the shared taskboard.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TB="/home/papa/atomizer/workspaces/shared/skills/taskboard/taskboard.sh"
|
||||||
|
|
||||||
|
# List your tasks
|
||||||
|
CALLER=my-agent-name bash "$TB" list --agent my-agent-name
|
||||||
|
|
||||||
|
# List by status or project
|
||||||
|
bash "$TB" list --status in-progress
|
||||||
|
bash "$TB" list --project gigabit-m2
|
||||||
|
|
||||||
|
# View task details
|
||||||
|
bash "$TB" view TASK-001
|
||||||
|
|
||||||
|
# Update task status (any agent can update their own tasks)
|
||||||
|
CALLER=my-agent-name bash "$TB" update TASK-001 --status in-progress --note "Started research"
|
||||||
|
CALLER=my-agent-name bash "$TB" update TASK-001 --status review --note "Draft posted to #technical"
|
||||||
|
|
||||||
|
# Kanban summary (counts per column)
|
||||||
|
bash "$TB" summary
|
||||||
|
|
||||||
|
# Kanban snapshot (markdown for Discord)
|
||||||
|
bash "$TB" snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manager-Only Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a task
|
||||||
|
CALLER=manager bash "$TB" create \
|
||||||
|
--title "Research CCZ thermal properties" \
|
||||||
|
--assignee webster \
|
||||||
|
--priority high \
|
||||||
|
--project gigabit-m2 \
|
||||||
|
--description "Full thermal comparison: CTE, conductivity for CCZ HS vs Zerodur" \
|
||||||
|
--deliverable-type analysis \
|
||||||
|
--deliverable-channel technical
|
||||||
|
|
||||||
|
# Complete a task
|
||||||
|
CALLER=manager bash "$TB" complete TASK-001 --note "Deliverable accepted"
|
||||||
|
|
||||||
|
# Cancel a task
|
||||||
|
CALLER=manager bash "$TB" cancel TASK-002 --reason "Superseded by new approach"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
backlog → todo → in-progress → review → done
|
||||||
|
↓
|
||||||
|
cancelled
|
||||||
|
```
|
||||||
|
|
||||||
|
## For Executor Agents (non-Manager)
|
||||||
|
|
||||||
|
On every session start:
|
||||||
|
1. Check your tasks: `CALLER=<you> bash "$TB" list --agent <you>`
|
||||||
|
2. If you have `todo` tasks: update to `in-progress` and start working
|
||||||
|
3. When work is done: update to `review` and post deliverable to the target Discord channel
|
||||||
|
4. Append progress to `shared/project_log.md`
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
- **taskboard.json is the single source of truth** — never edit it directly
|
||||||
|
- **Only Manager creates/completes/cancels tasks** — other agents update status
|
||||||
|
- **All writes are atomic** (tmp file + mv) with flock locking
|
||||||
|
- **Set CALLER env var** so your name appears in logs and notes
|
||||||
380
tools/taskboard/taskboard.sh
Executable file
380
tools/taskboard/taskboard.sh
Executable file
@@ -0,0 +1,380 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# taskboard.sh — Kanban task management for Atomizer HQ
|
||||||
|
# Usage: taskboard.sh <command> [options]
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# list [--agent <name>] [--status <s>] [--project <tag>]
|
||||||
|
# create --title '...' --assignee <agent> --priority <p> --project <tag> --description '...' [--deliverable-type <type>] [--deliverable-channel <ch>] [--due <iso>]
|
||||||
|
# update <task-id> --status <new> [--note '...']
|
||||||
|
# complete <task-id> [--note '...']
|
||||||
|
# cancel <task-id> --reason '...'
|
||||||
|
# view <task-id>
|
||||||
|
# summary
|
||||||
|
# snapshot
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# CALLER — agent name (used for logging, optional)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TASKBOARD="/home/papa/atomizer/workspaces/shared/taskboard.json"
|
||||||
|
LOCKFILE="/home/papa/atomizer/workspaces/shared/.taskboard.lock"
|
||||||
|
PROJECT_LOG="/home/papa/atomizer/workspaces/shared/project_log.md"
|
||||||
|
CALLER="${CALLER:-unknown}"
|
||||||
|
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
NOW_LOG=$(date -u +"%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
die() { echo "❌ $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Atomic write: write to tmp, then mv
|
||||||
|
atomic_write() {
|
||||||
|
local tmp="${TASKBOARD}.tmp.$$"
|
||||||
|
cat > "$tmp"
|
||||||
|
mv -f "$tmp" "$TASKBOARD"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write lock (flock on fd 9)
|
||||||
|
lock_write() {
|
||||||
|
exec 9>"$LOCKFILE"
|
||||||
|
flock -w 5 9 || die "Could not acquire lock"
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock_write() {
|
||||||
|
flock -u 9 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Append to project log
|
||||||
|
log_entry() {
|
||||||
|
echo "[$NOW_LOG] [$CALLER] $*" >> "$PROJECT_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get next task ID
|
||||||
|
next_id() {
|
||||||
|
local max
|
||||||
|
max=$(jq -r '[.tasks[].id | ltrimstr("TASK-") | tonumber] | max // 0' "$TASKBOARD")
|
||||||
|
printf "TASK-%03d" $(( max + 1 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Commands ---
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
local agent="" status="" project=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--agent) agent="$2"; shift 2 ;;
|
||||||
|
--status) status="$2"; shift 2 ;;
|
||||||
|
--project) project="$2"; shift 2 ;;
|
||||||
|
*) die "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local filter=".tasks"
|
||||||
|
[[ -n "$agent" ]] && filter="$filter | map(select(.assignee == \"$agent\"))"
|
||||||
|
[[ -n "$status" ]] && filter="$filter | map(select(.status == \"$status\"))"
|
||||||
|
[[ -n "$project" ]] && filter="$filter | map(select(.project == \"$project\"))"
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(jq -r "$filter | .[] | \"[\(.status | ascii_upcase)] \(.id) [\(.priority)] \(.title) → \(.assignee)\"" "$TASKBOARD")
|
||||||
|
|
||||||
|
if [[ -z "$result" ]]; then
|
||||||
|
echo "No tasks found."
|
||||||
|
else
|
||||||
|
echo "$result"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_create() {
|
||||||
|
local title="" assignee="" priority="medium" project="" description=""
|
||||||
|
local del_type="" del_channel="" due="null"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--title) title="$2"; shift 2 ;;
|
||||||
|
--assignee) assignee="$2"; shift 2 ;;
|
||||||
|
--priority) priority="$2"; shift 2 ;;
|
||||||
|
--project) project="$2"; shift 2 ;;
|
||||||
|
--description) description="$2"; shift 2 ;;
|
||||||
|
--deliverable-type) del_type="$2"; shift 2 ;;
|
||||||
|
--deliverable-channel) del_channel="$2"; shift 2 ;;
|
||||||
|
--due) due="\"$2\""; shift 2 ;;
|
||||||
|
*) die "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$title" ]] && die "Missing --title"
|
||||||
|
[[ -z "$assignee" ]] && die "Missing --assignee"
|
||||||
|
[[ -z "$project" ]] && die "Missing --project"
|
||||||
|
|
||||||
|
lock_write
|
||||||
|
|
||||||
|
local id
|
||||||
|
id=$(next_id)
|
||||||
|
|
||||||
|
# Build deliverable object
|
||||||
|
local deliverable="null"
|
||||||
|
if [[ -n "$del_type" || -n "$del_channel" ]]; then
|
||||||
|
deliverable=$(jq -n \
|
||||||
|
--arg t "${del_type:-document}" \
|
||||||
|
--arg c "${del_channel:-reports}" \
|
||||||
|
'{type: $t, targetChannel: $c, format: ""}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add task
|
||||||
|
jq --arg id "$id" \
|
||||||
|
--arg title "$title" \
|
||||||
|
--arg desc "$description" \
|
||||||
|
--arg status "todo" \
|
||||||
|
--arg priority "$priority" \
|
||||||
|
--arg assignee "$assignee" \
|
||||||
|
--arg requestedBy "$CALLER" \
|
||||||
|
--arg project "$project" \
|
||||||
|
--argjson deliverable "$deliverable" \
|
||||||
|
--arg created "$NOW" \
|
||||||
|
--arg updated "$NOW" \
|
||||||
|
--argjson dueBy "$due" \
|
||||||
|
'.tasks += [{
|
||||||
|
id: $id,
|
||||||
|
title: $title,
|
||||||
|
description: $desc,
|
||||||
|
status: $status,
|
||||||
|
priority: $priority,
|
||||||
|
assignee: $assignee,
|
||||||
|
requestedBy: $requestedBy,
|
||||||
|
project: $project,
|
||||||
|
deliverable: $deliverable,
|
||||||
|
created: $created,
|
||||||
|
updated: $updated,
|
||||||
|
dueBy: $dueBy,
|
||||||
|
notes: [],
|
||||||
|
completedAt: null
|
||||||
|
}] | .lastUpdated = $created | .updatedBy = $requestedBy' \
|
||||||
|
"$TASKBOARD" | atomic_write
|
||||||
|
|
||||||
|
unlock_write
|
||||||
|
log_entry "$id: Created — $title (assigned to $assignee)"
|
||||||
|
echo "✅ Created $id: $title → $assignee"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
local task_id="${1:-}"; shift || die "Usage: update <task-id> --status <s> [--note '...']"
|
||||||
|
local status="" note=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--status) status="$2"; shift 2 ;;
|
||||||
|
--note) note="$2"; shift 2 ;;
|
||||||
|
*) die "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$status" ]] && die "Missing --status"
|
||||||
|
|
||||||
|
# Validate status
|
||||||
|
case "$status" in
|
||||||
|
backlog|todo|in-progress|review|done|cancelled) ;;
|
||||||
|
*) die "Invalid status: $status (must be backlog|todo|in-progress|review|done|cancelled)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
lock_write
|
||||||
|
|
||||||
|
# Check task exists
|
||||||
|
local exists
|
||||||
|
exists=$(jq -r --arg id "$task_id" '.tasks | map(select(.id == $id)) | length' "$TASKBOARD")
|
||||||
|
[[ "$exists" -eq 0 ]] && { unlock_write; die "Task $task_id not found"; }
|
||||||
|
|
||||||
|
local note_entry=""
|
||||||
|
[[ -n "$note" ]] && note_entry="[$NOW_LOG] [$CALLER] $note"
|
||||||
|
|
||||||
|
jq --arg id "$task_id" \
|
||||||
|
--arg status "$status" \
|
||||||
|
--arg updated "$NOW" \
|
||||||
|
--arg updatedBy "$CALLER" \
|
||||||
|
--arg note "$note_entry" \
|
||||||
|
'(.tasks[] | select(.id == $id)) |= (
|
||||||
|
.status = $status |
|
||||||
|
.updated = $updated |
|
||||||
|
if $note != "" then .notes += [$note] else . end
|
||||||
|
) | .lastUpdated = $updated | .updatedBy = $updatedBy' \
|
||||||
|
"$TASKBOARD" | atomic_write
|
||||||
|
|
||||||
|
unlock_write
|
||||||
|
log_entry "$task_id: Status → $status${note:+ — $note}"
|
||||||
|
echo "✅ $task_id → $status"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_complete() {
|
||||||
|
local task_id="${1:-}"; shift || die "Usage: complete <task-id> [--note '...']"
|
||||||
|
local note=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--note) note="$2"; shift 2 ;;
|
||||||
|
*) die "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
lock_write
|
||||||
|
|
||||||
|
local exists
|
||||||
|
exists=$(jq -r --arg id "$task_id" '.tasks | map(select(.id == $id)) | length' "$TASKBOARD")
|
||||||
|
[[ "$exists" -eq 0 ]] && { unlock_write; die "Task $task_id not found"; }
|
||||||
|
|
||||||
|
local note_entry=""
|
||||||
|
[[ -n "$note" ]] && note_entry="[$NOW_LOG] [$CALLER] $note"
|
||||||
|
|
||||||
|
jq --arg id "$task_id" \
|
||||||
|
--arg updated "$NOW" \
|
||||||
|
--arg updatedBy "$CALLER" \
|
||||||
|
--arg note "$note_entry" \
|
||||||
|
'(.tasks[] | select(.id == $id)) |= (
|
||||||
|
.status = "done" |
|
||||||
|
.updated = $updated |
|
||||||
|
.completedAt = $updated |
|
||||||
|
if $note != "" then .notes += [$note] else . end
|
||||||
|
) | .lastUpdated = $updated | .updatedBy = $updatedBy' \
|
||||||
|
"$TASKBOARD" | atomic_write
|
||||||
|
|
||||||
|
unlock_write
|
||||||
|
log_entry "$task_id: Completed${note:+ — $note}"
|
||||||
|
echo "✅ $task_id → done"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_cancel() {
|
||||||
|
local task_id="${1:-}"; shift || die "Usage: cancel <task-id> --reason '...'"
|
||||||
|
local reason=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--reason) reason="$2"; shift 2 ;;
|
||||||
|
*) die "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$reason" ]] && die "Missing --reason"
|
||||||
|
|
||||||
|
lock_write
|
||||||
|
|
||||||
|
local exists
|
||||||
|
exists=$(jq -r --arg id "$task_id" '.tasks | map(select(.id == $id)) | length' "$TASKBOARD")
|
||||||
|
[[ "$exists" -eq 0 ]] && { unlock_write; die "Task $task_id not found"; }
|
||||||
|
|
||||||
|
local note_entry="[$NOW_LOG] [$CALLER] Cancelled: $reason"
|
||||||
|
|
||||||
|
jq --arg id "$task_id" \
|
||||||
|
--arg updated "$NOW" \
|
||||||
|
--arg updatedBy "$CALLER" \
|
||||||
|
--arg note "$note_entry" \
|
||||||
|
'(.tasks[] | select(.id == $id)) |= (
|
||||||
|
.status = "cancelled" |
|
||||||
|
.updated = $updated |
|
||||||
|
.notes += [$note]
|
||||||
|
) | .lastUpdated = $updated | .updatedBy = $updatedBy' \
|
||||||
|
"$TASKBOARD" | atomic_write
|
||||||
|
|
||||||
|
unlock_write
|
||||||
|
log_entry "$task_id: Cancelled — $reason"
|
||||||
|
echo "✅ $task_id → cancelled"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_view() {
|
||||||
|
local task_id="${1:-}"; [[ -z "$task_id" ]] && die "Usage: view <task-id>"
|
||||||
|
|
||||||
|
local task
|
||||||
|
task=$(jq -r --arg id "$task_id" '.tasks[] | select(.id == $id)' "$TASKBOARD")
|
||||||
|
[[ -z "$task" ]] && die "Task $task_id not found"
|
||||||
|
|
||||||
|
echo "$task" | jq -r '
|
||||||
|
"═══════════════════════════════════════",
|
||||||
|
" \(.id) — \(.title)",
|
||||||
|
"═══════════════════════════════════════",
|
||||||
|
" Status: \(.status)",
|
||||||
|
" Priority: \(.priority)",
|
||||||
|
" Assignee: \(.assignee)",
|
||||||
|
" Requested by: \(.requestedBy)",
|
||||||
|
" Project: \(.project)",
|
||||||
|
" Created: \(.created)",
|
||||||
|
" Updated: \(.updated)",
|
||||||
|
" Due: \(.dueBy // "none")",
|
||||||
|
" Completed: \(.completedAt // "—")",
|
||||||
|
"",
|
||||||
|
" Description:",
|
||||||
|
" \(.description)",
|
||||||
|
"",
|
||||||
|
if (.deliverable != null) then
|
||||||
|
" Deliverable:",
|
||||||
|
" Type: \(.deliverable.type)",
|
||||||
|
" Channel: #\(.deliverable.targetChannel)",
|
||||||
|
""
|
||||||
|
else "" end,
|
||||||
|
if ((.notes | length) > 0) then
|
||||||
|
" Notes:",
|
||||||
|
(.notes[] | " • \(.)")
|
||||||
|
else " Notes: (none)" end
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_summary() {
|
||||||
|
echo "📋 Taskboard Summary"
|
||||||
|
echo "════════════════════"
|
||||||
|
jq -r '
|
||||||
|
.tasks | group_by(.status) | map({
|
||||||
|
status: .[0].status,
|
||||||
|
count: length
|
||||||
|
}) | sort_by(.status) | .[] |
|
||||||
|
" \(.status | ascii_upcase): \(.count)"
|
||||||
|
' "$TASKBOARD"
|
||||||
|
|
||||||
|
local total
|
||||||
|
total=$(jq '.tasks | length' "$TASKBOARD")
|
||||||
|
echo "────────────────────"
|
||||||
|
echo " TOTAL: $total"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_snapshot() {
|
||||||
|
# Markdown kanban for Discord posting
|
||||||
|
echo "# 📋 Taskboard Snapshot"
|
||||||
|
echo "_Updated: $(date -u +"%Y-%m-%d %H:%M UTC")_"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for status in backlog todo in-progress review done; do
|
||||||
|
local label
|
||||||
|
case "$status" in
|
||||||
|
backlog) label="📥 Backlog" ;;
|
||||||
|
todo) label="📌 To Do" ;;
|
||||||
|
in-progress) label="🔄 In Progress" ;;
|
||||||
|
review) label="🔍 Review" ;;
|
||||||
|
done) label="✅ Done" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local tasks
|
||||||
|
tasks=$(jq -r --arg s "$status" '
|
||||||
|
.tasks | map(select(.status == $s)) |
|
||||||
|
if length == 0 then "_(empty)_"
|
||||||
|
else .[] | "- **\(.id)** \(.title) → `\(.assignee)` [\(.priority)]"
|
||||||
|
end
|
||||||
|
' "$TASKBOARD")
|
||||||
|
|
||||||
|
echo "## $label"
|
||||||
|
echo "$tasks"
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
|
||||||
|
cmd="${1:-}"; shift || die "Usage: taskboard.sh <command> [options]
|
||||||
|
Commands: list, create, update, complete, cancel, view, summary, snapshot"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
list) cmd_list "$@" ;;
|
||||||
|
create) cmd_create "$@" ;;
|
||||||
|
update) cmd_update "$@" ;;
|
||||||
|
complete) cmd_complete "$@" ;;
|
||||||
|
cancel) cmd_cancel "$@" ;;
|
||||||
|
view) cmd_view "$@" ;;
|
||||||
|
summary) cmd_summary ;;
|
||||||
|
snapshot) cmd_snapshot ;;
|
||||||
|
*) die "Unknown command: $cmd" ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user