Phase 1: PM agent (pm-decompose.sh) + issue-template + auditor stub
This commit is contained in:
123
pm/pm-decompose.sh
Executable file
123
pm/pm-decompose.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user