From c3125b458bdaf5f0bf306b7ce92889e2b1bb71cb Mon Sep 17 00:00:00 2001 From: Antoine Date: Tue, 17 Feb 2026 01:39:33 +0000 Subject: [PATCH] Add taskboard CLI tool for kanban orchestration (Phase 1 of plan 13) --- tools/taskboard/SKILL.md | 72 +++++++ tools/taskboard/taskboard.sh | 380 +++++++++++++++++++++++++++++++++++ 2 files changed, 452 insertions(+) create mode 100644 tools/taskboard/SKILL.md create mode 100755 tools/taskboard/taskboard.sh diff --git a/tools/taskboard/SKILL.md b/tools/taskboard/SKILL.md new file mode 100644 index 00000000..d6ff144c --- /dev/null +++ b/tools/taskboard/SKILL.md @@ -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= bash "$TB" list --agent ` +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 diff --git a/tools/taskboard/taskboard.sh b/tools/taskboard/taskboard.sh new file mode 100755 index 00000000..695c5746 --- /dev/null +++ b/tools/taskboard/taskboard.sh @@ -0,0 +1,380 @@ +#!/usr/bin/env bash +# taskboard.sh — Kanban task management for Atomizer HQ +# Usage: taskboard.sh [options] +# +# Commands: +# list [--agent ] [--status ] [--project ] +# create --title '...' --assignee --priority

--project --description '...' [--deliverable-type ] [--deliverable-channel ] [--due ] +# update --status [--note '...'] +# complete [--note '...'] +# cancel --reason '...' +# view +# 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 --status [--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 [--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 --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 " + + 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 [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