Files
openclaw/tools/kanban-cli.sh
Clawd Bot ca9b510922 chore: align with upstream openclaw/openclaw and overlay local additions
- 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>
2026-03-03 07:40:46 +01:00

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