#!/usr/bin/env bash # pm-decompose.sh — given a high-level goal for a Gitea repo, ask claude to # decompose it into 1–5 structured agent-issues and post each to Gitea. # # Usage: pm-decompose.sh / "" # Optional env knobs: # REPO_STATE freeform string describing what's already in the repo # (helps claude avoid duplicating work). Default: empty. # MODEL which model the PM itself uses to think. Default: sonnet. # GITEA_URL default https://gitea.dannyhaslund.dk # GITEA_TOKEN required; if unset, reads /etc/agent/gitea-token set -euo pipefail REPO="${1:-}" GOAL="${2:-}" [[ -z "$REPO" || -z "$GOAL" ]] && { echo "usage: $0 / ''" >&2; exit 64; } GITEA_URL="${GITEA_URL:-https://gitea.dannyhaslund.dk}" if [[ -z "${GITEA_TOKEN:-}" ]]; then if [[ -r /etc/agent/gitea-token ]]; then GITEA_TOKEN="$(cat /etc/agent/gitea-token)" else echo "GITEA_TOKEN unset and /etc/agent/gitea-token unreadable" >&2; exit 65 fi fi MODEL="${MODEL:-sonnet}" REPO_STATE="${REPO_STATE:-(unknown; assume repo is empty or near-empty)}" SCHEMA='{ "type": "object", "required": ["issues"], "properties": { "issues": { "type": "array", "minItems": 1, "maxItems": 5, "items": { "type": "object", "required": ["title", "goal", "done_criteria", "model"], "properties": { "title": { "type": "string", "maxLength": 80 }, "goal": { "type": "string", "minLength": 20 }, "done_criteria": { "type": "array", "minItems": 1, "items": { "type": "string" } }, "hints": { "type": "string" }, "model": { "type": "string", "enum": ["sonnet", "opus"] } } } } } }' PROMPT="You are the project manager for an autonomous coding empire. Decompose the following goal for repo '$REPO' into 1 to 5 structured agent-issues that, when implemented in order, achieve the goal. # Current repo state $REPO_STATE # Goal for this iteration $GOAL # Constraints on the issues you produce - Each issue must fit in <30 minutes of autonomous agent time on Sonnet. - Done criteria must be specific and mechanically verifiable (file exists, endpoint returns X, test passes). No taste calls. - Order issues by dependency (foundation first). - Use 'opus' only for tasks that require design tradeoffs; routine scaffolding/CRUD/refactors are 'sonnet'. - Prefer fewer larger issues over many trivial ones, as long as each still fits in 30 min. - Do NOT call any tools — produce the JSON from the goal text alone. Return only the JSON matching the schema." TMP="$(mktemp)" trap 'rm -f "$TMP"' EXIT echo "[pm] decomposing goal into agent-issues (model=$MODEL)..." >&2 claude -p "$PROMPT" \ --model "$MODEL" \ --output-format json \ --json-schema "$SCHEMA" \ --allowedTools "" \ < /dev/null > "$TMP" 2>&1 # Parse structured output issues_json="$(jq -c '.structured_output.issues // empty' "$TMP")" if [[ -z "$issues_json" || "$issues_json" == "null" ]]; then echo "[pm] ERROR: claude did not return structured_output.issues" >&2 echo "--- raw claude output ---" >&2 jq -r '.result // .error // .' "$TMP" >&2 | head -40 >&2 exit 1 fi count="$(jq 'length' <<<"$issues_json")" echo "[pm] claude proposed $count issue(s); posting to Gitea..." >&2 created_ids=() for i in $(seq 0 $((count - 1))); do issue="$(jq -c ".[$i]" <<<"$issues_json")" title="$(jq -r .title <<<"$issue")" goal_t="$(jq -r .goal <<<"$issue")" done_b="$(jq -r '.done_criteria | map("- [ ] " + .) | join("\n")' <<<"$issue")" hints="$(jq -r '.hints // ""' <<<"$issue")" model="$(jq -r '.model // "sonnet"' <<<"$issue")" body="$(printf '## Goal\n\n%s\n\n## Done criteria\n\n%s\n\n## Hints\n\n%s\n\n## Model\n\n%s\n' \ "$goal_t" "$done_b" "${hints:-(none)}" "$model")" resp="$(curl -fsS -X POST \ -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ -d "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" \ "$GITEA_URL/api/v1/repos/$REPO/issues")" num="$(jq -r .number <<<"$resp")" url="$(jq -r .html_url <<<"$resp")" echo " #$num $title → $url" created_ids+=("$num") done echo "[pm] done. created ${#created_ids[@]} issue(s): ${created_ids[*]}" >&2