diff --git a/.gitignore b/.gitignore index c3464d8..5efe449 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ metrics/supervisor-metrics.jsonl .DS_Store dev/ci-fixes-*.json gardener/dust.jsonl + +# Planner persistent memory (local only) +planner/MEMORY.md diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index e304c30..cbb50e7 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -161,7 +161,6 @@ check_script gardener/gardener-agent.sh check_script gardener/gardener-poll.sh check_script review/review-pr.sh check_script review/review-poll.sh -check_script planner/planner-agent.sh check_script planner/planner-poll.sh check_script supervisor/supervisor-poll.sh check_script supervisor/update-prompt.sh diff --git a/AGENTS.md b/AGENTS.md index e843bde..56443c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ disinto/ ├── dev/ dev-poll.sh, dev-agent.sh, phase-handler.sh — issue implementation ├── review/ review-poll.sh, review-pr.sh — PR review ├── gardener/ gardener-poll.sh, gardener-agent.sh — backlog grooming -├── planner/ planner-poll.sh, planner-agent.sh — vision gap analysis +├── planner/ planner-poll.sh — files action issue for run-planner formula │ prediction-poll.sh, prediction-agent.sh — evidence-based predictions ├── supervisor/ supervisor-poll.sh — health monitoring ├── vault/ vault-poll.sh, vault-agent.sh, vault-fire.sh — action gating @@ -148,25 +148,33 @@ P3 (degraded PRs, circular deps, stale deps), P4 (housekeeping). ### Planner (`planner/`) -**Role**: Three-phase planning. Phase 1: update the AGENTS.md documentation -tree to reflect recent code changes. Phase 1.5: triage `prediction/unreviewed` -issues filed by the [Predictor](#predictor-planner) — accept as action/backlog -issues or dismiss as noise. Phase 2: gap-analyse VISION.md vs current project -state (including accepted predictions), create up to 5 backlog issues for the -highest-leverage gaps. +**Role**: Five-phase strategic planning, executed as an action formula. +Phase 0 (preflight): pull latest code, load persistent memory from +`planner/MEMORY.md`. Phase 1: update the AGENTS.md documentation tree to +reflect recent code changes (fast-track PR). Phase 1.5: triage +`prediction/unreviewed` issues filed by the [Predictor](#predictor-planner) — +accept as action/backlog issues or dismiss as noise. Phase 2: strategic planning +via resource+leverage gap analysis — reasons about VISION.md, RESOURCES.md, +formula catalog, and project state to create up to 5 backlog issues prioritized +by leverage. Phase 3: persist learnings to `planner/MEMORY.md`. -**Trigger**: `planner-poll.sh` runs weekly via cron. +**Trigger**: `planner-poll.sh` runs weekly via cron. It files an `action` +issue referencing `formulas/run-planner.toml`; the [action-agent](#action-action) +picks it up and executes the planning steps in an interactive Claude tmux session. **Key files**: -- `planner/planner-poll.sh` — Cron wrapper: lock, memory guard, runs planner-agent.sh -- `planner/planner-agent.sh` — Phase 1: uses `claude -p --model sonnet --max-turns 30` (one-shot with tool access) to read/update AGENTS.md files. Phase 1.5: fetches `prediction/unreviewed` issues and uses `claude -p --model sonnet` to triage each prediction (ACCEPT_ACTION, ACCEPT_BACKLOG, or DISMISS); creates corresponding action/backlog issues and relabels predictions to `prediction/backlog` or closes them. Phase 2: uses `claude -p --model sonnet` to compare AGENTS.md tree vs VISION.md (plus accepted predictions from Phase 1.5) and create gap issues. All phases are one-shot (`claude -p`), not interactive sessions +- `planner/planner-poll.sh` — Cron wrapper: memory guard, dedup check, files action issue +- `formulas/run-planner.toml` — Execution spec: five steps (preflight, agents-update, + prediction-triage, strategic-planning, memory-update) with `needs` dependencies. + Steps 2 and 3 are independent; step 4 depends on both. Claude executes all steps + in a single interactive session with tool access +- `planner/MEMORY.md` — Persistent memory across runs (gitignored, local only) **Future direction**: The [Predictor](#predictor-planner) already reads `evidence/` JSON and files prediction issues for the planner to triage. The next step is evidence-gated deployment (see `docs/EVIDENCE-ARCHITECTURE.md`): replacing human "ship it" decisions with automated gates across dimensions (holdout, red-team, user-test, evolution fitness, protocol metrics, funnel). Not yet implemented. -**Environment variables consumed**: +**Environment variables consumed** (by the action-agent session): - `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT` - `PRIMARY_BRANCH` -- `CLAUDE_TIMEOUT` - `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` ### Predictor (`planner/`) diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 0b1c91d..a9b83aa 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -105,7 +105,7 @@ This ensures dev-agent can't merge its own PRs — it must wait for review-agent ### Required: Seed the `AGENTS.md` tree -The planner-agent maintains an `AGENTS.md` tree — architecture docs with +The planner maintains an `AGENTS.md` tree — architecture docs with per-file `` watermarks. You must seed this before the first planner run, otherwise the planner sees no watermarks and treats the entire repo as "new", generating a noisy first-run diff. @@ -134,7 +134,7 @@ entire repo as "new", generating a noisy first-run diff. 5. Commit and push. The planner will now see 0 changes on its first run and only update files when real commits land. -See `planner/planner-agent.sh` for the full AGENTS.md conventions. +See `formulas/run-planner.toml` (agents-update step) for the full AGENTS.md conventions. ## 3. Write Good Issues diff --git a/README.md b/README.md index ece95ff..d10ef65 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ disinto/ │ └── best-practices.md # Gardener knowledge base ├── planner/ │ ├── planner-poll.sh # Cron entry: weekly vision gap analysis -│ └── planner-agent.sh # Updates AGENTS.md, creates backlog issues (claude -p) +│ └── (formula-driven) # run-planner.toml executed by action-agent ├── vault/ │ ├── vault-poll.sh # Cron entry: process pending dangerous actions │ ├── vault-agent.sh # Classifies and routes actions (claude -p) diff --git a/action/action-agent.sh b/action/action-agent.sh index 3c75f27..cca560b 100644 --- a/action/action-agent.sh +++ b/action/action-agent.sh @@ -76,6 +76,14 @@ fi log "Issue: ${ISSUE_TITLE}" +# --- Extract model from YAML front matter (if present) --- +YAML_MODEL=$(printf '%s' "$ISSUE_BODY" | \ + sed -n '/^---$/,/^---$/p' | grep '^model:' | awk '{print $2}' | tr -d '"' || true) +if [ -n "$YAML_MODEL" ]; then + export CLAUDE_MODEL="$YAML_MODEL" + log "model from front matter: ${YAML_MODEL}" +fi + # --- Fetch existing comments (resume context) --- COMMENTS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ "${CODEBERG_API}/issues/${ISSUE}/comments?limit=50") || true diff --git a/formulas/run-planner.toml b/formulas/run-planner.toml new file mode 100644 index 0000000..df29237 --- /dev/null +++ b/formulas/run-planner.toml @@ -0,0 +1,230 @@ +# formulas/run-planner.toml — Strategic planning formula +# +# Executed by the action-agent via cron-filed action issues. +# planner-poll.sh files an action issue referencing this formula weekly; +# action-poll.sh picks it up and spawns a tmux session where Claude +# executes these steps autonomously. + +name = "run-planner" +description = "Strategic planning: update docs, triage predictions, resource+leverage gap analysis" +version = 1 +model = "opus" + +[context] +files = ["VISION.md", "AGENTS.md", "RESOURCES.md"] + +[[steps]] +id = "preflight" +title = "Pull latest code and load planner memory" +description = """ +Set up the working environment for this planning run. + +1. Change to the project repository: + cd "$PROJECT_REPO_ROOT" + +2. Pull the latest code: + git fetch origin "$PRIMARY_BRANCH" --quiet + git checkout "$PRIMARY_BRANCH" --quiet + git pull --ff-only origin "$PRIMARY_BRANCH" --quiet + +3. Record the current HEAD SHA — you will need it for AGENTS.md watermarks: + HEAD_SHA=$(git rev-parse HEAD) + echo "$HEAD_SHA" > /tmp/planner-head-sha + +4. Read the planner memory file at: $FACTORY_ROOT/planner/MEMORY.md + If it does not exist, this is the first planning run. + Keep this memory context in mind for all subsequent steps. +""" + +[[steps]] +id = "agents-update" +title = "Update AGENTS.md documentation tree" +description = """ +Check all AGENTS.md files for staleness and update any that are outdated. + +1. Read the HEAD SHA from preflight: + HEAD_SHA=$(cat /tmp/planner-head-sha) + +2. Find all AGENTS.md files: + find "$PROJECT_REPO_ROOT" -name "AGENTS.md" -not -path "*/.git/*" + +3. For each file, read the watermark from line 1: + + +4. Check for changes since the watermark: + git log --oneline ..HEAD -- + If zero changes, the file is current — skip it. + +5. For stale files: + - Read the AGENTS.md and the source files in that directory + - Update the documentation to reflect code changes since the watermark + - Set the watermark to the HEAD SHA from the preflight step + - Conventions: max ~200 lines, architecture and WHY not implementation details + +6. If you made changes: + a. Create a branch: + git checkout -B "chore/planner-agents-$(date -u +%Y%m%d)" + b. Stage only AGENTS.md files: + find . -name "AGENTS.md" -not -path "./.git/*" -exec git add {} + + c. Commit: + git commit -m "chore: planner update AGENTS.md tree" + d. Push: + git push -f origin "chore/planner-agents-$(date -u +%Y%m%d)" + e. Create a PR (failure here is non-fatal — log and continue): + curl -sf -X POST \ + -H "Authorization: token $CODEBERG_TOKEN" \ + -H "Content-Type: application/json" \ + "$CODEBERG_API/pulls" \ + -d '{"title":"chore: planner update AGENTS.md tree", + "head":"","base":"", + "body":"Automated AGENTS.md update — review-agent fast-tracks doc-only PRs."}' + f. Return to primary branch: + git checkout "$PRIMARY_BRANCH" + +7. If no AGENTS.md files need updating, skip this step entirely. + +CRITICAL: If this step fails for any reason, log the failure and move on. +Do NOT let an AGENTS.md failure prevent prediction triage or strategic planning. +""" +needs = ["preflight"] + +[[steps]] +id = "prediction-triage" +title = "Triage prediction/unreviewed issues" +description = """ +Triage prediction issues filed by the predictor (goblin). + +1. Fetch unreviewed predictions: + curl -sf -H "Authorization: token $CODEBERG_TOKEN" \ + "$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50" + + If there are none, note that and proceed to strategic-planning. + +2. Read available formulas from $FACTORY_ROOT/formulas/*.toml so you know + what actions can be dispatched. + +3. Fetch all open issues to check for overlap: + curl -sf -H "Authorization: token $CODEBERG_TOKEN" \ + "$CODEBERG_API/issues?state=open&type=issues&limit=50" + +4. For each prediction, read the title and body. Decide: + - ACCEPT_ACTION: maps to an available formula -> create an action issue + with YAML front matter referencing the formula name and vars + - ACCEPT_BACKLOG: warrants dev work -> create a backlog issue + - DISMISS: noise, already covered by an open issue, or not actionable -> + post an explanation comment, then close the prediction issue + +5. For each accepted prediction: + - Create the new issue with the 'backlog' label (or 'action' label for + formula-matching actions) + - Remove 'prediction/unreviewed' label from the original prediction + - Add 'prediction/backlog' label to the original prediction + - Note what you accepted — you will need it for strategic-planning + +6. Validation: if you reference a formula, verify it exists on disk. + Fall back to a freeform backlog issue for unknown formulas. + +Be decisive — the predictor intentionally over-signals; your job is to filter. + +CRITICAL: If this step fails, log the failure and move on to strategic-planning. +""" +needs = ["preflight"] + +[[steps]] +id = "strategic-planning" +title = "Strategic planning — resource+leverage gap analysis" +description = """ +This is the core planning step. Reason about leverage and create +the highest-impact issues. + +Read these inputs: + - VISION.md — where we want to be + - All AGENTS.md files — what exists today + - $FACTORY_ROOT/RESOURCES.md — what we have (may not exist) + - $FACTORY_ROOT/formulas/*.toml — what actions can be dispatched + - Open issues (fetched via API) — what's already planned + - $FACTORY_ROOT/metrics/supervisor-metrics.jsonl — operational trends (may not exist) + - Planner memory (loaded in preflight) + - Accepted predictions from the triage step + +Reason through these five questions: + +1. **What resources do you need that you don't have?** + Analytics, domains, accounts, compute, integrations — things required + by the vision that aren't in RESOURCES.md or aren't set up yet. + +2. **What resources are underutilized?** + Compute capacity idle most of the day. Domains with no traffic. + CI capacity unused at night. Accounts not being leveraged. + +3. **What's the highest-leverage action?** + The one thing that unblocks the most progress toward the vision. + Can you dispatch a formula for it? + +4. **What task gaps remain?** + Things in VISION.md not covered by open issues or the current + project state. + +5. **What should be deferred?** + Things that depend on blocked resources or aren't high-leverage + right now. Do NOT create issues for these. + +Then create up to 5 issues, prioritized by leverage: + +For formula-matching gaps, include YAML front matter in the body: + --- + formula: + vars: + key: "value" + --- + + +For freeform gaps: + + +Create each issue via the API with the 'backlog' label: + curl -sf -X POST \ + -H "Authorization: token $CODEBERG_TOKEN" \ + -H "Content-Type: application/json" \ + "$CODEBERG_API/issues" \ + -d '{"title":"...","body":"...","labels":[]}' + +Rules: +- Max 5 new issues — highest leverage first +- Do NOT create issues that overlap with ANY existing open issue +- Do NOT create issues for items you identified as "deferred" +- Each body: what's missing, why it matters, rough approach +- When deploying/operating, reference the resource alias from RESOURCES.md +- Add ## Depends on section for issues that depend on other open issues +- Only reference formulas that exist in formulas/*.toml +- When metrics show systemic problems, create optimization issues + +If there are no gaps, note that the backlog is aligned with the vision. +""" +needs = ["agents-update", "prediction-triage"] + +[[steps]] +id = "memory-update" +title = "Persist learnings to planner/MEMORY.md" +description = """ +Reflect on this planning run and write the updated memory file. + +Write to: $FACTORY_ROOT/planner/MEMORY.md (replace the entire file) + +Include: +- Date of this run +- What was observed (resource state, metric trends, project progress) +- What was decided (issues created, predictions triaged, what was deferred) +- Patterns and learnings useful for future planning runs +- Things to watch for next time + +Rules: +- Keep under 100 lines total +- Replace the file contents — prune outdated entries from previous runs +- Focus on PATTERNS and LEARNINGS, not transient state +- Do NOT include specific issue counts or numbers that will be stale +- Most recent entries at top + +Format: simple markdown with dated sections. +""" +needs = ["strategic-planning"] diff --git a/lib/agent-session.sh b/lib/agent-session.sh index 2377f7e..81d6aae 100644 --- a/lib/agent-session.sh +++ b/lib/agent-session.sh @@ -245,8 +245,12 @@ create_agent_session() { fi rm -f "$idle_marker" + local model_flag="" + if [ -n "${CLAUDE_MODEL:-}" ]; then + model_flag="--model ${CLAUDE_MODEL}" + fi tmux new-session -d -s "$session" -c "$workdir" \ - "claude --dangerously-skip-permissions" 2>/dev/null + "claude --dangerously-skip-permissions ${model_flag}" 2>/dev/null sleep 1 tmux has-session -t "$session" 2>/dev/null || return 1 agent_wait_for_claude_ready "$session" 120 || return 1 diff --git a/planner/planner-agent.sh b/planner/planner-agent.sh deleted file mode 100755 index 992c3c1..0000000 --- a/planner/planner-agent.sh +++ /dev/null @@ -1,622 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# planner-agent.sh — Update AGENTS.md tree, triage predictions, gap-analyse -# -# Three-phase planner run: -# Phase 1: Navigate and update AGENTS.md tree using Claude with tool access -# Phase 1.5: Triage prediction/unreviewed issues from the predictor (goblin) -# Phase 2: Compare AGENTS.md vs VISION.md, create backlog issues for gaps -# -# Usage: planner-agent.sh (no args — uses env vars from .env / env.sh) -# ============================================================================= -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" - -# shellcheck source=../lib/env.sh -source "$FACTORY_ROOT/lib/env.sh" - -LOG_FILE="$SCRIPT_DIR/planner.log" -CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-3600}" -VISION_FILE="${PROJECT_REPO_ROOT}/VISION.md" -RESOURCES_FILE="${FACTORY_ROOT}/RESOURCES.md" - -log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; } - -# ── Preflight ──────────────────────────────────────────────────────────── -cd "$PROJECT_REPO_ROOT" -git fetch origin "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true -git checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true -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}) ---" - -# ── 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 - -## Current AGENTS.md files -$(echo -e "$AGENTS_INFO") -## Current HEAD: ${HEAD_SHA} - -## 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. - -## 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 - # Commit and push - find . -name "AGENTS.md" -not -path "./.git/*" -exec git add {} + - - 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 - - # 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 "ERROR: failed to create PR" - fi - else - 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 - - log "Phase 1 done" -fi - -# ── Phase 1.5: Prediction Triage ────────────────────────────────────── -log "Phase 1.5: prediction triage" - -PRED_ISSUES=$(codeberg_api GET "/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50" 2>/dev/null || true) -PRED_COUNT=0 -if [ -n "$PRED_ISSUES" ] && [ "$PRED_ISSUES" != "null" ]; then - PRED_COUNT=$(printf '%s' "$PRED_ISSUES" | jq 'length' 2>/dev/null || echo 0) -fi - -ACCEPTED_PREDICTIONS="" - -if [ "${PRED_COUNT:-0}" -gt 0 ] 2>/dev/null; then - log "Found $PRED_COUNT prediction/unreviewed issues to triage" - - # Build prediction details for the prompt - PRED_DETAILS=$(printf '%s' "$PRED_ISSUES" | jq -r \ - '.[] | "### Prediction #\(.number): \(.title)\n\(.body)\n"' 2>/dev/null || true) - - # Look up label IDs - ALL_LABELS=$(codeberg_api GET "/labels" 2>/dev/null || true) - UNREVIEWED_LABEL_ID=$(printf '%s' "$ALL_LABELS" | \ - jq -r '.[] | select(.name == "prediction/unreviewed") | .id' 2>/dev/null || true) - BACKLOG_PRED_LABEL_ID=$(printf '%s' "$ALL_LABELS" | \ - jq -r '.[] | select(.name == "prediction/backlog") | .id' 2>/dev/null || true) - BACKLOG_LABEL_ID_TRIAGE=$(printf '%s' "$ALL_LABELS" | \ - jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true) - - # Create prediction/backlog label if missing - if [ -z "$BACKLOG_PRED_LABEL_ID" ]; then - LABEL_RESULT=$(codeberg_api POST "/labels" \ - -d "$(jq -nc '{name:"prediction/backlog", color:"#0e8a16", description:"Triaged prediction — accepted by planner"}')" \ - 2>/dev/null || true) - BACKLOG_PRED_LABEL_ID=$(printf '%s' "$LABEL_RESULT" | jq -r '.id // empty' 2>/dev/null || true) - if [ -n "$BACKLOG_PRED_LABEL_ID" ]; then - log "Created 'prediction/backlog' label (id: $BACKLOG_PRED_LABEL_ID)" - else - log "WARN: failed to create 'prediction/backlog' label" - fi - fi - - # Build formula catalog for triage prompt - TRIAGE_FORMULA_CATALOG="" - if [ -d "$FACTORY_ROOT/formulas" ]; then - for _tf in "$FACTORY_ROOT/formulas"/*.toml; do - [ -f "$_tf" ] || continue - TRIAGE_FORMULA_CATALOG="${TRIAGE_FORMULA_CATALOG} ---- $(basename "$_tf" .toml) --- -$(cat "$_tf") -" - done - fi - - # Fetch open issues summary for overlap check - _TRIAGE_ISSUES=$(codeberg_api GET "/issues?state=open&type=issues&limit=50&sort=updated&direction=desc" 2>/dev/null || true) - TRIAGE_OPEN_SUMMARY="" - if [ -n "$_TRIAGE_ISSUES" ] && [ "$_TRIAGE_ISSUES" != "null" ]; then - TRIAGE_OPEN_SUMMARY=$(printf '%s' "$_TRIAGE_ISSUES" | \ - jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"' 2>/dev/null || true) - fi - - # Load VISION for context - TRIAGE_VISION="" - [ -f "$VISION_FILE" ] && TRIAGE_VISION=$(cat "$VISION_FILE") - - TRIAGE_PROMPT="You are the planner for ${CODEBERG_REPO}. The predictor has filed these observations: - -${PRED_DETAILS} - -For each prediction, decide: -- ACCEPT_ACTION: maps to a formula → emit action issue JSON -- ACCEPT_BACKLOG: warrants dev work → emit backlog issue JSON -- DISMISS: noise, already covered, or not actionable → close with reason - -Context for your decisions: - -## VISION.md -${TRIAGE_VISION:-"(not found)"} - -## Available formulas -${TRIAGE_FORMULA_CATALOG:-"(no formulas available)"} - -## All open issues (check for overlap) -${TRIAGE_OPEN_SUMMARY:-"(could not fetch)"} - -## Output format - -For each prediction, output one JSON object per line (no array wrapper, no markdown fences): - -{\"prediction\": , \"decision\": \"ACCEPT_ACTION\", \"title\": \"action title\", \"formula\": \"formula-name\", \"vars\": {\"var1\": \"value1\"}, \"reason\": \"why\"} -{\"prediction\": , \"decision\": \"ACCEPT_BACKLOG\", \"title\": \"backlog issue title\", \"body\": \"problem + approach\", \"reason\": \"why\"} -{\"prediction\": , \"decision\": \"DISMISS\", \"reason\": \"why this is noise or already covered\"} - -## Rules -- Triage ALL predictions — every prediction must appear in output -- Only use ACCEPT_ACTION when the prediction clearly maps to an available formula -- ACCEPT_BACKLOG for predictions that warrant real dev work -- DISMISS predictions that are noise, already covered by open issues, or not actionable -- Be decisive — the predictor intentionally over-signals; your job is to filter - -Output ONLY the JSON lines — no preamble, no markdown fences." - - TRIAGE_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$TRIAGE_PROMPT" \ - --model sonnet \ - 2>/dev/null) || { - log "ERROR: claude exited with code $? during phase 1.5 — skipping triage" - TRIAGE_OUTPUT="" - } - - if [ -n "$TRIAGE_OUTPUT" ]; then - log "Phase 1.5 claude finished ($(printf '%s' "$TRIAGE_OUTPUT" | wc -c) bytes)" - - # Build valid formula list for validation - TRIAGE_VALID_FORMULAS="" - if [ -d "$FACTORY_ROOT/formulas" ]; then - for _vf in "$FACTORY_ROOT/formulas"/*.toml; do - [ -f "$_vf" ] || continue - TRIAGE_VALID_FORMULAS="${TRIAGE_VALID_FORMULAS} $(basename "$_vf" .toml)" - done - fi - - TRIAGE_ACCEPTED=0 - TRIAGE_DISMISSED=0 - while IFS= read -r line; do - [ -z "$line" ] && continue - printf '%s' "$line" | jq -e . >/dev/null 2>&1 || continue - - PRED_NUM=$(printf '%s' "$line" | jq -r '.prediction') - DECISION=$(printf '%s' "$line" | jq -r '.decision') - REASON=$(printf '%s' "$line" | jq -r '.reason // ""') - - case "$DECISION" in - ACCEPT_ACTION) - A_TITLE=$(printf '%s' "$line" | jq -r '.title') - A_FORMULA=$(printf '%s' "$line" | jq -r '.formula // empty') - - # Validate formula against on-disk catalog - if [ -n "$A_FORMULA" ] && ! echo "$TRIAGE_VALID_FORMULAS" | grep -qw "$A_FORMULA"; then - log "WARN: unknown formula '${A_FORMULA}' for prediction #${PRED_NUM} — falling back to ACCEPT_BACKLOG" - A_FORMULA="" - fi - - if [ -n "$A_FORMULA" ]; then - A_VARS_YAML=$(printf '%s' "$line" | jq -r ' - .vars // {} | to_entries | - map(" " + .key + ": " + (.value | @json)) | join("\n")') - - A_BODY="--- -formula: ${A_FORMULA} -vars: -${A_VARS_YAML} ---- - -Triaged from prediction #${PRED_NUM}. -Reason: ${REASON}" - else - A_BODY="Triaged from prediction #${PRED_NUM}. -Reason: ${REASON}" - fi - - A_PAYLOAD=$(jq -nc --arg t "$A_TITLE" --arg b "$A_BODY" '{title:$t, body:$b}') - if [ -n "$BACKLOG_LABEL_ID_TRIAGE" ]; then - A_PAYLOAD=$(printf '%s' "$A_PAYLOAD" | jq --argjson lid "$BACKLOG_LABEL_ID_TRIAGE" '.labels = [$lid]') - fi - - A_RESULT=$(codeberg_api POST "/issues" -d "$A_PAYLOAD" 2>/dev/null || true) - A_ISSUE=$(printf '%s' "$A_RESULT" | jq -r '.number // "?"' 2>/dev/null || echo "?") - log "Created action issue #${A_ISSUE} from prediction #${PRED_NUM} (formula: ${A_FORMULA:-freeform})" - matrix_send "planner" "📋 Action #${A_ISSUE} from prediction #${PRED_NUM} [${A_FORMULA:-freeform}]: ${A_TITLE}" 2>/dev/null || true - - ACCEPTED_PREDICTIONS="${ACCEPTED_PREDICTIONS} -- Prediction #${PRED_NUM} → action issue #${A_ISSUE} (formula: ${A_FORMULA:-freeform}): ${A_TITLE}" - TRIAGE_ACCEPTED=$((TRIAGE_ACCEPTED + 1)) - - # Relabel: prediction/unreviewed → prediction/backlog - if [ -n "$UNREVIEWED_LABEL_ID" ]; then - codeberg_api DELETE "/issues/${PRED_NUM}/labels/${UNREVIEWED_LABEL_ID}" 2>/dev/null || true - fi - if [ -n "$BACKLOG_PRED_LABEL_ID" ]; then - codeberg_api POST "/issues/${PRED_NUM}/labels" \ - -d "$(jq -nc --argjson lid "$BACKLOG_PRED_LABEL_ID" '{labels:[$lid]}')" 2>/dev/null || true - fi - ;; - - ACCEPT_BACKLOG) - B_TITLE=$(printf '%s' "$line" | jq -r '.title') - B_BODY=$(printf '%s' "$line" | jq -r '.body // ""') - B_BODY="${B_BODY} - -Triaged from prediction #${PRED_NUM}. -Reason: ${REASON}" - - B_PAYLOAD=$(jq -nc --arg t "$B_TITLE" --arg b "$B_BODY" '{title:$t, body:$b}') - if [ -n "$BACKLOG_LABEL_ID_TRIAGE" ]; then - B_PAYLOAD=$(printf '%s' "$B_PAYLOAD" | jq --argjson lid "$BACKLOG_LABEL_ID_TRIAGE" '.labels = [$lid]') - fi - - B_RESULT=$(codeberg_api POST "/issues" -d "$B_PAYLOAD" 2>/dev/null || true) - B_ISSUE=$(printf '%s' "$B_RESULT" | jq -r '.number // "?"' 2>/dev/null || echo "?") - log "Created backlog issue #${B_ISSUE} from prediction #${PRED_NUM}" - matrix_send "planner" "📋 Backlog #${B_ISSUE} from prediction #${PRED_NUM}: ${B_TITLE}" 2>/dev/null || true - - ACCEPTED_PREDICTIONS="${ACCEPTED_PREDICTIONS} -- Prediction #${PRED_NUM} → backlog issue #${B_ISSUE}: ${B_TITLE}" - TRIAGE_ACCEPTED=$((TRIAGE_ACCEPTED + 1)) - - # Relabel: prediction/unreviewed → prediction/backlog - if [ -n "$UNREVIEWED_LABEL_ID" ]; then - codeberg_api DELETE "/issues/${PRED_NUM}/labels/${UNREVIEWED_LABEL_ID}" 2>/dev/null || true - fi - if [ -n "$BACKLOG_PRED_LABEL_ID" ]; then - codeberg_api POST "/issues/${PRED_NUM}/labels" \ - -d "$(jq -nc --argjson lid "$BACKLOG_PRED_LABEL_ID" '{labels:[$lid]}')" 2>/dev/null || true - fi - ;; - - DISMISS) - codeberg_api POST "/issues/${PRED_NUM}/comments" \ - -d "$(jq -nc --arg b "Dismissed by planner triage: ${REASON}" '{body:$b}')" 2>/dev/null || true - codeberg_api PATCH "/issues/${PRED_NUM}" \ - -d '{"state":"closed"}' 2>/dev/null || true - log "Dismissed prediction #${PRED_NUM}: ${REASON}" - TRIAGE_DISMISSED=$((TRIAGE_DISMISSED + 1)) - ;; - - *) - log "WARN: unknown triage decision '${DECISION}' for prediction #${PRED_NUM}" - ;; - esac - done <<< "$TRIAGE_OUTPUT" - - log "Phase 1.5 done — accepted: $TRIAGE_ACCEPTED, dismissed: $TRIAGE_DISMISSED" - fi -else - log "No prediction/unreviewed issues found — skipping triage" -fi - -# ── Phase 2: Gap analysis ─────────────────────────────────────────────── -log "Phase 2: gap analysis" - -# Build project state from AGENTS.md tree -PROJECT_STATE="" -for f in $(find . -name "AGENTS.md" -not -path "./.git/*" | sort); do - PROJECT_STATE="${PROJECT_STATE} -### ${f} -$(cat "$f") -" -done - -VISION="" -[ -f "$VISION_FILE" ] && VISION=$(cat "$VISION_FILE") - -if [ -z "$VISION" ]; then - log "No VISION.md found — skipping gap analysis" - log "--- Planner done ---" - exit 0 -fi - -RESOURCES="" -[ -f "$RESOURCES_FILE" ] && RESOURCES=$(cat "$RESOURCES_FILE") - -# Fetch open issues (all labels) -OPEN_ISSUES=$(codeberg_api GET "/issues?state=open&type=issues&limit=50&sort=updated&direction=desc" 2>/dev/null || true) -if [ -z "$OPEN_ISSUES" ] || [ "$OPEN_ISSUES" = "null" ]; then - log "Failed to fetch open issues" - exit 1 -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) - -# Read supervisor metrics for trend analysis (last 7 days) -METRICS_FILE="${FACTORY_ROOT}/metrics/supervisor-metrics.jsonl" -METRICS_SUMMARY="(no metrics data — supervisor has not yet written metrics)" -if [ -f "$METRICS_FILE" ] && [ -s "$METRICS_FILE" ]; then - _METRICS_CUTOFF=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M) - METRICS_SUMMARY=$(jq -c --arg cutoff "$_METRICS_CUTOFF" 'select(.ts >= $cutoff)' \ - "$METRICS_FILE" 2>/dev/null | \ - jq -rs --arg proj "${PROJECT_NAME:-}" ' - ( [.[] | select(.type=="ci" and .project==$proj) | .duration_min] | if length>0 then add/length|round else null end ) as $ci_avg | - ( [.[] | select(.type=="ci" and .project==$proj) | select(.status=="success")] | length ) as $ci_ok | - ( [.[] | select(.type=="ci" and .project==$proj)] | length ) as $ci_n | - ( [.[] | select(.type=="infra") | .ram_used_pct] | if length>0 then add/length|round else null end ) as $ram_avg | - ( [.[] | select(.type=="infra") | .disk_used_pct] | if length>0 then add/length|round else null end ) as $disk_avg | - ( [.[] | select(.type=="dev" and .project==$proj)] | last ) as $dev_last | - "CI (\($ci_n) pipelines): avg \(if $ci_avg then "\($ci_avg)min" else "n/a" end), success rate \(if $ci_n > 0 then "\($ci_ok * 100 / $ci_n | round)%" else "n/a" end)\n" + - "Infra: avg RAM \(if $ram_avg then "\($ram_avg)%" else "n/a" end) used, avg disk \(if $disk_avg then "\($disk_avg)%" else "n/a" end) used\n" + - "Dev (latest): \(if $dev_last then "\($dev_last.issues_in_backlog) in backlog, \($dev_last.issues_blocked) blocked (\(if $dev_last.issues_in_backlog > 0 then $dev_last.issues_blocked * 100 / $dev_last.issues_in_backlog | round else 0 end)% blocked), \($dev_last.pr_open) open PRs" else "n/a" end) - ' 2>/dev/null) || METRICS_SUMMARY="(metrics parse error)" - log "Metrics: ${METRICS_SUMMARY:0:120}" -fi - -# ── Build formula catalog ────────────────────────────────────────────── -FORMULA_DIR="$FACTORY_ROOT/formulas" -FORMULA_CATALOG="" -if [ -d "$FORMULA_DIR" ]; then - for formula_file in "$FORMULA_DIR"/*.toml; do - [ -f "$formula_file" ] || continue - formula_name=$(basename "$formula_file" .toml) - FORMULA_CATALOG="${FORMULA_CATALOG} ---- ${formula_name} --- -$(cat "$formula_file") -" - done -fi - -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} - -## Current project state (AGENTS.md tree) -${PROJECT_STATE} - -## RESOURCES.md (shared factory infrastructure) -${RESOURCES:-"(not found — copy RESOURCES.example.md to RESOURCES.md and fill in your infrastructure)"} - -## Vision-labeled issues (goal anchors) -${VISION_ISSUES:-"(none)"} - -## All open issues -${OPEN_SUMMARY} - -## Recently accepted predictions (from triage) -${ACCEPTED_PREDICTIONS:-"(none — no predictions were triaged this cycle)"} - -## Operational metrics (last 7 days from supervisor) -${METRICS_SUMMARY} - -## Available formulas (from formulas/ directory) -When a gap maps directly to one of these formula types, emit a formula instance -instead of a freeform issue. Only use a formula when the gap clearly fits — -do not force-fit gaps into formulas. Fill in the formula vars with concrete values. - -${FORMULA_CATALOG:-"(no formulas available)"} - -## Task -Identify gaps — things implied by VISION.md that are neither reflected in the project state nor covered by an existing open issue. -When a gap involves deploying, hosting, or operating a service, reference the specific resource alias from RESOURCES.md (e.g. \"deploy to \") so issues are actionable. - -For each gap, output a JSON object (one per line, no array wrapper). - -If a gap matches an available formula, emit a formula instance: -{\"title\": \"action-oriented title\", \"formula\": \"\", \"vars\": {\"var1\": \"value1\", ...}, \"body\": \"why this matters\", \"depends\": [...]} - -Otherwise, emit a freeform issue: -{\"title\": \"action-oriented title\", \"body\": \"problem statement + why it matters + rough approach\", \"depends\": [...]} - -## Rules -- Max 5 new issues — focus on highest-leverage gaps only -- Do NOT create issues for things already documented in AGENTS.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 -- Prefer formula instances when a gap clearly fits a known formula — but freeform is fine when none matches -- When metrics indicate a systemic problem conflicting with VISION.md (slow CI, high blocked ratio, disk pressure), create an optimization issue even if not explicitly in VISION.md - -If there are no gaps, output exactly: NO_GAPS - -Output ONLY the JSON lines (or NO_GAPS) — no preamble, no markdown fences." - -PHASE2_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PHASE2_PROMPT" \ - --model sonnet \ - 2>/dev/null) || { - log "ERROR: claude exited with code $? during phase 2" - exit 1 -} - -if echo "$PHASE2_OUTPUT" | grep -q "NO_GAPS"; then - log "No gaps found — backlog is aligned with vision" - log "--- Planner done ---" - exit 0 -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) - -# Build list of valid formula names from disk for validation -VALID_FORMULAS="" -if [ -d "$FORMULA_DIR" ]; then - for _vf in "$FORMULA_DIR"/*.toml; do - [ -f "$_vf" ] || continue - VALID_FORMULAS="${VALID_FORMULAS} $(basename "$_vf" .toml)" - done -fi - -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') - DEPS=$(echo "$line" | jq -r '.depends // [] | map("#\(.)") | join(", ")') - - # Check if this is a formula instance and validate against on-disk catalog - FORMULA_NAME=$(echo "$line" | jq -r '.formula // empty') - if [ -n "$FORMULA_NAME" ]; then - if ! echo "$VALID_FORMULAS" | grep -qw "$FORMULA_NAME"; then - log "WARN: Claude emitted unknown formula '${FORMULA_NAME}' — falling back to freeform" - FORMULA_NAME="" - fi - fi - - if [ -n "$FORMULA_NAME" ]; then - # Build YAML front matter for formula instance (values quoted for safety) - # Note: formula label is NOT applied — dev-agent does not yet support - # formula dispatch, so adding the label would block the dev pipeline. - # The YAML front matter is enough structure for future formula dispatch. - VARS_YAML=$(echo "$line" | jq -r ' - .vars // {} | to_entries | - map(" " + .key + ": " + (.value | @json)) | join("\n")') - EXTRA_BODY=$(echo "$line" | jq -r '.body // ""') - BODY="--- -formula: ${FORMULA_NAME} -vars: -${VARS_YAML} ---- - -${EXTRA_BODY}" - else - BODY=$(echo "$line" | jq -r '.body') - fi - - # Add dependency section if present - if [ -n "$DEPS" ] && [ "$DEPS" != "" ]; then - BODY="${BODY} - -## Depends on -${DEPS}" - fi - - # Create issue (backlog label only — no formula label until dev-agent supports dispatch) - CREATE_PAYLOAD=$(jq -nc --arg t "$TITLE" --arg b "$BODY" '{title:$t, body:$b}') - - if [ -n "$BACKLOG_LABEL_ID" ]; then - CREATE_PAYLOAD=$(echo "$CREATE_PAYLOAD" | jq --argjson lid "$BACKLOG_LABEL_ID" '.labels = [$lid]') - fi - - RESULT=$(codeberg_api POST "/issues" -d "$CREATE_PAYLOAD" 2>/dev/null || true) - ISSUE_NUM=$(echo "$RESULT" | jq -r '.number // "?"' 2>/dev/null || echo "?") - - if [ -n "$FORMULA_NAME" ]; then - log "Created #${ISSUE_NUM} (formula:${FORMULA_NAME}): ${TITLE}" - matrix_send "planner" "📋 Formula issue #${ISSUE_NUM} [${FORMULA_NAME}]: ${TITLE}" 2>/dev/null || true - else - log "Created #${ISSUE_NUM}: ${TITLE}" - matrix_send "planner" "📋 Gap issue #${ISSUE_NUM}: ${TITLE}" 2>/dev/null || true - fi - CREATED=$((CREATED + 1)) - - [ "$CREATED" -ge 5 ] && break -done <<< "$PHASE2_OUTPUT" - -log "Phase 2 done — created $CREATED issues" -log "--- Planner done ---" diff --git a/planner/planner-poll.sh b/planner/planner-poll.sh index 4ec7a5c..6bd0f44 100755 --- a/planner/planner-poll.sh +++ b/planner/planner-poll.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash # ============================================================================= -# planner-poll.sh — Cron wrapper for planner-agent +# planner-poll.sh — Cron wrapper: files action issue for run-planner formula # # Runs weekly (or on-demand). Guards against concurrent runs and low memory. +# Files an action issue referencing formulas/run-planner.toml; the action-agent +# picks it up and executes the planning steps in an interactive Claude session. # ============================================================================= set -euo pipefail @@ -38,14 +40,54 @@ fi log "--- Planner poll start ---" -# ── Run planner agent ───────────────────────────────────────────────────── -"$SCRIPT_DIR/planner-agent.sh" 2>&1 | while IFS= read -r line; do - log " $line" -done - -EXIT_CODE=${PIPESTATUS[0]} -if [ "$EXIT_CODE" -ne 0 ]; then - log "poll: planner-agent exited with code $EXIT_CODE" +# ── Dedup: skip if an open run-planner action issue already exists ──────── +OPEN_ACTIONS=$(codeberg_api GET "/issues?state=open&type=issues&labels=action&limit=50" 2>/dev/null || true) +if [ -n "$OPEN_ACTIONS" ] && [ "$OPEN_ACTIONS" != "null" ]; then + EXISTING=$(printf '%s' "$OPEN_ACTIONS" | \ + jq '[.[] | select(.title | test("run-planner"))] | length' 2>/dev/null || echo 0) + if [ "${EXISTING:-0}" -gt 0 ]; then + log "poll: open run-planner action issue already exists — skipping" + log "--- Planner poll done ---" + exit 0 + fi fi +# ── Fetch 'action' label ID ────────────────────────────────────────────── +ACTION_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null | \ + jq -r '.[] | select(.name == "action") | .id' 2>/dev/null || true) + +if [ -z "$ACTION_LABEL_ID" ]; then + log "ERROR: 'action' label not found — cannot file planner issue" + exit 1 +fi + +# ── File action issue ───────────────────────────────────────────────────── +ISSUE_BODY="--- +formula: run-planner +model: opus +--- + +Periodic strategic planning run. The action-agent reads \`formulas/run-planner.toml\` +and executes the five phases: preflight, AGENTS.md update, prediction triage, +strategic planning (resource+leverage gap analysis), and memory update. + +Filed automatically by \`planner-poll.sh\`." + +PAYLOAD=$(jq -nc \ + --arg title "action: run-planner — periodic strategic planning" \ + --arg body "$ISSUE_BODY" \ + --argjson labels "[$ACTION_LABEL_ID]" \ + '{title: $title, body: $body, labels: $labels}') + +RESULT=$(codeberg_api POST "/issues" -d "$PAYLOAD" 2>/dev/null || true) +ISSUE_NUM=$(printf '%s' "$RESULT" | jq -r '.number // empty' 2>/dev/null || true) + +if [ -z "$ISSUE_NUM" ]; then + log "ERROR: failed to create action issue for run-planner" + exit 1 +fi + +log "Filed action issue #${ISSUE_NUM} for run-planner formula" +matrix_send "planner" "Filed action #${ISSUE_NUM}: run-planner — periodic strategic planning" 2>/dev/null || true + log "--- Planner poll done ---"