#!/usr/bin/env bash # ============================================================================= # planner-agent.sh — Update AGENTS.md tree, triage predictions, gap-analyse # # Three-phase planner run: # Phase 1: Navigate and update AGENTS.md tree using Claude with tool access # Phase 1.5: Triage prediction/unreviewed issues from the predictor (goblin) # Phase 2: Compare AGENTS.md vs VISION.md, create backlog issues for gaps # # Usage: planner-agent.sh (no args — uses env vars from .env / env.sh) # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" # shellcheck source=../lib/env.sh source "$FACTORY_ROOT/lib/env.sh" LOG_FILE="$SCRIPT_DIR/planner.log" CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-3600}" VISION_FILE="${PROJECT_REPO_ROOT}/VISION.md" RESOURCES_FILE="${FACTORY_ROOT}/RESOURCES.md" log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; } # ── Preflight ──────────────────────────────────────────────────────────── cd "$PROJECT_REPO_ROOT" git fetch origin "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true git pull --ff-only origin "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true HEAD_SHA=$(git rev-parse HEAD) log "--- Planner start (HEAD: ${HEAD_SHA:0:7}) ---" # ── Phase 1: Update AGENTS.md tree ────────────────────────────────────── log "Phase 1: updating AGENTS.md tree" # Find all AGENTS.md files and their watermarks AGENTS_FILES=$(find . -name "AGENTS.md" -not -path "./.git/*" | sort) AGENTS_INFO="" NEEDS_UPDATE=false for f in $AGENTS_FILES; do WATERMARK=$(grep -oP '(?<=\` on line 1 - The watermark tells you which commits are already reflected ## Current AGENTS.md files $(echo -e "$AGENTS_INFO") ## Current HEAD: ${HEAD_SHA} ## Your workflow 1. Read the root AGENTS.md. Note its watermark SHA. 2. Run \`git log --stat ..HEAD\` to see what changed since last review. If watermark is 'none', use \`git log --stat -20\` for recent history. 3. For structural changes (new files, renames, major refactors), run \`git show \` or read the affected source files to understand the change. 4. Follow references to sub-directory AGENTS.md files. Repeat steps 1-3 for each. 5. Update any AGENTS.md file that is stale or missing information about changes. 6. If a directory has significant logic but no AGENTS.md, create one. ## AGENTS.md conventions (follow these strictly) - Max ~200 lines per file — if longer, split into sub-directory files - Describe architecture and conventions (WHAT and WHY), not implementation details - Link to source files for specifics: \`See [file.sol](path) for X\` - Progressive disclosure: high-level in root, details in sub-directory files - After updating a file, set its watermark to: \`\` - The watermark MUST be the very first line of the file ## Important - Only update files that are actually stale (have changes since watermark) - Do NOT rewrite files that are already current - Do NOT remove existing accurate content — only add, update, or restructure - Keep the writing factual and architectural — no changelog language" PHASE1_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PHASE1_PROMPT" \ --model sonnet \ --dangerously-skip-permissions \ --max-turns 30 \ 2>/dev/null) || { EXIT_CODE=$? log "ERROR: claude exited with code $EXIT_CODE during phase 1" git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true exit 1 } log "Phase 1 claude finished ($(echo "$PHASE1_OUTPUT" | wc -c) bytes)" # Check if any files were modified if git diff --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then log "No AGENTS.md changes — nothing to commit" git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true else # Commit and push find . -name "AGENTS.md" -not -path "./.git/*" -exec git add {} + if ! git diff --cached --quiet; then git commit -m "chore: planner update AGENTS.md tree" --quiet 2>/dev/null git push -f origin "$BRANCH_NAME" --quiet 2>/dev/null || { log "ERROR: failed to push $BRANCH_NAME" git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true exit 1 } git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true # Create or update PR EXISTING_PR=$(codeberg_api GET "/pulls?state=open&limit=50" 2>/dev/null | \ jq -r --arg branch "$BRANCH_NAME" '.[] | select(.head.ref == $branch) | .number' | head -1) if [ -z "$EXISTING_PR" ]; then PR_RESPONSE=$(codeberg_api POST "/pulls" \ "$(jq -nc --arg h "$BRANCH_NAME" --arg b "$PRIMARY_BRANCH" \ '{title:"chore: planner update AGENTS.md tree",head:$h,base:$b,body:"Automated AGENTS.md tree update from git history analysis."}')" \ 2>/dev/null) PR_NUM=$(echo "$PR_RESPONSE" | jq -r '.number // empty') if [ -n "$PR_NUM" ]; then log "Created PR #${PR_NUM} for AGENTS.md update" matrix_send "planner" "📋 PR #${PR_NUM}: planner update AGENTS.md tree" 2>/dev/null || true else log "ERROR: failed to create PR" fi else log "Updated existing PR #${EXISTING_PR}" fi else git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true log "No AGENTS.md changes after filtering" fi fi log "Phase 1 done" fi # ── Phase 1.5: Prediction Triage ────────────────────────────────────── log "Phase 1.5: prediction triage" PRED_ISSUES=$(codeberg_api GET "/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50" 2>/dev/null || true) PRED_COUNT=0 if [ -n "$PRED_ISSUES" ] && [ "$PRED_ISSUES" != "null" ]; then PRED_COUNT=$(printf '%s' "$PRED_ISSUES" | jq 'length' 2>/dev/null || echo 0) fi ACCEPTED_PREDICTIONS="" if [ "${PRED_COUNT:-0}" -gt 0 ] 2>/dev/null; then log "Found $PRED_COUNT prediction/unreviewed issues to triage" # Build prediction details for the prompt PRED_DETAILS=$(printf '%s' "$PRED_ISSUES" | jq -r \ '.[] | "### Prediction #\(.number): \(.title)\n\(.body)\n"' 2>/dev/null || true) # Look up label IDs ALL_LABELS=$(codeberg_api GET "/labels" 2>/dev/null || true) UNREVIEWED_LABEL_ID=$(printf '%s' "$ALL_LABELS" | \ jq -r '.[] | select(.name == "prediction/unreviewed") | .id' 2>/dev/null || true) BACKLOG_PRED_LABEL_ID=$(printf '%s' "$ALL_LABELS" | \ jq -r '.[] | select(.name == "prediction/backlog") | .id' 2>/dev/null || true) BACKLOG_LABEL_ID_TRIAGE=$(printf '%s' "$ALL_LABELS" | \ jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true) # Create prediction/backlog label if missing if [ -z "$BACKLOG_PRED_LABEL_ID" ]; then LABEL_RESULT=$(codeberg_api POST "/labels" \ -d "$(jq -nc '{name:"prediction/backlog", color:"#0e8a16", description:"Triaged prediction — accepted by planner"}')" \ 2>/dev/null || true) BACKLOG_PRED_LABEL_ID=$(printf '%s' "$LABEL_RESULT" | jq -r '.id // empty' 2>/dev/null || true) if [ -n "$BACKLOG_PRED_LABEL_ID" ]; then log "Created 'prediction/backlog' label (id: $BACKLOG_PRED_LABEL_ID)" else log "WARN: failed to create 'prediction/backlog' label" fi fi # Build formula catalog for triage prompt TRIAGE_FORMULA_CATALOG="" if [ -d "$FACTORY_ROOT/formulas" ]; then for _tf in "$FACTORY_ROOT/formulas"/*.toml; do [ -f "$_tf" ] || continue TRIAGE_FORMULA_CATALOG="${TRIAGE_FORMULA_CATALOG} --- $(basename "$_tf" .toml) --- $(cat "$_tf") " done fi # Fetch open issues summary for overlap check _TRIAGE_ISSUES=$(codeberg_api GET "/issues?state=open&type=issues&limit=50&sort=updated&direction=desc" 2>/dev/null || true) TRIAGE_OPEN_SUMMARY="" if [ -n "$_TRIAGE_ISSUES" ] && [ "$_TRIAGE_ISSUES" != "null" ]; then TRIAGE_OPEN_SUMMARY=$(printf '%s' "$_TRIAGE_ISSUES" | \ jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"' 2>/dev/null || true) fi # Load VISION for context TRIAGE_VISION="" [ -f "$VISION_FILE" ] && TRIAGE_VISION=$(cat "$VISION_FILE") TRIAGE_PROMPT="You are the planner for ${CODEBERG_REPO}. The predictor has filed these observations: ${PRED_DETAILS} For each prediction, decide: - ACCEPT_ACTION: maps to a formula → emit action issue JSON - ACCEPT_BACKLOG: warrants dev work → emit backlog issue JSON - DISMISS: noise, already covered, or not actionable → close with reason Context for your decisions: ## VISION.md ${TRIAGE_VISION:-"(not found)"} ## Available formulas ${TRIAGE_FORMULA_CATALOG:-"(no formulas available)"} ## All open issues (check for overlap) ${TRIAGE_OPEN_SUMMARY:-"(could not fetch)"} ## Output format For each prediction, output one JSON object per line (no array wrapper, no markdown fences): {\"prediction\": , \"decision\": \"ACCEPT_ACTION\", \"title\": \"action title\", \"formula\": \"formula-name\", \"vars\": {\"var1\": \"value1\"}, \"reason\": \"why\"} {\"prediction\": , \"decision\": \"ACCEPT_BACKLOG\", \"title\": \"backlog issue title\", \"body\": \"problem + approach\", \"reason\": \"why\"} {\"prediction\": , \"decision\": \"DISMISS\", \"reason\": \"why this is noise or already covered\"} ## Rules - Triage ALL predictions — every prediction must appear in output - Only use ACCEPT_ACTION when the prediction clearly maps to an available formula - ACCEPT_BACKLOG for predictions that warrant real dev work - DISMISS predictions that are noise, already covered by open issues, or not actionable - Be decisive — the predictor intentionally over-signals; your job is to filter Output ONLY the JSON lines — no preamble, no markdown fences." TRIAGE_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$TRIAGE_PROMPT" \ --model sonnet \ 2>/dev/null) || { log "ERROR: claude exited with code $? during phase 1.5 — skipping triage" TRIAGE_OUTPUT="" } if [ -n "$TRIAGE_OUTPUT" ]; then log "Phase 1.5 claude finished ($(printf '%s' "$TRIAGE_OUTPUT" | wc -c) bytes)" # Build valid formula list for validation TRIAGE_VALID_FORMULAS="" if [ -d "$FACTORY_ROOT/formulas" ]; then for _vf in "$FACTORY_ROOT/formulas"/*.toml; do [ -f "$_vf" ] || continue TRIAGE_VALID_FORMULAS="${TRIAGE_VALID_FORMULAS} $(basename "$_vf" .toml)" done fi TRIAGE_ACCEPTED=0 TRIAGE_DISMISSED=0 while IFS= read -r line; do [ -z "$line" ] && continue printf '%s' "$line" | jq -e . >/dev/null 2>&1 || continue PRED_NUM=$(printf '%s' "$line" | jq -r '.prediction') DECISION=$(printf '%s' "$line" | jq -r '.decision') REASON=$(printf '%s' "$line" | jq -r '.reason // ""') case "$DECISION" in ACCEPT_ACTION) A_TITLE=$(printf '%s' "$line" | jq -r '.title') A_FORMULA=$(printf '%s' "$line" | jq -r '.formula // empty') # Validate formula against on-disk catalog if [ -n "$A_FORMULA" ] && ! echo "$TRIAGE_VALID_FORMULAS" | grep -qw "$A_FORMULA"; then log "WARN: unknown formula '${A_FORMULA}' for prediction #${PRED_NUM} — falling back to ACCEPT_BACKLOG" A_FORMULA="" fi if [ -n "$A_FORMULA" ]; then A_VARS_YAML=$(printf '%s' "$line" | jq -r ' .vars // {} | to_entries | map(" " + .key + ": " + (.value | @json)) | join("\n")') A_BODY="--- formula: ${A_FORMULA} vars: ${A_VARS_YAML} --- Triaged from prediction #${PRED_NUM}. Reason: ${REASON}" else A_BODY="Triaged from prediction #${PRED_NUM}. Reason: ${REASON}" fi A_PAYLOAD=$(jq -nc --arg t "$A_TITLE" --arg b "$A_BODY" '{title:$t, body:$b}') if [ -n "$BACKLOG_LABEL_ID_TRIAGE" ]; then A_PAYLOAD=$(printf '%s' "$A_PAYLOAD" | jq --argjson lid "$BACKLOG_LABEL_ID_TRIAGE" '.labels = [$lid]') fi A_RESULT=$(codeberg_api POST "/issues" -d "$A_PAYLOAD" 2>/dev/null || true) A_ISSUE=$(printf '%s' "$A_RESULT" | jq -r '.number // "?"' 2>/dev/null || echo "?") log "Created action issue #${A_ISSUE} from prediction #${PRED_NUM} (formula: ${A_FORMULA:-freeform})" matrix_send "planner" "📋 Action #${A_ISSUE} from prediction #${PRED_NUM} [${A_FORMULA:-freeform}]: ${A_TITLE}" 2>/dev/null || true ACCEPTED_PREDICTIONS="${ACCEPTED_PREDICTIONS} - Prediction #${PRED_NUM} → action issue #${A_ISSUE} (formula: ${A_FORMULA:-freeform}): ${A_TITLE}" TRIAGE_ACCEPTED=$((TRIAGE_ACCEPTED + 1)) # Relabel: prediction/unreviewed → prediction/backlog if [ -n "$UNREVIEWED_LABEL_ID" ]; then codeberg_api DELETE "/issues/${PRED_NUM}/labels/${UNREVIEWED_LABEL_ID}" 2>/dev/null || true fi if [ -n "$BACKLOG_PRED_LABEL_ID" ]; then codeberg_api POST "/issues/${PRED_NUM}/labels" \ -d "$(jq -nc --argjson lid "$BACKLOG_PRED_LABEL_ID" '{labels:[$lid]}')" 2>/dev/null || true fi ;; ACCEPT_BACKLOG) B_TITLE=$(printf '%s' "$line" | jq -r '.title') B_BODY=$(printf '%s' "$line" | jq -r '.body // ""') B_BODY="${B_BODY} Triaged from prediction #${PRED_NUM}. Reason: ${REASON}" B_PAYLOAD=$(jq -nc --arg t "$B_TITLE" --arg b "$B_BODY" '{title:$t, body:$b}') if [ -n "$BACKLOG_LABEL_ID_TRIAGE" ]; then B_PAYLOAD=$(printf '%s' "$B_PAYLOAD" | jq --argjson lid "$BACKLOG_LABEL_ID_TRIAGE" '.labels = [$lid]') fi B_RESULT=$(codeberg_api POST "/issues" -d "$B_PAYLOAD" 2>/dev/null || true) B_ISSUE=$(printf '%s' "$B_RESULT" | jq -r '.number // "?"' 2>/dev/null || echo "?") log "Created backlog issue #${B_ISSUE} from prediction #${PRED_NUM}" matrix_send "planner" "📋 Backlog #${B_ISSUE} from prediction #${PRED_NUM}: ${B_TITLE}" 2>/dev/null || true ACCEPTED_PREDICTIONS="${ACCEPTED_PREDICTIONS} - Prediction #${PRED_NUM} → backlog issue #${B_ISSUE}: ${B_TITLE}" TRIAGE_ACCEPTED=$((TRIAGE_ACCEPTED + 1)) # Relabel: prediction/unreviewed → prediction/backlog if [ -n "$UNREVIEWED_LABEL_ID" ]; then codeberg_api DELETE "/issues/${PRED_NUM}/labels/${UNREVIEWED_LABEL_ID}" 2>/dev/null || true fi if [ -n "$BACKLOG_PRED_LABEL_ID" ]; then codeberg_api POST "/issues/${PRED_NUM}/labels" \ -d "$(jq -nc --argjson lid "$BACKLOG_PRED_LABEL_ID" '{labels:[$lid]}')" 2>/dev/null || true fi ;; DISMISS) codeberg_api POST "/issues/${PRED_NUM}/comments" \ -d "$(jq -nc --arg b "Dismissed by planner triage: ${REASON}" '{body:$b}')" 2>/dev/null || true codeberg_api PATCH "/issues/${PRED_NUM}" \ -d '{"state":"closed"}' 2>/dev/null || true log "Dismissed prediction #${PRED_NUM}: ${REASON}" TRIAGE_DISMISSED=$((TRIAGE_DISMISSED + 1)) ;; *) log "WARN: unknown triage decision '${DECISION}' for prediction #${PRED_NUM}" ;; esac done <<< "$TRIAGE_OUTPUT" log "Phase 1.5 done — accepted: $TRIAGE_ACCEPTED, dismissed: $TRIAGE_DISMISSED" fi else log "No prediction/unreviewed issues found — skipping triage" fi # ── Phase 2: Gap analysis ─────────────────────────────────────────────── log "Phase 2: gap analysis" # Build project state from AGENTS.md tree PROJECT_STATE="" for f in $(find . -name "AGENTS.md" -not -path "./.git/*" | sort); do PROJECT_STATE="${PROJECT_STATE} ### ${f} $(cat "$f") " done VISION="" [ -f "$VISION_FILE" ] && VISION=$(cat "$VISION_FILE") if [ -z "$VISION" ]; then log "No VISION.md found — skipping gap analysis" log "--- Planner done ---" exit 0 fi RESOURCES="" [ -f "$RESOURCES_FILE" ] && RESOURCES=$(cat "$RESOURCES_FILE") # Fetch open issues (all labels) OPEN_ISSUES=$(codeberg_api GET "/issues?state=open&type=issues&limit=50&sort=updated&direction=desc" 2>/dev/null || true) if [ -z "$OPEN_ISSUES" ] || [ "$OPEN_ISSUES" = "null" ]; then log "Failed to fetch open issues" exit 1 fi OPEN_SUMMARY=$(echo "$OPEN_ISSUES" | jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"' 2>/dev/null || true) # Fetch vision-labeled issues specifically VISION_ISSUES=$(echo "$OPEN_ISSUES" | jq -r '.[] | select(.labels | map(.name) | index("vision")) | "#\(.number) \(.title)\n\(.body)"' 2>/dev/null || true) # Read supervisor metrics for trend analysis (last 7 days) METRICS_FILE="${FACTORY_ROOT}/metrics/supervisor-metrics.jsonl" METRICS_SUMMARY="(no metrics data — supervisor has not yet written metrics)" if [ -f "$METRICS_FILE" ] && [ -s "$METRICS_FILE" ]; then _METRICS_CUTOFF=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M) METRICS_SUMMARY=$(jq -c --arg cutoff "$_METRICS_CUTOFF" 'select(.ts >= $cutoff)' \ "$METRICS_FILE" 2>/dev/null | \ jq -rs --arg proj "${PROJECT_NAME:-}" ' ( [.[] | select(.type=="ci" and .project==$proj) | .duration_min] | if length>0 then add/length|round else null end ) as $ci_avg | ( [.[] | select(.type=="ci" and .project==$proj) | select(.status=="success")] | length ) as $ci_ok | ( [.[] | select(.type=="ci" and .project==$proj)] | length ) as $ci_n | ( [.[] | select(.type=="infra") | .ram_used_pct] | if length>0 then add/length|round else null end ) as $ram_avg | ( [.[] | select(.type=="infra") | .disk_used_pct] | if length>0 then add/length|round else null end ) as $disk_avg | ( [.[] | select(.type=="dev" and .project==$proj)] | last ) as $dev_last | "CI (\($ci_n) pipelines): avg \(if $ci_avg then "\($ci_avg)min" else "n/a" end), success rate \(if $ci_n > 0 then "\($ci_ok * 100 / $ci_n | round)%" else "n/a" end)\n" + "Infra: avg RAM \(if $ram_avg then "\($ram_avg)%" else "n/a" end) used, avg disk \(if $disk_avg then "\($disk_avg)%" else "n/a" end) used\n" + "Dev (latest): \(if $dev_last then "\($dev_last.issues_in_backlog) in backlog, \($dev_last.issues_blocked) blocked (\(if $dev_last.issues_in_backlog > 0 then $dev_last.issues_blocked * 100 / $dev_last.issues_in_backlog | round else 0 end)% blocked), \($dev_last.pr_open) open PRs" else "n/a" end) ' 2>/dev/null) || METRICS_SUMMARY="(metrics parse error)" log "Metrics: ${METRICS_SUMMARY:0:120}" fi # ── Build formula catalog ────────────────────────────────────────────── FORMULA_DIR="$FACTORY_ROOT/formulas" FORMULA_CATALOG="" if [ -d "$FORMULA_DIR" ]; then for formula_file in "$FORMULA_DIR"/*.toml; do [ -f "$formula_file" ] || continue formula_name=$(basename "$formula_file" .toml) FORMULA_CATALOG="${FORMULA_CATALOG} --- ${formula_name} --- $(cat "$formula_file") " done fi PHASE2_PROMPT="You are the planner for ${CODEBERG_REPO}. Your job: find gaps between the project vision and current reality. ## VISION.md (human-maintained goals) ${VISION} ## Current project state (AGENTS.md tree) ${PROJECT_STATE} ## RESOURCES.md (shared factory infrastructure) ${RESOURCES:-"(not found — copy RESOURCES.example.md to RESOURCES.md and fill in your infrastructure)"} ## Vision-labeled issues (goal anchors) ${VISION_ISSUES:-"(none)"} ## All open issues ${OPEN_SUMMARY} ## Recently accepted predictions (from triage) ${ACCEPTED_PREDICTIONS:-"(none — no predictions were triaged this cycle)"} ## Operational metrics (last 7 days from supervisor) ${METRICS_SUMMARY} ## Available formulas (from formulas/ directory) When a gap maps directly to one of these formula types, emit a formula instance instead of a freeform issue. Only use a formula when the gap clearly fits — do not force-fit gaps into formulas. Fill in the formula vars with concrete values. ${FORMULA_CATALOG:-"(no formulas available)"} ## Task Identify gaps — things implied by VISION.md that are neither reflected in the project state nor covered by an existing open issue. When a gap involves deploying, hosting, or operating a service, reference the specific resource alias from RESOURCES.md (e.g. \"deploy to \") so issues are actionable. For each gap, output a JSON object (one per line, no array wrapper). If a gap matches an available formula, emit a formula instance: {\"title\": \"action-oriented title\", \"formula\": \"\", \"vars\": {\"var1\": \"value1\", ...}, \"body\": \"why this matters\", \"depends\": [...]} Otherwise, emit a freeform issue: {\"title\": \"action-oriented title\", \"body\": \"problem statement + why it matters + rough approach\", \"depends\": [...]} ## Rules - Max 5 new issues — focus on highest-leverage gaps only - Do NOT create issues for things already documented in AGENTS.md - Do NOT create issues that overlap with ANY existing open issue, even partially - Do NOT create issues about vision items, tech-debt, or in-progress work - Each title should be a plain, action-oriented sentence - Each body should explain: what's missing, why it matters for the vision, rough approach - Reference blocking issues by number in depends array - Prefer formula instances when a gap clearly fits a known formula — but freeform is fine when none matches - When metrics indicate a systemic problem conflicting with VISION.md (slow CI, high blocked ratio, disk pressure), create an optimization issue even if not explicitly in VISION.md If there are no gaps, output exactly: NO_GAPS Output ONLY the JSON lines (or NO_GAPS) — no preamble, no markdown fences." PHASE2_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PHASE2_PROMPT" \ --model sonnet \ 2>/dev/null) || { log "ERROR: claude exited with code $? during phase 2" exit 1 } if echo "$PHASE2_OUTPUT" | grep -q "NO_GAPS"; then log "No gaps found — backlog is aligned with vision" log "--- Planner done ---" exit 0 fi # ── Create issues from gap analysis ────────────────────────────────────── # Find backlog label ID BACKLOG_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null | \ jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true) # Build list of valid formula names from disk for validation VALID_FORMULAS="" if [ -d "$FORMULA_DIR" ]; then for _vf in "$FORMULA_DIR"/*.toml; do [ -f "$_vf" ] || continue VALID_FORMULAS="${VALID_FORMULAS} $(basename "$_vf" .toml)" done fi CREATED=0 while IFS= read -r line; do [ -z "$line" ] && continue # Skip non-JSON lines echo "$line" | jq -e . >/dev/null 2>&1 || continue TITLE=$(echo "$line" | jq -r '.title') DEPS=$(echo "$line" | jq -r '.depends // [] | map("#\(.)") | join(", ")') # Check if this is a formula instance and validate against on-disk catalog FORMULA_NAME=$(echo "$line" | jq -r '.formula // empty') if [ -n "$FORMULA_NAME" ]; then if ! echo "$VALID_FORMULAS" | grep -qw "$FORMULA_NAME"; then log "WARN: Claude emitted unknown formula '${FORMULA_NAME}' — falling back to freeform" FORMULA_NAME="" fi fi if [ -n "$FORMULA_NAME" ]; then # Build YAML front matter for formula instance (values quoted for safety) # Note: formula label is NOT applied — dev-agent does not yet support # formula dispatch, so adding the label would block the dev pipeline. # The YAML front matter is enough structure for future formula dispatch. VARS_YAML=$(echo "$line" | jq -r ' .vars // {} | to_entries | map(" " + .key + ": " + (.value | @json)) | join("\n")') EXTRA_BODY=$(echo "$line" | jq -r '.body // ""') BODY="--- formula: ${FORMULA_NAME} vars: ${VARS_YAML} --- ${EXTRA_BODY}" else BODY=$(echo "$line" | jq -r '.body') fi # Add dependency section if present if [ -n "$DEPS" ] && [ "$DEPS" != "" ]; then BODY="${BODY} ## Depends on ${DEPS}" fi # Create issue (backlog label only — no formula label until dev-agent supports dispatch) CREATE_PAYLOAD=$(jq -nc --arg t "$TITLE" --arg b "$BODY" '{title:$t, body:$b}') if [ -n "$BACKLOG_LABEL_ID" ]; then CREATE_PAYLOAD=$(echo "$CREATE_PAYLOAD" | jq --argjson lid "$BACKLOG_LABEL_ID" '.labels = [$lid]') fi RESULT=$(codeberg_api POST "/issues" -d "$CREATE_PAYLOAD" 2>/dev/null || true) ISSUE_NUM=$(echo "$RESULT" | jq -r '.number // "?"' 2>/dev/null || echo "?") if [ -n "$FORMULA_NAME" ]; then log "Created #${ISSUE_NUM} (formula:${FORMULA_NAME}): ${TITLE}" matrix_send "planner" "📋 Formula issue #${ISSUE_NUM} [${FORMULA_NAME}]: ${TITLE}" 2>/dev/null || true else log "Created #${ISSUE_NUM}: ${TITLE}" matrix_send "planner" "📋 Gap issue #${ISSUE_NUM}: ${TITLE}" 2>/dev/null || true fi CREATED=$((CREATED + 1)) [ "$CREATED" -ge 5 ] && break done <<< "$PHASE2_OUTPUT" log "Phase 2 done — created $CREATED issues" log "--- Planner done ---"