summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-21 20:59:57 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-21 20:59:57 +0000
commita10e7478a130d6453abbd8fb0694948785dd2155 (patch)
tree9f9e823f005f59948cf12b84a4bdef548caab6be
parentc408c21c81d353d2f08c4b1c63118c6bd7b48061 (diff)
feat: add cancel and cancel-all-failed to ct-task
Adds ct-task cancel <prefix> (works from any state, falls back to direct DB update for terminal states) and ct-task cancel-all-failed to clear out stuck FAILED tasks in bulk. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--scripts/ct-task169
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