#!/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