- Reset master to upstream/main (16,697 commits) - Overlay 2,271 local-only files (skills, tools, workspace, configs, apps) - Restore IDENTITY.md and USER.md templates - Build verified, gateway running, Discord working Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
358 lines
10 KiB
Bash
358 lines
10 KiB
Bash
#!/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 <command> [options]
|
|
|
|
COMMANDS:
|
|
create <type> <title> 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
|