Merge pull request 'fix: refactor: architect pitching should be bash-driven with stateless model calls (#490)' (#496) from fix/issue-490 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

This commit is contained in:
dev-bot 2026-04-09 11:11:35 +00:00
commit 2478765dfa
3 changed files with 605 additions and 487 deletions

View file

@ -35,38 +35,34 @@ the steps for:
- Pitch: creating structured sprint PRs - Pitch: creating structured sprint PRs
- Sub-issue filing: creating concrete implementation issues - Sub-issue filing: creating concrete implementation issues
## Bash-driven design phase ## Bash-driven orchestration
The design phase (ACCEPT → research → questions → answers → sub-issues) is Bash in `architect-run.sh` handles state detection and orchestration:
orchestrated by bash in `architect-run.sh`, not by the formula. This ensures:
- **Deterministic state detection**: Bash reads the Forgejo reviews API to detect - **Deterministic state detection**: Bash reads the Forgejo reviews API to detect
ACCEPT/REJECT decisions — no model-dependent API parsing ACCEPT/REJECT decisions — no model-dependent API parsing
- **Human guidance injection**: Review body text from ACCEPT reviews is injected - **Human guidance injection**: Review body text from ACCEPT reviews is injected
directly into the research prompt as context directly into the research prompt as context
- **Stateful session resumption**: When answers arrive on a subsequent run, the - **Response processing**: When ACCEPT/REJECT responses are detected, bash invokes
saved Claude session is resumed (`--resume session_id`), preserving full the agent with appropriate context (session resumed for questions phase)
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) ### State transitions
``` ```
New vision issue → pitch PR (model) New vision issue → pitch PR (model generates pitch, bash creates PR)
ACCEPT review → research + questions (model, session saved) ACCEPT review → research + questions (model, session saved to $SID_FILE)
Answers received → sub-issue filing (model, session resumed) Answers received → sub-issue filing (model, session resumed via --resume)
REJECT review → close PR + journal (bash only) REJECT review → close PR + journal (model processes rejection, bash merges PR)
``` ```
### Per-PR session files ### Session management
Session IDs are saved per-PR in `/tmp/architect-sessions-{project}/pr-{number}.sid`. The agent maintains a global session file at `/tmp/architect-session-{project}.sid`.
This allows multiple architect PRs to be in different design phases simultaneously, When processing responses, bash checks if the PR is in the questions phase and
each with its own resumable session context. resumes the session using `--resume session_id` to preserve codebase context.
## Execution ## Execution
@ -77,9 +73,20 @@ Run via `architect/architect-run.sh`, which:
- Uses FORGE_ARCHITECT_TOKEN for authentication - Uses FORGE_ARCHITECT_TOKEN for authentication
- Processes existing architect PRs via bash-driven design phase - Processes existing architect PRs via bash-driven design phase
- Loads the formula and builds context from VISION.md, AGENTS.md, and ops repo - Loads the formula and builds context from VISION.md, AGENTS.md, and ops repo
- Executes the formula via `agent_run` for new pitches - Bash orchestrates state management:
- Fetches open vision issues, open architect PRs, and merged sprint PRs from Forgejo API
- Filters out visions already with open PRs, in-progress label, sub-issues, or merged sprint PRs
- Selects up to `pitch_budget` (3 - open architect PRs) remaining vision issues
- For each selected issue, invokes stateless `claude -p` with issue body + context
- Creates PRs directly from pitch content (no scratch files)
- Agent is invoked only for response processing (ACCEPT/REJECT handling)
**Multi-sprint pitching**: The architect pitches up to 3 sprints per run. The pitch budget is `3 <open architect PRs>`. 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. **Multi-sprint pitching**: The architect pitches up to 3 sprints per run. Bash handles all state management:
- Fetches Forgejo API data (vision issues, open PRs, merged PRs)
- Filters and deduplicates (no model-level dedup or journal-based memory)
- For each selected vision issue, bash invokes stateless `claude -p` to generate pitch markdown
- Bash creates the PR with pitch content and posts ACCEPT/REJECT footer comment
- Branch names use issue number (architect/sprint-vision-{issue_number}) to avoid collisions
## Cron ## Cron

View file

@ -10,12 +10,12 @@
# 2. Precondition checks: skip if no work (no vision issues, no responses) # 2. Precondition checks: skip if no work (no vision issues, no responses)
# 3. Load formula (formulas/run-architect.toml) # 3. Load formula (formulas/run-architect.toml)
# 4. Context: VISION.md, AGENTS.md, ops:prerequisites.md, structural graph # 4. Context: VISION.md, AGENTS.md, ops:prerequisites.md, structural graph
# 5. Bash-driven design phase: # 5. Stateless pitch generation: for each selected issue:
# a. Fetch reviews API for ACCEPT/REJECT detection (deterministic) # - Fetch issue body from Forgejo API (bash)
# b. REJECT: handled entirely in bash (close PR, delete branch, journal) # - Invoke claude -p with issue body + context (stateless, no API calls)
# c. ACCEPT: invoke claude with human guidance injected into prompt # - Create PR with pitch content (bash)
# d. Answers: resume saved session with answers injected # - Post footer comment (bash)
# 6. New pitches: agent_run(worktree, prompt) # 6. Response processing: handle ACCEPT/REJECT on existing PRs
# #
# Precondition checks (bash before model): # Precondition checks (bash before model):
# - Skip if no vision issues AND no open architect PRs # - Skip if no vision issues AND no open architect PRs
@ -60,7 +60,7 @@ SCRATCH_FILE_PREFIX="/tmp/architect-${PROJECT_NAME}-scratch"
WORKTREE="/tmp/${PROJECT_NAME}-architect-run" WORKTREE="/tmp/${PROJECT_NAME}-architect-run"
# Override LOG_AGENT for consistent agent identification # Override LOG_AGENT for consistent agent identification
# shellcheck disable=SC2034 # consumed by agent-sdk.sh and env.sh log() # shellcheck disable=SC2034 # consumed by agent-sdk.sh and env.sh
LOG_AGENT="architect" LOG_AGENT="architect"
# Override log() to append to architect-specific log file # Override log() to append to architect-specific log file
@ -100,162 +100,88 @@ build_graph_section
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE") SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE") SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# ── Build prompt footer ────────────────────────────────────────────────── # ── Build prompt ─────────────────────────────────────────────────────────
build_sdk_prompt_footer build_sdk_prompt_footer
# ── Design phase: bash-driven review detection ──────────────────────────── # Architect prompt: strategic decomposition of vision into sprints
# Fetch PR reviews from Forgejo API (deterministic, not model-dependent). # See: architect/AGENTS.md for full role description
# Sets global output variables (not stdout — guidance text is often multiline): # Pattern: heredoc function to avoid inline prompt construction
# REVIEW_DECISION — ACCEPT|REJECT|NONE # Note: Uses CONTEXT_BLOCK, GRAPH_SECTION, SCRATCH_CONTEXT from formula-session.sh
# REVIEW_GUIDANCE — human guidance text (review body or comment text) # Architecture Decision: AD-003 — The runtime creates and destroys, the formula preserves.
# Args: pr_number build_architect_prompt() {
fetch_pr_review_decision() { cat <<_PROMPT_EOF_
local pr_num="$1" You are the architect agent for ${FORGE_REPO}. Work through the formula below.
REVIEW_DECISION="NONE"
REVIEW_GUIDANCE=""
# Step 1: Check PR reviews (Forgejo review UI) — takes precedence Your role: strategic decomposition of vision issues into development sprints.
local reviews_json Propose sprints via PRs on the ops repo, converse with humans through PR comments,
reviews_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ and file sub-issues after design forks are resolved.
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}/reviews" 2>/dev/null) || reviews_json='[]'
# Find most recent non-bot review with a decision state ## Project context
local review_decision review_body ${CONTEXT_BLOCK}
review_decision=$(printf '%s' "$reviews_json" | jq -r ' ${GRAPH_SECTION}
[.[] | select(.user.login | test("bot$"; "i") | not) ${SCRATCH_CONTEXT}
| select(.state == "APPROVED" or .state == "REQUEST_CHANGES")] $(formula_lessons_block)
| last | .state // empty ## Formula
' 2>/dev/null) || review_decision="" ${FORMULA_CONTENT}
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=""
if [ "$review_decision" = "APPROVED" ]; then ${SCRATCH_INSTRUCTION}
REVIEW_DECISION="ACCEPT" ${PROMPT_FOOTER}
REVIEW_GUIDANCE="$review_body" _PROMPT_EOF_
return 0
elif [ "$review_decision" = "REQUEST_CHANGES" ]; then
REVIEW_DECISION="REJECT"
REVIEW_GUIDANCE="$review_body"
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
REVIEW_DECISION="ACCEPT"
# Extract guidance text after ACCEPT (e.g., "ACCEPT — use SSH approach" → "use SSH approach")
REVIEW_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 "$REVIEW_GUIDANCE" ]; then
REVIEW_GUIDANCE=$(printf '%s' "$comment_body" | tail -n +2)
fi
elif printf '%s' "$comment_body" | grep -qiE '^\s*REJECT'; then
REVIEW_DECISION="REJECT"
REVIEW_GUIDANCE=$(printf '%s' "$comment_body" | sed -n 's/^[[:space:]]*[Rr][Ee][Jj][Ee][Cc][Tt][[:space:]]*[—:-]*[[:space:]]*//p' | head -1)
if [ -z "$REVIEW_GUIDANCE" ]; then
REVIEW_GUIDANCE=$(printf '%s' "$comment_body" | tail -n +2)
fi
fi
fi
} }
# Handle REJECT entirely in bash — no model invocation needed. PROMPT=$(build_architect_prompt)
# Args: pr_number, pr_head_branch, rejection_reason
handle_reject() {
local pr_num="$1"
local pr_branch="$2"
local reason="$3"
log "Handling REJECT for PR #${pr_num}: ${reason}" # ── Create worktree ──────────────────────────────────────────────────────
formula_worktree_setup "$WORKTREE"
# Close the PR via Forgejo API # ── Detect if PR is in questions-awaiting-answers phase ──────────────────
curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \ # A PR is in the questions phase if it has a `## Design forks` section and
-H 'Content-Type: application/json' \ # question comments. We check this to decide whether to resume the session
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" \ # from the research/questions run (preserves codebase context for answer parsing).
-d '{"state":"closed"}' >/dev/null 2>&1 || log "WARN: failed to close PR #${pr_num}" detect_questions_phase() {
local pr_number=""
local pr_body=""
# Delete the branch via Forgejo API # Get open architect PRs on ops repo
if [ -n "$pr_branch" ]; then local ops_repo="${OPS_REPO_ROOT:-/home/agent/data/ops}"
curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \ if [ ! -d "${ops_repo}/.git" ]; then
"${FORGE_API}/repos/${FORGE_OPS_REPO}/git/branches/${pr_branch}" >/dev/null 2>&1 \ return 1
|| log "WARN: failed to delete branch ${pr_branch}"
fi fi
# Remove in-progress label from the vision issue referenced in the PR # Use Forgejo API to find open architect PRs
local pr_body local response
pr_body=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ response=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}" 2>/dev/null | jq -r '.body // ""') || pr_body="" "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open" 2>/dev/null) || return 1
local vision_ref
vision_ref=$(printf '%s' "$pr_body" | grep -oE '#[0-9]+' | head -1 | tr -d '#') || vision_ref=""
if [ -n "$vision_ref" ]; then # Check each open PR for architect markers
# Look up in-progress label ID pr_number=$(printf '%s' "$response" | jq -r '.[] | select(.title | contains("architect:")) | .number' 2>/dev/null | head -1) || return 1
local label_id
label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ if [ -z "$pr_number" ]; then
"${FORGE_API}/labels" 2>/dev/null | jq -r '.[] | select(.name == "in-progress") | .id' 2>/dev/null) || label_id="" return 1
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 fi
# Journal the rejection via .profile (if available) # Fetch PR body
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}" \ 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 "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_number}" 2>/dev/null | jq -r '.body // empty') || return 1
# Check for `## Design forks` section (added by #101 after ACCEPT)
if ! printf '%s' "$pr_body" | grep -q "## Design forks"; then if ! printf '%s' "$pr_body" | grep -q "## Design forks"; then
return 1 return 1
fi fi
# Fetch comments and look for answer patterns (Q1: A, Q2: B, etc.) # Check for question comments (Q1:, Q2:, etc.)
local comments_json # Use jq to extract body text before grepping (handles JSON escaping properly)
comments_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ local comments
"${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_num}/comments" 2>/dev/null) || return 1 comments=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_number}/comments" 2>/dev/null) || return 1
# Find the most recent comment containing answer patterns if ! printf '%s' "$comments" | jq -r '.[].body // empty' | grep -qE 'Q[0-9]+:'; then
local answer_comment return 1
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 fi
return 1 # PR is in questions phase
log "Detected PR #${pr_number} in questions-awaiting-answers phase"
return 0
} }
# ── Sub-issue existence check ──────────────────────────────────────────── # ── Sub-issue existence check ────────────────────────────────────────────
@ -335,6 +261,245 @@ has_merged_sprint_pr() {
return 1 # No merged sprint PR return 1 # No merged sprint PR
} }
# ── Helper: Fetch all open vision issues from Forgejo API ─────────────────
# Returns: JSON array of vision issue objects
fetch_vision_issues() {
curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/issues?labels=vision&state=open&limit=100" 2>/dev/null || echo '[]'
}
# ── Helper: Fetch open architect PRs from ops repo Forgejo API ───────────
# Returns: JSON array of architect PR objects
fetch_open_architect_prs() {
curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=100" 2>/dev/null || echo '[]'
}
# ── Helper: Get vision issue body by number ──────────────────────────────
# Args: issue_number
# Returns: issue body text
get_vision_issue_body() {
local issue_num="$1"
curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/issues/${issue_num}" 2>/dev/null | jq -r '.body // ""'
}
# ── Helper: Get vision issue title by number ─────────────────────────────
# Args: issue_number
# Returns: issue title
get_vision_issue_title() {
local issue_num="$1"
curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/issues/${issue_num}" 2>/dev/null | jq -r '.title // ""'
}
# ── Helper: Create a sprint pitch via stateless claude -p call ───────────
# The model NEVER calls Forgejo API. It only reads context and generates pitch.
# Args: vision_issue_number vision_issue_title vision_issue_body
# Returns: pitch markdown to stdout
#
# This is a stateless invocation: the model has no memory between calls.
# All state management (which issues to pitch, dedup logic, etc.) happens in bash.
generate_pitch() {
local issue_num="$1"
local issue_title="$2"
local issue_body="$3"
# Build context block with vision issue details
local pitch_context
pitch_context="
## Vision Issue #${issue_num}
### Title
${issue_title}
### Description
${issue_body}
## Project Context
${CONTEXT_BLOCK}
${GRAPH_SECTION}
$(formula_lessons_block)
## Formula
${FORMULA_CONTENT}
${SCRATCH_INSTRUCTION}
${PROMPT_FOOTER}
"
# Prompt: model generates pitch markdown only, no API calls
local pitch_prompt="You are the architect agent for ${FORGE_REPO}. Write a sprint pitch for the vision issue above.
Instructions:
1. Output ONLY the pitch markdown (no explanations, no preamble, no postscript)
2. Use this exact format:
# Sprint: <sprint-name>
## Vision issues
- #${issue_num} — ${issue_title}
## What this enables
<what the project can do after this sprint that it can't do now>
## What exists today
<current state — infrastructure, interfaces, code that can be reused>
## Complexity
<number of files/subsystems, estimated sub-issues>
<gluecode vs greenfield ratio>
## Risks
<what could go wrong, what breaks if this is done badly>
## Cost — new infra to maintain
<what ongoing maintenance burden does this sprint add>
<new services, cron jobs, formulas, agent roles>
## Recommendation
<architect's assessment: worth it / defer / alternative approach>
IMPORTANT: Do NOT include design forks or questions. This is a go/no-go pitch.
---
${pitch_context}
"
# Execute stateless claude -p call
local pitch_output
pitch_output=$(agent_run -p "$pitch_prompt" --output-format json --dangerously-skip-permissions --max-turns 200 ${CLAUDE_MODEL:+--model "$CLAUDE_MODEL"} 2>>"$LOGFILE") || true
# Extract pitch content from JSON response
local pitch
pitch=$(printf '%s' "$pitch_output" | jq -r '.content // empty' 2>/dev/null) || pitch=""
if [ -z "$pitch" ]; then
log "WARNING: empty pitch generated for vision issue #${issue_num}"
return 1
fi
# Output pitch to stdout for caller to use
printf '%s' "$pitch"
}
# ── Helper: Create PR on ops repo via Forgejo API ────────────────────────
# Args: sprint_title sprint_body branch_name
# Returns: PR number on success, empty on failure
create_sprint_pr() {
local sprint_title="$1"
local sprint_body="$2"
local branch_name="$3"
# Create branch on ops repo
if ! curl -sf -X POST \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/branches" \
-d "{\"new_branch_name\": \"${branch_name}\", \"old_branch_name\": \"${PRIMARY_BRANCH:-main}\"}" >/dev/null 2>&1; then
log "WARNING: failed to create branch ${branch_name}"
return 1
fi
# Extract sprint name from title for filename
local sprint_name
sprint_name=$(printf '%s' "$sprint_title" | sed 's/^architect: *//; s/ *$//')
local sprint_slug
sprint_slug=$(printf '%s' "$sprint_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | sed 's/--*/-/g')
# Prepare sprint spec content
local sprint_spec="# Sprint: ${sprint_name}
${sprint_body}
"
# Base64 encode the content
local sprint_spec_b64
sprint_spec_b64=$(printf '%s' "$sprint_spec" | base64 -w 0)
# Write sprint spec file to branch
if ! curl -sf -X PUT \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/contents/sprints/${sprint_slug}.md" \
-d "{\"message\": \"sprint: add ${sprint_slug}.md\", \"content\": \"${sprint_spec_b64}\", \"branch\": \"${branch_name}\"}" >/dev/null 2>&1; then
log "WARNING: failed to write sprint spec file"
return 1
fi
# Create PR - use jq to build JSON payload safely (prevents injection from markdown)
local pr_payload
pr_payload=$(jq -n \
--arg title "$sprint_title" \
--arg body "$sprint_body" \
--arg head "$branch_name" \
--arg base "${PRIMARY_BRANCH:-main}" \
'{title: $title, body: $body, head: $head, base: $base}')
local pr_response
pr_response=$(curl -sf -X POST \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls" \
-d "$pr_payload" 2>/dev/null) || return 1
# Extract PR number
local pr_number
pr_number=$(printf '%s' "$pr_response" | jq -r '.number // empty')
log "Created sprint PR #${pr_number}: ${sprint_title}"
printf '%s' "$pr_number"
}
# ── Helper: Post footer comment on PR ────────────────────────────────────
# Args: pr_number
post_pr_footer() {
local pr_number="$1"
local footer="Reply \`ACCEPT\` to proceed with design questions, or \`REJECT: <reason>\` to decline."
if curl -sf -X POST \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_number}/comments" \
-d "{\"body\": \"${footer}\"}" >/dev/null 2>&1; then
log "Posted footer comment on PR #${pr_number}"
return 0
else
log "WARNING: failed to post footer comment on PR #${pr_number}"
return 1
fi
}
# ── Helper: Add in-progress label to vision issue ────────────────────────
# Args: vision_issue_number
add_inprogress_label() {
local issue_num="$1"
# Get label ID for 'in-progress'
local labels_json
labels_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/labels" 2>/dev/null) || return 1
local inprogress_label_id
inprogress_label_id=$(printf '%s' "$labels_json" | jq -r --arg label "in-progress" '.[] | select(.name == $label) | .id' 2>/dev/null) || true
if [ -z "$inprogress_label_id" ]; then
log "WARNING: in-progress label not found"
return 1
fi
# Add label to issue
if curl -sf -X POST \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${FORGE_API}/repos/${FORGE_REPO}/issues/${issue_num}/labels" \
-d "{\"labels\": [${inprogress_label_id}]}" >/dev/null 2>&1; then
log "Added in-progress label to vision issue #${issue_num}"
return 0
else
log "WARNING: failed to add in-progress label to vision issue #${issue_num}"
return 1
fi
}
# ── Precondition checks in bash before invoking the model ───────────────── # ── Precondition checks in bash before invoking the model ─────────────────
# Check 1: Skip if no vision issues exist and no open architect PRs to handle # Check 1: Skip if no vision issues exist and no open architect PRs to handle
@ -350,190 +515,38 @@ if [ "${vision_count:-0}" -eq 0 ]; then
fi fi
fi fi
# ── Design phase: process existing architect PRs (bash-driven) ──────────── # Check 2: Skip if already at max open pitches (3), unless there are responses to process
# Bash reads the reviews API and handles state transitions deterministically. open_arch_prs=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
# Model is only invoked for research (ACCEPT) and answer processing. "${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=100" 2>/dev/null | jq '[.[] | select(.title | startswith("architect:"))] | length') || open_arch_prs=0
if [ "${open_arch_prs:-0}" -ge 3 ]; then
open_arch_prs_json=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \ # Check if any open architect PRs have ACCEPT/REJECT responses that need processing
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=10" 2>/dev/null) || open_arch_prs_json='[]' has_responses=false
open_arch_prs=$(printf '%s' "$open_arch_prs_json" | jq '[.[] | select(.title | startswith("architect:"))] | length') || open_arch_prs=0 pr_numbers=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=100" 2>/dev/null | jq -r '.[] | select(.title | startswith("architect:")) | .number') || pr_numbers=""
# Track whether we processed any responses (to decide if pitching is needed) for pr_num in $pr_numbers; do
processed_responses=false comments=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_num}/comments" 2>/dev/null) || continue
# Iterate over open architect PRs and handle each based on review state if printf '%s' "$comments" | jq -r '.[].body // empty' | grep -qE '(ACCEPT|REJECT):'; then
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="" has_responses=true
break
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 fi
done
# Build answer-processing prompt with answers injected if [ "$has_responses" = false ]; then
# shellcheck disable=SC2034 log "already 3 open architect PRs with no responses to process — skipping"
SID_FILE="$pr_sid_file" exit 0
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 #<vision_issue_number>' 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 fi
log "3 open architect PRs found but responses detected — processing"
# Second check: fetch review decision (ACCEPT/REJECT/NONE) # Track that we have responses to process (even if pitch_budget=0)
# Sets REVIEW_DECISION and REVIEW_GUIDANCE global variables has_responses_to_process=true
fetch_pr_review_decision "$pr_num"
decision="$REVIEW_DECISION"
guidance="$REVIEW_GUIDANCE"
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/<path> with branch ${pr_branch}
6. Update the PR body to include the Design forks section (required for answer detection):
- PATCH ${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}
- Body: {\"body\": \"<existing PR body + Design forks section>\"}
- The PR body MUST contain '## Design forks' after this step
7. 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 fi
# ── Bash-driven state management: Select vision issues for pitching ───────
# This logic is also documented in formulas/run-architect.toml preflight step
# Fetch all data from Forgejo API upfront (bash handles state, not model)
vision_issues_json=$(fetch_vision_issues)
open_arch_prs_json=$(fetch_open_architect_prs)
# Build list of vision issues that already have open architect PRs # Build list of vision issues that already have open architect PRs
declare -A _arch_vision_issues_with_open_prs declare -A _arch_vision_issues_with_open_prs
while IFS= read -r pr_num; do while IFS= read -r pr_num; do
@ -602,70 +615,126 @@ while IFS= read -r vision_issue; do
log "Selected vision issue #${vision_issue} for pitching" log "Selected vision issue #${vision_issue} for pitching"
done <<< "$vision_issue_nums" done <<< "$vision_issue_nums"
# If no issues selected and no responses processed, signal done # If no issues selected, decide whether to exit or process responses
if [ ${#ARCHITECT_TARGET_ISSUES[@]} -eq 0 ]; then if [ ${#ARCHITECT_TARGET_ISSUES[@]} -eq 0 ]; then
if [ "$processed_responses" = true ]; then if [ "${has_responses_to_process:-false}" = "true" ]; then
log "No new pitches needed — responses already processed" log "No new pitches needed — responses to process"
# Fall through to response processing block below
else else
log "No vision issues available for pitching (all have open PRs, sub-issues, or merged sprint PRs) — signaling PHASE:done" log "No vision issues available for pitching (all have open PRs, sub-issues, or merged sprint PRs) — signaling PHASE:done"
fi # Signal PHASE:done by writing to phase file if it exists
# Signal PHASE:done by writing to phase file if it exists if [ -f "/tmp/architect-${PROJECT_NAME}.phase" ]; then
if [ -f "/tmp/architect-${PROJECT_NAME}.phase" ]; then echo "PHASE:done" > "/tmp/architect-${PROJECT_NAME}.phase"
echo "PHASE:done" > "/tmp/architect-${PROJECT_NAME}.phase" fi
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 exit 0
fi fi
fi fi
log "Selected ${#ARCHITECT_TARGET_ISSUES[@]} vision issue(s) for pitching: ${ARCHITECT_TARGET_ISSUES[*]}" log "Selected ${#ARCHITECT_TARGET_ISSUES[@]} vision issue(s) for pitching: ${ARCHITECT_TARGET_ISSUES[*]}"
# ── Pitch prompt: research + PR creation (model handles pitching only) ──── # ── Stateless pitch generation and PR creation (bash-driven, no model API calls) ──
# Architecture Decision: AD-003 — The runtime creates and destroys, the formula preserves. # For each target issue:
build_architect_prompt() { # 1. Fetch issue body from Forgejo API (bash)
cat <<_PROMPT_EOF_ # 2. Invoke claude -p with issue body + context (stateless, no API calls)
You are the architect agent for ${FORGE_REPO}. Work through the formula below. # 3. Create PR with pitch content (bash)
# 4. Post footer comment (bash)
Your role: strategic decomposition of vision issues into development sprints. pitch_count=0
Propose sprints via PRs on the ops repo. for vision_issue in "${ARCHITECT_TARGET_ISSUES[@]}"; do
log "Processing vision issue #${vision_issue}"
## Target vision issues for pitching # Fetch vision issue details from Forgejo API (bash, not model)
${ARCHITECT_TARGET_ISSUES[*]} issue_title=$(get_vision_issue_title "$vision_issue")
issue_body=$(get_vision_issue_body "$vision_issue")
## Project context if [ -z "$issue_title" ] || [ -z "$issue_body" ]; then
${CONTEXT_BLOCK} log "WARNING: failed to fetch vision issue #${vision_issue} details"
${GRAPH_SECTION} continue
${SCRATCH_CONTEXT} fi
$(formula_lessons_block)
## Formula
${FORMULA_CONTENT}
${SCRATCH_INSTRUCTION} # Generate pitch via stateless claude -p call (model has no API access)
${PROMPT_FOOTER} log "Generating pitch for vision issue #${vision_issue}"
_PROMPT_EOF_ pitch=$(generate_pitch "$vision_issue" "$issue_title" "$issue_body") || true
}
PROMPT=$(build_architect_prompt) if [ -z "$pitch" ]; then
log "WARNING: failed to generate pitch for vision issue #${vision_issue}"
continue
fi
# ── Create worktree (if not already set up from design phase) ───────────── # Create sprint PR (bash, not model)
if [ ! -d "$WORKTREE" ]; then # Use issue number in branch name to avoid collisions across runs
formula_worktree_setup "$WORKTREE" branch_name="architect/sprint-vision-${vision_issue}"
pr_number=$(create_sprint_pr "architect: ${issue_title}" "$pitch" "$branch_name")
if [ -z "$pr_number" ]; then
log "WARNING: failed to create PR for vision issue #${vision_issue}"
continue
fi
# Post footer comment
post_pr_footer "$pr_number"
# Add in-progress label to vision issue
add_inprogress_label "$vision_issue"
pitch_count=$((pitch_count + 1))
log "Completed pitch for vision issue #${vision_issue} — PR #${pr_number}"
done
log "Generated ${pitch_count} sprint pitch(es)"
# ── Run agent for response processing if needed ───────────────────────────
# Always process ACCEPT/REJECT responses when present, regardless of new pitches
if [ "${has_responses_to_process:-false}" = "true" ]; then
log "Processing ACCEPT/REJECT responses on existing PRs"
# Check if any PRs have responses that need agent handling
needs_agent=false
pr_numbers=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls?state=open&limit=100" 2>/dev/null | jq -r '.[] | select(.title | startswith("architect:")) | .number') || pr_numbers=""
for pr_num in $pr_numbers; do
# Check for ACCEPT/REJECT in comments
comments=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/issues/${pr_num}/comments" 2>/dev/null) || continue
# Check for review decisions (higher precedence)
reviews=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"${FORGE_API}/repos/${FORGE_OPS_REPO}/pulls/${pr_num}/reviews" 2>/dev/null) || reviews=""
# Check for ACCEPT (APPROVED review or ACCEPT comment)
if printf '%s' "$reviews" | jq -e '.[] | select(.state == "APPROVED")' >/dev/null 2>&1; then
log "PR #${pr_num} has APPROVED review — needs agent handling"
needs_agent=true
elif printf '%s' "$comments" | jq -r '.[].body // empty' | grep -qiE '^[^:]+: *ACCEPT'; then
log "PR #${pr_num} has ACCEPT comment — needs agent handling"
needs_agent=true
elif printf '%s' "$comments" | jq -r '.[].body // empty' | grep -qiE '^[^:]+: *REJECT:'; then
log "PR #${pr_num} has REJECT comment — needs agent handling"
needs_agent=true
fi
done
# Run agent only if there are responses to process
if [ "$needs_agent" = "true" ]; then
# Determine whether to resume session
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"
fi
agent_run "${RESUME_ARGS[@]}" --worktree "$WORKTREE" "$PROMPT"
log "agent_run complete"
fi
fi fi
# ── Run agent for pitching ──────────────────────────────────────────────── # ── Clean up scratch files (legacy single file + per-issue files) ──────────
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" rm -f "$SCRATCH_FILE"
rm -f "${SCRATCH_FILE_PREFIX}"-*.md rm -f "${SCRATCH_FILE_PREFIX}"-*.md

View file

@ -3,18 +3,26 @@
# Executed by architect-run.sh via cron — strategic decomposition of vision # Executed by architect-run.sh via cron — strategic decomposition of vision
# issues into development sprints. # issues into development sprints.
# #
# This formula orchestrates the architect agent's pitching workflow: # This formula orchestrates the architect agent's workflow:
# Step 1: Preflight — validate prerequisites, select up to 3 target issues # Step 1: Preflight — bash handles state management:
# Step 2: Research + pitch — analyze codebase and write sprint pitch (loop over selected issues) # - Fetch open vision issues from Forgejo API
# Step 3: Sprint PR creation (one PR per pitch) # - Fetch open architect PRs on ops repo
# - Fetch merged architect PRs (already pitched visions)
# - Filter: remove visions with open PRs, merged sprints, or sub-issues
# - Select up to 3 remaining vision issues for pitching
# Step 2: Stateless pitch generation — for each selected issue:
# - Invoke claude -p with: vision issue body + codebase context
# - Model NEVER calls Forgejo API — only generates pitch markdown
# - Bash creates the ops PR with pitch content
# - Bash posts the ACCEPT/REJECT footer comment
# Step 3: Sprint PR creation with questions (issue #101) (one PR per pitch)
# Step 4: Answer parsing + sub-issue filing (issue #102)
# #
# Design phase (ACCEPT/REJECT handling, questions, answer processing) is # Architecture:
# orchestrated by bash in architect-run.sh — not by this formula. # - Bash script (architect-run.sh) handles ALL state management
# See architect-run.sh for the bash-driven state machine: # - Model calls are stateless — no Forgejo API access, no memory between calls
# - Bash reads reviews API for ACCEPT/REJECT detection (deterministic) # - Dedup is automatic via bash filters (no journal-based memory needed)
# - REJECT handled entirely in bash (close PR, delete branch, journal) # - Max 3 open architect PRs at any time
# - 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). # AGENTS.md maintenance is handled by the gardener (#246).
@ -30,19 +38,32 @@ files = ["VISION.md", "AGENTS.md"]
[[steps]] [[steps]]
id = "preflight" id = "preflight"
title = "Preflight: validate prerequisites, select up to 3 target issues" title = "Preflight: bash-driven state management and issue selection"
description = """ description = """
This step is performed by bash in architect-run.sh before the model is invoked. This step performs preflight checks and selects up to 3 vision issues for pitching.
IMPORTANT: All state management is handled by bash (architect-run.sh), NOT the model.
Bash handles: Architecture Decision: Bash-driven orchestration with stateless model calls
1. Pull latest code from both disinto repo and ops repo - The model NEVER calls Forgejo API during pitching
2. Read prerequisite tree from $OPS_REPO_ROOT/prerequisites.md - Bash fetches all data from Forgejo API (vision issues, open PRs, merged PRs)
3. Fetch open issues labeled 'vision' from Forgejo API - Bash filters and deduplicates (no model-level dedup or journal-based memory)
4. Check for open architect PRs on ops repo - For each selected issue, bash invokes stateless claude -p (model only generates pitch)
5. Process existing PRs via bash-driven design phase (ACCEPT/REJECT/answers) - Bash creates PRs and posts footer comments (no model API access)
6. After handling existing PRs, count remaining open architect PRs
7. Select up to (3 - open_architect_pr_count) vision issues for new pitches Bash Actions (in architect-run.sh):
8. If no vision issues, signal PHASE:done 1. Fetch open vision issues from Forgejo API: GET /repos/{owner}/{repo}/issues?labels=vision&state=open
2. Fetch open architect PRs from ops repo: GET /repos/{owner}/{repo}/pulls?state=open
3. Fetch merged sprint PRs: GET /repos/{owner}/{repo}/pulls?state=closed (filter merged=true)
4. Filter out visions that:
- Already have open architect PRs (check PR body for issue number reference)
- Have in-progress label
- Have open sub-issues (check for 'Decomposed from #N' pattern)
- Have merged sprint PRs (decomposition already done)
5. Select up to (3 - open_architect_pr_count) remaining vision issues
6. If no issues remain AND no responses to process, signal PHASE:done
If open architect PRs exist, handle accept/reject responses FIRST (see Capability B below).
After handling existing PRs, count remaining open architect PRs and calculate pitch_budget.
## Multi-pitch selection (up to 3 per run) ## Multi-pitch selection (up to 3 per run)
@ -71,82 +92,36 @@ Output:
[[steps]] [[steps]]
id = "research_pitch" id = "research_pitch"
title = "Research + pitch: analyze codebase and write sprint pitches (loop over selected issues)" title = "Stateless pitch generation: model generates content, bash creates PRs"
description = """ description = """
This step performs deep codebase research and writes a sprint pitch for EACH IMPORTANT: This step is executed by bash (architect-run.sh) via stateless claude -p calls.
vision issue in ARCHITECT_TARGET_ISSUES. The model NEVER calls Forgejo API it only reads context and generates pitch markdown.
For each issue in ARCHITECT_TARGET_ISSUES, perform the following: Architecture:
- Bash orchestrates the loop over ARCHITECT_TARGET_ISSUES
- For each issue: bash fetches issue body from Forgejo API, then invokes stateless claude -p
- Model receives: vision issue body + codebase context (VISION.md, AGENTS.md, prerequisites.md)
- Model outputs: sprint pitch markdown ONLY (no API calls, no side effects)
- Bash creates the PR and posts the ACCEPT/REJECT footer comment
Actions: For each issue in ARCHITECT_TARGET_ISSUES, bash performs:
1. Read the codebase deeply: 1. Fetch vision issue details from Forgejo API:
- Read all files mentioned in the issue body - GET /repos/{owner}/{repo}/issues/{issue_number}
- Search for existing interfaces that could be reused - Extract: title, body
- Check what infrastructure already exists
2. Assess complexity and cost: 2. Invoke stateless claude -p with prompt:
- How many files/subsystems are touched? "Write a sprint pitch for this vision issue. Output only the pitch markdown."
- What new infrastructure would need to be maintained after this sprint? Context provided:
- What are the risks (breaking changes, security implications, integration complexity)? - Vision issue #N: <title>
- Is this mostly gluecode or greenfield? - Vision issue body
- Project context (VISION.md, AGENTS.md)
- Codebase context (prerequisites.md, graph section)
- Formula content
3. Write sprint pitch to a per-issue scratch file for PR creation step: 3. Model generates pitch markdown (NO API CALLS):
- File path: /tmp/architect-{project}-scratch-{issue_number}.md
# Sprint pitch: <name> # Sprint: <sprint-name>
## Vision issues
- #N — <title>
## What this enables
<what the project can do after this sprint that it can't do now>
## What exists today
<current state infrastructure, interfaces, code that can be reused>
## Complexity
<number of files, subsystems, estimated sub-issues>
<gluecode vs greenfield ratio>
## Risks
<what could go wrong, what breaks if this is done badly>
## Cost — new infra to maintain
<what ongoing maintenance burden does this sprint add>
<new services, cron jobs, formulas, agent roles>
## Recommendation
<architect's assessment: worth it / defer / alternative approach>
IMPORTANT: Do NOT include design forks or questions yet. The pitch is a go/no-go
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
- If ARCHITECT_TARGET_ISSUES is empty (budget exhausted or no candidates), skip this step
"""
[[steps]]
id = "sprint_pr_creation"
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.
## 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):
1. Create branch `architect/<sprint-slug>` on ops repo via Forgejo API
- Sprint slug: lowercase, hyphenated version of sprint name
- Use Forgejo API: POST /repos/{owner}/{repo}/git/branches
2. Write sprint spec file to sprints/<sprint-slug>.md on the new branch:
# Sprint: <name>
## Vision issues ## Vision issues
- #N — <title> - #N — <title>
@ -171,19 +146,86 @@ For each vision issue in ARCHITECT_TARGET_ISSUES that produced a scratch file
## Recommendation ## Recommendation
<architect's assessment: worth it / defer / alternative approach> <architect's assessment: worth it / defer / alternative approach>
3. Create PR on ops repo via Forgejo API: IMPORTANT: Do NOT include design forks or questions yet. The pitch is a go/no-go
- Title: `architect: <sprint summary>` decision for the human. Questions come only after acceptance.
- Body: **plain markdown text** from the scratch file (pitch content with sections: What this enables, Complexity, Risks, Cost, Recommendation). Preserve newlines as-is do NOT JSON-encode the body.
- Base branch: primary branch (main/master)
- Head branch: architect/<sprint-slug>
- Footer: "Reply `ACCEPT` to proceed with design questions, or `REJECT: <reason>` to decline."
4. Add `in-progress` label to the vision issue on the disinto repo: 4. Bash creates PR:
- Look up the `in-progress` label ID via `GET /repos/{owner}/{repo}/labels` - Create branch: architect/sprint-{pitch-number}
- Add the label via `POST /repos/{owner}/{repo}/issues/{vision_issue_number}/labels` - Write sprint spec to sprints/{sprint-slug}.md
Body: `{"labels": [<in-progress-label-id>]}` - Create PR with pitch content as body
- Post footer comment: "Reply ACCEPT to proceed with design questions, or REJECT: <reason> to decline."
- Add in-progress label to vision issue
5. After creating all PRs, signal PHASE:done Output:
- One PR per vision issue (up to 3 per run)
- Each PR contains the pitch markdown
- If ARCHITECT_TARGET_ISSUES is empty, skip this step
"""
[[steps]]
id = "sprint_pr_creation"
title = "Sprint PR creation with questions (issue #101) — handled by bash"
description = """
IMPORTANT: PR creation is handled by bash (architect-run.sh) during the pitch step.
This step is for documentation only the actual PR creation happens in research_pitch.
Architecture:
- Bash creates PRs during stateless pitch generation (step 2)
- Model has no role in PR creation no Forgejo API access
- This step describes the PR format for reference
PR Format (created by bash):
1. Branch: architect/sprint-{pitch-number}
2. Sprint spec file: sprints/{sprint-slug}.md
Contains the pitch markdown from the model.
3. PR via Forgejo API:
- Title: architect: <sprint summary>
- Body: plain markdown text from model output
- Base: main (or PRIMARY_BRANCH)
- Head: architect/sprint-{pitch-number}
- Footer comment: "Reply ACCEPT to proceed with design questions, or REJECT: <reason> to decline."
4. Add in-progress label to vision issue:
- Look up label ID: GET /repos/{owner}/{repo}/labels
- Add label: POST /repos/{owner}/{repo}/issues/{issue_number}/labels
After creating all PRs, signal PHASE:done.
## Forgejo API Reference
All operations use the Forgejo API with Authorization: token ${FORGE_TOKEN} header.
### Create branch
```
POST /repos/{owner}/{repo}/branches
Body: {"new_branch_name": "architect/<sprint-slug>", "old_branch_name": "main"}
```
### Create/update file
```
PUT /repos/{owner}/{repo}/contents/<path>
Body: {"message": "sprint: add <sprint-slug>.md", "content": "<base64-encoded-content>", "branch": "architect/<sprint-slug>"}
```
### Create PR
```
POST /repos/{owner}/{repo}/pulls
Body: {"title": "architect: <sprint summary>", "body": "<markdown-text>", "head": "architect/<sprint-slug>", "base": "main"}
```
**Important: PR body format**
- The body field must contain plain markdown text (the raw content from the model)
- 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
### Add label to issue
```
POST /repos/{owner}/{repo}/issues/{index}/labels
Body: {"labels": [<label-id>]}
```
## Forgejo API Reference ## Forgejo API Reference