fix: Replace Codeberg dependency with local Forgejo instance (#611)

- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
  creates admin + bot users (dev-bot, review-bot), generates API
  tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
  FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
  FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
  FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
  →forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
  FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-23 16:57:12 +00:00
parent 39d30faf45
commit a66bd91721
58 changed files with 863 additions and 628 deletions

View file

@ -3,24 +3,33 @@
# NEVER commit .env to the repo.
# ── Per-project config ────────────────────────────────────────────────────
# Project-specific settings (CODEBERG_REPO, PROJECT_REPO_ROOT, PRIMARY_BRANCH,
# Project-specific settings (FORGE_REPO, PROJECT_REPO_ROOT, PRIMARY_BRANCH,
# WOODPECKER_REPO_ID) now live in projects/*.toml — see projects/harb.toml
# for an example. Do NOT set them here; they leak into every session.
# ── Forge (Forgejo) ─────────────────────────────────────────────────────
# Base URL for the local Forgejo instance. disinto init provisions this.
FORGE_URL=http://localhost:3000
# ── Auth tokens ───────────────────────────────────────────────────────────
# Dev-agent token: push branches, create PRs, merge PRs.
# Use the dedicated bot account (e.g. factory_bot / disinto_dev).
# Use the dedicated bot account (e.g. dev-bot).
# Branch protection: this account must be in the merge whitelist.
CODEBERG_TOKEN=
FORGE_TOKEN=
# Review-agent token: post review comments and submit formal approvals.
# Use the human/admin account (e.g. johba).
# Use the review bot account (e.g. review-bot).
# Branch protection: this account must be in the approvals whitelist.
REVIEW_BOT_TOKEN=
FORGE_REVIEW_TOKEN=
# Comma-separated Codeberg usernames to filter from issue comments.
# Comma-separated forge usernames to filter from issue comments.
# The token owner is auto-detected; add extra bot accounts here if needed.
CODEBERG_BOT_USERNAMES=
FORGE_BOT_USERNAMES=
# ── Backwards compatibility ───────────────────────────────────────────────
# If CODEBERG_TOKEN is set but FORGE_TOKEN is not, env.sh falls back to
# CODEBERG_TOKEN automatically (same for REVIEW_BOT_TOKEN, CODEBERG_REPO,
# CODEBERG_BOT_USERNAMES). No action needed for existing deployments.
# ── Woodpecker CI ─────────────────────────────────────────────────────────
WOODPECKER_TOKEN=

View file

@ -92,7 +92,7 @@ echo "=== 2/2 Function resolution ==="
# Functions provided by shared lib files (available to all agent scripts via source).
#
# Included — these are inline-sourced by agent scripts:
# lib/env.sh — sourced by every agent (log, codeberg_api, etc.)
# lib/env.sh — sourced by every agent (log, forge_api, etc.)
# lib/agent-session.sh — sourced by orchestrators (create_agent_session, monitor_phase_loop, etc.)
# lib/ci-helpers.sh — sourced by pollers and review (ci_passed, classify_pipeline_failure, etc.)
# lib/load-project.sh — sourced by env.sh when PROJECT_TOML is set

View file

@ -4,7 +4,7 @@
## What this repo is
Disinto is an autonomous code factory. It manages eight agents (dev, review,
gardener, supervisor, planner, predictor, action, vault) that pick up issues from Codeberg,
gardener, supervisor, planner, predictor, action, vault) that pick up issues from forge,
implement them, review PRs, plan from the vision, gate dangerous actions, and
keep the system healthy — all via cron and `claude -p`.
@ -42,7 +42,7 @@ disinto/
- **Shell**: bash (all agents are bash scripts)
- **AI**: `claude -p` (one-shot) or `claude` (interactive/tmux sessions)
- **CI**: Woodpecker CI (queried via REST API + Postgres)
- **VCS**: Codeberg (git + Gitea REST API)
- **VCS**: Forgejo (git + Gitea-compatible REST API)
- **Notifications**: Matrix (optional)
## Coding conventions

View file

@ -1,18 +1,36 @@
# Bootstrapping a New Project
How to point disinto at a new target project and get all four agents running.
How to point disinto at a new target project and get all agents running.
## Prerequisites
Before starting, ensure you have:
- [ ] A **Codeberg repo** with at least one issue labeled `backlog`
- [ ] A **git repo** (GitHub, Codeberg, or any URL) with at least one issue labeled `backlog`
- [ ] A **Woodpecker CI** pipeline (`.woodpecker/` dir with at least one `.yml`)
- [ ] A **second Codeberg account** for the review bot (branch protection requires reviews from a different user)
- [ ] **Docker** installed (for local Forgejo provisioning) — or a running Forgejo instance
- [ ] A **local clone** of the target repo on the same machine as disinto
- [ ] `claude` CLI installed and authenticated (`claude --version`)
- [ ] `tmux` installed (`tmux -V`) — required for persistent dev sessions (issue #80+)
## Quick Start
The fastest path is `disinto init`, which provisions a local Forgejo instance, creates bot users and tokens, clones the repo, and sets up cron — all in one command:
```bash
disinto init https://github.com/org/repo
```
This will:
1. Start a local Forgejo instance via Docker (at `http://localhost:3000`)
2. Create admin + bot users (dev-bot, review-bot) with API tokens
3. Create the repo on Forgejo and push your code
4. Generate a `projects/<name>.toml` config
5. Create standard labels (backlog, in-progress, blocked, etc.)
6. Install cron entries for the agents
No external accounts or tokens needed.
## 1. Configure `.env`
```bash
@ -22,19 +40,15 @@ cp .env.example .env
Fill in:
```bash
# ── Target project ──────────────────────────────────────────
CODEBERG_REPO=org/project # Codeberg slug
PROJECT_REPO_ROOT=/home/you/project # absolute path to local clone
PRIMARY_BRANCH=main # main or master
# ── Auth ────────────────────────────────────────────────────
# CODEBERG_TOKEN= # or use ~/.netrc (machine codeberg.org)
REVIEW_BOT_TOKEN=tok_xxxxxxxx # the second account's API token
# ── Forge (auto-populated by disinto init) ─────────────────
FORGE_URL=http://localhost:3000 # local Forgejo instance
FORGE_TOKEN= # dev-bot token (auto-generated)
FORGE_REVIEW_TOKEN= # review-bot token (auto-generated)
# ── Woodpecker CI ───────────────────────────────────────────
WOODPECKER_TOKEN=tok_xxxxxxxx
WOODPECKER_SERVER=http://localhost:8000
WOODPECKER_REPO_ID=2 # numeric — find via Woodpecker UI or API
# WOODPECKER_REPO_ID — now per-project, set in projects/*.toml [ci] section
# Woodpecker Postgres (for direct pipeline queries)
WOODPECKER_DB_PASSWORD=secret
@ -52,23 +66,35 @@ WOODPECKER_DB_NAME=woodpecker
CLAUDE_TIMEOUT=7200 # seconds per Claude invocation
```
### Backwards compatibility
If you have an existing deployment using `CODEBERG_TOKEN` / `REVIEW_BOT_TOKEN` in `.env`, those still work — `env.sh` falls back to the old names automatically. No migration needed.
## 2. Configure Project TOML
Each project needs a `projects/<name>.toml` file with box-specific settings
(absolute paths, Woodpecker CI IDs, Matrix credentials). These files are
(absolute paths, Woodpecker CI IDs, Matrix credentials, forge URL). These files are
**gitignored** — they are local installation config, not shared code.
To create one:
```bash
# Automatic — generates TOML, clones repo, sets up cron:
disinto init https://codeberg.org/org/repo
disinto init https://github.com/org/repo
# Manual — copy a template and fill in your values:
cp projects/myproject.toml.example projects/myproject.toml
vim projects/myproject.toml
```
The `forge_url` field in the TOML tells all agents where to find the forge API:
```toml
name = "myproject"
repo = "org/myproject"
forge_url = "http://localhost:3000"
```
The repo ships `projects/*.toml.example` templates showing the expected
structure. See any `.toml.example` file for the full field reference.
@ -133,7 +159,7 @@ The dev-agent reads this file via `claude -p` before implementing any issue. The
### Required: Issue labels
Create two labels on the Codeberg repo:
`disinto init` creates these automatically. If setting up manually, create these labels on the forge repo:
| Label | Purpose |
|-------|---------|
@ -150,7 +176,7 @@ Optional but recommended:
### Required: Branch protection
On Codeberg, set up branch protection for your primary branch:
On Forgejo, set up branch protection for your primary branch:
- **Require pull request reviews**: enabled
- **Required approvals**: 1 (from the review bot account)
@ -159,8 +185,8 @@ On Codeberg, set up branch protection for your primary branch:
This ensures dev-agent can't merge its own PRs — it must wait for review-agent (running as the bot account) to approve.
> **Common pitfall:** Approvals alone are not enough. You must also:
> 1. Add `review_bot` as a **write** collaborator on the repo (Settings → Collaborators)
> 2. Set both `approvals_whitelist_username` **and** `merge_whitelist_usernames` to include `review_bot` in the branch protection rule
> 1. Add `review-bot` as a **write** collaborator on the repo (Settings → Collaborators)
> 2. Set both `approvals_whitelist_username` **and** `merge_whitelist_usernames` to include `review-bot` in the branch protection rule
>
> Without write access, the bot's approval is counted but the merge API returns HTTP 405.
@ -361,7 +387,7 @@ Meanwhile:
|---------|-------|-----|
| Dev-agent for project B never starts | Shared lock file path | Each TOML `name` field must be unique — lock is `/tmp/dev-agent-{name}.lock` |
| Review-poll skips all PRs | CI gate with no CI configured | Set `woodpecker_repo_id = 0` in the TOML `[ci]` section to bypass the CI check |
| Approved PRs never merge (HTTP 405) | `review_bot` not in merge/approvals whitelist | Add as write collaborator; set both `approvals_whitelist_username` and `merge_whitelist_usernames` in branch protection |
| Approved PRs never merge (HTTP 405) | `review-bot` not in merge/approvals whitelist | Add as write collaborator; set both `approvals_whitelist_username` and `merge_whitelist_usernames` in branch protection |
| Dev-agent churns through issues without waiting for open PRs to land | No single-threaded enforcement | `WAITING_PRS` check in dev-poll holds new work — verify TOML `name` is consistent across invocations |
| Label ping-pong (issue reopened then immediately re-closed) | `already_done` handler doesn't close issue | Review dev-agent log; `already_done` status should auto-close the issue |

View file

@ -14,7 +14,7 @@
<br>
Point it at a Codeberg repo with a Woodpecker CI pipeline and it will pick up issues, implement them, review PRs, and keep the system healthy — all on its own.
Point it at a git repo with a Woodpecker CI pipeline and it will pick up issues, implement them, review PRs, and keep the system healthy — all on its own.
## Architecture
@ -49,9 +49,8 @@ all agents ──→ matrix_send() ← status updates, escalations, merge no
**Required:**
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) — `claude` in PATH, authenticated
- [Codeberg](https://codeberg.org/) account with an API token — disinto reads issues, opens PRs, posts comments, and merges via the Codeberg API
- A second Codeberg account for the review bot — reviews posted under a separate identity so the dev-agent doesn't review its own PRs (`REVIEW_BOT_TOKEN`)
- [Woodpecker CI](https://woodpecker-ci.org/) — local instance connected to your Codeberg repo; disinto monitors pipelines, retries failures, and queries the Woodpecker Postgres DB directly
- [Docker](https://docker.com/) — for provisioning a local Forgejo instance (or a running Forgejo/Gitea instance)
- [Woodpecker CI](https://woodpecker-ci.org/) — local instance connected to your forge; disinto monitors pipelines, retries failures, and queries the Woodpecker Postgres DB directly
- PostgreSQL client (`psql`) — for Woodpecker DB queries (pipeline status, build counts)
- `jq`, `curl`, `git`
@ -65,24 +64,20 @@ all agents ──→ matrix_send() ← status updates, escalations, merge no
```bash
# 1. Clone
git clone ssh://git@codeberg.org/johba/disinto.git
git clone https://github.com/johba/disinto.git
cd disinto
# 2. Configure
cp .env.example .env
# 2. Bootstrap a project (provisions local Forgejo, creates tokens, clones repo)
disinto init https://github.com/yourorg/yourproject
```
Edit `.env` with your values:
Or configure manually — edit `.env` with your values:
```bash
# Target repo
CODEBERG_REPO=yourorg/yourproject # Codeberg org/repo slug
CODEBERG_API=https://codeberg.org/api/v1/repos/yourorg/yourproject
PROJECT_REPO_ROOT=/path/to/your/project # local clone of the target repo
# Auth tokens
CODEBERG_TOKEN=... # main account — or put it in ~/.netrc
REVIEW_BOT_TOKEN=... # separate Codeberg account for code reviews
# Forge (auto-populated by disinto init)
FORGE_URL=http://localhost:3000 # local Forgejo instance
FORGE_TOKEN=... # dev-bot token
FORGE_REVIEW_TOKEN=... # review-bot token
# Woodpecker CI
WOODPECKER_SERVER=http://localhost:8000
@ -148,7 +143,7 @@ disinto/
├── memory.md
├── disk.md
├── ci.md
├── codeberg.md
├── forge.md
├── dev-agent.md
├── review-agent.md
└── git.md

View file

@ -31,7 +31,7 @@
| Service | Purpose | Limits |
|---------|---------|--------|
| Codeberg | source hosting + CI triggers | 10 GB storage, 1000 min/mo CI |
| Forge (Forgejo) | source hosting + CI triggers | 10 GB storage, 1000 min/mo CI |
| Anthropic | Claude API | $X/mo budget, rate limit: 100k TPM |
| Cloudflare | DNS + CDN | free tier |

View file

@ -34,7 +34,7 @@ A solo founder sets the vision and defines quality gates. Disinto derives the ba
## Growth goals
- **Attract developers** — the project should be easy to understand, easy to fork, easy to contribute to.
- **Stars and forks** — measure traction through Codeberg/GitHub engagement.
- **Stars and forks** — measure traction through forge/GitHub engagement.
- **Contributors** — lower the barrier to entry. Good docs, clear architecture, working examples.
- **Reference deployments** — showcase real projects built and operated by Disinto.
- **Vault as differentiator** — the quality gate model (vision + vault) is what sets Disinto apart from generic CI/CD. Make it visible and easy to understand.

View file

@ -25,7 +25,7 @@ issues labeled `action` that have no active tmux session, then spawns
8. For human input: Claude sends a Matrix message and waits; the reply is injected into the session by `matrix_listener.sh`.
**Environment variables consumed**:
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `CODEBERG_WEB`
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `FORGE_WEB`
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Matrix notifications + human input
- `ACTION_IDLE_TIMEOUT` — Max seconds before killing idle session (default 14400 = 4h)
- `ACTION_MAX_LIFETIME` — Max total session wall-clock seconds (default 28800 = 8h); caps session independently of idle timeout

View file

@ -42,7 +42,7 @@ SESSION_START_EPOCH=$(date +%s)
# --- Phase handler globals (agent-specific; defaults in phase-handler.sh) ---
# shellcheck disable=SC2034 # used by phase-handler.sh
API="${CODEBERG_API}"
API="${FORGE_API}"
BRANCH="action/issue-${ISSUE}"
# shellcheck disable=SC2034 # used by phase-handler.sh
WORKTREE="/tmp/action-${ISSUE}-$(date +%s)"
@ -133,8 +133,8 @@ fi
# --- Fetch issue ---
log "fetching issue #${ISSUE}"
ISSUE_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/issues/${ISSUE}") || true
ISSUE_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/issues/${ISSUE}") || true
if [ -z "$ISSUE_JSON" ] || ! printf '%s' "$ISSUE_JSON" | jq -e '.id' >/dev/null 2>&1; then
log "ERROR: failed to fetch issue #${ISSUE}"
@ -161,18 +161,18 @@ if [ -n "$YAML_MODEL" ]; then
fi
# --- Resolve bot username(s) for comment filtering ---
_bot_login=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true)
_bot_login=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true)
# Build list: token owner + any extra names from CODEBERG_BOT_USERNAMES (comma-separated)
# Build list: token owner + any extra names from FORGE_BOT_USERNAMES (comma-separated)
_bot_logins="${_bot_login}"
if [ -n "${CODEBERG_BOT_USERNAMES:-}" ]; then
_bot_logins="${_bot_logins:+${_bot_logins},}${CODEBERG_BOT_USERNAMES}"
if [ -n "${FORGE_BOT_USERNAMES:-}" ]; then
_bot_logins="${_bot_logins:+${_bot_logins},}${FORGE_BOT_USERNAMES}"
fi
# --- Fetch existing comments (resume context, excluding bot comments) ---
COMMENTS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/issues/${ISSUE}/comments?limit=50") || true
COMMENTS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/issues/${ISSUE}/comments?limit=50") || true
PRIOR_COMMENTS=""
if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSON" != "[]" ]; then
@ -184,7 +184,7 @@ if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSO
fi
# --- Create Matrix thread for this issue ---
ISSUE_URL="${CODEBERG_WEB}/issues/${ISSUE}"
ISSUE_URL="${FORGE_WEB}/issues/${ISSUE}"
_thread_id=$(matrix_send_ctx "action" \
"⚡ Action #${ISSUE}: ${ISSUE_TITLE}${ISSUE_URL}" \
"⚡ <a href='${ISSUE_URL}'>Action #${ISSUE}</a>: ${ISSUE_TITLE}") || true
@ -254,9 +254,9 @@ ${PRIOR_SECTION}## Instructions
3. Post progress as comments on issue #${ISSUE} after significant steps:
curl -sf -X POST \\
-H \"Authorization: token \${CODEBERG_TOKEN}\" \\
-H \"Authorization: token \${FORGE_TOKEN}\" \\
-H 'Content-Type: application/json' \\
\"${CODEBERG_API}/issues/${ISSUE}/comments\" \\
\"${FORGE_API}/issues/${ISSUE}/comments\" \\
-d \"{\\\"body\\\": \\\"your comment here\\\"}\"
4. If a step requires human input or approval, send a Matrix message explaining
@ -278,19 +278,19 @@ ${PRIOR_SECTION}## Instructions
files you need to persistent paths before signaling done.
- Close the issue:
curl -sf -X PATCH \\
-H \"Authorization: token \${CODEBERG_TOKEN}\" \\
-H \"Authorization: token \${FORGE_TOKEN}\" \\
-H 'Content-Type: application/json' \\
\"${CODEBERG_API}/issues/${ISSUE}\" \\
\"${FORGE_API}/issues/${ISSUE}\" \\
-d '{\"state\": \"closed\"}'
- Signal completion: echo \"PHASE:done\" > \"${PHASE_FILE}\"
5. Environment variables available in your bash sessions:
CODEBERG_TOKEN, CODEBERG_API, CODEBERG_REPO, CODEBERG_WEB, PROJECT_NAME
FORGE_TOKEN, FORGE_API, FORGE_REPO, FORGE_WEB, PROJECT_NAME
(all sourced from ${FACTORY_ROOT}/.env)
### CRITICAL: Never embed secrets in issue bodies, comments, or PR descriptions
- NEVER put API keys, tokens, passwords, or private keys in issue text or comments.
- Always reference secrets via env var names (e.g. \\\$BASE_RPC_URL, \\\$CODEBERG_TOKEN).
- Always reference secrets via env var names (e.g. \\\$BASE_RPC_URL, \\\${FORGE_TOKEN}).
- If a formula step needs a secret, read it from .env or the environment at runtime.
- Before posting any comment, verify it contains no credentials, hex keys > 32 chars,
or URLs with embedded API keys.
@ -330,9 +330,9 @@ _lifetime_watchdog() {
# Post summary comment on issue
local body="Action session killed: wall-clock lifetime cap (${hours}h) reached."
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/issues/${ISSUE}/comments" \
"${FORGE_API}/issues/${ISSUE}/comments" \
-d "{\"body\": \"${body}\"}" >/dev/null 2>&1 || true
printf 'PHASE:failed\nReason: max_lifetime (%sh) reached\n' "$hours" > "$PHASE_FILE"
# Touch phase-changed marker so monitor_phase_loop picks up immediately

View file

@ -31,8 +31,8 @@ fi
# --- Find open 'action' issues ---
log "scanning for open action issues"
ACTION_ISSUES=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/issues?state=open&labels=action&limit=50&type=issues") || true
ACTION_ISSUES=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/issues?state=open&labels=action&limit=50&type=issues") || true
if [ -z "$ACTION_ISSUES" ] || [ "$ACTION_ISSUES" = "null" ]; then
log "no action issues found"

View file

@ -7,8 +7,8 @@
# disinto status Show factory status
#
# Usage:
# disinto init https://codeberg.org/user/repo
# disinto init https://codeberg.org/user/repo --branch main --ci-id 3
# disinto init https://github.com/user/repo
# disinto init user/repo --branch main --ci-id 3
# disinto status
# =============================================================================
set -euo pipefail
@ -30,45 +30,49 @@ Init options:
--branch <name> Primary branch (default: auto-detect)
--repo-root <path> Local clone path (default: ~/name)
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
--token <token> Codeberg API token (saved to ~/.netrc)
--forge-url <url> Forge base URL (default: http://localhost:3000)
--yes Skip confirmation prompts
EOF
exit 1
}
# Extract org/repo slug from various URL formats.
# Accepts: https://codeberg.org/user/repo, codeberg.org/user/repo,
# user/repo, https://codeberg.org/user/repo.git
# Accepts: https://github.com/user/repo, https://codeberg.org/user/repo,
# http://localhost:3000/user/repo, user/repo, *.git
parse_repo_slug() {
local url="$1"
url="${url#https://}"
url="${url#http://}"
url="${url#codeberg.org/}"
# Strip any hostname (anything before the first / that contains a dot or colon)
if [[ "$url" =~ ^[a-zA-Z0-9._:-]+/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+ ]]; then
url="${url#*/}" # strip host part
fi
url="${url%.git}"
url="${url%/}"
if [[ ! "$url" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
echo "Error: invalid repo URL — expected https://codeberg.org/org/repo or org/repo" >&2
echo "Error: invalid repo URL — expected https://host/org/repo or org/repo" >&2
exit 1
fi
printf '%s' "$url"
}
# Build a clone-able URL from a slug.
# Build a clone-able URL from a slug and forge URL.
clone_url_from_slug() {
printf 'https://codeberg.org/%s.git' "$1"
local slug="$1" forge_url="${2:-${FORGE_URL:-http://localhost:3000}}"
printf '%s/%s.git' "$forge_url" "$slug"
}
# Write (or update) Codeberg credentials in ~/.netrc.
# Write (or update) credentials in ~/.netrc for a given host.
write_netrc() {
local login="$1" token="$2"
local host="$1" login="$2" token="$3"
local netrc="${HOME}/.netrc"
# Remove existing codeberg.org entry if present
# Remove existing entry for this host if present
if [ -f "$netrc" ]; then
local tmp
tmp=$(mktemp)
awk '
/^machine codeberg\.org/ { skip=1; next }
awk -v h="$host" '
$0 ~ "^machine " h { skip=1; next }
/^machine / { skip=0 }
!skip
' "$netrc" > "$tmp"
@ -76,93 +80,252 @@ write_netrc() {
fi
# Append new entry
printf 'machine codeberg.org\nlogin %s\npassword %s\n' "$login" "$token" >> "$netrc"
printf 'machine %s\nlogin %s\npassword %s\n' "$host" "$login" "$token" >> "$netrc"
chmod 600 "$netrc"
}
# Interactively set up Codeberg auth if missing.
# Args: [token_from_flag]
setup_codeberg_auth() {
local token_flag="${1:-}"
local repo_slug="${2:-}"
FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
# --token flag takes priority: verify and save
if [ -n "$token_flag" ]; then
local verify_url="https://codeberg.org/api/v1/repos/${repo_slug}"
if ! curl -sf --max-time 10 \
-H "Authorization: token ${token_flag}" \
"$verify_url" >/dev/null 2>&1; then
echo "Error: provided token failed verification" >&2
# Provision or connect to a local Forgejo instance.
# Creates admin + bot users, generates API tokens, stores in .env.
setup_forge() {
local forge_url="$1"
local repo_slug="$2"
echo ""
echo "── Forge setup ────────────────────────────────────────"
# Check if Forgejo is already running
if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then
echo "Forgejo: ${forge_url} (already running)"
else
echo "Forgejo not reachable at ${forge_url}"
echo "Starting Forgejo via Docker..."
if ! command -v docker &>/dev/null; then
echo "Error: docker not found — needed to provision Forgejo" >&2
echo " Install Docker or start Forgejo manually at ${forge_url}" >&2
exit 1
fi
write_netrc "token" "$token_flag"
echo "Saving to ~/.netrc... done."
echo "Verified: token accepted ✓"
export CODEBERG_TOKEN="$token_flag"
return
fi
# Existing auth — skip
if [ -n "${CODEBERG_TOKEN:-}" ]; then
return
fi
if grep -q 'codeberg\.org' ~/.netrc 2>/dev/null; then
CODEBERG_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
export CODEBERG_TOKEN
return
fi
# Create data directory
mkdir -p "${FORGEJO_DATA_DIR}"
# Non-interactive — fail with guidance
if [ ! -t 0 ]; then
echo "Error: no Codeberg auth found" >&2
echo " Set CODEBERG_TOKEN, configure ~/.netrc, or use --token <token>" >&2
exit 1
fi
# Extract port from forge_url
local forge_port
forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|')
forge_port="${forge_port:-3000}"
# Interactive guided flow
echo ""
echo "No Codeberg authentication found."
echo ""
echo "1. Open https://codeberg.org/user/settings/applications"
echo "2. Create a token with these scopes:"
echo " - write:issue (create issues, add labels, post comments, close issues)"
echo " - write:repository (push branches, create PRs, merge PRs)"
echo "3. Paste the token below."
echo ""
while true; do
read -rsp "Codeberg token: " token_input
echo ""
if [ -z "$token_input" ]; then
echo "Token cannot be empty. Try again." >&2
continue
# Start Forgejo container
if docker ps -a --format '{{.Names}}' | grep -q '^disinto-forgejo$'; then
docker start disinto-forgejo >/dev/null 2>&1 || true
else
docker run -d \
--name disinto-forgejo \
--restart unless-stopped \
-p "${forge_port}:3000" \
-p 2222:22 \
-v "${FORGEJO_DATA_DIR}:/data" \
-e "FORGEJO__database__DB_TYPE=sqlite3" \
-e "FORGEJO__server__ROOT_URL=${forge_url}/" \
-e "FORGEJO__server__HTTP_PORT=3000" \
-e "FORGEJO__service__DISABLE_REGISTRATION=true" \
forgejo/forgejo:latest
fi
local verify_url="https://codeberg.org/api/v1/repos/${repo_slug}"
if ! curl -sf --max-time 10 \
-H "Authorization: token ${token_input}" \
"$verify_url" >/dev/null 2>&1; then
echo "Token verification failed. Check your token and try again." >&2
read -rp "Retry? [Y/n] " retry
if [[ "$retry" =~ ^[Nn] ]]; then
echo "Aborted." >&2
# Wait for Forgejo to become healthy
echo -n "Waiting for Forgejo to start"
local retries=0
while ! curl -sf --max-time 3 "${forge_url}/api/v1/version" >/dev/null 2>&1; do
retries=$((retries + 1))
if [ "$retries" -gt 60 ]; then
echo ""
echo "Error: Forgejo did not become ready within 60s" >&2
exit 1
fi
continue
echo -n "."
sleep 1
done
echo " ready"
fi
# Create admin user if it doesn't exist
local admin_user="disinto-admin"
local admin_pass
admin_pass="admin-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
echo "Creating admin user: ${admin_user}"
docker exec disinto-forgejo forgejo admin user create \
--admin \
--username "${admin_user}" \
--password "${admin_pass}" \
--email "admin@disinto.local" \
--must-change-password=false 2>/dev/null || true
fi
# Get or create admin token
local admin_token
admin_token=$(curl -sf -X POST \
-u "${admin_user}:${admin_pass}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/users/${admin_user}/tokens" \
-d '{"name":"disinto-admin-token","scopes":["all"]}' 2>/dev/null \
| jq -r '.sha1 // empty') || admin_token=""
if [ -z "$admin_token" ]; then
# Token might already exist — try listing
admin_token=$(curl -sf \
-u "${admin_user}:${admin_pass}" \
"${forge_url}/api/v1/users/${admin_user}/tokens" 2>/dev/null \
| jq -r '.[0].sha1 // empty') || admin_token=""
fi
# Create bot users and tokens
local dev_token="" review_token=""
for bot_user in dev-bot review-bot; do
local bot_pass
bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
if ! curl -sf --max-time 5 \
-H "Authorization: token ${admin_token}" \
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
echo "Creating bot user: ${bot_user}"
docker exec disinto-forgejo forgejo admin user create \
--username "${bot_user}" \
--password "${bot_pass}" \
--email "${bot_user}@disinto.local" \
--must-change-password=false 2>/dev/null || true
fi
write_netrc "token" "$token_input"
echo "Saving to ~/.netrc... done."
echo "Verified: token accepted ✓"
export CODEBERG_TOKEN="$token_input"
return
# Generate token via API (using admin credentials for the bot)
local token
token=$(curl -sf -X POST \
-H "Authorization: token ${admin_token}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/users/${bot_user}/tokens" \
-d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \
| jq -r '.sha1 // empty') || token=""
if [ -z "$token" ]; then
# Token name collision — create with timestamp suffix
token=$(curl -sf -X POST \
-H "Authorization: token ${admin_token}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/users/${bot_user}/tokens" \
-d "{\"name\":\"disinto-${bot_user}-$(date +%s)\",\"scopes\":[\"all\"]}" 2>/dev/null \
| jq -r '.sha1 // empty') || token=""
fi
if [ "$bot_user" = "dev-bot" ]; then
dev_token="$token"
else
review_token="$token"
fi
done
# Store tokens in .env
local env_file="${FACTORY_ROOT}/.env"
if [ -n "$dev_token" ]; then
if grep -q '^FORGE_TOKEN=' "$env_file" 2>/dev/null; then
sed -i "s|^FORGE_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file"
elif grep -q '^CODEBERG_TOKEN=' "$env_file" 2>/dev/null; then
sed -i "s|^CODEBERG_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file"
else
printf '\nFORGE_TOKEN=%s\n' "$dev_token" >> "$env_file"
fi
export FORGE_TOKEN="$dev_token"
export CODEBERG_TOKEN="$dev_token"
echo " dev-bot token saved"
fi
if [ -n "$review_token" ]; then
if grep -q '^FORGE_REVIEW_TOKEN=' "$env_file" 2>/dev/null; then
sed -i "s|^FORGE_REVIEW_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file"
elif grep -q '^REVIEW_BOT_TOKEN=' "$env_file" 2>/dev/null; then
sed -i "s|^REVIEW_BOT_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file"
else
printf 'FORGE_REVIEW_TOKEN=%s\n' "$review_token" >> "$env_file"
fi
export FORGE_REVIEW_TOKEN="$review_token"
export REVIEW_BOT_TOKEN="$review_token"
echo " review-bot token saved"
fi
# Store FORGE_URL in .env if not already present
if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then
printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file"
fi
# Create the repo on Forgejo if it doesn't exist
local org_name="${repo_slug%%/*}"
local repo_name="${repo_slug##*/}"
# Check if repo already exists
if ! curl -sf --max-time 5 \
-H "Authorization: token ${FORGE_TOKEN}" \
"${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
# Try creating org first (ignore if exists)
curl -sf -X POST \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/orgs" \
-d "{\"username\":\"${org_name}\",\"visibility\":\"public\"}" >/dev/null 2>&1 || true
# Create repo under org
if ! curl -sf -X POST \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/orgs/${org_name}/repos" \
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
# Fallback: create under the dev-bot user
curl -sf -X POST \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/user/repos" \
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1 || true
fi
# Add bot users as collaborators
for bot_user in dev-bot review-bot; do
curl -sf -X PUT \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/repos/${repo_slug}/collaborators/${bot_user}" \
-d '{"permission":"write"}' >/dev/null 2>&1 || true
done
echo "Repo: ${repo_slug} created on Forgejo"
else
echo "Repo: ${repo_slug} (already exists on Forgejo)"
fi
echo "Forge: ${forge_url} (ready)"
}
# Push local clone to the Forgejo remote.
push_to_forge() {
local repo_root="$1" forge_url="$2" repo_slug="$3"
local remote_url="${forge_url}/${repo_slug}.git"
if git -C "$repo_root" remote get-url forgejo >/dev/null 2>&1; then
echo "Remote: forgejo (already configured)"
else
git -C "$repo_root" remote add forgejo "$remote_url" 2>/dev/null || \
git -C "$repo_root" remote set-url forgejo "$remote_url"
echo "Remote: forgejo -> ${remote_url}"
fi
# Push all branches
git -C "$repo_root" push forgejo --all 2>/dev/null || true
git -C "$repo_root" push forgejo --tags 2>/dev/null || true
}
# Preflight check — verify all factory requirements before proceeding.
preflight_check() {
local repo_slug="${1:-}"
local forge_url="${2:-${FORGE_URL:-http://localhost:3000}}"
local errors=0
# ── Required commands ──
@ -207,37 +370,20 @@ preflight_check() {
fi
fi
# ── Codeberg auth (setup_codeberg_auth handles interactive setup;
# this verifies the API actually works) ──
if [ -n "${CODEBERG_TOKEN:-}" ] && command -v curl &>/dev/null; then
local curl_args=(-sf --max-time 10)
if [ -n "${CODEBERG_TOKEN:-}" ]; then
curl_args+=(-H "Authorization: token ${CODEBERG_TOKEN}")
else
curl_args+=(--netrc)
fi
if ! curl "${curl_args[@]}" "https://codeberg.org/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
echo "Error: Codeberg API auth failed" >&2
echo " Verify your CODEBERG_TOKEN or ~/.netrc credentials" >&2
errors=$((errors + 1))
fi
fi
# ── Codeberg SSH access ──
if command -v ssh &>/dev/null; then
local ssh_output
ssh_output=$(ssh -T -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
git@codeberg.org 2>&1) || true
if ! printf '%s' "$ssh_output" | grep -qi "successfully authenticated"; then
echo "Error: Codeberg SSH access failed (agents push via SSH)" >&2
echo " Add your SSH key: https://codeberg.org/user/settings/keys" >&2
# ── Forge API check (verify the forge is reachable and token works) ──
if [ -n "${FORGE_TOKEN:-}" ] && command -v curl &>/dev/null; then
if ! curl -sf --max-time 10 \
-H "Authorization: token ${FORGE_TOKEN}" \
"${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
echo "Error: Forge API auth failed at ${forge_url}" >&2
echo " Verify your FORGE_TOKEN and that Forgejo is running" >&2
errors=$((errors + 1))
fi
fi
# ── Optional tools (warn only) ──
if ! command -v docker &>/dev/null; then
echo "Warning: docker not found (some projects may need it)" >&2
echo "Warning: docker not found (needed for Forgejo provisioning)" >&2
fi
if [ "$errors" -gt 0 ]; then
@ -249,13 +395,13 @@ preflight_check() {
# Clone the repo if the target directory doesn't exist; validate if it does.
clone_or_validate() {
local slug="$1" target="$2"
local slug="$1" target="$2" forge_url="${3:-${FORGE_URL:-http://localhost:3000}}"
if [ -d "${target}/.git" ]; then
echo "Repo: ${target} (existing clone)"
return
fi
local url
url=$(clone_url_from_slug "$slug")
url=$(clone_url_from_slug "$slug" "$forge_url")
echo "Cloning: ${url} -> ${target}"
git clone "$url" "$target"
}
@ -278,7 +424,7 @@ detect_branch() {
# Generate projects/<name>.toml config file.
generate_toml() {
local path="$1" name="$2" repo="$3" root="$4" branch="$5" ci_id="$6"
local path="$1" name="$2" repo="$3" root="$4" branch="$5" ci_id="$6" forge_url="$7"
cat > "$path" <<EOF
# projects/${name}.toml — Project config for ${repo}
#
@ -286,6 +432,7 @@ generate_toml() {
name = "${name}"
repo = "${repo}"
forge_url = "${forge_url}"
repo_root = "${root}"
primary_branch = "${branch}"
@ -303,10 +450,11 @@ check_pipeline_stall = false
EOF
}
# Create standard labels on the Codeberg repo.
# Create standard labels on the forge repo.
create_labels() {
local repo="$1"
local api="https://codeberg.org/api/v1/repos/${repo}"
local forge_url="${2:-${FORGE_URL:-http://localhost:3000}}"
local api="${forge_url}/api/v1/repos/${repo}"
local -A labels=(
["backlog"]="#0075ca"
@ -323,7 +471,7 @@ create_labels() {
for name in backlog in-progress blocked tech-debt underspecified vision action; do
color="${labels[$name]}"
if curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${api}/labels" \
-d "{\"name\":\"${name}\",\"color\":\"${color}\"}" >/dev/null 2>&1; then
@ -415,27 +563,31 @@ disinto_init() {
shift
# Parse flags
local branch="" repo_root="" ci_id="0" auto_yes=false token_flag=""
local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag=""
while [ $# -gt 0 ]; do
case "$1" in
--branch) branch="$2"; shift 2 ;;
--repo-root) repo_root="$2"; shift 2 ;;
--ci-id) ci_id="$2"; shift 2 ;;
--token) token_flag="$2"; shift 2 ;;
--forge-url) forge_url_flag="$2"; shift 2 ;;
--yes) auto_yes=true; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# Extract org/repo slug
local codeberg_repo
codeberg_repo=$(parse_repo_slug "$repo_url")
local project_name="${codeberg_repo##*/}"
local forge_repo
forge_repo=$(parse_repo_slug "$repo_url")
local project_name="${forge_repo##*/}"
local toml_path="${FACTORY_ROOT}/projects/${project_name}.toml"
# Determine forge URL (flag > env > default)
local forge_url="${forge_url_flag:-${FORGE_URL:-http://localhost:3000}}"
echo "=== disinto init ==="
echo "Project: ${codeberg_repo}"
echo "Project: ${forge_repo}"
echo "Name: ${project_name}"
echo "Forge: ${forge_url}"
# Check for existing config
local toml_exists=false
@ -492,17 +644,27 @@ p.write_text(text)
fi
fi
# Set up Codeberg auth (interactive if needed, before preflight)
setup_codeberg_auth "$token_flag" "$codeberg_repo"
# Set up local Forgejo instance (provision if needed, create users/tokens/repo)
setup_forge "$forge_url" "$forge_repo"
# Preflight: verify factory requirements
preflight_check "$codeberg_repo"
preflight_check "$forge_repo" "$forge_url"
# Determine repo root (for new projects)
repo_root="${repo_root:-/home/${USER}/${project_name}}"
# Clone or validate
clone_or_validate "$codeberg_repo" "$repo_root"
# Clone or validate (try origin first for initial clone from upstream)
if [ ! -d "${repo_root}/.git" ]; then
# For initial setup, clone from the provided URL directly
echo "Cloning: ${repo_url} -> ${repo_root}"
git clone "$repo_url" "$repo_root" 2>/dev/null || \
clone_or_validate "$forge_repo" "$repo_root" "$forge_url"
else
echo "Repo: ${repo_root} (existing clone)"
fi
# Push to local Forgejo
push_to_forge "$repo_root" "$forge_url" "$forge_repo"
# Detect primary branch
if [ -z "$branch" ]; then
@ -518,12 +680,12 @@ p.write_text(text)
ci_id="${user_ci_id:-0}"
fi
generate_toml "$toml_path" "$project_name" "$codeberg_repo" "$repo_root" "$branch" "$ci_id"
generate_toml "$toml_path" "$project_name" "$forge_repo" "$repo_root" "$branch" "$ci_id" "$forge_url"
echo "Created: ${toml_path}"
fi
# Create labels on remote
create_labels "$codeberg_repo"
create_labels "$forge_repo" "$forge_url"
# Generate VISION.md template
generate_vision "$repo_root" "$project_name"
@ -535,6 +697,7 @@ p.write_text(text)
echo "Done. Project ${project_name} is ready."
echo " Config: ${toml_path}"
echo " Clone: ${repo_root}"
echo " Forge: ${forge_url}/${forge_repo}"
echo " Run 'disinto status' to verify."
}
@ -548,8 +711,8 @@ disinto_status() {
[ -f "$toml" ] || continue
found=true
# Parse name and repo from TOML
local pname prepo
# Parse name, repo, forge_url from TOML
local pname prepo pforge_url
pname=$(python3 -c "
import sys, tomllib
with open(sys.argv[1], 'rb') as f:
@ -560,6 +723,12 @@ import sys, tomllib
with open(sys.argv[1], 'rb') as f:
print(tomllib.load(f)['repo'])
" "$toml" 2>/dev/null) || continue
pforge_url=$(python3 -c "
import sys, tomllib
with open(sys.argv[1], 'rb') as f:
print(tomllib.load(f).get('forge_url', ''))
" "$toml" 2>/dev/null) || pforge_url=""
pforge_url="${pforge_url:-${FORGE_URL:-http://localhost:3000}}"
echo "== ${pname} (${prepo}) =="
@ -578,24 +747,24 @@ with open(sys.argv[1], 'rb') as f:
fi
# Backlog depth via API
if [ -n "${CODEBERG_TOKEN:-}" ]; then
local api="https://codeberg.org/api/v1/repos/${prepo}"
if [ -n "${FORGE_TOKEN:-}" ]; then
local api="${pforge_url}/api/v1/repos/${prepo}"
local backlog_count pr_count
backlog_count=$(curl -sf -I \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
"${api}/issues?state=open&labels=backlog&limit=1" 2>/dev/null \
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || backlog_count="?"
echo " Backlog: ${backlog_count:-0} issues"
pr_count=$(curl -sf -I \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
"${api}/pulls?state=open&limit=1" 2>/dev/null \
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || pr_count="?"
echo " Open PRs: ${pr_count:-0}"
else
echo " Backlog: (no CODEBERG_TOKEN)"
echo " Open PRs: (no CODEBERG_TOKEN)"
echo " Backlog: (no FORGE_TOKEN)"
echo " Open PRs: (no FORGE_TOKEN)"
fi
echo ""

View file

@ -19,8 +19,8 @@ while a dev-agent session is active on another issue.
- `dev/phase-test.sh` — Integration test for the phase protocol
**Environment variables consumed** (via `lib/env.sh` + project TOML):
- `CODEBERG_TOKEN` — Dev-agent token (push, PR creation, merge) — use the dedicated bot account
- `CODEBERG_REPO`, `CODEBERG_API` — Target repository
- `FORGE_TOKEN` — Dev-agent token (push, PR creation, merge) — use the dedicated bot account
- `FORGE_REPO`, `FORGE_API` — Target repository
- `PROJECT_NAME`, `PROJECT_REPO_ROOT` — Local checkout path
- `PRIMARY_BRANCH` — Branch to merge into (e.g. `main`, `master`)
- `WOODPECKER_REPO_ID` — CI pipeline lookups

View file

@ -34,21 +34,21 @@ git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true
# --- Config ---
ISSUE="${1:?Usage: dev-agent.sh <issue-number>}"
# shellcheck disable=SC2034
REPO="${CODEBERG_REPO}"
REPO="${FORGE_REPO}"
# shellcheck disable=SC2034
REPO_ROOT="${PROJECT_REPO_ROOT}"
API="${CODEBERG_API}"
API="${FORGE_API}"
LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock"
STATUSFILE="/tmp/dev-agent-status-${PROJECT_NAME:-default}"
# Gitea labels API requires []int64 — look up the "backlog" label ID once
BACKLOG_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
BACKLOG_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true)
BACKLOG_LABEL_ID="${BACKLOG_LABEL_ID:-1300815}"
# Same for "in-progress" label
IN_PROGRESS_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
IN_PROGRESS_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "in-progress") | .id' 2>/dev/null || true)
IN_PROGRESS_LABEL_ID="${IN_PROGRESS_LABEL_ID:-1300818}"
@ -128,14 +128,14 @@ cleanup_worktree() {
cleanup_labels() {
curl -sf -X DELETE \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE}/labels/${IN_PROGRESS_LABEL_ID}" >/dev/null 2>&1 || true
}
restore_to_backlog() {
cleanup_labels
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/labels" \
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
@ -151,10 +151,10 @@ cleanup() {
if [ "$CLAIMED" = true ] && [ -z "${PR_NUMBER:-}" ]; then
log "cleanup: unclaiming issue (no PR created)"
curl -sf -X DELETE \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE}/labels/${IN_PROGRESS_LABEL_ID}" >/dev/null 2>&1 || true
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/labels" \
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
@ -198,7 +198,7 @@ echo $$ > "$LOCKFILE"
# FETCH ISSUE
# =============================================================================
status "fetching issue"
ISSUE_JSON=$(curl -s -H "Authorization: token ${CODEBERG_TOKEN}" "${API}/issues/${ISSUE}") || true
ISSUE_JSON=$(curl -s -H "Authorization: token ${FORGE_TOKEN}" "${API}/issues/${ISSUE}") || true
if [ -z "$ISSUE_JSON" ] || ! echo "$ISSUE_JSON" | jq -e '.id' >/dev/null 2>&1; then
log "ERROR: failed to fetch issue #${ISSUE} (API down or invalid response)"
exit 1
@ -208,17 +208,17 @@ ISSUE_BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""')
ISSUE_BODY_ORIGINAL="$ISSUE_BODY"
# --- Resolve bot username(s) for comment filtering ---
_bot_login=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
_bot_login=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true)
# Build list: token owner + any extra names from CODEBERG_BOT_USERNAMES (comma-separated)
# Build list: token owner + any extra names from FORGE_BOT_USERNAMES (comma-separated)
_bot_logins="${_bot_login}"
if [ -n "${CODEBERG_BOT_USERNAMES:-}" ]; then
_bot_logins="${_bot_logins:+${_bot_logins},}${CODEBERG_BOT_USERNAMES}"
if [ -n "${FORGE_BOT_USERNAMES:-}" ]; then
_bot_logins="${_bot_logins:+${_bot_logins},}${FORGE_BOT_USERNAMES}"
fi
# Append human comments to issue body (filter out bot accounts)
ISSUE_COMMENTS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
ISSUE_COMMENTS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE}/comments" | \
jq -r --arg bots "$_bot_logins" \
'($bots | split(",") | map(select(. != ""))) as $bl |
@ -264,7 +264,7 @@ if [ -n "$DEP_NUMBERS" ]; then
while IFS= read -r dep_num; do
[ -z "$dep_num" ] && continue
# Check if dependency issue is closed (= satisfied)
DEP_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
DEP_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${dep_num}" | jq -r '.state // "unknown"')
if [ "$DEP_STATE" != "closed" ]; then
@ -280,9 +280,9 @@ if [ "${#BLOCKED_BY[@]}" -gt 0 ]; then
# Find a suggestion: look for the first blocker that itself has no unmet deps
SUGGESTION=""
for blocker in "${BLOCKED_BY[@]}"; do
BLOCKER_BODY=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
BLOCKER_BODY=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${blocker}" | jq -r '.body // ""')
BLOCKER_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
BLOCKER_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${blocker}" | jq -r '.state')
if [ "$BLOCKER_STATE" != "open" ]; then
@ -302,7 +302,7 @@ if [ "${#BLOCKED_BY[@]}" -gt 0 ]; then
if [ -n "$BLOCKER_DEPS" ]; then
while IFS= read -r bd; do
[ -z "$bd" ] && continue
BD_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
BD_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${bd}" | jq -r '.state // "unknown"')
if [ "$BD_STATE" != "closed" ]; then
BLOCKER_BLOCKED=true
@ -329,7 +329,7 @@ if [ "${#BLOCKED_BY[@]}" -gt 0 ]; then
# Post comment ONLY if last comment isn't already an unmet dependency notice
BLOCKED_LIST=$(printf '#%s, ' "${BLOCKED_BY[@]}" | sed 's/, $//')
LAST_COMMENT_IS_BLOCK=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
LAST_COMMENT_IS_BLOCK=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE}/comments?limit=1" | \
jq -r '.[0].body // ""' | grep -c 'Dev-agent: Unmet dependency' || true)
@ -352,7 +352,7 @@ This issue depends on ${BLOCKED_LIST}, which $(if [ "${#BLOCKED_BY[@]}" -eq 1 ];
printf '%s' "$BLOCK_COMMENT" > /tmp/block-comment.txt
jq -Rs '{body: .}' < /tmp/block-comment.txt > /tmp/block-comment.json
curl -sf -o /dev/null -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/comments" \
--data-binary @/tmp/block-comment.json 2>/dev/null || true
@ -373,13 +373,13 @@ log "preflight passed — no explicit unmet dependencies"
# CLAIM ISSUE
# =============================================================================
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/labels" \
-d "{\"labels\":[${IN_PROGRESS_LABEL_ID}]}" >/dev/null 2>&1 || true
curl -sf -X DELETE \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE}/labels/${BACKLOG_LABEL_ID}" >/dev/null 2>&1 || true
CLAIMED=true
@ -393,7 +393,7 @@ RECOVERY_MODE=false
BODY_PR=$(echo "$ISSUE_BODY_ORIGINAL" | grep -oP 'Existing PR:\s*#\K[0-9]+' | head -1) || true
if [ -n "$BODY_PR" ]; then
PR_CHECK=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PR_CHECK=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${BODY_PR}" | jq -r '{state, head_ref: .head.ref}')
PR_CHECK_STATE=$(echo "$PR_CHECK" | jq -r '.state')
if [ "$PR_CHECK_STATE" = "open" ]; then
@ -405,7 +405,7 @@ fi
if [ -z "$EXISTING_PR" ]; then
# Priority 1: match by branch name (most reliable)
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg branch "$BRANCH" \
'.[] | select(.head.ref == $branch) | "\(.number) \(.head.ref)"' | head -1) || true
@ -418,7 +418,7 @@ fi
if [ -z "$EXISTING_PR" ]; then
# Priority 2: match "Fixes #NNN" in PR body
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg issue "ixes #${ISSUE}\\b" \
'.[] | select(.body | test($issue; "i")) | "\(.number) \(.head.ref)"' | head -1) || true
@ -432,14 +432,14 @@ fi
# Priority 3: check CLOSED PRs for prior art (don't redo work from scratch)
PRIOR_ART_DIFF=""
if [ -z "$EXISTING_PR" ]; then
CLOSED_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
CLOSED_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=closed&limit=30" | \
jq -r --arg issue "#${ISSUE}" \
'.[] | select(.merged != true) | select((.title | contains($issue)) or (.body // "" | test("ixes " + $issue + "\\b"; "i"))) | "\(.number) \(.head.ref)"' | head -1) || true
if [ -n "$CLOSED_PR" ]; then
CLOSED_PR_NUM=$(echo "$CLOSED_PR" | awk '{print $1}')
log "found closed (unmerged) PR #${CLOSED_PR_NUM} as prior art"
PRIOR_ART_DIFF=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PRIOR_ART_DIFF=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${CLOSED_PR_NUM}.diff" | head -500) || true
if [ -n "$PRIOR_ART_DIFF" ]; then
log "captured prior art diff from PR #${CLOSED_PR_NUM} ($(echo "$PRIOR_ART_DIFF" | wc -l) lines)"
@ -530,7 +530,7 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# =============================================================================
# BUILD PROMPT
# =============================================================================
OPEN_ISSUES_SUMMARY=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
OPEN_ISSUES_SUMMARY=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues?state=open&labels=backlog&limit=20&type=issues" | \
jq -r '.[] | "#\(.number) \(.title)"' 2>/dev/null || echo "(could not fetch)")
@ -607,12 +607,12 @@ if [ "$RECOVERY_MODE" = true ]; then
GIT_DIFF_STAT=$(git -C "$WORKTREE" diff "origin/${PRIMARY_BRANCH}..HEAD" --stat 2>/dev/null | head -20 || echo "(no diff)")
LAST_PHASE=$(read_phase)
CI_RESULT=$(cat "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" 2>/dev/null || echo "")
REVIEW_COMMENTS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
REVIEW_COMMENTS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${PR_NUMBER}/comments?limit=10" | \
jq -r '.[-3:] | .[] | "[\(.user.login)] \(.body[:500])"' 2>/dev/null || echo "(none)")
INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}.
This is issue #${ISSUE} for the ${CODEBERG_REPO} project.
This is issue #${ISSUE} for the ${FORGE_REPO} project.
## Issue: ${ISSUE_TITLE}
@ -647,7 +647,7 @@ ${PHASE_PROTOCOL_INSTRUCTIONS}"
else
# Normal mode: initial implementation prompt
INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}.
You have been assigned issue #${ISSUE} for the ${CODEBERG_REPO} project.
You have been assigned issue #${ISSUE} for the ${FORGE_REPO} project.
## Issue: ${ISSUE_TITLE}
@ -713,7 +713,7 @@ fi
# CREATE MATRIX THREAD (before tmux so MATRIX_THREAD_ID is available for Stop hook)
# =============================================================================
if [ ! -f "${THREAD_FILE}" ] || [ -z "$(cat "$THREAD_FILE" 2>/dev/null)" ]; then
ISSUE_URL="${CODEBERG_WEB}/issues/${ISSUE}"
ISSUE_URL="${FORGE_WEB}/issues/${ISSUE}"
_thread_id=$(matrix_send_ctx "dev" \
"🔧 Issue #${ISSUE}: ${ISSUE_TITLE}${ISSUE_URL}" \
"🔧 <a href='${ISSUE_URL}'>Issue #${ISSUE}</a>: ${ISSUE_TITLE}") || true
@ -760,11 +760,11 @@ case "${_MONITOR_LOOP_EXIT:-}" in
if [ "${_MONITOR_LOOP_EXIT:-}" = "idle_prompt" ]; then
notify_ctx \
"session finished without phase signal — killed. Marking blocked." \
"session finished without phase signal — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
"session finished without phase signal — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
else
notify_ctx \
"session idle for 2h — killed. Marking blocked." \
"session idle for 2h — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
"session idle for 2h — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
fi
# Post diagnostic comment + label issue blocked
post_blocked_diagnostic "${_MONITOR_LOOP_EXIT:-idle_timeout}"

View file

@ -22,7 +22,7 @@ source "$(dirname "$0")/../lib/env.sh"
source "$(dirname "$0")/../lib/ci-helpers.sh"
# Gitea labels API requires []int64 — look up the "underspecified" label ID once
UNDERSPECIFIED_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
UNDERSPECIFIED_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "underspecified") | .id' 2>/dev/null || true)
UNDERSPECIFIED_LABEL_ID="${UNDERSPECIFIED_LABEL_ID:-1300816}"
@ -81,7 +81,7 @@ else:
# Check whether an issue already has the "blocked" label
is_blocked() {
local issue="$1"
codeberg_api GET "/issues/${issue}/labels" 2>/dev/null \
forge_api GET "/issues/${issue}/labels" 2>/dev/null \
| jq -e '.[] | select(.name == "blocked")' >/dev/null 2>&1
}
@ -103,14 +103,14 @@ _post_ci_blocked_comment() {
| PR | #${pr_num} |"
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}/issues/${issue_num}/comments" \
"${FORGE_API}/issues/${issue_num}/comments" \
-d "$(jq -nc --arg b "$comment" '{body:$b}')" >/dev/null 2>&1 || true
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}/issues/${issue_num}/labels" \
"${FORGE_API}/issues/${issue_num}/labels" \
-d "{\"labels\":[${blocked_id}]}" >/dev/null 2>&1 || true
}
@ -169,7 +169,7 @@ handle_ci_exhaustion() {
# HELPER: merge an approved PR directly (no Claude needed)
#
# Merging an approved, CI-green PR is a single API call. Spawning dev-agent
# for this fails when the issue is already closed (Codeberg auto-closes issues
# for this fails when the issue is already closed (forge auto-closes issues
# on PR creation when body contains "Fixes #N"), causing a respawn loop (#344).
# =============================================================================
try_direct_merge() {
@ -179,7 +179,7 @@ try_direct_merge() {
local merge_resp merge_http
merge_resp=$(curl -sf -w '\n%{http_code}' -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${API}/pulls/${pr_num}/merge" \
-d '{"Do":"merge","delete_branch_after_merge":true}' 2>/dev/null) || true
@ -189,15 +189,15 @@ try_direct_merge() {
if [ "${merge_http:-0}" = "200" ] || [ "${merge_http:-0}" = "204" ]; then
log "PR #${pr_num} merged successfully"
if [ "$issue_num" -gt 0 ]; then
# Close the issue (may already be closed by Codeberg auto-close)
# Close the issue (may already be closed by forge auto-close)
curl -sf -X PATCH \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${API}/issues/${issue_num}" \
-d '{"state":"closed"}' >/dev/null 2>&1 || true
# Remove in-progress label
curl -sf -X DELETE \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${issue_num}/labels/in-progress" >/dev/null 2>&1 || true
# Clean up phase/session artifacts
rm -f "/tmp/dev-session-${PROJECT_NAME}-${issue_num}.phase" \
@ -215,7 +215,7 @@ try_direct_merge() {
return 1
}
API="${CODEBERG_API}"
API="${FORGE_API}"
LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock"
LOGFILE="${FACTORY_ROOT}/dev/dev-agent-${PROJECT_NAME:-default}.log"
PREFLIGHT_RESULT="/tmp/dev-agent-preflight.json"
@ -233,7 +233,7 @@ log() {
# (See #531: direct merges should not be blocked by agent lock)
# =============================================================================
log "pre-lock: scanning for mergeable PRs"
PL_PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PL_PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20")
PL_MERGED_ANY=false
@ -261,7 +261,7 @@ for i in $(seq 0 $(($(echo "$PL_PRS" | jq 'length') - 1))); do
fi
fi
PL_CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PL_CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/commits/${PL_PR_SHA}/status" | jq -r '.state // "unknown"') || true
# Non-code PRs may have no CI — treat as passed
@ -274,7 +274,7 @@ for i in $(seq 0 $(($(echo "$PL_PRS" | jq 'length') - 1))); do
fi
# Check for approval (non-stale)
PL_REVIEWS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PL_REVIEWS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${PL_PR_NUM}/reviews") || true
PL_HAS_APPROVE=$(echo "$PL_REVIEWS" | \
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
@ -319,7 +319,7 @@ dep_is_merged() {
# Check issue is closed
local dep_state
dep_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
dep_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${dep_num}" | jq -r '.state // "open"')
if [ "$dep_state" != "closed" ]; then
return 1
@ -370,7 +370,7 @@ issue_is_ready() {
# PRIORITY 1: orphaned in-progress issues
# =============================================================================
log "checking for in-progress issues"
ORPHANS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
ORPHANS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues?state=open&labels=in-progress&limit=10&type=issues")
ORPHAN_COUNT=$(echo "$ORPHANS_JSON" | jq 'length')
@ -383,21 +383,21 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then
SKIP_LABEL=$(echo "$ORPHAN_LABELS" | grep -oE '^(formula|action|prediction/backlog|prediction/unreviewed)$' | head -1) || true
if [ -n "$SKIP_LABEL" ]; then
log "issue #${ISSUE_NUM} has '${SKIP_LABEL}' label — removing in-progress, skipping"
curl -sf -X DELETE -H "Authorization: token ${CODEBERG_TOKEN}" \
curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE_NUM}/labels/in-progress" >/dev/null 2>&1 || true
exit 0
fi
# Check if there's already an open PR for this issue
HAS_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
HAS_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg branch "fix/issue-${ISSUE_NUM}" \
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
if [ -n "$HAS_PR" ]; then
PR_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PR_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${HAS_PR}" | jq -r '.head.sha') || true
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
@ -407,7 +407,7 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then
fi
# Check formal reviews (single fetch to avoid race window)
REVIEWS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
REVIEWS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${HAS_PR}/reviews") || true
HAS_APPROVE=$(echo "$REVIEWS_JSON" | \
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
@ -482,7 +482,7 @@ fi
# PRIORITY 1.5: any open PR with REQUEST_CHANGES or CI failure (stuck PRs)
# =============================================================================
log "checking for stuck PRs"
OPEN_PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
OPEN_PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20")
for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do
@ -510,7 +510,7 @@ for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do
fi
fi
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
@ -520,7 +520,7 @@ for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do
fi
# Single fetch to avoid race window between review checks
REVIEWS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
REVIEWS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${PR_NUM}/reviews") || true
HAS_CHANGES=$(echo "$REVIEWS_JSON" | \
jq -r '[.[] | select(.state == "REQUEST_CHANGES") | select(.stale == false)] | length') || true
@ -601,12 +601,12 @@ log "scanning backlog for ready issues"
ensure_priority_label >/dev/null 2>&1 || true
# Tier 1: issues with both "priority" and "backlog" labels
PRIORITY_BACKLOG_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PRIORITY_BACKLOG_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues?state=open&labels=priority,backlog&limit=20&type=issues&sort=oldest") || true
PRIORITY_BACKLOG_JSON="${PRIORITY_BACKLOG_JSON:-[]}"
# Tier 2: all "backlog" issues (includes priority ones — deduplicated below)
ALL_BACKLOG_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
ALL_BACKLOG_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues?state=open&labels=backlog&limit=20&type=issues&sort=oldest")
# Combine: priority issues first, then remaining backlog issues (deduped)
@ -644,15 +644,15 @@ for i in $(seq 0 $((BACKLOG_COUNT - 1))); do
fi
# Check if there's already an open PR for this issue that needs attention
EXISTING_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
EXISTING_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg branch "fix/issue-${ISSUE_NUM}" --arg num "#${ISSUE_NUM}" \
'.[] | select((.head.ref == $branch) or (.title | contains($num))) | .number' | head -1) || true
if [ -n "$EXISTING_PR" ]; then
PR_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PR_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${EXISTING_PR}" | jq -r '.head.sha') || true
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
@ -662,7 +662,7 @@ for i in $(seq 0 $((BACKLOG_COUNT - 1))); do
fi
# Single fetch to avoid race window between review checks
REVIEWS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
REVIEWS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${EXISTING_PR}/reviews") || true
HAS_APPROVE=$(echo "$REVIEWS_JSON" | \
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
@ -766,7 +766,7 @@ if [ -f "$PREFLIGHT_RESULT" ]; then
REASON=$(jq -r '.reason // "unspecified"' < "$PREFLIGHT_RESULT" 2>/dev/null || echo "unspecified")
log "#${READY_ISSUE} too large: ${REASON}"
# Label as underspecified
curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
curl -sf -X POST -H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${READY_ISSUE}/labels" \
-d "{\"labels\":[${UNDERSPECIFIED_LABEL_ID}]}" >/dev/null 2>&1 || true

View file

@ -5,7 +5,7 @@
# Defines: post_refusal_comment(), _on_phase_change(), build_phase_protocol_prompt()
#
# Required globals (set by calling agent before or after sourcing):
# ISSUE, CODEBERG_TOKEN, API, CODEBERG_WEB, PROJECT_NAME, FACTORY_ROOT
# ISSUE, FORGE_TOKEN, API, FORGE_WEB, PROJECT_NAME, FACTORY_ROOT
# BRANCH, PHASE_FILE, WORKTREE, IMPL_SUMMARY_FILE, THREAD_FILE
# PRIMARY_BRANCH, SESSION_NAME, LOGFILE, ISSUE_TITLE
# WOODPECKER_REPO_ID, WOODPECKER_TOKEN, WOODPECKER_SERVER
@ -47,7 +47,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/../lib/ci-helpers.sh"
# in-progress label, and adds the "blocked" label.
#
# Args: reason [session_name]
# Uses globals: ISSUE, SESSION_NAME, PR_NUMBER, CODEBERG_TOKEN, API
# Uses globals: ISSUE, SESSION_NAME, PR_NUMBER, FORGE_TOKEN, API
post_blocked_diagnostic() {
local reason="$1"
local session="${2:-${SESSION_NAME:-}}"
@ -88,7 +88,7 @@ ${tmux_output}
# Post comment to issue
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/comments" \
-d "$(jq -nc --arg b "$comment" '{body:$b}')" >/dev/null 2>&1 || true
@ -99,7 +99,7 @@ ${tmux_output}
blocked_id=$(ensure_blocked_label_id)
if [ -n "$blocked_id" ]; then
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/labels" \
-d "{\"labels\":[${blocked_id}]}" >/dev/null 2>&1 || true
@ -173,7 +173,7 @@ _PHASE_PROTOCOL_EOF_
}
# --- Merge helper ---
# do_merge — attempt to merge PR via Codeberg API.
# do_merge — attempt to merge PR via forge API.
# Args: pr_num
# Returns:
# 0 = merged successfully
@ -183,7 +183,7 @@ do_merge() {
local pr_num="$1"
local merge_response merge_http_code merge_body
merge_response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${API}/pulls/${pr_num}/merge" \
-d '{"Do":"merge","delete_branch_after_merge":true}') || true
@ -199,7 +199,7 @@ do_merge() {
# Before escalating, check whether the PR was already merged by another agent.
if [ "$merge_http_code" = "405" ]; then
local pr_state
pr_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
pr_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${pr_num}" | jq -r '.merged // false') || pr_state="false"
if [ "$pr_state" = "true" ]; then
log "do_merge: PR #${pr_num} already merged (detected after HTTP 405) — treating as success"
@ -220,7 +220,7 @@ do_merge() {
post_refusal_comment() {
local emoji="$1" title="$2" body="$3"
local last_has_title
last_has_title=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
last_has_title=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE}/comments?limit=5" | \
jq -r --arg t "Dev-agent: ${title}" '[.[] | .body // ""] | any(contains($t)) | tostring') || true
if [ "$last_has_title" = "true" ]; then
@ -237,7 +237,7 @@ ${body}
printf '%s' "$comment" > "/tmp/refusal-comment.txt"
jq -Rs '{body: .}' < "/tmp/refusal-comment.txt" > "/tmp/refusal-comment.json"
curl -sf -o /dev/null -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/comments" \
--data-binary @"/tmp/refusal-comment.json" 2>/dev/null || \
@ -278,7 +278,7 @@ _on_phase_change() {
'{title: $title, body: $body, head: $head, base: $base}' > "/tmp/pr-request-${ISSUE}.json"
PR_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/pulls" \
--data-binary @"/tmp/pr-request-${ISSUE}.json")
@ -290,13 +290,13 @@ _on_phase_change() {
if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then
PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number')
log "created PR #${PR_NUMBER}"
PR_URL="${CODEBERG_WEB}/pulls/${PR_NUMBER}"
PR_URL="${FORGE_WEB}/pulls/${PR_NUMBER}"
notify_ctx \
"PR #${PR_NUMBER} created: ${ISSUE_TITLE}" \
"PR <a href='${PR_URL}'>#${PR_NUMBER}</a> created: ${ISSUE_TITLE}"
elif [ "$PR_HTTP_CODE" = "409" ]; then
# PR already exists (race condition) — find it
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg branch "$BRANCH" \
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
@ -305,7 +305,7 @@ _on_phase_change() {
log "PR already exists: #${PR_NUMBER}"
else
log "ERROR: PR creation got 409 but no existing PR found"
agent_inject_into_session "$SESSION_NAME" "ERROR: Could not create PR (HTTP 409, no existing PR found). Check the Codeberg API. Retry by writing PHASE:awaiting_ci again after verifying the branch was pushed."
agent_inject_into_session "$SESSION_NAME" "ERROR: Could not create PR (HTTP 409, no existing PR found). Check the forge API. Retry by writing PHASE:awaiting_ci again after verifying the branch was pushed."
return 0
fi
else
@ -327,7 +327,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
# Poll CI until done or timeout
status "waiting for CI on PR #${PR_NUMBER}"
CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || \
curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha')
CI_DONE=false
@ -346,7 +346,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
# Re-fetch HEAD — Claude may have pushed new commits since loop started
CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || echo "$CI_CURRENT_SHA")
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/commits/${CI_CURRENT_SHA}/status" | jq -r '.state // "unknown"')
if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
CI_DONE=true
@ -370,7 +370,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
echo \"PHASE:awaiting_review\" > \"${PHASE_FILE}\""
else
# Fetch CI error details
PIPELINE_NUM=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PIPELINE_NUM=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/commits/${CI_CURRENT_SHA}/status" | \
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
@ -411,7 +411,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating"
notify_ctx \
"CI exhausted after ${CI_FIX_COUNT} attempts — escalating for human help" \
"CI exhausted after ${CI_FIX_COUNT} attempts on PR <a href='${PR_URL:-${CODEBERG_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline</a><br>Step: <code>${FAILED_STEP:-unknown}</code> — escalating for human help"
"CI exhausted after ${CI_FIX_COUNT} attempts on PR <a href='${PR_URL:-${FORGE_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline</a><br>Step: <code>${FAILED_STEP:-unknown}</code> — escalating for human help"
printf 'PHASE:escalate\nReason: ci_exhausted after %d attempts (step: %s)\n' "$CI_FIX_COUNT" "${FAILED_STEP:-unknown}" > "$PHASE_FILE"
# Do NOT update LAST_PHASE_MTIME here — let the main loop detect PHASE:escalate
return 0
@ -431,7 +431,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
_ci_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500 | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
notify_ctx \
"CI failed on PR #${PR_NUMBER}: step=${FAILED_STEP:-unknown} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES})" \
"CI failed on PR <a href='${PR_URL:-${CODEBERG_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline #${PIPELINE_NUM:-?}</a><br>Step: <code>${FAILED_STEP:-unknown}</code> (exit ${FAILED_EXIT:-?})<br>Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}<br><pre>${_ci_snippet:-no logs}</pre>"
"CI failed on PR <a href='${PR_URL:-${FORGE_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline #${PIPELINE_NUM:-?}</a><br>Step: <code>${FAILED_STEP:-unknown}</code> (exit ${FAILED_EXIT:-?})<br>Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}<br><pre>${_ci_snippet:-no logs}</pre>"
agent_inject_into_session "$SESSION_NAME" "CI failed on PR #${PR_NUMBER} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}).
@ -460,7 +460,7 @@ Instructions:
if [ -z "${PR_NUMBER:-}" ]; then
log "WARNING: awaiting_review but PR_NUMBER unknown — searching for PR"
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg branch "$BRANCH" \
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
@ -498,9 +498,9 @@ Instructions:
break
fi
REVIEW_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
REVIEW_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha') || true
REVIEW_COMMENT=$(codeberg_api_all "/issues/${PR_NUMBER}/comments" | \
REVIEW_COMMENT=$(forge_api_all "/issues/${PR_NUMBER}/comments" | \
jq -r --arg sha "$REVIEW_SHA" \
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
@ -516,9 +516,9 @@ Instructions:
VERDICT=$(echo "$REVIEW_TEXT" | grep -oP '\*\*(APPROVE|REQUEST_CHANGES|DISCUSS)\*\*' | head -1 | tr -d '*' || true)
log "review verdict: ${VERDICT:-unknown}"
# Also check formal Codeberg reviews
# Also check formal forge reviews
if [ -z "$VERDICT" ]; then
VERDICT=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
VERDICT=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${PR_NUMBER}/reviews" | \
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
if [ "$VERDICT" = "APPROVED" ]; then
@ -548,7 +548,7 @@ Instructions:
if [ "$_merge_rc" -eq 0 ]; then
# Merge succeeded — close issue and signal done
curl -sf -X PATCH \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${API}/issues/${ISSUE}" \
-d '{"state":"closed"}' >/dev/null 2>&1 || true
@ -596,7 +596,7 @@ Instructions:
fi
# Check if PR was merged or closed externally
PR_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PR_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${PR_NUMBER}") || true
PR_STATE=$(echo "$PR_JSON" | jq -r '.state // "unknown"')
PR_MERGED=$(echo "$PR_JSON" | jq -r '.merged // false')
@ -605,8 +605,8 @@ Instructions:
log "PR #${PR_NUMBER} was merged externally"
notify_ctx \
"✅ PR #${PR_NUMBER} merged externally! Issue #${ISSUE} done." \
"✅ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged externally! <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
"✅ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged externally! <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
cleanup_labels
@ -637,9 +637,9 @@ Instructions:
elif [ "$phase" = "PHASE:escalate" ]; then
status "escalated — waiting for human input on issue #${ISSUE}"
ESCALATE_REASON=$(sed -n '2p' "$PHASE_FILE" 2>/dev/null | sed 's/^Reason: //' || echo "")
_issue_url="${CODEBERG_WEB}/issues/${ISSUE}"
_issue_url="${FORGE_WEB}/issues/${ISSUE}"
_pr_link=""
[ -n "${PR_NUMBER:-}" ] && _pr_link=" | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>"
[ -n "${PR_NUMBER:-}" ] && _pr_link=" | PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>"
notify_ctx \
"⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) escalated — needs human input.${ESCALATE_REASON:+ Reason: ${ESCALATE_REASON}}" \
"⚠️ <a href='${_issue_url}'>Issue #${ISSUE}</a>${_pr_link} escalated — needs human input.${ESCALATE_REASON:+ Reason: ${ESCALATE_REASON}}<br>Reply in this thread to send guidance to the agent."
@ -653,12 +653,12 @@ Instructions:
status "phase done — PR #${PR_NUMBER} merged, cleaning up"
notify_ctx \
"✅ PR #${PR_NUMBER} merged! Issue #${ISSUE} done." \
"✅ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged! <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
"✅ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged! <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
else
status "phase done — issue #${ISSUE} complete, cleaning up"
notify_ctx \
"✅ Issue #${ISSUE} done." \
"✅ <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
"✅ <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
fi
# Belt-and-suspenders: ensure in-progress label removed (idempotent)
@ -680,10 +680,10 @@ Instructions:
FAILURE_REASON="${FAILURE_REASON:-unspecified}"
log "phase: failed — reason: ${FAILURE_REASON}"
# Gitea labels API requires []int64 — look up the "backlog" label ID once
BACKLOG_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
BACKLOG_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true)
BACKLOG_LABEL_ID="${BACKLOG_LABEL_ID:-1300815}"
UNDERSPECIFIED_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
UNDERSPECIFIED_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "underspecified") | .id' 2>/dev/null || true)
UNDERSPECIFIED_LABEL_ID="${UNDERSPECIFIED_LABEL_ID:-1300816}"
@ -703,7 +703,7 @@ Instructions:
# Unclaim issue (restore backlog label, remove in-progress)
cleanup_labels
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/labels" \
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
@ -732,12 +732,12 @@ ${REASON}
### Next steps
A maintainer should split this issue or add more detail to the spec."
curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}/labels" \
-d "{\"labels\":[${UNDERSPECIFIED_LABEL_ID}]}" >/dev/null 2>&1 || true
curl -sf -X DELETE \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
"${API}/issues/${ISSUE}/labels/${BACKLOG_LABEL_ID}" >/dev/null 2>&1 || true
notify "refused #${ISSUE}: too large — ${REASON}"
;;
@ -749,7 +749,7 @@ ${REASON}
Closing as already implemented."
curl -sf -X PATCH \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE}" \
-d '{"state":"closed"}' >/dev/null 2>&1 || true
@ -779,7 +779,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
log "session failed: ${FAILURE_REASON}"
notify_ctx \
"❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}" \
"❌ <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> session failed: ${FAILURE_REASON}${PR_NUMBER:+ | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
"❌ <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> session failed: ${FAILURE_REASON}${PR_NUMBER:+ | PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
post_blocked_diagnostic "$FAILURE_REASON"
agent_kill_session "$SESSION_NAME"
@ -801,7 +801,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
log "session crashed for issue #${ISSUE}"
notify_ctx \
"session crashed unexpectedly — marking blocked" \
"session crashed unexpectedly — marking blocked${PR_NUMBER:+ | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
"session crashed unexpectedly — marking blocked${PR_NUMBER:+ | PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
post_blocked_diagnostic "crashed"
[ -z "${PR_NUMBER:-}" ] && cleanup_worktree
[ -n "${PR_NUMBER:-}" ] && log "keeping worktree (PR #${PR_NUMBER} still open)"

View file

@ -24,7 +24,7 @@ Different domains have different platforms:
| Domain | Platform | What it tracks | Status |
|--------|----------|---------------|--------|
| Code | Codeberg | Issues, PRs, reviews | **Implemented** — Live |
| Code | forge | Issues, PRs, reviews | **Implemented** — Live |
| CI/CD | Woodpecker | Build/test results | **Implemented** — Live |
| Protocol | Ponder / GraphQL | On-chain state, trades, positions | **Partial** — Live (not yet wired to evidence) |
| Infrastructure | DigitalOcean / system stats | CPU, RAM, disk, containers | **Planned** — Supervisor monitors, no evidence output yet |

View file

@ -82,7 +82,7 @@ PHASE:escalate → send Matrix notification with context (issue/PR link,
on reply → matrix_listener.sh injects reply into tmux session
on timeout → 24h: label issue blocked, kill session
PHASE:done → verify PR merged on Codeberg
PHASE:done → verify PR merged on forge
if merged → kill tmux session, clean labels, close issue
if not → inject "PR not merged yet" into session

View file

@ -24,12 +24,12 @@ a `mode` field. Two modes are supported:
In this mode, skip the normal tech-debt grooming pipeline. Instead:
a. Fetch the target issue:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<target_issue>"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<target_issue>"
b. Fetch ALL comments on the target issue to understand scope and
prior bounce reasons:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<target_issue>/comments?limit=50"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<target_issue>/comments?limit=50"
c. Read the affected files listed in the issue body to understand
the actual code scope.
d. Break the issue into 2-5 sub-issues, each sized for a single
@ -63,8 +63,8 @@ description = """
This step only runs in grooming mode. Skip if in breakdown mode.
Fetch all open tech-debt issues:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
For each issue compute a triage score:
@ -94,14 +94,14 @@ These are issues that block backlog items but are not themselves labeled backlog
The dev-agent is completely starved until they are promoted or resolved.
For each tier-0 issue:
- Read the full body: curl -sf -H "Authorization: token $CODEBERG_TOKEN" "$CODEBERG_API/issues/{number}"
- Read the full body: curl -sf -H "Authorization: token $FORGE_TOKEN" "$FORGE_API/issues/{number}"
- If resolvable: promote to backlog add acceptance criteria, affected files, relabel
- If needs human decision: add to ESCALATE block
- If invalid / wontfix: close with explanation comment
After completing all tier-0, re-fetch to check for new blockers:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
If new tier-0 blockers appeared, process those too.
@ -172,8 +172,8 @@ id = "verify"
title = "Verify completion and loop until zero tech-debt"
description = """
Re-fetch ALL open tech-debt issues and count them:
REMAINING=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
REMAINING=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))] | length')
echo "Remaining tech-debt: $REMAINING"

View file

@ -78,25 +78,25 @@ If you discover pre-existing issues (NOT introduced by this PR), create
tech-debt issues via API so they are tracked separately:
# Look up tech-debt label ID (create if missing):
TECH_DEBT_ID=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/labels" | jq -r '.[] | select(.name=="tech-debt") | .id')
TECH_DEBT_ID=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/labels" | jq -r '.[] | select(.name=="tech-debt") | .id')
if [ -z "$TECH_DEBT_ID" ]; then
TECH_DEBT_ID=$(curl -sf -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/labels" \
"$FORGE_API/labels" \
-d '{"name":"tech-debt","color":"#6B7280","description":"Pre-existing tech debt flagged by AI review"}' | jq -r '.id')
fi
# Check for duplicate before creating:
EXISTING=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=open&labels=tech-debt&limit=50" | \
EXISTING=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&labels=tech-debt&limit=50" | \
jq --arg t "TITLE" '[.[] | select(.title == $t)] | length')
# Create only if no duplicate:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" "$CODEBERG_API/issues" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" "$FORGE_API/issues" \
-d '{"title":"...","body":"Flagged by AI reviewer in PR #NNN.\n\n## Problem\n...\n\n---\n*Auto-created from AI review*","labels":[TECH_DEBT_ID]}'
Only create follow-ups for clear, actionable tech debt. Do not create
@ -138,5 +138,5 @@ For a re-review, structure the markdown as:
After writing the JSON file, signal completion:
echo "PHASE:done" > "$PHASE_FILE"
Then STOP and wait. The orchestrator will post your review to Codeberg.
Then STOP and wait. The orchestrator will post your review to the forge.
"""

View file

@ -56,8 +56,8 @@ Groom the open issue backlog. This step is the core Claude-driven analysis
Pre-checks (bash, zero tokens detect problems before invoking Claude):
1. Fetch all open issues:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=open&type=issues&limit=50&sort=updated&direction=desc"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&type=issues&limit=50&sort=updated&direction=desc"
2. Duplicate detection: compare issue titles pairwise. Normalize
(lowercase, strip prefixes like feat:/fix:/refactor:, collapse whitespace)
@ -162,7 +162,7 @@ Sibling dependency rule (CRITICAL):
If either section is missing:
a. Write a comment action to the manifest:
echo '{"action":"comment","issue":NNN,"body":"This issue is missing required sections. Please use the issue templates at `.codeberg/ISSUE_TEMPLATE/` — needs: <missing items>."}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
echo '{"action":"comment","issue":NNN,"body":"This issue is missing required sections. Please use the issue templates at `.forgejo/ISSUE_TEMPLATE/` — needs: <missing items>."}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
Where <missing items> is a comma-separated list of what's absent
(e.g. "acceptance criteria, affected files" or just "affected files").
b. Write a remove_label action to the manifest:
@ -249,19 +249,19 @@ Review all issues labeled 'blocked' and decide their fate.
(See issue #352 for the blocked label convention.)
1. Fetch all blocked issues:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=open&type=issues&labels=blocked&limit=50"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&type=issues&labels=blocked&limit=50"
2. For each blocked issue, read the full body and comments:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<number>"
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<number>/comments"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<number>"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<number>/comments"
3. Check dependencies extract issue numbers from ## Dependencies /
## Depends on / ## Blocked by sections. For each dependency:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<dep_number>"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<dep_number>"
Check if the dependency is now closed.
4. For each blocked issue, choose ONE action:
@ -459,9 +459,9 @@ executes them after the PR merges.
git push -u origin "$BRANCH"
g. Create a PR:
PR_RESPONSE=$(curl -sf -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/pulls" \
"$FORGE_API/pulls" \
-d '{"title":"chore: gardener housekeeping",
"head":"'"$BRANCH"'","base":"'"$PRIMARY_BRANCH"'",
"body":"Automated gardener housekeeping — AGENTS.md updates + pending actions manifest.\\n\\nReview `gardener/pending-actions.json` for proposed grooming actions (label changes, closures, comments). These execute after merge."}')

View file

@ -60,8 +60,8 @@ Evidence from the preflight step informs whether each prediction is valid
(e.g. "red-team stale since March 12" is confirmed by evidence/ timestamps).
1. Fetch unreviewed predictions:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
If there are none, note that and skip to step 3b (label resolution
is still required the file-at-constraints step needs label IDs).
@ -72,10 +72,10 @@ Evidence from the preflight step informs whether each prediction is valid
Project formulas are dispatched via action issues on the project repo.
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"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&type=issues&limit=50"
3b. Resolve label IDs needed for triage AND filing (fetch via $CODEBERG_API/labels).
3b. Resolve label IDs needed for triage AND filing (fetch via $FORGE_API/labels).
ALWAYS execute this step, even if there are no predictions to triage
the file-at-constraints step depends on these IDs:
- <unreviewed_label_id> prediction/unreviewed
@ -120,65 +120,65 @@ Evidence from the preflight step informs whether each prediction is valid
Example body structure:
## Problem\n<what the prediction identified>\n\n## Proposed solution\n<approach>\n\n## Affected files\n- <file1>\n- <file2>\n\n## Acceptance criteria\n- [ ] <criterion 1>\n- [ ] CI green
Create the issue:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" "$CODEBERG_API/issues" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" "$FORGE_API/issues" \
-d '{"title":"...","body":"...","labels":[<label_id>]}'
Extract the issue number from the response (jq -r '.number').
a2. Verify the label was applied (Codeberg may silently drop labels
a2. Verify the label was applied (the forge may silently drop labels
on creation). Re-apply via a separate POST if missing:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<new_issue_num>/labels" \
"$FORGE_API/issues/<new_issue_num>/labels" \
-d '{"labels":[<label_id>]}'
b. Comment on the prediction with "Actioned as #NNN — <reasoning>":
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>/comments" \
"$FORGE_API/issues/<pred_num>/comments" \
-d '{"body":"Actioned as #NNN — <reasoning>"}'
c. Relabel: remove prediction/unreviewed, add prediction/actioned:
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>/labels" \
"$FORGE_API/issues/<pred_num>/labels" \
-d '{"labels":[<actioned_label_id>]}'
d. Close the prediction:
curl -sf -X PATCH -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X PATCH -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>" \
"$FORGE_API/issues/<pred_num>" \
-d '{"state":"closed"}'
For WATCH:
a. Comment with reasoning why not urgent:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>/comments" \
"$FORGE_API/issues/<pred_num>/comments" \
-d '{"body":"Watching — <reasoning>"}'
b. Replace prediction/unreviewed label with prediction/backlog:
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>/labels" \
"$FORGE_API/issues/<pred_num>/labels" \
-d '{"labels":[<prediction_backlog_label_id>]}'
For DISMISS:
a. Comment with explicit reasoning:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>/comments" \
"$FORGE_API/issues/<pred_num>/comments" \
-d '{"body":"Dismissed — <reasoning>"}'
b. Relabel: remove prediction/unreviewed, add prediction/actioned:
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>/labels" \
"$FORGE_API/issues/<pred_num>/labels" \
-d '{"labels":[<actioned_label_id>]}'
c. Close the prediction:
curl -sf -X PATCH -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X PATCH -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<pred_num>" \
"$FORGE_API/issues/<pred_num>" \
-d '{"state":"closed"}'
6. Track promoted predictions they are added to the prerequisite tree
@ -208,8 +208,8 @@ Read these inputs:
- $PROJECT_REPO_ROOT/formulas/*.toml project-specific formulas
- Open issues (fetched via API, or reuse from prediction-triage)
- Closed issues (fetch recently closed to detect resolved prerequisites):
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=closed&type=issues&limit=50&sort=updated&direction=desc"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=closed&type=issues&limit=50&sort=updated&direction=desc"
- Planner memory (loaded in preflight)
- Promoted predictions from prediction-triage (add as prerequisites if relevant)
@ -218,8 +218,8 @@ Read these inputs:
For each issue referenced in the prerequisite tree (by #number), fetch its
recent comments to detect signals that the issue is stuck or bouncing:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<number>/comments?limit=10"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<number>/comments?limit=10"
Scan each comment body for these signals:
@ -394,17 +394,17 @@ Filing gate — for each constraint (that is NOT stuck):
## Problem\n<what this prerequisite is and which objectives it blocks>\n\n## Proposed solution\n<rough approach>\n\n## Affected files\n- <file1>\n- <file2>\n\n## Acceptance criteria\n- [ ] <criterion derived from the constraint>\n- [ ] CI green\n\n## Dependencies\n- #NNN (if depends on other open issues)
Create the issue:
curl -sf -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues" \
"$FORGE_API/issues" \
-d '{"title":"...","body":"...","labels":[<backlog_label_id>]}'
Extract the issue number from the response (jq -r '.number').
2b. Verify the label was applied (Codeberg may silently drop labels
2b. Verify the label was applied (the forge may silently drop labels
on creation). Always re-apply via a separate POST to be safe:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<new_issue_num>/labels" \
"$FORGE_API/issues/<new_issue_num>/labels" \
-d '{"labels":[<backlog_label_id>]}'
3. If an issue already exists and is open, skip it no duplicate filing.
@ -422,20 +422,20 @@ is purely additive.
5. **Add `priority` to top-5 constraint issues:**
For each of the top 5 constraint issues (whether just filed or already
existing), check if it already has the `priority` label. If not, add it:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues/<issue_number>/labels" \
"$FORGE_API/issues/<issue_number>/labels" \
-d '{"labels":[<priority_label_id>]}'
6. **Remove `priority` from issues no longer in top 5:**
Fetch all open issues that currently have the `priority` label:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=open&labels=priority&type=issues&limit=50"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&labels=priority&type=issues&limit=50"
For each issue in this list that is NOT one of the current top 5
constraint issues, remove the `priority` label (demote back to plain
`backlog`):
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues/<issue_number>/labels/<priority_label_id>"
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues/<issue_number>/labels/<priority_label_id>"
This keeps the priority set current only the active bottleneck issues
get priority, not stale constraints from previous runs.
@ -634,9 +634,9 @@ run — only file changes (tree, journal, MEMORY.md) need the PR.
git push -u origin "$BRANCH"
g. Create a PR:
curl -sf -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/pulls" \
"$FORGE_API/pulls" \
-d '{"title":"chore: planner run — prerequisite tree update",
"head":"<branch>","base":"<primary-branch>",
"body":"Automated planner run — prerequisite tree update and journal entry."}'

View file

@ -3,7 +3,7 @@
# Goal: find the project's biggest weakness. Explore when uncertain,
# exploit when confident (dispatch a formula to prove the theory).
#
# Memory: previous predictions on Codeberg ARE the memory.
# Memory: previous predictions on the forge ARE the memory.
# No separate memory file — the issue tracker is the source of truth.
#
# Executed by predictor/predictor-run.sh via cron — no action issues.
@ -33,12 +33,12 @@ Set up the working environment and load your prediction history.
git pull --ff-only origin "$PRIMARY_BRANCH" --quiet
2. Fetch ALL your previous predictions (open + recently closed):
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Fbacklog&limit=50"
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/issues?state=closed&type=issues&labels=prediction%2Factioned&limit=50&sort=updated&direction=desc"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Fbacklog&limit=50"
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/issues?state=closed&type=issues&labels=prediction%2Factioned&limit=50&sort=updated&direction=desc"
For each prediction, note:
- What you predicted (title + body)
@ -150,21 +150,21 @@ For each weakness you identify, choose one:
## Filing
1. Look up label IDs:
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/labels" | jq '[.[] | select(.name | startswith("prediction")) | {name, id}]'
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/labels" | jq '.[] | select(.name == "action") | .id'
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/labels" | jq '[.[] | select(.name | startswith("prediction")) | {name, id}]'
curl -sf -H "Authorization: token $FORGE_TOKEN" \
"$FORGE_API/labels" | jq '.[] | select(.name == "action") | .id'
2. File predictions:
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues" \
"$FORGE_API/issues" \
-d '{"title":"<title>","body":"<body>","labels":[<prediction_unreviewed_id>]}'
3. File action dispatches (if exploiting):
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$CODEBERG_API/issues" \
"$FORGE_API/issues" \
-d '{"title":"action: test prediction #NNN — <formula> <focus>","body":"<body>","labels":[<action_label_id>]}'
4. Do NOT duplicate existing open predictions. If your theory matches

View file

@ -28,7 +28,7 @@ runs directly from cron like the planner, predictor, and supervisor.
PR, reviewed alongside AGENTS.md changes, executed by gardener-run.sh after merge.
**Environment variables consumed**:
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by gardener-run.sh)
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`

View file

@ -105,7 +105,7 @@ If no file changes in commit-and-pr:
echo 'PHASE:done' > '${PHASE_FILE}'"
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
PROMPT="You are the issue gardener for ${CODEBERG_REPO}. Work through the formula below. Follow the phase protocol: if the commit-and-pr step creates a PR, write PHASE:awaiting_ci and wait for orchestrator CI/review/merge handling. If no file changes, write PHASE:done. The orchestrator will time you out if you return to the prompt without signalling.
PROMPT="You are the issue gardener for ${FORGE_REPO}. Work through the formula below. Follow the phase protocol: if the commit-and-pr step creates a PR, write PHASE:awaiting_ci and wait for orchestrator CI/review/merge handling. If no file changes, write PHASE:done. The orchestrator will time you out if you return to the prompt without signalling.
You have full shell access and --dangerously-skip-permissions.
Fix what you can. Escalate what you cannot. Do NOT ask permission — act first, report after.
@ -162,13 +162,13 @@ _gardener_execute_manifest() {
add_label)
local label label_id
label=$(jq -r ".[$i].label" "$manifest_file")
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/labels" | jq -r --arg n "$label" \
'.[] | select(.name == $n) | .id') || true
if [ -n "$label_id" ]; then
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
if curl -sf -X POST -H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/issues/${issue}/labels" \
"${FORGE_API}/issues/${issue}/labels" \
-d "{\"labels\":[${label_id}]}" >/dev/null 2>&1; then
log "manifest: add_label '${label}' to #${issue}"
else
@ -182,12 +182,12 @@ _gardener_execute_manifest() {
remove_label)
local label label_id
label=$(jq -r ".[$i].label" "$manifest_file")
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/labels" | jq -r --arg n "$label" \
'.[] | select(.name == $n) | .id') || true
if [ -n "$label_id" ]; then
if curl -sf -X DELETE -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1; then
if curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1; then
log "manifest: remove_label '${label}' from #${issue}"
else
log "manifest: FAILED remove_label '${label}' from #${issue}"
@ -200,9 +200,9 @@ _gardener_execute_manifest() {
close)
local reason
reason=$(jq -r ".[$i].reason // empty" "$manifest_file")
if curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
if curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/issues/${issue}" \
"${FORGE_API}/issues/${issue}" \
-d '{"state":"closed"}' >/dev/null 2>&1; then
log "manifest: closed #${issue} (${reason})"
else
@ -214,9 +214,9 @@ _gardener_execute_manifest() {
local body escaped_body
body=$(jq -r ".[$i].body" "$manifest_file")
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
if curl -sf -X POST -H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/issues/${issue}/comments" \
"${FORGE_API}/issues/${issue}/comments" \
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
log "manifest: commented on #${issue}"
else
@ -235,8 +235,8 @@ _gardener_execute_manifest() {
label_ids="[]"
if [ -n "$labels" ]; then
local all_labels ids_json=""
all_labels=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/labels") || true
all_labels=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/labels") || true
while IFS= read -r lname; do
local lid
lid=$(echo "$all_labels" | jq -r --arg n "$lname" \
@ -245,9 +245,9 @@ _gardener_execute_manifest() {
done <<< "$labels"
[ -n "$ids_json" ] && label_ids="[${ids_json}]"
fi
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
if curl -sf -X POST -H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/issues" \
"${FORGE_API}/issues" \
-d "{\"title\":${escaped_title},\"body\":${escaped_body},\"labels\":${label_ids}}" >/dev/null 2>&1; then
log "manifest: created issue '${title}'"
else
@ -259,9 +259,9 @@ _gardener_execute_manifest() {
local body escaped_body
body=$(jq -r ".[$i].body" "$manifest_file")
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
if curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
if curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/issues/${issue}" \
"${FORGE_API}/issues/${issue}" \
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
log "manifest: edited body of #${issue}"
else
@ -284,9 +284,9 @@ _gardener_execute_manifest() {
_gardener_merge() {
local merge_response merge_http_code
merge_response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/pulls/${_GARDENER_PR}/merge" \
"${FORGE_API}/pulls/${_GARDENER_PR}/merge" \
-d '{"Do":"merge","delete_branch_after_merge":true}') || true
merge_http_code=$(echo "$merge_response" | tail -1)
@ -300,8 +300,8 @@ _gardener_merge() {
# Already merged (race)?
if [ "$merge_http_code" = "405" ]; then
local pr_merged
pr_merged=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
pr_merged=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
if [ "$pr_merged" = "true" ]; then
log "gardener PR #${_GARDENER_PR} already merged"
_gardener_execute_manifest
@ -329,9 +329,9 @@ _gardener_timeout_cleanup() {
log "gardener merge-through timed out (${_GARDENER_MERGE_TIMEOUT}s) — closing PR"
if [ -n "$_GARDENER_PR" ]; then
curl -sf -X PATCH \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H 'Content-Type: application/json' \
"${CODEBERG_API}/pulls/${_GARDENER_PR}" \
"${FORGE_API}/pulls/${_GARDENER_PR}" \
-d '{"state":"closed"}' >/dev/null 2>&1 || true
fi
printf 'PHASE:failed\nReason: merge-through timeout (%ss)\n' \
@ -360,8 +360,8 @@ _gardener_handle_ci() {
fi
# Fallback: search for open gardener PRs
if [ -z "$_GARDENER_PR" ]; then
_GARDENER_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/pulls?state=open&limit=10" | \
_GARDENER_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/pulls?state=open&limit=10" | \
jq -r '[.[] | select(.head.ref | startswith("chore/gardener-"))] | .[0].number // empty') || true
fi
if [ -z "$_GARDENER_PR" ]; then
@ -395,8 +395,8 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
# Get HEAD SHA from PR
local head_sha
head_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
if [ -z "$head_sha" ]; then
log "WARNING: could not get HEAD SHA for PR #${_GARDENER_PR}"
@ -426,11 +426,11 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
fi
# Re-fetch HEAD in case Claude pushed new commits
head_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
ci_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/commits/${head_sha}/status" | jq -r '.state // "unknown"') || ci_state="unknown"
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/commits/${head_sha}/status" | jq -r '.state // "unknown"') || ci_state="unknown"
case "$ci_state" in
success|failure|error) ci_done=true; break ;;
@ -463,8 +463,8 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
# Get error details
local pipeline_num ci_error_log
pipeline_num=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/commits/${head_sha}/status" | \
pipeline_num=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/commits/${head_sha}/status" | \
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
ci_error_log=""
@ -518,10 +518,10 @@ _gardener_handle_review() {
# Check for review on current HEAD
local review_sha review_comment
review_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
review_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
review_comment=$(codeberg_api_all "/issues/${_GARDENER_PR}/comments" 2>/dev/null | \
review_comment=$(forge_api_all "/issues/${_GARDENER_PR}/comments" 2>/dev/null | \
jq -r --arg sha "${review_sha:-none}" \
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
@ -536,10 +536,10 @@ _gardener_handle_review() {
verdict=$(echo "$review_text" | grep -oP '\*\*(APPROVE|REQUEST_CHANGES|DISCUSS)\*\*' | head -1 | tr -d '*' || true)
# Check formal Codeberg reviews as fallback
# Check formal forge reviews as fallback
if [ -z "$verdict" ]; then
verdict=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/pulls/${_GARDENER_PR}/reviews" | \
verdict=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/pulls/${_GARDENER_PR}/reviews" | \
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
[ "$verdict" = "APPROVED" ] && verdict="APPROVE"
[[ "$verdict" != "REQUEST_CHANGES" && "$verdict" != "APPROVE" ]] && verdict=""
@ -576,8 +576,8 @@ Then stop and wait."
# Check if PR was merged or closed externally
local pr_json pr_state pr_merged
pr_json=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${CODEBERG_API}/pulls/${_GARDENER_PR}") || true
pr_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_API}/pulls/${_GARDENER_PR}") || true
pr_state=$(echo "$pr_json" | jq -r '.state // "unknown"')
pr_merged=$(echo "$pr_json" | jq -r '.merged // false')

View file

@ -6,10 +6,10 @@ sourced as needed.
| File | What it provides | Sourced by |
|---|---|---|
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`CODEBERG_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `codeberg_api()`, `codeberg_api_all()` (accepts optional second TOKEN parameter, defaults to `$CODEBERG_TOKEN`), `woodpecker_api()`, `wpdb()`, `matrix_send()`, `matrix_send_ctx()`. Auto-loads project TOML if `PROJECT_TOML` is set. | Every agent |
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`FORGE_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `forge_api()`, `forge_api_all()` (accepts optional second TOKEN parameter, defaults to `$FORGE_TOKEN`), `woodpecker_api()`, `wpdb()`, `matrix_send()`, `matrix_send_ctx()`. Auto-loads project TOML if `PROJECT_TOML` is set. | Every agent |
| `lib/ci-helpers.sh` | `ci_passed()` — returns 0 if CI state is "success" (or no CI configured). `ci_required_for_pr()` — returns 0 if PR has code files (CI required), 1 if non-code only (CI not required). `is_infra_step()` — returns 0 if a single CI step failure matches infra heuristics (clone/git exit 128, any exit 137, log timeout patterns). `classify_pipeline_failure()` — returns "infra \<reason>" if any failed Woodpecker step matches infra heuristics via `is_infra_step()`, else "code". `ensure_priority_label()` — looks up (or creates) the `priority` label and returns its ID; caches in `_PRIORITY_LABEL_ID`. | dev-poll, review-poll, review-pr, supervisor-poll |
| `lib/ci-debug.sh` | CLI tool for Woodpecker CI: `list`, `status`, `logs`, `failures` subcommands. Not sourced — run directly. | Humans / dev-agent (tool access) |
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `CODEBERG_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `FORGE_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
| `lib/parse-deps.sh` | Extracts dependency issue numbers from an issue body (stdin → stdout, one number per line). Matches `## Dependencies` / `## Depends on` / `## Blocked by` sections and inline `depends on #N` / `blocked by #N` patterns. Inline scan skips fenced code blocks to prevent false positives from code examples in issue bodies. Not sourced — executed via `bash lib/parse-deps.sh`. | dev-poll, supervisor-poll |
| `lib/matrix_listener.sh` | Long-poll Matrix sync daemon. Dispatches thread replies to the correct agent via tmux session injection (dev, action, vault, review) or well-known files (`/tmp/{agent}-escalation-reply` for supervisor/gardener). Handles all agent reply routing. Run as systemd service. | Standalone daemon |
| `lib/formula-session.sh` | `acquire_cron_lock()`, `check_memory()`, `load_formula()`, `build_context_block()`, `consume_escalation_reply()`, `start_formula_session()`, `formula_phase_callback()`, `build_prompt_footer()`, `run_formula_and_monitor(AGENT [TIMEOUT] [CALLBACK])` — shared helpers for formula-driven cron agents (lock, memory guard, formula loading, prompt assembly, tmux session, monitor loop, crash recovery). `formula_phase_callback()` handles `PHASE:escalate` (unified escalation path — kills the session; callers may follow up via Matrix). `run_formula_and_monitor` accepts an optional CALLBACK (default: `formula_phase_callback`) so callers can install custom merge-through or escalation handlers. | planner-run.sh, predictor-run.sh, gardener-run.sh, supervisor-run.sh, dev-agent.sh, action-agent.sh |

View file

@ -13,7 +13,7 @@ set -euo pipefail
source "$(dirname "$0")/../lib/env.sh"
# WOODPECKER_TOKEN loaded from .env via env.sh
REPO="${CODEBERG_REPO}"
REPO="${FORGE_REPO}"
API="${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}"
api() {

View file

@ -8,19 +8,19 @@ set -euo pipefail
# ensure_blocked_label_id — look up (or create) the "blocked" label, print its ID.
# Caches the result in _BLOCKED_LABEL_ID to avoid repeated API calls.
# Requires: CODEBERG_TOKEN, CODEBERG_API (from env.sh), codeberg_api()
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
ensure_blocked_label_id() {
if [ -n "${_BLOCKED_LABEL_ID:-}" ]; then
printf '%s' "$_BLOCKED_LABEL_ID"
return 0
fi
_BLOCKED_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
_BLOCKED_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "blocked") | .id' 2>/dev/null || true)
if [ -z "$_BLOCKED_LABEL_ID" ]; then
_BLOCKED_LABEL_ID=$(curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}/labels" \
"${FORGE_API}/labels" \
-d '{"name":"blocked","color":"#e11d48"}' 2>/dev/null \
| jq -r '.id // empty' 2>/dev/null || true)
fi
@ -29,19 +29,19 @@ ensure_blocked_label_id() {
# ensure_priority_label — look up (or create) the "priority" label, print its ID.
# Caches the result in _PRIORITY_LABEL_ID to avoid repeated API calls.
# Requires: CODEBERG_TOKEN, CODEBERG_API (from env.sh), codeberg_api()
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
ensure_priority_label() {
if [ -n "${_PRIORITY_LABEL_ID:-}" ]; then
printf '%s' "$_PRIORITY_LABEL_ID"
return 0
fi
_PRIORITY_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
_PRIORITY_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "priority") | .id' 2>/dev/null || true)
if [ -z "$_PRIORITY_LABEL_ID" ]; then
_PRIORITY_LABEL_ID=$(curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}/labels" \
"${FORGE_API}/labels" \
-d '{"name":"priority","color":"#f59e0b"}' 2>/dev/null \
| jq -r '.id // empty' 2>/dev/null || true)
fi
@ -68,7 +68,7 @@ diff_has_code_files() {
ci_required_for_pr() {
local pr_num="$1"
local files all_json
all_json=$(codeberg_api_all "/pulls/${pr_num}/files") || return 0
all_json=$(forge_api_all "/pulls/${pr_num}/files") || return 0
files=$(printf '%s' "$all_json" | jq -r '.[].filename' 2>/dev/null) || return 0
if [ -z "$files" ]; then
return 0 # empty file list — require CI as safety default
@ -113,7 +113,7 @@ ci_failed() {
is_infra_step() {
local sname="$1" ecode="$2" log_data="${3:-}"
# Clone/git step exit 128 → Codeberg connection failure / rate limit
# Clone/git step exit 128 → forge connection failure / rate limit
if { [[ "$sname" == *clone* ]] || [[ "$sname" == git* ]]; } && [ "$ecode" = "128" ]; then
echo "${sname} exit 128 (connection failure)"
return 0

View file

@ -24,17 +24,33 @@ if [ -n "${PROJECT_TOML:-}" ] && [ -f "$PROJECT_TOML" ]; then
source "${FACTORY_ROOT}/lib/load-project.sh" "$PROJECT_TOML"
fi
# Codeberg token: env var > ~/.netrc
if [ -z "${CODEBERG_TOKEN:-}" ]; then
CODEBERG_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
# Forge token: new FORGE_TOKEN > legacy CODEBERG_TOKEN > ~/.netrc
if [ -z "${FORGE_TOKEN:-}" ]; then
FORGE_TOKEN="${CODEBERG_TOKEN:-}"
fi
export CODEBERG_TOKEN
if [ -z "${FORGE_TOKEN:-}" ]; then
FORGE_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
fi
export FORGE_TOKEN
export CODEBERG_TOKEN="${FORGE_TOKEN}" # backwards compat
# Project config
export CODEBERG_REPO="${CODEBERG_REPO:-}"
export CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1/repos/${CODEBERG_REPO}}"
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
export PROJECT_NAME="${PROJECT_NAME:-${CODEBERG_REPO##*/}}"
# Review bot token: FORGE_REVIEW_TOKEN > legacy REVIEW_BOT_TOKEN
export FORGE_REVIEW_TOKEN="${FORGE_REVIEW_TOKEN:-${REVIEW_BOT_TOKEN:-}}"
export REVIEW_BOT_TOKEN="${FORGE_REVIEW_TOKEN}" # backwards compat
# Bot usernames filter: FORGE_BOT_USERNAMES > legacy CODEBERG_BOT_USERNAMES
export FORGE_BOT_USERNAMES="${FORGE_BOT_USERNAMES:-${CODEBERG_BOT_USERNAMES:-}}"
export CODEBERG_BOT_USERNAMES="${FORGE_BOT_USERNAMES}" # backwards compat
# Project config (FORGE_* preferred, CODEBERG_* fallback)
export FORGE_REPO="${FORGE_REPO:-${CODEBERG_REPO:-}}"
export CODEBERG_REPO="${FORGE_REPO}" # backwards compat
export FORGE_URL="${FORGE_URL:-http://localhost:3000}"
export FORGE_API="${FORGE_API:-${FORGE_URL}/api/v1/repos/${FORGE_REPO}}"
export FORGE_WEB="${FORGE_WEB:-${FORGE_URL}/${FORGE_REPO}}"
export CODEBERG_API="${FORGE_API}" # backwards compat
export CODEBERG_WEB="${FORGE_WEB}" # backwards compat
export PROJECT_NAME="${PROJECT_NAME:-${FORGE_REPO##*/}}"
export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}"
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
export WOODPECKER_REPO_ID="${WOODPECKER_REPO_ID:-}"
@ -46,23 +62,25 @@ log() {
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
}
# Codeberg API helper — usage: codeberg_api GET /issues?state=open
codeberg_api() {
# Forge API helper — usage: forge_api GET /issues?state=open
forge_api() {
local method="$1" path="$2"
shift 2
curl -sf -X "$method" \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}${path}" "$@"
"${FORGE_API}${path}" "$@"
}
# Backwards-compat alias
codeberg_api() { forge_api "$@"; }
# Paginate a Codeberg API GET endpoint and return all items as a merged JSON array.
# Usage: codeberg_api_all /path (no existing query params)
# codeberg_api_all /path?a=b (with existing params — appends &limit=50&page=N)
# codeberg_api_all /path TOKEN (optional second arg: token; defaults to $CODEBERG_TOKEN)
codeberg_api_all() {
# Paginate a Forge API GET endpoint and return all items as a merged JSON array.
# Usage: forge_api_all /path (no existing query params)
# forge_api_all /path?a=b (with existing params — appends &limit=50&page=N)
# forge_api_all /path TOKEN (optional second arg: token; defaults to $FORGE_TOKEN)
forge_api_all() {
local path_prefix="$1"
local CODEBERG_TOKEN="${2:-${CODEBERG_TOKEN}}"
local FORGE_TOKEN="${2:-${FORGE_TOKEN}}"
local sep page page_items count all_items="[]"
case "$path_prefix" in
*"?"*) sep="&" ;;
@ -70,7 +88,7 @@ codeberg_api_all() {
esac
page=1
while true; do
page_items=$(codeberg_api GET "${path_prefix}${sep}limit=50&page=${page}")
page_items=$(forge_api GET "${path_prefix}${sep}limit=50&page=${page}")
count=$(printf '%s' "$page_items" | jq 'length')
[ "$count" -eq 0 ] && break
all_items=$(printf '%s\n%s' "$all_items" "$page_items" | jq -s 'add')
@ -79,6 +97,8 @@ codeberg_api_all() {
done
printf '%s' "$all_items"
}
# Backwards-compat alias
codeberg_api_all() { forge_api_all "$@"; }
# Woodpecker API helper
woodpecker_api() {

View file

@ -2,7 +2,7 @@
# file-action-issue.sh — File an action issue for a formula run
#
# Usage: source this file, then call file_action_issue.
# Requires: codeberg_api() from lib/env.sh, jq, lib/secret-scan.sh
# Requires: forge_api() from lib/env.sh, jq, lib/secret-scan.sh
#
# file_action_issue <formula_name> <title> <body>
# Sets FILED_ISSUE_NUM on success.
@ -24,7 +24,7 @@ file_action_issue() {
# Dedup: skip if an open action issue for this formula already exists
local open_actions
open_actions=$(codeberg_api_all "/issues?state=open&type=issues&labels=action" 2>/dev/null || true)
open_actions=$(forge_api_all "/issues?state=open&type=issues&labels=action" 2>/dev/null || true)
if [ -n "$open_actions" ] && [ "$open_actions" != "null" ]; then
local existing
existing=$(printf '%s' "$open_actions" | \
@ -36,7 +36,7 @@ file_action_issue() {
# Fetch 'action' label ID
local action_label_id
action_label_id=$(codeberg_api GET "/labels" 2>/dev/null | \
action_label_id=$(forge_api GET "/labels" 2>/dev/null | \
jq -r '.[] | select(.name == "action") | .id' 2>/dev/null || true)
if [ -z "$action_label_id" ]; then
return 2
@ -50,7 +50,7 @@ file_action_issue() {
--argjson labels "[$action_label_id]" \
'{title: $title, body: $body, labels: $labels}')
result=$(codeberg_api POST "/issues" -d "$payload" 2>/dev/null || true)
result=$(forge_api POST "/issues" -d "$payload" 2>/dev/null || true)
FILED_ISSUE_NUM=$(printf '%s' "$result" | jq -r '.number // empty' 2>/dev/null || true)
if [ -z "$FILED_ISSUE_NUM" ]; then

View file

@ -208,21 +208,21 @@ read_scratch_context() {
# ── Prompt + monitor helpers ──────────────────────────────────────────────
# build_prompt_footer [EXTRA_API_LINES]
# Assembles the common Codeberg API reference + environment + phase protocol
# Assembles the common forge API reference + environment + phase protocol
# block for formula prompts. Sets PROMPT_FOOTER.
# Pass additional API endpoint lines (pre-formatted, newline-prefixed) via $1.
# Requires globals: CODEBERG_API, FACTORY_ROOT, PROJECT_REPO_ROOT,
# Requires globals: FORGE_API, FACTORY_ROOT, PROJECT_REPO_ROOT,
# PRIMARY_BRANCH, PHASE_FILE.
build_prompt_footer() {
local extra_api="${1:-}"
# shellcheck disable=SC2034 # consumed by the calling script's PROMPT
PROMPT_FOOTER="## Codeberg API reference
Base URL: ${CODEBERG_API}
Auth header: -H \"Authorization: token \$CODEBERG_TOKEN\"
Read issue: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body'
Create issue: curl -sf -X POST -H \"Authorization: token \$CODEBERG_TOKEN\" -H 'Content-Type: application/json' '${CODEBERG_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'${extra_api}
List labels: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels'
NEVER echo or include the actual token value in output — always reference \$CODEBERG_TOKEN.
PROMPT_FOOTER="## Forge API reference
Base URL: ${FORGE_API}
Auth header: -H \"Authorization: token \${FORGE_TOKEN}\"
Read issue: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/issues/{number}' | jq '.body'
Create issue: curl -sf -X POST -H \"Authorization: token \${FORGE_TOKEN}\" -H 'Content-Type: application/json' '${FORGE_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'${extra_api}
List labels: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/labels'
NEVER echo or include the actual token value in output — always reference \${FORGE_TOKEN}.
## Environment
FACTORY_ROOT=${FACTORY_ROOT}
@ -240,7 +240,7 @@ On unrecoverable error:
# run_formula_and_monitor AGENT_NAME [TIMEOUT]
# Starts the formula session, injects PROMPT, monitors phase, and logs result.
# Requires globals: SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT,
# CODEBERG_REPO, CLAUDE_MODEL (exported).
# FORGE_REPO, CLAUDE_MODEL (exported).
# shellcheck disable=SC2154 # SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT set by caller
run_formula_and_monitor() {
local agent_name="$1"
@ -258,7 +258,7 @@ run_formula_and_monitor() {
agent_inject_into_session "$SESSION_NAME" "$PROMPT"
log "Prompt sent to tmux session"
matrix_send "$agent_name" "${agent_name^} session started for ${CODEBERG_REPO}" 2>/dev/null || true
matrix_send "$agent_name" "${agent_name^} session started for ${FORGE_REPO}" 2>/dev/null || true
log "Monitoring phase file: ${PHASE_FILE}"
_FORMULA_CRASH_COUNT=0

View file

@ -5,7 +5,7 @@
# Blocks:
# - git push --force / -f to primary branch
# - rm -rf targeting paths outside the worktree
# - Direct Codeberg API merge calls (should go through phase protocol)
# - Direct forge API merge calls (should go through phase protocol)
# - Direct issue close calls (should go through phase protocol)
# - git checkout / git switch to primary branch (stay on feature branch)
# - FACTORY_ROOT access from worktrees (formula agents exempted)
@ -88,7 +88,7 @@ if [ -n "$worktree_path" ] \
fi
fi
# --- Guard 3: Direct Codeberg API merge calls ---
# --- Guard 3: Direct forge API merge calls ---
if printf '%s' "$command_str" | grep -qE '/pulls/[0-9]+/merge'; then
printf 'BLOCKED: Direct API merge calls must go through the phase protocol. Push your changes and write PHASE:awaiting_ci — the orchestrator handles merges.\n'
exit 2

View file

@ -5,9 +5,11 @@
# source lib/load-project.sh projects/harb.toml
#
# Exports:
# PROJECT_NAME, CODEBERG_REPO, CODEBERG_API, PROJECT_REPO_ROOT,
# PRIMARY_BRANCH, WOODPECKER_REPO_ID, PROJECT_CONTAINERS,
# CHECK_PRS, CHECK_DEV_AGENT, CHECK_PIPELINE_STALL, CI_STALE_MINUTES
# PROJECT_NAME, FORGE_REPO, FORGE_API, FORGE_WEB, FORGE_URL,
# PROJECT_REPO_ROOT, PRIMARY_BRANCH, WOODPECKER_REPO_ID,
# PROJECT_CONTAINERS, CHECK_PRS, CHECK_DEV_AGENT,
# CHECK_PIPELINE_STALL, CI_STALE_MINUTES
# (plus backwards-compat aliases: CODEBERG_REPO, CODEBERG_API, CODEBERG_WEB)
#
# If no argument given, does nothing (allows poll scripts to work with
# plain .env fallback for backwards compatibility).
@ -35,7 +37,8 @@ def emit(key, val):
# Top-level
emit('PROJECT_NAME', cfg.get('name', ''))
emit('CODEBERG_REPO', cfg.get('repo', ''))
emit('FORGE_REPO', cfg.get('repo', ''))
emit('FORGE_URL', cfg.get('forge_url', ''))
if 'repo_root' in cfg:
emit('PROJECT_REPO_ROOT', cfg['repo_root'])
@ -79,11 +82,17 @@ while IFS='=' read -r _key _val; do
export "$_key=$_val"
done <<< "$_PROJECT_VARS"
# Derive CODEBERG_API and CODEBERG_WEB if repo changed
if [ -n "$CODEBERG_REPO" ]; then
export CODEBERG_API="https://codeberg.org/api/v1/repos/${CODEBERG_REPO}"
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
# Derive FORGE_API and FORGE_WEB from forge_url + repo
# FORGE_URL: TOML forge_url > existing FORGE_URL > default
export FORGE_URL="${FORGE_URL:-http://localhost:3000}"
if [ -n "$FORGE_REPO" ]; then
export FORGE_API="${FORGE_URL}/api/v1/repos/${FORGE_REPO}"
export FORGE_WEB="${FORGE_URL}/${FORGE_REPO}"
fi
# Backwards-compat aliases
export CODEBERG_REPO="${FORGE_REPO}"
export CODEBERG_API="${FORGE_API:-}"
export CODEBERG_WEB="${FORGE_WEB:-}"
# Derive PROJECT_REPO_ROOT if not explicitly set
if [ -z "${PROJECT_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then

View file

@ -33,8 +33,9 @@ _SAFE_PATTERNS=(
# Git SHAs in typical git contexts (commit refs, not standalone secrets)
'commit [0-9a-f]{40}'
'Merge [0-9a-f]{40}'
# Codeberg/GitHub URLs with short hex (PR refs, commit links)
# Forge/GitHub URLs with short hex (PR refs, commit links)
'codeberg\.org/[^[:space:]]+'
'localhost:3000/[^[:space:]]+'
# ShellCheck directive codes
'SC[0-9]{4}'
)

View file

@ -61,6 +61,6 @@ prerequisite tree but NOT as issues. This prevents the "spray issues across
all milestones" pattern that produced premature work in planner v1/v2.
**Environment variables consumed**:
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to opus by planner-run.sh)
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`

View file

@ -80,13 +80,13 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# ── Build prompt ─────────────────────────────────────────────────────────
build_prompt_footer "
Relabel: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}'
Comment: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'
Close: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"state\":\"closed\"}'
Relabel: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X PUT -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}'
Comment: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X POST -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'
Close: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X PATCH -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}' -d '{\"state\":\"closed\"}'
"
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
PROMPT="You are the strategic planner for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
PROMPT="You are the strategic planner for ${FORGE_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
## Project context
${CONTEXT_BLOCK}${MEMORY_BLOCK}${JOURNAL_BLOCK}

View file

@ -28,7 +28,7 @@ memory check (skips if available RAM < 2000 MB).
**Key files**:
- `predictor/predictor-run.sh` — Cron wrapper + orchestrator: lock, memory guard,
sources disinto project config, builds prompt with formula + Codeberg API
sources disinto project config, builds prompt with formula + forge API
reference, creates tmux session (sonnet), monitors phase file, handles crash
recovery via `run_formula_and_monitor`
- `formulas/run-predictor.toml` — Execution spec: two steps (preflight,
@ -37,7 +37,7 @@ memory check (skips if available RAM < 2000 MB).
interactive session
**Environment variables consumed**:
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by predictor-run.sh)
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Notifications (optional)

View file

@ -55,7 +55,7 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
build_prompt_footer
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
PROMPT="You are the prediction agent (goblin) for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
PROMPT="You are the prediction agent (goblin) for ${FORGE_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
Your role: abstract adversary. Find the project's biggest weakness, challenge
planner claims, and generate evidence. Explore when uncertain (file a prediction),

View file

@ -1,10 +1,11 @@
# projects/disinto.toml.example — Template for disinto self-management
#
# Copy to projects/disinto.toml and fill in box-specific values,
# or run: disinto init https://codeberg.org/johba/disinto
# or run: disinto init johba/disinto
name = "disinto"
repo = "johba/disinto"
forge_url = "http://localhost:3000"
repo_root = "/home/YOU/dark-factory"
primary_branch = "main"

View file

@ -1,10 +1,11 @@
# projects/harb.toml.example — Template for johba/harb
#
# Copy to projects/harb.toml and fill in box-specific values,
# or run: disinto init https://codeberg.org/johba/harb
# or run: disinto init johba/harb
name = "harb"
repo = "johba/harb"
forge_url = "http://localhost:3000"
repo_root = "/home/YOU/harb"
primary_branch = "master"

View file

@ -1,10 +1,11 @@
# projects/versi.toml.example — Template for johba/versi
#
# Copy to projects/versi.toml and fill in box-specific values,
# or run: disinto init https://codeberg.org/johba/versi
# or run: disinto init johba/versi
name = "versi"
repo = "johba/versi"
forge_url = "http://localhost:3000"
repo_root = "/home/YOU/versi"
primary_branch = "main"

View file

@ -2,7 +2,7 @@
# Review Agent
**Role**: AI-powered PR review — post structured findings and formal
approve/request-changes verdicts to Codeberg.
approve/request-changes verdicts to forge.
**Trigger**: `review-poll.sh` runs every 10 min via cron. It scans open PRs
whose CI has passed and that lack a review for the current HEAD SHA, then
@ -10,11 +10,11 @@ spawns `review-pr.sh <pr-number>`.
**Key files**:
- `review/review-poll.sh` — Cron scheduler: finds unreviewed PRs with passing CI
- `review/review-pr.sh` — Creates/reuses a tmux session (`review-{project}-{pr}`), injects PR diff, waits for Claude to write structured JSON output, posts markdown review + formal Codeberg review, auto-creates follow-up issues for pre-existing tech debt
- `review/review-pr.sh` — Creates/reuses a tmux session (`review-{project}-{pr}`), injects PR diff, waits for Claude to write structured JSON output, posts markdown review + formal forge review, auto-creates follow-up issues for pre-existing tech debt
**Environment variables consumed**:
- `CODEBERG_TOKEN` — Dev-agent token (must not be the same account as REVIEW_BOT_TOKEN)
- `REVIEW_BOT_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist)
- `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `FORGE_TOKEN` — Dev-agent token (must not be the same account as FORGE_REVIEW_TOKEN)
- `FORGE_REVIEW_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist)
- `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `PRIMARY_BRANCH`, `WOODPECKER_REPO_ID`
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`

View file

@ -15,7 +15,7 @@ source "$(dirname "$0")/../lib/ci-helpers.sh"
REPO_ROOT="${PROJECT_REPO_ROOT}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
API_BASE="${CODEBERG_API}"
API_BASE="${FORGE_API}"
LOGFILE="$SCRIPT_DIR/review.log"
MAX_REVIEWS=3
REVIEW_IDLE_TIMEOUT=14400 # 4h: kill review session if idle
@ -44,7 +44,7 @@ if [ -n "$REVIEW_SESSIONS" ]; then
phase_file="/tmp/review-session-${PROJECT_NAME}-${pr_num}.phase"
# Check if PR is still open
pr_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
pr_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/pulls/${pr_num}" | jq -r '.state // "unknown"' 2>/dev/null) || true
if [ "$pr_state" != "open" ]; then
@ -92,7 +92,7 @@ if [ -n "$REVIEW_SESSIONS" ]; then
done <<< "$REVIEW_SESSIONS"
fi
PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/pulls?state=open&limit=20" | \
jq -r --arg branch "${PRIMARY_BRANCH}" '.[] | select(.base.ref == $branch) | select(.draft != true) | select(.title | test("^\\[?WIP[\\]:]"; "i") | not) | "\(.number) \(.head.sha) \(.head.ref)"')
@ -124,7 +124,7 @@ inject_review_into_dev_session() {
[ "${current_phase}" = "PHASE:awaiting_review" ] || return 0
local review_comment
review_comment=$(codeberg_api_all "/issues/${pr_num}/comments" | \
review_comment=$(forge_api_all "/issues/${pr_num}/comments" | \
jq -r --arg sha "${pr_sha}" \
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
if [ -z "${review_comment}" ] || [ "${review_comment}" = "null" ]; then
@ -185,7 +185,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
reviewed_sha=$(sed -n 's/^SHA://p' "$phase_file" 2>/dev/null | tr -d '[:space:]' || true)
[ -n "$reviewed_sha" ] || continue
pr_json=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
pr_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/pulls/${pr_num}" 2>/dev/null || true)
[ -n "$pr_json" ] || continue
@ -196,7 +196,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
pr_branch=$(printf '%s' "$pr_json" | jq -r '.head.ref // ""')
if [ -z "$current_sha" ] || [ "$current_sha" = "$reviewed_sha" ]; then continue; fi
ci_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/commits/${current_sha}/status" | jq -r '.state // "unknown"')
if ! ci_passed "$ci_state"; then
@ -210,7 +210,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
if "${SCRIPT_DIR}/review-pr.sh" "$pr_num" 2>&1; then
REVIEWED=$((REVIEWED + 1))
FRESH_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
FRESH_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/pulls/${pr_num}" | jq -r '.head.sha // ""') || true
inject_review_into_dev_session "$pr_num" "${FRESH_SHA:-$current_sha}" "$pr_branch"
else
@ -227,7 +227,7 @@ while IFS= read -r line; do
PR_SHA=$(echo "$line" | awk '{print $2}')
PR_BRANCH=$(echo "$line" | awk '{print $3}')
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
# Skip if CI is running/failed. Allow "success", no CI configured, or non-code PRs
@ -240,8 +240,8 @@ while IFS= read -r line; do
log " #${PR_NUM} CI=${CI_STATE} but no code files — proceeding"
fi
# Check formal Codeberg reviews (not comment markers)
HAS_REVIEW=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
# Check formal forge reviews (not comment markers)
HAS_REVIEW=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/pulls/${PR_NUM}/reviews" | \
jq -r --arg sha "$PR_SHA" \
'[.[] | select(.commit_id == $sha) | select(.state != "COMMENT")] | length')
@ -259,7 +259,7 @@ while IFS= read -r line; do
# Re-fetch current SHA: review-pr.sh fetches the PR independently and tags its
# comment with whatever SHA it saw. If a commit arrived while review-pr.sh was
# running those two SHA captures diverge and we would miss the comment.
FRESH_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
FRESH_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API_BASE}/pulls/${PR_NUM}" | jq -r '.head.sha // ""') || true
inject_review_into_dev_session "$PR_NUM" "${FRESH_SHA:-$PR_SHA}" "$PR_BRANCH"
else

View file

@ -10,7 +10,7 @@ git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true
PR_NUMBER="${1:?Usage: review-pr.sh <pr-number> [--force]}"
FORCE="${2:-}"
API="${CODEBERG_API}"
API="${FORGE_API}"
LOGFILE="${FACTORY_ROOT}/review/review.log"
SESSION="review-${PROJECT_NAME}-${PR_NUMBER}"
PHASE_FILE="/tmp/review-session-${PROJECT_NAME}-${PR_NUMBER}.phase"
@ -37,7 +37,7 @@ if [ -f "$LOCKFILE" ]; then
fi
echo $$ > "$LOCKFILE"
status "fetching metadata"
PR_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" "${API}/pulls/${PR_NUMBER}")
PR_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" "${API}/pulls/${PR_NUMBER}")
PR_TITLE=$(printf '%s' "$PR_JSON" | jq -r '.title')
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')
PR_HEAD=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
@ -50,16 +50,16 @@ if [ "$PR_STATE" != "open" ]; then
cd "${PROJECT_REPO_ROOT}"; git worktree remove "$WORKTREE" --force 2>/dev/null || true
rm -rf "$WORKTREE" "$PHASE_FILE" "$OUTPUT_FILE" 2>/dev/null || true; exit 0
fi
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
CI_NOTE=""; if ! ci_passed "$CI_STATE"; then
ci_required_for_pr "$PR_NUMBER" && { log "SKIP: CI=${CI_STATE}"; exit 0; }
CI_NOTE=" (not required — non-code PR)"; fi
ALL_COMMENTS=$(codeberg_api_all "/issues/${PR_NUMBER}/comments")
ALL_COMMENTS=$(forge_api_all "/issues/${PR_NUMBER}/comments")
HAS_CMT=$(printf '%s' "$ALL_COMMENTS" | jq --arg s "$PR_SHA" \
'[.[]|select(.body|contains("<!-- reviewed: "+$s+" -->"))]|length')
[ "${HAS_CMT:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: reviewed ${PR_SHA:0:7}"; exit 0; }
HAS_FML=$(codeberg_api_all "/pulls/${PR_NUMBER}/reviews" | jq --arg s "$PR_SHA" \
HAS_FML=$(forge_api_all "/pulls/${PR_NUMBER}/reviews" | jq --arg s "$PR_SHA" \
'[.[]|select(.commit_id==$s)|select(.state!="COMMENT")]|length')
[ "${HAS_FML:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: formal review"; exit 0; }
PREV_CONTEXT="" IS_RE_REVIEW=false PREV_SHA=""
@ -81,7 +81,7 @@ if [ -n "$PREV_REV" ] && [ "$PREV_REV" != "null" ]; then
fi
fi
status "fetching diff"
curl -s -H "Authorization: token ${CODEBERG_TOKEN}" \
curl -s -H "Authorization: token ${FORGE_TOKEN}" \
"${API}/pulls/${PR_NUMBER}.diff" > "${REVIEW_TMPDIR}/full.diff"
FSIZE=$(stat -c%s "${REVIEW_TMPDIR}/full.diff" 2>/dev/null || echo 0)
DIFF=$(head -c "$MAX_DIFF" "${REVIEW_TMPDIR}/full.diff")
@ -97,15 +97,15 @@ status "preparing review session"
FORMULA=$(cat "${FACTORY_ROOT}/formulas/review-pr.toml")
{
printf 'You are the review agent for %s. Follow the formula to review PR #%s.\nYou MUST write PHASE:done to '\''%s'\'' when finished.\n\n' \
"${CODEBERG_REPO}" "${PR_NUMBER}" "${PHASE_FILE}"
"${FORGE_REPO}" "${PR_NUMBER}" "${PHASE_FILE}"
printf '## PR Context\n**%s** (%s → %s) | SHA: %s | CI: %s%s\nRe-review: %s\n\n' \
"$PR_TITLE" "$PR_HEAD" "$PR_BASE" "$PR_SHA" "$CI_STATE" "$CI_NOTE" "$IS_RE_REVIEW"
printf '### Description\n%s\n\n### Changed Files\n%s\n\n### Diff%s\n```diff\n%s\n```\n' \
"$PR_BODY" "$FILES" "$DNOTE" "$DIFF"
[ -n "$PREV_CONTEXT" ] && printf '%s\n' "$PREV_CONTEXT"
printf '\n## Formula\n%s\n\n## Environment\nREVIEW_OUTPUT_FILE=%s\nPHASE_FILE=%s\nCODEBERG_API=%s\nPR_NUMBER=%s\nFACTORY_ROOT=%s\n' \
printf '\n## Formula\n%s\n\n## Environment\nREVIEW_OUTPUT_FILE=%s\nPHASE_FILE=%s\nFORGE_API=%s\nPR_NUMBER=%s\nFACTORY_ROOT=%s\n' \
"$FORMULA" "$OUTPUT_FILE" "$PHASE_FILE" "$API" "$PR_NUMBER" "$FACTORY_ROOT"
printf 'NEVER echo the actual token — always reference $CODEBERG_TOKEN or $REVIEW_BOT_TOKEN.\n'
printf 'NEVER echo the actual token — always reference ${FORGE_TOKEN} or ${FORGE_REVIEW_TOKEN}.\n'
} > "${REVIEW_TMPDIR}/prompt.md"
PROMPT=$(cat "${REVIEW_TMPDIR}/prompt.md")
@ -142,7 +142,7 @@ fi
if [ -z "$REVIEW_JSON" ]; then
log "ERROR: no valid review output"
jq -n --arg b "## AI Review — Error\n<!-- review-error: ${PR_SHA} -->\nReview failed.\n---\n*${PR_SHA:0:7}*" \
'{body: $b}' | curl -sf -o /dev/null -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
'{body: $b}' | curl -sf -o /dev/null -X POST -H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" "${API}/issues/${PR_NUMBER}/comments" -d @- || true
matrix_send "review" "PR #${PR_NUMBER} review failed" 2>/dev/null || true; exit 1
fi
@ -163,7 +163,7 @@ COMMENT_BODY=$(printf '## AI %s\n<!-- reviewed: %s -->\n\n%s\n\n### Verdict\n**%
printf '%s' "$COMMENT_BODY" > "${REVIEW_TMPDIR}/body.txt"
jq -Rs '{body: .}' < "${REVIEW_TMPDIR}/body.txt" > "${REVIEW_TMPDIR}/comment.json"
POST_RC=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${REVIEW_BOT_TOKEN}" -H "Content-Type: application/json" \
-H "Authorization: token ${FORGE_REVIEW_TOKEN}" -H "Content-Type: application/json" \
"${API}/issues/${PR_NUMBER}/comments" --data-binary @"${REVIEW_TMPDIR}/comment.json")
[ "$POST_RC" != "201" ] && { log "ERROR: comment HTTP ${POST_RC}"; exit 1; }
log "posted review comment"
@ -171,19 +171,19 @@ log "posted review comment"
REVENT="COMMENT"
case "$VERDICT" in APPROVE) REVENT="APPROVED" ;; REQUEST_CHANGES|DISCUSS) REVENT="REQUEST_CHANGES" ;; esac
if [ "$REVENT" = "APPROVED" ]; then
BLOGIN=$(curl -sf -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
BLOGIN=$(curl -sf -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
"${API%%/repos*}/user" 2>/dev/null | jq -r '.login // empty' || true)
[ -n "$BLOGIN" ] && codeberg_api_all "/pulls/${PR_NUMBER}/reviews" "$REVIEW_BOT_TOKEN" 2>/dev/null | \
[ -n "$BLOGIN" ] && forge_api_all "/pulls/${PR_NUMBER}/reviews" "${FORGE_REVIEW_TOKEN}" 2>/dev/null | \
jq -r --arg l "$BLOGIN" '.[]|select(.state=="REQUEST_CHANGES")|select(.user.login==$l)|.id' | \
while IFS= read -r rid; do
curl -sf -o /dev/null -X POST -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
curl -sf -o /dev/null -X POST -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews/${rid}/dismissals" \
-d '{"message":"Superseded by approval"}' || true; log "dismissed review ${rid}"
done || true
fi
jq -n --arg b "AI ${RTYPE}: **${VERDICT}** — ${REASON}" --arg e "$REVENT" --arg s "$PR_SHA" \
'{body: $b, event: $e, commit_id: $s}' > "${REVIEW_TMPDIR}/formal.json"
curl -s -o /dev/null -X POST -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
curl -s -o /dev/null -X POST -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews" \
--data-binary @"${REVIEW_TMPDIR}/formal.json" >/dev/null 2>&1 || true
log "formal ${REVENT} submitted"

View file

@ -2,7 +2,7 @@
# =============================================================================
# collect-metrics.sh — Collect factory metrics and write JSON for the dashboard
#
# Queries Codeberg API for PR/issue stats across all managed projects,
# Queries forge API for PR/issue stats across all managed projects,
# counts vault decisions, and checks CI pass rates. Writes a JSON snapshot
# to the live site directory so the dashboard can fetch it.
#
@ -47,12 +47,15 @@ collect_project_metrics() {
repo=$(grep '^repo ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//')
repo_name=$(grep '^name ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//')
local api_base="https://codeberg.org/api/v1/repos/${repo}"
local forge_url
forge_url=$(grep '^forge_url ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//') 2>/dev/null || true
forge_url="${forge_url:-${FORGE_URL:-http://localhost:3000}}"
local api_base="${forge_url}/api/v1/repos/${repo}"
# PRs merged (all time via state=closed + merged marker)
local prs_merged_week=0 prs_merged_month=0 prs_merged_total=0
local closed_prs
closed_prs=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
closed_prs=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/pulls?state=closed&sort=updated&limit=50" 2>/dev/null || echo "[]")
prs_merged_total=$(printf '%s' "$closed_prs" | jq '[.[] | select(.merged)] | length' 2>/dev/null || echo 0)
@ -69,7 +72,7 @@ collect_project_metrics() {
# Issues closed
local issues_closed_week=0 issues_closed_month=0
local closed_issues
closed_issues=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
closed_issues=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=closed&sort=updated&type=issues&limit=50" 2>/dev/null || echo "[]")
if [ -n "$WEEK_AGO" ]; then
@ -82,19 +85,19 @@ collect_project_metrics() {
fi
local total_closed_header
total_closed_header=$(curl -sf -I -H "Authorization: token ${CODEBERG_TOKEN}" \
total_closed_header=$(curl -sf -I -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=closed&type=issues&limit=1" 2>/dev/null | grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}' || echo "0")
local issues_closed_total="${total_closed_header:-0}"
# Open issues by label
local backlog_count in_progress_count blocked_count
backlog_count=$(curl -sf -I -H "Authorization: token ${CODEBERG_TOKEN}" \
backlog_count=$(curl -sf -I -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=open&labels=backlog&type=issues&limit=1" 2>/dev/null | \
grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}' || echo "0")
in_progress_count=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
in_progress_count=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=open&labels=in-progress&type=issues&limit=50" 2>/dev/null | \
jq 'length' 2>/dev/null || echo 0)
blocked_count=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
blocked_count=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null | \
jq 'length' 2>/dev/null || echo 0)

View file

@ -355,7 +355,7 @@
<div class="footer">
<div>Data refreshed every 6 hours by the metrics collector.</div>
<div style="margin-top:0.5rem">
<a href="https://codeberg.org/johba/disinto">source</a>
<a href="http://localhost:3000/johba/disinto">source</a>
</div>
</div>
@ -421,7 +421,7 @@
var card = el('div', 'project');
var nameDiv = el('div', 'name');
var nameLink = document.createElement('a');
nameLink.href = 'https://codeberg.org/' + p.repo;
nameLink.href = 'http://localhost:3000/' + p.repo;
nameLink.textContent = p.name;
nameDiv.appendChild(nameLink);
card.appendChild(nameDiv);

View file

@ -365,7 +365,7 @@
<!-- Eight Agents -->
<div class="section">
<h2>Eight agents</h2>
<p>Each agent has a single responsibility. They communicate through git, the Codeberg API, and the filesystem.</p>
<p>Each agent has a single responsibility. They communicate through git, the forge API, and the filesystem.</p>
<div class="agents-grid">
<div class="agent-card">
<div class="name">dev-agent</div>
@ -480,7 +480,7 @@
<p><strong>Bash scripts</strong> &mdash; every agent is a shell script. No compiled binaries, no runtimes to install.</p>
<p><strong>Claude CLI</strong> &mdash; AI is invoked via <code>claude -p</code> (one-shot) or <code>claude</code> (persistent tmux sessions).</p>
<p><strong>Cron</strong> &mdash; agents are triggered by cron jobs, not a daemon. Pull-based, not push-based.</p>
<p><strong>Codeberg + Woodpecker</strong> &mdash; git hosting and CI. All state lives in git and the issue tracker. No external databases.</p>
<p><strong>Forgejo + Woodpecker</strong> &mdash; git hosting and CI. All state lives in git and the issue tracker. No external databases.</p>
<p><strong>Single VPS</strong> &mdash; runs on an 8 GB server. Flat cost, no scaling surprises.</p>
</div>
</div>
@ -538,7 +538,7 @@ disinto/
<div class="footer">
<a href="/">&larr; disinto.ai</a> &middot;
<a href="/docs/quickstart">Quickstart</a> &middot;
<a href="https://codeberg.org/johba/disinto">Source</a>
<a href="http://localhost:3000/johba/disinto">Source</a>
</div>
</div>

View file

@ -328,8 +328,8 @@
<div class="label">Prerequisites</div>
<ul>
<li><strong>A VPS or server</strong> &mdash; 8 GB RAM minimum (Ubuntu/Debian recommended)</li>
<li><strong>A Codeberg account</strong> &mdash; with a repo and at least one issue</li>
<li><strong>A second Codeberg account</strong> &mdash; for the review bot (branch protection requires a different reviewer)</li>
<li><strong>A forge instance</strong> &mdash; with a repo and at least one issue</li>
<li><strong>A second forge instance</strong> &mdash; for the review bot (branch protection requires a different reviewer)</li>
<li><strong>Woodpecker CI</strong> &mdash; running and connected to your repo</li>
<li><strong>An Anthropic API key</strong> &mdash; with the <code>claude</code> CLI installed and authenticated</li>
<li><strong>tmux</strong> &mdash; for persistent dev sessions</li>
@ -343,13 +343,13 @@
Clone the factory
</div>
<p>Clone disinto onto your server. This is the factory &mdash; the code that runs your agents.</p>
<pre><code>git clone https://codeberg.org/johba/disinto.git ~/disinto
<pre><code>git clone http://localhost:3000/johba/disinto.git ~/disinto
cd ~/disinto
cp .env.example .env</code></pre>
<p>Edit <code>.env</code> with your tokens:</p>
<pre><code><span class="comment"># Required</span>
CODEBERG_TOKEN=your_codeberg_token
REVIEW_BOT_TOKEN=your_review_bot_token
FORGE_TOKEN=your_codeberg_token
FORGE_REVIEW_TOKEN=your_review_bot_token
<span class="comment"># Woodpecker CI</span>
WOODPECKER_TOKEN=your_woodpecker_token
@ -365,14 +365,14 @@ CLAUDE_TIMEOUT=7200</code></pre>
<span class="step-num">2</span>
Initialize your project
</div>
<p><code>disinto init</code> sets up everything: clones the repo, creates the project config, adds Codeberg labels, and installs cron jobs.</p>
<pre><code>bin/disinto init https://codeberg.org/you/your-project</code></pre>
<p><code>disinto init</code> provisions a local Forgejo instance, clones the repo, creates the project config, adds labels, and installs cron jobs.</p>
<pre><code>bin/disinto init http://localhost:3000/you/your-project</code></pre>
<div class="expected">
<div class="label">Expected output</div>
<code>=== disinto init ===
Project: you/your-project
Name: your-project
Cloning: https://codeberg.org/you/your-project.git -> /home/you/your-project
Cloning: http://localhost:3000/you/your-project.git -> /home/you/your-project
Branch: main
Created: /home/you/disinto/projects/your-project.toml
Creating labels on you/your-project...
@ -406,7 +406,7 @@ Done. Project your-project is ready.</code>
<ol>
<li><strong>A CI pipeline</strong> &mdash; at least one <code>.woodpecker/*.yml</code> file. Agents wait for CI before reviewing or merging.</li>
<li><strong>A CLAUDE.md</strong> &mdash; project context that agents read before every task. Describe your tech stack, how to build/test, coding conventions, and directory layout.</li>
<li><strong>Branch protection</strong> &mdash; on Codeberg, require PR reviews and add the review bot as a write collaborator.</li>
<li><strong>Branch protection</strong> &mdash; on Forgejo, require PR reviews and add the review bot as a write collaborator.</li>
</ol>
<pre><code><span class="comment"># Create CLAUDE.md in your project</span>
cat > ~/your-project/CLAUDE.md &lt;&lt;'EOF'
@ -434,7 +434,7 @@ git push</code></pre>
<span class="step-num">4</span>
File your first issue
</div>
<p>Create an issue on Codeberg with the <code>backlog</code> label. Be specific &mdash; the dev-agent works best with clear acceptance criteria.</p>
<p>Create an issue on the forge with the <code>backlog</code> label. Be specific &mdash; the dev-agent works best with clear acceptance criteria.</p>
<pre><code><span class="comment"># Title: Add health check endpoint</span>
<span class="comment"># Label: backlog</span>
<span class="comment"># Body:</span>
@ -523,7 +523,7 @@ git log --oneline -5</code></pre>
<div class="footer">
<a href="/">&larr; disinto.ai</a> &middot;
<a href="/docs/architecture">Architecture</a> &middot;
<a href="https://codeberg.org/johba/disinto">Source</a>
<a href="http://localhost:3000/johba/disinto">Source</a>
</div>
</div>

View file

@ -661,7 +661,7 @@
<p>
<strong>Bash scripts and Claude.</strong> No Kubernetes, no microservices,
no SaaS dependencies. Runs on an 8GB VPS.
Point it at a <strong>Codeberg repo</strong> with a
Point it at a <strong>forge repo</strong> with a
<strong>Woodpecker CI</strong> pipeline and it starts building.
</p>
<p>
@ -691,8 +691,8 @@
<div class="cta-links">
<a href="/docs/quickstart">Quickstart</a>
<a href="/docs/architecture">Architecture</a>
<a href="https://codeberg.org/johba/disinto">Browse the source</a>
<a href="https://codeberg.org/johba/disinto/issues">See active issues</a>
<a href="http://localhost:3000/johba/disinto">Browse the source</a>
<a href="http://localhost:3000/johba/disinto/issues">See active issues</a>
<a href="/dashboard">Live dashboard</a>
</div>
</div>
@ -702,7 +702,7 @@
<div class="links">
<a href="/docs/quickstart">quickstart</a>
<a href="/docs/architecture">architecture</a>
<a href="https://codeberg.org/johba/disinto">source</a>
<a href="http://localhost:3000/johba/disinto">source</a>
<a href="/dashboard">dashboard</a>
</div>
<div class="under-hood">

View file

@ -42,7 +42,7 @@ Matrix listener routes thread replies to `/tmp/supervisor-escalation-reply`,
which `supervisor-run.sh` consumes atomically on each run.
**Environment variables consumed**:
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by supervisor-run.sh)
- `WOODPECKER_TOKEN`, `WOODPECKER_SERVER`, `WOODPECKER_DB_PASSWORD`, `WOODPECKER_DB_USER`, `WOODPECKER_DB_HOST`, `WOODPECKER_DB_NAME` — CI database queries
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Matrix notifications + human input

View file

@ -1,6 +1,6 @@
# Supervisor Agent
You are the supervisor agent for `$CODEBERG_REPO`. You were called because
You are the supervisor agent for `$FORGE_REPO`. You were called because
`supervisor-poll.sh` detected an issue it couldn't auto-fix.
## Priority Order
@ -19,7 +19,7 @@ Before acting, read the relevant best-practices file:
- Memory issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/memory.md`
- Disk issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/disk.md`
- CI issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/ci.md`
- Codeberg / rate limits → `cat ${FACTORY_ROOT}/supervisor/best-practices/codeberg.md`
- forge / rate limits → `cat ${FACTORY_ROOT}/supervisor/best-practices/forge.md`
- Dev-agent issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/dev-agent.md`
- Review-agent issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/review-agent.md`
- Git issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/git.md`
@ -32,10 +32,10 @@ source ${FACTORY_ROOT}/lib/env.sh
```
This gives you:
- `codeberg_api GET "/pulls?state=open"` — Codeberg API (uses $CODEBERG_TOKEN)
- `forge_api GET "/pulls?state=open"` — forge API (uses $FORGE_TOKEN)
- `wpdb -c "SELECT ..."` — Woodpecker Postgres (uses $WOODPECKER_DB_PASSWORD)
- `woodpecker_api "/repos/$WOODPECKER_REPO_ID/pipelines"` — Woodpecker REST API (uses $WOODPECKER_TOKEN)
- `$REVIEW_BOT_TOKEN` — for posting reviews as the review_bot account
- `$FORGE_REVIEW_TOKEN` — for posting reviews as the review_bot account
- `$PROJECT_REPO_ROOT` — path to the target project repo
- `$PROJECT_NAME` — short project name (for worktree prefixes, container names)
- `$PRIMARY_BRANCH` — main branch (master or main)
@ -48,20 +48,20 @@ This gives you:
When you see "Circular dependency deadlock: #A -> #B -> #A", the backlog is permanently
stuck. Your job: figure out the correct dependency direction and fix the wrong one.
1. Read both issue bodies: `codeberg_api GET "/issues/A"`, `codeberg_api GET "/issues/B"`
1. Read both issue bodies: `forge_api GET "/issues/A"`, `forge_api GET "/issues/B"`
2. Read the referenced source files in `$PROJECT_REPO_ROOT` to understand which change
actually depends on which
3. Edit the issue that has the incorrect dep to remove the `#NNN` reference from its
`## Dependencies` section (replace with `- None` if it was the only dep)
4. If the correct direction is unclear from code, escalate with both issue summaries
Use the Codeberg API to edit issue bodies:
Use the forge API to edit issue bodies:
```bash
# Read current body
BODY=$(codeberg_api GET "/issues/NNN" | jq -r '.body')
BODY=$(forge_api GET "/issues/NNN" | jq -r '.body')
# Edit (remove the circular ref, keep other deps)
NEW_BODY=$(echo "$BODY" | sed 's/- #XXX/- None/')
codeberg_api PATCH "/issues/NNN" -d "$(jq -nc --arg b "$NEW_BODY" '{body:$b}')"
forge_api PATCH "/issues/NNN" -d "$(jq -nc --arg b "$NEW_BODY" '{body:$b}')"
```
### Stale dependencies (P3)

View file

@ -26,13 +26,13 @@
- Modifying pipeline configs in `.woodpecker/` directory
## Known Issues
- Codeberg rate-limits SSH clones. `git` step fails with exit 128. Retrigger usually works.
- forge rate-limits SSH clones. `git` step fails with exit 128. Retrigger usually works.
- `log_entries` table grows fast (was 5.6GB once). Truncate periodically.
- Example (harb): Running CI + harb stack = 14+ containers on 8GB. Memory pressure is real.
- CI images take hours to rebuild. Never run `docker system prune -a`.
## Lessons Learned
- Exit code 128 on git step = Codeberg rate limit, not a code problem. Retrigger.
- Exit code 128 on git step = forge rate limit, not a code problem. Retrigger.
- Exit code 137 = OOM kill. Check memory, kill stale processes, retrigger.
- `node-quality` step fails on eslint/typescript errors — these need code fixes, not CI fixes.

View file

@ -14,7 +14,7 @@
- Clean worktree: `cd $PROJECT_REPO_ROOT && git worktree remove /tmp/${PROJECT_NAME}-worktree-<N> --force`
- Remove `in-progress` label if agent died without cleanup:
```bash
codeberg_api DELETE "/issues/<N>/labels/in-progress"
forge_api DELETE "/issues/<N>/labels/in-progress"
```
## Dangerous (escalate)
@ -41,7 +41,7 @@
**Trust closed state.** If a dependency issue is closed, the code is on the primary branch. Period.
DO NOT try to find the specific PR that closed an issue. This is over-engineering that causes false negatives:
- Codeberg shares issue/PR numbering — no guaranteed relationship
- forge shares issue/PR numbering — no guaranteed relationship
- PRs don't always mention the issue number in title/body
- Searching last N closed PRs misses older merges
- The dev-agent closes issues after merging, so closed = merged
@ -52,7 +52,7 @@ The only check needed: `issue.state == "closed"`.
The supervisor-poll alert 'status unchanged for Nmin' is a false positive for complex implementation tasks. The status is set to 'claude assessing + implementing' at the START of the `timeout 7200 claude -p ...` call and only updates after Claude finishes. Normal complex tasks (multi-file Solidity changes + forge test) take 45-90 minutes. To distinguish a false positive from a real stuck agent: check that the claude PID is alive (`ps -p <PID>`), consuming CPU (>0%), and has active threads (`pstree -p <PID>`). If the process is alive and using CPU, do NOT restart it — this wastes completed work.
### False Positive: 'Waiting for CI + Review' Alert
The 'status unchanged for Nmin' alert is also a false positive when status is 'waiting for CI + review on PR #N (round R)'. This is an intentional sleep/poll loop — the agent is waiting for CI to pass and then for review-poll to post a review. CI can take 2040 minutes; review follows. Do NOT restart the agent. Confirm by checking: (1) agent PID is alive, (2) CI commit status via `codeberg_api GET /commits/<sha>/status`, (3) review-poll log shows it will pick up the PR on next cycle.
The 'status unchanged for Nmin' alert is also a false positive when status is 'waiting for CI + review on PR #N (round R)'. This is an intentional sleep/poll loop — the agent is waiting for CI to pass and then for review-poll to post a review. CI can take 2040 minutes; review follows. Do NOT restart the agent. Confirm by checking: (1) agent PID is alive, (2) CI commit status via `forge_api GET /commits/<sha>/status`, (3) review-poll log shows it will pick up the PR on next cycle.
### False Positive: Shared Status File Causes Giant Age (29M+ min)
When the status file `/tmp/dev-agent-status` doesn't exist, `stat -c %Y` fails and the supervisor falls back to epoch 0. The computed age is then `NOW_EPOCH/60 ≈ 29,567,290 min`, which is unmistakably a false positive.
@ -73,7 +73,7 @@ Symptom: agent in awaiting_review with PR CI=failure and push CI=success.
Fix: inject with explicit pipeline #623 (the pull_request event pipeline), point to the failing step and the specific duplicate blocks to fix. Use: woodpecker_api /repos/4/pipelines?event=pull_request (or look for event=pull_request in recent pipelines list) to find the correct pipeline number before injecting.
### Race Condition: Review Posted Before PHASE:awaiting_review Transitions
**Symptom:** Dev-agent status unchanged at 'waiting for review on PR #N', no `review-injected-disinto-N` sentinel, but a formal review already exists on Codeberg and `/tmp/disinto-review-output-N.json` was written before the phase file updated.
**Symptom:** Dev-agent status unchanged at 'waiting for review on PR #N', no `review-injected-disinto-N` sentinel, but a formal review already exists on forge and `/tmp/disinto-review-output-N.json` was written before the phase file updated.
**Root cause:** review-pr.sh runs while the dev-agent is still in PHASE:awaiting_ci. inject_review_into_dev_session returns early (phase check fails). On subsequent review-poll cycles, the PR is skipped (formal review already exists for SHA), so inject is never called again.
@ -84,7 +84,7 @@ PROJECT_TOML=/home/debian/dark-factory/projects/disinto.toml
source /home/debian/dark-factory/lib/load-project.sh "$PROJECT_TOML"
PHASE_FILE="/tmp/dev-session-${PROJECT_NAME}-<ISSUE>.phase"
PR_NUM=<N>; PR_BRANCH="fix/issue-<ISSUE>"; PR_SHA=$(cat /tmp/dev-session-${PROJECT_NAME}-<ISSUE>.phase | grep SHA | cut -d: -f2 || git -C $PROJECT_REPO_ROOT rev-parse origin/$PR_BRANCH)
REVIEW_TEXT=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" "${CODEBERG_API}/issues/${PR_NUM}/comments?limit=50" | jq -r --arg sha "$PR_SHA" '[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty | .body')
REVIEW_TEXT=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" "${FORGE_API}/issues/${PR_NUM}/comments?limit=50" | jq -r --arg sha "$PR_SHA" '[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty | .body')
INJECT_MSG="Review: REQUEST_CHANGES on PR #${PR_NUM}:\n\n${REVIEW_TEXT}\n\nInstructions:\n1. Address each piece of feedback carefully.\n2. Run lint and tests when done.\n3. Commit your changes and push: git push origin ${PR_BRANCH}\n4. Write: echo PHASE:awaiting_ci > "${PHASE_FILE}"\n5. Stop and wait for the next CI result."
INJECT_TMP=$(mktemp); printf '%s' "$INJECT_MSG" > "$INJECT_TMP"
tmux load-buffer -b inject "$INJECT_TMP" && tmux paste-buffer -t "dev-${PROJECT_NAME}-<ISSUE>" -b inject && sleep 0.5 && tmux send-keys -t "dev-${PROJECT_NAME}-<ISSUE>" '' Enter

View file

@ -1,7 +1,7 @@
# Codeberg Best Practices
# Forge Best Practices
## Rate Limiting
Codeberg rate-limits SSH and HTTPS clones. Symptoms:
The forge (Forgejo/Gitea) may rate-limit SSH and HTTPS clones. Symptoms:
- Woodpecker `git` step fails with exit code 128
- Multiple pipelines fail in quick succession with the same error
- Retriggers make it WORSE by adding more clone attempts
@ -26,10 +26,10 @@ cd <worktree> && git commit --allow-empty -m "ci: retrigger" --no-verify && git
- One pipeline at a time is ideal on this VPS (resource + rate limit reasons).
- If >3 pipelines are pending/running, do NOT create more work.
## OAuth Tokens
- OAuth tokens expire ~2h. If Codeberg is down during refresh, re-login required.
- API token is in `~/.netrc` — read via `awk` in env.sh.
- Review bot has a separate token ($REVIEW_BOT_TOKEN) for formal reviews.
## API Tokens
- API token is in `.env` as `FORGE_TOKEN` — loaded via env.sh.
- Review bot has a separate token (`$FORGE_REVIEW_TOKEN`) for formal reviews.
- With local Forgejo, tokens don't expire. For remote forges, check provider docs.
## Lessons Learned
- Retrigger storm on 2026-03-12: supervisor + dev-agent both retriggered during rate limit, caused 5+ failed pipelines. Added cooldown awareness.

View file

@ -3,7 +3,7 @@
## Architecture
- `review-poll.sh` (cron */10) → finds open PRs with CI pass + no review → spawns `review-pr.sh`
- `review-pr.sh` uses `claude -p` to review the diff, posts structured comment
- Uses `review_bot` Codeberg account for formal reviews (separate from main account)
- Uses `review_bot` forge account for formal reviews (separate from main account)
- Skips WIP/draft PRs (`[WIP]` in title or draft flag)
## Safe Fixes
@ -27,4 +27,4 @@
- Review bot must output JSON — prevents self-narration collapse
- DISCUSS verdict should be treated same as REQUEST_CHANGES by dev-agent
- Error comments must NOT include `<!-- reviewed: SHA -->` — would falsely mark as reviewed
- Review bot uses Codeberg formal reviews API — branch protection requires different user than PR author
- Review bot uses forge formal reviews API — branch protection requires different user than PR author

View file

@ -132,16 +132,16 @@ echo ""
# ── Open PRs ──────────────────────────────────────────────────────────────
echo "## Open PRs (${PROJECT_NAME})"
_open_prs=$(codeberg_api GET "/pulls?state=open&limit=10" 2>/dev/null || echo "[]")
_open_prs=$(forge_api GET "/pulls?state=open&limit=10" 2>/dev/null || echo "[]")
echo "$_open_prs" | jq -r '.[] | "#\(.number) [\(.head.ref)] \(.title) — updated \(.updated_at)"' 2>/dev/null || echo "No open PRs or query failed"
echo ""
# ── Backlog + In-Progress ─────────────────────────────────────────────────
echo "## Issue Status (${PROJECT_NAME})"
_backlog_count=$(codeberg_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null | jq 'length' 2>/dev/null || echo "?")
_in_progress_count=$(codeberg_api GET "/issues?state=open&labels=in-progress&type=issues&limit=50" 2>/dev/null | jq 'length' 2>/dev/null || echo "?")
_blocked_count=$(codeberg_api GET "/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null | jq 'length' 2>/dev/null || echo "?")
_backlog_count=$(forge_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null | jq 'length' 2>/dev/null || echo "?")
_in_progress_count=$(forge_api GET "/issues?state=open&labels=in-progress&type=issues&limit=50" 2>/dev/null | jq 'length' 2>/dev/null || echo "?")
_blocked_count=$(forge_api GET "/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null | jq 'length' 2>/dev/null || echo "?")
echo "Backlog: ${_backlog_count}, In-progress: ${_in_progress_count}, Blocked: ${_blocked_count}"
echo ""
@ -161,7 +161,7 @@ echo ""
# ── Blocked Issues ────────────────────────────────────────────────────────
echo "## Blocked Issues"
_blocked_issues=$(codeberg_api GET "/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null || echo "[]")
_blocked_issues=$(forge_api GET "/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null || echo "[]")
_blocked_n=$(echo "$_blocked_issues" | jq 'length' 2>/dev/null || echo 0)
if [ "${_blocked_n:-0}" -gt 0 ]; then
echo "$_blocked_issues" | jq -r '.[] | " #\(.number): \(.title)"' 2>/dev/null || echo " (query failed)"

View file

@ -28,13 +28,13 @@ emit_metric() {
printf '%s\n' "$1" >> "$METRICS_FILE"
}
# Count all matching items from a paginated Codeberg API endpoint.
# Count all matching items from a paginated forge API endpoint.
# Usage: codeberg_count_paginated "/issues?state=open&labels=backlog&type=issues"
# Returns total count across all pages (max 20 pages = 1000 items).
codeberg_count_paginated() {
local endpoint="$1" total=0 page=1 count
while true; do
count=$(codeberg_api GET "${endpoint}&limit=50&page=${page}" 2>/dev/null | jq 'length' 2>/dev/null || echo 0)
count=$(forge_api GET "${endpoint}&limit=50&page=${page}" 2>/dev/null | jq 'length' 2>/dev/null || echo 0)
total=$((total + ${count:-0}))
[ "${count:-0}" -lt 50 ] && break
page=$((page + 1))
@ -244,7 +244,7 @@ mkdir -p "$_RETRY_DIR"
# Function: run all per-project checks for the currently loaded project config
check_project() {
local proj_name="${PROJECT_NAME:-unknown}"
flog "── checking project: ${proj_name} (${CODEBERG_REPO}) ──"
flog "── checking project: ${proj_name} (${FORGE_REPO}) ──"
# ===========================================================================
# P2: FACTORY STOPPED — CI, dev-agent, git
@ -366,8 +366,8 @@ check_project() {
if [ "${CHECK_PIPELINE_STALL:-true}" = "true" ]; then
status "P2: ${proj_name}: checking pipeline stall"
BACKLOG_COUNT=$(codeberg_api GET "/issues?state=open&labels=backlog&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
IN_PROGRESS=$(codeberg_api GET "/issues?state=open&labels=in-progress&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
BACKLOG_COUNT=$(forge_api GET "/issues?state=open&labels=backlog&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
IN_PROGRESS=$(forge_api GET "/issues?state=open&labels=in-progress&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
if [ "${BACKLOG_COUNT:-0}" -gt 0 ] && [ "${IN_PROGRESS:-0}" -eq 0 ]; then
DEV_LOG="${FACTORY_ROOT}/dev/dev-agent.log"
@ -408,14 +408,14 @@ check_project() {
if [ "${CHECK_PRS:-true}" = "true" ]; then
status "P3: ${proj_name}: checking PRs"
OPEN_PRS=$(codeberg_api GET "/pulls?state=open&limit=10" 2>/dev/null | jq -r '.[].number' 2>/dev/null || true)
OPEN_PRS=$(forge_api GET "/pulls?state=open&limit=10" 2>/dev/null | jq -r '.[].number' 2>/dev/null || true)
for pr in $OPEN_PRS; do
PR_JSON=$(codeberg_api GET "/pulls/${pr}" 2>/dev/null || true)
PR_JSON=$(forge_api GET "/pulls/${pr}" 2>/dev/null || true)
[ -z "$PR_JSON" ] && continue
PR_SHA=$(echo "$PR_JSON" | jq -r '.head.sha // ""')
[ -z "$PR_SHA" ] && continue
CI_STATE=$(codeberg_api GET "/commits/${PR_SHA}/status" 2>/dev/null | jq -r '.state // "unknown"' 2>/dev/null || true)
CI_STATE=$(forge_api GET "/commits/${PR_SHA}/status" 2>/dev/null | jq -r '.state // "unknown"' 2>/dev/null || true)
MERGEABLE=$(echo "$PR_JSON" | jq -r '.mergeable // true')
if [ "$MERGEABLE" = "false" ] && ci_passed "$CI_STATE"; then
@ -429,7 +429,7 @@ check_project() {
[ "$AGE_MIN" -gt 30 ] && p3 "${proj_name}: PR #${pr}: CI=${CI_STATE}, stale ${AGE_MIN}min"
fi
elif ci_passed "$CI_STATE"; then
HAS_REVIEW=$(codeberg_api GET "/issues/${pr}/comments?limit=50" 2>/dev/null | \
HAS_REVIEW=$(forge_api GET "/issues/${pr}/comments?limit=50" 2>/dev/null | \
jq -r --arg sha "$PR_SHA" '[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | length' 2>/dev/null || echo "0")
if [ "${HAS_REVIEW:-0}" -eq 0 ]; then
@ -454,7 +454,7 @@ check_project() {
# ===========================================================================
status "P3: ${proj_name}: checking for circular dependencies"
BACKLOG_FOR_DEPS=$(codeberg_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null || true)
BACKLOG_FOR_DEPS=$(forge_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null || true)
if [ -n "$BACKLOG_FOR_DEPS" ] && [ "$BACKLOG_FOR_DEPS" != "null" ] && [ "$(echo "$BACKLOG_FOR_DEPS" | jq 'length' 2>/dev/null || echo 0)" -gt 0 ]; then
PARSE_DEPS="${FACTORY_ROOT}/lib/parse-deps.sh"
@ -524,7 +524,7 @@ check_project() {
if [ -n "${DEP_CACHE[$dep]+x}" ]; then
DEP_INFO="${DEP_CACHE[$dep]}"
else
DEP_JSON=$(codeberg_api GET "/issues/${dep}" 2>/dev/null || true)
DEP_JSON=$(forge_api GET "/issues/${dep}" 2>/dev/null || true)
[ -z "$DEP_JSON" ] && continue
DEP_STATE=$(echo "$DEP_JSON" | jq -r '.state // "unknown"')
DEP_CREATED=$(echo "$DEP_JSON" | jq -r '.created_at // ""')
@ -646,8 +646,8 @@ Instructions:
_sess_issue="${_sess#dev-"${proj_name}"-}"
[[ "$_sess_issue" =~ ^[0-9]+$ ]] || continue
# Check Codeberg: is the issue still open?
_issue_state=$(codeberg_api GET "/issues/${_sess_issue}" 2>/dev/null \
# Check forge: is the issue still open?
_issue_state=$(forge_api GET "/issues/${_sess_issue}" 2>/dev/null \
| jq -r '.state // "open"' 2>/dev/null || echo "open")
_should_cleanup=false
@ -671,7 +671,7 @@ Instructions:
_has_open_pr=0
_pr_page=1
while true; do
_pr_page_json=$(codeberg_api GET "/pulls?state=open&limit=50&page=${_pr_page}" \
_pr_page_json=$(forge_api GET "/pulls?state=open&limit=50&page=${_pr_page}" \
2>/dev/null || echo "[]")
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
_pr_match=$(printf '%s' "$_pr_page_json" | \
@ -689,7 +689,7 @@ Instructions:
_has_closed_pr=0
_pr_page=1
while true; do
_pr_page_json=$(codeberg_api GET "/pulls?state=closed&limit=50&page=${_pr_page}" \
_pr_page_json=$(forge_api GET "/pulls?state=closed&limit=50&page=${_pr_page}" \
2>/dev/null || echo "[]")
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
_pr_match=$(printf '%s' "$_pr_page_json" | \
@ -771,7 +771,7 @@ if [ -d "$PROJECTS_DIR" ]; then
[ -f "$project_toml" ] || continue
PROJECT_COUNT=$((PROJECT_COUNT + 1))
# Load project config (overrides CODEBERG_REPO, PROJECT_REPO_ROOT, etc.)
# Load project config (overrides FORGE_REPO, PROJECT_REPO_ROOT, etc.)
source "${FACTORY_ROOT}/lib/load-project.sh" "$project_toml"
check_project || flog "check_project failed for ${project_toml} (per-project checks incomplete)"

View file

@ -71,7 +71,7 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
build_prompt_footer
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
PROMPT="You are the supervisor agent for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
PROMPT="You are the supervisor agent for ${FORGE_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
You have full shell access and --dangerously-skip-permissions.
Fix what you can. Escalate what you cannot. Do NOT ask permission — act first, report after.

View file

@ -1,6 +1,6 @@
# Vault Agent
You are the vault agent for `$CODEBERG_REPO`. You were called by
You are the vault agent for `$FORGE_REPO`. You were called by
`vault-poll.sh` because one or more actions in `vault/pending/` need
classification and routing.