diff --git a/architect/AGENTS.md b/architect/AGENTS.md index 32ec25a..9145ae1 100644 --- a/architect/AGENTS.md +++ b/architect/AGENTS.md @@ -11,7 +11,7 @@ converses with humans through PR comments. - **Input**: Vision issues from VISION.md, prerequisite tree from ops repo - **Output**: Sprint proposals as PRs on the ops repo, sub-issue files -- **Mechanism**: Formula-driven execution via `formulas/run-architect.toml` +- **Mechanism**: Bash-driven orchestration in `architect-run.sh`, pitching formula via `formulas/run-architect.toml` - **Identity**: `architect-bot` on Forgejo ## Responsibilities @@ -29,13 +29,45 @@ converses with humans through PR comments. ## Formula -The architect is driven by `formulas/run-architect.toml`. This formula defines +The architect pitching is driven by `formulas/run-architect.toml`. This formula defines the steps for: - Research: analyzing vision items and prerequisite tree -- Design: identifying implementation approaches and forks -- Sprint proposal: creating structured sprint PRs +- Pitch: creating structured sprint PRs - Sub-issue filing: creating concrete implementation issues +## Bash-driven design phase + +The design phase (ACCEPT → research → questions → answers → sub-issues) is +orchestrated by bash in `architect-run.sh`, not by the formula. This ensures: + +- **Deterministic state detection**: Bash reads the Forgejo reviews API to detect + ACCEPT/REJECT decisions — no model-dependent API parsing +- **Human guidance injection**: Review body text from ACCEPT reviews is injected + directly into the research prompt as context +- **Stateful session resumption**: When answers arrive on a subsequent run, the + saved Claude session is resumed (`--resume session_id`), preserving full + codebase context from the research phase +- **REJECT without model**: Rejections are handled entirely in bash (close PR, + delete branch, remove in-progress label, journal) — no model invocation needed + +### State transitions (bash-driven) + +``` +New vision issue → pitch PR (model) + ↓ +ACCEPT review → research + questions (model, session saved) + ↓ +Answers received → sub-issue filing (model, session resumed) + ↓ +REJECT review → close PR + journal (bash only) +``` + +### Per-PR session files + +Session IDs are saved per-PR in `/tmp/architect-sessions-{project}/pr-{number}.sid`. +This allows multiple architect PRs to be in different design phases simultaneously, +each with its own resumable session context. + ## Execution Run via `architect/architect-run.sh`, which: @@ -43,13 +75,12 @@ Run via `architect/architect-run.sh`, which: - Cleans up per-issue scratch files from previous runs (`/tmp/architect-{project}-scratch-*.md`) - Sources shared libraries (env.sh, formula-session.sh) - Uses FORGE_ARCHITECT_TOKEN for authentication +- Processes existing architect PRs via bash-driven design phase - Loads the formula and builds context from VISION.md, AGENTS.md, and ops repo -- Executes the formula via `agent_run` +- Executes the formula via `agent_run` for new pitches **Multi-sprint pitching**: The architect pitches up to 3 sprints per run. The pitch budget is `3 − `. After handling existing PRs (accept/reject/answer parsing), the architect selects up to `pitch_budget` vision issues (skipping any already with an open architect PR or `in-progress` label), then writes one per-issue scratch file (`/tmp/architect-{project}-scratch-{issue_number}.md`) and creates one sprint PR per scratch file. -**Session resumption (answer_parsing)**: When processing human answers on a PR in the `questions` phase (PR body has `## Design forks` + question comments), `architect-run.sh` resumes the prior Claude session (from `SID_FILE`) rather than starting fresh. This preserves deep codebase understanding from the research phase so sub-issues include specific file references. - ## Cron Suggested cron entry (every 6 hours): @@ -68,3 +99,4 @@ empty file not created, just document it). - #100: Architect formula — research + design fork identification - #101: Architect formula — sprint PR creation with questions - #102: Architect formula — answer parsing + sub-issue filing +- #491: Refactor — bash-driven design phase with stateful session resumption diff --git a/architect/architect-run.sh b/architect/architect-run.sh index 31df1e6..5cbf29c 100755 --- a/architect/architect-run.sh +++ b/architect/architect-run.sh @@ -10,7 +10,12 @@ # 2. Precondition checks: skip if no work (no vision issues, no responses) # 3. Load formula (formulas/run-architect.toml) # 4. Context: VISION.md, AGENTS.md, ops:prerequisites.md, structural graph -# 5. agent_run(worktree, prompt) → Claude decomposes vision into sprints +# 5. Bash-driven design phase: +# a. Fetch reviews API for ACCEPT/REJECT detection (deterministic) +# b. REJECT: handled entirely in bash (close PR, delete branch, journal) +# c. ACCEPT: invoke claude with human guidance injected into prompt +# d. Answers: resume saved session with answers injected +# 6. New pitches: agent_run(worktree, prompt) # # Precondition checks (bash before model): # - Skip if no vision issues AND no open architect PRs @@ -47,6 +52,9 @@ LOG_FILE="${DISINTO_LOG_DIR}/architect/architect.log" LOGFILE="$LOG_FILE" # shellcheck disable=SC2034 # consumed by agent-sdk.sh SID_FILE="/tmp/architect-session-${PROJECT_NAME}.sid" +# Per-PR session files for stateful resumption across runs +SID_DIR="/tmp/architect-sessions-${PROJECT_NAME}" +mkdir -p "$SID_DIR" SCRATCH_FILE="/tmp/architect-${PROJECT_NAME}-scratch.md" SCRATCH_FILE_PREFIX="/tmp/architect-${PROJECT_NAME}-scratch" WORKTREE="/tmp/${PROJECT_NAME}-architect-run" @@ -92,88 +100,164 @@ build_graph_section SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE") SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE") -# ── Build prompt ───────────────────────────────────────────────────────── +# ── Build prompt footer ────────────────────────────────────────────────── build_sdk_prompt_footer -# Architect prompt: strategic decomposition of vision into sprints -# See: architect/AGENTS.md for full role description -# Pattern: heredoc function to avoid inline prompt construction -# Note: Uses CONTEXT_BLOCK, GRAPH_SECTION, SCRATCH_CONTEXT from formula-session.sh -# Architecture Decision: AD-003 — The runtime creates and destroys, the formula preserves. -build_architect_prompt() { - cat <<_PROMPT_EOF_ -You are the architect agent for ${FORGE_REPO}. Work through the formula below. +# ── Design phase: bash-driven review detection ──────────────────────────── +# Fetch PR reviews from Forgejo API (deterministic, not model-dependent). +# Returns: decision (ACCEPT|REJECT|NONE), human_guidance, pr_number +# Args: pr_number +fetch_pr_review_decision() { + local pr_num="$1" + local decision="NONE" + local guidance="" -Your role: strategic decomposition of vision issues into development sprints. -Propose sprints via PRs on the ops repo, converse with humans through PR comments, -and file sub-issues after design forks are resolved. + # Step 1: Check PR reviews (Forgejo review UI) — takes precedence + local reviews_json + reviews_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}/reviews" 2>/dev/null) || reviews_json='[]' -## Project context -${CONTEXT_BLOCK} -${GRAPH_SECTION} -${SCRATCH_CONTEXT} -$(formula_lessons_block) -## Formula -${FORMULA_CONTENT} + # Find most recent non-bot review with a decision state + local review_decision review_body + review_decision=$(printf '%s' "$reviews_json" | jq -r ' + [.[] | select(.user.login | test("bot$"; "i") | not) + | select(.state == "APPROVED" or .state == "REQUEST_CHANGES")] + | last | .state // empty + ' 2>/dev/null) || review_decision="" + review_body=$(printf '%s' "$reviews_json" | jq -r ' + [.[] | select(.user.login | test("bot$"; "i") | not) + | select(.state == "APPROVED" or .state == "REQUEST_CHANGES")] + | last | .body // empty + ' 2>/dev/null) || review_body="" -${SCRATCH_INSTRUCTION} -${PROMPT_FOOTER} -_PROMPT_EOF_ + if [ "$review_decision" = "APPROVED" ]; then + decision="ACCEPT" + guidance="$review_body" + printf '%s\t%s' "$decision" "$guidance" + return 0 + elif [ "$review_decision" = "REQUEST_CHANGES" ]; then + decision="REJECT" + guidance="$review_body" + printf '%s\t%s' "$decision" "$guidance" + return 0 + fi + + # Step 2: Fallback — check PR comments for ACCEPT/REJECT text + local comments_json + comments_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_num}/comments" 2>/dev/null) || comments_json='[]' + + # Find most recent comment with ACCEPT or REJECT (case insensitive) + local comment_body + comment_body=$(printf '%s' "$comments_json" | jq -r ' + [.[] | select(.body | test("(?i)^\\s*(ACCEPT|REJECT)"))] | last | .body // empty + ' 2>/dev/null) || comment_body="" + + if [ -n "$comment_body" ]; then + if printf '%s' "$comment_body" | grep -qiE '^\s*ACCEPT'; then + decision="ACCEPT" + # Extract guidance text after ACCEPT (e.g., "ACCEPT — use SSH approach" → "use SSH approach") + guidance=$(printf '%s' "$comment_body" | sed -n 's/^[[:space:]]*[Aa][Cc][Cc][Ee][Pp][Tt][[:space:]]*[—:–-]*[[:space:]]*//p' | head -1) + # If guidance is empty on first line, use rest of comment + if [ -z "$guidance" ]; then + guidance=$(printf '%s' "$comment_body" | tail -n +2) + fi + elif printf '%s' "$comment_body" | grep -qiE '^\s*REJECT'; then + decision="REJECT" + guidance=$(printf '%s' "$comment_body" | sed -n 's/^[[:space:]]*[Rr][Ee][Jj][Ee][Cc][Tt][[:space:]]*[—:–-]*[[:space:]]*//p' | head -1) + if [ -z "$guidance" ]; then + guidance=$(printf '%s' "$comment_body" | tail -n +2) + fi + fi + fi + + printf '%s\t%s' "$decision" "$guidance" } -PROMPT=$(build_architect_prompt) +# Handle REJECT entirely in bash — no model invocation needed. +# Args: pr_number, pr_head_branch, rejection_reason +handle_reject() { + local pr_num="$1" + local pr_branch="$2" + local reason="$3" -# ── Create worktree ────────────────────────────────────────────────────── -formula_worktree_setup "$WORKTREE" + log "Handling REJECT for PR #${pr_num}: ${reason}" -# ── Detect if PR is in questions-awaiting-answers phase ────────────────── -# A PR is in the questions phase if it has a `## Design forks` section and -# question comments. We check this to decide whether to resume the session -# from the research/questions run (preserves codebase context for answer parsing). -detect_questions_phase() { - local pr_number="" - local pr_body="" + # Close the PR via Forgejo API + curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \ + -H 'Content-Type: application/json' \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" \ + -d '{"state":"closed"}' >/dev/null 2>&1 || log "WARN: failed to close PR #${pr_num}" - # Get open architect PRs on ops repo - local ops_repo="${OPS_REPO_ROOT:-/home/agent/data/ops}" - if [ ! -d "${ops_repo}/.git" ]; then - return 1 + # Delete the branch via Forgejo API + if [ -n "$pr_branch" ]; then + curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/git/branches/${pr_branch}" >/dev/null 2>&1 \ + || log "WARN: failed to delete branch ${pr_branch}" fi - # Use Forgejo API to find open architect PRs - local response - response=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open" 2>/dev/null) || return 1 - - # Check each open PR for architect markers - pr_number=$(printf '%s' "$response" | jq -r '.[] | select(.title | contains("architect:")) | .number' 2>/dev/null | head -1) || return 1 - - if [ -z "$pr_number" ]; then - return 1 - fi - - # Fetch PR body + # Remove in-progress label from the vision issue referenced in the PR + local pr_body pr_body=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_number}" 2>/dev/null | jq -r '.body // empty') || return 1 + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" 2>/dev/null | jq -r '.body // ""') || pr_body="" + local vision_ref + vision_ref=$(printf '%s' "$pr_body" | grep -oE '#[0-9]+' | head -1 | tr -d '#') || vision_ref="" + + if [ -n "$vision_ref" ]; then + # Look up in-progress label ID + local label_id + label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/labels" 2>/dev/null | jq -r '.[] | select(.name == "in-progress") | .id' 2>/dev/null) || label_id="" + if [ -n "$label_id" ]; then + curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/issues/${vision_ref}/labels/${label_id}" >/dev/null 2>&1 \ + || log "WARN: failed to remove in-progress label from issue #${vision_ref}" + fi + fi + + # Journal the rejection via .profile (if available) + profile_write_journal "architect-reject-${pr_num}" \ + "Sprint PR #${pr_num} rejected" \ + "rejected: ${reason}" "" || true + + # Clean up per-PR session file + rm -f "${SID_DIR}/pr-${pr_num}.sid" + + log "REJECT handled for PR #${pr_num}" +} + +# Detect answers on a PR in questions phase. +# Returns answer text via stdout, empty if no answers found. +# Args: pr_number +fetch_pr_answers() { + local pr_num="$1" + + # Get PR body to check for Design forks section + local pr_body + pr_body=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" 2>/dev/null | jq -r '.body // ""') || return 1 - # Check for `## Design forks` section (added by #101 after ACCEPT) if ! printf '%s' "$pr_body" | grep -q "## Design forks"; then return 1 fi - # Check for question comments (Q1:, Q2:, etc.) - # Use jq to extract body text before grepping (handles JSON escaping properly) - local comments - comments=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_number}/comments" 2>/dev/null) || return 1 + # Fetch comments and look for answer patterns (Q1: A, Q2: B, etc.) + local comments_json + comments_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_num}/comments" 2>/dev/null) || return 1 - if ! printf '%s' "$comments" | jq -r '.[].body // empty' | grep -qE 'Q[0-9]+:'; then - return 1 + # Find the most recent comment containing answer patterns + local answer_comment + answer_comment=$(printf '%s' "$comments_json" | jq -r ' + [.[] | select(.body | test("Q[0-9]+:\\s*[A-Da-d]"))] | last | .body // empty + ' 2>/dev/null) || answer_comment="" + + if [ -n "$answer_comment" ]; then + printf '%s' "$answer_comment" + return 0 fi - # PR is in questions phase - log "Detected PR #${pr_number} in questions-awaiting-answers phase" - return 0 + return 1 } # ── Sub-issue existence check ──────────────────────────────────────────── @@ -268,41 +352,184 @@ if [ "${vision_count:-0}" -eq 0 ]; then fi fi -# Check 2: Skip if already at max open pitches (3), unless there are responses to process -open_arch_prs=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ - "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=10" 2>/dev/null | jq '[.[] | select(.title | startswith("architect:"))] | length') || open_arch_prs=0 -if [ "${open_arch_prs:-0}" -ge 3 ]; then - # Check if any open architect PRs have ACCEPT/REJECT responses that need processing - has_responses=false - pr_numbers=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ - "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=10" 2>/dev/null | jq -r '.[] | select(.title | startswith("architect:")) | .number') || pr_numbers="" - for pr_num in $pr_numbers; do - comments=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ - "${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_num}/comments" 2>/dev/null) || continue - if printf '%s' "$comments" | jq -r '.[].body // empty' | grep -qE '(ACCEPT|REJECT):'; then - has_responses=true - break - fi - done - if [ "$has_responses" = false ]; then - log "already 3 open architect PRs with no responses to process — skipping" - 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 +# ── Design phase: process existing architect PRs (bash-driven) ──────────── +# Bash reads the reviews API and handles state transitions deterministically. +# Model is only invoked for research (ACCEPT) and answer processing. -# ── 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='[]' +open_arch_prs=$(printf '%s' "$open_arch_prs_json" | jq '[.[] | select(.title | startswith("architect:"))] | length') || open_arch_prs=0 + +# Track whether we processed any responses (to decide if pitching is needed) +processed_responses=false + +# Iterate over open architect PRs and handle each based on review state +arch_pr_data=$(printf '%s' "$open_arch_prs_json" | jq -r '.[] | select(.title | startswith("architect:")) | "\(.number)\t\(.head.ref // "")"' 2>/dev/null) || arch_pr_data="" + +while IFS=$'\t' read -r pr_num pr_branch; do + [ -z "$pr_num" ] && continue + + log "Checking PR #${pr_num} (branch: ${pr_branch})" + + # First check: is this PR in the answers phase (questions posted, answers received)? + answer_text="" + answer_text=$(fetch_pr_answers "$pr_num") || true + + if [ -n "$answer_text" ]; then + # ── Answers received: resume saved session with answers injected ── + log "Answers detected on PR #${pr_num} — resuming design session" + processed_responses=true + + pr_sid_file="${SID_DIR}/pr-${pr_num}.sid" + RESUME_ARGS=() + if [ -f "$pr_sid_file" ]; then + RESUME_SESSION=$(cat "$pr_sid_file") + RESUME_ARGS=(--resume "$RESUME_SESSION") + log "Resuming session ${RESUME_SESSION:0:12}... for answer processing" + else + log "No saved session for PR #${pr_num} — starting fresh for answers" + fi + + # Build answer-processing prompt with answers injected + # shellcheck disable=SC2034 + SID_FILE="$pr_sid_file" + ANSWER_PROMPT="You are the architect agent for ${FORGE_REPO}. You previously researched a sprint and posted design questions on PR #${pr_num}. + +Human answered the design fork questions. Parse the answers and file concrete sub-issues. + +## Human answers +${answer_text} + +## Project context +${CONTEXT_BLOCK} +${GRAPH_SECTION} +$(formula_lessons_block) + +## Instructions +1. Parse each answer (e.g. Q1: A, Q2: C) +2. Read the sprint spec from the PR branch +3. Look up the backlog label ID on the disinto repo: + GET ${FORGE_API}/labels — find label with name 'backlog' +4. Generate final sub-issues based on answers: + - Each sub-issue uses the appropriate issue template + - Fill all template fields (problem, solution, affected files max 3, acceptance criteria max 5, dependencies) + - File via Forgejo API on the disinto repo (not ops repo) + - MUST include 'labels' with backlog label ID in create-issue request + - Include 'Decomposed from #' in each issue body +5. Comment on PR #${pr_num}: 'Sprint filed: #N, #N, #N' +6. Merge the PR via: POST ${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}/merge with body {\"Do\":\"merge\"} + +${PROMPT_FOOTER}" + + # Create worktree if not already set up + if [ ! -d "$WORKTREE" ]; then + formula_worktree_setup "$WORKTREE" + fi + + export CLAUDE_MODEL="sonnet" + agent_run "${RESUME_ARGS[@]}" --worktree "$WORKTREE" "$ANSWER_PROMPT" + # Restore SID_FILE to default + # shellcheck disable=SC2034 # consumed by agent-sdk.sh + SID_FILE="/tmp/architect-session-${PROJECT_NAME}.sid" + log "Answer processing complete for PR #${pr_num}" + continue + fi + + # Second check: fetch review decision (ACCEPT/REJECT/NONE) + review_result=$(fetch_pr_review_decision "$pr_num") + decision=$(printf '%s' "$review_result" | cut -f1) + guidance=$(printf '%s' "$review_result" | cut -f2-) + + case "$decision" in + REJECT) + # ── REJECT: handled entirely in bash ── + handle_reject "$pr_num" "$pr_branch" "$guidance" + processed_responses=true + # Decrement open PR count (PR is now closed) + open_arch_prs=$((open_arch_prs - 1)) + ;; + + ACCEPT) + # ── ACCEPT: invoke model with human guidance for research + questions ── + log "ACCEPT detected on PR #${pr_num} with guidance: ${guidance:-(none)}" + processed_responses=true + + # Build human guidance block + GUIDANCE_BLOCK="" + if [ -n "$guidance" ]; then + GUIDANCE_BLOCK="## Human guidance (from sprint PR review) +${guidance} + +The architect MUST factor this guidance into design fork identification +and question formulation — if the human specifies an approach, that approach +should be the default fork, and questions should refine it rather than +re-evaluate it." + fi + + # Build research + questions prompt + RESEARCH_PROMPT="You are the architect agent for ${FORGE_REPO}. A sprint pitch on PR #${pr_num} has been ACCEPTED by a human reviewer. + +Your task: research the codebase deeply, identify design forks, and formulate questions. + +${GUIDANCE_BLOCK} + +## Project context +${CONTEXT_BLOCK} +${GRAPH_SECTION} +${SCRATCH_CONTEXT} +$(formula_lessons_block) + +## Instructions +1. Read the sprint spec from PR #${pr_num} on the ops repo (branch: ${pr_branch}) +2. Research the codebase deeply: + - Read all files mentioned in the sprint spec + - Search for existing interfaces that could be reused + - Check what infrastructure already exists +3. Identify design forks — multiple valid implementation approaches +4. Formulate multiple-choice questions (Q1, Q2, Q3...) +5. Update the sprint spec file on the PR branch: + - Add '## Design forks' section with fork options + - Add '## Proposed sub-issues' section with concrete issues per fork path + - Use Forgejo API: PUT ${FORGE_API}/repos/${FORGE_OPS_REPO}/contents/ with branch ${pr_branch} +6. Comment on PR #${pr_num} with the questions formatted as multiple choice: + - POST ${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_num}/comments + +${SCRATCH_INSTRUCTION} +${PROMPT_FOOTER}" + + # Use per-PR session file for stateful resumption + pr_sid_file="${SID_DIR}/pr-${pr_num}.sid" + # shellcheck disable=SC2034 + SID_FILE="$pr_sid_file" + + # Create worktree if not already set up + if [ ! -d "$WORKTREE" ]; then + formula_worktree_setup "$WORKTREE" + fi + + export CLAUDE_MODEL="sonnet" + agent_run --worktree "$WORKTREE" "$RESEARCH_PROMPT" + log "Research + questions posted for PR #${pr_num}, session saved: ${pr_sid_file}" + # Restore SID_FILE to default + # shellcheck disable=SC2034 # consumed by agent-sdk.sh + SID_FILE="/tmp/architect-session-${PROJECT_NAME}.sid" + ;; + + NONE) + log "PR #${pr_num} — no response yet, skipping" + ;; + esac +done <<< "$arch_pr_data" + +# ── Preflight: Select vision issues for pitching ────────────────────────── +# Recalculate open PR count after handling responses (REJECTs reduce count) + +# Re-fetch if we processed any responses (PR count may have changed) +if [ "$processed_responses" = true ]; then + 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='[]' + open_arch_prs=$(printf '%s' "$open_arch_prs_json" | jq '[.[] | select(.title | startswith("architect:"))] | length') || open_arch_prs=0 +fi # Build list of vision issues that already have open architect PRs declare -A _arch_vision_issues_with_open_prs @@ -317,6 +544,10 @@ while IFS= read -r pr_num; do 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 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 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="" @@ -334,7 +565,7 @@ while IFS= read -r vision_issue; do vision_issue_count=$((vision_issue_count + 1)) # Skip if pitch budget exhausted - if [ ${#ARCHITECT_TARGET_ISSUES[@]} -ge $pitch_budget ]; then + if [ "${pitch_budget}" -le 0 ] || [ ${#ARCHITECT_TARGET_ISSUES[@]} -ge "$pitch_budget" ]; then log "Pitch budget exhausted (${#ARCHITECT_TARGET_ISSUES[@]}/${pitch_budget})" break fi @@ -368,43 +599,68 @@ while IFS= read -r vision_issue; do 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 no issues selected and no responses processed, signal done 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" + if [ "$processed_responses" = true ]; then + log "No new pitches needed — responses already processed" 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 + fi + # 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 + if [ ${#ARCHITECT_TARGET_ISSUES[@]} -eq 0 ] && [ "$processed_responses" = false ]; then + exit 0 + fi + # If responses were processed but no pitches, still clean up and exit + if [ ${#ARCHITECT_TARGET_ISSUES[@]} -eq 0 ]; then + rm -f "$SCRATCH_FILE" + rm -f "${SCRATCH_FILE_PREFIX}"-*.md + profile_write_journal "architect-run" "Architect run $(date -u +%Y-%m-%d)" "complete" "" || true + log "--- Architect run done ---" exit 0 fi fi log "Selected ${#ARCHITECT_TARGET_ISSUES[@]} vision issue(s) for pitching: ${ARCHITECT_TARGET_ISSUES[*]}" -# ── Run agent ───────────────────────────────────────────────────────────── -export CLAUDE_MODEL="sonnet" +# ── Pitch prompt: research + PR creation (model handles pitching only) ──── +# Architecture Decision: AD-003 — The runtime creates and destroys, the formula preserves. +build_architect_prompt() { + cat <<_PROMPT_EOF_ +You are the architect agent for ${FORGE_REPO}. Work through the formula below. -# Determine whether to resume session: -# - If answers detected (PR in questions phase), resume prior session to preserve -# codebase context from research/questions run -# - Otherwise, start fresh (new pitch or PR not in questions phase) -RESUME_ARGS=() -if detect_questions_phase && [ -f "$SID_FILE" ]; then - RESUME_SESSION=$(cat "$SID_FILE") - RESUME_ARGS=(--resume "$RESUME_SESSION") - log "Resuming session from questions phase run: ${RESUME_SESSION:0:12}..." -elif ! detect_questions_phase; then - log "PR not in questions phase — starting fresh session" -elif [ ! -f "$SID_FILE" ]; then - log "No session ID found for questions phase — starting fresh session" +Your role: strategic decomposition of vision issues into development sprints. +Propose sprints via PRs on the ops repo. + +## Target vision issues for pitching +${ARCHITECT_TARGET_ISSUES[*]} + +## Project context +${CONTEXT_BLOCK} +${GRAPH_SECTION} +${SCRATCH_CONTEXT} +$(formula_lessons_block) +## Formula +${FORMULA_CONTENT} + +${SCRATCH_INSTRUCTION} +${PROMPT_FOOTER} +_PROMPT_EOF_ +} + +PROMPT=$(build_architect_prompt) + +# ── Create worktree (if not already set up from design phase) ───────────── +if [ ! -d "$WORKTREE" ]; then + formula_worktree_setup "$WORKTREE" fi -agent_run "${RESUME_ARGS[@]}" --worktree "$WORKTREE" "$PROMPT" -log "agent_run complete" +# ── Run agent for pitching ──────────────────────────────────────────────── +export CLAUDE_MODEL="sonnet" +agent_run --worktree "$WORKTREE" "$PROMPT" +log "agent_run complete (pitching)" # Clean up scratch files (legacy single file + per-issue files) rm -f "$SCRATCH_FILE" diff --git a/formulas/run-architect.toml b/formulas/run-architect.toml index f4996d9..e35c571 100644 --- a/formulas/run-architect.toml +++ b/formulas/run-architect.toml @@ -3,21 +3,24 @@ # Executed by architect-run.sh via cron — strategic decomposition of vision # issues into development sprints. # -# This formula orchestrates the architect agent's workflow: -# Step 1: Preflight — validate prerequisites, handle existing PRs, select up to 3 target issues +# This formula orchestrates the architect agent's pitching workflow: +# Step 1: Preflight — validate prerequisites, select up to 3 target issues # Step 2: Research + pitch — analyze codebase and write sprint pitch (loop over selected issues) -# Step 3: Sprint PR creation with questions (issue #101) (one PR per pitch) -# Step 4: Answer parsing + sub-issue filing (issue #102) +# Step 3: Sprint PR creation (one PR per pitch) # -# The architect pitches up to 3 sprints per run when multiple vision issues -# exist. Existing PRs (accept/reject) are handled first, then new pitches -# are created with remaining budget. Max 3 open architect PRs at any time. +# Design phase (ACCEPT/REJECT handling, questions, answer processing) is +# orchestrated by bash in architect-run.sh — not by this formula. +# See architect-run.sh for the bash-driven state machine: +# - Bash reads reviews API for ACCEPT/REJECT detection (deterministic) +# - REJECT handled entirely in bash (close PR, delete branch, journal) +# - ACCEPT: bash invokes model with human guidance injected +# - Answers: bash resumes saved session with answers injected # # AGENTS.md maintenance is handled by the gardener (#246). name = "run-architect" description = "Architect: strategic decomposition of vision into sprints" -version = 1 +version = 2 model = "opus" [context] @@ -27,140 +30,43 @@ files = ["VISION.md", "AGENTS.md"] [[steps]] id = "preflight" -title = "Preflight: validate prerequisites, handle existing PRs, select up to 3 target issues" +title = "Preflight: validate prerequisites, select up to 3 target issues" description = """ -This step performs preflight checks, handles existing architect PRs, and selects -up to 3 vision issues for pitching. +This step is performed by bash in architect-run.sh before the model is invoked. -Actions: +Bash handles: 1. Pull latest code from both disinto repo and ops repo 2. Read prerequisite tree from $OPS_REPO_ROOT/prerequisites.md 3. Fetch open issues labeled 'vision' from Forgejo API -4. Check for open architect PRs on ops repo (handled by #101/#102) -5. If open architect PRs exist, handle accept/reject responses FIRST (see Capability B below) +4. Check for open architect PRs on ops repo +5. Process existing PRs via bash-driven design phase (ACCEPT/REJECT/answers) 6. After handling existing PRs, count remaining open architect PRs 7. Select up to (3 - open_architect_pr_count) vision issues for new pitches 8. If no vision issues, signal PHASE:done ## Multi-pitch selection (up to 3 per run) -After handling existing PRs (accept/reject/answer parsing), determine how many -new pitches can be created: +After handling existing PRs, determine how many new pitches can be created: pitch_budget = 3 - 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. 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) +3. Check for existing sub-issues filed from this vision issue +4. Check for merged sprint PRs referencing this vision issue +5. From remaining candidates, pick the most unblocking issue first 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 pitch_budget <= 0 (already 3 open architect PRs), skip pitching - 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 +- If all vision issues have open sub-issues, skip pitching +- If all vision issues have merged sprint PRs, skip pitching Output: - Sets ARCHITECT_TARGET_ISSUES as a JSON array of issue numbers to pitch (up to 3) -- Exports VISION_ISSUES as a JSON array of issue objects - -## Capability B: Handle accept/reject on existing pitch PRs - -When open architect PRs exist on the ops repo: - -1. Check for human response — reviews first, then comments. - - **Step 1 — Check PR reviews (Forgejo review UI):** - Fetch reviews via Forgejo API: - ``` - GET /repos/{owner}/{repo}/pulls/{index}/reviews - ``` - Scan the response array for reviews with non-bot authors: - - `state: "APPROVED"` → treat as **ACCEPT**. Save the review `body` field as - human guidance text — it often contains critical architectural constraints - (e.g. "the caddy is on another host, use a dispatchable container with SSH key as secret"). - - `state: "REQUEST_CHANGES"` → treat as **REJECT** with the review `body` as the reason. - - Other states (COMMENT, PENDING) → ignore, not a decision. - - If multiple reviews exist, use the most recent one (last in array). - - **Step 2 — Check PR comments (fallback for backwards compat):** - Fetch comments on each open architect PR via Forgejo API. - Scan for ACCEPT/REJECT text as described below. - - **Precedence:** If both a review decision and a comment decision exist, - the review takes precedence. - -2. Act on the human response: - - **ACCEPT** (case insensitive, from review APPROVED or comment text): Human wants to proceed - - Extract the human guidance text: - - From review APPROVED: use the review `body` field (may be empty) - - From comment: use any text after ACCEPT (e.g. "ACCEPT — use SSH approach" → "use SSH approach") - - Architect does deep research for design forks (same as #100 research but now identifying decision points). - If human guidance text is non-empty, prepend it to the research context: - - ## Human guidance (from sprint PR review) - - - The architect MUST factor this guidance into design fork identification - and question formulation — e.g. if the human specifies an approach, - that approach should be the default fork, and questions should refine - it rather than re-evaluate it. - - Formulates multiple-choice questions (Q1, Q2, Q3...) - - Updates the sprint spec file on the PR branch: - - Adds `## Design forks` section with fork options - - Adds `## Proposed sub-issues` section with concrete issues per fork path - - Comments on the PR with the questions formatted as multiple choice - - Signal PHASE:done (answer processing is #102) - - **REJECT: ** (case insensitive, from review REQUEST_CHANGES or comment text; - for reviews the reason is the review body, for comments the reason follows the colon): - - Journal the rejection reason via profile_write_journal (if .profile exists) - — the architect learns what pitches fail - - Close the PR via Forgejo API (do not merge — rejected pitches do not persist in sprints/) - - Remove the branch via Forgejo API - - Remove `in-progress` label from the vision issue on the disinto repo: - - Look up the `in-progress` label ID via `GET /repos/{owner}/{repo}/labels` - - Remove the label via `DELETE /repos/{owner}/{repo}/issues/{vision_issue_number}/labels/{label_id}` - - Signal PHASE:done - - **No response yet**: skip silently, signal PHASE:done - -All git operations use the Forgejo API (create branch, write/update file, create PR, -close PR, delete branch). No SSH. """ [[steps]] @@ -185,7 +91,7 @@ Actions: - What are the risks (breaking changes, security implications, integration complexity)? - Is this mostly gluecode or greenfield? -3. Write sprint pitch to a per-issue scratch file for PR creation step (#101): +3. Write sprint pitch to a per-issue scratch file for PR creation step: - File path: /tmp/architect-{project}-scratch-{issue_number}.md # Sprint pitch: @@ -218,18 +124,18 @@ decision for the human. Questions come only after acceptance. Output: - Writes one scratch file per vision issue: /tmp/architect-{project}-scratch-{issue_number}.md -- Each pitch serves as input for sprint PR creation step (#101) +- Each pitch serves as input for sprint PR creation step - If ARCHITECT_TARGET_ISSUES is empty (budget exhausted or no candidates), skip this step """ [[steps]] id = "sprint_pr_creation" -title = "Sprint PR creation with questions (issue #101)" +title = "Sprint PR creation (one PR per pitch)" description = """ This step creates a PR on the ops repo for EACH sprint pitch produced in step 2. One PR per vision issue — loop over all scratch files. -## Capability A: Create pitch PRs (from research output) +## Create pitch PRs (from research output) For each vision issue in ARCHITECT_TARGET_ISSUES that produced a scratch file (/tmp/architect-{project}-scratch-{issue_number}.md): @@ -276,7 +182,6 @@ For each vision issue in ARCHITECT_TARGET_ISSUES that produced a scratch file - Look up the `in-progress` label ID via `GET /repos/{owner}/{repo}/labels` - Add the label via `POST /repos/{owner}/{repo}/issues/{vision_issue_number}/labels` Body: `{"labels": []}` - - This makes the vision issue visible as actively worked on 5. After creating all PRs, signal PHASE:done @@ -306,7 +211,6 @@ Body: {"title": "architect: ", "body": "", "head" - The `body` field must contain **plain markdown text** (the raw content from the scratch file) - Do NOT JSON-encode or escape the body — pass it as a JSON string value - Newlines and markdown formatting (headings, lists, etc.) must be preserved as-is -- Example: if the sprint pitch contains `## What this enables`, the PR body should render this as a markdown heading, not as literal `## What this enables` text ### Close PR ``` @@ -335,122 +239,3 @@ Body: {"labels": []} DELETE /repos/{owner}/{repo}/issues/{index}/labels/{label-id} ``` """ - -[[steps]] -id = "answer_parsing" -title = "Answer parsing + sub-issue filing (issue #102)" -description = """ -This step processes human answers to design questions and files sub-issues. - -## Session resumption - -When processing answers, the architect resumes the session from the research/questions -run (step 2) to preserve codebase context. This ensures Claude has full understanding -of dispatcher.sh, vault.sh, branch-protection.sh, and all formulas when filing -sub-issues, resulting in more specific file references and implementation details. - -The session ID is persisted to `$SID_FILE` after the research/questions run. On -answer_parsing runs, if the PR is in the questions phase, the session is resumed -instead of starting fresh. - -## Preflight: Detect PRs in question phase - -An architect PR is in the question phase if ALL of the following are true: -- PR is open -- PR body or sprint spec file contains a `## Design forks` section (added by #101 after ACCEPT) -- PR has question comments (Q1, Q2, Q3... format) - -## Answer parsing - -Human comments on the PR use this format: -``` -Q1: A -Q2: B -Q3: A -``` - -Parser matches lines starting with `Q` + digit(s) + `:` + space + letter A-D (case insensitive). -Ignore other content in the comment. - -## Processing paths - -### All questions answered (every `### Q` heading has a matching `Q: ` comment) - -1. Parse each answer (e.g. `Q1: A`, `Q2: C`) -2. Read the sprint spec from the PR branch -3. Look up the `backlog` label ID on the disinto repo: - - `GET /repos/{owner}/{repo}/labels` — find the label with `name: "backlog"` and note its `id` - - This ID is required for the issue creation API call below - -4. Generate final sub-issues based on answers: - - Each sub-issue uses the appropriate issue template (bug/feature/refactor from `.codeberg/ISSUE_TEMPLATE/`) - - Fill all template fields: - - Problem/motivation (feature) or What's broken (bug/refactor) - - Proposed solution (feature) or Approach (refactor) or Steps to reproduce (bug) - - Affected files (max 3) - - Acceptance criteria (max 5) - - Dependencies - - File via Forgejo API on the **disinto repo** (not ops repo) - - **MUST include `"labels": []`** in the create-issue request body - so dev-poll picks them up -5. Comment on PR: "Sprint filed: #N, #N, #N" -6. Merge the PR (sprint spec with answers persists in `ops/sprints/`) - -### Some questions answered, not all - -1. Acknowledge answers received -2. Comment listing remaining unanswered questions -3. Signal PHASE:done (check again next poll) - -### No answers yet (questions posted but human hasn't responded) - -1. Skip — signal PHASE:done - -## Forgejo API for filing issues on disinto repo - -All operations use the Forgejo API with `Authorization: token ${FORGE_TOKEN}` header. - -### Create issue (with backlog label — required) -``` -POST /repos/{owner}/{repo}/issues -Body: { - "title": "", - "body": "", - "labels": [] -} -``` - -### Close PR -``` -PATCH /repos/{owner}/{repo}/pulls/{index} -Body: {"state": "closed"} -``` - -### Merge PR -``` -POST /repos/{owner}/{repo}/pulls/{index}/merge -Body: {"Do": "merge"} -``` - -### Post comment on PR (via issues endpoint) -``` -POST /repos/{owner}/{repo}/issues/{index}/comments -Body: {"body": ""} -``` - -### Get labels (look up label IDs by name) -``` -GET /repos/{owner}/{repo}/labels -``` - -### Add label to issue -``` -POST /repos/{owner}/{repo}/issues/{index}/labels -Body: {"labels": []} -``` - -### Remove label from issue -``` -DELETE /repos/{owner}/{repo}/issues/{index}/labels/{label-id} -``` -"""