124 lines
4.2 KiB
Bash
Executable File
124 lines
4.2 KiB
Bash
Executable File
#!/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 <owner>/<repo> "<goal text>"
|
||
# 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 <owner>/<repo> '<goal text>'" >&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
|