#!/usr/bin/env python3 """ AutoJanet Intake Service Accepts task submissions and creates Vikunja tasks with the appropriate agent label so the dispatcher picks them up automatically. POST /task { "title": "Add dark mode to the dashboard", "description": "...", # optional "role": "coder" # optional, defaults to "pm" (PM decomposes) } GET /health """ import logging import os import sys import httpx from fastapi import FastAPI, HTTPException from pydantic import BaseModel logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", stream=sys.stdout) log = logging.getLogger("intake") VIKUNJA_BASE_URL = os.environ["VIKUNJA_BASE_URL"] VIKUNJA_PM_TOKEN = os.environ["VIKUNJA_PM_TOKEN"] VIKUNJA_PROJECT_ID = int(os.environ.get("VIKUNJA_PROJECT_ID", "78")) VIKUNJA_TODO_BUCKET_ID = int(os.environ.get("VIKUNJA_TODO_BUCKET_ID", "116")) VIKUNJA_VIEW_ID = int(os.environ.get("VIKUNJA_VIEW_ID", "114")) # Label IDs for agent roles (from Vikunja) ROLE_LABEL_IDS = { "pm": 1, "coder": 3, "code-reviewer": 4, "test-engineer": 5, "devsecops": 6, "secops": 7, "sre": 8, "kubernetes-pilot": 9, "linux-admin": 10, "systems-engineer": 11, "networking": 12, "dba": 13, "prometheus-expert": 14, "tofu-engineer": 15, "release-manager": 16, "doc-updater": 17, "doc-writer": 18, "technical-writer": 19, "cost-optimizer": 20, } app = FastAPI(title="AutoJanet Intake", version="1.0.0") VIKUNJA_HEADERS = { "Authorization": f"Bearer {VIKUNJA_PM_TOKEN}", "Content-Type": "application/json", } class TaskRequest(BaseModel): title: str description: str = "" role: str = "pm" class TaskResponse(BaseModel): task_id: int title: str role: str url: str @app.get("/health") def health(): return {"status": "ok"} @app.post("/task", response_model=TaskResponse) def submit_task(req: TaskRequest): role = req.role.lower() if role not in ROLE_LABEL_IDS: raise HTTPException(status_code=400, detail=f"Unknown role '{role}'. Valid: {sorted(ROLE_LABEL_IDS)}") label_id = ROLE_LABEL_IDS[role] # Create the task with httpx.Client(timeout=15) as client: resp = client.put( f"{VIKUNJA_BASE_URL}/api/v1/projects/{VIKUNJA_PROJECT_ID}/tasks", headers=VIKUNJA_HEADERS, json={ "title": req.title, "description": req.description, "labels": [{"id": label_id}], }, ) if resp.status_code not in (200, 201): log.error("Vikunja task creation failed: %s %s", resp.status_code, resp.text) raise HTTPException(status_code=502, detail="Failed to create Vikunja task") task = resp.json() task_id = task["id"] log.info("Created task %d: %s (role=%s)", task_id, req.title, role) # Move to Todo bucket bucket_resp = client.post( f"{VIKUNJA_BASE_URL}/api/v1/projects/{VIKUNJA_PROJECT_ID}/views/{VIKUNJA_VIEW_ID}/buckets/{VIKUNJA_TODO_BUCKET_ID}/tasks", headers=VIKUNJA_HEADERS, json={"task_id": task_id}, ) if bucket_resp.status_code not in (200, 201): log.warning("Failed to move task %d to Todo bucket: %s", task_id, bucket_resp.text) # Set the label explicitly (belt and suspenders) label_resp = client.put( f"{VIKUNJA_BASE_URL}/api/v1/tasks/{task_id}/labels", headers=VIKUNJA_HEADERS, json={"label_id": label_id}, ) if label_resp.status_code not in (200, 201): log.warning("Failed to set label on task %d: %s", task_id, label_resp.text) return TaskResponse( task_id=task_id, title=req.title, role=role, url=f"https://tasks.ctz.fyi/projects/{VIKUNJA_PROJECT_ID}/tasks/{task_id}", )