Skill: mirni/gh-taskqueue (version 1.0.0)

--- name: gh-taskqueue description: "In-memory priority task queue for AI agents. Create tasks with priorities and tags, claim the next highest-priority task, mark tasks complete or failed, filter by status or tag, and get queue statistics. Like a lightweight Celery for agent workflows." metadata: {"openclaw":{"emoji":"📋","requires":{"bins":["python"]},"install":[{"id":"pip","kind":"uv","packages":["fastapi","uvicorn","pydantic"]}]}} --- # TaskQueue Priority task queue for agent workflows. ## Start the server ```bash uvicorn taskqueue.app:app --port 8014 ``` ## Create a task ```bash curl -s -X POST http://localhost:8014/v1/tasks \ -H "Content-Type: application/json" \ -d '{"title": "Scan skill", "payload": {"skill": "my-skill"}, "priority": 5, "tags": ["security"]}' | jq ``` ## Claim the next task (highest priority first) ```bash curl -s -X POST http://localhost:8014/v1/claim | jq ``` Returns the task with status changed to `running`. ## Complete or fail a task ```bash curl -s -X POST http://localhost:8014/v1/tasks/TASK_ID/complete \ -H "Content-Type: application/json" \ -d '{"result": {"output": "all clear"}}' | jq curl -s -X POST http://localhost:8014/v1/tasks/TASK_ID/fail \ -H "Content-Type: application/json" \ -d '{"error": "scan timed out"}' | jq ``` ## List and filter tasks ```bash curl -s "http://localhost:8014/v1/tasks?status=pending" | jq curl -s "http://localhost:8014/v1/tasks?tag=security" | jq ``` ## Queue stats ```bash curl -s http://localhost:8014/v1/stats | jq ``` Returns `total`, `pending`, `running`, `completed`, `failed`. ## Endpoints | Method | Path | Description | |--------|------|-------------| | POST | /v1/tasks | Create a task | | GET | /v1/tasks | List tasks (filter: ?status=, ?tag=) | | GET | /v1/tasks/{id} | Get task by ID | | POST | /v1/claim | Claim next pending task | | POST | /v1/tasks/{id}/complete | Mark done with result | | POST | /v1/tasks/{id}/fail | Mark failed with error | | GET | /v1/stats | Queue statistics |

path: taskqueue/app.py
content: """
TaskQueue API — In-memory priority task queue.

Endpoints:
  POST   /v1/tasks              — create a task
  GET    /v1/tasks               — list tasks (filter by status, tag)
  GET    /v1/tasks/{id}          — get task by ID
  POST   /v1/claim               — claim the next highest-priority pending task
  POST   /v1/tasks/{id}/complete — mark task completed with result
  POST   /v1/tasks/{id}/fail     — mark task failed with error
  GET    /v1/stats               — queue statistics
"""

from fastapi import FastAPI, HTTPException, Query

from .models import (
    CompleteRequest,
    CreateTaskRequest,
    FailRequest,
    QueueStatsResponse,
    TaskListResponse,
    TaskResponse,
)
from .state import claim_next, complete_task, create_task, fail_task, get_task, list_tasks, queue_stats

app = FastAPI(
    title="TaskQueue API",
    description="In-memory priority task queue for AI agent workflows.",
    version="0.1.0",
)


def _task_to_response(task) -> TaskResponse:
    return TaskResponse(
        id=task.id, title=task.title, status=task.status,
        priority=task.priority, tags=task.tags, payload=task.payload,
        result=task.result, error=task.error, created_at=task.created_at,
    )


@app.post("/v1/tasks", response_model=TaskResponse)
async def create(request: CreateTaskRequest) -> TaskResponse:
    task = create_task(request.title, request.payload, request.priority, request.tags)
    return _task_to_response(task)


@app.get("/v1/tasks", response_model=TaskListResponse)
async def list_all(
    status: str | None = Query(default=None),
    tag: str | None = Query(default=None),
) -> TaskListResponse:
    tasks = list_tasks(status, tag)
    return TaskListResponse(tasks=[_task_to_response(t) for t in tasks], total=len(tasks))


@app.get("/v1/tasks/{task_id}", response_model=TaskResponse)
async def get_by_id(task_id: str) -> TaskResponse:
    task = get_task(task_id)
    if task is None:
        raise HTTPException(status_code=404, detail=f"Task not found: {task_id}")
    return _task_to_response(task)


@app.post("/v1/claim", response_model=TaskResponse)
async def claim() -> TaskResponse:
    task = claim_next()
    if task is None:
        raise HTTPException(status_code=404, detail="No pending tasks to claim")
    return _task_to_response(task)


@app.post("/v1/tasks/{task_id}/complete", response_model=TaskResponse)
async def complete(task_id: str, request: CompleteRequest) -> TaskResponse:
    task = complete_task(task_id, request.result)
    if task is None:
        raise HTTPException(status_code=404, detail=f"Task not found: {task_id}")
    return _task_to_response(task)


@app.post("/v1/tasks/{task_id}/fail", response_model=TaskResponse)
async def fail(task_id: str, request: FailRequest) -> TaskResponse:
    task = fail_task(task_id, request.error)
    if task is None:
        raise HTTPException(status_code=404, detail=f"Task not found: {task_id}")
    return _task_to_response(task)


@app.get("/v1/stats", response_model=QueueStatsResponse)
async def stats() -> QueueStatsResponse:
    return QueueStatsResponse(**queue_stats())


path: taskqueue/models.py
content: """
Pydantic models for TaskQueue API.
"""

from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field


class CreateTaskRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    title: str = Field(..., min_length=1, max_length=500)
    payload: Any = Field(...)
    priority: int = Field(default=0, ge=0, le=100)
    tags: list[str] = Field(default_factory=list, max_length=20)


class TaskResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    id: str
    title: str
    status: Literal["pending", "running", "completed", "failed"]
    priority: int
    tags: list[str]
    payload: Any
    result: Any = None
    error: str = ""
    created_at: str


class TaskListResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    tasks: list[TaskResponse]
    total: int = Field(..., ge=0)


class CompleteRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    result: Any = Field(...)


class FailRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    error: str = Field(..., min_length=1)


class QueueStatsResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    total: int = Field(..., ge=0)
    pending: int = Field(..., ge=0)
    running: int = Field(..., ge=0)
    completed: int = Field(..., ge=0)
    failed: int = Field(..., ge=0)


path: taskqueue/state.py
content: """
In-memory priority task queue.
"""

import time
import uuid
from dataclasses import dataclass, field
from typing import Any


@dataclass
class Task:
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    title: str = ""
    status: str = "pending"  # pending, running, completed, failed
    priority: int = 0
    tags: list[str] = field(default_factory=list)
    payload: Any = None
    result: Any = None
    error: str = ""
    created_at: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))


_tasks: dict[str, Task] = {}


def create_task(title: str, payload: Any, priority: int = 0, tags: list[str] | None = None) -> Task:
    task = Task(title=title, payload=payload, priority=priority, tags=tags or [])
    _tasks[task.id] = task
    return task


def get_task(task_id: str) -> Task | None:
    return _tasks.get(task_id)


def list_tasks(status: str | None = None, tag: str | None = None) -> list[Task]:
    results = list(_tasks.values())
    if status:
        results = [t for t in results if t.status == status]
    if tag:
        results = [t for t in results if tag in t.tags]
    return sorted(results, key=lambda t: -t.priority)


def claim_next() -> Task | None:
    """Claim the highest-priority pending task."""
    pending = [t for t in _tasks.values() if t.status == "pending"]
    if not pending:
        return None
    pending.sort(key=lambda t: -t.priority)
    task = pending[0]
    task.status = "running"
    return task


def complete_task(task_id: str, result: Any) -> Task | None:
    task = _tasks.get(task_id)
    if task is None:
        return None
    task.status = "completed"
    task.result = result
    return task


def fail_task(task_id: str, error: str) -> Task | None:
    task = _tasks.get(task_id)
    if task is None:
        return None
    task.status = "failed"
    task.error = error
    return task


def queue_stats() -> dict[str, int]:
    tasks = list(_tasks.values())
    return {
        "total": len(tasks),
        "pending": sum(1 for t in tasks if t.status == "pending"),
        "running": sum(1 for t in tasks if t.status == "running"),
        "completed": sum(1 for t in tasks if t.status == "completed"),
        "failed": sum(1 for t in tasks if t.status == "failed"),
    }


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)