diff --git a/architect/AGENTS.md b/architect/AGENTS.md index 49d32b3..e705f23 100644 --- a/architect/AGENTS.md +++ b/architect/AGENTS.md @@ -10,9 +10,9 @@ converses with humans through PR comments. ## Role - **Input**: Vision issues from VISION.md, prerequisite tree from ops repo -- **Output**: Sprint proposals as PRs on the ops repo, sub-issue files +- **Output**: Sprint proposals as PRs on the ops repo (with embedded `## Sub-issues` blocks) - **Mechanism**: Bash-driven orchestration in `architect-run.sh`, pitching formula via `formulas/run-architect.toml` -- **Identity**: `architect-bot` on Forgejo +- **Identity**: `architect-bot` on Forgejo (READ-ONLY on project repo, write on ops repo only — #764) ## Responsibilities @@ -24,16 +24,17 @@ converses with humans through PR comments. acceptance criteria and dependencies 4. **Human conversation**: Respond to PR comments, refine sprint proposals based on human feedback -5. **Sub-issue filing**: After design forks are resolved, file concrete sub-issues - for implementation +5. **Sub-issue definition**: Define concrete sub-issues in the `## Sub-issues` + block of the sprint spec. Filing is handled by `filer-bot` after sprint PR + merge (#764) ## Formula The architect pitching is driven by `formulas/run-architect.toml`. This formula defines the steps for: - Research: analyzing vision items and prerequisite tree -- Pitch: creating structured sprint PRs -- Sub-issue filing: creating concrete implementation issues +- Pitch: creating structured sprint PRs with embedded `## Sub-issues` blocks +- Design Q&A: refining the sprint via PR comments after human ACCEPT ## Bash-driven orchestration @@ -57,22 +58,31 @@ APPROVED review → start design questions (model posts Q1:, adds Design forks s ↓ Answers received → continue Q&A (model processes answers, posts follow-ups) ↓ -All forks resolved → sub-issue filing (model files implementation issues) +All forks resolved → finalize ## Sub-issues section in sprint spec + ↓ +Sprint PR merged → filer-bot files sub-issues on project repo (#764) ↓ REJECT review → close PR + journal (model processes rejection, bash merges PR) ``` ### Vision issue lifecycle -Vision issues decompose into sprint sub-issues tracked via "Decomposed from #N" in sub-issue bodies. The architect automatically closes vision issues when all sub-issues are closed: +Vision issues decompose into sprint sub-issues. Sub-issues are defined in the +`## Sub-issues` block of the sprint spec (between `` and +`` markers) and filed by `filer-bot` after the sprint PR merges +on the ops repo (#764). -1. Before picking new vision issues, the architect checks each open vision issue -2. For each, it queries merged sprint PRs — **only PRs whose title or body reference the specific vision issue** (matched via `#N` pattern, filtering out unrelated PRs that happen to close unrelated issues) (#735/#736) -3. Extracts sub-issue numbers from those PRs, excluding the vision issue itself -4. If all sub-issues are closed, posts a summary comment listing completed sub-issues (with an idempotency guard: checks both comment presence AND `.state == "closed"` — if the comment exists but the issue is still open, retries the close rather than returning early) (#737) -5. The vision issue is then closed automatically +Each filer-created sub-issue carries a `` +marker in its body for idempotency and traceability. -This ensures vision issues transition from `open` → `closed` once their work is complete, without manual intervention. The #N-scoped matching prevents false positives where unrelated sub-issues would incorrectly trigger vision issue closure. +The filer-bot (via `lib/sprint-filer.sh`) handles vision lifecycle: +1. After filing sub-issues, adds `in-progress` label to the vision issue +2. On each run, checks if all sub-issues for a vision are closed +3. If all closed, posts a summary comment and closes the vision issue + +The architect no longer writes to the project repo — it is read-only (#764). +All project-repo writes (issue filing, label management, vision closure) are +handled by filer-bot with its narrowly-scoped `FORGE_FILER_TOKEN`. ### Session management @@ -95,7 +105,9 @@ Run via `architect/architect-run.sh`, which: - 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) +- Agent is invoked for stateless pitch generation and response processing (ACCEPT/REJECT handling) +- NOTE: architect-bot is read-only on the project repo (#764) — sub-issue filing + and in-progress label management are handled by filer-bot after sprint PR merge **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) @@ -120,4 +132,5 @@ 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 +- #764: Permission scoping — architect read-only on project repo, filer-bot files sub-issues - #491: Refactor — bash-driven design phase with stateful session resumption diff --git a/lib/sprint-filer.sh b/lib/sprint-filer.sh index e2b45a6..916d7c3 100755 --- a/lib/sprint-filer.sh +++ b/lib/sprint-filer.sh @@ -42,6 +42,31 @@ filer_log() { : "${FORGE_FILER_TOKEN:?sprint-filer.sh requires FORGE_FILER_TOKEN}" : "${FORGE_API:?sprint-filer.sh requires FORGE_API}" +# ── Paginated Forgejo API fetch ────────────────────────────────────────── +# Fetches all pages of a Forgejo API list endpoint and merges into one JSON array. +# Args: api_path (e.g. /issues?state=all&type=issues) +# Output: merged JSON array to stdout +filer_api_all() { + local path_prefix="$1" + local sep page page_items count all_items="[]" + case "$path_prefix" in + *"?"*) sep="&" ;; + *) sep="?" ;; + esac + page=1 + while true; do + page_items=$(curl -sf -H "Authorization: token ${FORGE_FILER_TOKEN}" \ + "${FORGE_API}${path_prefix}${sep}limit=50&page=${page}" 2>/dev/null) || page_items="[]" + count=$(printf '%s' "$page_items" | jq 'length' 2>/dev/null) || count=0 + [ -z "$count" ] && count=0 + [ "$count" -eq 0 ] && break + all_items=$(printf '%s\n%s' "$all_items" "$page_items" | jq -s 'add') + [ "$count" -lt 50 ] && break + page=$((page + 1)) + done + printf '%s' "$all_items" +} + # ── Parse sub-issues block from a sprint markdown file ─────────────────── # Extracts the YAML-in-markdown between and # Args: sprint_file_path @@ -93,11 +118,36 @@ parse_subissues_block() { } # ── Extract vision issue number from sprint file ───────────────────────── -# Looks for "## Vision issues" section with "#N" references +# Looks for "#N" references specifically in the "## Vision issues" section +# to avoid picking up cross-links or related-issue mentions earlier in the file. +# Falls back to first #N in the file if no "## Vision issues" section found. # Args: sprint_file_path # Output: first vision issue number found extract_vision_issue() { local sprint_file="$1" + + # Try to extract from "## Vision issues" section first + local in_section=false + local result="" + while IFS= read -r line; do + if [[ "$line" =~ ^##[[:space:]]+Vision[[:space:]]+issues ]]; then + in_section=true + continue + fi + # Stop at next heading + if [ "$in_section" = true ] && [[ "$line" =~ ^## ]]; then + break + fi + if [ "$in_section" = true ]; then + result=$(printf '%s' "$line" | grep -oE '#[0-9]+' | head -1 | tr -d '#') + if [ -n "$result" ]; then + printf '%s' "$result" + return 0 + fi + fi + done < "$sprint_file" + + # Fallback: first #N in the entire file grep -oE '#[0-9]+' "$sprint_file" | head -1 | tr -d '#' } @@ -255,10 +305,9 @@ subissue_exists() { local marker="" - # Search for issues with this exact marker + # Search all issues (paginated) for the exact marker local issues_json - issues_json=$(curl -sf -H "Authorization: token ${FORGE_FILER_TOKEN}" \ - "${FORGE_API}/issues?state=all&limit=50&type=issues" 2>/dev/null) || issues_json="[]" + issues_json=$(filer_api_all "/issues?state=all&type=issues") if printf '%s' "$issues_json" | jq -e --arg marker "$marker" \ '[.[] | select(.body // "" | contains($marker))] | length > 0' >/dev/null 2>&1; then @@ -444,8 +493,7 @@ check_and_close_completed_visions() { filer_log "Checking for vision issues with all sub-issues complete..." local vision_issues_json - vision_issues_json=$(curl -sf -H "Authorization: token ${FORGE_FILER_TOKEN}" \ - "${FORGE_API}/issues?labels=vision&state=open&limit=100" 2>/dev/null) || vision_issues_json="[]" + vision_issues_json=$(filer_api_all "/issues?labels=vision&state=open") if [ "$vision_issues_json" = "[]" ] || [ "$vision_issues_json" = "null" ]; then filer_log "No open vision issues found" @@ -453,8 +501,7 @@ check_and_close_completed_visions() { fi local all_issues - all_issues=$(curl -sf -H "Authorization: token ${FORGE_FILER_TOKEN}" \ - "${FORGE_API}/issues?state=all&limit=200&type=issues" 2>/dev/null) || all_issues="[]" + all_issues=$(filer_api_all "/issues?state=all&type=issues") local vision_nums vision_nums=$(printf '%s' "$vision_issues_json" | jq -r '.[].number' 2>/dev/null) || return 0