Files
Atomizer/tools/taskboard/taskboard.sh

381 lines
11 KiB
Bash
Executable File

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