diff --git a/planner/planner-agent.sh b/planner/planner-agent.sh index e0c17b4..013ec4b 100755 --- a/planner/planner-agent.sh +++ b/planner/planner-agent.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # ============================================================================= -# planner-agent.sh — Keep AGENTS.md current, then gap-analyse against VISION.md +# planner-agent.sh — Update AGENTS.md tree, then gap-analyse against VISION.md # # Two-phase planner run: -# Phase 1: Review recent git history, suggest AGENTS.md updates via PR -# Phase 2: Compare AGENTS.md vs VISION.md, create backlog issues for gaps +# Phase 1: Navigate and update AGENTS.md tree using Claude with tool access +# Phase 2: Compare AGENTS.md + STATE.md vs VISION.md, create backlog issues # # Usage: planner-agent.sh (no args — uses env vars from .env / env.sh) # ============================================================================= @@ -18,8 +18,7 @@ source "$FACTORY_ROOT/lib/env.sh" LOG_FILE="$SCRIPT_DIR/planner.log" CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-3600}" -MARKER_FILE="${PROJECT_REPO_ROOT}/.last-planner-sha" -AGENTS_FILE="${PROJECT_REPO_ROOT}/AGENTS.md" +STATE_FILE="${PROJECT_REPO_ROOT}/STATE.md" VISION_FILE="${PROJECT_REPO_ROOT}/VISION.md" log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; } @@ -33,133 +32,150 @@ git pull --ff-only origin "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true HEAD_SHA=$(git rev-parse HEAD) log "--- Planner start (HEAD: ${HEAD_SHA:0:7}) ---" -# ── Determine git log range ───────────────────────────────────────────── -if [ -f "$MARKER_FILE" ]; then - LAST_SHA=$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]') - if git cat-file -e "$LAST_SHA" 2>/dev/null; then - GIT_RANGE="${LAST_SHA}..HEAD" +# ── Phase 1: Update AGENTS.md tree ────────────────────────────────────── +log "Phase 1: updating AGENTS.md tree" + +# Find all AGENTS.md files and their watermarks +AGENTS_FILES=$(find . -name "AGENTS.md" -not -path "./.git/*" | sort) +AGENTS_INFO="" +NEEDS_UPDATE=false + +for f in $AGENTS_FILES; do + WATERMARK=$(grep -oP '(?<=\` on line 1 +- The watermark tells you which commits are already reflected -# ── Phase 1: AGENTS.md maintenance ────────────────────────────────────── -if [ "$COMMIT_COUNT" -eq 0 ]; then - log "No new commits since last run — skipping AGENTS.md review" -else - log "Phase 1: reviewing AGENTS.md against recent changes" +## Current AGENTS.md files +$(echo -e "$AGENTS_INFO") +## Current HEAD: ${HEAD_SHA} - CURRENT_AGENTS="" - [ -f "$AGENTS_FILE" ] && CURRENT_AGENTS=$(cat "$AGENTS_FILE") +## Your workflow +1. Read the root AGENTS.md. Note its watermark SHA. +2. Run \`git log --stat ..HEAD\` to see what changed since last review. + If watermark is 'none', use \`git log --stat -20\` for recent history. +3. For structural changes (new files, renames, major refactors), run \`git show \` + or read the affected source files to understand the change. +4. Follow references to sub-directory AGENTS.md files. Repeat steps 1-3 for each. +5. Update any AGENTS.md file that is stale or missing information about changes. +6. If a directory has significant logic but no AGENTS.md, create one. - if [ -z "$CURRENT_AGENTS" ]; then - log "No AGENTS.md found — skipping phase 1" +## AGENTS.md conventions (follow these strictly) +- Max ~200 lines per file — if longer, split into sub-directory files +- Describe architecture and conventions (WHAT and WHY), not implementation details +- Link to source files for specifics: \`See [file.sol](path) for X\` +- Progressive disclosure: high-level in root, details in sub-directory files +- After updating a file, set its watermark to: \`\` +- The watermark MUST be the very first line of the file + +## Important +- Only update files that are actually stale (have changes since watermark) +- Do NOT rewrite files that are already current +- Do NOT remove existing accurate content — only add, update, or restructure +- Keep the writing factual and architectural — no changelog language" + + PHASE1_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PHASE1_PROMPT" \ + --model sonnet \ + --dangerously-skip-permissions \ + --max-turns 30 \ + 2>/dev/null) || { + EXIT_CODE=$? + log "ERROR: claude exited with code $EXIT_CODE during phase 1" + git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true + exit 1 + } + + log "Phase 1 claude finished ($(echo "$PHASE1_OUTPUT" | wc -c) bytes)" + + # Check if any files were modified + if git diff --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then + log "No AGENTS.md changes — nothing to commit" + git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true else - # Files changed in this range - FILES_CHANGED=$(git diff --name-only "$GIT_RANGE" 2>/dev/null | sort -u || true) + # Commit and push + git add -A "*.md" 2>/dev/null || git add -A 2>/dev/null + # Only add AGENTS.md files and STATE.md + git diff --cached --name-only | grep -vE '(AGENTS|STATE)\.md$' | xargs -r git reset HEAD -- 2>/dev/null || true - PHASE1_PROMPT="You maintain AGENTS.md for the ${PROJECT_NAME} repository. AGENTS.md is the primary onboarding document — it describes the project's architecture, file layout, conventions, and how to work in the codebase. + if ! git diff --cached --quiet; then + git commit -m "chore: planner update AGENTS.md tree" --quiet 2>/dev/null + git push -f origin "$BRANCH_NAME" --quiet 2>/dev/null || { + log "ERROR: failed to push $BRANCH_NAME" + git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true + exit 1 + } + git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true -## Current AGENTS.md -${CURRENT_AGENTS} - -## Recent commits (since last review) -${GIT_LOG} - -## Files changed -${FILES_CHANGED} - -## Repository file tree (top 3 levels) -${FILE_TREE} - -## Task -Review AGENTS.md against the recent changes. Output an UPDATED version of AGENTS.md that: -- Reflects any new directories, scripts, tools, or conventions introduced by the recent commits -- Removes or updates references to things that were deleted or renamed -- Keeps the existing structure, voice, and level of detail — this is a human-curated document, preserve its character -- Does NOT add issue/PR references — AGENTS.md is timeless documentation, not a changelog -- Does NOT rewrite sections that haven't changed — preserve the original text where possible - -If AGENTS.md is already fully up to date and no changes are needed, output exactly: NO_CHANGES - -Otherwise, output the complete updated AGENTS.md content (not a diff, the full file). -Do NOT wrap the output in markdown fences. Start directly with the file content." - - PHASE1_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PHASE1_PROMPT" \ - --model sonnet \ - --dangerously-skip-permissions \ - 2>/dev/null) || { - log "ERROR: claude exited with code $? during phase 1" - # Update marker even on failure to avoid re-processing same range - echo "$HEAD_SHA" > "$MARKER_FILE" - exit 1 - } - - if echo "$PHASE1_OUTPUT" | grep -q "^NO_CHANGES$"; then - log "AGENTS.md is up to date — no changes needed" - elif [ -n "$PHASE1_OUTPUT" ]; then - # Write updated AGENTS.md and create PR - TEMP_FILE=$(mktemp "${AGENTS_FILE}.XXXXXX") - printf '%s\n' "$PHASE1_OUTPUT" > "$TEMP_FILE" - mv "$TEMP_FILE" "$AGENTS_FILE" - - if ! git diff --quiet "$AGENTS_FILE" 2>/dev/null; then - branch_name="chore/planner-agents-$(date -u +%Y%m%d)" - git checkout -B "$branch_name" 2>/dev/null - git add "$AGENTS_FILE" - git commit -m "chore: planner update AGENTS.md" --quiet 2>/dev/null - git push -f origin "$branch_name" --quiet 2>/dev/null || { log "ERROR: failed to push $branch_name"; git checkout "${PRIMARY_BRANCH}" 2>/dev/null; } - git checkout "${PRIMARY_BRANCH}" 2>/dev/null - # Restore AGENTS.md to master version after branch push - git checkout "$AGENTS_FILE" 2>/dev/null || true - - # Create or update PR - EXISTING_PR=$(codeberg_api GET "/pulls?state=open&limit=50" 2>/dev/null | \ - jq -r --arg branch "$branch_name" '.[] | select(.head.ref == $branch) | .number' | head -1) - if [ -z "$EXISTING_PR" ]; then - PR_RESPONSE=$(codeberg_api POST "/pulls" \ - "{\"title\":\"chore: planner update AGENTS.md\",\"head\":\"${branch_name}\",\"base\":\"${PRIMARY_BRANCH}\",\"body\":\"Automated AGENTS.md update based on recent commits (${COMMIT_COUNT} changes since last review).\"}" \ - 2>/dev/null) - PR_NUM=$(echo "$PR_RESPONSE" | jq -r '.number // empty') - if [ -n "$PR_NUM" ]; then - log "Created PR #${PR_NUM} for AGENTS.md update" - matrix_send "planner" "📋 PR #${PR_NUM}: planner update AGENTS.md (${COMMIT_COUNT} commits reviewed)" 2>/dev/null || true - else - log "ERROR: failed to create PR" - fi + # Create or update PR + EXISTING_PR=$(codeberg_api GET "/pulls?state=open&limit=50" 2>/dev/null | \ + jq -r --arg branch "$BRANCH_NAME" '.[] | select(.head.ref == $branch) | .number' | head -1) + if [ -z "$EXISTING_PR" ]; then + PR_RESPONSE=$(codeberg_api POST "/pulls" \ + "$(jq -nc --arg h "$BRANCH_NAME" --arg b "$PRIMARY_BRANCH" \ + '{title:"chore: planner update AGENTS.md tree",head:$h,base:$b,body:"Automated AGENTS.md tree update from git history analysis."}')" \ + 2>/dev/null) + PR_NUM=$(echo "$PR_RESPONSE" | jq -r '.number // empty') + if [ -n "$PR_NUM" ]; then + log "Created PR #${PR_NUM} for AGENTS.md update" + matrix_send "planner" "📋 PR #${PR_NUM}: planner update AGENTS.md tree" 2>/dev/null || true else - log "Updated existing PR #${EXISTING_PR}" + log "ERROR: failed to create PR" fi else - log "AGENTS.md diff was empty after write — no PR needed" + log "Updated existing PR #${EXISTING_PR}" fi + else + git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true + log "No AGENTS.md changes after filtering" fi fi - # Update marker - echo "$HEAD_SHA" > "$MARKER_FILE" log "Phase 1 done" fi # ── Phase 2: Gap analysis ─────────────────────────────────────────────── log "Phase 2: gap analysis" -AGENTS_CONTENT=$(cat "$AGENTS_FILE" 2>/dev/null || true) +# Build project state from AGENTS.md tree + STATE.md +PROJECT_STATE="" +for f in $(find . -name "AGENTS.md" -not -path "./.git/*" | sort); do + PROJECT_STATE="${PROJECT_STATE} +### ${f} +$(cat "$f") +" +done +[ -f "$STATE_FILE" ] && PROJECT_STATE="${PROJECT_STATE} +### STATE.md +$(cat "$STATE_FILE") +" + VISION="" [ -f "$VISION_FILE" ] && VISION=$(cat "$VISION_FILE") @@ -178,27 +194,34 @@ fi OPEN_SUMMARY=$(echo "$OPEN_ISSUES" | jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"' 2>/dev/null || true) +# Fetch vision-labeled issues specifically +VISION_ISSUES=$(echo "$OPEN_ISSUES" | jq -r '.[] | select(.labels | map(.name) | index("vision")) | "#\(.number) \(.title)\n\(.body)"' 2>/dev/null || true) + PHASE2_PROMPT="You are the planner for ${CODEBERG_REPO}. Your job: find gaps between the project vision and current reality. ## VISION.md (human-maintained goals) ${VISION} -## AGENTS.md (current project state — the ground truth) -${AGENTS_CONTENT} +## Current project state (AGENTS.md tree + STATE.md) +${PROJECT_STATE} + +## Vision-labeled issues (goal anchors) +${VISION_ISSUES:-"(none)"} ## All open issues ${OPEN_SUMMARY} ## Task -Identify gaps — things implied by VISION.md that are neither reflected in AGENTS.md nor covered by an existing open issue. +Identify gaps — things implied by VISION.md that are neither reflected in the project state nor covered by an existing open issue. For each gap, output a JSON object (one per line, no array wrapper): {\"title\": \"action-oriented title\", \"body\": \"problem statement + why it matters + rough approach\", \"depends\": [list of blocking issue numbers or empty]} ## Rules - Max 5 new issues — focus on highest-leverage gaps only -- Do NOT create issues for things already described in AGENTS.md (already done) +- Do NOT create issues for things already documented in AGENTS.md or STATE.md - Do NOT create issues that overlap with ANY existing open issue, even partially +- Do NOT create issues about vision items, tech-debt, or in-progress work - Each title should be a plain, action-oriented sentence - Each body should explain: what's missing, why it matters for the vision, rough approach - Reference blocking issues by number in depends array @@ -209,7 +232,6 @@ Output ONLY the JSON lines (or NO_GAPS) — no preamble, no markdown fences." PHASE2_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PHASE2_PROMPT" \ --model sonnet \ - --dangerously-skip-permissions \ 2>/dev/null) || { log "ERROR: claude exited with code $? during phase 2" exit 1 @@ -222,18 +244,21 @@ if echo "$PHASE2_OUTPUT" | grep -q "NO_GAPS"; then fi # ── Create issues from gap analysis ────────────────────────────────────── +# Find backlog label ID BACKLOG_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null | \ jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true) CREATED=0 while IFS= read -r line; do [ -z "$line" ] && continue + # Skip non-JSON lines echo "$line" | jq -e . >/dev/null 2>&1 || continue TITLE=$(echo "$line" | jq -r '.title') BODY=$(echo "$line" | jq -r '.body') DEPS=$(echo "$line" | jq -r '.depends // [] | map("#\(.)") | join(", ")') + # Add dependency section if present if [ -n "$DEPS" ] && [ "$DEPS" != "" ]; then BODY="${BODY} @@ -241,7 +266,10 @@ while IFS= read -r line; do ${DEPS}" fi + # Create issue CREATE_PAYLOAD=$(jq -nc --arg t "$TITLE" --arg b "$BODY" '{title:$t, body:$b}') + + # Add label if we found the backlog label ID if [ -n "$BACKLOG_LABEL_ID" ]; then CREATE_PAYLOAD=$(echo "$CREATE_PAYLOAD" | jq --argjson lid "$BACKLOG_LABEL_ID" '.labels = [$lid]') fi @@ -249,7 +277,6 @@ ${DEPS}" RESULT=$(codeberg_api POST "/issues" -d "$CREATE_PAYLOAD" 2>/dev/null || true) ISSUE_NUM=$(echo "$RESULT" | jq -r '.number // "?"' 2>/dev/null || echo "?") log "Created #${ISSUE_NUM}: ${TITLE}" - matrix_send "planner" "📋 Gap issue #${ISSUE_NUM}: ${TITLE}" 2>/dev/null || true CREATED=$((CREATED + 1)) [ "$CREATED" -ge 5 ] && break