Merge pull request 'fix: fix: architect should close parent vision issue when all sprint sub-issues complete (#689)' (#694) from fix/issue-689 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
commit
df08b654b5
2 changed files with 215 additions and 0 deletions
|
|
@ -60,6 +60,17 @@ All forks resolved → sub-issue filing (model files implementation issues)
|
||||||
REJECT review → close PR + journal (model processes rejection, bash merges PR)
|
REJECT review → close PR + journal (model processes rejection, bash merges PR)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Vision issue lifecycle
|
||||||
|
|
||||||
|
Vision issues decompose into sprint sub-issues tracked via "Decomposed from #N" in sub-issue bodies. The architect automatically closes vision issues when all sub-issues are closed:
|
||||||
|
|
||||||
|
1. Before picking new vision issues, the architect checks each open vision issue
|
||||||
|
2. For each, it queries for sub-issues with "Decomposed from #N" in their body (regardless of state)
|
||||||
|
3. If all sub-issues are closed, it posts a summary comment listing completed sub-issues
|
||||||
|
4. The vision issue is then closed automatically
|
||||||
|
|
||||||
|
This ensures vision issues transition from `open` → `closed` once their work is complete, without manual intervention.
|
||||||
|
|
||||||
### Session management
|
### Session management
|
||||||
|
|
||||||
The agent maintains a global session file at `/tmp/architect-session-{project}.sid`.
|
The agent maintains a global session file at `/tmp/architect-session-{project}.sid`.
|
||||||
|
|
|
||||||
|
|
@ -417,6 +417,206 @@ fetch_vision_issues() {
|
||||||
"${FORGE_API}/issues?labels=vision&state=open&limit=100" 2>/dev/null || echo '[]'
|
"${FORGE_API}/issues?labels=vision&state=open&limit=100" 2>/dev/null || echo '[]'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Helper: Fetch all sub-issues for a vision issue ───────────────────────
|
||||||
|
# Sub-issues are identified by:
|
||||||
|
# 1. Issues whose body contains "Decomposed from #N" pattern
|
||||||
|
# 2. Issues referenced in merged sprint PR bodies
|
||||||
|
# Returns: newline-separated list of sub-issue numbers (empty if none)
|
||||||
|
# Args: vision_issue_number
|
||||||
|
get_vision_subissues() {
|
||||||
|
local vision_issue="$1"
|
||||||
|
local subissues=()
|
||||||
|
|
||||||
|
# Method 1: Find issues with "Decomposed from #N" in body
|
||||||
|
local issues_json
|
||||||
|
issues_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
"${FORGE_API}/issues?limit=100" 2>/dev/null) || true
|
||||||
|
|
||||||
|
if [ -n "$issues_json" ] && [ "$issues_json" != "null" ]; then
|
||||||
|
while IFS= read -r subissue_num; do
|
||||||
|
[ -z "$subissue_num" ] && continue
|
||||||
|
subissues+=("$subissue_num")
|
||||||
|
done <<< "$(printf '%s' "$issues_json" | jq -r --arg vid "$vision_issue" \
|
||||||
|
'[.[] | select(.number != ($vid | tonumber)) | select(.body // "" | contains("Decomposed from #" + $vid))] | .[].number' 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Method 2: Find issues referenced in merged sprint PR bodies
|
||||||
|
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) || true
|
||||||
|
|
||||||
|
if [ -n "$prs_json" ] && [ "$prs_json" != "null" ]; then
|
||||||
|
while IFS= read -r pr_num; do
|
||||||
|
[ -z "$pr_num" ] && continue
|
||||||
|
|
||||||
|
# Check if PR is merged and references the vision issue
|
||||||
|
local pr_details pr_body
|
||||||
|
pr_details=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" 2>/dev/null) || continue
|
||||||
|
|
||||||
|
local is_merged
|
||||||
|
is_merged=$(printf '%s' "$pr_details" | jq -r '.merged // false') || continue
|
||||||
|
|
||||||
|
if [ "$is_merged" != "true" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
pr_body=$(printf '%s' "$pr_details" | jq -r '.body // ""') || continue
|
||||||
|
|
||||||
|
# Extract all issue numbers from PR body
|
||||||
|
while IFS= read -r ref_issue; do
|
||||||
|
[ -z "$ref_issue" ] && continue
|
||||||
|
# Skip if already in list
|
||||||
|
local found=false
|
||||||
|
for existing in "${subissues[@]+"${subissues[@]}"}"; do
|
||||||
|
[ "$existing" = "$ref_issue" ] && found=true && break
|
||||||
|
done
|
||||||
|
if [ "$found" = false ]; then
|
||||||
|
subissues+=("$ref_issue")
|
||||||
|
fi
|
||||||
|
done <<< "$(printf '%s' "$pr_body" | grep -oE '#[0-9]+' | tr -d '#' | sort -u)"
|
||||||
|
done <<< "$(printf '%s' "$prs_json" | jq -r '.[] | select(.title | contains("architect:")) | .number')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output unique sub-issues
|
||||||
|
printf '%s\n' "${subissues[@]}" | sort -u | grep -v '^$' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helper: Check if all sub-issues of a vision issue are closed ───────────
|
||||||
|
# Returns: 0 if all sub-issues are closed, 1 if any are still open
|
||||||
|
# Args: vision_issue_number
|
||||||
|
all_subissues_closed() {
|
||||||
|
local vision_issue="$1"
|
||||||
|
local subissues
|
||||||
|
subissues=$(get_vision_subissues "$vision_issue")
|
||||||
|
|
||||||
|
# If no sub-issues found, parent cannot be considered complete
|
||||||
|
if [ -z "$subissues" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check each sub-issue state
|
||||||
|
while IFS= read -r subissue_num; do
|
||||||
|
[ -z "$subissue_num" ] && continue
|
||||||
|
|
||||||
|
local sub_state
|
||||||
|
sub_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
"${FORGE_API}/issues/${subissue_num}" 2>/dev/null | jq -r '.state // "unknown"') || true
|
||||||
|
|
||||||
|
if [ "$sub_state" != "closed" ]; then
|
||||||
|
log "Sub-issue #${subissue_num} is ${sub_state} — vision issue #${vision_issue} not ready to close"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done <<< "$subissues"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helper: Close vision issue with summary comment ────────────────────────
|
||||||
|
# Posts a comment listing all completed sub-issues before closing.
|
||||||
|
# Returns: 0 on success, 1 on failure
|
||||||
|
# Args: vision_issue_number
|
||||||
|
close_vision_issue() {
|
||||||
|
local vision_issue="$1"
|
||||||
|
local subissues
|
||||||
|
subissues=$(get_vision_subissues "$vision_issue")
|
||||||
|
|
||||||
|
# Build summary comment
|
||||||
|
local summary=""
|
||||||
|
local count=0
|
||||||
|
while IFS= read -r subissue_num; do
|
||||||
|
[ -z "$subissue_num" ] && continue
|
||||||
|
local sub_title
|
||||||
|
sub_title=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
"${FORGE_API}/issues/${subissue_num}" 2>/dev/null | jq -r '.title // "Untitled"') || sub_title="Untitled"
|
||||||
|
summary+="- #${subissue_num}: ${sub_title}"$'\n'
|
||||||
|
count=$((count + 1))
|
||||||
|
done <<< "$subissues"
|
||||||
|
|
||||||
|
local comment
|
||||||
|
comment=$(cat <<EOF
|
||||||
|
## Vision Issue Completed
|
||||||
|
|
||||||
|
All sub-issues have been implemented and merged. This vision issue is now closed.
|
||||||
|
|
||||||
|
### Completed sub-issues (${count}):
|
||||||
|
${summary}
|
||||||
|
---
|
||||||
|
*Automated closure by architect · $(date -u '+%Y-%m-%d %H:%M UTC')*
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Post comment before closing
|
||||||
|
local tmpfile tmpjson
|
||||||
|
tmpfile=$(mktemp /tmp/vision-close-XXXXXX.md)
|
||||||
|
tmpjson="${tmpfile}.json"
|
||||||
|
printf '%s' "$comment" > "$tmpfile"
|
||||||
|
jq -Rs '{body:.}' < "$tmpfile" > "$tmpjson"
|
||||||
|
|
||||||
|
if ! curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${FORGE_API}/issues/${vision_issue}/comments" \
|
||||||
|
--data-binary @"$tmpjson" >/dev/null 2>&1; then
|
||||||
|
log "WARNING: failed to post closure comment on vision issue #${vision_issue}"
|
||||||
|
rm -f "$tmpfile" "$tmpjson"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$tmpfile" "$tmpjson"
|
||||||
|
|
||||||
|
# Clear assignee and close the issue
|
||||||
|
curl -sf -X PATCH \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${FORGE_API}/issues/${vision_issue}" \
|
||||||
|
-d '{"assignees":[]}' >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
curl -sf -X PATCH \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${FORGE_API}/issues/${vision_issue}" \
|
||||||
|
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
log "Closed vision issue #${vision_issue} — all ${count} sub-issue(s) complete"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Lifecycle check: Close vision issues with all sub-issues complete ──────
|
||||||
|
# Runs before picking new vision issues for decomposition.
|
||||||
|
# Checks each open vision issue and closes it if all sub-issues are closed.
|
||||||
|
check_and_close_completed_visions() {
|
||||||
|
log "Checking for vision issues with all sub-issues complete..."
|
||||||
|
|
||||||
|
local vision_issues_json
|
||||||
|
vision_issues_json=$(fetch_vision_issues)
|
||||||
|
|
||||||
|
if [ -z "$vision_issues_json" ] || [ "$vision_issues_json" = "null" ]; then
|
||||||
|
log "No open vision issues found"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get all vision issue numbers
|
||||||
|
local vision_issue_nums
|
||||||
|
vision_issue_nums=$(printf '%s' "$vision_issues_json" | jq -r '.[].number' 2>/dev/null) || vision_issue_nums=""
|
||||||
|
|
||||||
|
local closed_count=0
|
||||||
|
while IFS= read -r vision_issue; do
|
||||||
|
[ -z "$vision_issue" ] && continue
|
||||||
|
|
||||||
|
if all_subissues_closed "$vision_issue"; then
|
||||||
|
if close_vision_issue "$vision_issue"; then
|
||||||
|
closed_count=$((closed_count + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$vision_issue_nums"
|
||||||
|
|
||||||
|
if [ "$closed_count" -gt 0 ]; then
|
||||||
|
log "Closed ${closed_count} vision issue(s) with all sub-issues complete"
|
||||||
|
else
|
||||||
|
log "No vision issues ready for closure"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── Helper: Fetch open architect PRs from ops repo Forgejo API ───────────
|
# ── Helper: Fetch open architect PRs from ops repo Forgejo API ───────────
|
||||||
# Returns: JSON array of architect PR objects
|
# Returns: JSON array of architect PR objects
|
||||||
fetch_open_architect_prs() {
|
fetch_open_architect_prs() {
|
||||||
|
|
@ -689,6 +889,10 @@ if [ "${open_arch_prs:-0}" -ge 3 ]; then
|
||||||
log "3 open architect PRs found but responses detected — processing"
|
log "3 open architect PRs found but responses detected — processing"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Lifecycle check: Close vision issues with all sub-issues complete ──────
|
||||||
|
# Run before picking new vision issues for decomposition
|
||||||
|
check_and_close_completed_visions
|
||||||
|
|
||||||
# ── Bash-driven state management: Select vision issues for pitching ───────
|
# ── Bash-driven state management: Select vision issues for pitching ───────
|
||||||
# This logic is also documented in formulas/run-architect.toml preflight step
|
# This logic is also documented in formulas/run-architect.toml preflight step
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue