fix: feat: planner as cron-driven formula (no issue tracking) (#232)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-20 09:00:56 +00:00
parent b4dce50f2c
commit 6c7557e87b
8 changed files with 305 additions and 647 deletions

3
.gitignore vendored
View file

@ -18,3 +18,6 @@ metrics/supervisor-metrics.jsonl
.DS_Store .DS_Store
dev/ci-fixes-*.json dev/ci-fixes-*.json
gardener/dust.jsonl gardener/dust.jsonl
# Planner persistent memory (local only)
planner/MEMORY.md

View file

@ -161,7 +161,6 @@ check_script gardener/gardener-agent.sh
check_script gardener/gardener-poll.sh check_script gardener/gardener-poll.sh
check_script review/review-pr.sh check_script review/review-pr.sh
check_script review/review-poll.sh check_script review/review-poll.sh
check_script planner/planner-agent.sh
check_script planner/planner-poll.sh check_script planner/planner-poll.sh
check_script supervisor/supervisor-poll.sh check_script supervisor/supervisor-poll.sh
check_script supervisor/update-prompt.sh check_script supervisor/update-prompt.sh

View file

@ -17,7 +17,7 @@ disinto/
├── dev/ dev-poll.sh, dev-agent.sh, phase-handler.sh — issue implementation ├── dev/ dev-poll.sh, dev-agent.sh, phase-handler.sh — issue implementation
├── review/ review-poll.sh, review-pr.sh — PR review ├── review/ review-poll.sh, review-pr.sh — PR review
├── gardener/ gardener-poll.sh, gardener-agent.sh — backlog grooming ├── 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 │ prediction-poll.sh, prediction-agent.sh — evidence-based predictions
├── supervisor/ supervisor-poll.sh — health monitoring ├── supervisor/ supervisor-poll.sh — health monitoring
├── vault/ vault-poll.sh, vault-agent.sh, vault-fire.sh — action gating ├── 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/`) ### Planner (`planner/`)
**Role**: Three-phase planning. Phase 1: update the AGENTS.md documentation **Role**: Five-phase strategic planning, executed as an action formula.
tree to reflect recent code changes. Phase 1.5: triage `prediction/unreviewed` Phase 0 (preflight): pull latest code, load persistent memory from
issues filed by the [Predictor](#predictor-planner) — accept as action/backlog `planner/MEMORY.md`. Phase 1: update the AGENTS.md documentation tree to
issues or dismiss as noise. Phase 2: gap-analyse VISION.md vs current project reflect recent code changes (fast-track PR). Phase 1.5: triage
state (including accepted predictions), create up to 5 backlog issues for the `prediction/unreviewed` issues filed by the [Predictor](#predictor-planner) —
highest-leverage gaps. 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**: **Key files**:
- `planner/planner-poll.sh` — Cron wrapper: lock, memory guard, runs planner-agent.sh - `planner/planner-poll.sh` — Cron wrapper: memory guard, dedup check, files action issue
- `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 - `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. **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` - `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `PRIMARY_BRANCH` - `PRIMARY_BRANCH`
- `CLAUDE_TIMEOUT`
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` - `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
### Predictor (`planner/`) ### Predictor (`planner/`)

View file

@ -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 ### 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 `<!-- last-reviewed: SHA -->` watermarks. You must seed this before per-file `<!-- last-reviewed: SHA -->` watermarks. You must seed this before
the first planner run, otherwise the planner sees no watermarks and treats the the first planner run, otherwise the planner sees no watermarks and treats the
entire repo as "new", generating a noisy first-run diff. 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 5. Commit and push. The planner will now see 0 changes on its first run and
only update files when real commits land. 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 ## 3. Write Good Issues

View file

@ -133,7 +133,7 @@ disinto/
│ └── best-practices.md # Gardener knowledge base │ └── best-practices.md # Gardener knowledge base
├── planner/ ├── planner/
│ ├── planner-poll.sh # Cron entry: weekly vision gap analysis │ ├── 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/
│ ├── vault-poll.sh # Cron entry: process pending dangerous actions │ ├── vault-poll.sh # Cron entry: process pending dangerous actions
│ ├── vault-agent.sh # Classifies and routes actions (claude -p) │ ├── vault-agent.sh # Classifies and routes actions (claude -p)

229
formulas/run-planner.toml Normal file
View file

@ -0,0 +1,229 @@
# 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
[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:
<!-- last-reviewed: <sha> -->
4. Check for changes since the watermark:
git log --oneline <watermark>..HEAD -- <directory>
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":"<branch>","base":"<primary-branch>",
"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: <name>
vars:
key: "value"
---
<explanation of why this matters>
For freeform gaps:
<problem statement + why it matters for the vision + rough approach>
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":[<backlog_label_id>]}'
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"]

View file

@ -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 '(?<=<!-- last-reviewed: )[a-f0-9]+' "$f" 2>/dev/null | head -1 || true)
LINE_COUNT=$(wc -l < "$f")
if [ -n "$WATERMARK" ]; then
if git cat-file -e "$WATERMARK" 2>/dev/null; then
CHANGES=$(git log --oneline "${WATERMARK}..HEAD" -- "$(dirname "$f")" 2>/dev/null | wc -l || true)
else
CHANGES="unknown"
fi
else
WATERMARK="none"
CHANGES="all"
fi
AGENTS_INFO="${AGENTS_INFO} ${f} (${LINE_COUNT} lines, watermark: ${WATERMARK:0:7}, changes: ${CHANGES})\n"
[ "$CHANGES" != "0" ] && NEEDS_UPDATE=true
done
if [ "$NEEDS_UPDATE" = false ] && [ -n "$AGENTS_FILES" ]; then
log "All AGENTS.md files up to date — skipping phase 1"
else
# Create branch for changes
BRANCH_NAME="chore/planner-agents-$(date -u +%Y%m%d)"
git checkout -B "$BRANCH_NAME" 2>/dev/null
PHASE1_PROMPT="You maintain the AGENTS.md documentation tree for this repository.
Your job: keep every AGENTS.md file accurate, concise, and current.
## How AGENTS.md works
- Each directory with significant logic has its own AGENTS.md
- Root AGENTS.md references sub-directory files
- Each file has a watermark: \`<!-- last-reviewed: <sha> -->\` 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 <watermark>..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 <sha>\`
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: \`<!-- last-reviewed: ${HEAD_SHA} -->\`
- 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\": <issue-number>, \"decision\": \"ACCEPT_ACTION\", \"title\": \"action title\", \"formula\": \"formula-name\", \"vars\": {\"var1\": \"value1\"}, \"reason\": \"why\"}
{\"prediction\": <issue-number>, \"decision\": \"ACCEPT_BACKLOG\", \"title\": \"backlog issue title\", \"body\": \"problem + approach\", \"reason\": \"why\"}
{\"prediction\": <issue-number>, \"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 <host-alias>\") 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\": \"<formula-name>\", \"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 ---"

View file

@ -1,8 +1,10 @@
#!/usr/bin/env bash #!/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. # 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 set -euo pipefail
@ -38,14 +40,53 @@ fi
log "--- Planner poll start ---" log "--- Planner poll start ---"
# ── Run planner agent ───────────────────────────────────────────────────── # ── Dedup: skip if an open run-planner action issue already exists ────────
"$SCRIPT_DIR/planner-agent.sh" 2>&1 | while IFS= read -r line; do OPEN_ACTIONS=$(codeberg_api GET "/issues?state=open&type=issues&labels=action&limit=50" 2>/dev/null || true)
log " $line" if [ -n "$OPEN_ACTIONS" ] && [ "$OPEN_ACTIONS" != "null" ]; then
done EXISTING=$(printf '%s' "$OPEN_ACTIONS" | \
jq '[.[] | select(.title | test("run-planner"))] | length' 2>/dev/null || echo 0)
EXIT_CODE=${PIPESTATUS[0]} if [ "${EXISTING:-0}" -gt 0 ]; then
if [ "$EXIT_CODE" -ne 0 ]; then log "poll: open run-planner action issue already exists — skipping"
log "poll: planner-agent exited with code $EXIT_CODE" log "--- Planner poll done ---"
exit 0
fi
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
---
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 ---" log "--- Planner poll done ---"