#!/usr/bin/env bash # Kanban CLI - Task Management with Hierarchy Support # Part of Epic 335: Kanban Development Pipeline set -euo pipefail API_BASE="http://127.0.0.1:5003/api" TAXONOMY="/tmp/kanban-taxonomy.md" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color error() { echo -e "${RED}❌ $*${NC}" >&2; exit 1; } success() { echo -e "${GREEN}✅ $*${NC}"; } warn() { echo -e "${YELLOW}⚠️ $*${NC}"; } usage() { cat << EOF Kanban CLI - Task Management with Hierarchy USAGE: kanban-cli [options] COMMANDS: create Create task (epic|story|task|subtask) update <id> Update task fields link <id> --parent <ref> Link task to parent (epic:305) list [--type <t>] List tasks view <id> Show task details delete <id> Delete task validate <id> Validate task structure OPTIONS: --title <text> Task title --description <text> Task description --tags <csv> Comma-separated tags --parent <ref> Parent reference (epic:335, story:307) --priority <0-3> Priority level --status <status> Task status (queue|in_progress|done) --agent <agent> Agent assignment --size <xs|s|m|l|xl> Task size --setup <setup> Setup requirements EXAMPLES: # Create epic kanban-cli create epic "Feature X" --tags "size:xl,agent:nvidia" --priority 3 # Create story under epic kanban-cli create story "Implement API" --parent epic:335 --size l # Create task under story kanban-cli create task "Write tests" --parent story:336 --size s # Link existing task kanban-cli link 307 --parent epic:305 # Update tags kanban-cli update 333 --tags "priority:critical,agent:nvidia" # List by type kanban-cli list --type epic kanban-cli list --agent gemini-flash --size s TAXONOMY: type: epic, story, task, subtask size: xs, s, m, l, xl setup: local, pi, openclaw, external, skill, docs agent: gemini-flash, gemini-pro, nvidia, deepseek, human domain: security, reddit, onboarding, cost, home-automation, development priority: 0-3 (low, medium, high, critical) EOF exit 0 } validate_tag() { local tag="$1" # Extract category and value if [[ ! "$tag" =~ ^([a-z-]+):([a-z0-9-]+)$ ]]; then warn "Invalid tag format: $tag (expected category:value)" return 1 fi local cat="${BASH_REMATCH[1]}" local val="${BASH_REMATCH[2]}" # Validate against taxonomy case "$cat" in type) [[ "$val" =~ ^(epic|story|task|subtask)$ ]] || { warn "Invalid type: $val"; return 1; } ;; size) [[ "$val" =~ ^(xs|s|m|l|xl)$ ]] || { warn "Invalid size: $val"; return 1; } ;; setup) [[ "$val" =~ ^(local|pi|openclaw|external|skill|docs)$ ]] || { warn "Invalid setup: $val"; return 1; } ;; agent) [[ "$val" =~ ^(gemini-flash|gemini-pro|nvidia|deepseek|human)$ ]] || { warn "Invalid agent: $val"; return 1; } ;; domain) [[ "$val" =~ ^(security|reddit|onboarding|cost|home-automation|development)$ ]] || { warn "Invalid domain: $val"; return 1; } ;; priority) [[ "$val" =~ ^(low|medium|high|critical)$ ]] || { warn "Invalid priority: $val"; return 1; } ;; *) # Allow unknown categories (for flexibility) warn "Unknown category: $cat (allowed but not in taxonomy)" ;; esac return 0 } validate_tags() { local tags="$1" IFS=',' read -ra TAG_ARRAY <<< "$tags" for tag in "${TAG_ARRAY[@]}"; do tag=$(echo "$tag" | xargs) # trim whitespace validate_tag "$tag" || return 1 done return 0 } cmd_create() { local type="$1"; shift local title="$1"; shift # Parse options local tags="" description="" parent="" priority=0 status="queue" while [[ $# -gt 0 ]]; do case "$1" in --tags) tags="$2"; shift 2;; --description) description="$2"; shift 2;; --parent) parent="$2"; shift 2;; --priority) priority="$2"; shift 2;; --status) status="$2"; shift 2;; --agent) tags="$tags,agent:$2"; shift 2;; --size) tags="$tags,size:$2"; shift 2;; --setup) tags="$tags,setup:$2"; shift 2;; *) error "Unknown option: $1";; esac done # Add type tag tags="type:$type${tags:+,$tags}" # Validate tags validate_tags "$tags" || error "Tag validation failed" # Build JSON local json=$(jq -n \ --arg title "$title" \ --arg desc "$description" \ --arg tags "$tags" \ --arg status "$status" \ --arg parent "$parent" \ --argjson priority "$priority" \ '{ title: $title, description: $desc, tags: $tags, status: $status, external_id: (if $parent == "" then null else $parent end), priority: $priority }') # Create task local response=$(curl -sS -X POST "$API_BASE/tasks" \ -H "Content-Type: application/json" \ -d "$json") local id=$(echo "$response" | jq -r '.id') if [[ "$id" == "null" ]]; then error "Failed to create task: $(echo "$response" | jq -r '.error // .message // "Unknown error"')" fi success "Created $type [$id]: $title" # Fetch back to show actual persisted data local created=$(curl -sS "$API_BASE/tasks?status=queue" | jq --arg id "$id" '.[] | select(.id == ($id | tonumber))') echo "$created" | jq -r '" Tags: \(.tags // "pending...")\n Parent: \(.external_id // "none")\n Priority: \(.priority)"' } cmd_update() { local id="$1"; shift # Parse options local title="" description="" tags="" status="" priority="" while [[ $# -gt 0 ]]; do case "$1" in --title) title="$2"; shift 2;; --description) description="$2"; shift 2;; --tags) tags="$2"; shift 2;; --status) status="$2"; shift 2;; --priority) priority="$2"; shift 2;; *) error "Unknown option: $1";; esac done # Validate tags if provided if [[ -n "$tags" ]]; then validate_tags "$tags" || error "Tag validation failed" fi # Build JSON local json="{}" [[ -n "$title" ]] && json=$(jq --arg v "$title" '. + {title: $v}' <<< "$json") [[ -n "$description" ]] && json=$(jq --arg v "$description" '. + {description: $v}' <<< "$json") [[ -n "$tags" ]] && json=$(jq --arg v "$tags" '. + {tags: $v}' <<< "$json") [[ -n "$status" ]] && json=$(jq --arg v "$status" '. + {status: $v}' <<< "$json") [[ -n "$priority" ]] && json=$(jq --argjson v "$priority" '. + {priority: $v}' <<< "$json") # Update task local response=$(curl -sS -X PATCH "$API_BASE/tasks/$id" \ -H "Content-Type: application/json" \ -d "$json") local updated_id=$(echo "$response" | jq -r '.id') if [[ "$updated_id" == "null" ]]; then error "Failed to update task: $(echo "$response" | jq -r '.error // .message // "Unknown error"')" fi success "Updated task [$id]" echo "$response" | jq -r '" Title: \(.title)\n Tags: \(.tags)\n Status: \(.status)"' } cmd_link() { local id="$1" local parent="" shift while [[ $# -gt 0 ]]; do case "$1" in --parent) parent="$2"; shift 2;; *) error "Unknown option: $1";; esac done [[ -z "$parent" ]] && error "Parent reference required (--parent epic:305)" # Update external_id local response=$(curl -sS -X PATCH "$API_BASE/tasks/$id" \ -H "Content-Type: application/json" \ -d "{\"external_id\": \"$parent\"}") local updated_id=$(echo "$response" | jq -r '.id') if [[ "$updated_id" == "null" ]]; then error "Failed to link task: $(echo "$response" | jq -r '.error // .message // "Unknown error"')" fi success "Linked task [$id] → $parent" } cmd_list() { local filters="status=queue" while [[ $# -gt 0 ]]; do case "$1" in --type) filters="$filters&type=$2"; shift 2;; --agent) filters="$filters&agent=$2"; shift 2;; --size) filters="$filters&size=$2"; shift 2;; --status) filters="${filters/status=queue/status=$2}"; shift 2;; *) error "Unknown filter: $1";; esac done curl -sS "$API_BASE/tasks?$filters" | \ jq -r '.[] | "[\(.id)] \(.title)\n Tags: \(.tags // "none")\n Parent: \(.external_id // "none")\n"' } cmd_view() { local id="$1" # Use batch endpoint (single-task GET is broken) curl -sS "$API_BASE/tasks?status=queue" | \ jq -r --arg id "$id" '.[] | select(.id == ($id | tonumber)) | "[\(.id)] \(.title)\n\nDescription:\n\(.description // "none")\n\nTags: \(.tags // "none")\nParent: \(.external_id // "none")\nStatus: \(.status)\nPriority: \(.priority)\nCreated: \(.created_at)\nUpdated: \(.updated_at)"' } cmd_delete() { local id="$1" echo "Delete task $id? (y/N)" read -r confirm if [[ "${confirm,,}" != "y" ]]; then echo "Aborted" exit 0 fi curl -sS -X DELETE "$API_BASE/tasks/$id" success "Deleted task [$id]" } cmd_validate() { local id="$1" # Get task (use batch endpoint) local task=$(curl -sS "$API_BASE/tasks?status=queue" | jq --arg id "$id" '.[] | select(.id == ($id | tonumber))') local title=$(echo "$task" | jq -r '.title') local tags=$(echo "$task" | jq -r '.tags // ""') local parent=$(echo "$task" | jq -r '.external_id // ""') echo "Validating task [$id]: $title" echo "" # Check tags if [[ -z "$tags" ]]; then warn "No tags set" else echo "Validating tags..." validate_tags "$tags" && success "Tags valid" || error "Tags invalid" fi # Check parent link if [[ -n "$parent" ]]; then echo "Checking parent link: $parent" local parent_id=$(echo "$parent" | grep -oP ':\K[0-9]+') if curl -sS "$API_BASE/tasks?status=queue" | jq --arg id "$parent_id" '.[] | select(.id == ($id | tonumber))' | jq -e '.id' >/dev/null 2>&1; then success "Parent exists" else error "Parent not found: $parent" fi fi success "Task validated" } # Main if [[ $# -eq 0 ]]; then usage; fi cmd="$1"; shift case "$cmd" in create) cmd_create "$@";; update) cmd_update "$@";; link) cmd_link "$@";; list) cmd_list "$@";; view) cmd_view "$@";; delete) cmd_delete "$@";; validate) cmd_validate "$@";; help|--help|-h) usage;; *) error "Unknown command: $cmd (try 'help')";; esac