feat: add work-queue skill for Marcus and Steve
Some checks failed
ci / build-test-push (push) Failing after 29s
Some checks failed
ci / build-test-push (push) Failing after 29s
This commit is contained in:
115
skill/SKILL.md
Normal file
115
skill/SKILL.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: work-queue
|
||||
description: "Work queue skill for TheLab agents. Submit, dispatch, track, and complete work items via the Work Queue API. Embeds TheLab dispatch opinion: one in_progress per agent, stale detection, automatic blocking of timed-out items."
|
||||
---
|
||||
|
||||
# Work Queue Skill
|
||||
|
||||
Thin wrapper around the Work Queue API with embedded dispatch opinion.
|
||||
|
||||
## Setup
|
||||
|
||||
Set the base URL:
|
||||
```bash
|
||||
export WORK_QUEUE_API_URL=https://api.example.com # replace with actual API URL
|
||||
```
|
||||
|
||||
The skill reads `WORK_QUEUE_API_URL` from env.
|
||||
|
||||
## Core Commands
|
||||
|
||||
### `wq add`
|
||||
|
||||
Submit a new work item (status=queued).
|
||||
|
||||
```bash
|
||||
wq add <type> <description> [--agent <agent>] [--project-id <id>] [--priority 1-5>] [--payload <json>]
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
wq add code_review "Review PR #3 in shopping-list-api" --agent steve-w --priority 2 --payload '{"pr":3,"repo":"shopping-list-api"}'
|
||||
```
|
||||
|
||||
### `wq dispatch`
|
||||
|
||||
Dispatch a queued item to an agent (moves queued→dispatched→in_progress atomically).
|
||||
|
||||
```bash
|
||||
wq dispatch <work_item_id> <agent>
|
||||
```
|
||||
|
||||
Fails if agent already has an in_progress item.
|
||||
|
||||
### `wq update`
|
||||
|
||||
Update status, outcome, or notes on a work item.
|
||||
|
||||
```bash
|
||||
wq update <work_item_id> [--status <status>] [--outcome <success|failed|cancelled>] [--notes <text>]
|
||||
```
|
||||
|
||||
Valid status transitions:
|
||||
- dispatched → in_progress (agent picked it up)
|
||||
- in_progress → blocked (waiting on something)
|
||||
- in_progress → completed (done)
|
||||
- in_progress → failed (unrecoverable error)
|
||||
- queued → cancelled
|
||||
- dispatched → cancelled
|
||||
|
||||
### `wq list`
|
||||
|
||||
List work items, optionally filtered.
|
||||
|
||||
```bash
|
||||
wq list [--status <status>] [--agent <agent>] [--project-id <id>]
|
||||
```
|
||||
|
||||
### `wq get`
|
||||
|
||||
Get a single work item by ID.
|
||||
|
||||
```bash
|
||||
wq get <work_item_id>
|
||||
```
|
||||
|
||||
### `wq my-queue`
|
||||
|
||||
Short-cut: list items assigned to a given agent with status=dispatched (what Steve should poll).
|
||||
|
||||
```bash
|
||||
wq my-queue <agent>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dispatch Opinion
|
||||
|
||||
These rules are enforced automatically by the skill:
|
||||
|
||||
1. **One in_progress per agent** — dispatch fails if target agent is already in_progress
|
||||
2. **Stale detection** — on every heartbeat, `wq stale-check` is called; items in_progress >30min are automatically marked blocked
|
||||
3. **Terminal states require outcome** — moving to completed/failed/cancelled requires outcome field
|
||||
4. **Cancelled only from queued/dispatched** — cannot cancel something already in_progress
|
||||
|
||||
---
|
||||
|
||||
## Stale Check (for heartbeat)
|
||||
|
||||
```bash
|
||||
wq stale-check [--timeout-minutes 30]
|
||||
```
|
||||
|
||||
Finds all in_progress items older than timeout, marks them blocked, prints a summary line per item.
|
||||
|
||||
---
|
||||
|
||||
## Integration with Gitea Watcher
|
||||
|
||||
The Gitea watcher (`gitea_cron/check.sh`) outputs `dispatch:` lines. On heartbeat, parse those lines and for each:
|
||||
|
||||
```bash
|
||||
wq add <type> <description> --agent steve-w --payload '<json>'
|
||||
```
|
||||
|
||||
The watcher itself does NOT call the API — it just emits dispatch lines. Marcus's heartbeat parses them, creates real work items via `wq add`, then dispatches via `wq dispatch`.
|
||||
47
skill/bin/wq
Executable file
47
skill/bin/wq
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# wq — Work Queue CLI wrapper
|
||||
# Usage: wq <command> [options]
|
||||
|
||||
set -e
|
||||
|
||||
API_URL="${WORK_QUEUE_API_URL:-}"
|
||||
if [[ -z "$API_URL" ]]; then
|
||||
if [[ -f ~/.config/work_queue_api_url ]]; then
|
||||
API_URL=$(cat ~/.config/work_queue_api_url)
|
||||
else
|
||||
echo "Error: WORK_QUEUE_API_URL not set and no ~/.config/work_queue_api_url" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
CMD="$1"
|
||||
shift || { echo "Usage: wq <command> [args]" >&2; exit 1; }
|
||||
|
||||
case "$CMD" in
|
||||
add)
|
||||
wq_add "$@"
|
||||
;;
|
||||
dispatch)
|
||||
wq_dispatch "$@"
|
||||
;;
|
||||
update)
|
||||
wq_update "$@"
|
||||
;;
|
||||
list)
|
||||
wq_list "$@"
|
||||
;;
|
||||
get)
|
||||
wq_get "$@"
|
||||
;;
|
||||
my-queue)
|
||||
wq_my_queue "$@"
|
||||
;;
|
||||
stale-check)
|
||||
wq_stale_check "$@"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $CMD" >&2
|
||||
echo "Commands: add, dispatch, update, list, get, my-queue, stale-check" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
39
skill/bin/wq_add
Executable file
39
skill/bin/wq_add
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# wq_add — submit a new work item
|
||||
|
||||
wq_add() {
|
||||
local type desc agent project_id priority payload_json
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--agent) agent="$2"; shift 2;;
|
||||
--project-id) project_id="$2"; shift 2;;
|
||||
--priority) priority="$2"; shift 2;;
|
||||
--payload) payload_json="$2"; shift 2;;
|
||||
--) shift; break;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1;;
|
||||
*)
|
||||
if [[ -z "$type" ]]; then
|
||||
type="$1"
|
||||
elif [[ -z "$desc" ]]; then
|
||||
desc="$1"
|
||||
fi
|
||||
shift;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$type" ]] || [[ -z "$desc" ]]; then
|
||||
echo "Usage: wq add <type> <description> [--agent <agent>] [--project-id <id>] [--priority 1-5>] [--payload <json>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local body="{\"type\":\"$type\",\"description\":\"$desc\"}"
|
||||
[[ -n "$agent" ]] && body=$(echo "$body" | jq ".assigned_agent=\"$agent\"")
|
||||
[[ -n "$project_id" ]] && body=$(echo "$body" | jq ".project_id=\"$project_id\"")
|
||||
[[ -n "$priority" ]] && body=$(echo "$body" | jq ".priority=$priority")
|
||||
[[ -n "$payload_json" ]] && body=$(echo "$body" | jq ".payload=$payload_json")
|
||||
|
||||
curl -sf -X POST "$API_URL/work" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$body" | jq .
|
||||
}
|
||||
27
skill/bin/wq_dispatch
Executable file
27
skill/bin/wq_dispatch
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# wq_dispatch — dispatch queued item to agent (queued→dispatched→in_progress atomically)
|
||||
|
||||
wq_dispatch() {
|
||||
local work_id="$1" agent="$2"
|
||||
|
||||
if [[ -z "$work_id" ]] || [[ -z "$agent" ]]; then
|
||||
echo "Usage: wq dispatch <work_item_id> <agent>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if agent already has in_progress
|
||||
local existing
|
||||
existing=$(curl -sf "$API_URL/work?status=in_progress&agent=$agent" | jq 'length')
|
||||
if [[ "$existing" -gt 0 ]]; then
|
||||
echo "Error: $agent already has an in_progress item" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -sf -X PATCH "$API_URL/work/$work_id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"status\":\"dispatched\",\"assigned_agent\":\"$agent\"}" | jq .
|
||||
|
||||
curl -sf -X PATCH "$API_URL/work/$work_id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"status\":\"in_progress\"}" | jq .
|
||||
}
|
||||
8
skill/bin/wq_get
Executable file
8
skill/bin/wq_get
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# wq_get — get single work item
|
||||
|
||||
wq_get() {
|
||||
local work_id="$1"
|
||||
[[ -z "$work_id" ]] && { echo "Usage: wq get <work_item_id>" >&2; exit 1; }
|
||||
curl -sf "$API_URL/work/$work_id" | jq .
|
||||
}
|
||||
25
skill/bin/wq_list
Executable file
25
skill/bin/wq_list
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# wq_list — list work items with optional filters
|
||||
|
||||
wq_list() {
|
||||
local status agent project_id since qs="?"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--status) status="$2"; shift 2;;
|
||||
--agent) agent="$2"; shift 2;;
|
||||
--project-id) project_id="$2"; shift 2;;
|
||||
--since) since="$2"; shift 2;;
|
||||
--) shift; break;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$status" ]] && qs="${qs}status=$status&"
|
||||
[[ -n "$agent" ]] && qs="${qs}agent=$agent&"
|
||||
[[ -n "$project_id" ]] && qs="${qs}project_id=$project_id&"
|
||||
[[ -n "$since" ]] && qs="${qs}since=$since&"
|
||||
|
||||
curl -sf "$API_URL/work${qs}" | jq .
|
||||
}
|
||||
8
skill/bin/wq_my_queue
Executable file
8
skill/bin/wq_my_queue
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# wq_my_queue — list dispatched items for a given agent (what Steve polls)
|
||||
|
||||
wq_my_queue() {
|
||||
local agent="$1"
|
||||
[[ -z "$agent" ]] && { echo "Usage: wq my-queue <agent>" >&2; exit 1; }
|
||||
curl -sf "$API_URL/work?status=dispatched&agent=$agent" | jq .
|
||||
}
|
||||
35
skill/bin/wq_stale_check
Executable file
35
skill/bin/wq_stale_check
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# wq_stale_check — find in_progress items older than timeout, mark blocked
|
||||
|
||||
wq_stale_check() {
|
||||
local timeout_minutes="${1:-30}"
|
||||
|
||||
local items
|
||||
items=$(curl -sf "$API_URL/work?status=in_progress" | jq -r '.[] | @json' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$items" ]]; then
|
||||
echo "No in_progress items found."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local count=0
|
||||
now_ts=$(date -u +%s)
|
||||
|
||||
while IFS= read -r item; do
|
||||
[[ -z "$item" ]] && continue
|
||||
work_id=$(echo "$item" | jq -r '.id')
|
||||
updated_at=$(echo "$item" | jq -r '.updated_at')
|
||||
agent=$(echo "$item" | jq -r '.assigned_agent')
|
||||
age_minutes=$(( (now_ts - $(date -u -d "$updated_at" +%s 2>/dev/null || echo "$now_ts")) / 60 ))
|
||||
|
||||
if [[ "$age_minutes" -gt "$timeout_minutes" ]]; then
|
||||
echo "Stale: $work_id ($agent, ${age_minutes}m old) — marking blocked"
|
||||
curl -sf -X PATCH "$API_URL/work/$work_id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"status\":\"blocked\",\"notes\":\"Auto-blocked: stale for ${age_minutes} minutes (>$timeout_minutes)\"}" | jq -r '.id' > /dev/null
|
||||
count=$((count + 1))
|
||||
fi
|
||||
done <<< "$items"
|
||||
|
||||
echo "Stale check complete: $count items marked blocked."
|
||||
}
|
||||
40
skill/bin/wq_update
Executable file
40
skill/bin/wq_update
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# wq_update — update status, outcome, or notes
|
||||
|
||||
wq_update() {
|
||||
local work_id status outcome notes
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--status) status="$2"; shift 2;;
|
||||
--outcome) outcome="$2"; shift 2;;
|
||||
--notes) notes="$2"; shift 2;;
|
||||
--) shift; break;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1;;
|
||||
*)
|
||||
if [[ -z "$work_id" ]]; then
|
||||
work_id="$1"
|
||||
fi
|
||||
shift;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$work_id" ]]; then
|
||||
echo "Usage: wq update <work_item_id> [--status <status>] [--outcome <success|failed|cancelled>] [--notes <text>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$status" ]] && [[ -z "$outcome" ]] && [[ -z "$notes" ]]; then
|
||||
echo "Error: at least one of --status, --outcome, or --notes required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local body="{}"
|
||||
[[ -n "$status" ]] && body=$(echo "$body" | jq ".status=\"$status\"")
|
||||
[[ -n "$outcome" ]] && body=$(echo "$body" | jq ".outcome=\"$outcome\"")
|
||||
[[ -n "$notes" ]] && body=$(echo "$body" | jq ".notes=\"$notes\"")
|
||||
|
||||
curl -sf -X PATCH "$API_URL/work/$work_id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$body" | jq .
|
||||
}
|
||||
Reference in New Issue
Block a user