fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker, creates admin + bot users (dev-bot, review-bot), generates API tokens, creates repo, and pushes code — all automated - Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→ FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→ FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→ FORGE_BOT_USERNAMES (with backwards-compat fallbacks) - Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all() →forge_api_all() (with compat aliases) - Add forge_url field to project TOML; load-project.sh derives FORGE_API/FORGE_WEB from forge_url + repo - Update parse_repo_slug() to accept any host URL, not just codeberg - Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo) - Update all 58 files: agent scripts, formulas, docs, site HTML Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
39d30faf45
commit
a66bd91721
58 changed files with 863 additions and 628 deletions
23
.env.example
23
.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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
64
BOOTSTRAP.md
64
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/<name>.toml` config
|
||||
5. Create standard labels (backlog, in-progress, blocked, etc.)
|
||||
6. Install cron entries for the agents
|
||||
|
||||
No external accounts or tokens needed.
|
||||
|
||||
## 1. Configure `.env`
|
||||
|
||||
```bash
|
||||
|
|
@ -22,19 +40,15 @@ cp .env.example .env
|
|||
Fill in:
|
||||
|
||||
```bash
|
||||
# ── Target project ──────────────────────────────────────────
|
||||
CODEBERG_REPO=org/project # Codeberg slug
|
||||
PROJECT_REPO_ROOT=/home/you/project # absolute path to local clone
|
||||
PRIMARY_BRANCH=main # main or master
|
||||
|
||||
# ── Auth ────────────────────────────────────────────────────
|
||||
# CODEBERG_TOKEN= # or use ~/.netrc (machine codeberg.org)
|
||||
REVIEW_BOT_TOKEN=tok_xxxxxxxx # the second account's API token
|
||||
# ── Forge (auto-populated by disinto init) ─────────────────
|
||||
FORGE_URL=http://localhost:3000 # local Forgejo instance
|
||||
FORGE_TOKEN= # dev-bot token (auto-generated)
|
||||
FORGE_REVIEW_TOKEN= # review-bot token (auto-generated)
|
||||
|
||||
# ── Woodpecker CI ───────────────────────────────────────────
|
||||
WOODPECKER_TOKEN=tok_xxxxxxxx
|
||||
WOODPECKER_SERVER=http://localhost:8000
|
||||
WOODPECKER_REPO_ID=2 # numeric — find via Woodpecker UI or API
|
||||
# WOODPECKER_REPO_ID — now per-project, set in projects/*.toml [ci] section
|
||||
|
||||
# Woodpecker Postgres (for direct pipeline queries)
|
||||
WOODPECKER_DB_PASSWORD=secret
|
||||
|
|
@ -52,23 +66,35 @@ WOODPECKER_DB_NAME=woodpecker
|
|||
CLAUDE_TIMEOUT=7200 # seconds per Claude invocation
|
||||
```
|
||||
|
||||
### Backwards compatibility
|
||||
|
||||
If you have an existing deployment using `CODEBERG_TOKEN` / `REVIEW_BOT_TOKEN` in `.env`, those still work — `env.sh` falls back to the old names automatically. No migration needed.
|
||||
|
||||
## 2. Configure Project TOML
|
||||
|
||||
Each project needs a `projects/<name>.toml` file with box-specific settings
|
||||
(absolute paths, Woodpecker CI IDs, Matrix credentials). These files are
|
||||
(absolute paths, Woodpecker CI IDs, Matrix credentials, forge URL). These files are
|
||||
**gitignored** — they are local installation config, not shared code.
|
||||
|
||||
To create one:
|
||||
|
||||
```bash
|
||||
# Automatic — generates TOML, clones repo, sets up cron:
|
||||
disinto init https://codeberg.org/org/repo
|
||||
disinto init https://github.com/org/repo
|
||||
|
||||
# Manual — copy a template and fill in your values:
|
||||
cp projects/myproject.toml.example projects/myproject.toml
|
||||
vim projects/myproject.toml
|
||||
```
|
||||
|
||||
The `forge_url` field in the TOML tells all agents where to find the forge API:
|
||||
|
||||
```toml
|
||||
name = "myproject"
|
||||
repo = "org/myproject"
|
||||
forge_url = "http://localhost:3000"
|
||||
```
|
||||
|
||||
The repo ships `projects/*.toml.example` templates showing the expected
|
||||
structure. See any `.toml.example` file for the full field reference.
|
||||
|
||||
|
|
@ -133,7 +159,7 @@ The dev-agent reads this file via `claude -p` before implementing any issue. The
|
|||
|
||||
### Required: Issue labels
|
||||
|
||||
Create two labels on the Codeberg repo:
|
||||
`disinto init` creates these automatically. If setting up manually, create these labels on the forge repo:
|
||||
|
||||
| Label | Purpose |
|
||||
|-------|---------|
|
||||
|
|
@ -150,7 +176,7 @@ Optional but recommended:
|
|||
|
||||
### Required: Branch protection
|
||||
|
||||
On Codeberg, set up branch protection for your primary branch:
|
||||
On Forgejo, set up branch protection for your primary branch:
|
||||
|
||||
- **Require pull request reviews**: enabled
|
||||
- **Required approvals**: 1 (from the review bot account)
|
||||
|
|
@ -159,8 +185,8 @@ On Codeberg, set up branch protection for your primary branch:
|
|||
This ensures dev-agent can't merge its own PRs — it must wait for review-agent (running as the bot account) to approve.
|
||||
|
||||
> **Common pitfall:** Approvals alone are not enough. You must also:
|
||||
> 1. Add `review_bot` as a **write** collaborator on the repo (Settings → Collaborators)
|
||||
> 2. Set both `approvals_whitelist_username` **and** `merge_whitelist_usernames` to include `review_bot` in the branch protection rule
|
||||
> 1. Add `review-bot` as a **write** collaborator on the repo (Settings → Collaborators)
|
||||
> 2. Set both `approvals_whitelist_username` **and** `merge_whitelist_usernames` to include `review-bot` in the branch protection rule
|
||||
>
|
||||
> Without write access, the bot's approval is counted but the merge API returns HTTP 405.
|
||||
|
||||
|
|
@ -361,7 +387,7 @@ Meanwhile:
|
|||
|---------|-------|-----|
|
||||
| Dev-agent for project B never starts | Shared lock file path | Each TOML `name` field must be unique — lock is `/tmp/dev-agent-{name}.lock` |
|
||||
| Review-poll skips all PRs | CI gate with no CI configured | Set `woodpecker_repo_id = 0` in the TOML `[ci]` section to bypass the CI check |
|
||||
| Approved PRs never merge (HTTP 405) | `review_bot` not in merge/approvals whitelist | Add as write collaborator; set both `approvals_whitelist_username` and `merge_whitelist_usernames` in branch protection |
|
||||
| Approved PRs never merge (HTTP 405) | `review-bot` not in merge/approvals whitelist | Add as write collaborator; set both `approvals_whitelist_username` and `merge_whitelist_usernames` in branch protection |
|
||||
| Dev-agent churns through issues without waiting for open PRs to land | No single-threaded enforcement | `WAITING_PRS` check in dev-poll holds new work — verify TOML `name` is consistent across invocations |
|
||||
| Label ping-pong (issue reopened then immediately re-closed) | `already_done` handler doesn't close issue | Review dev-agent log; `already_done` status should auto-close the issue |
|
||||
|
||||
|
|
|
|||
29
README.md
29
README.md
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<br>
|
||||
|
||||
Point it at a Codeberg repo with a Woodpecker CI pipeline and it will pick up issues, implement them, review PRs, and keep the system healthy — all on its own.
|
||||
Point it at a git repo with a Woodpecker CI pipeline and it will pick up issues, implement them, review PRs, and keep the system healthy — all on its own.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
@ -49,9 +49,8 @@ all agents ──→ matrix_send() ← status updates, escalations, merge no
|
|||
**Required:**
|
||||
|
||||
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) — `claude` in PATH, authenticated
|
||||
- [Codeberg](https://codeberg.org/) account with an API token — disinto reads issues, opens PRs, posts comments, and merges via the Codeberg API
|
||||
- A second Codeberg account for the review bot — reviews posted under a separate identity so the dev-agent doesn't review its own PRs (`REVIEW_BOT_TOKEN`)
|
||||
- [Woodpecker CI](https://woodpecker-ci.org/) — local instance connected to your Codeberg repo; disinto monitors pipelines, retries failures, and queries the Woodpecker Postgres DB directly
|
||||
- [Docker](https://docker.com/) — for provisioning a local Forgejo instance (or a running Forgejo/Gitea instance)
|
||||
- [Woodpecker CI](https://woodpecker-ci.org/) — local instance connected to your forge; disinto monitors pipelines, retries failures, and queries the Woodpecker Postgres DB directly
|
||||
- PostgreSQL client (`psql`) — for Woodpecker DB queries (pipeline status, build counts)
|
||||
- `jq`, `curl`, `git`
|
||||
|
||||
|
|
@ -65,24 +64,20 @@ all agents ──→ matrix_send() ← status updates, escalations, merge no
|
|||
|
||||
```bash
|
||||
# 1. Clone
|
||||
git clone ssh://git@codeberg.org/johba/disinto.git
|
||||
git clone https://github.com/johba/disinto.git
|
||||
cd disinto
|
||||
|
||||
# 2. Configure
|
||||
cp .env.example .env
|
||||
# 2. Bootstrap a project (provisions local Forgejo, creates tokens, clones repo)
|
||||
disinto init https://github.com/yourorg/yourproject
|
||||
```
|
||||
|
||||
Edit `.env` with your values:
|
||||
Or configure manually — edit `.env` with your values:
|
||||
|
||||
```bash
|
||||
# Target repo
|
||||
CODEBERG_REPO=yourorg/yourproject # Codeberg org/repo slug
|
||||
CODEBERG_API=https://codeberg.org/api/v1/repos/yourorg/yourproject
|
||||
PROJECT_REPO_ROOT=/path/to/your/project # local clone of the target repo
|
||||
|
||||
# Auth tokens
|
||||
CODEBERG_TOKEN=... # main account — or put it in ~/.netrc
|
||||
REVIEW_BOT_TOKEN=... # separate Codeberg account for code reviews
|
||||
# Forge (auto-populated by disinto init)
|
||||
FORGE_URL=http://localhost:3000 # local Forgejo instance
|
||||
FORGE_TOKEN=... # dev-bot token
|
||||
FORGE_REVIEW_TOKEN=... # review-bot token
|
||||
|
||||
# Woodpecker CI
|
||||
WOODPECKER_SERVER=http://localhost:8000
|
||||
|
|
@ -148,7 +143,7 @@ disinto/
|
|||
├── memory.md
|
||||
├── disk.md
|
||||
├── ci.md
|
||||
├── codeberg.md
|
||||
├── forge.md
|
||||
├── dev-agent.md
|
||||
├── review-agent.md
|
||||
└── git.md
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ SESSION_START_EPOCH=$(date +%s)
|
|||
|
||||
# --- Phase handler globals (agent-specific; defaults in phase-handler.sh) ---
|
||||
# shellcheck disable=SC2034 # used by phase-handler.sh
|
||||
API="${CODEBERG_API}"
|
||||
API="${FORGE_API}"
|
||||
BRANCH="action/issue-${ISSUE}"
|
||||
# shellcheck disable=SC2034 # used by phase-handler.sh
|
||||
WORKTREE="/tmp/action-${ISSUE}-$(date +%s)"
|
||||
|
|
@ -133,8 +133,8 @@ fi
|
|||
|
||||
# --- Fetch issue ---
|
||||
log "fetching issue #${ISSUE}"
|
||||
ISSUE_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/issues/${ISSUE}") || true
|
||||
ISSUE_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/issues/${ISSUE}") || true
|
||||
|
||||
if [ -z "$ISSUE_JSON" ] || ! printf '%s' "$ISSUE_JSON" | jq -e '.id' >/dev/null 2>&1; then
|
||||
log "ERROR: failed to fetch issue #${ISSUE}"
|
||||
|
|
@ -161,18 +161,18 @@ if [ -n "$YAML_MODEL" ]; then
|
|||
fi
|
||||
|
||||
# --- Resolve bot username(s) for comment filtering ---
|
||||
_bot_login=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true)
|
||||
_bot_login=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true)
|
||||
|
||||
# Build list: token owner + any extra names from CODEBERG_BOT_USERNAMES (comma-separated)
|
||||
# Build list: token owner + any extra names from FORGE_BOT_USERNAMES (comma-separated)
|
||||
_bot_logins="${_bot_login}"
|
||||
if [ -n "${CODEBERG_BOT_USERNAMES:-}" ]; then
|
||||
_bot_logins="${_bot_logins:+${_bot_logins},}${CODEBERG_BOT_USERNAMES}"
|
||||
if [ -n "${FORGE_BOT_USERNAMES:-}" ]; then
|
||||
_bot_logins="${_bot_logins:+${_bot_logins},}${FORGE_BOT_USERNAMES}"
|
||||
fi
|
||||
|
||||
# --- Fetch existing comments (resume context, excluding bot comments) ---
|
||||
COMMENTS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/issues/${ISSUE}/comments?limit=50") || true
|
||||
COMMENTS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/issues/${ISSUE}/comments?limit=50") || true
|
||||
|
||||
PRIOR_COMMENTS=""
|
||||
if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSON" != "[]" ]; then
|
||||
|
|
@ -184,7 +184,7 @@ if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSO
|
|||
fi
|
||||
|
||||
# --- Create Matrix thread for this issue ---
|
||||
ISSUE_URL="${CODEBERG_WEB}/issues/${ISSUE}"
|
||||
ISSUE_URL="${FORGE_WEB}/issues/${ISSUE}"
|
||||
_thread_id=$(matrix_send_ctx "action" \
|
||||
"⚡ Action #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \
|
||||
"⚡ <a href='${ISSUE_URL}'>Action #${ISSUE}</a>: ${ISSUE_TITLE}") || true
|
||||
|
|
@ -254,9 +254,9 @@ ${PRIOR_SECTION}## Instructions
|
|||
|
||||
3. Post progress as comments on issue #${ISSUE} after significant steps:
|
||||
curl -sf -X POST \\
|
||||
-H \"Authorization: token \${CODEBERG_TOKEN}\" \\
|
||||
-H \"Authorization: token \${FORGE_TOKEN}\" \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
\"${CODEBERG_API}/issues/${ISSUE}/comments\" \\
|
||||
\"${FORGE_API}/issues/${ISSUE}/comments\" \\
|
||||
-d \"{\\\"body\\\": \\\"your comment here\\\"}\"
|
||||
|
||||
4. If a step requires human input or approval, send a Matrix message explaining
|
||||
|
|
@ -278,19 +278,19 @@ ${PRIOR_SECTION}## Instructions
|
|||
files you need to persistent paths before signaling done.
|
||||
- Close the issue:
|
||||
curl -sf -X PATCH \\
|
||||
-H \"Authorization: token \${CODEBERG_TOKEN}\" \\
|
||||
-H \"Authorization: token \${FORGE_TOKEN}\" \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
\"${CODEBERG_API}/issues/${ISSUE}\" \\
|
||||
\"${FORGE_API}/issues/${ISSUE}\" \\
|
||||
-d '{\"state\": \"closed\"}'
|
||||
- Signal completion: echo \"PHASE:done\" > \"${PHASE_FILE}\"
|
||||
|
||||
5. Environment variables available in your bash sessions:
|
||||
CODEBERG_TOKEN, CODEBERG_API, CODEBERG_REPO, CODEBERG_WEB, PROJECT_NAME
|
||||
FORGE_TOKEN, FORGE_API, FORGE_REPO, FORGE_WEB, PROJECT_NAME
|
||||
(all sourced from ${FACTORY_ROOT}/.env)
|
||||
|
||||
### CRITICAL: Never embed secrets in issue bodies, comments, or PR descriptions
|
||||
- NEVER put API keys, tokens, passwords, or private keys in issue text or comments.
|
||||
- Always reference secrets via env var names (e.g. \\\$BASE_RPC_URL, \\\$CODEBERG_TOKEN).
|
||||
- Always reference secrets via env var names (e.g. \\\$BASE_RPC_URL, \\\${FORGE_TOKEN}).
|
||||
- If a formula step needs a secret, read it from .env or the environment at runtime.
|
||||
- Before posting any comment, verify it contains no credentials, hex keys > 32 chars,
|
||||
or URLs with embedded API keys.
|
||||
|
|
@ -330,9 +330,9 @@ _lifetime_watchdog() {
|
|||
# Post summary comment on issue
|
||||
local body="Action session killed: wall-clock lifetime cap (${hours}h) reached."
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/issues/${ISSUE}/comments" \
|
||||
"${FORGE_API}/issues/${ISSUE}/comments" \
|
||||
-d "{\"body\": \"${body}\"}" >/dev/null 2>&1 || true
|
||||
printf 'PHASE:failed\nReason: max_lifetime (%sh) reached\n' "$hours" > "$PHASE_FILE"
|
||||
# Touch phase-changed marker so monitor_phase_loop picks up immediately
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
433
bin/disinto
433
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 <name> Primary branch (default: auto-detect)
|
||||
--repo-root <path> Local clone path (default: ~/name)
|
||||
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
|
||||
--token <token> Codeberg API token (saved to ~/.netrc)
|
||||
--forge-url <url> Forge base URL (default: http://localhost:3000)
|
||||
--yes Skip confirmation prompts
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract org/repo slug from various URL formats.
|
||||
# Accepts: https://codeberg.org/user/repo, codeberg.org/user/repo,
|
||||
# user/repo, https://codeberg.org/user/repo.git
|
||||
# Accepts: https://github.com/user/repo, https://codeberg.org/user/repo,
|
||||
# http://localhost:3000/user/repo, user/repo, *.git
|
||||
parse_repo_slug() {
|
||||
local url="$1"
|
||||
url="${url#https://}"
|
||||
url="${url#http://}"
|
||||
url="${url#codeberg.org/}"
|
||||
# Strip any hostname (anything before the first / that contains a dot or colon)
|
||||
if [[ "$url" =~ ^[a-zA-Z0-9._:-]+/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+ ]]; then
|
||||
url="${url#*/}" # strip host part
|
||||
fi
|
||||
url="${url%.git}"
|
||||
url="${url%/}"
|
||||
if [[ ! "$url" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
|
||||
echo "Error: invalid repo URL — expected https://codeberg.org/org/repo or org/repo" >&2
|
||||
echo "Error: invalid repo URL — expected https://host/org/repo or org/repo" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "$url"
|
||||
}
|
||||
|
||||
# Build a clone-able URL from a slug.
|
||||
# Build a clone-able URL from a slug and forge URL.
|
||||
clone_url_from_slug() {
|
||||
printf 'https://codeberg.org/%s.git' "$1"
|
||||
local slug="$1" forge_url="${2:-${FORGE_URL:-http://localhost:3000}}"
|
||||
printf '%s/%s.git' "$forge_url" "$slug"
|
||||
}
|
||||
|
||||
# Write (or update) Codeberg credentials in ~/.netrc.
|
||||
# Write (or update) credentials in ~/.netrc for a given host.
|
||||
write_netrc() {
|
||||
local login="$1" token="$2"
|
||||
local host="$1" login="$2" token="$3"
|
||||
local netrc="${HOME}/.netrc"
|
||||
|
||||
# Remove existing codeberg.org entry if present
|
||||
# Remove existing entry for this host if present
|
||||
if [ -f "$netrc" ]; then
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
awk '
|
||||
/^machine codeberg\.org/ { skip=1; next }
|
||||
awk -v h="$host" '
|
||||
$0 ~ "^machine " h { skip=1; next }
|
||||
/^machine / { skip=0 }
|
||||
!skip
|
||||
' "$netrc" > "$tmp"
|
||||
|
|
@ -76,93 +80,252 @@ write_netrc() {
|
|||
fi
|
||||
|
||||
# Append new entry
|
||||
printf 'machine codeberg.org\nlogin %s\npassword %s\n' "$login" "$token" >> "$netrc"
|
||||
printf 'machine %s\nlogin %s\npassword %s\n' "$host" "$login" "$token" >> "$netrc"
|
||||
chmod 600 "$netrc"
|
||||
}
|
||||
|
||||
# Interactively set up Codeberg auth if missing.
|
||||
# Args: [token_from_flag]
|
||||
setup_codeberg_auth() {
|
||||
local token_flag="${1:-}"
|
||||
local repo_slug="${2:-}"
|
||||
FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
|
||||
|
||||
# --token flag takes priority: verify and save
|
||||
if [ -n "$token_flag" ]; then
|
||||
local verify_url="https://codeberg.org/api/v1/repos/${repo_slug}"
|
||||
if ! curl -sf --max-time 10 \
|
||||
-H "Authorization: token ${token_flag}" \
|
||||
"$verify_url" >/dev/null 2>&1; then
|
||||
echo "Error: provided token failed verification" >&2
|
||||
exit 1
|
||||
fi
|
||||
write_netrc "token" "$token_flag"
|
||||
echo "Saving to ~/.netrc... done."
|
||||
echo "Verified: token accepted ✓"
|
||||
export CODEBERG_TOKEN="$token_flag"
|
||||
return
|
||||
fi
|
||||
# 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"
|
||||
|
||||
# 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
|
||||
echo ""
|
||||
echo "── Forge setup ────────────────────────────────────────"
|
||||
|
||||
# Non-interactive — fail with guidance
|
||||
if [ ! -t 0 ]; then
|
||||
echo "Error: no Codeberg auth found" >&2
|
||||
echo " Set CODEBERG_TOKEN, configure ~/.netrc, or use --token <token>" >&2
|
||||
# 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
|
||||
|
||||
# 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 ""
|
||||
# Create data directory
|
||||
mkdir -p "${FORGEJO_DATA_DIR}"
|
||||
|
||||
while true; do
|
||||
read -rsp "Codeberg token: " token_input
|
||||
echo ""
|
||||
# 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}"
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
write_netrc "token" "$token_input"
|
||||
echo "Saving to ~/.netrc... done."
|
||||
echo "Verified: token accepted ✓"
|
||||
export CODEBERG_TOKEN="$token_input"
|
||||
return
|
||||
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
|
||||
|
||||
# Generate token via API (using admin credentials for the bot)
|
||||
local token
|
||||
token=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${admin_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${forge_url}/api/v1/users/${bot_user}/tokens" \
|
||||
-d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
||||
| jq -r '.sha1 // empty') || token=""
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
# Token name collision — create with timestamp suffix
|
||||
token=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${admin_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${forge_url}/api/v1/users/${bot_user}/tokens" \
|
||||
-d "{\"name\":\"disinto-${bot_user}-$(date +%s)\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
||||
| jq -r '.sha1 // empty') || token=""
|
||||
fi
|
||||
|
||||
if [ "$bot_user" = "dev-bot" ]; then
|
||||
dev_token="$token"
|
||||
else
|
||||
review_token="$token"
|
||||
fi
|
||||
done
|
||||
|
||||
# Store tokens in .env
|
||||
local env_file="${FACTORY_ROOT}/.env"
|
||||
if [ -n "$dev_token" ]; then
|
||||
if grep -q '^FORGE_TOKEN=' "$env_file" 2>/dev/null; then
|
||||
sed -i "s|^FORGE_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file"
|
||||
elif grep -q '^CODEBERG_TOKEN=' "$env_file" 2>/dev/null; then
|
||||
sed -i "s|^CODEBERG_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file"
|
||||
else
|
||||
printf '\nFORGE_TOKEN=%s\n' "$dev_token" >> "$env_file"
|
||||
fi
|
||||
export FORGE_TOKEN="$dev_token"
|
||||
export CODEBERG_TOKEN="$dev_token"
|
||||
echo " dev-bot token saved"
|
||||
fi
|
||||
|
||||
if [ -n "$review_token" ]; then
|
||||
if grep -q '^FORGE_REVIEW_TOKEN=' "$env_file" 2>/dev/null; then
|
||||
sed -i "s|^FORGE_REVIEW_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file"
|
||||
elif grep -q '^REVIEW_BOT_TOKEN=' "$env_file" 2>/dev/null; then
|
||||
sed -i "s|^REVIEW_BOT_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file"
|
||||
else
|
||||
printf 'FORGE_REVIEW_TOKEN=%s\n' "$review_token" >> "$env_file"
|
||||
fi
|
||||
export FORGE_REVIEW_TOKEN="$review_token"
|
||||
export REVIEW_BOT_TOKEN="$review_token"
|
||||
echo " review-bot token saved"
|
||||
fi
|
||||
|
||||
# Store FORGE_URL in .env if not already present
|
||||
if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then
|
||||
printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file"
|
||||
fi
|
||||
|
||||
# Create the repo on Forgejo if it doesn't exist
|
||||
local org_name="${repo_slug%%/*}"
|
||||
local repo_name="${repo_slug##*/}"
|
||||
|
||||
# Check if repo already exists
|
||||
if ! curl -sf --max-time 5 \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
|
||||
|
||||
# Try creating org first (ignore if exists)
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${forge_url}/api/v1/orgs" \
|
||||
-d "{\"username\":\"${org_name}\",\"visibility\":\"public\"}" >/dev/null 2>&1 || true
|
||||
|
||||
# Create repo under org
|
||||
if ! curl -sf -X POST \
|
||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${forge_url}/api/v1/orgs/${org_name}/repos" \
|
||||
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
|
||||
# Fallback: create under the dev-bot user
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${forge_url}/api/v1/user/repos" \
|
||||
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Add bot users as collaborators
|
||||
for bot_user in dev-bot review-bot; do
|
||||
curl -sf -X PUT \
|
||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${forge_url}/api/v1/repos/${repo_slug}/collaborators/${bot_user}" \
|
||||
-d '{"permission":"write"}' >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
echo "Repo: ${repo_slug} created on Forgejo"
|
||||
else
|
||||
echo "Repo: ${repo_slug} (already exists on Forgejo)"
|
||||
fi
|
||||
|
||||
echo "Forge: ${forge_url} (ready)"
|
||||
}
|
||||
|
||||
# Push local clone to the Forgejo remote.
|
||||
push_to_forge() {
|
||||
local repo_root="$1" forge_url="$2" repo_slug="$3"
|
||||
local remote_url="${forge_url}/${repo_slug}.git"
|
||||
|
||||
if git -C "$repo_root" remote get-url forgejo >/dev/null 2>&1; then
|
||||
echo "Remote: forgejo (already configured)"
|
||||
else
|
||||
git -C "$repo_root" remote add forgejo "$remote_url" 2>/dev/null || \
|
||||
git -C "$repo_root" remote set-url forgejo "$remote_url"
|
||||
echo "Remote: forgejo -> ${remote_url}"
|
||||
fi
|
||||
|
||||
# Push all branches
|
||||
git -C "$repo_root" push forgejo --all 2>/dev/null || true
|
||||
git -C "$repo_root" push forgejo --tags 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Preflight check — verify all factory requirements before proceeding.
|
||||
preflight_check() {
|
||||
local repo_slug="${1:-}"
|
||||
local forge_url="${2:-${FORGE_URL:-http://localhost:3000}}"
|
||||
local errors=0
|
||||
|
||||
# ── Required commands ──
|
||||
|
|
@ -207,37 +370,20 @@ preflight_check() {
|
|||
fi
|
||||
fi
|
||||
|
||||
# ── Codeberg auth (setup_codeberg_auth handles interactive setup;
|
||||
# this verifies the API actually works) ──
|
||||
if [ -n "${CODEBERG_TOKEN:-}" ] && command -v curl &>/dev/null; then
|
||||
local curl_args=(-sf --max-time 10)
|
||||
if [ -n "${CODEBERG_TOKEN:-}" ]; then
|
||||
curl_args+=(-H "Authorization: token ${CODEBERG_TOKEN}")
|
||||
else
|
||||
curl_args+=(--netrc)
|
||||
fi
|
||||
if ! curl "${curl_args[@]}" "https://codeberg.org/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
|
||||
echo "Error: Codeberg API auth failed" >&2
|
||||
echo " Verify your CODEBERG_TOKEN or ~/.netrc credentials" >&2
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Codeberg SSH access ──
|
||||
if command -v ssh &>/dev/null; then
|
||||
local ssh_output
|
||||
ssh_output=$(ssh -T -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
|
||||
git@codeberg.org 2>&1) || true
|
||||
if ! printf '%s' "$ssh_output" | grep -qi "successfully authenticated"; then
|
||||
echo "Error: Codeberg SSH access failed (agents push via SSH)" >&2
|
||||
echo " Add your SSH key: https://codeberg.org/user/settings/keys" >&2
|
||||
# ── Forge API check (verify the forge is reachable and token works) ──
|
||||
if [ -n "${FORGE_TOKEN:-}" ] && command -v curl &>/dev/null; then
|
||||
if ! curl -sf --max-time 10 \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
|
||||
echo "Error: Forge API auth failed at ${forge_url}" >&2
|
||||
echo " Verify your FORGE_TOKEN and that Forgejo is running" >&2
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Optional tools (warn only) ──
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo "Warning: docker not found (some projects may need it)" >&2
|
||||
echo "Warning: docker not found (needed for Forgejo provisioning)" >&2
|
||||
fi
|
||||
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
|
|
@ -249,13 +395,13 @@ preflight_check() {
|
|||
|
||||
# Clone the repo if the target directory doesn't exist; validate if it does.
|
||||
clone_or_validate() {
|
||||
local slug="$1" target="$2"
|
||||
local slug="$1" target="$2" forge_url="${3:-${FORGE_URL:-http://localhost:3000}}"
|
||||
if [ -d "${target}/.git" ]; then
|
||||
echo "Repo: ${target} (existing clone)"
|
||||
return
|
||||
fi
|
||||
local url
|
||||
url=$(clone_url_from_slug "$slug")
|
||||
url=$(clone_url_from_slug "$slug" "$forge_url")
|
||||
echo "Cloning: ${url} -> ${target}"
|
||||
git clone "$url" "$target"
|
||||
}
|
||||
|
|
@ -278,7 +424,7 @@ detect_branch() {
|
|||
|
||||
# Generate projects/<name>.toml config file.
|
||||
generate_toml() {
|
||||
local path="$1" name="$2" repo="$3" root="$4" branch="$5" ci_id="$6"
|
||||
local path="$1" name="$2" repo="$3" root="$4" branch="$5" ci_id="$6" forge_url="$7"
|
||||
cat > "$path" <<EOF
|
||||
# projects/${name}.toml — Project config for ${repo}
|
||||
#
|
||||
|
|
@ -286,6 +432,7 @@ generate_toml() {
|
|||
|
||||
name = "${name}"
|
||||
repo = "${repo}"
|
||||
forge_url = "${forge_url}"
|
||||
repo_root = "${root}"
|
||||
primary_branch = "${branch}"
|
||||
|
||||
|
|
@ -303,10 +450,11 @@ check_pipeline_stall = false
|
|||
EOF
|
||||
}
|
||||
|
||||
# Create standard labels on the Codeberg repo.
|
||||
# Create standard labels on the forge repo.
|
||||
create_labels() {
|
||||
local repo="$1"
|
||||
local api="https://codeberg.org/api/v1/repos/${repo}"
|
||||
local forge_url="${2:-${FORGE_URL:-http://localhost:3000}}"
|
||||
local api="${forge_url}/api/v1/repos/${repo}"
|
||||
|
||||
local -A labels=(
|
||||
["backlog"]="#0075ca"
|
||||
|
|
@ -323,7 +471,7 @@ create_labels() {
|
|||
for name in backlog in-progress blocked tech-debt underspecified vision action; do
|
||||
color="${labels[$name]}"
|
||||
if curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${api}/labels" \
|
||||
-d "{\"name\":\"${name}\",\"color\":\"${color}\"}" >/dev/null 2>&1; then
|
||||
|
|
@ -415,27 +563,31 @@ disinto_init() {
|
|||
shift
|
||||
|
||||
# Parse flags
|
||||
local branch="" repo_root="" ci_id="0" auto_yes=false token_flag=""
|
||||
local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--branch) branch="$2"; shift 2 ;;
|
||||
--repo-root) repo_root="$2"; shift 2 ;;
|
||||
--ci-id) ci_id="$2"; shift 2 ;;
|
||||
--token) token_flag="$2"; shift 2 ;;
|
||||
--forge-url) forge_url_flag="$2"; shift 2 ;;
|
||||
--yes) auto_yes=true; shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Extract org/repo slug
|
||||
local codeberg_repo
|
||||
codeberg_repo=$(parse_repo_slug "$repo_url")
|
||||
local project_name="${codeberg_repo##*/}"
|
||||
local forge_repo
|
||||
forge_repo=$(parse_repo_slug "$repo_url")
|
||||
local project_name="${forge_repo##*/}"
|
||||
local toml_path="${FACTORY_ROOT}/projects/${project_name}.toml"
|
||||
|
||||
# Determine forge URL (flag > env > default)
|
||||
local forge_url="${forge_url_flag:-${FORGE_URL:-http://localhost:3000}}"
|
||||
|
||||
echo "=== disinto init ==="
|
||||
echo "Project: ${codeberg_repo}"
|
||||
echo "Project: ${forge_repo}"
|
||||
echo "Name: ${project_name}"
|
||||
echo "Forge: ${forge_url}"
|
||||
|
||||
# Check for existing config
|
||||
local toml_exists=false
|
||||
|
|
@ -492,17 +644,27 @@ p.write_text(text)
|
|||
fi
|
||||
fi
|
||||
|
||||
# Set up Codeberg auth (interactive if needed, before preflight)
|
||||
setup_codeberg_auth "$token_flag" "$codeberg_repo"
|
||||
# Set up local Forgejo instance (provision if needed, create users/tokens/repo)
|
||||
setup_forge "$forge_url" "$forge_repo"
|
||||
|
||||
# Preflight: verify factory requirements
|
||||
preflight_check "$codeberg_repo"
|
||||
preflight_check "$forge_repo" "$forge_url"
|
||||
|
||||
# Determine repo root (for new projects)
|
||||
repo_root="${repo_root:-/home/${USER}/${project_name}}"
|
||||
|
||||
# Clone or validate
|
||||
clone_or_validate "$codeberg_repo" "$repo_root"
|
||||
# Clone or validate (try origin first for initial clone from upstream)
|
||||
if [ ! -d "${repo_root}/.git" ]; then
|
||||
# For initial setup, clone from the provided URL directly
|
||||
echo "Cloning: ${repo_url} -> ${repo_root}"
|
||||
git clone "$repo_url" "$repo_root" 2>/dev/null || \
|
||||
clone_or_validate "$forge_repo" "$repo_root" "$forge_url"
|
||||
else
|
||||
echo "Repo: ${repo_root} (existing clone)"
|
||||
fi
|
||||
|
||||
# Push to local Forgejo
|
||||
push_to_forge "$repo_root" "$forge_url" "$forge_repo"
|
||||
|
||||
# Detect primary branch
|
||||
if [ -z "$branch" ]; then
|
||||
|
|
@ -518,12 +680,12 @@ p.write_text(text)
|
|||
ci_id="${user_ci_id:-0}"
|
||||
fi
|
||||
|
||||
generate_toml "$toml_path" "$project_name" "$codeberg_repo" "$repo_root" "$branch" "$ci_id"
|
||||
generate_toml "$toml_path" "$project_name" "$forge_repo" "$repo_root" "$branch" "$ci_id" "$forge_url"
|
||||
echo "Created: ${toml_path}"
|
||||
fi
|
||||
|
||||
# Create labels on remote
|
||||
create_labels "$codeberg_repo"
|
||||
create_labels "$forge_repo" "$forge_url"
|
||||
|
||||
# Generate VISION.md template
|
||||
generate_vision "$repo_root" "$project_name"
|
||||
|
|
@ -535,6 +697,7 @@ p.write_text(text)
|
|||
echo "Done. Project ${project_name} is ready."
|
||||
echo " Config: ${toml_path}"
|
||||
echo " Clone: ${repo_root}"
|
||||
echo " Forge: ${forge_url}/${forge_repo}"
|
||||
echo " Run 'disinto status' to verify."
|
||||
}
|
||||
|
||||
|
|
@ -548,8 +711,8 @@ disinto_status() {
|
|||
[ -f "$toml" ] || continue
|
||||
found=true
|
||||
|
||||
# Parse name and repo from TOML
|
||||
local pname prepo
|
||||
# Parse name, repo, forge_url from TOML
|
||||
local pname prepo pforge_url
|
||||
pname=$(python3 -c "
|
||||
import sys, tomllib
|
||||
with open(sys.argv[1], 'rb') as f:
|
||||
|
|
@ -560,6 +723,12 @@ import sys, tomllib
|
|||
with open(sys.argv[1], 'rb') as f:
|
||||
print(tomllib.load(f)['repo'])
|
||||
" "$toml" 2>/dev/null) || continue
|
||||
pforge_url=$(python3 -c "
|
||||
import sys, tomllib
|
||||
with open(sys.argv[1], 'rb') as f:
|
||||
print(tomllib.load(f).get('forge_url', ''))
|
||||
" "$toml" 2>/dev/null) || pforge_url=""
|
||||
pforge_url="${pforge_url:-${FORGE_URL:-http://localhost:3000}}"
|
||||
|
||||
echo "== ${pname} (${prepo}) =="
|
||||
|
||||
|
|
@ -578,24 +747,24 @@ with open(sys.argv[1], 'rb') as f:
|
|||
fi
|
||||
|
||||
# Backlog depth via API
|
||||
if [ -n "${CODEBERG_TOKEN:-}" ]; then
|
||||
local api="https://codeberg.org/api/v1/repos/${prepo}"
|
||||
if [ -n "${FORGE_TOKEN:-}" ]; then
|
||||
local api="${pforge_url}/api/v1/repos/${prepo}"
|
||||
local backlog_count pr_count
|
||||
|
||||
backlog_count=$(curl -sf -I \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${api}/issues?state=open&labels=backlog&limit=1" 2>/dev/null \
|
||||
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || backlog_count="?"
|
||||
echo " Backlog: ${backlog_count:-0} issues"
|
||||
|
||||
pr_count=$(curl -sf -I \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${api}/pulls?state=open&limit=1" 2>/dev/null \
|
||||
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || pr_count="?"
|
||||
echo " Open PRs: ${pr_count:-0}"
|
||||
else
|
||||
echo " Backlog: (no CODEBERG_TOKEN)"
|
||||
echo " Open PRs: (no CODEBERG_TOKEN)"
|
||||
echo " Backlog: (no FORGE_TOKEN)"
|
||||
echo " Open PRs: (no FORGE_TOKEN)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,21 +34,21 @@ git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true
|
|||
# --- Config ---
|
||||
ISSUE="${1:?Usage: dev-agent.sh <issue-number>}"
|
||||
# shellcheck disable=SC2034
|
||||
REPO="${CODEBERG_REPO}"
|
||||
REPO="${FORGE_REPO}"
|
||||
# shellcheck disable=SC2034
|
||||
REPO_ROOT="${PROJECT_REPO_ROOT}"
|
||||
|
||||
API="${CODEBERG_API}"
|
||||
API="${FORGE_API}"
|
||||
LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock"
|
||||
STATUSFILE="/tmp/dev-agent-status-${PROJECT_NAME:-default}"
|
||||
|
||||
# Gitea labels API requires []int64 — look up the "backlog" label ID once
|
||||
BACKLOG_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
|
||||
BACKLOG_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
||||
| jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true)
|
||||
BACKLOG_LABEL_ID="${BACKLOG_LABEL_ID:-1300815}"
|
||||
|
||||
# Same for "in-progress" label
|
||||
IN_PROGRESS_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
|
||||
IN_PROGRESS_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
||||
| jq -r '.[] | select(.name == "in-progress") | .id' 2>/dev/null || true)
|
||||
IN_PROGRESS_LABEL_ID="${IN_PROGRESS_LABEL_ID:-1300818}"
|
||||
|
||||
|
|
@ -128,14 +128,14 @@ cleanup_worktree() {
|
|||
|
||||
cleanup_labels() {
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${ISSUE}/labels/${IN_PROGRESS_LABEL_ID}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
restore_to_backlog() {
|
||||
cleanup_labels
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/labels" \
|
||||
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||
|
|
@ -151,10 +151,10 @@ cleanup() {
|
|||
if [ "$CLAIMED" = true ] && [ -z "${PR_NUMBER:-}" ]; then
|
||||
log "cleanup: unclaiming issue (no PR created)"
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${ISSUE}/labels/${IN_PROGRESS_LABEL_ID}" >/dev/null 2>&1 || true
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/labels" \
|
||||
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||
|
|
@ -198,7 +198,7 @@ echo $$ > "$LOCKFILE"
|
|||
# FETCH ISSUE
|
||||
# =============================================================================
|
||||
status "fetching issue"
|
||||
ISSUE_JSON=$(curl -s -H "Authorization: token ${CODEBERG_TOKEN}" "${API}/issues/${ISSUE}") || true
|
||||
ISSUE_JSON=$(curl -s -H "Authorization: token ${FORGE_TOKEN}" "${API}/issues/${ISSUE}") || true
|
||||
if [ -z "$ISSUE_JSON" ] || ! echo "$ISSUE_JSON" | jq -e '.id' >/dev/null 2>&1; then
|
||||
log "ERROR: failed to fetch issue #${ISSUE} (API down or invalid response)"
|
||||
exit 1
|
||||
|
|
@ -208,17 +208,17 @@ ISSUE_BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""')
|
|||
ISSUE_BODY_ORIGINAL="$ISSUE_BODY"
|
||||
|
||||
# --- Resolve bot username(s) for comment filtering ---
|
||||
_bot_login=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
_bot_login=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true)
|
||||
|
||||
# Build list: token owner + any extra names from CODEBERG_BOT_USERNAMES (comma-separated)
|
||||
# Build list: token owner + any extra names from FORGE_BOT_USERNAMES (comma-separated)
|
||||
_bot_logins="${_bot_login}"
|
||||
if [ -n "${CODEBERG_BOT_USERNAMES:-}" ]; then
|
||||
_bot_logins="${_bot_logins:+${_bot_logins},}${CODEBERG_BOT_USERNAMES}"
|
||||
if [ -n "${FORGE_BOT_USERNAMES:-}" ]; then
|
||||
_bot_logins="${_bot_logins:+${_bot_logins},}${FORGE_BOT_USERNAMES}"
|
||||
fi
|
||||
|
||||
# Append human comments to issue body (filter out bot accounts)
|
||||
ISSUE_COMMENTS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
ISSUE_COMMENTS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${ISSUE}/comments" | \
|
||||
jq -r --arg bots "$_bot_logins" \
|
||||
'($bots | split(",") | map(select(. != ""))) as $bl |
|
||||
|
|
@ -264,7 +264,7 @@ if [ -n "$DEP_NUMBERS" ]; then
|
|||
while IFS= read -r dep_num; do
|
||||
[ -z "$dep_num" ] && continue
|
||||
# Check if dependency issue is closed (= satisfied)
|
||||
DEP_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
DEP_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${dep_num}" | jq -r '.state // "unknown"')
|
||||
|
||||
if [ "$DEP_STATE" != "closed" ]; then
|
||||
|
|
@ -280,9 +280,9 @@ if [ "${#BLOCKED_BY[@]}" -gt 0 ]; then
|
|||
# Find a suggestion: look for the first blocker that itself has no unmet deps
|
||||
SUGGESTION=""
|
||||
for blocker in "${BLOCKED_BY[@]}"; do
|
||||
BLOCKER_BODY=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
BLOCKER_BODY=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${blocker}" | jq -r '.body // ""')
|
||||
BLOCKER_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
BLOCKER_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${blocker}" | jq -r '.state')
|
||||
|
||||
if [ "$BLOCKER_STATE" != "open" ]; then
|
||||
|
|
@ -302,7 +302,7 @@ if [ "${#BLOCKED_BY[@]}" -gt 0 ]; then
|
|||
if [ -n "$BLOCKER_DEPS" ]; then
|
||||
while IFS= read -r bd; do
|
||||
[ -z "$bd" ] && continue
|
||||
BD_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
BD_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${bd}" | jq -r '.state // "unknown"')
|
||||
if [ "$BD_STATE" != "closed" ]; then
|
||||
BLOCKER_BLOCKED=true
|
||||
|
|
@ -329,7 +329,7 @@ if [ "${#BLOCKED_BY[@]}" -gt 0 ]; then
|
|||
|
||||
# Post comment ONLY if last comment isn't already an unmet dependency notice
|
||||
BLOCKED_LIST=$(printf '#%s, ' "${BLOCKED_BY[@]}" | sed 's/, $//')
|
||||
LAST_COMMENT_IS_BLOCK=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
LAST_COMMENT_IS_BLOCK=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${ISSUE}/comments?limit=1" | \
|
||||
jq -r '.[0].body // ""' | grep -c 'Dev-agent: Unmet dependency' || true)
|
||||
|
||||
|
|
@ -352,7 +352,7 @@ This issue depends on ${BLOCKED_LIST}, which $(if [ "${#BLOCKED_BY[@]}" -eq 1 ];
|
|||
printf '%s' "$BLOCK_COMMENT" > /tmp/block-comment.txt
|
||||
jq -Rs '{body: .}' < /tmp/block-comment.txt > /tmp/block-comment.json
|
||||
curl -sf -o /dev/null -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/comments" \
|
||||
--data-binary @/tmp/block-comment.json 2>/dev/null || true
|
||||
|
|
@ -373,13 +373,13 @@ log "preflight passed — no explicit unmet dependencies"
|
|||
# CLAIM ISSUE
|
||||
# =============================================================================
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/labels" \
|
||||
-d "{\"labels\":[${IN_PROGRESS_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${ISSUE}/labels/${BACKLOG_LABEL_ID}" >/dev/null 2>&1 || true
|
||||
|
||||
CLAIMED=true
|
||||
|
|
@ -393,7 +393,7 @@ RECOVERY_MODE=false
|
|||
|
||||
BODY_PR=$(echo "$ISSUE_BODY_ORIGINAL" | grep -oP 'Existing PR:\s*#\K[0-9]+' | head -1) || true
|
||||
if [ -n "$BODY_PR" ]; then
|
||||
PR_CHECK=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
PR_CHECK=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${BODY_PR}" | jq -r '{state, head_ref: .head.ref}')
|
||||
PR_CHECK_STATE=$(echo "$PR_CHECK" | jq -r '.state')
|
||||
if [ "$PR_CHECK_STATE" = "open" ]; then
|
||||
|
|
@ -405,7 +405,7 @@ fi
|
|||
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
# Priority 1: match by branch name (most reliable)
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls?state=open&limit=20" | \
|
||||
jq -r --arg branch "$BRANCH" \
|
||||
'.[] | select(.head.ref == $branch) | "\(.number) \(.head.ref)"' | head -1) || true
|
||||
|
|
@ -418,7 +418,7 @@ fi
|
|||
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
# Priority 2: match "Fixes #NNN" in PR body
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls?state=open&limit=20" | \
|
||||
jq -r --arg issue "ixes #${ISSUE}\\b" \
|
||||
'.[] | select(.body | test($issue; "i")) | "\(.number) \(.head.ref)"' | head -1) || true
|
||||
|
|
@ -432,14 +432,14 @@ fi
|
|||
# Priority 3: check CLOSED PRs for prior art (don't redo work from scratch)
|
||||
PRIOR_ART_DIFF=""
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
CLOSED_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
CLOSED_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls?state=closed&limit=30" | \
|
||||
jq -r --arg issue "#${ISSUE}" \
|
||||
'.[] | select(.merged != true) | select((.title | contains($issue)) or (.body // "" | test("ixes " + $issue + "\\b"; "i"))) | "\(.number) \(.head.ref)"' | head -1) || true
|
||||
if [ -n "$CLOSED_PR" ]; then
|
||||
CLOSED_PR_NUM=$(echo "$CLOSED_PR" | awk '{print $1}')
|
||||
log "found closed (unmerged) PR #${CLOSED_PR_NUM} as prior art"
|
||||
PRIOR_ART_DIFF=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
PRIOR_ART_DIFF=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${CLOSED_PR_NUM}.diff" | head -500) || true
|
||||
if [ -n "$PRIOR_ART_DIFF" ]; then
|
||||
log "captured prior art diff from PR #${CLOSED_PR_NUM} ($(echo "$PRIOR_ART_DIFF" | wc -l) lines)"
|
||||
|
|
@ -530,7 +530,7 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
|||
# =============================================================================
|
||||
# BUILD PROMPT
|
||||
# =============================================================================
|
||||
OPEN_ISSUES_SUMMARY=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
OPEN_ISSUES_SUMMARY=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues?state=open&labels=backlog&limit=20&type=issues" | \
|
||||
jq -r '.[] | "#\(.number) \(.title)"' 2>/dev/null || echo "(could not fetch)")
|
||||
|
||||
|
|
@ -607,12 +607,12 @@ if [ "$RECOVERY_MODE" = true ]; then
|
|||
GIT_DIFF_STAT=$(git -C "$WORKTREE" diff "origin/${PRIMARY_BRANCH}..HEAD" --stat 2>/dev/null | head -20 || echo "(no diff)")
|
||||
LAST_PHASE=$(read_phase)
|
||||
CI_RESULT=$(cat "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" 2>/dev/null || echo "")
|
||||
REVIEW_COMMENTS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
REVIEW_COMMENTS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${PR_NUMBER}/comments?limit=10" | \
|
||||
jq -r '.[-3:] | .[] | "[\(.user.login)] \(.body[:500])"' 2>/dev/null || echo "(none)")
|
||||
|
||||
INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}.
|
||||
This is issue #${ISSUE} for the ${CODEBERG_REPO} project.
|
||||
This is issue #${ISSUE} for the ${FORGE_REPO} project.
|
||||
|
||||
## Issue: ${ISSUE_TITLE}
|
||||
|
||||
|
|
@ -647,7 +647,7 @@ ${PHASE_PROTOCOL_INSTRUCTIONS}"
|
|||
else
|
||||
# Normal mode: initial implementation prompt
|
||||
INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}.
|
||||
You have been assigned issue #${ISSUE} for the ${CODEBERG_REPO} project.
|
||||
You have been assigned issue #${ISSUE} for the ${FORGE_REPO} project.
|
||||
|
||||
## Issue: ${ISSUE_TITLE}
|
||||
|
||||
|
|
@ -713,7 +713,7 @@ fi
|
|||
# CREATE MATRIX THREAD (before tmux so MATRIX_THREAD_ID is available for Stop hook)
|
||||
# =============================================================================
|
||||
if [ ! -f "${THREAD_FILE}" ] || [ -z "$(cat "$THREAD_FILE" 2>/dev/null)" ]; then
|
||||
ISSUE_URL="${CODEBERG_WEB}/issues/${ISSUE}"
|
||||
ISSUE_URL="${FORGE_WEB}/issues/${ISSUE}"
|
||||
_thread_id=$(matrix_send_ctx "dev" \
|
||||
"🔧 Issue #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \
|
||||
"🔧 <a href='${ISSUE_URL}'>Issue #${ISSUE}</a>: ${ISSUE_TITLE}") || true
|
||||
|
|
@ -760,11 +760,11 @@ case "${_MONITOR_LOOP_EXIT:-}" in
|
|||
if [ "${_MONITOR_LOOP_EXIT:-}" = "idle_prompt" ]; then
|
||||
notify_ctx \
|
||||
"session finished without phase signal — killed. Marking blocked." \
|
||||
"session finished without phase signal — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
"session finished without phase signal — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
else
|
||||
notify_ctx \
|
||||
"session idle for 2h — killed. Marking blocked." \
|
||||
"session idle for 2h — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
"session idle for 2h — killed. Marking blocked.${PR_NUMBER:+ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
fi
|
||||
# Post diagnostic comment + label issue blocked
|
||||
post_blocked_diagnostic "${_MONITOR_LOOP_EXIT:-idle_timeout}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Defines: post_refusal_comment(), _on_phase_change(), build_phase_protocol_prompt()
|
||||
#
|
||||
# Required globals (set by calling agent before or after sourcing):
|
||||
# ISSUE, CODEBERG_TOKEN, API, CODEBERG_WEB, PROJECT_NAME, FACTORY_ROOT
|
||||
# ISSUE, FORGE_TOKEN, API, FORGE_WEB, PROJECT_NAME, FACTORY_ROOT
|
||||
# BRANCH, PHASE_FILE, WORKTREE, IMPL_SUMMARY_FILE, THREAD_FILE
|
||||
# PRIMARY_BRANCH, SESSION_NAME, LOGFILE, ISSUE_TITLE
|
||||
# WOODPECKER_REPO_ID, WOODPECKER_TOKEN, WOODPECKER_SERVER
|
||||
|
|
@ -47,7 +47,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/../lib/ci-helpers.sh"
|
|||
# in-progress label, and adds the "blocked" label.
|
||||
#
|
||||
# Args: reason [session_name]
|
||||
# Uses globals: ISSUE, SESSION_NAME, PR_NUMBER, CODEBERG_TOKEN, API
|
||||
# Uses globals: ISSUE, SESSION_NAME, PR_NUMBER, FORGE_TOKEN, API
|
||||
post_blocked_diagnostic() {
|
||||
local reason="$1"
|
||||
local session="${2:-${SESSION_NAME:-}}"
|
||||
|
|
@ -88,7 +88,7 @@ ${tmux_output}
|
|||
|
||||
# Post comment to issue
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/comments" \
|
||||
-d "$(jq -nc --arg b "$comment" '{body:$b}')" >/dev/null 2>&1 || true
|
||||
|
|
@ -99,7 +99,7 @@ ${tmux_output}
|
|||
blocked_id=$(ensure_blocked_label_id)
|
||||
if [ -n "$blocked_id" ]; then
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/labels" \
|
||||
-d "{\"labels\":[${blocked_id}]}" >/dev/null 2>&1 || true
|
||||
|
|
@ -173,7 +173,7 @@ _PHASE_PROTOCOL_EOF_
|
|||
}
|
||||
|
||||
# --- Merge helper ---
|
||||
# do_merge — attempt to merge PR via Codeberg API.
|
||||
# do_merge — attempt to merge PR via forge API.
|
||||
# Args: pr_num
|
||||
# Returns:
|
||||
# 0 = merged successfully
|
||||
|
|
@ -183,7 +183,7 @@ do_merge() {
|
|||
local pr_num="$1"
|
||||
local merge_response merge_http_code merge_body
|
||||
merge_response=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${API}/pulls/${pr_num}/merge" \
|
||||
-d '{"Do":"merge","delete_branch_after_merge":true}') || true
|
||||
|
|
@ -199,7 +199,7 @@ do_merge() {
|
|||
# Before escalating, check whether the PR was already merged by another agent.
|
||||
if [ "$merge_http_code" = "405" ]; then
|
||||
local pr_state
|
||||
pr_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
pr_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${pr_num}" | jq -r '.merged // false') || pr_state="false"
|
||||
if [ "$pr_state" = "true" ]; then
|
||||
log "do_merge: PR #${pr_num} already merged (detected after HTTP 405) — treating as success"
|
||||
|
|
@ -220,7 +220,7 @@ do_merge() {
|
|||
post_refusal_comment() {
|
||||
local emoji="$1" title="$2" body="$3"
|
||||
local last_has_title
|
||||
last_has_title=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
last_has_title=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${ISSUE}/comments?limit=5" | \
|
||||
jq -r --arg t "Dev-agent: ${title}" '[.[] | .body // ""] | any(contains($t)) | tostring') || true
|
||||
if [ "$last_has_title" = "true" ]; then
|
||||
|
|
@ -237,7 +237,7 @@ ${body}
|
|||
printf '%s' "$comment" > "/tmp/refusal-comment.txt"
|
||||
jq -Rs '{body: .}' < "/tmp/refusal-comment.txt" > "/tmp/refusal-comment.json"
|
||||
curl -sf -o /dev/null -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/comments" \
|
||||
--data-binary @"/tmp/refusal-comment.json" 2>/dev/null || \
|
||||
|
|
@ -278,7 +278,7 @@ _on_phase_change() {
|
|||
'{title: $title, body: $body, head: $head, base: $base}' > "/tmp/pr-request-${ISSUE}.json"
|
||||
|
||||
PR_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/pulls" \
|
||||
--data-binary @"/tmp/pr-request-${ISSUE}.json")
|
||||
|
|
@ -290,13 +290,13 @@ _on_phase_change() {
|
|||
if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then
|
||||
PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number')
|
||||
log "created PR #${PR_NUMBER}"
|
||||
PR_URL="${CODEBERG_WEB}/pulls/${PR_NUMBER}"
|
||||
PR_URL="${FORGE_WEB}/pulls/${PR_NUMBER}"
|
||||
notify_ctx \
|
||||
"PR #${PR_NUMBER} created: ${ISSUE_TITLE}" \
|
||||
"PR <a href='${PR_URL}'>#${PR_NUMBER}</a> created: ${ISSUE_TITLE}"
|
||||
elif [ "$PR_HTTP_CODE" = "409" ]; then
|
||||
# PR already exists (race condition) — find it
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls?state=open&limit=20" | \
|
||||
jq -r --arg branch "$BRANCH" \
|
||||
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
||||
|
|
@ -305,7 +305,7 @@ _on_phase_change() {
|
|||
log "PR already exists: #${PR_NUMBER}"
|
||||
else
|
||||
log "ERROR: PR creation got 409 but no existing PR found"
|
||||
agent_inject_into_session "$SESSION_NAME" "ERROR: Could not create PR (HTTP 409, no existing PR found). Check the Codeberg API. Retry by writing PHASE:awaiting_ci again after verifying the branch was pushed."
|
||||
agent_inject_into_session "$SESSION_NAME" "ERROR: Could not create PR (HTTP 409, no existing PR found). Check the forge API. Retry by writing PHASE:awaiting_ci again after verifying the branch was pushed."
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
|
|
@ -327,7 +327,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
|||
# Poll CI until done or timeout
|
||||
status "waiting for CI on PR #${PR_NUMBER}"
|
||||
CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || \
|
||||
curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha')
|
||||
|
||||
CI_DONE=false
|
||||
|
|
@ -346,7 +346,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
|||
# Re-fetch HEAD — Claude may have pushed new commits since loop started
|
||||
CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || echo "$CI_CURRENT_SHA")
|
||||
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/commits/${CI_CURRENT_SHA}/status" | jq -r '.state // "unknown"')
|
||||
if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
|
||||
CI_DONE=true
|
||||
|
|
@ -370,7 +370,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
|||
echo \"PHASE:awaiting_review\" > \"${PHASE_FILE}\""
|
||||
else
|
||||
# Fetch CI error details
|
||||
PIPELINE_NUM=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
PIPELINE_NUM=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/commits/${CI_CURRENT_SHA}/status" | \
|
||||
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
|
||||
|
||||
|
|
@ -411,7 +411,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
|||
log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating"
|
||||
notify_ctx \
|
||||
"CI exhausted after ${CI_FIX_COUNT} attempts — escalating for human help" \
|
||||
"CI exhausted after ${CI_FIX_COUNT} attempts on PR <a href='${PR_URL:-${CODEBERG_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline</a><br>Step: <code>${FAILED_STEP:-unknown}</code> — escalating for human help"
|
||||
"CI exhausted after ${CI_FIX_COUNT} attempts on PR <a href='${PR_URL:-${FORGE_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline</a><br>Step: <code>${FAILED_STEP:-unknown}</code> — escalating for human help"
|
||||
printf 'PHASE:escalate\nReason: ci_exhausted after %d attempts (step: %s)\n' "$CI_FIX_COUNT" "${FAILED_STEP:-unknown}" > "$PHASE_FILE"
|
||||
# Do NOT update LAST_PHASE_MTIME here — let the main loop detect PHASE:escalate
|
||||
return 0
|
||||
|
|
@ -431,7 +431,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
|||
_ci_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500 | sed 's/&/\&/g; 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 <a href='${PR_URL:-${CODEBERG_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline #${PIPELINE_NUM:-?}</a><br>Step: <code>${FAILED_STEP:-unknown}</code> (exit ${FAILED_EXIT:-?})<br>Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}<br><pre>${_ci_snippet:-no logs}</pre>"
|
||||
"CI failed on PR <a href='${PR_URL:-${FORGE_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline #${PIPELINE_NUM:-?}</a><br>Step: <code>${FAILED_STEP:-unknown}</code> (exit ${FAILED_EXIT:-?})<br>Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}<br><pre>${_ci_snippet:-no logs}</pre>"
|
||||
|
||||
agent_inject_into_session "$SESSION_NAME" "CI failed on PR #${PR_NUMBER} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}).
|
||||
|
||||
|
|
@ -460,7 +460,7 @@ Instructions:
|
|||
|
||||
if [ -z "${PR_NUMBER:-}" ]; then
|
||||
log "WARNING: awaiting_review but PR_NUMBER unknown — searching for PR"
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FOUND_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls?state=open&limit=20" | \
|
||||
jq -r --arg branch "$BRANCH" \
|
||||
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
||||
|
|
@ -498,9 +498,9 @@ Instructions:
|
|||
break
|
||||
fi
|
||||
|
||||
REVIEW_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
REVIEW_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha') || true
|
||||
REVIEW_COMMENT=$(codeberg_api_all "/issues/${PR_NUMBER}/comments" | \
|
||||
REVIEW_COMMENT=$(forge_api_all "/issues/${PR_NUMBER}/comments" | \
|
||||
jq -r --arg sha "$REVIEW_SHA" \
|
||||
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
||||
|
||||
|
|
@ -516,9 +516,9 @@ Instructions:
|
|||
VERDICT=$(echo "$REVIEW_TEXT" | grep -oP '\*\*(APPROVE|REQUEST_CHANGES|DISCUSS)\*\*' | head -1 | tr -d '*' || true)
|
||||
log "review verdict: ${VERDICT:-unknown}"
|
||||
|
||||
# Also check formal Codeberg reviews
|
||||
# Also check formal forge reviews
|
||||
if [ -z "$VERDICT" ]; then
|
||||
VERDICT=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
VERDICT=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}/reviews" | \
|
||||
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
|
||||
if [ "$VERDICT" = "APPROVED" ]; then
|
||||
|
|
@ -548,7 +548,7 @@ Instructions:
|
|||
if [ "$_merge_rc" -eq 0 ]; then
|
||||
# Merge succeeded — close issue and signal done
|
||||
curl -sf -X PATCH \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${API}/issues/${ISSUE}" \
|
||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||
|
|
@ -596,7 +596,7 @@ Instructions:
|
|||
fi
|
||||
|
||||
# Check if PR was merged or closed externally
|
||||
PR_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
PR_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}") || true
|
||||
PR_STATE=$(echo "$PR_JSON" | jq -r '.state // "unknown"')
|
||||
PR_MERGED=$(echo "$PR_JSON" | jq -r '.merged // false')
|
||||
|
|
@ -605,8 +605,8 @@ Instructions:
|
|||
log "PR #${PR_NUMBER} was merged externally"
|
||||
notify_ctx \
|
||||
"✅ PR #${PR_NUMBER} merged externally! Issue #${ISSUE} done." \
|
||||
"✅ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged externally! <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"✅ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged externally! <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||
curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||
cleanup_labels
|
||||
|
|
@ -637,9 +637,9 @@ Instructions:
|
|||
elif [ "$phase" = "PHASE:escalate" ]; then
|
||||
status "escalated — waiting for human input on issue #${ISSUE}"
|
||||
ESCALATE_REASON=$(sed -n '2p' "$PHASE_FILE" 2>/dev/null | sed 's/^Reason: //' || echo "")
|
||||
_issue_url="${CODEBERG_WEB}/issues/${ISSUE}"
|
||||
_issue_url="${FORGE_WEB}/issues/${ISSUE}"
|
||||
_pr_link=""
|
||||
[ -n "${PR_NUMBER:-}" ] && _pr_link=" | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>"
|
||||
[ -n "${PR_NUMBER:-}" ] && _pr_link=" | PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>"
|
||||
notify_ctx \
|
||||
"⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) escalated — needs human input.${ESCALATE_REASON:+ Reason: ${ESCALATE_REASON}}" \
|
||||
"⚠️ <a href='${_issue_url}'>Issue #${ISSUE}</a>${_pr_link} escalated — needs human input.${ESCALATE_REASON:+ Reason: ${ESCALATE_REASON}}<br>Reply in this thread to send guidance to the agent."
|
||||
|
|
@ -653,12 +653,12 @@ Instructions:
|
|||
status "phase done — PR #${PR_NUMBER} merged, cleaning up"
|
||||
notify_ctx \
|
||||
"✅ PR #${PR_NUMBER} merged! Issue #${ISSUE} done." \
|
||||
"✅ PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged! <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||
"✅ PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged! <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||
else
|
||||
status "phase done — issue #${ISSUE} complete, cleaning up"
|
||||
notify_ctx \
|
||||
"✅ Issue #${ISSUE} done." \
|
||||
"✅ <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||
"✅ <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||
fi
|
||||
|
||||
# Belt-and-suspenders: ensure in-progress label removed (idempotent)
|
||||
|
|
@ -680,10 +680,10 @@ Instructions:
|
|||
FAILURE_REASON="${FAILURE_REASON:-unspecified}"
|
||||
log "phase: failed — reason: ${FAILURE_REASON}"
|
||||
# Gitea labels API requires []int64 — look up the "backlog" label ID once
|
||||
BACKLOG_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
|
||||
BACKLOG_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
||||
| jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true)
|
||||
BACKLOG_LABEL_ID="${BACKLOG_LABEL_ID:-1300815}"
|
||||
UNDERSPECIFIED_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
|
||||
UNDERSPECIFIED_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
||||
| jq -r '.[] | select(.name == "underspecified") | .id' 2>/dev/null || true)
|
||||
UNDERSPECIFIED_LABEL_ID="${UNDERSPECIFIED_LABEL_ID:-1300816}"
|
||||
|
||||
|
|
@ -703,7 +703,7 @@ Instructions:
|
|||
# Unclaim issue (restore backlog label, remove in-progress)
|
||||
cleanup_labels
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/labels" \
|
||||
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||
|
|
@ -732,12 +732,12 @@ ${REASON}
|
|||
### Next steps
|
||||
A maintainer should split this issue or add more detail to the spec."
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}/labels" \
|
||||
-d "{\"labels\":[${UNDERSPECIFIED_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/issues/${ISSUE}/labels/${BACKLOG_LABEL_ID}" >/dev/null 2>&1 || true
|
||||
notify "refused #${ISSUE}: too large — ${REASON}"
|
||||
;;
|
||||
|
|
@ -749,7 +749,7 @@ ${REASON}
|
|||
|
||||
Closing as already implemented."
|
||||
curl -sf -X PATCH \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE}" \
|
||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||
|
|
@ -779,7 +779,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
|
|||
log "session failed: ${FAILURE_REASON}"
|
||||
notify_ctx \
|
||||
"❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}" \
|
||||
"❌ <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> session failed: ${FAILURE_REASON}${PR_NUMBER:+ | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
"❌ <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> session failed: ${FAILURE_REASON}${PR_NUMBER:+ | PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
post_blocked_diagnostic "$FAILURE_REASON"
|
||||
|
||||
agent_kill_session "$SESSION_NAME"
|
||||
|
|
@ -801,7 +801,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
|
|||
log "session crashed for issue #${ISSUE}"
|
||||
notify_ctx \
|
||||
"session crashed unexpectedly — marking blocked" \
|
||||
"session crashed unexpectedly — marking blocked${PR_NUMBER:+ | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
"session crashed unexpectedly — marking blocked${PR_NUMBER:+ | PR <a href='${FORGE_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||
post_blocked_diagnostic "crashed"
|
||||
[ -z "${PR_NUMBER:-}" ] && cleanup_worktree
|
||||
[ -n "${PR_NUMBER:-}" ] && log "keeping worktree (PR #${PR_NUMBER} still open)"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Different domains have different platforms:
|
|||
|
||||
| Domain | Platform | What it tracks | Status |
|
||||
|--------|----------|---------------|--------|
|
||||
| Code | Codeberg | Issues, PRs, reviews | **Implemented** — Live |
|
||||
| Code | forge | Issues, PRs, reviews | **Implemented** — Live |
|
||||
| CI/CD | Woodpecker | Build/test results | **Implemented** — Live |
|
||||
| Protocol | Ponder / GraphQL | On-chain state, trades, positions | **Partial** — Live (not yet wired to evidence) |
|
||||
| Infrastructure | DigitalOcean / system stats | CPU, RAM, disk, containers | **Planned** — Supervisor monitors, no evidence output yet |
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ PHASE:escalate → send Matrix notification with context (issue/PR link,
|
|||
on reply → matrix_listener.sh injects reply into tmux session
|
||||
on timeout → 24h: label issue blocked, kill session
|
||||
|
||||
PHASE:done → verify PR merged on Codeberg
|
||||
PHASE:done → verify PR merged on forge
|
||||
if merged → kill tmux session, clean labels, close issue
|
||||
if not → inject "PR not merged yet" into session
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ a `mode` field. Two modes are supported:
|
|||
|
||||
In this mode, skip the normal tech-debt grooming pipeline. Instead:
|
||||
a. Fetch the target issue:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<target_issue>"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<target_issue>"
|
||||
b. Fetch ALL comments on the target issue to understand scope and
|
||||
prior bounce reasons:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<target_issue>/comments?limit=50"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<target_issue>/comments?limit=50"
|
||||
c. Read the affected files listed in the issue body to understand
|
||||
the actual code scope.
|
||||
d. Break the issue into 2-5 sub-issues, each sized for a single
|
||||
|
|
@ -63,8 +63,8 @@ description = """
|
|||
This step only runs in grooming mode. Skip if in breakdown mode.
|
||||
|
||||
Fetch all open tech-debt issues:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
|
||||
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
|
||||
|
||||
For each issue compute a triage score:
|
||||
|
|
@ -94,14 +94,14 @@ These are issues that block backlog items but are not themselves labeled backlog
|
|||
The dev-agent is completely starved until they are promoted or resolved.
|
||||
|
||||
For each tier-0 issue:
|
||||
- Read the full body: curl -sf -H "Authorization: token $CODEBERG_TOKEN" "$CODEBERG_API/issues/{number}"
|
||||
- Read the full body: curl -sf -H "Authorization: token $FORGE_TOKEN" "$FORGE_API/issues/{number}"
|
||||
- If resolvable: promote to backlog — add acceptance criteria, affected files, relabel
|
||||
- If needs human decision: add to ESCALATE block
|
||||
- If invalid / wontfix: close with explanation comment
|
||||
|
||||
After completing all tier-0, re-fetch to check for new blockers:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
|
||||
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
|
||||
|
||||
If new tier-0 blockers appeared, process those too.
|
||||
|
|
@ -172,8 +172,8 @@ id = "verify"
|
|||
title = "Verify completion and loop until zero tech-debt"
|
||||
description = """
|
||||
Re-fetch ALL open tech-debt issues and count them:
|
||||
REMAINING=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
|
||||
REMAINING=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
|
||||
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))] | length')
|
||||
echo "Remaining tech-debt: $REMAINING"
|
||||
|
||||
|
|
|
|||
|
|
@ -78,25 +78,25 @@ If you discover pre-existing issues (NOT introduced by this PR), create
|
|||
tech-debt issues via API so they are tracked separately:
|
||||
|
||||
# Look up tech-debt label ID (create if missing):
|
||||
TECH_DEBT_ID=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/labels" | jq -r '.[] | select(.name=="tech-debt") | .id')
|
||||
TECH_DEBT_ID=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/labels" | jq -r '.[] | select(.name=="tech-debt") | .id')
|
||||
|
||||
if [ -z "$TECH_DEBT_ID" ]; then
|
||||
TECH_DEBT_ID=$(curl -sf -X POST \
|
||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/labels" \
|
||||
"$FORGE_API/labels" \
|
||||
-d '{"name":"tech-debt","color":"#6B7280","description":"Pre-existing tech debt flagged by AI review"}' | jq -r '.id')
|
||||
fi
|
||||
|
||||
# Check for duplicate before creating:
|
||||
EXISTING=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&labels=tech-debt&limit=50" | \
|
||||
EXISTING=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&labels=tech-debt&limit=50" | \
|
||||
jq --arg t "TITLE" '[.[] | select(.title == $t)] | length')
|
||||
|
||||
# Create only if no duplicate:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
-H "Content-Type: application/json" "$CODEBERG_API/issues" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" "$FORGE_API/issues" \
|
||||
-d '{"title":"...","body":"Flagged by AI reviewer in PR #NNN.\n\n## Problem\n...\n\n---\n*Auto-created from AI review*","labels":[TECH_DEBT_ID]}'
|
||||
|
||||
Only create follow-ups for clear, actionable tech debt. Do not create
|
||||
|
|
@ -138,5 +138,5 @@ For a re-review, structure the markdown as:
|
|||
After writing the JSON file, signal completion:
|
||||
echo "PHASE:done" > "$PHASE_FILE"
|
||||
|
||||
Then STOP and wait. The orchestrator will post your review to Codeberg.
|
||||
Then STOP and wait. The orchestrator will post your review to the forge.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ Groom the open issue backlog. This step is the core Claude-driven analysis
|
|||
Pre-checks (bash, zero tokens — detect problems before invoking Claude):
|
||||
|
||||
1. Fetch all open issues:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&type=issues&limit=50&sort=updated&direction=desc"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&type=issues&limit=50&sort=updated&direction=desc"
|
||||
|
||||
2. Duplicate detection: compare issue titles pairwise. Normalize
|
||||
(lowercase, strip prefixes like feat:/fix:/refactor:, collapse whitespace)
|
||||
|
|
@ -162,7 +162,7 @@ Sibling dependency rule (CRITICAL):
|
|||
|
||||
If either section is missing:
|
||||
a. Write a comment action to the manifest:
|
||||
echo '{"action":"comment","issue":NNN,"body":"This issue is missing required sections. Please use the issue templates at `.codeberg/ISSUE_TEMPLATE/` — needs: <missing items>."}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||
echo '{"action":"comment","issue":NNN,"body":"This issue is missing required sections. Please use the issue templates at `.forgejo/ISSUE_TEMPLATE/` — needs: <missing items>."}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||
Where <missing items> is a comma-separated list of what's absent
|
||||
(e.g. "acceptance criteria, affected files" or just "affected files").
|
||||
b. Write a remove_label action to the manifest:
|
||||
|
|
@ -249,19 +249,19 @@ Review all issues labeled 'blocked' and decide their fate.
|
|||
(See issue #352 for the blocked label convention.)
|
||||
|
||||
1. Fetch all blocked issues:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&type=issues&labels=blocked&limit=50"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&type=issues&labels=blocked&limit=50"
|
||||
|
||||
2. For each blocked issue, read the full body and comments:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<number>"
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<number>/comments"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<number>"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<number>/comments"
|
||||
|
||||
3. Check dependencies — extract issue numbers from ## Dependencies /
|
||||
## Depends on / ## Blocked by sections. For each dependency:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<dep_number>"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<dep_number>"
|
||||
Check if the dependency is now closed.
|
||||
|
||||
4. For each blocked issue, choose ONE action:
|
||||
|
|
@ -459,9 +459,9 @@ executes them after the PR merges.
|
|||
git push -u origin "$BRANCH"
|
||||
g. Create a PR:
|
||||
PR_RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/pulls" \
|
||||
"$FORGE_API/pulls" \
|
||||
-d '{"title":"chore: gardener housekeeping",
|
||||
"head":"'"$BRANCH"'","base":"'"$PRIMARY_BRANCH"'",
|
||||
"body":"Automated gardener housekeeping — AGENTS.md updates + pending actions manifest.\\n\\nReview `gardener/pending-actions.json` for proposed grooming actions (label changes, closures, comments). These execute after merge."}')
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ Evidence from the preflight step informs whether each prediction is valid
|
|||
(e.g. "red-team stale since March 12" is confirmed by evidence/ timestamps).
|
||||
|
||||
1. Fetch unreviewed predictions:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
|
||||
|
||||
If there are none, note that and skip to step 3b (label resolution
|
||||
is still required — the file-at-constraints step needs label IDs).
|
||||
|
|
@ -72,10 +72,10 @@ Evidence from the preflight step informs whether each prediction is valid
|
|||
Project formulas are dispatched via action issues on the project repo.
|
||||
|
||||
3. Fetch all open issues to check for overlap:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&type=issues&limit=50"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&type=issues&limit=50"
|
||||
|
||||
3b. Resolve label IDs needed for triage AND filing (fetch via $CODEBERG_API/labels).
|
||||
3b. Resolve label IDs needed for triage AND filing (fetch via $FORGE_API/labels).
|
||||
ALWAYS execute this step, even if there are no predictions to triage —
|
||||
the file-at-constraints step depends on these IDs:
|
||||
- <unreviewed_label_id> → prediction/unreviewed
|
||||
|
|
@ -120,65 +120,65 @@ Evidence from the preflight step informs whether each prediction is valid
|
|||
Example body structure:
|
||||
## Problem\n<what the prediction identified>\n\n## Proposed solution\n<approach>\n\n## Affected files\n- <file1>\n- <file2>\n\n## Acceptance criteria\n- [ ] <criterion 1>\n- [ ] CI green
|
||||
Create the issue:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
-H "Content-Type: application/json" "$CODEBERG_API/issues" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" "$FORGE_API/issues" \
|
||||
-d '{"title":"...","body":"...","labels":[<label_id>]}'
|
||||
Extract the issue number from the response (jq -r '.number').
|
||||
a2. Verify the label was applied (Codeberg may silently drop labels
|
||||
a2. Verify the label was applied (the forge may silently drop labels
|
||||
on creation). Re-apply via a separate POST if missing:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<new_issue_num>/labels" \
|
||||
"$FORGE_API/issues/<new_issue_num>/labels" \
|
||||
-d '{"labels":[<label_id>]}'
|
||||
b. Comment on the prediction with "Actioned as #NNN — <reasoning>":
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>/comments" \
|
||||
"$FORGE_API/issues/<pred_num>/comments" \
|
||||
-d '{"body":"Actioned as #NNN — <reasoning>"}'
|
||||
c. Relabel: remove prediction/unreviewed, add prediction/actioned:
|
||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>/labels" \
|
||||
"$FORGE_API/issues/<pred_num>/labels" \
|
||||
-d '{"labels":[<actioned_label_id>]}'
|
||||
d. Close the prediction:
|
||||
curl -sf -X PATCH -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X PATCH -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>" \
|
||||
"$FORGE_API/issues/<pred_num>" \
|
||||
-d '{"state":"closed"}'
|
||||
|
||||
For WATCH:
|
||||
a. Comment with reasoning why not urgent:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>/comments" \
|
||||
"$FORGE_API/issues/<pred_num>/comments" \
|
||||
-d '{"body":"Watching — <reasoning>"}'
|
||||
b. Replace prediction/unreviewed label with prediction/backlog:
|
||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>/labels" \
|
||||
"$FORGE_API/issues/<pred_num>/labels" \
|
||||
-d '{"labels":[<prediction_backlog_label_id>]}'
|
||||
|
||||
For DISMISS:
|
||||
a. Comment with explicit reasoning:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>/comments" \
|
||||
"$FORGE_API/issues/<pred_num>/comments" \
|
||||
-d '{"body":"Dismissed — <reasoning>"}'
|
||||
b. Relabel: remove prediction/unreviewed, add prediction/actioned:
|
||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>/labels" \
|
||||
"$FORGE_API/issues/<pred_num>/labels" \
|
||||
-d '{"labels":[<actioned_label_id>]}'
|
||||
c. Close the prediction:
|
||||
curl -sf -X PATCH -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X PATCH -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<pred_num>" \
|
||||
"$FORGE_API/issues/<pred_num>" \
|
||||
-d '{"state":"closed"}'
|
||||
|
||||
6. Track promoted predictions — they are added to the prerequisite tree
|
||||
|
|
@ -208,8 +208,8 @@ Read these inputs:
|
|||
- $PROJECT_REPO_ROOT/formulas/*.toml — project-specific formulas
|
||||
- Open issues (fetched via API, or reuse from prediction-triage)
|
||||
- Closed issues (fetch recently closed to detect resolved prerequisites):
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=closed&type=issues&limit=50&sort=updated&direction=desc"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=closed&type=issues&limit=50&sort=updated&direction=desc"
|
||||
- Planner memory (loaded in preflight)
|
||||
- Promoted predictions from prediction-triage (add as prerequisites if relevant)
|
||||
|
||||
|
|
@ -218,8 +218,8 @@ Read these inputs:
|
|||
For each issue referenced in the prerequisite tree (by #number), fetch its
|
||||
recent comments to detect signals that the issue is stuck or bouncing:
|
||||
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<number>/comments?limit=10"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<number>/comments?limit=10"
|
||||
|
||||
Scan each comment body for these signals:
|
||||
|
||||
|
|
@ -394,17 +394,17 @@ Filing gate — for each constraint (that is NOT stuck):
|
|||
## Problem\n<what this prerequisite is and which objectives it blocks>\n\n## Proposed solution\n<rough approach>\n\n## Affected files\n- <file1>\n- <file2>\n\n## Acceptance criteria\n- [ ] <criterion derived from the constraint>\n- [ ] CI green\n\n## Dependencies\n- #NNN (if depends on other open issues)
|
||||
Create the issue:
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues" \
|
||||
"$FORGE_API/issues" \
|
||||
-d '{"title":"...","body":"...","labels":[<backlog_label_id>]}'
|
||||
Extract the issue number from the response (jq -r '.number').
|
||||
|
||||
2b. Verify the label was applied (Codeberg may silently drop labels
|
||||
2b. Verify the label was applied (the forge may silently drop labels
|
||||
on creation). Always re-apply via a separate POST to be safe:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<new_issue_num>/labels" \
|
||||
"$FORGE_API/issues/<new_issue_num>/labels" \
|
||||
-d '{"labels":[<backlog_label_id>]}'
|
||||
|
||||
3. If an issue already exists and is open, skip it — no duplicate filing.
|
||||
|
|
@ -422,20 +422,20 @@ is purely additive.
|
|||
5. **Add `priority` to top-5 constraint issues:**
|
||||
For each of the top 5 constraint issues (whether just filed or already
|
||||
existing), check if it already has the `priority` label. If not, add it:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues/<issue_number>/labels" \
|
||||
"$FORGE_API/issues/<issue_number>/labels" \
|
||||
-d '{"labels":[<priority_label_id>]}'
|
||||
|
||||
6. **Remove `priority` from issues no longer in top 5:**
|
||||
Fetch all open issues that currently have the `priority` label:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&labels=priority&type=issues&limit=50"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&labels=priority&type=issues&limit=50"
|
||||
For each issue in this list that is NOT one of the current top 5
|
||||
constraint issues, remove the `priority` label (demote back to plain
|
||||
`backlog`):
|
||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues/<issue_number>/labels/<priority_label_id>"
|
||||
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues/<issue_number>/labels/<priority_label_id>"
|
||||
This keeps the priority set current — only the active bottleneck issues
|
||||
get priority, not stale constraints from previous runs.
|
||||
|
||||
|
|
@ -634,9 +634,9 @@ run — only file changes (tree, journal, MEMORY.md) need the PR.
|
|||
git push -u origin "$BRANCH"
|
||||
g. Create a PR:
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/pulls" \
|
||||
"$FORGE_API/pulls" \
|
||||
-d '{"title":"chore: planner run — prerequisite tree update",
|
||||
"head":"<branch>","base":"<primary-branch>",
|
||||
"body":"Automated planner run — prerequisite tree update and journal entry."}'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# Goal: find the project's biggest weakness. Explore when uncertain,
|
||||
# exploit when confident (dispatch a formula to prove the theory).
|
||||
#
|
||||
# Memory: previous predictions on Codeberg ARE the memory.
|
||||
# Memory: previous predictions on the forge ARE the memory.
|
||||
# No separate memory file — the issue tracker is the source of truth.
|
||||
#
|
||||
# Executed by predictor/predictor-run.sh via cron — no action issues.
|
||||
|
|
@ -33,12 +33,12 @@ Set up the working environment and load your prediction history.
|
|||
git pull --ff-only origin "$PRIMARY_BRANCH" --quiet
|
||||
|
||||
2. Fetch ALL your previous predictions (open + recently closed):
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Fbacklog&limit=50"
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/issues?state=closed&type=issues&labels=prediction%2Factioned&limit=50&sort=updated&direction=desc"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Fbacklog&limit=50"
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/issues?state=closed&type=issues&labels=prediction%2Factioned&limit=50&sort=updated&direction=desc"
|
||||
|
||||
For each prediction, note:
|
||||
- What you predicted (title + body)
|
||||
|
|
@ -150,21 +150,21 @@ For each weakness you identify, choose one:
|
|||
## Filing
|
||||
|
||||
1. Look up label IDs:
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/labels" | jq '[.[] | select(.name | startswith("prediction")) | {name, id}]'
|
||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
"$CODEBERG_API/labels" | jq '.[] | select(.name == "action") | .id'
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/labels" | jq '[.[] | select(.name | startswith("prediction")) | {name, id}]'
|
||||
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||
"$FORGE_API/labels" | jq '.[] | select(.name == "action") | .id'
|
||||
|
||||
2. File predictions:
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues" \
|
||||
"$FORGE_API/issues" \
|
||||
-d '{"title":"<title>","body":"<body>","labels":[<prediction_unreviewed_id>]}'
|
||||
|
||||
3. File action dispatches (if exploiting):
|
||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
||||
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$CODEBERG_API/issues" \
|
||||
"$FORGE_API/issues" \
|
||||
-d '{"title":"action: test prediction #NNN — <formula> <focus>","body":"<body>","labels":[<action_label_id>]}'
|
||||
|
||||
4. Do NOT duplicate existing open predictions. If your theory matches
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ runs directly from cron like the planner, predictor, and supervisor.
|
|||
PR, reviewed alongside AGENTS.md changes, executed by gardener-run.sh after merge.
|
||||
|
||||
**Environment variables consumed**:
|
||||
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by gardener-run.sh)
|
||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ If no file changes in commit-and-pr:
|
|||
echo 'PHASE:done' > '${PHASE_FILE}'"
|
||||
|
||||
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
||||
PROMPT="You are the issue gardener for ${CODEBERG_REPO}. Work through the formula below. Follow the phase protocol: if the commit-and-pr step creates a PR, write PHASE:awaiting_ci and wait for orchestrator CI/review/merge handling. If no file changes, write PHASE:done. The orchestrator will time you out if you return to the prompt without signalling.
|
||||
PROMPT="You are the issue gardener for ${FORGE_REPO}. Work through the formula below. Follow the phase protocol: if the commit-and-pr step creates a PR, write PHASE:awaiting_ci and wait for orchestrator CI/review/merge handling. If no file changes, write PHASE:done. The orchestrator will time you out if you return to the prompt without signalling.
|
||||
|
||||
You have full shell access and --dangerously-skip-permissions.
|
||||
Fix what you can. Escalate what you cannot. Do NOT ask permission — act first, report after.
|
||||
|
|
@ -162,13 +162,13 @@ _gardener_execute_manifest() {
|
|||
add_label)
|
||||
local label label_id
|
||||
label=$(jq -r ".[$i].label" "$manifest_file")
|
||||
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
|
||||
label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/labels" | jq -r --arg n "$label" \
|
||||
'.[] | select(.name == $n) | .id') || true
|
||||
if [ -n "$label_id" ]; then
|
||||
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
if curl -sf -X POST -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/issues/${issue}/labels" \
|
||||
"${FORGE_API}/issues/${issue}/labels" \
|
||||
-d "{\"labels\":[${label_id}]}" >/dev/null 2>&1; then
|
||||
log "manifest: add_label '${label}' to #${issue}"
|
||||
else
|
||||
|
|
@ -182,12 +182,12 @@ _gardener_execute_manifest() {
|
|||
remove_label)
|
||||
local label label_id
|
||||
label=$(jq -r ".[$i].label" "$manifest_file")
|
||||
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
|
||||
label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/labels" | jq -r --arg n "$label" \
|
||||
'.[] | select(.name == $n) | .id') || true
|
||||
if [ -n "$label_id" ]; then
|
||||
if curl -sf -X DELETE -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1; then
|
||||
if curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1; then
|
||||
log "manifest: remove_label '${label}' from #${issue}"
|
||||
else
|
||||
log "manifest: FAILED remove_label '${label}' from #${issue}"
|
||||
|
|
@ -200,9 +200,9 @@ _gardener_execute_manifest() {
|
|||
close)
|
||||
local reason
|
||||
reason=$(jq -r ".[$i].reason // empty" "$manifest_file")
|
||||
if curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
if curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/issues/${issue}" \
|
||||
"${FORGE_API}/issues/${issue}" \
|
||||
-d '{"state":"closed"}' >/dev/null 2>&1; then
|
||||
log "manifest: closed #${issue} (${reason})"
|
||||
else
|
||||
|
|
@ -214,9 +214,9 @@ _gardener_execute_manifest() {
|
|||
local body escaped_body
|
||||
body=$(jq -r ".[$i].body" "$manifest_file")
|
||||
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
|
||||
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
if curl -sf -X POST -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/issues/${issue}/comments" \
|
||||
"${FORGE_API}/issues/${issue}/comments" \
|
||||
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
||||
log "manifest: commented on #${issue}"
|
||||
else
|
||||
|
|
@ -235,8 +235,8 @@ _gardener_execute_manifest() {
|
|||
label_ids="[]"
|
||||
if [ -n "$labels" ]; then
|
||||
local all_labels ids_json=""
|
||||
all_labels=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/labels") || true
|
||||
all_labels=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/labels") || true
|
||||
while IFS= read -r lname; do
|
||||
local lid
|
||||
lid=$(echo "$all_labels" | jq -r --arg n "$lname" \
|
||||
|
|
@ -245,9 +245,9 @@ _gardener_execute_manifest() {
|
|||
done <<< "$labels"
|
||||
[ -n "$ids_json" ] && label_ids="[${ids_json}]"
|
||||
fi
|
||||
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
if curl -sf -X POST -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/issues" \
|
||||
"${FORGE_API}/issues" \
|
||||
-d "{\"title\":${escaped_title},\"body\":${escaped_body},\"labels\":${label_ids}}" >/dev/null 2>&1; then
|
||||
log "manifest: created issue '${title}'"
|
||||
else
|
||||
|
|
@ -259,9 +259,9 @@ _gardener_execute_manifest() {
|
|||
local body escaped_body
|
||||
body=$(jq -r ".[$i].body" "$manifest_file")
|
||||
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
|
||||
if curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
if curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/issues/${issue}" \
|
||||
"${FORGE_API}/issues/${issue}" \
|
||||
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
||||
log "manifest: edited body of #${issue}"
|
||||
else
|
||||
|
|
@ -284,9 +284,9 @@ _gardener_execute_manifest() {
|
|||
_gardener_merge() {
|
||||
local merge_response merge_http_code
|
||||
merge_response=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}/merge" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}/merge" \
|
||||
-d '{"Do":"merge","delete_branch_after_merge":true}') || true
|
||||
merge_http_code=$(echo "$merge_response" | tail -1)
|
||||
|
||||
|
|
@ -300,8 +300,8 @@ _gardener_merge() {
|
|||
# Already merged (race)?
|
||||
if [ "$merge_http_code" = "405" ]; then
|
||||
local pr_merged
|
||||
pr_merged=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
|
||||
pr_merged=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
|
||||
if [ "$pr_merged" = "true" ]; then
|
||||
log "gardener PR #${_GARDENER_PR} already merged"
|
||||
_gardener_execute_manifest
|
||||
|
|
@ -329,9 +329,9 @@ _gardener_timeout_cleanup() {
|
|||
log "gardener merge-through timed out (${_GARDENER_MERGE_TIMEOUT}s) — closing PR"
|
||||
if [ -n "$_GARDENER_PR" ]; then
|
||||
curl -sf -X PATCH \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}" \
|
||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||
fi
|
||||
printf 'PHASE:failed\nReason: merge-through timeout (%ss)\n' \
|
||||
|
|
@ -360,8 +360,8 @@ _gardener_handle_ci() {
|
|||
fi
|
||||
# Fallback: search for open gardener PRs
|
||||
if [ -z "$_GARDENER_PR" ]; then
|
||||
_GARDENER_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/pulls?state=open&limit=10" | \
|
||||
_GARDENER_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/pulls?state=open&limit=10" | \
|
||||
jq -r '[.[] | select(.head.ref | startswith("chore/gardener-"))] | .[0].number // empty') || true
|
||||
fi
|
||||
if [ -z "$_GARDENER_PR" ]; then
|
||||
|
|
@ -395,8 +395,8 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
|
|||
|
||||
# Get HEAD SHA from PR
|
||||
local head_sha
|
||||
head_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||
|
||||
if [ -z "$head_sha" ]; then
|
||||
log "WARNING: could not get HEAD SHA for PR #${_GARDENER_PR}"
|
||||
|
|
@ -426,11 +426,11 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
|
|||
fi
|
||||
|
||||
# Re-fetch HEAD in case Claude pushed new commits
|
||||
head_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||
|
||||
ci_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/commits/${head_sha}/status" | jq -r '.state // "unknown"') || ci_state="unknown"
|
||||
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/commits/${head_sha}/status" | jq -r '.state // "unknown"') || ci_state="unknown"
|
||||
|
||||
case "$ci_state" in
|
||||
success|failure|error) ci_done=true; break ;;
|
||||
|
|
@ -463,8 +463,8 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
|
|||
|
||||
# Get error details
|
||||
local pipeline_num ci_error_log
|
||||
pipeline_num=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/commits/${head_sha}/status" | \
|
||||
pipeline_num=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/commits/${head_sha}/status" | \
|
||||
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
|
||||
|
||||
ci_error_log=""
|
||||
|
|
@ -518,10 +518,10 @@ _gardener_handle_review() {
|
|||
|
||||
# Check for review on current HEAD
|
||||
local review_sha review_comment
|
||||
review_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||
review_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||
|
||||
review_comment=$(codeberg_api_all "/issues/${_GARDENER_PR}/comments" 2>/dev/null | \
|
||||
review_comment=$(forge_api_all "/issues/${_GARDENER_PR}/comments" 2>/dev/null | \
|
||||
jq -r --arg sha "${review_sha:-none}" \
|
||||
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
||||
|
||||
|
|
@ -536,10 +536,10 @@ _gardener_handle_review() {
|
|||
|
||||
verdict=$(echo "$review_text" | grep -oP '\*\*(APPROVE|REQUEST_CHANGES|DISCUSS)\*\*' | head -1 | tr -d '*' || true)
|
||||
|
||||
# Check formal Codeberg reviews as fallback
|
||||
# Check formal forge reviews as fallback
|
||||
if [ -z "$verdict" ]; then
|
||||
verdict=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}/reviews" | \
|
||||
verdict=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}/reviews" | \
|
||||
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
|
||||
[ "$verdict" = "APPROVED" ] && verdict="APPROVE"
|
||||
[[ "$verdict" != "REQUEST_CHANGES" && "$verdict" != "APPROVE" ]] && verdict=""
|
||||
|
|
@ -576,8 +576,8 @@ Then stop and wait."
|
|||
|
||||
# Check if PR was merged or closed externally
|
||||
local pr_json pr_state pr_merged
|
||||
pr_json=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}") || true
|
||||
pr_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_API}/pulls/${_GARDENER_PR}") || true
|
||||
pr_state=$(echo "$pr_json" | jq -r '.state // "unknown"')
|
||||
pr_merged=$(echo "$pr_json" | jq -r '.merged // false')
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ sourced as needed.
|
|||
|
||||
| File | What it provides | Sourced by |
|
||||
|---|---|---|
|
||||
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`CODEBERG_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `codeberg_api()`, `codeberg_api_all()` (accepts optional second TOKEN parameter, defaults to `$CODEBERG_TOKEN`), `woodpecker_api()`, `wpdb()`, `matrix_send()`, `matrix_send_ctx()`. Auto-loads project TOML if `PROJECT_TOML` is set. | Every agent |
|
||||
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`FORGE_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `forge_api()`, `forge_api_all()` (accepts optional second TOKEN parameter, defaults to `$FORGE_TOKEN`), `woodpecker_api()`, `wpdb()`, `matrix_send()`, `matrix_send_ctx()`. Auto-loads project TOML if `PROJECT_TOML` is set. | Every agent |
|
||||
| `lib/ci-helpers.sh` | `ci_passed()` — returns 0 if CI state is "success" (or no CI configured). `ci_required_for_pr()` — returns 0 if PR has code files (CI required), 1 if non-code only (CI not required). `is_infra_step()` — returns 0 if a single CI step failure matches infra heuristics (clone/git exit 128, any exit 137, log timeout patterns). `classify_pipeline_failure()` — returns "infra \<reason>" if any failed Woodpecker step matches infra heuristics via `is_infra_step()`, else "code". `ensure_priority_label()` — looks up (or creates) the `priority` label and returns its ID; caches in `_PRIORITY_LABEL_ID`. | dev-poll, review-poll, review-pr, supervisor-poll |
|
||||
| `lib/ci-debug.sh` | CLI tool for Woodpecker CI: `list`, `status`, `logs`, `failures` subcommands. Not sourced — run directly. | Humans / dev-agent (tool access) |
|
||||
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `CODEBERG_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
|
||||
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `FORGE_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
|
||||
| `lib/parse-deps.sh` | Extracts dependency issue numbers from an issue body (stdin → stdout, one number per line). Matches `## Dependencies` / `## Depends on` / `## Blocked by` sections and inline `depends on #N` / `blocked by #N` patterns. Inline scan skips fenced code blocks to prevent false positives from code examples in issue bodies. Not sourced — executed via `bash lib/parse-deps.sh`. | dev-poll, supervisor-poll |
|
||||
| `lib/matrix_listener.sh` | Long-poll Matrix sync daemon. Dispatches thread replies to the correct agent via tmux session injection (dev, action, vault, review) or well-known files (`/tmp/{agent}-escalation-reply` for supervisor/gardener). Handles all agent reply routing. Run as systemd service. | Standalone daemon |
|
||||
| `lib/formula-session.sh` | `acquire_cron_lock()`, `check_memory()`, `load_formula()`, `build_context_block()`, `consume_escalation_reply()`, `start_formula_session()`, `formula_phase_callback()`, `build_prompt_footer()`, `run_formula_and_monitor(AGENT [TIMEOUT] [CALLBACK])` — shared helpers for formula-driven cron agents (lock, memory guard, formula loading, prompt assembly, tmux session, monitor loop, crash recovery). `formula_phase_callback()` handles `PHASE:escalate` (unified escalation path — kills the session; callers may follow up via Matrix). `run_formula_and_monitor` accepts an optional CALLBACK (default: `formula_phase_callback`) so callers can install custom merge-through or escalation handlers. | planner-run.sh, predictor-run.sh, gardener-run.sh, supervisor-run.sh, dev-agent.sh, action-agent.sh |
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ set -euo pipefail
|
|||
source "$(dirname "$0")/../lib/env.sh"
|
||||
|
||||
# WOODPECKER_TOKEN loaded from .env via env.sh
|
||||
REPO="${CODEBERG_REPO}"
|
||||
REPO="${FORGE_REPO}"
|
||||
API="${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}"
|
||||
|
||||
api() {
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@ set -euo pipefail
|
|||
|
||||
# ensure_blocked_label_id — look up (or create) the "blocked" label, print its ID.
|
||||
# Caches the result in _BLOCKED_LABEL_ID to avoid repeated API calls.
|
||||
# Requires: CODEBERG_TOKEN, CODEBERG_API (from env.sh), codeberg_api()
|
||||
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
|
||||
ensure_blocked_label_id() {
|
||||
if [ -n "${_BLOCKED_LABEL_ID:-}" ]; then
|
||||
printf '%s' "$_BLOCKED_LABEL_ID"
|
||||
return 0
|
||||
fi
|
||||
_BLOCKED_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
|
||||
_BLOCKED_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
||||
| jq -r '.[] | select(.name == "blocked") | .id' 2>/dev/null || true)
|
||||
if [ -z "$_BLOCKED_LABEL_ID" ]; then
|
||||
_BLOCKED_LABEL_ID=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CODEBERG_API}/labels" \
|
||||
"${FORGE_API}/labels" \
|
||||
-d '{"name":"blocked","color":"#e11d48"}' 2>/dev/null \
|
||||
| jq -r '.id // empty' 2>/dev/null || true)
|
||||
fi
|
||||
|
|
@ -29,19 +29,19 @@ ensure_blocked_label_id() {
|
|||
|
||||
# ensure_priority_label — look up (or create) the "priority" label, print its ID.
|
||||
# Caches the result in _PRIORITY_LABEL_ID to avoid repeated API calls.
|
||||
# Requires: CODEBERG_TOKEN, CODEBERG_API (from env.sh), codeberg_api()
|
||||
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
|
||||
ensure_priority_label() {
|
||||
if [ -n "${_PRIORITY_LABEL_ID:-}" ]; then
|
||||
printf '%s' "$_PRIORITY_LABEL_ID"
|
||||
return 0
|
||||
fi
|
||||
_PRIORITY_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
|
||||
_PRIORITY_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
||||
| jq -r '.[] | select(.name == "priority") | .id' 2>/dev/null || true)
|
||||
if [ -z "$_PRIORITY_LABEL_ID" ]; then
|
||||
_PRIORITY_LABEL_ID=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CODEBERG_API}/labels" \
|
||||
"${FORGE_API}/labels" \
|
||||
-d '{"name":"priority","color":"#f59e0b"}' 2>/dev/null \
|
||||
| jq -r '.id // empty' 2>/dev/null || true)
|
||||
fi
|
||||
|
|
@ -68,7 +68,7 @@ diff_has_code_files() {
|
|||
ci_required_for_pr() {
|
||||
local pr_num="$1"
|
||||
local files all_json
|
||||
all_json=$(codeberg_api_all "/pulls/${pr_num}/files") || return 0
|
||||
all_json=$(forge_api_all "/pulls/${pr_num}/files") || return 0
|
||||
files=$(printf '%s' "$all_json" | jq -r '.[].filename' 2>/dev/null) || return 0
|
||||
if [ -z "$files" ]; then
|
||||
return 0 # empty file list — require CI as safety default
|
||||
|
|
@ -113,7 +113,7 @@ ci_failed() {
|
|||
is_infra_step() {
|
||||
local sname="$1" ecode="$2" log_data="${3:-}"
|
||||
|
||||
# Clone/git step exit 128 → Codeberg connection failure / rate limit
|
||||
# Clone/git step exit 128 → forge connection failure / rate limit
|
||||
if { [[ "$sname" == *clone* ]] || [[ "$sname" == git* ]]; } && [ "$ecode" = "128" ]; then
|
||||
echo "${sname} exit 128 (connection failure)"
|
||||
return 0
|
||||
|
|
|
|||
60
lib/env.sh
60
lib/env.sh
|
|
@ -24,17 +24,33 @@ if [ -n "${PROJECT_TOML:-}" ] && [ -f "$PROJECT_TOML" ]; then
|
|||
source "${FACTORY_ROOT}/lib/load-project.sh" "$PROJECT_TOML"
|
||||
fi
|
||||
|
||||
# Codeberg token: env var > ~/.netrc
|
||||
if [ -z "${CODEBERG_TOKEN:-}" ]; then
|
||||
CODEBERG_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
|
||||
# Forge token: new FORGE_TOKEN > legacy CODEBERG_TOKEN > ~/.netrc
|
||||
if [ -z "${FORGE_TOKEN:-}" ]; then
|
||||
FORGE_TOKEN="${CODEBERG_TOKEN:-}"
|
||||
fi
|
||||
export CODEBERG_TOKEN
|
||||
if [ -z "${FORGE_TOKEN:-}" ]; then
|
||||
FORGE_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
|
||||
fi
|
||||
export FORGE_TOKEN
|
||||
export CODEBERG_TOKEN="${FORGE_TOKEN}" # backwards compat
|
||||
|
||||
# Project config
|
||||
export CODEBERG_REPO="${CODEBERG_REPO:-}"
|
||||
export CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1/repos/${CODEBERG_REPO}}"
|
||||
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
|
||||
export PROJECT_NAME="${PROJECT_NAME:-${CODEBERG_REPO##*/}}"
|
||||
# Review bot token: FORGE_REVIEW_TOKEN > legacy REVIEW_BOT_TOKEN
|
||||
export FORGE_REVIEW_TOKEN="${FORGE_REVIEW_TOKEN:-${REVIEW_BOT_TOKEN:-}}"
|
||||
export REVIEW_BOT_TOKEN="${FORGE_REVIEW_TOKEN}" # backwards compat
|
||||
|
||||
# Bot usernames filter: FORGE_BOT_USERNAMES > legacy CODEBERG_BOT_USERNAMES
|
||||
export FORGE_BOT_USERNAMES="${FORGE_BOT_USERNAMES:-${CODEBERG_BOT_USERNAMES:-}}"
|
||||
export CODEBERG_BOT_USERNAMES="${FORGE_BOT_USERNAMES}" # backwards compat
|
||||
|
||||
# Project config (FORGE_* preferred, CODEBERG_* fallback)
|
||||
export FORGE_REPO="${FORGE_REPO:-${CODEBERG_REPO:-}}"
|
||||
export CODEBERG_REPO="${FORGE_REPO}" # backwards compat
|
||||
export FORGE_URL="${FORGE_URL:-http://localhost:3000}"
|
||||
export FORGE_API="${FORGE_API:-${FORGE_URL}/api/v1/repos/${FORGE_REPO}}"
|
||||
export FORGE_WEB="${FORGE_WEB:-${FORGE_URL}/${FORGE_REPO}}"
|
||||
export CODEBERG_API="${FORGE_API}" # backwards compat
|
||||
export CODEBERG_WEB="${FORGE_WEB}" # backwards compat
|
||||
export PROJECT_NAME="${PROJECT_NAME:-${FORGE_REPO##*/}}"
|
||||
export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}"
|
||||
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
|
||||
export WOODPECKER_REPO_ID="${WOODPECKER_REPO_ID:-}"
|
||||
|
|
@ -46,23 +62,25 @@ log() {
|
|||
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
|
||||
}
|
||||
|
||||
# Codeberg API helper — usage: codeberg_api GET /issues?state=open
|
||||
codeberg_api() {
|
||||
# Forge API helper — usage: forge_api GET /issues?state=open
|
||||
forge_api() {
|
||||
local method="$1" path="$2"
|
||||
shift 2
|
||||
curl -sf -X "$method" \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CODEBERG_API}${path}" "$@"
|
||||
"${FORGE_API}${path}" "$@"
|
||||
}
|
||||
# Backwards-compat alias
|
||||
codeberg_api() { forge_api "$@"; }
|
||||
|
||||
# Paginate a Codeberg API GET endpoint and return all items as a merged JSON array.
|
||||
# Usage: codeberg_api_all /path (no existing query params)
|
||||
# codeberg_api_all /path?a=b (with existing params — appends &limit=50&page=N)
|
||||
# codeberg_api_all /path TOKEN (optional second arg: token; defaults to $CODEBERG_TOKEN)
|
||||
codeberg_api_all() {
|
||||
# Paginate a Forge API GET endpoint and return all items as a merged JSON array.
|
||||
# Usage: forge_api_all /path (no existing query params)
|
||||
# forge_api_all /path?a=b (with existing params — appends &limit=50&page=N)
|
||||
# forge_api_all /path TOKEN (optional second arg: token; defaults to $FORGE_TOKEN)
|
||||
forge_api_all() {
|
||||
local path_prefix="$1"
|
||||
local CODEBERG_TOKEN="${2:-${CODEBERG_TOKEN}}"
|
||||
local FORGE_TOKEN="${2:-${FORGE_TOKEN}}"
|
||||
local sep page page_items count all_items="[]"
|
||||
case "$path_prefix" in
|
||||
*"?"*) sep="&" ;;
|
||||
|
|
@ -70,7 +88,7 @@ codeberg_api_all() {
|
|||
esac
|
||||
page=1
|
||||
while true; do
|
||||
page_items=$(codeberg_api GET "${path_prefix}${sep}limit=50&page=${page}")
|
||||
page_items=$(forge_api GET "${path_prefix}${sep}limit=50&page=${page}")
|
||||
count=$(printf '%s' "$page_items" | jq 'length')
|
||||
[ "$count" -eq 0 ] && break
|
||||
all_items=$(printf '%s\n%s' "$all_items" "$page_items" | jq -s 'add')
|
||||
|
|
@ -79,6 +97,8 @@ codeberg_api_all() {
|
|||
done
|
||||
printf '%s' "$all_items"
|
||||
}
|
||||
# Backwards-compat alias
|
||||
codeberg_api_all() { forge_api_all "$@"; }
|
||||
|
||||
# Woodpecker API helper
|
||||
woodpecker_api() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# file-action-issue.sh — File an action issue for a formula run
|
||||
#
|
||||
# Usage: source this file, then call file_action_issue.
|
||||
# Requires: codeberg_api() from lib/env.sh, jq, lib/secret-scan.sh
|
||||
# Requires: forge_api() from lib/env.sh, jq, lib/secret-scan.sh
|
||||
#
|
||||
# file_action_issue <formula_name> <title> <body>
|
||||
# Sets FILED_ISSUE_NUM on success.
|
||||
|
|
@ -24,7 +24,7 @@ file_action_issue() {
|
|||
|
||||
# Dedup: skip if an open action issue for this formula already exists
|
||||
local open_actions
|
||||
open_actions=$(codeberg_api_all "/issues?state=open&type=issues&labels=action" 2>/dev/null || true)
|
||||
open_actions=$(forge_api_all "/issues?state=open&type=issues&labels=action" 2>/dev/null || true)
|
||||
if [ -n "$open_actions" ] && [ "$open_actions" != "null" ]; then
|
||||
local existing
|
||||
existing=$(printf '%s' "$open_actions" | \
|
||||
|
|
@ -36,7 +36,7 @@ file_action_issue() {
|
|||
|
||||
# Fetch 'action' label ID
|
||||
local action_label_id
|
||||
action_label_id=$(codeberg_api GET "/labels" 2>/dev/null | \
|
||||
action_label_id=$(forge_api GET "/labels" 2>/dev/null | \
|
||||
jq -r '.[] | select(.name == "action") | .id' 2>/dev/null || true)
|
||||
if [ -z "$action_label_id" ]; then
|
||||
return 2
|
||||
|
|
@ -50,7 +50,7 @@ file_action_issue() {
|
|||
--argjson labels "[$action_label_id]" \
|
||||
'{title: $title, body: $body, labels: $labels}')
|
||||
|
||||
result=$(codeberg_api POST "/issues" -d "$payload" 2>/dev/null || true)
|
||||
result=$(forge_api POST "/issues" -d "$payload" 2>/dev/null || true)
|
||||
FILED_ISSUE_NUM=$(printf '%s' "$result" | jq -r '.number // empty' 2>/dev/null || true)
|
||||
|
||||
if [ -z "$FILED_ISSUE_NUM" ]; then
|
||||
|
|
|
|||
|
|
@ -208,21 +208,21 @@ read_scratch_context() {
|
|||
# ── Prompt + monitor helpers ──────────────────────────────────────────────
|
||||
|
||||
# build_prompt_footer [EXTRA_API_LINES]
|
||||
# Assembles the common Codeberg API reference + environment + phase protocol
|
||||
# Assembles the common forge API reference + environment + phase protocol
|
||||
# block for formula prompts. Sets PROMPT_FOOTER.
|
||||
# Pass additional API endpoint lines (pre-formatted, newline-prefixed) via $1.
|
||||
# Requires globals: CODEBERG_API, FACTORY_ROOT, PROJECT_REPO_ROOT,
|
||||
# Requires globals: FORGE_API, FACTORY_ROOT, PROJECT_REPO_ROOT,
|
||||
# PRIMARY_BRANCH, PHASE_FILE.
|
||||
build_prompt_footer() {
|
||||
local extra_api="${1:-}"
|
||||
# shellcheck disable=SC2034 # consumed by the calling script's PROMPT
|
||||
PROMPT_FOOTER="## Codeberg API reference
|
||||
Base URL: ${CODEBERG_API}
|
||||
Auth header: -H \"Authorization: token \$CODEBERG_TOKEN\"
|
||||
Read issue: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body'
|
||||
Create issue: curl -sf -X POST -H \"Authorization: token \$CODEBERG_TOKEN\" -H 'Content-Type: application/json' '${CODEBERG_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'${extra_api}
|
||||
List labels: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels'
|
||||
NEVER echo or include the actual token value in output — always reference \$CODEBERG_TOKEN.
|
||||
PROMPT_FOOTER="## Forge API reference
|
||||
Base URL: ${FORGE_API}
|
||||
Auth header: -H \"Authorization: token \${FORGE_TOKEN}\"
|
||||
Read issue: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/issues/{number}' | jq '.body'
|
||||
Create issue: curl -sf -X POST -H \"Authorization: token \${FORGE_TOKEN}\" -H 'Content-Type: application/json' '${FORGE_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'${extra_api}
|
||||
List labels: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/labels'
|
||||
NEVER echo or include the actual token value in output — always reference \${FORGE_TOKEN}.
|
||||
|
||||
## Environment
|
||||
FACTORY_ROOT=${FACTORY_ROOT}
|
||||
|
|
@ -240,7 +240,7 @@ On unrecoverable error:
|
|||
# run_formula_and_monitor AGENT_NAME [TIMEOUT]
|
||||
# Starts the formula session, injects PROMPT, monitors phase, and logs result.
|
||||
# Requires globals: SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT,
|
||||
# CODEBERG_REPO, CLAUDE_MODEL (exported).
|
||||
# FORGE_REPO, CLAUDE_MODEL (exported).
|
||||
# shellcheck disable=SC2154 # SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT set by caller
|
||||
run_formula_and_monitor() {
|
||||
local agent_name="$1"
|
||||
|
|
@ -258,7 +258,7 @@ run_formula_and_monitor() {
|
|||
|
||||
agent_inject_into_session "$SESSION_NAME" "$PROMPT"
|
||||
log "Prompt sent to tmux session"
|
||||
matrix_send "$agent_name" "${agent_name^} session started for ${CODEBERG_REPO}" 2>/dev/null || true
|
||||
matrix_send "$agent_name" "${agent_name^} session started for ${FORGE_REPO}" 2>/dev/null || true
|
||||
|
||||
log "Monitoring phase file: ${PHASE_FILE}"
|
||||
_FORMULA_CRASH_COUNT=0
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Blocks:
|
||||
# - git push --force / -f to primary branch
|
||||
# - rm -rf targeting paths outside the worktree
|
||||
# - Direct Codeberg API merge calls (should go through phase protocol)
|
||||
# - Direct forge API merge calls (should go through phase protocol)
|
||||
# - Direct issue close calls (should go through phase protocol)
|
||||
# - git checkout / git switch to primary branch (stay on feature branch)
|
||||
# - FACTORY_ROOT access from worktrees (formula agents exempted)
|
||||
|
|
@ -88,7 +88,7 @@ if [ -n "$worktree_path" ] \
|
|||
fi
|
||||
fi
|
||||
|
||||
# --- Guard 3: Direct Codeberg API merge calls ---
|
||||
# --- Guard 3: Direct forge API merge calls ---
|
||||
if printf '%s' "$command_str" | grep -qE '/pulls/[0-9]+/merge'; then
|
||||
printf 'BLOCKED: Direct API merge calls must go through the phase protocol. Push your changes and write PHASE:awaiting_ci — the orchestrator handles merges.\n'
|
||||
exit 2
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
# source lib/load-project.sh projects/harb.toml
|
||||
#
|
||||
# Exports:
|
||||
# PROJECT_NAME, CODEBERG_REPO, CODEBERG_API, PROJECT_REPO_ROOT,
|
||||
# PRIMARY_BRANCH, WOODPECKER_REPO_ID, PROJECT_CONTAINERS,
|
||||
# CHECK_PRS, CHECK_DEV_AGENT, CHECK_PIPELINE_STALL, CI_STALE_MINUTES
|
||||
# PROJECT_NAME, FORGE_REPO, FORGE_API, FORGE_WEB, FORGE_URL,
|
||||
# PROJECT_REPO_ROOT, PRIMARY_BRANCH, WOODPECKER_REPO_ID,
|
||||
# PROJECT_CONTAINERS, CHECK_PRS, CHECK_DEV_AGENT,
|
||||
# CHECK_PIPELINE_STALL, CI_STALE_MINUTES
|
||||
# (plus backwards-compat aliases: CODEBERG_REPO, CODEBERG_API, CODEBERG_WEB)
|
||||
#
|
||||
# If no argument given, does nothing (allows poll scripts to work with
|
||||
# plain .env fallback for backwards compatibility).
|
||||
|
|
@ -35,7 +37,8 @@ def emit(key, val):
|
|||
|
||||
# Top-level
|
||||
emit('PROJECT_NAME', cfg.get('name', ''))
|
||||
emit('CODEBERG_REPO', cfg.get('repo', ''))
|
||||
emit('FORGE_REPO', cfg.get('repo', ''))
|
||||
emit('FORGE_URL', cfg.get('forge_url', ''))
|
||||
|
||||
if 'repo_root' in cfg:
|
||||
emit('PROJECT_REPO_ROOT', cfg['repo_root'])
|
||||
|
|
@ -79,11 +82,17 @@ while IFS='=' read -r _key _val; do
|
|||
export "$_key=$_val"
|
||||
done <<< "$_PROJECT_VARS"
|
||||
|
||||
# Derive CODEBERG_API and CODEBERG_WEB if repo changed
|
||||
if [ -n "$CODEBERG_REPO" ]; then
|
||||
export CODEBERG_API="https://codeberg.org/api/v1/repos/${CODEBERG_REPO}"
|
||||
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
|
||||
# Derive FORGE_API and FORGE_WEB from forge_url + repo
|
||||
# FORGE_URL: TOML forge_url > existing FORGE_URL > default
|
||||
export FORGE_URL="${FORGE_URL:-http://localhost:3000}"
|
||||
if [ -n "$FORGE_REPO" ]; then
|
||||
export FORGE_API="${FORGE_URL}/api/v1/repos/${FORGE_REPO}"
|
||||
export FORGE_WEB="${FORGE_URL}/${FORGE_REPO}"
|
||||
fi
|
||||
# Backwards-compat aliases
|
||||
export CODEBERG_REPO="${FORGE_REPO}"
|
||||
export CODEBERG_API="${FORGE_API:-}"
|
||||
export CODEBERG_WEB="${FORGE_WEB:-}"
|
||||
|
||||
# Derive PROJECT_REPO_ROOT if not explicitly set
|
||||
if [ -z "${PROJECT_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ _SAFE_PATTERNS=(
|
|||
# Git SHAs in typical git contexts (commit refs, not standalone secrets)
|
||||
'commit [0-9a-f]{40}'
|
||||
'Merge [0-9a-f]{40}'
|
||||
# Codeberg/GitHub URLs with short hex (PR refs, commit links)
|
||||
# Forge/GitHub URLs with short hex (PR refs, commit links)
|
||||
'codeberg\.org/[^[:space:]]+'
|
||||
'localhost:3000/[^[:space:]]+'
|
||||
# ShellCheck directive codes
|
||||
'SC[0-9]{4}'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,6 @@ prerequisite tree but NOT as issues. This prevents the "spray issues across
|
|||
all milestones" pattern that produced premature work in planner v1/v2.
|
||||
|
||||
**Environment variables consumed**:
|
||||
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to opus by planner-run.sh)
|
||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
||||
|
|
|
|||
|
|
@ -80,13 +80,13 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
|||
|
||||
# ── Build prompt ─────────────────────────────────────────────────────────
|
||||
build_prompt_footer "
|
||||
Relabel: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}'
|
||||
Comment: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'
|
||||
Close: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"state\":\"closed\"}'
|
||||
Relabel: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X PUT -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}'
|
||||
Comment: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X POST -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'
|
||||
Close: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X PATCH -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}' -d '{\"state\":\"closed\"}'
|
||||
"
|
||||
|
||||
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
||||
PROMPT="You are the strategic planner for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
|
||||
PROMPT="You are the strategic planner for ${FORGE_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
|
||||
|
||||
## Project context
|
||||
${CONTEXT_BLOCK}${MEMORY_BLOCK}${JOURNAL_BLOCK}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ memory check (skips if available RAM < 2000 MB).
|
|||
|
||||
**Key files**:
|
||||
- `predictor/predictor-run.sh` — Cron wrapper + orchestrator: lock, memory guard,
|
||||
sources disinto project config, builds prompt with formula + Codeberg API
|
||||
sources disinto project config, builds prompt with formula + forge API
|
||||
reference, creates tmux session (sonnet), monitors phase file, handles crash
|
||||
recovery via `run_formula_and_monitor`
|
||||
- `formulas/run-predictor.toml` — Execution spec: two steps (preflight,
|
||||
|
|
@ -37,7 +37,7 @@ memory check (skips if available RAM < 2000 MB).
|
|||
interactive session
|
||||
|
||||
**Environment variables consumed**:
|
||||
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by predictor-run.sh)
|
||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Notifications (optional)
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
|||
build_prompt_footer
|
||||
|
||||
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
||||
PROMPT="You are the prediction agent (goblin) for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
|
||||
PROMPT="You are the prediction agent (goblin) for ${FORGE_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
|
||||
|
||||
Your role: abstract adversary. Find the project's biggest weakness, challenge
|
||||
planner claims, and generate evidence. Explore when uncertain (file a prediction),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# projects/disinto.toml.example — Template for disinto self-management
|
||||
#
|
||||
# Copy to projects/disinto.toml and fill in box-specific values,
|
||||
# or run: disinto init https://codeberg.org/johba/disinto
|
||||
# or run: disinto init johba/disinto
|
||||
|
||||
name = "disinto"
|
||||
repo = "johba/disinto"
|
||||
forge_url = "http://localhost:3000"
|
||||
repo_root = "/home/YOU/dark-factory"
|
||||
primary_branch = "main"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# projects/harb.toml.example — Template for johba/harb
|
||||
#
|
||||
# Copy to projects/harb.toml and fill in box-specific values,
|
||||
# or run: disinto init https://codeberg.org/johba/harb
|
||||
# or run: disinto init johba/harb
|
||||
|
||||
name = "harb"
|
||||
repo = "johba/harb"
|
||||
forge_url = "http://localhost:3000"
|
||||
repo_root = "/home/YOU/harb"
|
||||
primary_branch = "master"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# projects/versi.toml.example — Template for johba/versi
|
||||
#
|
||||
# Copy to projects/versi.toml and fill in box-specific values,
|
||||
# or run: disinto init https://codeberg.org/johba/versi
|
||||
# or run: disinto init johba/versi
|
||||
|
||||
name = "versi"
|
||||
repo = "johba/versi"
|
||||
forge_url = "http://localhost:3000"
|
||||
repo_root = "/home/YOU/versi"
|
||||
primary_branch = "main"
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Review Agent
|
||||
|
||||
**Role**: AI-powered PR review — post structured findings and formal
|
||||
approve/request-changes verdicts to Codeberg.
|
||||
approve/request-changes verdicts to forge.
|
||||
|
||||
**Trigger**: `review-poll.sh` runs every 10 min via cron. It scans open PRs
|
||||
whose CI has passed and that lack a review for the current HEAD SHA, then
|
||||
|
|
@ -10,11 +10,11 @@ spawns `review-pr.sh <pr-number>`.
|
|||
|
||||
**Key files**:
|
||||
- `review/review-poll.sh` — Cron scheduler: finds unreviewed PRs with passing CI
|
||||
- `review/review-pr.sh` — Creates/reuses a tmux session (`review-{project}-{pr}`), injects PR diff, waits for Claude to write structured JSON output, posts markdown review + formal Codeberg review, auto-creates follow-up issues for pre-existing tech debt
|
||||
- `review/review-pr.sh` — Creates/reuses a tmux session (`review-{project}-{pr}`), injects PR diff, waits for Claude to write structured JSON output, posts markdown review + formal forge review, auto-creates follow-up issues for pre-existing tech debt
|
||||
|
||||
**Environment variables consumed**:
|
||||
- `CODEBERG_TOKEN` — Dev-agent token (must not be the same account as REVIEW_BOT_TOKEN)
|
||||
- `REVIEW_BOT_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist)
|
||||
- `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `FORGE_TOKEN` — Dev-agent token (must not be the same account as FORGE_REVIEW_TOKEN)
|
||||
- `FORGE_REVIEW_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist)
|
||||
- `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `PRIMARY_BRANCH`, `WOODPECKER_REPO_ID`
|
||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ source "$(dirname "$0")/../lib/ci-helpers.sh"
|
|||
REPO_ROOT="${PROJECT_REPO_ROOT}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
API_BASE="${CODEBERG_API}"
|
||||
API_BASE="${FORGE_API}"
|
||||
LOGFILE="$SCRIPT_DIR/review.log"
|
||||
MAX_REVIEWS=3
|
||||
REVIEW_IDLE_TIMEOUT=14400 # 4h: kill review session if idle
|
||||
|
|
@ -44,7 +44,7 @@ if [ -n "$REVIEW_SESSIONS" ]; then
|
|||
phase_file="/tmp/review-session-${PROJECT_NAME}-${pr_num}.phase"
|
||||
|
||||
# Check if PR is still open
|
||||
pr_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
pr_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${pr_num}" | jq -r '.state // "unknown"' 2>/dev/null) || true
|
||||
|
||||
if [ "$pr_state" != "open" ]; then
|
||||
|
|
@ -92,7 +92,7 @@ if [ -n "$REVIEW_SESSIONS" ]; then
|
|||
done <<< "$REVIEW_SESSIONS"
|
||||
fi
|
||||
|
||||
PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls?state=open&limit=20" | \
|
||||
jq -r --arg branch "${PRIMARY_BRANCH}" '.[] | select(.base.ref == $branch) | select(.draft != true) | select(.title | test("^\\[?WIP[\\]:]"; "i") | not) | "\(.number) \(.head.sha) \(.head.ref)"')
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ inject_review_into_dev_session() {
|
|||
[ "${current_phase}" = "PHASE:awaiting_review" ] || return 0
|
||||
|
||||
local review_comment
|
||||
review_comment=$(codeberg_api_all "/issues/${pr_num}/comments" | \
|
||||
review_comment=$(forge_api_all "/issues/${pr_num}/comments" | \
|
||||
jq -r --arg sha "${pr_sha}" \
|
||||
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
||||
if [ -z "${review_comment}" ] || [ "${review_comment}" = "null" ]; then
|
||||
|
|
@ -185,7 +185,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
|||
reviewed_sha=$(sed -n 's/^SHA://p' "$phase_file" 2>/dev/null | tr -d '[:space:]' || true)
|
||||
[ -n "$reviewed_sha" ] || continue
|
||||
|
||||
pr_json=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
pr_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${pr_num}" 2>/dev/null || true)
|
||||
[ -n "$pr_json" ] || continue
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
|||
pr_branch=$(printf '%s' "$pr_json" | jq -r '.head.ref // ""')
|
||||
if [ -z "$current_sha" ] || [ "$current_sha" = "$reviewed_sha" ]; then continue; fi
|
||||
|
||||
ci_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/commits/${current_sha}/status" | jq -r '.state // "unknown"')
|
||||
|
||||
if ! ci_passed "$ci_state"; then
|
||||
|
|
@ -210,7 +210,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
|||
|
||||
if "${SCRIPT_DIR}/review-pr.sh" "$pr_num" 2>&1; then
|
||||
REVIEWED=$((REVIEWED + 1))
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${pr_num}" | jq -r '.head.sha // ""') || true
|
||||
inject_review_into_dev_session "$pr_num" "${FRESH_SHA:-$current_sha}" "$pr_branch"
|
||||
else
|
||||
|
|
@ -227,7 +227,7 @@ while IFS= read -r line; do
|
|||
PR_SHA=$(echo "$line" | awk '{print $2}')
|
||||
PR_BRANCH=$(echo "$line" | awk '{print $3}')
|
||||
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
|
||||
|
||||
# Skip if CI is running/failed. Allow "success", no CI configured, or non-code PRs
|
||||
|
|
@ -240,8 +240,8 @@ while IFS= read -r line; do
|
|||
log " #${PR_NUM} CI=${CI_STATE} but no code files — proceeding"
|
||||
fi
|
||||
|
||||
# Check formal Codeberg reviews (not comment markers)
|
||||
HAS_REVIEW=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
# Check formal forge reviews (not comment markers)
|
||||
HAS_REVIEW=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${PR_NUM}/reviews" | \
|
||||
jq -r --arg sha "$PR_SHA" \
|
||||
'[.[] | select(.commit_id == $sha) | select(.state != "COMMENT")] | length')
|
||||
|
|
@ -259,7 +259,7 @@ while IFS= read -r line; do
|
|||
# Re-fetch current SHA: review-pr.sh fetches the PR independently and tags its
|
||||
# comment with whatever SHA it saw. If a commit arrived while review-pr.sh was
|
||||
# running those two SHA captures diverge and we would miss the comment.
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${PR_NUM}" | jq -r '.head.sha // ""') || true
|
||||
inject_review_into_dev_session "$PR_NUM" "${FRESH_SHA:-$PR_SHA}" "$PR_BRANCH"
|
||||
else
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true
|
|||
|
||||
PR_NUMBER="${1:?Usage: review-pr.sh <pr-number> [--force]}"
|
||||
FORCE="${2:-}"
|
||||
API="${CODEBERG_API}"
|
||||
API="${FORGE_API}"
|
||||
LOGFILE="${FACTORY_ROOT}/review/review.log"
|
||||
SESSION="review-${PROJECT_NAME}-${PR_NUMBER}"
|
||||
PHASE_FILE="/tmp/review-session-${PROJECT_NAME}-${PR_NUMBER}.phase"
|
||||
|
|
@ -37,7 +37,7 @@ if [ -f "$LOCKFILE" ]; then
|
|||
fi
|
||||
echo $$ > "$LOCKFILE"
|
||||
status "fetching metadata"
|
||||
PR_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" "${API}/pulls/${PR_NUMBER}")
|
||||
PR_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" "${API}/pulls/${PR_NUMBER}")
|
||||
PR_TITLE=$(printf '%s' "$PR_JSON" | jq -r '.title')
|
||||
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')
|
||||
PR_HEAD=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
|
||||
|
|
@ -50,16 +50,16 @@ if [ "$PR_STATE" != "open" ]; then
|
|||
cd "${PROJECT_REPO_ROOT}"; git worktree remove "$WORKTREE" --force 2>/dev/null || true
|
||||
rm -rf "$WORKTREE" "$PHASE_FILE" "$OUTPUT_FILE" 2>/dev/null || true; exit 0
|
||||
fi
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
|
||||
CI_NOTE=""; if ! ci_passed "$CI_STATE"; then
|
||||
ci_required_for_pr "$PR_NUMBER" && { log "SKIP: CI=${CI_STATE}"; exit 0; }
|
||||
CI_NOTE=" (not required — non-code PR)"; fi
|
||||
ALL_COMMENTS=$(codeberg_api_all "/issues/${PR_NUMBER}/comments")
|
||||
ALL_COMMENTS=$(forge_api_all "/issues/${PR_NUMBER}/comments")
|
||||
HAS_CMT=$(printf '%s' "$ALL_COMMENTS" | jq --arg s "$PR_SHA" \
|
||||
'[.[]|select(.body|contains("<!-- reviewed: "+$s+" -->"))]|length')
|
||||
[ "${HAS_CMT:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: reviewed ${PR_SHA:0:7}"; exit 0; }
|
||||
HAS_FML=$(codeberg_api_all "/pulls/${PR_NUMBER}/reviews" | jq --arg s "$PR_SHA" \
|
||||
HAS_FML=$(forge_api_all "/pulls/${PR_NUMBER}/reviews" | jq --arg s "$PR_SHA" \
|
||||
'[.[]|select(.commit_id==$s)|select(.state!="COMMENT")]|length')
|
||||
[ "${HAS_FML:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: formal review"; exit 0; }
|
||||
PREV_CONTEXT="" IS_RE_REVIEW=false PREV_SHA=""
|
||||
|
|
@ -81,7 +81,7 @@ if [ -n "$PREV_REV" ] && [ "$PREV_REV" != "null" ]; then
|
|||
fi
|
||||
fi
|
||||
status "fetching diff"
|
||||
curl -s -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
curl -s -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}.diff" > "${REVIEW_TMPDIR}/full.diff"
|
||||
FSIZE=$(stat -c%s "${REVIEW_TMPDIR}/full.diff" 2>/dev/null || echo 0)
|
||||
DIFF=$(head -c "$MAX_DIFF" "${REVIEW_TMPDIR}/full.diff")
|
||||
|
|
@ -97,15 +97,15 @@ status "preparing review session"
|
|||
FORMULA=$(cat "${FACTORY_ROOT}/formulas/review-pr.toml")
|
||||
{
|
||||
printf 'You are the review agent for %s. Follow the formula to review PR #%s.\nYou MUST write PHASE:done to '\''%s'\'' when finished.\n\n' \
|
||||
"${CODEBERG_REPO}" "${PR_NUMBER}" "${PHASE_FILE}"
|
||||
"${FORGE_REPO}" "${PR_NUMBER}" "${PHASE_FILE}"
|
||||
printf '## PR Context\n**%s** (%s → %s) | SHA: %s | CI: %s%s\nRe-review: %s\n\n' \
|
||||
"$PR_TITLE" "$PR_HEAD" "$PR_BASE" "$PR_SHA" "$CI_STATE" "$CI_NOTE" "$IS_RE_REVIEW"
|
||||
printf '### Description\n%s\n\n### Changed Files\n%s\n\n### Diff%s\n```diff\n%s\n```\n' \
|
||||
"$PR_BODY" "$FILES" "$DNOTE" "$DIFF"
|
||||
[ -n "$PREV_CONTEXT" ] && printf '%s\n' "$PREV_CONTEXT"
|
||||
printf '\n## Formula\n%s\n\n## Environment\nREVIEW_OUTPUT_FILE=%s\nPHASE_FILE=%s\nCODEBERG_API=%s\nPR_NUMBER=%s\nFACTORY_ROOT=%s\n' \
|
||||
printf '\n## Formula\n%s\n\n## Environment\nREVIEW_OUTPUT_FILE=%s\nPHASE_FILE=%s\nFORGE_API=%s\nPR_NUMBER=%s\nFACTORY_ROOT=%s\n' \
|
||||
"$FORMULA" "$OUTPUT_FILE" "$PHASE_FILE" "$API" "$PR_NUMBER" "$FACTORY_ROOT"
|
||||
printf 'NEVER echo the actual token — always reference $CODEBERG_TOKEN or $REVIEW_BOT_TOKEN.\n'
|
||||
printf 'NEVER echo the actual token — always reference ${FORGE_TOKEN} or ${FORGE_REVIEW_TOKEN}.\n'
|
||||
} > "${REVIEW_TMPDIR}/prompt.md"
|
||||
PROMPT=$(cat "${REVIEW_TMPDIR}/prompt.md")
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ fi
|
|||
if [ -z "$REVIEW_JSON" ]; then
|
||||
log "ERROR: no valid review output"
|
||||
jq -n --arg b "## AI Review — Error\n<!-- review-error: ${PR_SHA} -->\nReview failed.\n---\n*${PR_SHA:0:7}*" \
|
||||
'{body: $b}' | curl -sf -o /dev/null -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
'{body: $b}' | curl -sf -o /dev/null -X POST -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" "${API}/issues/${PR_NUMBER}/comments" -d @- || true
|
||||
matrix_send "review" "PR #${PR_NUMBER} review failed" 2>/dev/null || true; exit 1
|
||||
fi
|
||||
|
|
@ -163,7 +163,7 @@ COMMENT_BODY=$(printf '## AI %s\n<!-- reviewed: %s -->\n\n%s\n\n### Verdict\n**%
|
|||
printf '%s' "$COMMENT_BODY" > "${REVIEW_TMPDIR}/body.txt"
|
||||
jq -Rs '{body: .}' < "${REVIEW_TMPDIR}/body.txt" > "${REVIEW_TMPDIR}/comment.json"
|
||||
POST_RC=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${REVIEW_BOT_TOKEN}" -H "Content-Type: application/json" \
|
||||
-H "Authorization: token ${FORGE_REVIEW_TOKEN}" -H "Content-Type: application/json" \
|
||||
"${API}/issues/${PR_NUMBER}/comments" --data-binary @"${REVIEW_TMPDIR}/comment.json")
|
||||
[ "$POST_RC" != "201" ] && { log "ERROR: comment HTTP ${POST_RC}"; exit 1; }
|
||||
log "posted review comment"
|
||||
|
|
@ -171,19 +171,19 @@ log "posted review comment"
|
|||
REVENT="COMMENT"
|
||||
case "$VERDICT" in APPROVE) REVENT="APPROVED" ;; REQUEST_CHANGES|DISCUSS) REVENT="REQUEST_CHANGES" ;; esac
|
||||
if [ "$REVENT" = "APPROVED" ]; then
|
||||
BLOGIN=$(curl -sf -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
|
||||
BLOGIN=$(curl -sf -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
|
||||
"${API%%/repos*}/user" 2>/dev/null | jq -r '.login // empty' || true)
|
||||
[ -n "$BLOGIN" ] && codeberg_api_all "/pulls/${PR_NUMBER}/reviews" "$REVIEW_BOT_TOKEN" 2>/dev/null | \
|
||||
[ -n "$BLOGIN" ] && forge_api_all "/pulls/${PR_NUMBER}/reviews" "${FORGE_REVIEW_TOKEN}" 2>/dev/null | \
|
||||
jq -r --arg l "$BLOGIN" '.[]|select(.state=="REQUEST_CHANGES")|select(.user.login==$l)|.id' | \
|
||||
while IFS= read -r rid; do
|
||||
curl -sf -o /dev/null -X POST -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
|
||||
curl -sf -o /dev/null -X POST -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
|
||||
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews/${rid}/dismissals" \
|
||||
-d '{"message":"Superseded by approval"}' || true; log "dismissed review ${rid}"
|
||||
done || true
|
||||
fi
|
||||
jq -n --arg b "AI ${RTYPE}: **${VERDICT}** — ${REASON}" --arg e "$REVENT" --arg s "$PR_SHA" \
|
||||
'{body: $b, event: $e, commit_id: $s}' > "${REVIEW_TMPDIR}/formal.json"
|
||||
curl -s -o /dev/null -X POST -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
|
||||
curl -s -o /dev/null -X POST -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
|
||||
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews" \
|
||||
--data-binary @"${REVIEW_TMPDIR}/formal.json" >/dev/null 2>&1 || true
|
||||
log "formal ${REVENT} submitted"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@
|
|||
<div class="footer">
|
||||
<div>Data refreshed every 6 hours by the metrics collector.</div>
|
||||
<div style="margin-top:0.5rem">
|
||||
<a href="https://codeberg.org/johba/disinto">source</a>
|
||||
<a href="http://localhost:3000/johba/disinto">source</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -421,7 +421,7 @@
|
|||
var card = el('div', 'project');
|
||||
var nameDiv = el('div', 'name');
|
||||
var nameLink = document.createElement('a');
|
||||
nameLink.href = 'https://codeberg.org/' + p.repo;
|
||||
nameLink.href = 'http://localhost:3000/' + p.repo;
|
||||
nameLink.textContent = p.name;
|
||||
nameDiv.appendChild(nameLink);
|
||||
card.appendChild(nameDiv);
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@
|
|||
<!-- Eight Agents -->
|
||||
<div class="section">
|
||||
<h2>Eight agents</h2>
|
||||
<p>Each agent has a single responsibility. They communicate through git, the Codeberg API, and the filesystem.</p>
|
||||
<p>Each agent has a single responsibility. They communicate through git, the forge API, and the filesystem.</p>
|
||||
<div class="agents-grid">
|
||||
<div class="agent-card">
|
||||
<div class="name">dev-agent</div>
|
||||
|
|
@ -480,7 +480,7 @@
|
|||
<p><strong>Bash scripts</strong> — every agent is a shell script. No compiled binaries, no runtimes to install.</p>
|
||||
<p><strong>Claude CLI</strong> — AI is invoked via <code>claude -p</code> (one-shot) or <code>claude</code> (persistent tmux sessions).</p>
|
||||
<p><strong>Cron</strong> — agents are triggered by cron jobs, not a daemon. Pull-based, not push-based.</p>
|
||||
<p><strong>Codeberg + Woodpecker</strong> — git hosting and CI. All state lives in git and the issue tracker. No external databases.</p>
|
||||
<p><strong>Forgejo + Woodpecker</strong> — git hosting and CI. All state lives in git and the issue tracker. No external databases.</p>
|
||||
<p><strong>Single VPS</strong> — runs on an 8 GB server. Flat cost, no scaling surprises.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -538,7 +538,7 @@ disinto/
|
|||
<div class="footer">
|
||||
<a href="/">← disinto.ai</a> ·
|
||||
<a href="/docs/quickstart">Quickstart</a> ·
|
||||
<a href="https://codeberg.org/johba/disinto">Source</a>
|
||||
<a href="http://localhost:3000/johba/disinto">Source</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -328,8 +328,8 @@
|
|||
<div class="label">Prerequisites</div>
|
||||
<ul>
|
||||
<li><strong>A VPS or server</strong> — 8 GB RAM minimum (Ubuntu/Debian recommended)</li>
|
||||
<li><strong>A Codeberg account</strong> — with a repo and at least one issue</li>
|
||||
<li><strong>A second Codeberg account</strong> — for the review bot (branch protection requires a different reviewer)</li>
|
||||
<li><strong>A forge instance</strong> — with a repo and at least one issue</li>
|
||||
<li><strong>A second forge instance</strong> — for the review bot (branch protection requires a different reviewer)</li>
|
||||
<li><strong>Woodpecker CI</strong> — running and connected to your repo</li>
|
||||
<li><strong>An Anthropic API key</strong> — with the <code>claude</code> CLI installed and authenticated</li>
|
||||
<li><strong>tmux</strong> — for persistent dev sessions</li>
|
||||
|
|
@ -343,13 +343,13 @@
|
|||
Clone the factory
|
||||
</div>
|
||||
<p>Clone disinto onto your server. This is the factory — the code that runs your agents.</p>
|
||||
<pre><code>git clone https://codeberg.org/johba/disinto.git ~/disinto
|
||||
<pre><code>git clone http://localhost:3000/johba/disinto.git ~/disinto
|
||||
cd ~/disinto
|
||||
cp .env.example .env</code></pre>
|
||||
<p>Edit <code>.env</code> with your tokens:</p>
|
||||
<pre><code><span class="comment"># Required</span>
|
||||
CODEBERG_TOKEN=your_codeberg_token
|
||||
REVIEW_BOT_TOKEN=your_review_bot_token
|
||||
FORGE_TOKEN=your_codeberg_token
|
||||
FORGE_REVIEW_TOKEN=your_review_bot_token
|
||||
|
||||
<span class="comment"># Woodpecker CI</span>
|
||||
WOODPECKER_TOKEN=your_woodpecker_token
|
||||
|
|
@ -365,14 +365,14 @@ CLAUDE_TIMEOUT=7200</code></pre>
|
|||
<span class="step-num">2</span>
|
||||
Initialize your project
|
||||
</div>
|
||||
<p><code>disinto init</code> sets up everything: clones the repo, creates the project config, adds Codeberg labels, and installs cron jobs.</p>
|
||||
<pre><code>bin/disinto init https://codeberg.org/you/your-project</code></pre>
|
||||
<p><code>disinto init</code> provisions a local Forgejo instance, clones the repo, creates the project config, adds labels, and installs cron jobs.</p>
|
||||
<pre><code>bin/disinto init http://localhost:3000/you/your-project</code></pre>
|
||||
<div class="expected">
|
||||
<div class="label">Expected output</div>
|
||||
<code>=== disinto init ===
|
||||
Project: you/your-project
|
||||
Name: your-project
|
||||
Cloning: https://codeberg.org/you/your-project.git -> /home/you/your-project
|
||||
Cloning: http://localhost:3000/you/your-project.git -> /home/you/your-project
|
||||
Branch: main
|
||||
Created: /home/you/disinto/projects/your-project.toml
|
||||
Creating labels on you/your-project...
|
||||
|
|
@ -406,7 +406,7 @@ Done. Project your-project is ready.</code>
|
|||
<ol>
|
||||
<li><strong>A CI pipeline</strong> — at least one <code>.woodpecker/*.yml</code> file. Agents wait for CI before reviewing or merging.</li>
|
||||
<li><strong>A CLAUDE.md</strong> — project context that agents read before every task. Describe your tech stack, how to build/test, coding conventions, and directory layout.</li>
|
||||
<li><strong>Branch protection</strong> — on Codeberg, require PR reviews and add the review bot as a write collaborator.</li>
|
||||
<li><strong>Branch protection</strong> — on Forgejo, require PR reviews and add the review bot as a write collaborator.</li>
|
||||
</ol>
|
||||
<pre><code><span class="comment"># Create CLAUDE.md in your project</span>
|
||||
cat > ~/your-project/CLAUDE.md <<'EOF'
|
||||
|
|
@ -434,7 +434,7 @@ git push</code></pre>
|
|||
<span class="step-num">4</span>
|
||||
File your first issue
|
||||
</div>
|
||||
<p>Create an issue on Codeberg with the <code>backlog</code> label. Be specific — the dev-agent works best with clear acceptance criteria.</p>
|
||||
<p>Create an issue on the forge with the <code>backlog</code> label. Be specific — the dev-agent works best with clear acceptance criteria.</p>
|
||||
<pre><code><span class="comment"># Title: Add health check endpoint</span>
|
||||
<span class="comment"># Label: backlog</span>
|
||||
<span class="comment"># Body:</span>
|
||||
|
|
@ -523,7 +523,7 @@ git log --oneline -5</code></pre>
|
|||
<div class="footer">
|
||||
<a href="/">← disinto.ai</a> ·
|
||||
<a href="/docs/architecture">Architecture</a> ·
|
||||
<a href="https://codeberg.org/johba/disinto">Source</a>
|
||||
<a href="http://localhost:3000/johba/disinto">Source</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@
|
|||
<p>
|
||||
<strong>Bash scripts and Claude.</strong> No Kubernetes, no microservices,
|
||||
no SaaS dependencies. Runs on an 8GB VPS.
|
||||
Point it at a <strong>Codeberg repo</strong> with a
|
||||
Point it at a <strong>forge repo</strong> with a
|
||||
<strong>Woodpecker CI</strong> pipeline and it starts building.
|
||||
</p>
|
||||
<p>
|
||||
|
|
@ -691,8 +691,8 @@
|
|||
<div class="cta-links">
|
||||
<a href="/docs/quickstart">Quickstart</a>
|
||||
<a href="/docs/architecture">Architecture</a>
|
||||
<a href="https://codeberg.org/johba/disinto">Browse the source</a>
|
||||
<a href="https://codeberg.org/johba/disinto/issues">See active issues</a>
|
||||
<a href="http://localhost:3000/johba/disinto">Browse the source</a>
|
||||
<a href="http://localhost:3000/johba/disinto/issues">See active issues</a>
|
||||
<a href="/dashboard">Live dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -702,7 +702,7 @@
|
|||
<div class="links">
|
||||
<a href="/docs/quickstart">quickstart</a>
|
||||
<a href="/docs/architecture">architecture</a>
|
||||
<a href="https://codeberg.org/johba/disinto">source</a>
|
||||
<a href="http://localhost:3000/johba/disinto">source</a>
|
||||
<a href="/dashboard">dashboard</a>
|
||||
</div>
|
||||
<div class="under-hood">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
- Clean worktree: `cd $PROJECT_REPO_ROOT && git worktree remove /tmp/${PROJECT_NAME}-worktree-<N> --force`
|
||||
- Remove `in-progress` label if agent died without cleanup:
|
||||
```bash
|
||||
codeberg_api DELETE "/issues/<N>/labels/in-progress"
|
||||
forge_api DELETE "/issues/<N>/labels/in-progress"
|
||||
```
|
||||
|
||||
## Dangerous (escalate)
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
**Trust closed state.** If a dependency issue is closed, the code is on the primary branch. Period.
|
||||
|
||||
DO NOT try to find the specific PR that closed an issue. This is over-engineering that causes false negatives:
|
||||
- Codeberg shares issue/PR numbering — no guaranteed relationship
|
||||
- forge shares issue/PR numbering — no guaranteed relationship
|
||||
- PRs don't always mention the issue number in title/body
|
||||
- Searching last N closed PRs misses older merges
|
||||
- The dev-agent closes issues after merging, so closed = merged
|
||||
|
|
@ -52,7 +52,7 @@ The only check needed: `issue.state == "closed"`.
|
|||
The supervisor-poll alert 'status unchanged for Nmin' is a false positive for complex implementation tasks. The status is set to 'claude assessing + implementing' at the START of the `timeout 7200 claude -p ...` call and only updates after Claude finishes. Normal complex tasks (multi-file Solidity changes + forge test) take 45-90 minutes. To distinguish a false positive from a real stuck agent: check that the claude PID is alive (`ps -p <PID>`), consuming CPU (>0%), and has active threads (`pstree -p <PID>`). If the process is alive and using CPU, do NOT restart it — this wastes completed work.
|
||||
|
||||
### False Positive: 'Waiting for CI + Review' Alert
|
||||
The 'status unchanged for Nmin' alert is also a false positive when status is 'waiting for CI + review on PR #N (round R)'. This is an intentional sleep/poll loop — the agent is waiting for CI to pass and then for review-poll to post a review. CI can take 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/<sha>/status`, (3) review-poll log shows it will pick up the PR on next cycle.
|
||||
The 'status unchanged for Nmin' alert is also a false positive when status is 'waiting for CI + review on PR #N (round R)'. This is an intentional sleep/poll loop — the agent is waiting for CI to pass and then for review-poll to post a review. CI can take 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/<sha>/status`, (3) review-poll log shows it will pick up the PR on next cycle.
|
||||
|
||||
### False Positive: Shared Status File Causes Giant Age (29M+ min)
|
||||
When the status file `/tmp/dev-agent-status` doesn't exist, `stat -c %Y` fails and the supervisor falls back to epoch 0. The computed age is then `NOW_EPOCH/60 ≈ 29,567,290 min`, which is unmistakably a false positive.
|
||||
|
|
@ -73,7 +73,7 @@ Symptom: agent in awaiting_review with PR CI=failure and push CI=success.
|
|||
Fix: inject with explicit pipeline #623 (the pull_request event pipeline), point to the failing step and the specific duplicate blocks to fix. Use: woodpecker_api /repos/4/pipelines?event=pull_request (or look for event=pull_request in recent pipelines list) to find the correct pipeline number before injecting.
|
||||
|
||||
### Race Condition: Review Posted Before PHASE:awaiting_review Transitions
|
||||
**Symptom:** Dev-agent status unchanged at 'waiting for review on PR #N', no `review-injected-disinto-N` sentinel, but a formal review already exists on Codeberg and `/tmp/disinto-review-output-N.json` was written before the phase file updated.
|
||||
**Symptom:** Dev-agent status unchanged at 'waiting for review on PR #N', no `review-injected-disinto-N` sentinel, but a formal review already exists on forge and `/tmp/disinto-review-output-N.json` was written before the phase file updated.
|
||||
|
||||
**Root cause:** review-pr.sh runs while the dev-agent is still in PHASE:awaiting_ci. inject_review_into_dev_session returns early (phase check fails). On subsequent review-poll cycles, the PR is skipped (formal review already exists for SHA), so inject is never called again.
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ PROJECT_TOML=/home/debian/dark-factory/projects/disinto.toml
|
|||
source /home/debian/dark-factory/lib/load-project.sh "$PROJECT_TOML"
|
||||
PHASE_FILE="/tmp/dev-session-${PROJECT_NAME}-<ISSUE>.phase"
|
||||
PR_NUM=<N>; PR_BRANCH="fix/issue-<ISSUE>"; PR_SHA=$(cat /tmp/dev-session-${PROJECT_NAME}-<ISSUE>.phase | grep SHA | cut -d: -f2 || git -C $PROJECT_REPO_ROOT rev-parse origin/$PR_BRANCH)
|
||||
REVIEW_TEXT=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" "${CODEBERG_API}/issues/${PR_NUM}/comments?limit=50" | jq -r --arg sha "$PR_SHA" '[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty | .body')
|
||||
REVIEW_TEXT=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" "${FORGE_API}/issues/${PR_NUM}/comments?limit=50" | jq -r --arg sha "$PR_SHA" '[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty | .body')
|
||||
INJECT_MSG="Review: REQUEST_CHANGES on PR #${PR_NUM}:\n\n${REVIEW_TEXT}\n\nInstructions:\n1. Address each piece of feedback carefully.\n2. Run lint and tests when done.\n3. Commit your changes and push: git push origin ${PR_BRANCH}\n4. Write: echo PHASE:awaiting_ci > "${PHASE_FILE}"\n5. Stop and wait for the next CI result."
|
||||
INJECT_TMP=$(mktemp); printf '%s' "$INJECT_MSG" > "$INJECT_TMP"
|
||||
tmux load-buffer -b inject "$INJECT_TMP" && tmux paste-buffer -t "dev-${PROJECT_NAME}-<ISSUE>" -b inject && sleep 0.5 && tmux send-keys -t "dev-${PROJECT_NAME}-<ISSUE>" '' Enter
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Codeberg Best Practices
|
||||
# Forge Best Practices
|
||||
|
||||
## Rate Limiting
|
||||
Codeberg rate-limits SSH and HTTPS clones. Symptoms:
|
||||
The forge (Forgejo/Gitea) may rate-limit SSH and HTTPS clones. Symptoms:
|
||||
- Woodpecker `git` step fails with exit code 128
|
||||
- Multiple pipelines fail in quick succession with the same error
|
||||
- Retriggers make it WORSE by adding more clone attempts
|
||||
|
|
@ -26,10 +26,10 @@ cd <worktree> && git commit --allow-empty -m "ci: retrigger" --no-verify && git
|
|||
- One pipeline at a time is ideal on this VPS (resource + rate limit reasons).
|
||||
- If >3 pipelines are pending/running, do NOT create more work.
|
||||
|
||||
## OAuth Tokens
|
||||
- OAuth tokens expire ~2h. If Codeberg is down during refresh, re-login required.
|
||||
- API token is in `~/.netrc` — read via `awk` in env.sh.
|
||||
- Review bot has a separate token ($REVIEW_BOT_TOKEN) for formal reviews.
|
||||
## API Tokens
|
||||
- API token is in `.env` as `FORGE_TOKEN` — loaded via env.sh.
|
||||
- Review bot has a separate token (`$FORGE_REVIEW_TOKEN`) for formal reviews.
|
||||
- With local Forgejo, tokens don't expire. For remote forges, check provider docs.
|
||||
|
||||
## Lessons Learned
|
||||
- Retrigger storm on 2026-03-12: supervisor + dev-agent both retriggered during rate limit, caused 5+ failed pipelines. Added cooldown awareness.
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
## Architecture
|
||||
- `review-poll.sh` (cron */10) → finds open PRs with CI pass + no review → spawns `review-pr.sh`
|
||||
- `review-pr.sh` uses `claude -p` to review the diff, posts structured comment
|
||||
- Uses `review_bot` Codeberg account for formal reviews (separate from main account)
|
||||
- Uses `review_bot` forge account for formal reviews (separate from main account)
|
||||
- Skips WIP/draft PRs (`[WIP]` in title or draft flag)
|
||||
|
||||
## Safe Fixes
|
||||
|
|
@ -27,4 +27,4 @@
|
|||
- Review bot must output JSON — prevents self-narration collapse
|
||||
- DISCUSS verdict should be treated same as REQUEST_CHANGES by dev-agent
|
||||
- Error comments must NOT include `<!-- reviewed: SHA -->` — would falsely mark as reviewed
|
||||
- Review bot uses Codeberg formal reviews API — branch protection requires different user than PR author
|
||||
- Review bot uses forge formal reviews API — branch protection requires different user than PR author
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ emit_metric() {
|
|||
printf '%s\n' "$1" >> "$METRICS_FILE"
|
||||
}
|
||||
|
||||
# Count all matching items from a paginated Codeberg API endpoint.
|
||||
# Count all matching items from a paginated forge API endpoint.
|
||||
# Usage: codeberg_count_paginated "/issues?state=open&labels=backlog&type=issues"
|
||||
# Returns total count across all pages (max 20 pages = 1000 items).
|
||||
codeberg_count_paginated() {
|
||||
local endpoint="$1" total=0 page=1 count
|
||||
while true; do
|
||||
count=$(codeberg_api GET "${endpoint}&limit=50&page=${page}" 2>/dev/null | jq 'length' 2>/dev/null || echo 0)
|
||||
count=$(forge_api GET "${endpoint}&limit=50&page=${page}" 2>/dev/null | jq 'length' 2>/dev/null || echo 0)
|
||||
total=$((total + ${count:-0}))
|
||||
[ "${count:-0}" -lt 50 ] && break
|
||||
page=$((page + 1))
|
||||
|
|
@ -244,7 +244,7 @@ mkdir -p "$_RETRY_DIR"
|
|||
# Function: run all per-project checks for the currently loaded project config
|
||||
check_project() {
|
||||
local proj_name="${PROJECT_NAME:-unknown}"
|
||||
flog "── checking project: ${proj_name} (${CODEBERG_REPO}) ──"
|
||||
flog "── checking project: ${proj_name} (${FORGE_REPO}) ──"
|
||||
|
||||
# ===========================================================================
|
||||
# P2: FACTORY STOPPED — CI, dev-agent, git
|
||||
|
|
@ -366,8 +366,8 @@ check_project() {
|
|||
if [ "${CHECK_PIPELINE_STALL:-true}" = "true" ]; then
|
||||
status "P2: ${proj_name}: checking pipeline stall"
|
||||
|
||||
BACKLOG_COUNT=$(codeberg_api GET "/issues?state=open&labels=backlog&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
|
||||
IN_PROGRESS=$(codeberg_api GET "/issues?state=open&labels=in-progress&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
|
||||
BACKLOG_COUNT=$(forge_api GET "/issues?state=open&labels=backlog&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
|
||||
IN_PROGRESS=$(forge_api GET "/issues?state=open&labels=in-progress&type=issues&limit=1" 2>/dev/null | jq -r 'length' 2>/dev/null || echo "0")
|
||||
|
||||
if [ "${BACKLOG_COUNT:-0}" -gt 0 ] && [ "${IN_PROGRESS:-0}" -eq 0 ]; then
|
||||
DEV_LOG="${FACTORY_ROOT}/dev/dev-agent.log"
|
||||
|
|
@ -408,14 +408,14 @@ check_project() {
|
|||
if [ "${CHECK_PRS:-true}" = "true" ]; then
|
||||
status "P3: ${proj_name}: checking PRs"
|
||||
|
||||
OPEN_PRS=$(codeberg_api GET "/pulls?state=open&limit=10" 2>/dev/null | jq -r '.[].number' 2>/dev/null || true)
|
||||
OPEN_PRS=$(forge_api GET "/pulls?state=open&limit=10" 2>/dev/null | jq -r '.[].number' 2>/dev/null || true)
|
||||
for pr in $OPEN_PRS; do
|
||||
PR_JSON=$(codeberg_api GET "/pulls/${pr}" 2>/dev/null || true)
|
||||
PR_JSON=$(forge_api GET "/pulls/${pr}" 2>/dev/null || true)
|
||||
[ -z "$PR_JSON" ] && continue
|
||||
PR_SHA=$(echo "$PR_JSON" | jq -r '.head.sha // ""')
|
||||
[ -z "$PR_SHA" ] && continue
|
||||
|
||||
CI_STATE=$(codeberg_api GET "/commits/${PR_SHA}/status" 2>/dev/null | jq -r '.state // "unknown"' 2>/dev/null || true)
|
||||
CI_STATE=$(forge_api GET "/commits/${PR_SHA}/status" 2>/dev/null | jq -r '.state // "unknown"' 2>/dev/null || true)
|
||||
|
||||
MERGEABLE=$(echo "$PR_JSON" | jq -r '.mergeable // true')
|
||||
if [ "$MERGEABLE" = "false" ] && ci_passed "$CI_STATE"; then
|
||||
|
|
@ -429,7 +429,7 @@ check_project() {
|
|||
[ "$AGE_MIN" -gt 30 ] && p3 "${proj_name}: PR #${pr}: CI=${CI_STATE}, stale ${AGE_MIN}min"
|
||||
fi
|
||||
elif ci_passed "$CI_STATE"; then
|
||||
HAS_REVIEW=$(codeberg_api GET "/issues/${pr}/comments?limit=50" 2>/dev/null | \
|
||||
HAS_REVIEW=$(forge_api GET "/issues/${pr}/comments?limit=50" 2>/dev/null | \
|
||||
jq -r --arg sha "$PR_SHA" '[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | length' 2>/dev/null || echo "0")
|
||||
|
||||
if [ "${HAS_REVIEW:-0}" -eq 0 ]; then
|
||||
|
|
@ -454,7 +454,7 @@ check_project() {
|
|||
# ===========================================================================
|
||||
status "P3: ${proj_name}: checking for circular dependencies"
|
||||
|
||||
BACKLOG_FOR_DEPS=$(codeberg_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null || true)
|
||||
BACKLOG_FOR_DEPS=$(forge_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null || true)
|
||||
if [ -n "$BACKLOG_FOR_DEPS" ] && [ "$BACKLOG_FOR_DEPS" != "null" ] && [ "$(echo "$BACKLOG_FOR_DEPS" | jq 'length' 2>/dev/null || echo 0)" -gt 0 ]; then
|
||||
|
||||
PARSE_DEPS="${FACTORY_ROOT}/lib/parse-deps.sh"
|
||||
|
|
@ -524,7 +524,7 @@ check_project() {
|
|||
if [ -n "${DEP_CACHE[$dep]+x}" ]; then
|
||||
DEP_INFO="${DEP_CACHE[$dep]}"
|
||||
else
|
||||
DEP_JSON=$(codeberg_api GET "/issues/${dep}" 2>/dev/null || true)
|
||||
DEP_JSON=$(forge_api GET "/issues/${dep}" 2>/dev/null || true)
|
||||
[ -z "$DEP_JSON" ] && continue
|
||||
DEP_STATE=$(echo "$DEP_JSON" | jq -r '.state // "unknown"')
|
||||
DEP_CREATED=$(echo "$DEP_JSON" | jq -r '.created_at // ""')
|
||||
|
|
@ -646,8 +646,8 @@ Instructions:
|
|||
_sess_issue="${_sess#dev-"${proj_name}"-}"
|
||||
[[ "$_sess_issue" =~ ^[0-9]+$ ]] || continue
|
||||
|
||||
# Check Codeberg: is the issue still open?
|
||||
_issue_state=$(codeberg_api GET "/issues/${_sess_issue}" 2>/dev/null \
|
||||
# Check forge: is the issue still open?
|
||||
_issue_state=$(forge_api GET "/issues/${_sess_issue}" 2>/dev/null \
|
||||
| jq -r '.state // "open"' 2>/dev/null || echo "open")
|
||||
|
||||
_should_cleanup=false
|
||||
|
|
@ -671,7 +671,7 @@ Instructions:
|
|||
_has_open_pr=0
|
||||
_pr_page=1
|
||||
while true; do
|
||||
_pr_page_json=$(codeberg_api GET "/pulls?state=open&limit=50&page=${_pr_page}" \
|
||||
_pr_page_json=$(forge_api GET "/pulls?state=open&limit=50&page=${_pr_page}" \
|
||||
2>/dev/null || echo "[]")
|
||||
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
|
||||
_pr_match=$(printf '%s' "$_pr_page_json" | \
|
||||
|
|
@ -689,7 +689,7 @@ Instructions:
|
|||
_has_closed_pr=0
|
||||
_pr_page=1
|
||||
while true; do
|
||||
_pr_page_json=$(codeberg_api GET "/pulls?state=closed&limit=50&page=${_pr_page}" \
|
||||
_pr_page_json=$(forge_api GET "/pulls?state=closed&limit=50&page=${_pr_page}" \
|
||||
2>/dev/null || echo "[]")
|
||||
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
|
||||
_pr_match=$(printf '%s' "$_pr_page_json" | \
|
||||
|
|
@ -771,7 +771,7 @@ if [ -d "$PROJECTS_DIR" ]; then
|
|||
[ -f "$project_toml" ] || continue
|
||||
PROJECT_COUNT=$((PROJECT_COUNT + 1))
|
||||
|
||||
# Load project config (overrides CODEBERG_REPO, PROJECT_REPO_ROOT, etc.)
|
||||
# Load project config (overrides FORGE_REPO, PROJECT_REPO_ROOT, etc.)
|
||||
source "${FACTORY_ROOT}/lib/load-project.sh" "$project_toml"
|
||||
|
||||
check_project || flog "check_project failed for ${project_toml} (per-project checks incomplete)"
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
|||
build_prompt_footer
|
||||
|
||||
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
||||
PROMPT="You are the supervisor agent for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
|
||||
PROMPT="You are the supervisor agent for ${FORGE_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
|
||||
|
||||
You have full shell access and --dangerously-skip-permissions.
|
||||
Fix what you can. Escalate what you cannot. Do NOT ask permission — act first, report after.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Vault Agent
|
||||
|
||||
You are the vault agent for `$CODEBERG_REPO`. You were called by
|
||||
You are the vault agent for `$FORGE_REPO`. You were called by
|
||||
`vault-poll.sh` because one or more actions in `vault/pending/` need
|
||||
classification and routing.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue