From 1426b1710fefdb9366117a8834fdf451a7ca62c2 Mon Sep 17 00:00:00 2001 From: Agent Date: Thu, 9 Apr 2026 08:43:48 +0000 Subject: [PATCH] fix: fix: architect creates duplicate sprint pitch for vision issues that already have sub-issues (#486) --- architect/architect-run.sh | 171 ++++++++++++++++++++++++++++++++++++ formulas/run-architect.toml | 34 ++++++- 2 files changed, 203 insertions(+), 2 deletions(-) diff --git a/architect/architect-run.sh b/architect/architect-run.sh index 73bf705..31df1e6 100755 --- a/architect/architect-run.sh +++ b/architect/architect-run.sh @@ -176,6 +176,83 @@ detect_questions_phase() { return 0 } +# ── Sub-issue existence check ──────────────────────────────────────────── +# Check if a vision issue already has sub-issues filed from it. +# Returns 0 if sub-issues exist and are open, 1 otherwise. +# Args: vision_issue_number +has_open_subissues() { + local vision_issue="$1" + local subissue_count=0 + + # Search for issues whose body contains 'Decomposed from #N' pattern + # Fetch all open issues with bodies in one API call (avoids N+1 calls) + local issues_json + issues_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/issues?state=open&limit=100" 2>/dev/null) || return 1 + + # Check each issue for the decomposition pattern using jq to extract bodies + subissue_count=$(printf '%s' "$issues_json" | jq -r --arg vid "$vision_issue" ' + [.[] | select(.number != $vid) | select(.body // "" | contains("Decomposed from #" + $vid))] | length + ' 2>/dev/null) || subissue_count=0 + + if [ "$subissue_count" -gt 0 ]; then + log "Vision issue #${vision_issue} has ${subissue_count} open sub-issue(s) — skipping" + return 0 # Has open sub-issues + fi + + log "Vision issue #${vision_issue} has no open sub-issues" + return 1 # No open sub-issues +} + +# ── Merged sprint PR check ─────────────────────────────────────────────── +# Check if a vision issue already has a merged sprint PR on the ops repo. +# Returns 0 if a merged sprint PR exists, 1 otherwise. +# Args: vision_issue_number +has_merged_sprint_pr() { + local vision_issue="$1" + + # Get closed PRs from ops repo + local prs_json + prs_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=closed&limit=100" 2>/dev/null) || return 1 + + # Check each closed PR for architect markers and vision issue reference + local pr_numbers + pr_numbers=$(printf '%s' "$prs_json" | jq -r '.[] | select(.title | contains("architect:")) | .number' 2>/dev/null) || return 1 + + local pr_num + while IFS= read -r pr_num; do + [ -z "$pr_num" ] && continue + + # Get PR details including merged status + local pr_details + pr_details=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" 2>/dev/null) || continue + + # Check if PR is actually merged (not just closed) + local is_merged + is_merged=$(printf '%s' "$pr_details" | jq -r '.merged // false') || continue + + if [ "$is_merged" != "true" ]; then + continue + fi + + # Get PR body and check for vision issue reference + local pr_body + pr_body=$(printf '%s' "$pr_details" | jq -r '.body // ""') || continue + + # Check if PR body references the vision issue number + # Look for patterns like "#N" where N is the vision issue number + if printf '%s' "$pr_body" | grep -qE "(#|refs|references)[[:space:]]*#${vision_issue}|#${vision_issue}[^0-9]|#${vision_issue}$"; then + log "Found merged sprint PR #${pr_num} referencing vision issue #${vision_issue} — skipping" + return 0 # Has merged sprint PR + fi + done <<< "$pr_numbers" + + log "Vision issue #${vision_issue} has no merged sprint PR" + return 1 # No merged sprint PR +} + # ── Precondition checks in bash before invoking the model ───────────────── # Check 1: Skip if no vision issues exist and no open architect PRs to handle @@ -212,8 +289,102 @@ if [ "${open_arch_prs:-0}" -ge 3 ]; then exit 0 fi log "3 open architect PRs found but responses detected — processing" + # Track that we have responses to process (even if pitch_budget=0) + has_responses_to_process=true fi +# ── Preflight: Select vision issues for pitching ────────────────────────── +# This logic is also documented in formulas/run-architect.toml preflight step + +# Get all open vision issues +vision_issues_json=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ + "${FORGE_API}/issues?labels=vision&state=open&limit=100" 2>/dev/null) || vision_issues_json='[]' + +# Get open architect PRs to skip issues they reference +open_arch_prs_json=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=10" 2>/dev/null) || open_arch_prs_json='[]' + +# Build list of vision issues that already have open architect PRs +declare -A _arch_vision_issues_with_open_prs +while IFS= read -r pr_num; do + [ -z "$pr_num" ] && continue + pr_body=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" 2>/dev/null | jq -r '.body // ""') || continue + # Extract vision issue numbers referenced in PR body (e.g., "refs #419" or "#419") + while IFS= read -r ref_issue; do + [ -z "$ref_issue" ] && continue + _arch_vision_issues_with_open_prs["$ref_issue"]=1 + done <<< "$(printf '%s' "$pr_body" | grep -oE '#[0-9]+' | tr -d '#' | sort -u)" +done <<< "$(printf '%s' "$open_arch_prs_json" | jq -r '.[] | select(.title | startswith("architect:")) | .number')" + +# Get issues with in-progress label +in_progress_issues=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ + "${FORGE_API}/issues?labels=in-progress&state=open&limit=100" 2>/dev/null | jq -r '.[].number' 2>/dev/null) || in_progress_issues="" + +# Select vision issues for pitching +ARCHITECT_TARGET_ISSUES=() +vision_issue_count=0 +pitch_budget=$((3 - open_arch_prs)) + +# Get all vision issue numbers +vision_issue_nums=$(printf '%s' "$vision_issues_json" | jq -r '.[].number' 2>/dev/null) || vision_issue_nums="" + +while IFS= read -r vision_issue; do + [ -z "$vision_issue" ] && continue + vision_issue_count=$((vision_issue_count + 1)) + + # Skip if pitch budget exhausted + if [ ${#ARCHITECT_TARGET_ISSUES[@]} -ge $pitch_budget ]; then + log "Pitch budget exhausted (${#ARCHITECT_TARGET_ISSUES[@]}/${pitch_budget})" + break + fi + + # Skip if vision issue already has open architect PR + if [ "${_arch_vision_issues_with_open_prs[$vision_issue]:-}" = "1" ]; then + log "Vision issue #${vision_issue} already has open architect PR — skipping" + continue + fi + + # Skip if vision issue has in-progress label + if printf '%s\n' "$in_progress_issues" | grep -q "^${vision_issue}$"; then + log "Vision issue #${vision_issue} has in-progress label — skipping" + continue + fi + + # Skip if vision issue has open sub-issues (already being worked on) + if has_open_subissues "$vision_issue"; then + log "Vision issue #${vision_issue} has open sub-issues — skipping" + continue + fi + + # Skip if vision issue has merged sprint PR (decomposition already done) + if has_merged_sprint_pr "$vision_issue"; then + log "Vision issue #${vision_issue} has merged sprint PR — skipping" + continue + fi + + # Add to target issues + ARCHITECT_TARGET_ISSUES+=("$vision_issue") + log "Selected vision issue #${vision_issue} for pitching" +done <<< "$vision_issue_nums" + +# If no issues selected, signal done +# BUT: if we have responses to process (from Check 2), still run agent to handle them +if [ ${#ARCHITECT_TARGET_ISSUES[@]} -eq 0 ]; then + if [ "${has_responses_to_process:-false}" = "true" ]; then + log "No new pitches, but responses detected — processing existing PRs" + else + log "No vision issues available for pitching (all have open PRs, sub-issues, or merged sprint PRs) — signaling PHASE:done" + # Signal PHASE:done by writing to phase file if it exists + if [ -f "/tmp/architect-${PROJECT_NAME}.phase" ]; then + echo "PHASE:done" > "/tmp/architect-${PROJECT_NAME}.phase" + fi + exit 0 + fi +fi + +log "Selected ${#ARCHITECT_TARGET_ISSUES[@]} vision issue(s) for pitching: ${ARCHITECT_TARGET_ISSUES[*]}" + # ── Run agent ───────────────────────────────────────────────────────────── export CLAUDE_MODEL="sonnet" diff --git a/formulas/run-architect.toml b/formulas/run-architect.toml index 3f7aad0..f4996d9 100644 --- a/formulas/run-architect.toml +++ b/formulas/run-architect.toml @@ -53,14 +53,44 @@ For each available pitch slot: 1. From the vision issues list, skip any issue that already has an open architect PR (match by checking if any open architect PR body references the vision issue number) 2. Skip any issue that already has the `in-progress` label -3. From remaining candidates, pick the most unblocking issue first (fewest open +3. Check for existing sub-issues filed from this vision issue (see Sub-issue existence check below) +4. Check for merged sprint PRs referencing this vision issue (see Merged sprint PR check below) +5. From remaining candidates, pick the most unblocking issue first (fewest open dependencies, or earliest created if tied) -4. Add to ARCHITECT_TARGET_ISSUES array +6. Add to ARCHITECT_TARGET_ISSUES array + +### Sub-issue existence check + +Before selecting a vision issue for pitching, check if it already has sub-issues: + +1. Search for issues whose body contains 'Decomposed from #N' where N is the vision issue number +2. Also check for issues filed by architect-bot that reference the vision issue number +3. If any sub-issues are open (state != 'closed'), skip this vision issue — it's already being worked on +4. If all sub-issues are closed, the vision issue may be ready for a new sprint (next phase) + +API calls: +- GET /repos/{owner}/{repo}/issues?labels=vision&state=open — fetch vision issues +- GET /repos/{owner}/{repo}/issues — search all issues for 'Decomposed from #N' pattern +- Check each issue's state field to determine if open or closed + +### Merged sprint PR check + +Before selecting a vision issue for pitching, check for merged architect PRs: + +1. Search for merged PRs on the ops repo where the body references the vision issue number +2. Use: GET /repos/{owner}/{repo}/pulls?state=closed (then filter for merged ones) +3. If a merged PR references the vision issue, decomposition already happened — skip this vision issue + +API calls: +- GET /repos/{owner}/{repo}/pulls?state=closed — fetch closed PRs from ops repo +- Check PR body for references to the vision issue number (e.g., "refs #N" or "#N") Skip conditions: - If no vision issues are found, signal PHASE:done - If pitch_budget <= 0 (already 3 open architect PRs), skip pitching — only handle existing PRs - If all vision issues already have open architect PRs, signal PHASE:done +- If all vision issues have open sub-issues, skip pitching — decomposition already in progress +- If all vision issues have merged sprint PRs, skip pitching — decomposition already completed Output: - Sets ARCHITECT_TARGET_ISSUES as a JSON array of issue numbers to pitch (up to 3)