From f3c38f10173af3b73bd56ed7ba4bbdb2f3bbebf4 Mon Sep 17 00:00:00 2001 From: danny8632 Date: Tue, 12 May 2026 06:54:51 +0000 Subject: [PATCH] Phase 1: PM agent (pm-decompose.sh) + issue-template + auditor stub --- pm/issue-template.md | 45 ++++++++++++++++ pm/pm-decompose.sh | 123 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 pm/issue-template.md create mode 100755 pm/pm-decompose.sh diff --git a/pm/issue-template.md b/pm/issue-template.md new file mode 100644 index 0000000..7149fd3 --- /dev/null +++ b/pm/issue-template.md @@ -0,0 +1,45 @@ +# Agent-issue template + +Every issue intended for a dev agent should follow this shape. The `dev-task.sh` +wrapper reads `Model:` to pick the model and ignores the rest of the headings; +they're for the agent and for any auditor that later reviews the PR. + +```markdown +## Goal + + + +## Done criteria + +- [ ] +- [ ] <…> + +## Hints + + + +## Model + +sonnet +``` + +## Rules of thumb + +- **Scope tight.** An issue should fit in 30 min of agent time. If it feels + like a full feature, decompose it into 2–3 smaller issues with `depends_on`. +- **Done criteria are how the auditor checks the PR.** If a criterion can't + be verified mechanically (or by reading the diff), it's the wrong shape. +- **Don't tell the agent how**, tell it what the result must look like. Hints + are for constraints (e.g. "no new dependencies", "match existing style"). +- **Pick `opus` only when the task involves design tradeoffs.** Routine + scaffolding, CRUD endpoints, and well-specified refactors are sonnet work. +- **Avoid taste calls.** "Make the UI look good" is not an agent task; "use + Tailwind utility classes for spacing" is. + +## Anti-patterns + +- ❌ "Refactor the whole repo for clarity" (no criterion — no PR will satisfy this) +- ❌ "Write a TODO app" (too broad — decompose) +- ❌ "Improve performance" (no measurable target) +- ✅ "Add GET /todos endpoint returning 2 hardcoded items as JSON" +- ✅ "Replace bcrypt with argon2id in auth/password.ts; existing tests must still pass" diff --git a/pm/pm-decompose.sh b/pm/pm-decompose.sh new file mode 100755 index 0000000..4ee6091 --- /dev/null +++ b/pm/pm-decompose.sh @@ -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 / "" +# 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