diff options
| -rw-r--r-- | scripts/ct-task | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/scripts/ct-task b/scripts/ct-task new file mode 100644 index 0000000..cd3388a --- /dev/null +++ b/scripts/ct-task @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# ct-task: Query, retry, cancel, or reset a Claudomator task by ID prefix. +# Usage: +# ct-task <id-prefix> — show task state + latest execution error +# ct-task <id-prefix> retry — reset to PENDING and queue +# ct-task <id-prefix> run — queue (must already be PENDING) +# ct-task <id-prefix> reset — reset state to PENDING only +# ct-task <id-prefix> cancel — cancel task (works from any state) +# ct-task list [state] — list tasks (optional: filter by state) +# ct-task cancel-all-failed — cancel every FAILED task +# +# Examples: +# ct-task f8829d6f +# ct-task f8829d6f retry +# ct-task f8829d6f cancel +# ct-task list FAILED +# ct-task cancel-all-failed + +set -euo pipefail + +API="http://localhost:8484" +DB="/site/doot.terst.org/data/claudomator.db" + +_jq() { python3 -c "import sys,json; d=json.load(sys.stdin); $1"; } + +task_id_from_prefix() { + local prefix="$1" + local id + id=$(sqlite3 "$DB" "SELECT id FROM tasks WHERE id LIKE '${prefix}%' ORDER BY created_at DESC LIMIT 1;") + if [[ -z "$id" ]]; then + echo "ERROR: no task found matching '${prefix}'" >&2 + exit 1 + fi + echo "$id" +} + +cmd_show() { + local id + id=$(task_id_from_prefix "$1") + local task + task=$(curl -sf "$API/api/tasks/$id") + echo "$task" | _jq " +t=d +print('ID: ', t['id']) +print('Name: ', t['name']) +print('State: ', t['state']) +print('Project:', t.get('project','')) +summary = t.get('summary','') +if summary: print('Summary:', summary) +rejection = t.get('rejection_comment','') +if rejection: print('Rejection:', rejection) +" + + # Latest execution error + local execs + execs=$(curl -sf "$API/api/tasks/$id/executions" 2>/dev/null || echo "[]") + local count + count=$(echo "$execs" | _jq "print(len(d))") + if [[ "$count" -gt 0 ]]; then + echo "" + echo "$execs" | _jq " +e=d[0] +print('Execution:', e['ID']) +print('Status: ', e['Status']) +print('Exit code:', e['ExitCode']) +err = e.get('ErrorMsg','') +if err: print('Error: ', err) +cost = e.get('CostUSD',0) +if cost: print('Cost: ', f'\${cost:.4f}') +" + fi +} + +cmd_reset() { + local id + id=$(task_id_from_prefix "$1") + sqlite3 "$DB" "UPDATE tasks SET state='PENDING' WHERE id='$id';" + echo "Reset $id to PENDING" +} + +cmd_run() { + local id + id=$(task_id_from_prefix "$1") + local resp + resp=$(curl -sf -X POST "$API/api/tasks/$id/run" || true) + local state + state=$(curl -sf "$API/api/tasks/$id" | _jq "print(d['state'])") + echo "$id → $state" +} + +cmd_retry() { + local id + id=$(task_id_from_prefix "$1") + sqlite3 "$DB" "UPDATE tasks SET state='PENDING' WHERE id='$id';" + curl -sf -X POST "$API/api/tasks/$id/run" >/dev/null || true + local state + state=$(curl -sf "$API/api/tasks/$id" | _jq "print(d['state'])") + echo "$id → $state" +} + +cmd_cancel() { + local id + id=$(task_id_from_prefix "$1") + # Try API cancel first (works for PENDING/QUEUED/RUNNING/BLOCKED) + local http_status + http_status=$(curl -sf -o /dev/null -w "%{http_code}" -X POST "$API/api/tasks/$id/cancel" 2>/dev/null || echo "000") + if [[ "$http_status" == "200" ]]; then + echo "$id → CANCELLED" + return + fi + # Terminal states (FAILED, TIMED_OUT, etc.) can't transition via API — force via DB. + sqlite3 "$DB" "UPDATE tasks SET state='CANCELLED' WHERE id='$id';" + echo "$id → CANCELLED (forced)" +} + +cmd_cancel_all_failed() { + local ids + ids=$(sqlite3 "$DB" "SELECT id FROM tasks WHERE state='FAILED';") + if [[ -z "$ids" ]]; then + echo "No FAILED tasks." + return + fi + while IFS= read -r id; do + sqlite3 "$DB" "UPDATE tasks SET state='CANCELLED' WHERE id='$id';" + echo "${id:0:8}… → CANCELLED" + done <<< "$ids" +} + +cmd_list() { + local filter="${1:-}" + local tasks + tasks=$(curl -sf "$API/api/tasks") + if [[ -n "$filter" ]]; then + echo "$tasks" | _jq " +for t in d: + if t['state'] == '${filter}'.upper() or '${filter}'.upper() in t['state']: + print(t['id'][:8], t['state'].ljust(12), t['name'][:60]) +" + else + echo "$tasks" | _jq " +for t in d: + print(t['id'][:8], t['state'].ljust(12), t['name'][:60]) +" + fi +} + +# Dispatch +PREFIX="${1:-}" +SUBCMD="${2:-show}" + +if [[ -z "$PREFIX" ]]; then + echo "Usage: ct-task <id-prefix> [show|retry|run|reset|cancel] OR ct-task list [state] OR ct-task cancel-all-failed" >&2 + exit 1 +fi + +case "$PREFIX" in + list) cmd_list "${2:-}" ;; + cancel-all-failed) cmd_cancel_all_failed ;; + *) + case "$SUBCMD" in + show|"") cmd_show "$PREFIX" ;; + retry) cmd_retry "$PREFIX" ;; + run) cmd_run "$PREFIX" ;; + reset) cmd_reset "$PREFIX" ;; + cancel) cmd_cancel "$PREFIX" ;; + *) echo "Unknown subcommand: $SUBCMD" >&2; exit 1 ;; + esac + ;; +esac |
