From a66bd9172137f6de19a8b0d3cd3699c165ddeb92 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 23 Mar 2026 16:57:12 +0000 Subject: [PATCH] fix: Replace Codeberg dependency with local Forgejo instance (#611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .env.example | 23 +- .woodpecker/agent-smoke.sh | 2 +- AGENTS.md | 4 +- BOOTSTRAP.md | 64 ++- README.md | 29 +- RESOURCES.example.md | 2 +- VISION.md | 2 +- action/AGENTS.md | 2 +- action/action-agent.sh | 38 +- action/action-poll.sh | 4 +- bin/disinto | 433 ++++++++++++------ dev/AGENTS.md | 4 +- dev/dev-agent.sh | 68 +-- dev/dev-poll.sh | 64 +-- dev/phase-handler.sh | 78 ++-- docs/EVIDENCE-ARCHITECTURE.md | 2 +- docs/PHASE-PROTOCOL.md | 2 +- formulas/groom-backlog.toml | 22 +- formulas/review-pr.toml | 18 +- formulas/run-gardener.toml | 26 +- formulas/run-planner.toml | 98 ++-- formulas/run-predictor.toml | 30 +- gardener/AGENTS.md | 2 +- gardener/gardener-run.sh | 86 ++-- lib/AGENTS.md | 4 +- lib/ci-debug.sh | 2 +- lib/ci-helpers.sh | 20 +- lib/env.sh | 60 ++- lib/file-action-issue.sh | 8 +- lib/formula-session.sh | 22 +- lib/hooks/on-pretooluse-guard.sh | 4 +- lib/load-project.sh | 25 +- lib/secret-scan.sh | 3 +- planner/AGENTS.md | 2 +- planner/planner-run.sh | 8 +- predictor/AGENTS.md | 4 +- predictor/predictor-run.sh | 2 +- projects/disinto.toml.example | 3 +- projects/harb.toml.example | 3 +- projects/versi.toml.example | 3 +- review/AGENTS.md | 10 +- review/review-poll.sh | 22 +- review/review-pr.sh | 30 +- site/collect-metrics.sh | 19 +- site/dashboard.html | 4 +- site/docs/architecture.html | 6 +- site/docs/quickstart.html | 22 +- site/index.html | 8 +- supervisor/AGENTS.md | 2 +- supervisor/PROMPT.md | 16 +- supervisor/best-practices/ci.md | 4 +- supervisor/best-practices/dev-agent.md | 10 +- .../best-practices/{codeberg.md => forge.md} | 12 +- supervisor/best-practices/review-agent.md | 4 +- supervisor/preflight.sh | 10 +- supervisor/supervisor-poll.sh | 32 +- supervisor/supervisor-run.sh | 2 +- vault/PROMPT.md | 2 +- 58 files changed, 863 insertions(+), 628 deletions(-) rename supervisor/best-practices/{codeberg.md => forge.md} (81%) diff --git a/.env.example b/.env.example index 3c64fa8..4fa2a15 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index 5f9f978..9fbf707 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -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 diff --git a/AGENTS.md b/AGENTS.md index de4852d..5ee4aca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 2d3fab3..b209b42 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -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/.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/.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 | diff --git a/README.md b/README.md index d10ef65..cdf9e9f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@
-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 diff --git a/RESOURCES.example.md b/RESOURCES.example.md index f52737f..5fda08b 100644 --- a/RESOURCES.example.md +++ b/RESOURCES.example.md @@ -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 | diff --git a/VISION.md b/VISION.md index dd8957f..ad6652e 100644 --- a/VISION.md +++ b/VISION.md @@ -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. diff --git a/action/AGENTS.md b/action/AGENTS.md index 00d01d1..dbee933 100644 --- a/action/AGENTS.md +++ b/action/AGENTS.md @@ -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 diff --git a/action/action-agent.sh b/action/action-agent.sh index 30bc6c9..633c378 100755 --- a/action/action-agent.sh +++ b/action/action-agent.sh @@ -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}" \ "⚡ Action #${ISSUE}: ${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 diff --git a/action/action-poll.sh b/action/action-poll.sh index d164977..61c26e8 100755 --- a/action/action-poll.sh +++ b/action/action-poll.sh @@ -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" diff --git a/bin/disinto b/bin/disinto index eca1196..259ffcd 100755 --- a/bin/disinto +++ b/bin/disinto @@ -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 Primary branch (default: auto-detect) --repo-root Local clone path (default: ~/name) --ci-id Woodpecker CI repo ID (default: 0 = no CI) - --token Codeberg API token (saved to ~/.netrc) + --forge-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 " >&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/.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" </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 "" diff --git a/dev/AGENTS.md b/dev/AGENTS.md index 0e37296..64ec3fb 100644 --- a/dev/AGENTS.md +++ b/dev/AGENTS.md @@ -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 diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh index 56a63c8..70fdbc7 100755 --- a/dev/dev-agent.sh +++ b/dev/dev-agent.sh @@ -34,21 +34,21 @@ git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true # --- Config --- ISSUE="${1:?Usage: dev-agent.sh }" # 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}" \ "🔧 Issue #${ISSUE}: ${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 #${PR_NUMBER}}" + "session finished without phase signal — killed. Marking blocked.${PR_NUMBER:+ PR #${PR_NUMBER}}" else notify_ctx \ "session idle for 2h — killed. Marking blocked." \ - "session idle for 2h — killed. Marking blocked.${PR_NUMBER:+ PR #${PR_NUMBER}}" + "session idle for 2h — killed. Marking blocked.${PR_NUMBER:+ PR #${PR_NUMBER}}" fi # Post diagnostic comment + label issue blocked post_blocked_diagnostic "${_MONITOR_LOOP_EXIT:-idle_timeout}" diff --git a/dev/dev-poll.sh b/dev/dev-poll.sh index b8509c8..5b89297 100755 --- a/dev/dev-poll.sh +++ b/dev/dev-poll.sh @@ -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 diff --git a/dev/phase-handler.sh b/dev/phase-handler.sh index 4365f5b..04614fe 100644 --- a/dev/phase-handler.sh +++ b/dev/phase-handler.sh @@ -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 #${PR_NUMBER} 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 #${PR_NUMBER} | Pipeline
Step: ${FAILED_STEP:-unknown} — escalating for human help" + "CI exhausted after ${CI_FIX_COUNT} attempts on PR #${PR_NUMBER} | Pipeline
Step: ${FAILED_STEP:-unknown} — 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/&/\&/g; s//\>/g') notify_ctx \ "CI failed on PR #${PR_NUMBER}: step=${FAILED_STEP:-unknown} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES})" \ - "CI failed on PR #${PR_NUMBER} | Pipeline #${PIPELINE_NUM:-?}
Step: ${FAILED_STEP:-unknown} (exit ${FAILED_EXIT:-?})
Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}
${_ci_snippet:-no logs}
" + "CI failed on PR #${PR_NUMBER} | Pipeline #${PIPELINE_NUM:-?}
Step: ${FAILED_STEP:-unknown} (exit ${FAILED_EXIT:-?})
Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}
${_ci_snippet:-no logs}
" 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(""))]|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\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\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" diff --git a/site/collect-metrics.sh b/site/collect-metrics.sh index facc8af..c9437f8 100644 --- a/site/collect-metrics.sh +++ b/site/collect-metrics.sh @@ -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) diff --git a/site/dashboard.html b/site/dashboard.html index 5e78ec2..81e12cb 100644 --- a/site/dashboard.html +++ b/site/dashboard.html @@ -355,7 +355,7 @@ @@ -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); diff --git a/site/docs/architecture.html b/site/docs/architecture.html index f4b35d7..2bce787 100644 --- a/site/docs/architecture.html +++ b/site/docs/architecture.html @@ -365,7 +365,7 @@

Eight agents

-

Each agent has a single responsibility. They communicate through git, the Codeberg API, and the filesystem.

+

Each agent has a single responsibility. They communicate through git, the forge API, and the filesystem.

dev-agent
@@ -480,7 +480,7 @@

Bash scripts — every agent is a shell script. No compiled binaries, no runtimes to install.

Claude CLI — AI is invoked via claude -p (one-shot) or claude (persistent tmux sessions).

Cron — agents are triggered by cron jobs, not a daemon. Pull-based, not push-based.

-

Codeberg + Woodpecker — git hosting and CI. All state lives in git and the issue tracker. No external databases.

+

Forgejo + Woodpecker — git hosting and CI. All state lives in git and the issue tracker. No external databases.

Single VPS — runs on an 8 GB server. Flat cost, no scaling surprises.

@@ -538,7 +538,7 @@ disinto/
diff --git a/site/docs/quickstart.html b/site/docs/quickstart.html index f4c19bf..ad11250 100644 --- a/site/docs/quickstart.html +++ b/site/docs/quickstart.html @@ -328,8 +328,8 @@
Prerequisites
  • A VPS or server — 8 GB RAM minimum (Ubuntu/Debian recommended)
  • -
  • A Codeberg account — with a repo and at least one issue
  • -
  • A second Codeberg account — for the review bot (branch protection requires a different reviewer)
  • +
  • A forge instance — with a repo and at least one issue
  • +
  • A second forge instance — for the review bot (branch protection requires a different reviewer)
  • Woodpecker CI — running and connected to your repo
  • An Anthropic API key — with the claude CLI installed and authenticated
  • tmux — for persistent dev sessions
  • @@ -343,13 +343,13 @@ Clone the factory

    Clone disinto onto your server. This is the factory — the code that runs your agents.

    -
    git clone https://codeberg.org/johba/disinto.git ~/disinto
    +
    git clone http://localhost:3000/johba/disinto.git ~/disinto
     cd ~/disinto
     cp .env.example .env

    Edit .env with your tokens:

    # Required
    -CODEBERG_TOKEN=your_codeberg_token
    -REVIEW_BOT_TOKEN=your_review_bot_token
    +FORGE_TOKEN=your_codeberg_token
    +FORGE_REVIEW_TOKEN=your_review_bot_token
     
     # Woodpecker CI
     WOODPECKER_TOKEN=your_woodpecker_token
    @@ -365,14 +365,14 @@ CLAUDE_TIMEOUT=7200
    2 Initialize your project -

    disinto init sets up everything: clones the repo, creates the project config, adds Codeberg labels, and installs cron jobs.

    -
    bin/disinto init https://codeberg.org/you/your-project
    +

    disinto init provisions a local Forgejo instance, clones the repo, creates the project config, adds labels, and installs cron jobs.

    +
    bin/disinto init http://localhost:3000/you/your-project
    Expected output
    === 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.
    1. A CI pipeline — at least one .woodpecker/*.yml file. Agents wait for CI before reviewing or merging.
    2. A CLAUDE.md — project context that agents read before every task. Describe your tech stack, how to build/test, coding conventions, and directory layout.
    3. -
    4. Branch protection — on Codeberg, require PR reviews and add the review bot as a write collaborator.
    5. +
    6. Branch protection — on Forgejo, require PR reviews and add the review bot as a write collaborator.
    # Create CLAUDE.md in your project
     cat > ~/your-project/CLAUDE.md <<'EOF'
    @@ -434,7 +434,7 @@ git push
    4 File your first issue
    -

    Create an issue on Codeberg with the backlog label. Be specific — the dev-agent works best with clear acceptance criteria.

    +

    Create an issue on the forge with the backlog label. Be specific — the dev-agent works best with clear acceptance criteria.

    # Title: Add health check endpoint
     # Label: backlog
     # Body:
    @@ -523,7 +523,7 @@ git log --oneline -5
    diff --git a/site/index.html b/site/index.html index 2fe980a..ec9fdd9 100644 --- a/site/index.html +++ b/site/index.html @@ -661,7 +661,7 @@

    Bash scripts and Claude. No Kubernetes, no microservices, no SaaS dependencies. Runs on an 8GB VPS. - Point it at a Codeberg repo with a + Point it at a forge repo with a Woodpecker CI pipeline and it starts building.

    @@ -691,8 +691,8 @@

    @@ -702,7 +702,7 @@
    diff --git a/supervisor/AGENTS.md b/supervisor/AGENTS.md index c1682ea..13b180a 100644 --- a/supervisor/AGENTS.md +++ b/supervisor/AGENTS.md @@ -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 diff --git a/supervisor/PROMPT.md b/supervisor/PROMPT.md index 007ad1a..e9b3aa1 100644 --- a/supervisor/PROMPT.md +++ b/supervisor/PROMPT.md @@ -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) diff --git a/supervisor/best-practices/ci.md b/supervisor/best-practices/ci.md index ce32086..ec46b3a 100644 --- a/supervisor/best-practices/ci.md +++ b/supervisor/best-practices/ci.md @@ -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. diff --git a/supervisor/best-practices/dev-agent.md b/supervisor/best-practices/dev-agent.md index c55aa52..8850df5 100644 --- a/supervisor/best-practices/dev-agent.md +++ b/supervisor/best-practices/dev-agent.md @@ -14,7 +14,7 @@ - Clean worktree: `cd $PROJECT_REPO_ROOT && git worktree remove /tmp/${PROJECT_NAME}-worktree- --force` - Remove `in-progress` label if agent died without cleanup: ```bash - codeberg_api DELETE "/issues//labels/in-progress" + forge_api DELETE "/issues//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 `), consuming CPU (>0%), and has active threads (`pstree -p `). 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 20–40 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//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 20–40 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//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}-.phase" PR_NUM=; PR_BRANCH="fix/issue-"; PR_SHA=$(cat /tmp/dev-session-${PROJECT_NAME}-.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("` — 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 diff --git a/supervisor/preflight.sh b/supervisor/preflight.sh index a03ccba..2d8e55c 100755 --- a/supervisor/preflight.sh +++ b/supervisor/preflight.sh @@ -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)" diff --git a/supervisor/supervisor-poll.sh b/supervisor/supervisor-poll.sh index c1a284f..a530d2d 100755 --- a/supervisor/supervisor-poll.sh +++ b/supervisor/supervisor-poll.sh @@ -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("