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.
|
# NEVER commit .env to the repo.
|
||||||
|
|
||||||
# ── Per-project config ────────────────────────────────────────────────────
|
# ── 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
|
# 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.
|
# 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 ───────────────────────────────────────────────────────────
|
# ── Auth tokens ───────────────────────────────────────────────────────────
|
||||||
# Dev-agent token: push branches, create PRs, merge PRs.
|
# 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.
|
# Branch protection: this account must be in the merge whitelist.
|
||||||
CODEBERG_TOKEN=
|
FORGE_TOKEN=
|
||||||
|
|
||||||
# Review-agent token: post review comments and submit formal approvals.
|
# 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.
|
# 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.
|
# 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 CI ─────────────────────────────────────────────────────────
|
||||||
WOODPECKER_TOKEN=
|
WOODPECKER_TOKEN=
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ echo "=== 2/2 Function resolution ==="
|
||||||
# Functions provided by shared lib files (available to all agent scripts via source).
|
# Functions provided by shared lib files (available to all agent scripts via source).
|
||||||
#
|
#
|
||||||
# Included — these are inline-sourced by agent scripts:
|
# 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/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/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
|
# lib/load-project.sh — sourced by env.sh when PROJECT_TOML is set
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
## What this repo is
|
## What this repo is
|
||||||
|
|
||||||
Disinto is an autonomous code factory. It manages eight agents (dev, review,
|
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
|
implement them, review PRs, plan from the vision, gate dangerous actions, and
|
||||||
keep the system healthy — all via cron and `claude -p`.
|
keep the system healthy — all via cron and `claude -p`.
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ disinto/
|
||||||
- **Shell**: bash (all agents are bash scripts)
|
- **Shell**: bash (all agents are bash scripts)
|
||||||
- **AI**: `claude -p` (one-shot) or `claude` (interactive/tmux sessions)
|
- **AI**: `claude -p` (one-shot) or `claude` (interactive/tmux sessions)
|
||||||
- **CI**: Woodpecker CI (queried via REST API + Postgres)
|
- **CI**: Woodpecker CI (queried via REST API + Postgres)
|
||||||
- **VCS**: Codeberg (git + Gitea REST API)
|
- **VCS**: Forgejo (git + Gitea-compatible REST API)
|
||||||
- **Notifications**: Matrix (optional)
|
- **Notifications**: Matrix (optional)
|
||||||
|
|
||||||
## Coding conventions
|
## Coding conventions
|
||||||
|
|
|
||||||
64
BOOTSTRAP.md
64
BOOTSTRAP.md
|
|
@ -1,18 +1,36 @@
|
||||||
# Bootstrapping a New Project
|
# 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
|
## Prerequisites
|
||||||
|
|
||||||
Before starting, ensure you have:
|
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 **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
|
- [ ] A **local clone** of the target repo on the same machine as disinto
|
||||||
- [ ] `claude` CLI installed and authenticated (`claude --version`)
|
- [ ] `claude` CLI installed and authenticated (`claude --version`)
|
||||||
- [ ] `tmux` installed (`tmux -V`) — required for persistent dev sessions (issue #80+)
|
- [ ] `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`
|
## 1. Configure `.env`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -22,19 +40,15 @@ cp .env.example .env
|
||||||
Fill in:
|
Fill in:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ── Target project ──────────────────────────────────────────
|
# ── Forge (auto-populated by disinto init) ─────────────────
|
||||||
CODEBERG_REPO=org/project # Codeberg slug
|
FORGE_URL=http://localhost:3000 # local Forgejo instance
|
||||||
PROJECT_REPO_ROOT=/home/you/project # absolute path to local clone
|
FORGE_TOKEN= # dev-bot token (auto-generated)
|
||||||
PRIMARY_BRANCH=main # main or master
|
FORGE_REVIEW_TOKEN= # review-bot token (auto-generated)
|
||||||
|
|
||||||
# ── Auth ────────────────────────────────────────────────────
|
|
||||||
# CODEBERG_TOKEN= # or use ~/.netrc (machine codeberg.org)
|
|
||||||
REVIEW_BOT_TOKEN=tok_xxxxxxxx # the second account's API token
|
|
||||||
|
|
||||||
# ── Woodpecker CI ───────────────────────────────────────────
|
# ── Woodpecker CI ───────────────────────────────────────────
|
||||||
WOODPECKER_TOKEN=tok_xxxxxxxx
|
WOODPECKER_TOKEN=tok_xxxxxxxx
|
||||||
WOODPECKER_SERVER=http://localhost:8000
|
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 Postgres (for direct pipeline queries)
|
||||||
WOODPECKER_DB_PASSWORD=secret
|
WOODPECKER_DB_PASSWORD=secret
|
||||||
|
|
@ -52,23 +66,35 @@ WOODPECKER_DB_NAME=woodpecker
|
||||||
CLAUDE_TIMEOUT=7200 # seconds per Claude invocation
|
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
|
## 2. Configure Project TOML
|
||||||
|
|
||||||
Each project needs a `projects/<name>.toml` file with box-specific settings
|
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.
|
**gitignored** — they are local installation config, not shared code.
|
||||||
|
|
||||||
To create one:
|
To create one:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Automatic — generates TOML, clones repo, sets up cron:
|
# 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:
|
# Manual — copy a template and fill in your values:
|
||||||
cp projects/myproject.toml.example projects/myproject.toml
|
cp projects/myproject.toml.example projects/myproject.toml
|
||||||
vim 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
|
The repo ships `projects/*.toml.example` templates showing the expected
|
||||||
structure. See any `.toml.example` file for the full field reference.
|
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
|
### 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 |
|
| Label | Purpose |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
|
|
@ -150,7 +176,7 @@ Optional but recommended:
|
||||||
|
|
||||||
### Required: Branch protection
|
### 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
|
- **Require pull request reviews**: enabled
|
||||||
- **Required approvals**: 1 (from the review bot account)
|
- **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.
|
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:
|
> **Common pitfall:** Approvals alone are not enough. You must also:
|
||||||
> 1. Add `review_bot` as a **write** collaborator on the repo (Settings → Collaborators)
|
> 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
|
> 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.
|
> 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` |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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>
|
<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
|
## Architecture
|
||||||
|
|
||||||
|
|
@ -49,9 +49,8 @@ all agents ──→ matrix_send() ← status updates, escalations, merge no
|
||||||
**Required:**
|
**Required:**
|
||||||
|
|
||||||
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) — `claude` in PATH, authenticated
|
- [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
|
- [Docker](https://docker.com/) — for provisioning a local Forgejo instance (or a running Forgejo/Gitea instance)
|
||||||
- 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 forge; disinto monitors pipelines, retries failures, and queries the Woodpecker Postgres DB directly
|
||||||
- [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
|
|
||||||
- PostgreSQL client (`psql`) — for Woodpecker DB queries (pipeline status, build counts)
|
- PostgreSQL client (`psql`) — for Woodpecker DB queries (pipeline status, build counts)
|
||||||
- `jq`, `curl`, `git`
|
- `jq`, `curl`, `git`
|
||||||
|
|
||||||
|
|
@ -65,24 +64,20 @@ all agents ──→ matrix_send() ← status updates, escalations, merge no
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone
|
# 1. Clone
|
||||||
git clone ssh://git@codeberg.org/johba/disinto.git
|
git clone https://github.com/johba/disinto.git
|
||||||
cd disinto
|
cd disinto
|
||||||
|
|
||||||
# 2. Configure
|
# 2. Bootstrap a project (provisions local Forgejo, creates tokens, clones repo)
|
||||||
cp .env.example .env
|
disinto init https://github.com/yourorg/yourproject
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `.env` with your values:
|
Or configure manually — edit `.env` with your values:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Target repo
|
# Forge (auto-populated by disinto init)
|
||||||
CODEBERG_REPO=yourorg/yourproject # Codeberg org/repo slug
|
FORGE_URL=http://localhost:3000 # local Forgejo instance
|
||||||
CODEBERG_API=https://codeberg.org/api/v1/repos/yourorg/yourproject
|
FORGE_TOKEN=... # dev-bot token
|
||||||
PROJECT_REPO_ROOT=/path/to/your/project # local clone of the target repo
|
FORGE_REVIEW_TOKEN=... # review-bot token
|
||||||
|
|
||||||
# Auth tokens
|
|
||||||
CODEBERG_TOKEN=... # main account — or put it in ~/.netrc
|
|
||||||
REVIEW_BOT_TOKEN=... # separate Codeberg account for code reviews
|
|
||||||
|
|
||||||
# Woodpecker CI
|
# Woodpecker CI
|
||||||
WOODPECKER_SERVER=http://localhost:8000
|
WOODPECKER_SERVER=http://localhost:8000
|
||||||
|
|
@ -148,7 +143,7 @@ disinto/
|
||||||
├── memory.md
|
├── memory.md
|
||||||
├── disk.md
|
├── disk.md
|
||||||
├── ci.md
|
├── ci.md
|
||||||
├── codeberg.md
|
├── forge.md
|
||||||
├── dev-agent.md
|
├── dev-agent.md
|
||||||
├── review-agent.md
|
├── review-agent.md
|
||||||
└── git.md
|
└── git.md
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
| Service | Purpose | Limits |
|
| 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 |
|
| Anthropic | Claude API | $X/mo budget, rate limit: 100k TPM |
|
||||||
| Cloudflare | DNS + CDN | free tier |
|
| 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
|
## Growth goals
|
||||||
|
|
||||||
- **Attract developers** — the project should be easy to understand, easy to fork, easy to contribute to.
|
- **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.
|
- **Contributors** — lower the barrier to entry. Good docs, clear architecture, working examples.
|
||||||
- **Reference deployments** — showcase real projects built and operated by Disinto.
|
- **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.
|
- **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`.
|
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**:
|
**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
|
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Matrix notifications + human input
|
||||||
- `ACTION_IDLE_TIMEOUT` — Max seconds before killing idle session (default 14400 = 4h)
|
- `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
|
- `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) ---
|
# --- Phase handler globals (agent-specific; defaults in phase-handler.sh) ---
|
||||||
# shellcheck disable=SC2034 # used by phase-handler.sh
|
# shellcheck disable=SC2034 # used by phase-handler.sh
|
||||||
API="${CODEBERG_API}"
|
API="${FORGE_API}"
|
||||||
BRANCH="action/issue-${ISSUE}"
|
BRANCH="action/issue-${ISSUE}"
|
||||||
# shellcheck disable=SC2034 # used by phase-handler.sh
|
# shellcheck disable=SC2034 # used by phase-handler.sh
|
||||||
WORKTREE="/tmp/action-${ISSUE}-$(date +%s)"
|
WORKTREE="/tmp/action-${ISSUE}-$(date +%s)"
|
||||||
|
|
@ -133,8 +133,8 @@ fi
|
||||||
|
|
||||||
# --- Fetch issue ---
|
# --- Fetch issue ---
|
||||||
log "fetching issue #${ISSUE}"
|
log "fetching issue #${ISSUE}"
|
||||||
ISSUE_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
ISSUE_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/issues/${ISSUE}") || true
|
"${FORGE_API}/issues/${ISSUE}") || true
|
||||||
|
|
||||||
if [ -z "$ISSUE_JSON" ] || ! printf '%s' "$ISSUE_JSON" | jq -e '.id' >/dev/null 2>&1; then
|
if [ -z "$ISSUE_JSON" ] || ! printf '%s' "$ISSUE_JSON" | jq -e '.id' >/dev/null 2>&1; then
|
||||||
log "ERROR: failed to fetch issue #${ISSUE}"
|
log "ERROR: failed to fetch issue #${ISSUE}"
|
||||||
|
|
@ -161,18 +161,18 @@ if [ -n "$YAML_MODEL" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Resolve bot username(s) for comment filtering ---
|
# --- 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}" \
|
||||||
"${CODEBERG_API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true)
|
"${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}"
|
_bot_logins="${_bot_login}"
|
||||||
if [ -n "${CODEBERG_BOT_USERNAMES:-}" ]; then
|
if [ -n "${FORGE_BOT_USERNAMES:-}" ]; then
|
||||||
_bot_logins="${_bot_logins:+${_bot_logins},}${CODEBERG_BOT_USERNAMES}"
|
_bot_logins="${_bot_logins:+${_bot_logins},}${FORGE_BOT_USERNAMES}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Fetch existing comments (resume context, excluding bot comments) ---
|
# --- Fetch existing comments (resume context, excluding bot comments) ---
|
||||||
COMMENTS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
COMMENTS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/issues/${ISSUE}/comments?limit=50") || true
|
"${FORGE_API}/issues/${ISSUE}/comments?limit=50") || true
|
||||||
|
|
||||||
PRIOR_COMMENTS=""
|
PRIOR_COMMENTS=""
|
||||||
if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSON" != "[]" ]; then
|
if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSON" != "[]" ]; then
|
||||||
|
|
@ -184,7 +184,7 @@ if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSO
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Create Matrix thread for this issue ---
|
# --- Create Matrix thread for this issue ---
|
||||||
ISSUE_URL="${CODEBERG_WEB}/issues/${ISSUE}"
|
ISSUE_URL="${FORGE_WEB}/issues/${ISSUE}"
|
||||||
_thread_id=$(matrix_send_ctx "action" \
|
_thread_id=$(matrix_send_ctx "action" \
|
||||||
"⚡ Action #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \
|
"⚡ Action #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \
|
||||||
"⚡ <a href='${ISSUE_URL}'>Action #${ISSUE}</a>: ${ISSUE_TITLE}") || true
|
"⚡ <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:
|
3. Post progress as comments on issue #${ISSUE} after significant steps:
|
||||||
curl -sf -X POST \\
|
curl -sf -X POST \\
|
||||||
-H \"Authorization: token \${CODEBERG_TOKEN}\" \\
|
-H \"Authorization: token \${FORGE_TOKEN}\" \\
|
||||||
-H 'Content-Type: application/json' \\
|
-H 'Content-Type: application/json' \\
|
||||||
\"${CODEBERG_API}/issues/${ISSUE}/comments\" \\
|
\"${FORGE_API}/issues/${ISSUE}/comments\" \\
|
||||||
-d \"{\\\"body\\\": \\\"your comment here\\\"}\"
|
-d \"{\\\"body\\\": \\\"your comment here\\\"}\"
|
||||||
|
|
||||||
4. If a step requires human input or approval, send a Matrix message explaining
|
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.
|
files you need to persistent paths before signaling done.
|
||||||
- Close the issue:
|
- Close the issue:
|
||||||
curl -sf -X PATCH \\
|
curl -sf -X PATCH \\
|
||||||
-H \"Authorization: token \${CODEBERG_TOKEN}\" \\
|
-H \"Authorization: token \${FORGE_TOKEN}\" \\
|
||||||
-H 'Content-Type: application/json' \\
|
-H 'Content-Type: application/json' \\
|
||||||
\"${CODEBERG_API}/issues/${ISSUE}\" \\
|
\"${FORGE_API}/issues/${ISSUE}\" \\
|
||||||
-d '{\"state\": \"closed\"}'
|
-d '{\"state\": \"closed\"}'
|
||||||
- Signal completion: echo \"PHASE:done\" > \"${PHASE_FILE}\"
|
- Signal completion: echo \"PHASE:done\" > \"${PHASE_FILE}\"
|
||||||
|
|
||||||
5. Environment variables available in your bash sessions:
|
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)
|
(all sourced from ${FACTORY_ROOT}/.env)
|
||||||
|
|
||||||
### CRITICAL: Never embed secrets in issue bodies, comments, or PR descriptions
|
### 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.
|
- 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.
|
- 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,
|
- Before posting any comment, verify it contains no credentials, hex keys > 32 chars,
|
||||||
or URLs with embedded API keys.
|
or URLs with embedded API keys.
|
||||||
|
|
@ -330,9 +330,9 @@ _lifetime_watchdog() {
|
||||||
# Post summary comment on issue
|
# Post summary comment on issue
|
||||||
local body="Action session killed: wall-clock lifetime cap (${hours}h) reached."
|
local body="Action session killed: wall-clock lifetime cap (${hours}h) reached."
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
"${CODEBERG_API}/issues/${ISSUE}/comments" \
|
"${FORGE_API}/issues/${ISSUE}/comments" \
|
||||||
-d "{\"body\": \"${body}\"}" >/dev/null 2>&1 || true
|
-d "{\"body\": \"${body}\"}" >/dev/null 2>&1 || true
|
||||||
printf 'PHASE:failed\nReason: max_lifetime (%sh) reached\n' "$hours" > "$PHASE_FILE"
|
printf 'PHASE:failed\nReason: max_lifetime (%sh) reached\n' "$hours" > "$PHASE_FILE"
|
||||||
# Touch phase-changed marker so monitor_phase_loop picks up immediately
|
# Touch phase-changed marker so monitor_phase_loop picks up immediately
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ fi
|
||||||
|
|
||||||
# --- Find open 'action' issues ---
|
# --- Find open 'action' issues ---
|
||||||
log "scanning for open action issues"
|
log "scanning for open action issues"
|
||||||
ACTION_ISSUES=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
ACTION_ISSUES=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/issues?state=open&labels=action&limit=50&type=issues") || true
|
"${FORGE_API}/issues?state=open&labels=action&limit=50&type=issues") || true
|
||||||
|
|
||||||
if [ -z "$ACTION_ISSUES" ] || [ "$ACTION_ISSUES" = "null" ]; then
|
if [ -z "$ACTION_ISSUES" ] || [ "$ACTION_ISSUES" = "null" ]; then
|
||||||
log "no action issues found"
|
log "no action issues found"
|
||||||
|
|
|
||||||
433
bin/disinto
433
bin/disinto
|
|
@ -7,8 +7,8 @@
|
||||||
# disinto status Show factory status
|
# disinto status Show factory status
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# disinto init https://codeberg.org/user/repo
|
# disinto init https://github.com/user/repo
|
||||||
# disinto init https://codeberg.org/user/repo --branch main --ci-id 3
|
# disinto init user/repo --branch main --ci-id 3
|
||||||
# disinto status
|
# disinto status
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -30,45 +30,49 @@ Init options:
|
||||||
--branch <name> Primary branch (default: auto-detect)
|
--branch <name> Primary branch (default: auto-detect)
|
||||||
--repo-root <path> Local clone path (default: ~/name)
|
--repo-root <path> Local clone path (default: ~/name)
|
||||||
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
|
--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
|
--yes Skip confirmation prompts
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract org/repo slug from various URL formats.
|
# Extract org/repo slug from various URL formats.
|
||||||
# Accepts: https://codeberg.org/user/repo, codeberg.org/user/repo,
|
# Accepts: https://github.com/user/repo, https://codeberg.org/user/repo,
|
||||||
# user/repo, https://codeberg.org/user/repo.git
|
# http://localhost:3000/user/repo, user/repo, *.git
|
||||||
parse_repo_slug() {
|
parse_repo_slug() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
url="${url#https://}"
|
url="${url#https://}"
|
||||||
url="${url#http://}"
|
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%.git}"
|
||||||
url="${url%/}"
|
url="${url%/}"
|
||||||
if [[ ! "$url" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf '%s' "$url"
|
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() {
|
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() {
|
write_netrc() {
|
||||||
local login="$1" token="$2"
|
local host="$1" login="$2" token="$3"
|
||||||
local netrc="${HOME}/.netrc"
|
local netrc="${HOME}/.netrc"
|
||||||
|
|
||||||
# Remove existing codeberg.org entry if present
|
# Remove existing entry for this host if present
|
||||||
if [ -f "$netrc" ]; then
|
if [ -f "$netrc" ]; then
|
||||||
local tmp
|
local tmp
|
||||||
tmp=$(mktemp)
|
tmp=$(mktemp)
|
||||||
awk '
|
awk -v h="$host" '
|
||||||
/^machine codeberg\.org/ { skip=1; next }
|
$0 ~ "^machine " h { skip=1; next }
|
||||||
/^machine / { skip=0 }
|
/^machine / { skip=0 }
|
||||||
!skip
|
!skip
|
||||||
' "$netrc" > "$tmp"
|
' "$netrc" > "$tmp"
|
||||||
|
|
@ -76,93 +80,252 @@ write_netrc() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Append new entry
|
# 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"
|
chmod 600 "$netrc"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Interactively set up Codeberg auth if missing.
|
FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
|
||||||
# Args: [token_from_flag]
|
|
||||||
setup_codeberg_auth() {
|
|
||||||
local token_flag="${1:-}"
|
|
||||||
local repo_slug="${2:-}"
|
|
||||||
|
|
||||||
# --token flag takes priority: verify and save
|
# Provision or connect to a local Forgejo instance.
|
||||||
if [ -n "$token_flag" ]; then
|
# Creates admin + bot users, generates API tokens, stores in .env.
|
||||||
local verify_url="https://codeberg.org/api/v1/repos/${repo_slug}"
|
setup_forge() {
|
||||||
if ! curl -sf --max-time 10 \
|
local forge_url="$1"
|
||||||
-H "Authorization: token ${token_flag}" \
|
local repo_slug="$2"
|
||||||
"$verify_url" >/dev/null 2>&1; then
|
|
||||||
echo "Error: provided token failed verification" >&2
|
echo ""
|
||||||
|
echo "── Forge setup ────────────────────────────────────────"
|
||||||
|
|
||||||
|
# Check if Forgejo is already running
|
||||||
|
if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then
|
||||||
|
echo "Forgejo: ${forge_url} (already running)"
|
||||||
|
else
|
||||||
|
echo "Forgejo not reachable at ${forge_url}"
|
||||||
|
echo "Starting Forgejo via Docker..."
|
||||||
|
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo "Error: docker not found — needed to provision Forgejo" >&2
|
||||||
|
echo " Install Docker or start Forgejo manually at ${forge_url}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
write_netrc "token" "$token_flag"
|
|
||||||
echo "Saving to ~/.netrc... done."
|
|
||||||
echo "Verified: token accepted ✓"
|
|
||||||
export CODEBERG_TOKEN="$token_flag"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Existing auth — skip
|
# Create data directory
|
||||||
if [ -n "${CODEBERG_TOKEN:-}" ]; then
|
mkdir -p "${FORGEJO_DATA_DIR}"
|
||||||
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
|
|
||||||
|
|
||||||
# Non-interactive — fail with guidance
|
# Extract port from forge_url
|
||||||
if [ ! -t 0 ]; then
|
local forge_port
|
||||||
echo "Error: no Codeberg auth found" >&2
|
forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|')
|
||||||
echo " Set CODEBERG_TOKEN, configure ~/.netrc, or use --token <token>" >&2
|
forge_port="${forge_port:-3000}"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Interactive guided flow
|
# Start Forgejo container
|
||||||
echo ""
|
if docker ps -a --format '{{.Names}}' | grep -q '^disinto-forgejo$'; then
|
||||||
echo "No Codeberg authentication found."
|
docker start disinto-forgejo >/dev/null 2>&1 || true
|
||||||
echo ""
|
else
|
||||||
echo "1. Open https://codeberg.org/user/settings/applications"
|
docker run -d \
|
||||||
echo "2. Create a token with these scopes:"
|
--name disinto-forgejo \
|
||||||
echo " - write:issue (create issues, add labels, post comments, close issues)"
|
--restart unless-stopped \
|
||||||
echo " - write:repository (push branches, create PRs, merge PRs)"
|
-p "${forge_port}:3000" \
|
||||||
echo "3. Paste the token below."
|
-p 2222:22 \
|
||||||
echo ""
|
-v "${FORGEJO_DATA_DIR}:/data" \
|
||||||
|
-e "FORGEJO__database__DB_TYPE=sqlite3" \
|
||||||
while true; do
|
-e "FORGEJO__server__ROOT_URL=${forge_url}/" \
|
||||||
read -rsp "Codeberg token: " token_input
|
-e "FORGEJO__server__HTTP_PORT=3000" \
|
||||||
echo ""
|
-e "FORGEJO__service__DISABLE_REGISTRATION=true" \
|
||||||
|
forgejo/forgejo:latest
|
||||||
if [ -z "$token_input" ]; then
|
|
||||||
echo "Token cannot be empty. Try again." >&2
|
|
||||||
continue
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local verify_url="https://codeberg.org/api/v1/repos/${repo_slug}"
|
# Wait for Forgejo to become healthy
|
||||||
if ! curl -sf --max-time 10 \
|
echo -n "Waiting for Forgejo to start"
|
||||||
-H "Authorization: token ${token_input}" \
|
local retries=0
|
||||||
"$verify_url" >/dev/null 2>&1; then
|
while ! curl -sf --max-time 3 "${forge_url}/api/v1/version" >/dev/null 2>&1; do
|
||||||
echo "Token verification failed. Check your token and try again." >&2
|
retries=$((retries + 1))
|
||||||
read -rp "Retry? [Y/n] " retry
|
if [ "$retries" -gt 60 ]; then
|
||||||
if [[ "$retry" =~ ^[Nn] ]]; then
|
echo ""
|
||||||
echo "Aborted." >&2
|
echo "Error: Forgejo did not become ready within 60s" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
continue
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo " ready"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create admin user if it doesn't exist
|
||||||
|
local admin_user="disinto-admin"
|
||||||
|
local admin_pass
|
||||||
|
admin_pass="admin-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
||||||
|
|
||||||
|
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating admin user: ${admin_user}"
|
||||||
|
docker exec disinto-forgejo forgejo admin user create \
|
||||||
|
--admin \
|
||||||
|
--username "${admin_user}" \
|
||||||
|
--password "${admin_pass}" \
|
||||||
|
--email "admin@disinto.local" \
|
||||||
|
--must-change-password=false 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get or create admin token
|
||||||
|
local admin_token
|
||||||
|
admin_token=$(curl -sf -X POST \
|
||||||
|
-u "${admin_user}:${admin_pass}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/users/${admin_user}/tokens" \
|
||||||
|
-d '{"name":"disinto-admin-token","scopes":["all"]}' 2>/dev/null \
|
||||||
|
| jq -r '.sha1 // empty') || admin_token=""
|
||||||
|
|
||||||
|
if [ -z "$admin_token" ]; then
|
||||||
|
# Token might already exist — try listing
|
||||||
|
admin_token=$(curl -sf \
|
||||||
|
-u "${admin_user}:${admin_pass}" \
|
||||||
|
"${forge_url}/api/v1/users/${admin_user}/tokens" 2>/dev/null \
|
||||||
|
| jq -r '.[0].sha1 // empty') || admin_token=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create bot users and tokens
|
||||||
|
local dev_token="" review_token=""
|
||||||
|
for bot_user in dev-bot review-bot; do
|
||||||
|
local bot_pass
|
||||||
|
bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
||||||
|
|
||||||
|
if ! curl -sf --max-time 5 \
|
||||||
|
-H "Authorization: token ${admin_token}" \
|
||||||
|
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating bot user: ${bot_user}"
|
||||||
|
docker exec disinto-forgejo forgejo admin user create \
|
||||||
|
--username "${bot_user}" \
|
||||||
|
--password "${bot_pass}" \
|
||||||
|
--email "${bot_user}@disinto.local" \
|
||||||
|
--must-change-password=false 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
write_netrc "token" "$token_input"
|
# Generate token via API (using admin credentials for the bot)
|
||||||
echo "Saving to ~/.netrc... done."
|
local token
|
||||||
echo "Verified: token accepted ✓"
|
token=$(curl -sf -X POST \
|
||||||
export CODEBERG_TOKEN="$token_input"
|
-H "Authorization: token ${admin_token}" \
|
||||||
return
|
-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
|
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 — verify all factory requirements before proceeding.
|
||||||
preflight_check() {
|
preflight_check() {
|
||||||
local repo_slug="${1:-}"
|
local repo_slug="${1:-}"
|
||||||
|
local forge_url="${2:-${FORGE_URL:-http://localhost:3000}}"
|
||||||
local errors=0
|
local errors=0
|
||||||
|
|
||||||
# ── Required commands ──
|
# ── Required commands ──
|
||||||
|
|
@ -207,37 +370,20 @@ preflight_check() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Codeberg auth (setup_codeberg_auth handles interactive setup;
|
# ── Forge API check (verify the forge is reachable and token works) ──
|
||||||
# this verifies the API actually works) ──
|
if [ -n "${FORGE_TOKEN:-}" ] && command -v curl &>/dev/null; then
|
||||||
if [ -n "${CODEBERG_TOKEN:-}" ] && command -v curl &>/dev/null; then
|
if ! curl -sf --max-time 10 \
|
||||||
local curl_args=(-sf --max-time 10)
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
if [ -n "${CODEBERG_TOKEN:-}" ]; then
|
"${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
|
||||||
curl_args+=(-H "Authorization: token ${CODEBERG_TOKEN}")
|
echo "Error: Forge API auth failed at ${forge_url}" >&2
|
||||||
else
|
echo " Verify your FORGE_TOKEN and that Forgejo is running" >&2
|
||||||
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
|
|
||||||
errors=$((errors + 1))
|
errors=$((errors + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Optional tools (warn only) ──
|
# ── Optional tools (warn only) ──
|
||||||
if ! command -v docker &>/dev/null; then
|
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
|
fi
|
||||||
|
|
||||||
if [ "$errors" -gt 0 ]; then
|
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 the repo if the target directory doesn't exist; validate if it does.
|
||||||
clone_or_validate() {
|
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
|
if [ -d "${target}/.git" ]; then
|
||||||
echo "Repo: ${target} (existing clone)"
|
echo "Repo: ${target} (existing clone)"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local url
|
local url
|
||||||
url=$(clone_url_from_slug "$slug")
|
url=$(clone_url_from_slug "$slug" "$forge_url")
|
||||||
echo "Cloning: ${url} -> ${target}"
|
echo "Cloning: ${url} -> ${target}"
|
||||||
git clone "$url" "$target"
|
git clone "$url" "$target"
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +424,7 @@ detect_branch() {
|
||||||
|
|
||||||
# Generate projects/<name>.toml config file.
|
# Generate projects/<name>.toml config file.
|
||||||
generate_toml() {
|
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
|
cat > "$path" <<EOF
|
||||||
# projects/${name}.toml — Project config for ${repo}
|
# projects/${name}.toml — Project config for ${repo}
|
||||||
#
|
#
|
||||||
|
|
@ -286,6 +432,7 @@ generate_toml() {
|
||||||
|
|
||||||
name = "${name}"
|
name = "${name}"
|
||||||
repo = "${repo}"
|
repo = "${repo}"
|
||||||
|
forge_url = "${forge_url}"
|
||||||
repo_root = "${root}"
|
repo_root = "${root}"
|
||||||
primary_branch = "${branch}"
|
primary_branch = "${branch}"
|
||||||
|
|
||||||
|
|
@ -303,10 +450,11 @@ check_pipeline_stall = false
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create standard labels on the Codeberg repo.
|
# Create standard labels on the forge repo.
|
||||||
create_labels() {
|
create_labels() {
|
||||||
local repo="$1"
|
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=(
|
local -A labels=(
|
||||||
["backlog"]="#0075ca"
|
["backlog"]="#0075ca"
|
||||||
|
|
@ -323,7 +471,7 @@ create_labels() {
|
||||||
for name in backlog in-progress blocked tech-debt underspecified vision action; do
|
for name in backlog in-progress blocked tech-debt underspecified vision action; do
|
||||||
color="${labels[$name]}"
|
color="${labels[$name]}"
|
||||||
if curl -sf -X POST \
|
if curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${api}/labels" \
|
"${api}/labels" \
|
||||||
-d "{\"name\":\"${name}\",\"color\":\"${color}\"}" >/dev/null 2>&1; then
|
-d "{\"name\":\"${name}\",\"color\":\"${color}\"}" >/dev/null 2>&1; then
|
||||||
|
|
@ -415,27 +563,31 @@ disinto_init() {
|
||||||
shift
|
shift
|
||||||
|
|
||||||
# Parse flags
|
# 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
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--branch) branch="$2"; shift 2 ;;
|
--branch) branch="$2"; shift 2 ;;
|
||||||
--repo-root) repo_root="$2"; shift 2 ;;
|
--repo-root) repo_root="$2"; shift 2 ;;
|
||||||
--ci-id) ci_id="$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 ;;
|
--yes) auto_yes=true; shift ;;
|
||||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Extract org/repo slug
|
# Extract org/repo slug
|
||||||
local codeberg_repo
|
local forge_repo
|
||||||
codeberg_repo=$(parse_repo_slug "$repo_url")
|
forge_repo=$(parse_repo_slug "$repo_url")
|
||||||
local project_name="${codeberg_repo##*/}"
|
local project_name="${forge_repo##*/}"
|
||||||
local toml_path="${FACTORY_ROOT}/projects/${project_name}.toml"
|
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 "=== disinto init ==="
|
||||||
echo "Project: ${codeberg_repo}"
|
echo "Project: ${forge_repo}"
|
||||||
echo "Name: ${project_name}"
|
echo "Name: ${project_name}"
|
||||||
|
echo "Forge: ${forge_url}"
|
||||||
|
|
||||||
# Check for existing config
|
# Check for existing config
|
||||||
local toml_exists=false
|
local toml_exists=false
|
||||||
|
|
@ -492,17 +644,27 @@ p.write_text(text)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up Codeberg auth (interactive if needed, before preflight)
|
# Set up local Forgejo instance (provision if needed, create users/tokens/repo)
|
||||||
setup_codeberg_auth "$token_flag" "$codeberg_repo"
|
setup_forge "$forge_url" "$forge_repo"
|
||||||
|
|
||||||
# Preflight: verify factory requirements
|
# Preflight: verify factory requirements
|
||||||
preflight_check "$codeberg_repo"
|
preflight_check "$forge_repo" "$forge_url"
|
||||||
|
|
||||||
# Determine repo root (for new projects)
|
# Determine repo root (for new projects)
|
||||||
repo_root="${repo_root:-/home/${USER}/${project_name}}"
|
repo_root="${repo_root:-/home/${USER}/${project_name}}"
|
||||||
|
|
||||||
# Clone or validate
|
# Clone or validate (try origin first for initial clone from upstream)
|
||||||
clone_or_validate "$codeberg_repo" "$repo_root"
|
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
|
# Detect primary branch
|
||||||
if [ -z "$branch" ]; then
|
if [ -z "$branch" ]; then
|
||||||
|
|
@ -518,12 +680,12 @@ p.write_text(text)
|
||||||
ci_id="${user_ci_id:-0}"
|
ci_id="${user_ci_id:-0}"
|
||||||
fi
|
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}"
|
echo "Created: ${toml_path}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create labels on remote
|
# Create labels on remote
|
||||||
create_labels "$codeberg_repo"
|
create_labels "$forge_repo" "$forge_url"
|
||||||
|
|
||||||
# Generate VISION.md template
|
# Generate VISION.md template
|
||||||
generate_vision "$repo_root" "$project_name"
|
generate_vision "$repo_root" "$project_name"
|
||||||
|
|
@ -535,6 +697,7 @@ p.write_text(text)
|
||||||
echo "Done. Project ${project_name} is ready."
|
echo "Done. Project ${project_name} is ready."
|
||||||
echo " Config: ${toml_path}"
|
echo " Config: ${toml_path}"
|
||||||
echo " Clone: ${repo_root}"
|
echo " Clone: ${repo_root}"
|
||||||
|
echo " Forge: ${forge_url}/${forge_repo}"
|
||||||
echo " Run 'disinto status' to verify."
|
echo " Run 'disinto status' to verify."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -548,8 +711,8 @@ disinto_status() {
|
||||||
[ -f "$toml" ] || continue
|
[ -f "$toml" ] || continue
|
||||||
found=true
|
found=true
|
||||||
|
|
||||||
# Parse name and repo from TOML
|
# Parse name, repo, forge_url from TOML
|
||||||
local pname prepo
|
local pname prepo pforge_url
|
||||||
pname=$(python3 -c "
|
pname=$(python3 -c "
|
||||||
import sys, tomllib
|
import sys, tomllib
|
||||||
with open(sys.argv[1], 'rb') as f:
|
with open(sys.argv[1], 'rb') as f:
|
||||||
|
|
@ -560,6 +723,12 @@ import sys, tomllib
|
||||||
with open(sys.argv[1], 'rb') as f:
|
with open(sys.argv[1], 'rb') as f:
|
||||||
print(tomllib.load(f)['repo'])
|
print(tomllib.load(f)['repo'])
|
||||||
" "$toml" 2>/dev/null) || continue
|
" "$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}) =="
|
echo "== ${pname} (${prepo}) =="
|
||||||
|
|
||||||
|
|
@ -578,24 +747,24 @@ with open(sys.argv[1], 'rb') as f:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Backlog depth via API
|
# Backlog depth via API
|
||||||
if [ -n "${CODEBERG_TOKEN:-}" ]; then
|
if [ -n "${FORGE_TOKEN:-}" ]; then
|
||||||
local api="https://codeberg.org/api/v1/repos/${prepo}"
|
local api="${pforge_url}/api/v1/repos/${prepo}"
|
||||||
local backlog_count pr_count
|
local backlog_count pr_count
|
||||||
|
|
||||||
backlog_count=$(curl -sf -I \
|
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 \
|
"${api}/issues?state=open&labels=backlog&limit=1" 2>/dev/null \
|
||||||
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || backlog_count="?"
|
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || backlog_count="?"
|
||||||
echo " Backlog: ${backlog_count:-0} issues"
|
echo " Backlog: ${backlog_count:-0} issues"
|
||||||
|
|
||||||
pr_count=$(curl -sf -I \
|
pr_count=$(curl -sf -I \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${api}/pulls?state=open&limit=1" 2>/dev/null \
|
"${api}/pulls?state=open&limit=1" 2>/dev/null \
|
||||||
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || pr_count="?"
|
| grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || pr_count="?"
|
||||||
echo " Open PRs: ${pr_count:-0}"
|
echo " Open PRs: ${pr_count:-0}"
|
||||||
else
|
else
|
||||||
echo " Backlog: (no CODEBERG_TOKEN)"
|
echo " Backlog: (no FORGE_TOKEN)"
|
||||||
echo " Open PRs: (no CODEBERG_TOKEN)"
|
echo " Open PRs: (no FORGE_TOKEN)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
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
|
- `dev/phase-test.sh` — Integration test for the phase protocol
|
||||||
|
|
||||||
**Environment variables consumed** (via `lib/env.sh` + project TOML):
|
**Environment variables consumed** (via `lib/env.sh` + project TOML):
|
||||||
- `CODEBERG_TOKEN` — Dev-agent token (push, PR creation, merge) — use the dedicated bot account
|
- `FORGE_TOKEN` — Dev-agent token (push, PR creation, merge) — use the dedicated bot account
|
||||||
- `CODEBERG_REPO`, `CODEBERG_API` — Target repository
|
- `FORGE_REPO`, `FORGE_API` — Target repository
|
||||||
- `PROJECT_NAME`, `PROJECT_REPO_ROOT` — Local checkout path
|
- `PROJECT_NAME`, `PROJECT_REPO_ROOT` — Local checkout path
|
||||||
- `PRIMARY_BRANCH` — Branch to merge into (e.g. `main`, `master`)
|
- `PRIMARY_BRANCH` — Branch to merge into (e.g. `main`, `master`)
|
||||||
- `WOODPECKER_REPO_ID` — CI pipeline lookups
|
- `WOODPECKER_REPO_ID` — CI pipeline lookups
|
||||||
|
|
|
||||||
|
|
@ -34,21 +34,21 @@ git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true
|
||||||
# --- Config ---
|
# --- Config ---
|
||||||
ISSUE="${1:?Usage: dev-agent.sh <issue-number>}"
|
ISSUE="${1:?Usage: dev-agent.sh <issue-number>}"
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
REPO="${CODEBERG_REPO}"
|
REPO="${FORGE_REPO}"
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
REPO_ROOT="${PROJECT_REPO_ROOT}"
|
REPO_ROOT="${PROJECT_REPO_ROOT}"
|
||||||
|
|
||||||
API="${CODEBERG_API}"
|
API="${FORGE_API}"
|
||||||
LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock"
|
LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock"
|
||||||
STATUSFILE="/tmp/dev-agent-status-${PROJECT_NAME:-default}"
|
STATUSFILE="/tmp/dev-agent-status-${PROJECT_NAME:-default}"
|
||||||
|
|
||||||
# Gitea labels API requires []int64 — look up the "backlog" label ID once
|
# 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)
|
| jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true)
|
||||||
BACKLOG_LABEL_ID="${BACKLOG_LABEL_ID:-1300815}"
|
BACKLOG_LABEL_ID="${BACKLOG_LABEL_ID:-1300815}"
|
||||||
|
|
||||||
# Same for "in-progress" label
|
# 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)
|
| jq -r '.[] | select(.name == "in-progress") | .id' 2>/dev/null || true)
|
||||||
IN_PROGRESS_LABEL_ID="${IN_PROGRESS_LABEL_ID:-1300818}"
|
IN_PROGRESS_LABEL_ID="${IN_PROGRESS_LABEL_ID:-1300818}"
|
||||||
|
|
||||||
|
|
@ -128,14 +128,14 @@ cleanup_worktree() {
|
||||||
|
|
||||||
cleanup_labels() {
|
cleanup_labels() {
|
||||||
curl -sf -X DELETE \
|
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
|
"${API}/issues/${ISSUE}/labels/${IN_PROGRESS_LABEL_ID}" >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
restore_to_backlog() {
|
restore_to_backlog() {
|
||||||
cleanup_labels
|
cleanup_labels
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/labels" \
|
"${API}/issues/${ISSUE}/labels" \
|
||||||
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||||
|
|
@ -151,10 +151,10 @@ cleanup() {
|
||||||
if [ "$CLAIMED" = true ] && [ -z "${PR_NUMBER:-}" ]; then
|
if [ "$CLAIMED" = true ] && [ -z "${PR_NUMBER:-}" ]; then
|
||||||
log "cleanup: unclaiming issue (no PR created)"
|
log "cleanup: unclaiming issue (no PR created)"
|
||||||
curl -sf -X DELETE \
|
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
|
"${API}/issues/${ISSUE}/labels/${IN_PROGRESS_LABEL_ID}" >/dev/null 2>&1 || true
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/labels" \
|
"${API}/issues/${ISSUE}/labels" \
|
||||||
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||||
|
|
@ -198,7 +198,7 @@ echo $$ > "$LOCKFILE"
|
||||||
# FETCH ISSUE
|
# FETCH ISSUE
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
status "fetching 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
|
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)"
|
log "ERROR: failed to fetch issue #${ISSUE} (API down or invalid response)"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -208,17 +208,17 @@ ISSUE_BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""')
|
||||||
ISSUE_BODY_ORIGINAL="$ISSUE_BODY"
|
ISSUE_BODY_ORIGINAL="$ISSUE_BODY"
|
||||||
|
|
||||||
# --- Resolve bot username(s) for comment filtering ---
|
# --- 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)
|
"${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}"
|
_bot_logins="${_bot_login}"
|
||||||
if [ -n "${CODEBERG_BOT_USERNAMES:-}" ]; then
|
if [ -n "${FORGE_BOT_USERNAMES:-}" ]; then
|
||||||
_bot_logins="${_bot_logins:+${_bot_logins},}${CODEBERG_BOT_USERNAMES}"
|
_bot_logins="${_bot_logins:+${_bot_logins},}${FORGE_BOT_USERNAMES}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Append human comments to issue body (filter out bot accounts)
|
# 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" | \
|
"${API}/issues/${ISSUE}/comments" | \
|
||||||
jq -r --arg bots "$_bot_logins" \
|
jq -r --arg bots "$_bot_logins" \
|
||||||
'($bots | split(",") | map(select(. != ""))) as $bl |
|
'($bots | split(",") | map(select(. != ""))) as $bl |
|
||||||
|
|
@ -264,7 +264,7 @@ if [ -n "$DEP_NUMBERS" ]; then
|
||||||
while IFS= read -r dep_num; do
|
while IFS= read -r dep_num; do
|
||||||
[ -z "$dep_num" ] && continue
|
[ -z "$dep_num" ] && continue
|
||||||
# Check if dependency issue is closed (= satisfied)
|
# 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"')
|
"${API}/issues/${dep_num}" | jq -r '.state // "unknown"')
|
||||||
|
|
||||||
if [ "$DEP_STATE" != "closed" ]; then
|
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
|
# Find a suggestion: look for the first blocker that itself has no unmet deps
|
||||||
SUGGESTION=""
|
SUGGESTION=""
|
||||||
for blocker in "${BLOCKED_BY[@]}"; do
|
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 // ""')
|
"${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')
|
"${API}/issues/${blocker}" | jq -r '.state')
|
||||||
|
|
||||||
if [ "$BLOCKER_STATE" != "open" ]; then
|
if [ "$BLOCKER_STATE" != "open" ]; then
|
||||||
|
|
@ -302,7 +302,7 @@ if [ "${#BLOCKED_BY[@]}" -gt 0 ]; then
|
||||||
if [ -n "$BLOCKER_DEPS" ]; then
|
if [ -n "$BLOCKER_DEPS" ]; then
|
||||||
while IFS= read -r bd; do
|
while IFS= read -r bd; do
|
||||||
[ -z "$bd" ] && continue
|
[ -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"')
|
"${API}/issues/${bd}" | jq -r '.state // "unknown"')
|
||||||
if [ "$BD_STATE" != "closed" ]; then
|
if [ "$BD_STATE" != "closed" ]; then
|
||||||
BLOCKER_BLOCKED=true
|
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
|
# Post comment ONLY if last comment isn't already an unmet dependency notice
|
||||||
BLOCKED_LIST=$(printf '#%s, ' "${BLOCKED_BY[@]}" | sed 's/, $//')
|
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" | \
|
"${API}/issues/${ISSUE}/comments?limit=1" | \
|
||||||
jq -r '.[0].body // ""' | grep -c 'Dev-agent: Unmet dependency' || true)
|
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
|
printf '%s' "$BLOCK_COMMENT" > /tmp/block-comment.txt
|
||||||
jq -Rs '{body: .}' < /tmp/block-comment.txt > /tmp/block-comment.json
|
jq -Rs '{body: .}' < /tmp/block-comment.txt > /tmp/block-comment.json
|
||||||
curl -sf -o /dev/null -X POST \
|
curl -sf -o /dev/null -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/comments" \
|
"${API}/issues/${ISSUE}/comments" \
|
||||||
--data-binary @/tmp/block-comment.json 2>/dev/null || true
|
--data-binary @/tmp/block-comment.json 2>/dev/null || true
|
||||||
|
|
@ -373,13 +373,13 @@ log "preflight passed — no explicit unmet dependencies"
|
||||||
# CLAIM ISSUE
|
# CLAIM ISSUE
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/labels" \
|
"${API}/issues/${ISSUE}/labels" \
|
||||||
-d "{\"labels\":[${IN_PROGRESS_LABEL_ID}]}" >/dev/null 2>&1 || true
|
-d "{\"labels\":[${IN_PROGRESS_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
curl -sf -X DELETE \
|
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
|
"${API}/issues/${ISSUE}/labels/${BACKLOG_LABEL_ID}" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
CLAIMED=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
|
BODY_PR=$(echo "$ISSUE_BODY_ORIGINAL" | grep -oP 'Existing PR:\s*#\K[0-9]+' | head -1) || true
|
||||||
if [ -n "$BODY_PR" ]; then
|
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}')
|
"${API}/pulls/${BODY_PR}" | jq -r '{state, head_ref: .head.ref}')
|
||||||
PR_CHECK_STATE=$(echo "$PR_CHECK" | jq -r '.state')
|
PR_CHECK_STATE=$(echo "$PR_CHECK" | jq -r '.state')
|
||||||
if [ "$PR_CHECK_STATE" = "open" ]; then
|
if [ "$PR_CHECK_STATE" = "open" ]; then
|
||||||
|
|
@ -405,7 +405,7 @@ fi
|
||||||
|
|
||||||
if [ -z "$EXISTING_PR" ]; then
|
if [ -z "$EXISTING_PR" ]; then
|
||||||
# Priority 1: match by branch name (most reliable)
|
# 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" | \
|
"${API}/pulls?state=open&limit=20" | \
|
||||||
jq -r --arg branch "$BRANCH" \
|
jq -r --arg branch "$BRANCH" \
|
||||||
'.[] | select(.head.ref == $branch) | "\(.number) \(.head.ref)"' | head -1) || true
|
'.[] | select(.head.ref == $branch) | "\(.number) \(.head.ref)"' | head -1) || true
|
||||||
|
|
@ -418,7 +418,7 @@ fi
|
||||||
|
|
||||||
if [ -z "$EXISTING_PR" ]; then
|
if [ -z "$EXISTING_PR" ]; then
|
||||||
# Priority 2: match "Fixes #NNN" in PR body
|
# 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" | \
|
"${API}/pulls?state=open&limit=20" | \
|
||||||
jq -r --arg issue "ixes #${ISSUE}\\b" \
|
jq -r --arg issue "ixes #${ISSUE}\\b" \
|
||||||
'.[] | select(.body | test($issue; "i")) | "\(.number) \(.head.ref)"' | head -1) || true
|
'.[] | 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)
|
# Priority 3: check CLOSED PRs for prior art (don't redo work from scratch)
|
||||||
PRIOR_ART_DIFF=""
|
PRIOR_ART_DIFF=""
|
||||||
if [ -z "$EXISTING_PR" ]; then
|
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" | \
|
"${API}/pulls?state=closed&limit=30" | \
|
||||||
jq -r --arg issue "#${ISSUE}" \
|
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
|
'.[] | 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
|
if [ -n "$CLOSED_PR" ]; then
|
||||||
CLOSED_PR_NUM=$(echo "$CLOSED_PR" | awk '{print $1}')
|
CLOSED_PR_NUM=$(echo "$CLOSED_PR" | awk '{print $1}')
|
||||||
log "found closed (unmerged) PR #${CLOSED_PR_NUM} as prior art"
|
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
|
"${API}/pulls/${CLOSED_PR_NUM}.diff" | head -500) || true
|
||||||
if [ -n "$PRIOR_ART_DIFF" ]; then
|
if [ -n "$PRIOR_ART_DIFF" ]; then
|
||||||
log "captured prior art diff from PR #${CLOSED_PR_NUM} ($(echo "$PRIOR_ART_DIFF" | wc -l) lines)"
|
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
|
# 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" | \
|
"${API}/issues?state=open&labels=backlog&limit=20&type=issues" | \
|
||||||
jq -r '.[] | "#\(.number) \(.title)"' 2>/dev/null || echo "(could not fetch)")
|
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)")
|
GIT_DIFF_STAT=$(git -C "$WORKTREE" diff "origin/${PRIMARY_BRANCH}..HEAD" --stat 2>/dev/null | head -20 || echo "(no diff)")
|
||||||
LAST_PHASE=$(read_phase)
|
LAST_PHASE=$(read_phase)
|
||||||
CI_RESULT=$(cat "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" 2>/dev/null || echo "")
|
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" | \
|
"${API}/issues/${PR_NUMBER}/comments?limit=10" | \
|
||||||
jq -r '.[-3:] | .[] | "[\(.user.login)] \(.body[:500])"' 2>/dev/null || echo "(none)")
|
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}.
|
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}
|
## Issue: ${ISSUE_TITLE}
|
||||||
|
|
||||||
|
|
@ -647,7 +647,7 @@ ${PHASE_PROTOCOL_INSTRUCTIONS}"
|
||||||
else
|
else
|
||||||
# Normal mode: initial implementation prompt
|
# Normal mode: initial implementation prompt
|
||||||
INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}.
|
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}
|
## Issue: ${ISSUE_TITLE}
|
||||||
|
|
||||||
|
|
@ -713,7 +713,7 @@ fi
|
||||||
# CREATE MATRIX THREAD (before tmux so MATRIX_THREAD_ID is available for Stop hook)
|
# 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
|
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" \
|
_thread_id=$(matrix_send_ctx "dev" \
|
||||||
"🔧 Issue #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \
|
"🔧 Issue #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \
|
||||||
"🔧 <a href='${ISSUE_URL}'>Issue #${ISSUE}</a>: ${ISSUE_TITLE}") || true
|
"🔧 <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
|
if [ "${_MONITOR_LOOP_EXIT:-}" = "idle_prompt" ]; then
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"session finished without phase signal — killed. Marking blocked." \
|
"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
|
else
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"session idle for 2h — killed. Marking blocked." \
|
"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
|
fi
|
||||||
# Post diagnostic comment + label issue blocked
|
# Post diagnostic comment + label issue blocked
|
||||||
post_blocked_diagnostic "${_MONITOR_LOOP_EXIT:-idle_timeout}"
|
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"
|
source "$(dirname "$0")/../lib/ci-helpers.sh"
|
||||||
|
|
||||||
# Gitea labels API requires []int64 — look up the "underspecified" label ID once
|
# 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)
|
| jq -r '.[] | select(.name == "underspecified") | .id' 2>/dev/null || true)
|
||||||
UNDERSPECIFIED_LABEL_ID="${UNDERSPECIFIED_LABEL_ID:-1300816}"
|
UNDERSPECIFIED_LABEL_ID="${UNDERSPECIFIED_LABEL_ID:-1300816}"
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ else:
|
||||||
# Check whether an issue already has the "blocked" label
|
# Check whether an issue already has the "blocked" label
|
||||||
is_blocked() {
|
is_blocked() {
|
||||||
local issue="$1"
|
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
|
| jq -e '.[] | select(.name == "blocked")' >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,14 +103,14 @@ _post_ci_blocked_comment() {
|
||||||
| PR | #${pr_num} |"
|
| PR | #${pr_num} |"
|
||||||
|
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-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
|
-d "$(jq -nc --arg b "$comment" '{body:$b}')" >/dev/null 2>&1 || true
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-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
|
-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)
|
# HELPER: merge an approved PR directly (no Claude needed)
|
||||||
#
|
#
|
||||||
# Merging an approved, CI-green PR is a single API call. Spawning dev-agent
|
# 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).
|
# on PR creation when body contains "Fixes #N"), causing a respawn loop (#344).
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
try_direct_merge() {
|
try_direct_merge() {
|
||||||
|
|
@ -179,7 +179,7 @@ try_direct_merge() {
|
||||||
|
|
||||||
local merge_resp merge_http
|
local merge_resp merge_http
|
||||||
merge_resp=$(curl -sf -w '\n%{http_code}' -X POST \
|
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' \
|
-H 'Content-Type: application/json' \
|
||||||
"${API}/pulls/${pr_num}/merge" \
|
"${API}/pulls/${pr_num}/merge" \
|
||||||
-d '{"Do":"merge","delete_branch_after_merge":true}' 2>/dev/null) || true
|
-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
|
if [ "${merge_http:-0}" = "200" ] || [ "${merge_http:-0}" = "204" ]; then
|
||||||
log "PR #${pr_num} merged successfully"
|
log "PR #${pr_num} merged successfully"
|
||||||
if [ "$issue_num" -gt 0 ]; then
|
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 \
|
curl -sf -X PATCH \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
"${API}/issues/${issue_num}" \
|
"${API}/issues/${issue_num}" \
|
||||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
# Remove in-progress label
|
# Remove in-progress label
|
||||||
curl -sf -X DELETE \
|
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
|
"${API}/issues/${issue_num}/labels/in-progress" >/dev/null 2>&1 || true
|
||||||
# Clean up phase/session artifacts
|
# Clean up phase/session artifacts
|
||||||
rm -f "/tmp/dev-session-${PROJECT_NAME}-${issue_num}.phase" \
|
rm -f "/tmp/dev-session-${PROJECT_NAME}-${issue_num}.phase" \
|
||||||
|
|
@ -215,7 +215,7 @@ try_direct_merge() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
API="${CODEBERG_API}"
|
API="${FORGE_API}"
|
||||||
LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock"
|
LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock"
|
||||||
LOGFILE="${FACTORY_ROOT}/dev/dev-agent-${PROJECT_NAME:-default}.log"
|
LOGFILE="${FACTORY_ROOT}/dev/dev-agent-${PROJECT_NAME:-default}.log"
|
||||||
PREFLIGHT_RESULT="/tmp/dev-agent-preflight.json"
|
PREFLIGHT_RESULT="/tmp/dev-agent-preflight.json"
|
||||||
|
|
@ -233,7 +233,7 @@ log() {
|
||||||
# (See #531: direct merges should not be blocked by agent lock)
|
# (See #531: direct merges should not be blocked by agent lock)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
log "pre-lock: scanning for mergeable PRs"
|
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")
|
"${API}/pulls?state=open&limit=20")
|
||||||
|
|
||||||
PL_MERGED_ANY=false
|
PL_MERGED_ANY=false
|
||||||
|
|
@ -261,7 +261,7 @@ for i in $(seq 0 $(($(echo "$PL_PRS" | jq 'length') - 1))); do
|
||||||
fi
|
fi
|
||||||
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
|
"${API}/commits/${PL_PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
||||||
|
|
||||||
# Non-code PRs may have no CI — treat as passed
|
# 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
|
fi
|
||||||
|
|
||||||
# Check for approval (non-stale)
|
# 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
|
"${API}/pulls/${PL_PR_NUM}/reviews") || true
|
||||||
PL_HAS_APPROVE=$(echo "$PL_REVIEWS" | \
|
PL_HAS_APPROVE=$(echo "$PL_REVIEWS" | \
|
||||||
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
|
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
|
||||||
|
|
@ -319,7 +319,7 @@ dep_is_merged() {
|
||||||
|
|
||||||
# Check issue is closed
|
# Check issue is closed
|
||||||
local dep_state
|
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"')
|
"${API}/issues/${dep_num}" | jq -r '.state // "open"')
|
||||||
if [ "$dep_state" != "closed" ]; then
|
if [ "$dep_state" != "closed" ]; then
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -370,7 +370,7 @@ issue_is_ready() {
|
||||||
# PRIORITY 1: orphaned in-progress issues
|
# PRIORITY 1: orphaned in-progress issues
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
log "checking for 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")
|
"${API}/issues?state=open&labels=in-progress&limit=10&type=issues")
|
||||||
|
|
||||||
ORPHAN_COUNT=$(echo "$ORPHANS_JSON" | jq 'length')
|
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
|
SKIP_LABEL=$(echo "$ORPHAN_LABELS" | grep -oE '^(formula|action|prediction/backlog|prediction/unreviewed)$' | head -1) || true
|
||||||
if [ -n "$SKIP_LABEL" ]; then
|
if [ -n "$SKIP_LABEL" ]; then
|
||||||
log "issue #${ISSUE_NUM} has '${SKIP_LABEL}' label — removing in-progress, skipping"
|
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
|
"${API}/issues/${ISSUE_NUM}/labels/in-progress" >/dev/null 2>&1 || true
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if there's already an open PR for this issue
|
# 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" | \
|
"${API}/pulls?state=open&limit=20" | \
|
||||||
jq -r --arg branch "fix/issue-${ISSUE_NUM}" \
|
jq -r --arg branch "fix/issue-${ISSUE_NUM}" \
|
||||||
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
||||||
|
|
||||||
if [ -n "$HAS_PR" ]; then
|
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
|
"${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
|
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
||||||
|
|
||||||
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
||||||
|
|
@ -407,7 +407,7 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check formal reviews (single fetch to avoid race window)
|
# 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
|
"${API}/pulls/${HAS_PR}/reviews") || true
|
||||||
HAS_APPROVE=$(echo "$REVIEWS_JSON" | \
|
HAS_APPROVE=$(echo "$REVIEWS_JSON" | \
|
||||||
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
|
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)
|
# PRIORITY 1.5: any open PR with REQUEST_CHANGES or CI failure (stuck PRs)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
log "checking for 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")
|
"${API}/pulls?state=open&limit=20")
|
||||||
|
|
||||||
for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do
|
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
|
||||||
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
|
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
||||||
|
|
||||||
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
# 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
|
fi
|
||||||
|
|
||||||
# Single fetch to avoid race window between review checks
|
# 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
|
"${API}/pulls/${PR_NUM}/reviews") || true
|
||||||
HAS_CHANGES=$(echo "$REVIEWS_JSON" | \
|
HAS_CHANGES=$(echo "$REVIEWS_JSON" | \
|
||||||
jq -r '[.[] | select(.state == "REQUEST_CHANGES") | select(.stale == false)] | length') || true
|
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
|
ensure_priority_label >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# Tier 1: issues with both "priority" and "backlog" labels
|
# 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
|
"${API}/issues?state=open&labels=priority,backlog&limit=20&type=issues&sort=oldest") || true
|
||||||
PRIORITY_BACKLOG_JSON="${PRIORITY_BACKLOG_JSON:-[]}"
|
PRIORITY_BACKLOG_JSON="${PRIORITY_BACKLOG_JSON:-[]}"
|
||||||
|
|
||||||
# Tier 2: all "backlog" issues (includes priority ones — deduplicated below)
|
# 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")
|
"${API}/issues?state=open&labels=backlog&limit=20&type=issues&sort=oldest")
|
||||||
|
|
||||||
# Combine: priority issues first, then remaining backlog issues (deduped)
|
# Combine: priority issues first, then remaining backlog issues (deduped)
|
||||||
|
|
@ -644,15 +644,15 @@ for i in $(seq 0 $((BACKLOG_COUNT - 1))); do
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if there's already an open PR for this issue that needs attention
|
# 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" | \
|
"${API}/pulls?state=open&limit=20" | \
|
||||||
jq -r --arg branch "fix/issue-${ISSUE_NUM}" --arg num "#${ISSUE_NUM}" \
|
jq -r --arg branch "fix/issue-${ISSUE_NUM}" --arg num "#${ISSUE_NUM}" \
|
||||||
'.[] | select((.head.ref == $branch) or (.title | contains($num))) | .number' | head -1) || true
|
'.[] | select((.head.ref == $branch) or (.title | contains($num))) | .number' | head -1) || true
|
||||||
|
|
||||||
if [ -n "$EXISTING_PR" ]; then
|
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
|
"${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
|
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
||||||
|
|
||||||
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
# 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
|
fi
|
||||||
|
|
||||||
# Single fetch to avoid race window between review checks
|
# 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
|
"${API}/pulls/${EXISTING_PR}/reviews") || true
|
||||||
HAS_APPROVE=$(echo "$REVIEWS_JSON" | \
|
HAS_APPROVE=$(echo "$REVIEWS_JSON" | \
|
||||||
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
|
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")
|
REASON=$(jq -r '.reason // "unspecified"' < "$PREFLIGHT_RESULT" 2>/dev/null || echo "unspecified")
|
||||||
log "#${READY_ISSUE} too large: ${REASON}"
|
log "#${READY_ISSUE} too large: ${REASON}"
|
||||||
# Label as underspecified
|
# 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" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${READY_ISSUE}/labels" \
|
"${API}/issues/${READY_ISSUE}/labels" \
|
||||||
-d "{\"labels\":[${UNDERSPECIFIED_LABEL_ID}]}" >/dev/null 2>&1 || true
|
-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()
|
# Defines: post_refusal_comment(), _on_phase_change(), build_phase_protocol_prompt()
|
||||||
#
|
#
|
||||||
# Required globals (set by calling agent before or after sourcing):
|
# 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
|
# BRANCH, PHASE_FILE, WORKTREE, IMPL_SUMMARY_FILE, THREAD_FILE
|
||||||
# PRIMARY_BRANCH, SESSION_NAME, LOGFILE, ISSUE_TITLE
|
# PRIMARY_BRANCH, SESSION_NAME, LOGFILE, ISSUE_TITLE
|
||||||
# WOODPECKER_REPO_ID, WOODPECKER_TOKEN, WOODPECKER_SERVER
|
# 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.
|
# in-progress label, and adds the "blocked" label.
|
||||||
#
|
#
|
||||||
# Args: reason [session_name]
|
# 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() {
|
post_blocked_diagnostic() {
|
||||||
local reason="$1"
|
local reason="$1"
|
||||||
local session="${2:-${SESSION_NAME:-}}"
|
local session="${2:-${SESSION_NAME:-}}"
|
||||||
|
|
@ -88,7 +88,7 @@ ${tmux_output}
|
||||||
|
|
||||||
# Post comment to issue
|
# Post comment to issue
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/comments" \
|
"${API}/issues/${ISSUE}/comments" \
|
||||||
-d "$(jq -nc --arg b "$comment" '{body:$b}')" >/dev/null 2>&1 || true
|
-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)
|
blocked_id=$(ensure_blocked_label_id)
|
||||||
if [ -n "$blocked_id" ]; then
|
if [ -n "$blocked_id" ]; then
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/labels" \
|
"${API}/issues/${ISSUE}/labels" \
|
||||||
-d "{\"labels\":[${blocked_id}]}" >/dev/null 2>&1 || true
|
-d "{\"labels\":[${blocked_id}]}" >/dev/null 2>&1 || true
|
||||||
|
|
@ -173,7 +173,7 @@ _PHASE_PROTOCOL_EOF_
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Merge helper ---
|
# --- Merge helper ---
|
||||||
# do_merge — attempt to merge PR via Codeberg API.
|
# do_merge — attempt to merge PR via forge API.
|
||||||
# Args: pr_num
|
# Args: pr_num
|
||||||
# Returns:
|
# Returns:
|
||||||
# 0 = merged successfully
|
# 0 = merged successfully
|
||||||
|
|
@ -183,7 +183,7 @@ do_merge() {
|
||||||
local pr_num="$1"
|
local pr_num="$1"
|
||||||
local merge_response merge_http_code merge_body
|
local merge_response merge_http_code merge_body
|
||||||
merge_response=$(curl -s -w "\n%{http_code}" -X POST \
|
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' \
|
-H 'Content-Type: application/json' \
|
||||||
"${API}/pulls/${pr_num}/merge" \
|
"${API}/pulls/${pr_num}/merge" \
|
||||||
-d '{"Do":"merge","delete_branch_after_merge":true}') || true
|
-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.
|
# Before escalating, check whether the PR was already merged by another agent.
|
||||||
if [ "$merge_http_code" = "405" ]; then
|
if [ "$merge_http_code" = "405" ]; then
|
||||||
local pr_state
|
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"
|
"${API}/pulls/${pr_num}" | jq -r '.merged // false') || pr_state="false"
|
||||||
if [ "$pr_state" = "true" ]; then
|
if [ "$pr_state" = "true" ]; then
|
||||||
log "do_merge: PR #${pr_num} already merged (detected after HTTP 405) — treating as success"
|
log "do_merge: PR #${pr_num} already merged (detected after HTTP 405) — treating as success"
|
||||||
|
|
@ -220,7 +220,7 @@ do_merge() {
|
||||||
post_refusal_comment() {
|
post_refusal_comment() {
|
||||||
local emoji="$1" title="$2" body="$3"
|
local emoji="$1" title="$2" body="$3"
|
||||||
local last_has_title
|
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" | \
|
"${API}/issues/${ISSUE}/comments?limit=5" | \
|
||||||
jq -r --arg t "Dev-agent: ${title}" '[.[] | .body // ""] | any(contains($t)) | tostring') || true
|
jq -r --arg t "Dev-agent: ${title}" '[.[] | .body // ""] | any(contains($t)) | tostring') || true
|
||||||
if [ "$last_has_title" = "true" ]; then
|
if [ "$last_has_title" = "true" ]; then
|
||||||
|
|
@ -237,7 +237,7 @@ ${body}
|
||||||
printf '%s' "$comment" > "/tmp/refusal-comment.txt"
|
printf '%s' "$comment" > "/tmp/refusal-comment.txt"
|
||||||
jq -Rs '{body: .}' < "/tmp/refusal-comment.txt" > "/tmp/refusal-comment.json"
|
jq -Rs '{body: .}' < "/tmp/refusal-comment.txt" > "/tmp/refusal-comment.json"
|
||||||
curl -sf -o /dev/null -X POST \
|
curl -sf -o /dev/null -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/comments" \
|
"${API}/issues/${ISSUE}/comments" \
|
||||||
--data-binary @"/tmp/refusal-comment.json" 2>/dev/null || \
|
--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"
|
'{title: $title, body: $body, head: $head, base: $base}' > "/tmp/pr-request-${ISSUE}.json"
|
||||||
|
|
||||||
PR_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/pulls" \
|
"${API}/pulls" \
|
||||||
--data-binary @"/tmp/pr-request-${ISSUE}.json")
|
--data-binary @"/tmp/pr-request-${ISSUE}.json")
|
||||||
|
|
@ -290,13 +290,13 @@ _on_phase_change() {
|
||||||
if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then
|
if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then
|
||||||
PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number')
|
PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number')
|
||||||
log "created PR #${PR_NUMBER}"
|
log "created PR #${PR_NUMBER}"
|
||||||
PR_URL="${CODEBERG_WEB}/pulls/${PR_NUMBER}"
|
PR_URL="${FORGE_WEB}/pulls/${PR_NUMBER}"
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"PR #${PR_NUMBER} created: ${ISSUE_TITLE}" \
|
"PR #${PR_NUMBER} created: ${ISSUE_TITLE}" \
|
||||||
"PR <a href='${PR_URL}'>#${PR_NUMBER}</a> created: ${ISSUE_TITLE}"
|
"PR <a href='${PR_URL}'>#${PR_NUMBER}</a> created: ${ISSUE_TITLE}"
|
||||||
elif [ "$PR_HTTP_CODE" = "409" ]; then
|
elif [ "$PR_HTTP_CODE" = "409" ]; then
|
||||||
# PR already exists (race condition) — find it
|
# 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" | \
|
"${API}/pulls?state=open&limit=20" | \
|
||||||
jq -r --arg branch "$BRANCH" \
|
jq -r --arg branch "$BRANCH" \
|
||||||
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
||||||
|
|
@ -305,7 +305,7 @@ _on_phase_change() {
|
||||||
log "PR already exists: #${PR_NUMBER}"
|
log "PR already exists: #${PR_NUMBER}"
|
||||||
else
|
else
|
||||||
log "ERROR: PR creation got 409 but no existing PR found"
|
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
|
return 0
|
||||||
fi
|
fi
|
||||||
else
|
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
|
# Poll CI until done or timeout
|
||||||
status "waiting for CI on PR #${PR_NUMBER}"
|
status "waiting for CI on PR #${PR_NUMBER}"
|
||||||
CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || \
|
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')
|
"${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha')
|
||||||
|
|
||||||
CI_DONE=false
|
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
|
# 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_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"')
|
"${API}/commits/${CI_CURRENT_SHA}/status" | jq -r '.state // "unknown"')
|
||||||
if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
|
if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
|
||||||
CI_DONE=true
|
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}\""
|
echo \"PHASE:awaiting_review\" > \"${PHASE_FILE}\""
|
||||||
else
|
else
|
||||||
# Fetch CI error details
|
# 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" | \
|
"${API}/commits/${CI_CURRENT_SHA}/status" | \
|
||||||
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
|
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"
|
log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating"
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"CI exhausted after ${CI_FIX_COUNT} attempts — escalating for human help" \
|
"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"
|
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
|
# Do NOT update LAST_PHASE_MTIME here — let the main loop detect PHASE:escalate
|
||||||
return 0
|
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')
|
_ci_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500 | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"CI failed on PR #${PR_NUMBER}: step=${FAILED_STEP:-unknown} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES})" \
|
"CI failed on PR #${PR_NUMBER}: 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}).
|
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
|
if [ -z "${PR_NUMBER:-}" ]; then
|
||||||
log "WARNING: awaiting_review but PR_NUMBER unknown — searching for PR"
|
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" | \
|
"${API}/pulls?state=open&limit=20" | \
|
||||||
jq -r --arg branch "$BRANCH" \
|
jq -r --arg branch "$BRANCH" \
|
||||||
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
|
||||||
|
|
@ -498,9 +498,9 @@ Instructions:
|
||||||
break
|
break
|
||||||
fi
|
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
|
"${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" \
|
jq -r --arg sha "$REVIEW_SHA" \
|
||||||
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
'[.[] | 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)
|
VERDICT=$(echo "$REVIEW_TEXT" | grep -oP '\*\*(APPROVE|REQUEST_CHANGES|DISCUSS)\*\*' | head -1 | tr -d '*' || true)
|
||||||
log "review verdict: ${VERDICT:-unknown}"
|
log "review verdict: ${VERDICT:-unknown}"
|
||||||
|
|
||||||
# Also check formal Codeberg reviews
|
# Also check formal forge reviews
|
||||||
if [ -z "$VERDICT" ]; then
|
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" | \
|
"${API}/pulls/${PR_NUMBER}/reviews" | \
|
||||||
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
|
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
|
||||||
if [ "$VERDICT" = "APPROVED" ]; then
|
if [ "$VERDICT" = "APPROVED" ]; then
|
||||||
|
|
@ -548,7 +548,7 @@ Instructions:
|
||||||
if [ "$_merge_rc" -eq 0 ]; then
|
if [ "$_merge_rc" -eq 0 ]; then
|
||||||
# Merge succeeded — close issue and signal done
|
# Merge succeeded — close issue and signal done
|
||||||
curl -sf -X PATCH \
|
curl -sf -X PATCH \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
"${API}/issues/${ISSUE}" \
|
"${API}/issues/${ISSUE}" \
|
||||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
|
|
@ -596,7 +596,7 @@ Instructions:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if PR was merged or closed externally
|
# 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
|
"${API}/pulls/${PR_NUMBER}") || true
|
||||||
PR_STATE=$(echo "$PR_JSON" | jq -r '.state // "unknown"')
|
PR_STATE=$(echo "$PR_JSON" | jq -r '.state // "unknown"')
|
||||||
PR_MERGED=$(echo "$PR_JSON" | jq -r '.merged // false')
|
PR_MERGED=$(echo "$PR_JSON" | jq -r '.merged // false')
|
||||||
|
|
@ -605,8 +605,8 @@ Instructions:
|
||||||
log "PR #${PR_NUMBER} was merged externally"
|
log "PR #${PR_NUMBER} was merged externally"
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"✅ PR #${PR_NUMBER} merged externally! Issue #${ISSUE} done." \
|
"✅ 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."
|
"✅ 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 ${CODEBERG_TOKEN}" \
|
curl -sf -X PATCH -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
cleanup_labels
|
cleanup_labels
|
||||||
|
|
@ -637,9 +637,9 @@ Instructions:
|
||||||
elif [ "$phase" = "PHASE:escalate" ]; then
|
elif [ "$phase" = "PHASE:escalate" ]; then
|
||||||
status "escalated — waiting for human input on issue #${ISSUE}"
|
status "escalated — waiting for human input on issue #${ISSUE}"
|
||||||
ESCALATE_REASON=$(sed -n '2p' "$PHASE_FILE" 2>/dev/null | sed 's/^Reason: //' || echo "")
|
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=""
|
_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 \
|
notify_ctx \
|
||||||
"⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) escalated — needs human input.${ESCALATE_REASON:+ Reason: ${ESCALATE_REASON}}" \
|
"⚠️ 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."
|
"⚠️ <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"
|
status "phase done — PR #${PR_NUMBER} merged, cleaning up"
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"✅ PR #${PR_NUMBER} merged! Issue #${ISSUE} done." \
|
"✅ 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
|
else
|
||||||
status "phase done — issue #${ISSUE} complete, cleaning up"
|
status "phase done — issue #${ISSUE} complete, cleaning up"
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"✅ Issue #${ISSUE} done." \
|
"✅ Issue #${ISSUE} done." \
|
||||||
"✅ <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
"✅ <a href='${FORGE_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Belt-and-suspenders: ensure in-progress label removed (idempotent)
|
# Belt-and-suspenders: ensure in-progress label removed (idempotent)
|
||||||
|
|
@ -680,10 +680,10 @@ Instructions:
|
||||||
FAILURE_REASON="${FAILURE_REASON:-unspecified}"
|
FAILURE_REASON="${FAILURE_REASON:-unspecified}"
|
||||||
log "phase: failed — reason: ${FAILURE_REASON}"
|
log "phase: failed — reason: ${FAILURE_REASON}"
|
||||||
# Gitea labels API requires []int64 — look up the "backlog" label ID once
|
# 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)
|
| jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true)
|
||||||
BACKLOG_LABEL_ID="${BACKLOG_LABEL_ID:-1300815}"
|
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)
|
| jq -r '.[] | select(.name == "underspecified") | .id' 2>/dev/null || true)
|
||||||
UNDERSPECIFIED_LABEL_ID="${UNDERSPECIFIED_LABEL_ID:-1300816}"
|
UNDERSPECIFIED_LABEL_ID="${UNDERSPECIFIED_LABEL_ID:-1300816}"
|
||||||
|
|
||||||
|
|
@ -703,7 +703,7 @@ Instructions:
|
||||||
# Unclaim issue (restore backlog label, remove in-progress)
|
# Unclaim issue (restore backlog label, remove in-progress)
|
||||||
cleanup_labels
|
cleanup_labels
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/labels" \
|
"${API}/issues/${ISSUE}/labels" \
|
||||||
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
-d "{\"labels\":[${BACKLOG_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||||
|
|
@ -732,12 +732,12 @@ ${REASON}
|
||||||
### Next steps
|
### Next steps
|
||||||
A maintainer should split this issue or add more detail to the spec."
|
A maintainer should split this issue or add more detail to the spec."
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}/labels" \
|
"${API}/issues/${ISSUE}/labels" \
|
||||||
-d "{\"labels\":[${UNDERSPECIFIED_LABEL_ID}]}" >/dev/null 2>&1 || true
|
-d "{\"labels\":[${UNDERSPECIFIED_LABEL_ID}]}" >/dev/null 2>&1 || true
|
||||||
curl -sf -X DELETE \
|
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
|
"${API}/issues/${ISSUE}/labels/${BACKLOG_LABEL_ID}" >/dev/null 2>&1 || true
|
||||||
notify "refused #${ISSUE}: too large — ${REASON}"
|
notify "refused #${ISSUE}: too large — ${REASON}"
|
||||||
;;
|
;;
|
||||||
|
|
@ -749,7 +749,7 @@ ${REASON}
|
||||||
|
|
||||||
Closing as already implemented."
|
Closing as already implemented."
|
||||||
curl -sf -X PATCH \
|
curl -sf -X PATCH \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}" \
|
"${API}/issues/${ISSUE}" \
|
||||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
|
|
@ -779,7 +779,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
|
||||||
log "session failed: ${FAILURE_REASON}"
|
log "session failed: ${FAILURE_REASON}"
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}" \
|
"❌ 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"
|
post_blocked_diagnostic "$FAILURE_REASON"
|
||||||
|
|
||||||
agent_kill_session "$SESSION_NAME"
|
agent_kill_session "$SESSION_NAME"
|
||||||
|
|
@ -801,7 +801,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
|
||||||
log "session crashed for issue #${ISSUE}"
|
log "session crashed for issue #${ISSUE}"
|
||||||
notify_ctx \
|
notify_ctx \
|
||||||
"session crashed unexpectedly — marking blocked" \
|
"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"
|
post_blocked_diagnostic "crashed"
|
||||||
[ -z "${PR_NUMBER:-}" ] && cleanup_worktree
|
[ -z "${PR_NUMBER:-}" ] && cleanup_worktree
|
||||||
[ -n "${PR_NUMBER:-}" ] && log "keeping worktree (PR #${PR_NUMBER} still open)"
|
[ -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 |
|
| 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 |
|
| CI/CD | Woodpecker | Build/test results | **Implemented** — Live |
|
||||||
| Protocol | Ponder / GraphQL | On-chain state, trades, positions | **Partial** — Live (not yet wired to evidence) |
|
| 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 |
|
| 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 reply → matrix_listener.sh injects reply into tmux session
|
||||||
on timeout → 24h: label issue blocked, kill 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 merged → kill tmux session, clean labels, close issue
|
||||||
if not → inject "PR not merged yet" into session
|
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:
|
In this mode, skip the normal tech-debt grooming pipeline. Instead:
|
||||||
a. Fetch the target issue:
|
a. Fetch the target issue:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<target_issue>"
|
"$FORGE_API/issues/<target_issue>"
|
||||||
b. Fetch ALL comments on the target issue to understand scope and
|
b. Fetch ALL comments on the target issue to understand scope and
|
||||||
prior bounce reasons:
|
prior bounce reasons:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<target_issue>/comments?limit=50"
|
"$FORGE_API/issues/<target_issue>/comments?limit=50"
|
||||||
c. Read the affected files listed in the issue body to understand
|
c. Read the affected files listed in the issue body to understand
|
||||||
the actual code scope.
|
the actual code scope.
|
||||||
d. Break the issue into 2-5 sub-issues, each sized for a single
|
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.
|
This step only runs in grooming mode. Skip if in breakdown mode.
|
||||||
|
|
||||||
Fetch all open tech-debt issues:
|
Fetch all open tech-debt issues:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
|
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
|
||||||
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
|
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
|
||||||
|
|
||||||
For each issue compute a triage score:
|
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.
|
The dev-agent is completely starved until they are promoted or resolved.
|
||||||
|
|
||||||
For each tier-0 issue:
|
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 resolvable: promote to backlog — add acceptance criteria, affected files, relabel
|
||||||
- If needs human decision: add to ESCALATE block
|
- If needs human decision: add to ESCALATE block
|
||||||
- If invalid / wontfix: close with explanation comment
|
- If invalid / wontfix: close with explanation comment
|
||||||
|
|
||||||
After completing all tier-0, re-fetch to check for new blockers:
|
After completing all tier-0, re-fetch to check for new blockers:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
|
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
|
||||||
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
|
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))]'
|
||||||
|
|
||||||
If new tier-0 blockers appeared, process those too.
|
If new tier-0 blockers appeared, process those too.
|
||||||
|
|
@ -172,8 +172,8 @@ id = "verify"
|
||||||
title = "Verify completion and loop until zero tech-debt"
|
title = "Verify completion and loop until zero tech-debt"
|
||||||
description = """
|
description = """
|
||||||
Re-fetch ALL open tech-debt issues and count them:
|
Re-fetch ALL open tech-debt issues and count them:
|
||||||
REMAINING=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
REMAINING=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?type=issues&state=open&limit=50" | \
|
"$FORGE_API/issues?type=issues&state=open&limit=50" | \
|
||||||
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))] | length')
|
jq '[.[] | select(.labels | map(.name) | any(. == "tech-debt"))] | length')
|
||||||
echo "Remaining tech-debt: $REMAINING"
|
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:
|
tech-debt issues via API so they are tracked separately:
|
||||||
|
|
||||||
# Look up tech-debt label ID (create if missing):
|
# Look up tech-debt label ID (create if missing):
|
||||||
TECH_DEBT_ID=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
TECH_DEBT_ID=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/labels" | jq -r '.[] | select(.name=="tech-debt") | .id')
|
"$FORGE_API/labels" | jq -r '.[] | select(.name=="tech-debt") | .id')
|
||||||
|
|
||||||
if [ -z "$TECH_DEBT_ID" ]; then
|
if [ -z "$TECH_DEBT_ID" ]; then
|
||||||
TECH_DEBT_ID=$(curl -sf -X POST \
|
TECH_DEBT_ID=$(curl -sf -X POST \
|
||||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-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')
|
-d '{"name":"tech-debt","color":"#6B7280","description":"Pre-existing tech debt flagged by AI review"}' | jq -r '.id')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for duplicate before creating:
|
# Check for duplicate before creating:
|
||||||
EXISTING=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
EXISTING=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&labels=tech-debt&limit=50" | \
|
"$FORGE_API/issues?state=open&labels=tech-debt&limit=50" | \
|
||||||
jq --arg t "TITLE" '[.[] | select(.title == $t)] | length')
|
jq --arg t "TITLE" '[.[] | select(.title == $t)] | length')
|
||||||
|
|
||||||
# Create only if no duplicate:
|
# Create only if no duplicate:
|
||||||
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" \
|
-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]}'
|
-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
|
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:
|
After writing the JSON file, signal completion:
|
||||||
echo "PHASE:done" > "$PHASE_FILE"
|
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):
|
Pre-checks (bash, zero tokens — detect problems before invoking Claude):
|
||||||
|
|
||||||
1. Fetch all open issues:
|
1. Fetch all open issues:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&type=issues&limit=50&sort=updated&direction=desc"
|
"$FORGE_API/issues?state=open&type=issues&limit=50&sort=updated&direction=desc"
|
||||||
|
|
||||||
2. Duplicate detection: compare issue titles pairwise. Normalize
|
2. Duplicate detection: compare issue titles pairwise. Normalize
|
||||||
(lowercase, strip prefixes like feat:/fix:/refactor:, collapse whitespace)
|
(lowercase, strip prefixes like feat:/fix:/refactor:, collapse whitespace)
|
||||||
|
|
@ -162,7 +162,7 @@ Sibling dependency rule (CRITICAL):
|
||||||
|
|
||||||
If either section is missing:
|
If either section is missing:
|
||||||
a. Write a comment action to the manifest:
|
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
|
Where <missing items> is a comma-separated list of what's absent
|
||||||
(e.g. "acceptance criteria, affected files" or just "affected files").
|
(e.g. "acceptance criteria, affected files" or just "affected files").
|
||||||
b. Write a remove_label action to the manifest:
|
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.)
|
(See issue #352 for the blocked label convention.)
|
||||||
|
|
||||||
1. Fetch all blocked issues:
|
1. Fetch all blocked issues:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&type=issues&labels=blocked&limit=50"
|
"$FORGE_API/issues?state=open&type=issues&labels=blocked&limit=50"
|
||||||
|
|
||||||
2. For each blocked issue, read the full body and comments:
|
2. For each blocked issue, read the full body and comments:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<number>"
|
"$FORGE_API/issues/<number>"
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<number>/comments"
|
"$FORGE_API/issues/<number>/comments"
|
||||||
|
|
||||||
3. Check dependencies — extract issue numbers from ## Dependencies /
|
3. Check dependencies — extract issue numbers from ## Dependencies /
|
||||||
## Depends on / ## Blocked by sections. For each dependency:
|
## Depends on / ## Blocked by sections. For each dependency:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<dep_number>"
|
"$FORGE_API/issues/<dep_number>"
|
||||||
Check if the dependency is now closed.
|
Check if the dependency is now closed.
|
||||||
|
|
||||||
4. For each blocked issue, choose ONE action:
|
4. For each blocked issue, choose ONE action:
|
||||||
|
|
@ -459,9 +459,9 @@ executes them after the PR merges.
|
||||||
git push -u origin "$BRANCH"
|
git push -u origin "$BRANCH"
|
||||||
g. Create a PR:
|
g. Create a PR:
|
||||||
PR_RESPONSE=$(curl -sf -X POST \
|
PR_RESPONSE=$(curl -sf -X POST \
|
||||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/pulls" \
|
"$FORGE_API/pulls" \
|
||||||
-d '{"title":"chore: gardener housekeeping",
|
-d '{"title":"chore: gardener housekeeping",
|
||||||
"head":"'"$BRANCH"'","base":"'"$PRIMARY_BRANCH"'",
|
"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."}')
|
"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).
|
(e.g. "red-team stale since March 12" is confirmed by evidence/ timestamps).
|
||||||
|
|
||||||
1. Fetch unreviewed predictions:
|
1. Fetch unreviewed predictions:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
|
"$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
|
If there are none, note that and skip to step 3b (label resolution
|
||||||
is still required — the file-at-constraints step needs label IDs).
|
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.
|
Project formulas are dispatched via action issues on the project repo.
|
||||||
|
|
||||||
3. Fetch all open issues to check for overlap:
|
3. Fetch all open issues to check for overlap:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&type=issues&limit=50"
|
"$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 —
|
ALWAYS execute this step, even if there are no predictions to triage —
|
||||||
the file-at-constraints step depends on these IDs:
|
the file-at-constraints step depends on these IDs:
|
||||||
- <unreviewed_label_id> → prediction/unreviewed
|
- <unreviewed_label_id> → prediction/unreviewed
|
||||||
|
|
@ -120,65 +120,65 @@ Evidence from the preflight step informs whether each prediction is valid
|
||||||
Example body structure:
|
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
|
## 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:
|
Create the issue:
|
||||||
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" \
|
-H "Content-Type: application/json" "$FORGE_API/issues" \
|
||||||
-d '{"title":"...","body":"...","labels":[<label_id>]}'
|
-d '{"title":"...","body":"...","labels":[<label_id>]}'
|
||||||
Extract the issue number from the response (jq -r '.number').
|
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:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<new_issue_num>/labels" \
|
"$FORGE_API/issues/<new_issue_num>/labels" \
|
||||||
-d '{"labels":[<label_id>]}'
|
-d '{"labels":[<label_id>]}'
|
||||||
b. Comment on the prediction with "Actioned as #NNN — <reasoning>":
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/comments" \
|
"$FORGE_API/issues/<pred_num>/comments" \
|
||||||
-d '{"body":"Actioned as #NNN — <reasoning>"}'
|
-d '{"body":"Actioned as #NNN — <reasoning>"}'
|
||||||
c. Relabel: remove prediction/unreviewed, add prediction/actioned:
|
c. Relabel: remove prediction/unreviewed, add prediction/actioned:
|
||||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/labels" \
|
"$FORGE_API/issues/<pred_num>/labels" \
|
||||||
-d '{"labels":[<actioned_label_id>]}'
|
-d '{"labels":[<actioned_label_id>]}'
|
||||||
d. Close the prediction:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>" \
|
"$FORGE_API/issues/<pred_num>" \
|
||||||
-d '{"state":"closed"}'
|
-d '{"state":"closed"}'
|
||||||
|
|
||||||
For WATCH:
|
For WATCH:
|
||||||
a. Comment with reasoning why not urgent:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/comments" \
|
"$FORGE_API/issues/<pred_num>/comments" \
|
||||||
-d '{"body":"Watching — <reasoning>"}'
|
-d '{"body":"Watching — <reasoning>"}'
|
||||||
b. Replace prediction/unreviewed label with prediction/backlog:
|
b. Replace prediction/unreviewed label with prediction/backlog:
|
||||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/labels" \
|
"$FORGE_API/issues/<pred_num>/labels" \
|
||||||
-d '{"labels":[<prediction_backlog_label_id>]}'
|
-d '{"labels":[<prediction_backlog_label_id>]}'
|
||||||
|
|
||||||
For DISMISS:
|
For DISMISS:
|
||||||
a. Comment with explicit reasoning:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/comments" \
|
"$FORGE_API/issues/<pred_num>/comments" \
|
||||||
-d '{"body":"Dismissed — <reasoning>"}'
|
-d '{"body":"Dismissed — <reasoning>"}'
|
||||||
b. Relabel: remove prediction/unreviewed, add prediction/actioned:
|
b. Relabel: remove prediction/unreviewed, add prediction/actioned:
|
||||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
"$FORGE_API/issues/<pred_num>/labels/<unreviewed_label_id>"
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>/labels" \
|
"$FORGE_API/issues/<pred_num>/labels" \
|
||||||
-d '{"labels":[<actioned_label_id>]}'
|
-d '{"labels":[<actioned_label_id>]}'
|
||||||
c. Close the prediction:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<pred_num>" \
|
"$FORGE_API/issues/<pred_num>" \
|
||||||
-d '{"state":"closed"}'
|
-d '{"state":"closed"}'
|
||||||
|
|
||||||
6. Track promoted predictions — they are added to the prerequisite tree
|
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
|
- $PROJECT_REPO_ROOT/formulas/*.toml — project-specific formulas
|
||||||
- Open issues (fetched via API, or reuse from prediction-triage)
|
- Open issues (fetched via API, or reuse from prediction-triage)
|
||||||
- Closed issues (fetch recently closed to detect resolved prerequisites):
|
- Closed issues (fetch recently closed to detect resolved prerequisites):
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=closed&type=issues&limit=50&sort=updated&direction=desc"
|
"$FORGE_API/issues?state=closed&type=issues&limit=50&sort=updated&direction=desc"
|
||||||
- Planner memory (loaded in preflight)
|
- Planner memory (loaded in preflight)
|
||||||
- Promoted predictions from prediction-triage (add as prerequisites if relevant)
|
- 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
|
For each issue referenced in the prerequisite tree (by #number), fetch its
|
||||||
recent comments to detect signals that the issue is stuck or bouncing:
|
recent comments to detect signals that the issue is stuck or bouncing:
|
||||||
|
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<number>/comments?limit=10"
|
"$FORGE_API/issues/<number>/comments?limit=10"
|
||||||
|
|
||||||
Scan each comment body for these signals:
|
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)
|
## 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:
|
Create the issue:
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues" \
|
"$FORGE_API/issues" \
|
||||||
-d '{"title":"...","body":"...","labels":[<backlog_label_id>]}'
|
-d '{"title":"...","body":"...","labels":[<backlog_label_id>]}'
|
||||||
Extract the issue number from the response (jq -r '.number').
|
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:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<new_issue_num>/labels" \
|
"$FORGE_API/issues/<new_issue_num>/labels" \
|
||||||
-d '{"labels":[<backlog_label_id>]}'
|
-d '{"labels":[<backlog_label_id>]}'
|
||||||
|
|
||||||
3. If an issue already exists and is open, skip it — no duplicate filing.
|
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:**
|
5. **Add `priority` to top-5 constraint issues:**
|
||||||
For each of the top 5 constraint issues (whether just filed or already
|
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:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues/<issue_number>/labels" \
|
"$FORGE_API/issues/<issue_number>/labels" \
|
||||||
-d '{"labels":[<priority_label_id>]}'
|
-d '{"labels":[<priority_label_id>]}'
|
||||||
|
|
||||||
6. **Remove `priority` from issues no longer in top 5:**
|
6. **Remove `priority` from issues no longer in top 5:**
|
||||||
Fetch all open issues that currently have the `priority` label:
|
Fetch all open issues that currently have the `priority` label:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&labels=priority&type=issues&limit=50"
|
"$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
|
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
|
constraint issues, remove the `priority` label (demote back to plain
|
||||||
`backlog`):
|
`backlog`):
|
||||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -X DELETE -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<issue_number>/labels/<priority_label_id>"
|
"$FORGE_API/issues/<issue_number>/labels/<priority_label_id>"
|
||||||
This keeps the priority set current — only the active bottleneck issues
|
This keeps the priority set current — only the active bottleneck issues
|
||||||
get priority, not stale constraints from previous runs.
|
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"
|
git push -u origin "$BRANCH"
|
||||||
g. Create a PR:
|
g. Create a PR:
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/pulls" \
|
"$FORGE_API/pulls" \
|
||||||
-d '{"title":"chore: planner run — prerequisite tree update",
|
-d '{"title":"chore: planner run — prerequisite tree update",
|
||||||
"head":"<branch>","base":"<primary-branch>",
|
"head":"<branch>","base":"<primary-branch>",
|
||||||
"body":"Automated planner run — prerequisite tree update and journal entry."}'
|
"body":"Automated planner run — prerequisite tree update and journal entry."}'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
# Goal: find the project's biggest weakness. Explore when uncertain,
|
# Goal: find the project's biggest weakness. Explore when uncertain,
|
||||||
# exploit when confident (dispatch a formula to prove the theory).
|
# 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.
|
# No separate memory file — the issue tracker is the source of truth.
|
||||||
#
|
#
|
||||||
# Executed by predictor/predictor-run.sh via cron — no action issues.
|
# 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
|
git pull --ff-only origin "$PRIMARY_BRANCH" --quiet
|
||||||
|
|
||||||
2. Fetch ALL your previous predictions (open + recently closed):
|
2. Fetch ALL your previous predictions (open + recently closed):
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
|
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Funreviewed&limit=50"
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&type=issues&labels=prediction%2Fbacklog&limit=50"
|
"$FORGE_API/issues?state=open&type=issues&labels=prediction%2Fbacklog&limit=50"
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=closed&type=issues&labels=prediction%2Factioned&limit=50&sort=updated&direction=desc"
|
"$FORGE_API/issues?state=closed&type=issues&labels=prediction%2Factioned&limit=50&sort=updated&direction=desc"
|
||||||
|
|
||||||
For each prediction, note:
|
For each prediction, note:
|
||||||
- What you predicted (title + body)
|
- What you predicted (title + body)
|
||||||
|
|
@ -150,21 +150,21 @@ For each weakness you identify, choose one:
|
||||||
## Filing
|
## Filing
|
||||||
|
|
||||||
1. Look up label IDs:
|
1. Look up label IDs:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/labels" | jq '[.[] | select(.name | startswith("prediction")) | {name, id}]'
|
"$FORGE_API/labels" | jq '[.[] | select(.name | startswith("prediction")) | {name, id}]'
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $FORGE_TOKEN" \
|
||||||
"$CODEBERG_API/labels" | jq '.[] | select(.name == "action") | .id'
|
"$FORGE_API/labels" | jq '.[] | select(.name == "action") | .id'
|
||||||
|
|
||||||
2. File predictions:
|
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" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/issues" \
|
"$FORGE_API/issues" \
|
||||||
-d '{"title":"<title>","body":"<body>","labels":[<prediction_unreviewed_id>]}'
|
-d '{"title":"<title>","body":"<body>","labels":[<prediction_unreviewed_id>]}'
|
||||||
|
|
||||||
3. File action dispatches (if exploiting):
|
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" \
|
-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>]}'
|
-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
|
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.
|
PR, reviewed alongside AGENTS.md changes, executed by gardener-run.sh after merge.
|
||||||
|
|
||||||
**Environment variables consumed**:
|
**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)
|
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by gardener-run.sh)
|
||||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ If no file changes in commit-and-pr:
|
||||||
echo 'PHASE:done' > '${PHASE_FILE}'"
|
echo 'PHASE:done' > '${PHASE_FILE}'"
|
||||||
|
|
||||||
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
# 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.
|
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.
|
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)
|
add_label)
|
||||||
local label label_id
|
local label label_id
|
||||||
label=$(jq -r ".[$i].label" "$manifest_file")
|
label=$(jq -r ".[$i].label" "$manifest_file")
|
||||||
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
|
"${FORGE_API}/labels" | jq -r --arg n "$label" \
|
||||||
'.[] | select(.name == $n) | .id') || true
|
'.[] | select(.name == $n) | .id') || true
|
||||||
if [ -n "$label_id" ]; then
|
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' \
|
-H 'Content-Type: application/json' \
|
||||||
"${CODEBERG_API}/issues/${issue}/labels" \
|
"${FORGE_API}/issues/${issue}/labels" \
|
||||||
-d "{\"labels\":[${label_id}]}" >/dev/null 2>&1; then
|
-d "{\"labels\":[${label_id}]}" >/dev/null 2>&1; then
|
||||||
log "manifest: add_label '${label}' to #${issue}"
|
log "manifest: add_label '${label}' to #${issue}"
|
||||||
else
|
else
|
||||||
|
|
@ -182,12 +182,12 @@ _gardener_execute_manifest() {
|
||||||
remove_label)
|
remove_label)
|
||||||
local label label_id
|
local label label_id
|
||||||
label=$(jq -r ".[$i].label" "$manifest_file")
|
label=$(jq -r ".[$i].label" "$manifest_file")
|
||||||
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
label_id=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
|
"${FORGE_API}/labels" | jq -r --arg n "$label" \
|
||||||
'.[] | select(.name == $n) | .id') || true
|
'.[] | select(.name == $n) | .id') || true
|
||||||
if [ -n "$label_id" ]; then
|
if [ -n "$label_id" ]; then
|
||||||
if curl -sf -X DELETE -H "Authorization: token ${CODEBERG_TOKEN}" \
|
if curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1; then
|
"${FORGE_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1; then
|
||||||
log "manifest: remove_label '${label}' from #${issue}"
|
log "manifest: remove_label '${label}' from #${issue}"
|
||||||
else
|
else
|
||||||
log "manifest: FAILED remove_label '${label}' from #${issue}"
|
log "manifest: FAILED remove_label '${label}' from #${issue}"
|
||||||
|
|
@ -200,9 +200,9 @@ _gardener_execute_manifest() {
|
||||||
close)
|
close)
|
||||||
local reason
|
local reason
|
||||||
reason=$(jq -r ".[$i].reason // empty" "$manifest_file")
|
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' \
|
-H 'Content-Type: application/json' \
|
||||||
"${CODEBERG_API}/issues/${issue}" \
|
"${FORGE_API}/issues/${issue}" \
|
||||||
-d '{"state":"closed"}' >/dev/null 2>&1; then
|
-d '{"state":"closed"}' >/dev/null 2>&1; then
|
||||||
log "manifest: closed #${issue} (${reason})"
|
log "manifest: closed #${issue} (${reason})"
|
||||||
else
|
else
|
||||||
|
|
@ -214,9 +214,9 @@ _gardener_execute_manifest() {
|
||||||
local body escaped_body
|
local body escaped_body
|
||||||
body=$(jq -r ".[$i].body" "$manifest_file")
|
body=$(jq -r ".[$i].body" "$manifest_file")
|
||||||
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
|
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' \
|
-H 'Content-Type: application/json' \
|
||||||
"${CODEBERG_API}/issues/${issue}/comments" \
|
"${FORGE_API}/issues/${issue}/comments" \
|
||||||
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
||||||
log "manifest: commented on #${issue}"
|
log "manifest: commented on #${issue}"
|
||||||
else
|
else
|
||||||
|
|
@ -235,8 +235,8 @@ _gardener_execute_manifest() {
|
||||||
label_ids="[]"
|
label_ids="[]"
|
||||||
if [ -n "$labels" ]; then
|
if [ -n "$labels" ]; then
|
||||||
local all_labels ids_json=""
|
local all_labels ids_json=""
|
||||||
all_labels=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
all_labels=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/labels") || true
|
"${FORGE_API}/labels") || true
|
||||||
while IFS= read -r lname; do
|
while IFS= read -r lname; do
|
||||||
local lid
|
local lid
|
||||||
lid=$(echo "$all_labels" | jq -r --arg n "$lname" \
|
lid=$(echo "$all_labels" | jq -r --arg n "$lname" \
|
||||||
|
|
@ -245,9 +245,9 @@ _gardener_execute_manifest() {
|
||||||
done <<< "$labels"
|
done <<< "$labels"
|
||||||
[ -n "$ids_json" ] && label_ids="[${ids_json}]"
|
[ -n "$ids_json" ] && label_ids="[${ids_json}]"
|
||||||
fi
|
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' \
|
-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
|
-d "{\"title\":${escaped_title},\"body\":${escaped_body},\"labels\":${label_ids}}" >/dev/null 2>&1; then
|
||||||
log "manifest: created issue '${title}'"
|
log "manifest: created issue '${title}'"
|
||||||
else
|
else
|
||||||
|
|
@ -259,9 +259,9 @@ _gardener_execute_manifest() {
|
||||||
local body escaped_body
|
local body escaped_body
|
||||||
body=$(jq -r ".[$i].body" "$manifest_file")
|
body=$(jq -r ".[$i].body" "$manifest_file")
|
||||||
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
|
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' \
|
-H 'Content-Type: application/json' \
|
||||||
"${CODEBERG_API}/issues/${issue}" \
|
"${FORGE_API}/issues/${issue}" \
|
||||||
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
||||||
log "manifest: edited body of #${issue}"
|
log "manifest: edited body of #${issue}"
|
||||||
else
|
else
|
||||||
|
|
@ -284,9 +284,9 @@ _gardener_execute_manifest() {
|
||||||
_gardener_merge() {
|
_gardener_merge() {
|
||||||
local merge_response merge_http_code
|
local merge_response merge_http_code
|
||||||
merge_response=$(curl -s -w "\n%{http_code}" -X POST \
|
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' \
|
-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
|
-d '{"Do":"merge","delete_branch_after_merge":true}') || true
|
||||||
merge_http_code=$(echo "$merge_response" | tail -1)
|
merge_http_code=$(echo "$merge_response" | tail -1)
|
||||||
|
|
||||||
|
|
@ -300,8 +300,8 @@ _gardener_merge() {
|
||||||
# Already merged (race)?
|
# Already merged (race)?
|
||||||
if [ "$merge_http_code" = "405" ]; then
|
if [ "$merge_http_code" = "405" ]; then
|
||||||
local pr_merged
|
local pr_merged
|
||||||
pr_merged=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
pr_merged=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
|
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
|
||||||
if [ "$pr_merged" = "true" ]; then
|
if [ "$pr_merged" = "true" ]; then
|
||||||
log "gardener PR #${_GARDENER_PR} already merged"
|
log "gardener PR #${_GARDENER_PR} already merged"
|
||||||
_gardener_execute_manifest
|
_gardener_execute_manifest
|
||||||
|
|
@ -329,9 +329,9 @@ _gardener_timeout_cleanup() {
|
||||||
log "gardener merge-through timed out (${_GARDENER_MERGE_TIMEOUT}s) — closing PR"
|
log "gardener merge-through timed out (${_GARDENER_MERGE_TIMEOUT}s) — closing PR"
|
||||||
if [ -n "$_GARDENER_PR" ]; then
|
if [ -n "$_GARDENER_PR" ]; then
|
||||||
curl -sf -X PATCH \
|
curl -sf -X PATCH \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" \
|
"${FORGE_API}/pulls/${_GARDENER_PR}" \
|
||||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
printf 'PHASE:failed\nReason: merge-through timeout (%ss)\n' \
|
printf 'PHASE:failed\nReason: merge-through timeout (%ss)\n' \
|
||||||
|
|
@ -360,8 +360,8 @@ _gardener_handle_ci() {
|
||||||
fi
|
fi
|
||||||
# Fallback: search for open gardener PRs
|
# Fallback: search for open gardener PRs
|
||||||
if [ -z "$_GARDENER_PR" ]; then
|
if [ -z "$_GARDENER_PR" ]; then
|
||||||
_GARDENER_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
_GARDENER_PR=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/pulls?state=open&limit=10" | \
|
"${FORGE_API}/pulls?state=open&limit=10" | \
|
||||||
jq -r '[.[] | select(.head.ref | startswith("chore/gardener-"))] | .[0].number // empty') || true
|
jq -r '[.[] | select(.head.ref | startswith("chore/gardener-"))] | .[0].number // empty') || true
|
||||||
fi
|
fi
|
||||||
if [ -z "$_GARDENER_PR" ]; then
|
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
|
# Get HEAD SHA from PR
|
||||||
local head_sha
|
local head_sha
|
||||||
head_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||||
|
|
||||||
if [ -z "$head_sha" ]; then
|
if [ -z "$head_sha" ]; then
|
||||||
log "WARNING: could not get HEAD SHA for PR #${_GARDENER_PR}"
|
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
|
fi
|
||||||
|
|
||||||
# Re-fetch HEAD in case Claude pushed new commits
|
# Re-fetch HEAD in case Claude pushed new commits
|
||||||
head_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||||
|
|
||||||
ci_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/commits/${head_sha}/status" | jq -r '.state // "unknown"') || ci_state="unknown"
|
"${FORGE_API}/commits/${head_sha}/status" | jq -r '.state // "unknown"') || ci_state="unknown"
|
||||||
|
|
||||||
case "$ci_state" in
|
case "$ci_state" in
|
||||||
success|failure|error) ci_done=true; break ;;
|
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
|
# Get error details
|
||||||
local pipeline_num ci_error_log
|
local pipeline_num ci_error_log
|
||||||
pipeline_num=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
pipeline_num=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/commits/${head_sha}/status" | \
|
"${FORGE_API}/commits/${head_sha}/status" | \
|
||||||
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
|
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
|
||||||
|
|
||||||
ci_error_log=""
|
ci_error_log=""
|
||||||
|
|
@ -518,10 +518,10 @@ _gardener_handle_review() {
|
||||||
|
|
||||||
# Check for review on current HEAD
|
# Check for review on current HEAD
|
||||||
local review_sha review_comment
|
local review_sha review_comment
|
||||||
review_sha=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
review_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
"${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}" \
|
jq -r --arg sha "${review_sha:-none}" \
|
||||||
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
'[.[] | 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)
|
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
|
if [ -z "$verdict" ]; then
|
||||||
verdict=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
verdict=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}/reviews" | \
|
"${FORGE_API}/pulls/${_GARDENER_PR}/reviews" | \
|
||||||
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
|
jq -r '[.[] | select(.stale == false)] | last | .state // empty' || true)
|
||||||
[ "$verdict" = "APPROVED" ] && verdict="APPROVE"
|
[ "$verdict" = "APPROVED" ] && verdict="APPROVE"
|
||||||
[[ "$verdict" != "REQUEST_CHANGES" && "$verdict" != "APPROVE" ]] && verdict=""
|
[[ "$verdict" != "REQUEST_CHANGES" && "$verdict" != "APPROVE" ]] && verdict=""
|
||||||
|
|
@ -576,8 +576,8 @@ Then stop and wait."
|
||||||
|
|
||||||
# Check if PR was merged or closed externally
|
# Check if PR was merged or closed externally
|
||||||
local pr_json pr_state pr_merged
|
local pr_json pr_state pr_merged
|
||||||
pr_json=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
pr_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}") || true
|
"${FORGE_API}/pulls/${_GARDENER_PR}") || true
|
||||||
pr_state=$(echo "$pr_json" | jq -r '.state // "unknown"')
|
pr_state=$(echo "$pr_json" | jq -r '.state // "unknown"')
|
||||||
pr_merged=$(echo "$pr_json" | jq -r '.merged // false')
|
pr_merged=$(echo "$pr_json" | jq -r '.merged // false')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ sourced as needed.
|
||||||
|
|
||||||
| File | What it provides | Sourced by |
|
| 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-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/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/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/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 |
|
| `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"
|
source "$(dirname "$0")/../lib/env.sh"
|
||||||
|
|
||||||
# WOODPECKER_TOKEN loaded from .env via 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="${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}"
|
||||||
|
|
||||||
api() {
|
api() {
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,19 @@ set -euo pipefail
|
||||||
|
|
||||||
# ensure_blocked_label_id — look up (or create) the "blocked" label, print its ID.
|
# 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.
|
# 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() {
|
ensure_blocked_label_id() {
|
||||||
if [ -n "${_BLOCKED_LABEL_ID:-}" ]; then
|
if [ -n "${_BLOCKED_LABEL_ID:-}" ]; then
|
||||||
printf '%s' "$_BLOCKED_LABEL_ID"
|
printf '%s' "$_BLOCKED_LABEL_ID"
|
||||||
return 0
|
return 0
|
||||||
fi
|
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)
|
| jq -r '.[] | select(.name == "blocked") | .id' 2>/dev/null || true)
|
||||||
if [ -z "$_BLOCKED_LABEL_ID" ]; then
|
if [ -z "$_BLOCKED_LABEL_ID" ]; then
|
||||||
_BLOCKED_LABEL_ID=$(curl -sf -X POST \
|
_BLOCKED_LABEL_ID=$(curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${CODEBERG_API}/labels" \
|
"${FORGE_API}/labels" \
|
||||||
-d '{"name":"blocked","color":"#e11d48"}' 2>/dev/null \
|
-d '{"name":"blocked","color":"#e11d48"}' 2>/dev/null \
|
||||||
| jq -r '.id // empty' 2>/dev/null || true)
|
| jq -r '.id // empty' 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
|
|
@ -29,19 +29,19 @@ ensure_blocked_label_id() {
|
||||||
|
|
||||||
# ensure_priority_label — look up (or create) the "priority" label, print its 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.
|
# 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() {
|
ensure_priority_label() {
|
||||||
if [ -n "${_PRIORITY_LABEL_ID:-}" ]; then
|
if [ -n "${_PRIORITY_LABEL_ID:-}" ]; then
|
||||||
printf '%s' "$_PRIORITY_LABEL_ID"
|
printf '%s' "$_PRIORITY_LABEL_ID"
|
||||||
return 0
|
return 0
|
||||||
fi
|
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)
|
| jq -r '.[] | select(.name == "priority") | .id' 2>/dev/null || true)
|
||||||
if [ -z "$_PRIORITY_LABEL_ID" ]; then
|
if [ -z "$_PRIORITY_LABEL_ID" ]; then
|
||||||
_PRIORITY_LABEL_ID=$(curl -sf -X POST \
|
_PRIORITY_LABEL_ID=$(curl -sf -X POST \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${CODEBERG_API}/labels" \
|
"${FORGE_API}/labels" \
|
||||||
-d '{"name":"priority","color":"#f59e0b"}' 2>/dev/null \
|
-d '{"name":"priority","color":"#f59e0b"}' 2>/dev/null \
|
||||||
| jq -r '.id // empty' 2>/dev/null || true)
|
| jq -r '.id // empty' 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
|
|
@ -68,7 +68,7 @@ diff_has_code_files() {
|
||||||
ci_required_for_pr() {
|
ci_required_for_pr() {
|
||||||
local pr_num="$1"
|
local pr_num="$1"
|
||||||
local files all_json
|
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
|
files=$(printf '%s' "$all_json" | jq -r '.[].filename' 2>/dev/null) || return 0
|
||||||
if [ -z "$files" ]; then
|
if [ -z "$files" ]; then
|
||||||
return 0 # empty file list — require CI as safety default
|
return 0 # empty file list — require CI as safety default
|
||||||
|
|
@ -113,7 +113,7 @@ ci_failed() {
|
||||||
is_infra_step() {
|
is_infra_step() {
|
||||||
local sname="$1" ecode="$2" log_data="${3:-}"
|
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
|
if { [[ "$sname" == *clone* ]] || [[ "$sname" == git* ]]; } && [ "$ecode" = "128" ]; then
|
||||||
echo "${sname} exit 128 (connection failure)"
|
echo "${sname} exit 128 (connection failure)"
|
||||||
return 0
|
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"
|
source "${FACTORY_ROOT}/lib/load-project.sh" "$PROJECT_TOML"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Codeberg token: env var > ~/.netrc
|
# Forge token: new FORGE_TOKEN > legacy CODEBERG_TOKEN > ~/.netrc
|
||||||
if [ -z "${CODEBERG_TOKEN:-}" ]; then
|
if [ -z "${FORGE_TOKEN:-}" ]; then
|
||||||
CODEBERG_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
|
FORGE_TOKEN="${CODEBERG_TOKEN:-}"
|
||||||
fi
|
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
|
# Review bot token: FORGE_REVIEW_TOKEN > legacy REVIEW_BOT_TOKEN
|
||||||
export CODEBERG_REPO="${CODEBERG_REPO:-}"
|
export FORGE_REVIEW_TOKEN="${FORGE_REVIEW_TOKEN:-${REVIEW_BOT_TOKEN:-}}"
|
||||||
export CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1/repos/${CODEBERG_REPO}}"
|
export REVIEW_BOT_TOKEN="${FORGE_REVIEW_TOKEN}" # backwards compat
|
||||||
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
|
|
||||||
export PROJECT_NAME="${PROJECT_NAME:-${CODEBERG_REPO##*/}}"
|
# 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 PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}"
|
||||||
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
|
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
|
||||||
export WOODPECKER_REPO_ID="${WOODPECKER_REPO_ID:-}"
|
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')" "$*"
|
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Codeberg API helper — usage: codeberg_api GET /issues?state=open
|
# Forge API helper — usage: forge_api GET /issues?state=open
|
||||||
codeberg_api() {
|
forge_api() {
|
||||||
local method="$1" path="$2"
|
local method="$1" path="$2"
|
||||||
shift 2
|
shift 2
|
||||||
curl -sf -X "$method" \
|
curl -sf -X "$method" \
|
||||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-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.
|
# Paginate a Forge API GET endpoint and return all items as a merged JSON array.
|
||||||
# Usage: codeberg_api_all /path (no existing query params)
|
# Usage: forge_api_all /path (no existing query params)
|
||||||
# codeberg_api_all /path?a=b (with existing params — appends &limit=50&page=N)
|
# forge_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)
|
# forge_api_all /path TOKEN (optional second arg: token; defaults to $FORGE_TOKEN)
|
||||||
codeberg_api_all() {
|
forge_api_all() {
|
||||||
local path_prefix="$1"
|
local path_prefix="$1"
|
||||||
local CODEBERG_TOKEN="${2:-${CODEBERG_TOKEN}}"
|
local FORGE_TOKEN="${2:-${FORGE_TOKEN}}"
|
||||||
local sep page page_items count all_items="[]"
|
local sep page page_items count all_items="[]"
|
||||||
case "$path_prefix" in
|
case "$path_prefix" in
|
||||||
*"?"*) sep="&" ;;
|
*"?"*) sep="&" ;;
|
||||||
|
|
@ -70,7 +88,7 @@ codeberg_api_all() {
|
||||||
esac
|
esac
|
||||||
page=1
|
page=1
|
||||||
while true; do
|
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=$(printf '%s' "$page_items" | jq 'length')
|
||||||
[ "$count" -eq 0 ] && break
|
[ "$count" -eq 0 ] && break
|
||||||
all_items=$(printf '%s\n%s' "$all_items" "$page_items" | jq -s 'add')
|
all_items=$(printf '%s\n%s' "$all_items" "$page_items" | jq -s 'add')
|
||||||
|
|
@ -79,6 +97,8 @@ codeberg_api_all() {
|
||||||
done
|
done
|
||||||
printf '%s' "$all_items"
|
printf '%s' "$all_items"
|
||||||
}
|
}
|
||||||
|
# Backwards-compat alias
|
||||||
|
codeberg_api_all() { forge_api_all "$@"; }
|
||||||
|
|
||||||
# Woodpecker API helper
|
# Woodpecker API helper
|
||||||
woodpecker_api() {
|
woodpecker_api() {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# file-action-issue.sh — File an action issue for a formula run
|
# file-action-issue.sh — File an action issue for a formula run
|
||||||
#
|
#
|
||||||
# Usage: source this file, then call file_action_issue.
|
# 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>
|
# file_action_issue <formula_name> <title> <body>
|
||||||
# Sets FILED_ISSUE_NUM on success.
|
# 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
|
# Dedup: skip if an open action issue for this formula already exists
|
||||||
local open_actions
|
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
|
if [ -n "$open_actions" ] && [ "$open_actions" != "null" ]; then
|
||||||
local existing
|
local existing
|
||||||
existing=$(printf '%s' "$open_actions" | \
|
existing=$(printf '%s' "$open_actions" | \
|
||||||
|
|
@ -36,7 +36,7 @@ file_action_issue() {
|
||||||
|
|
||||||
# Fetch 'action' label ID
|
# Fetch 'action' label ID
|
||||||
local 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)
|
jq -r '.[] | select(.name == "action") | .id' 2>/dev/null || true)
|
||||||
if [ -z "$action_label_id" ]; then
|
if [ -z "$action_label_id" ]; then
|
||||||
return 2
|
return 2
|
||||||
|
|
@ -50,7 +50,7 @@ file_action_issue() {
|
||||||
--argjson labels "[$action_label_id]" \
|
--argjson labels "[$action_label_id]" \
|
||||||
'{title: $title, body: $body, labels: $labels}')
|
'{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)
|
FILED_ISSUE_NUM=$(printf '%s' "$result" | jq -r '.number // empty' 2>/dev/null || true)
|
||||||
|
|
||||||
if [ -z "$FILED_ISSUE_NUM" ]; then
|
if [ -z "$FILED_ISSUE_NUM" ]; then
|
||||||
|
|
|
||||||
|
|
@ -208,21 +208,21 @@ read_scratch_context() {
|
||||||
# ── Prompt + monitor helpers ──────────────────────────────────────────────
|
# ── Prompt + monitor helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
# build_prompt_footer [EXTRA_API_LINES]
|
# 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.
|
# block for formula prompts. Sets PROMPT_FOOTER.
|
||||||
# Pass additional API endpoint lines (pre-formatted, newline-prefixed) via $1.
|
# 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.
|
# PRIMARY_BRANCH, PHASE_FILE.
|
||||||
build_prompt_footer() {
|
build_prompt_footer() {
|
||||||
local extra_api="${1:-}"
|
local extra_api="${1:-}"
|
||||||
# shellcheck disable=SC2034 # consumed by the calling script's PROMPT
|
# shellcheck disable=SC2034 # consumed by the calling script's PROMPT
|
||||||
PROMPT_FOOTER="## Codeberg API reference
|
PROMPT_FOOTER="## Forge API reference
|
||||||
Base URL: ${CODEBERG_API}
|
Base URL: ${FORGE_API}
|
||||||
Auth header: -H \"Authorization: token \$CODEBERG_TOKEN\"
|
Auth header: -H \"Authorization: token \${FORGE_TOKEN}\"
|
||||||
Read issue: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body'
|
Read issue: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_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}
|
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 \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels'
|
List labels: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/labels'
|
||||||
NEVER echo or include the actual token value in output — always reference \$CODEBERG_TOKEN.
|
NEVER echo or include the actual token value in output — always reference \${FORGE_TOKEN}.
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
FACTORY_ROOT=${FACTORY_ROOT}
|
FACTORY_ROOT=${FACTORY_ROOT}
|
||||||
|
|
@ -240,7 +240,7 @@ On unrecoverable error:
|
||||||
# run_formula_and_monitor AGENT_NAME [TIMEOUT]
|
# run_formula_and_monitor AGENT_NAME [TIMEOUT]
|
||||||
# Starts the formula session, injects PROMPT, monitors phase, and logs result.
|
# Starts the formula session, injects PROMPT, monitors phase, and logs result.
|
||||||
# Requires globals: SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT,
|
# 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
|
# shellcheck disable=SC2154 # SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT set by caller
|
||||||
run_formula_and_monitor() {
|
run_formula_and_monitor() {
|
||||||
local agent_name="$1"
|
local agent_name="$1"
|
||||||
|
|
@ -258,7 +258,7 @@ run_formula_and_monitor() {
|
||||||
|
|
||||||
agent_inject_into_session "$SESSION_NAME" "$PROMPT"
|
agent_inject_into_session "$SESSION_NAME" "$PROMPT"
|
||||||
log "Prompt sent to tmux session"
|
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}"
|
log "Monitoring phase file: ${PHASE_FILE}"
|
||||||
_FORMULA_CRASH_COUNT=0
|
_FORMULA_CRASH_COUNT=0
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
# Blocks:
|
# Blocks:
|
||||||
# - git push --force / -f to primary branch
|
# - git push --force / -f to primary branch
|
||||||
# - rm -rf targeting paths outside the worktree
|
# - 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)
|
# - Direct issue close calls (should go through phase protocol)
|
||||||
# - git checkout / git switch to primary branch (stay on feature branch)
|
# - git checkout / git switch to primary branch (stay on feature branch)
|
||||||
# - FACTORY_ROOT access from worktrees (formula agents exempted)
|
# - FACTORY_ROOT access from worktrees (formula agents exempted)
|
||||||
|
|
@ -88,7 +88,7 @@ if [ -n "$worktree_path" ] \
|
||||||
fi
|
fi
|
||||||
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
|
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'
|
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
|
exit 2
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@
|
||||||
# source lib/load-project.sh projects/harb.toml
|
# source lib/load-project.sh projects/harb.toml
|
||||||
#
|
#
|
||||||
# Exports:
|
# Exports:
|
||||||
# PROJECT_NAME, CODEBERG_REPO, CODEBERG_API, PROJECT_REPO_ROOT,
|
# PROJECT_NAME, FORGE_REPO, FORGE_API, FORGE_WEB, FORGE_URL,
|
||||||
# PRIMARY_BRANCH, WOODPECKER_REPO_ID, PROJECT_CONTAINERS,
|
# PROJECT_REPO_ROOT, PRIMARY_BRANCH, WOODPECKER_REPO_ID,
|
||||||
# CHECK_PRS, CHECK_DEV_AGENT, CHECK_PIPELINE_STALL, CI_STALE_MINUTES
|
# 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
|
# If no argument given, does nothing (allows poll scripts to work with
|
||||||
# plain .env fallback for backwards compatibility).
|
# plain .env fallback for backwards compatibility).
|
||||||
|
|
@ -35,7 +37,8 @@ def emit(key, val):
|
||||||
|
|
||||||
# Top-level
|
# Top-level
|
||||||
emit('PROJECT_NAME', cfg.get('name', ''))
|
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:
|
if 'repo_root' in cfg:
|
||||||
emit('PROJECT_REPO_ROOT', cfg['repo_root'])
|
emit('PROJECT_REPO_ROOT', cfg['repo_root'])
|
||||||
|
|
@ -79,11 +82,17 @@ while IFS='=' read -r _key _val; do
|
||||||
export "$_key=$_val"
|
export "$_key=$_val"
|
||||||
done <<< "$_PROJECT_VARS"
|
done <<< "$_PROJECT_VARS"
|
||||||
|
|
||||||
# Derive CODEBERG_API and CODEBERG_WEB if repo changed
|
# Derive FORGE_API and FORGE_WEB from forge_url + repo
|
||||||
if [ -n "$CODEBERG_REPO" ]; then
|
# FORGE_URL: TOML forge_url > existing FORGE_URL > default
|
||||||
export CODEBERG_API="https://codeberg.org/api/v1/repos/${CODEBERG_REPO}"
|
export FORGE_URL="${FORGE_URL:-http://localhost:3000}"
|
||||||
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
|
if [ -n "$FORGE_REPO" ]; then
|
||||||
|
export FORGE_API="${FORGE_URL}/api/v1/repos/${FORGE_REPO}"
|
||||||
|
export FORGE_WEB="${FORGE_URL}/${FORGE_REPO}"
|
||||||
fi
|
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
|
# Derive PROJECT_REPO_ROOT if not explicitly set
|
||||||
if [ -z "${PROJECT_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then
|
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)
|
# Git SHAs in typical git contexts (commit refs, not standalone secrets)
|
||||||
'commit [0-9a-f]{40}'
|
'commit [0-9a-f]{40}'
|
||||||
'Merge [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:]]+'
|
'codeberg\.org/[^[:space:]]+'
|
||||||
|
'localhost:3000/[^[:space:]]+'
|
||||||
# ShellCheck directive codes
|
# ShellCheck directive codes
|
||||||
'SC[0-9]{4}'
|
'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.
|
all milestones" pattern that produced premature work in planner v1/v2.
|
||||||
|
|
||||||
**Environment variables consumed**:
|
**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)
|
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to opus by planner-run.sh)
|
||||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,13 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
||||||
|
|
||||||
# ── Build prompt ─────────────────────────────────────────────────────────
|
# ── Build prompt ─────────────────────────────────────────────────────────
|
||||||
build_prompt_footer "
|
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]}'
|
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 \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'
|
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 \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"state\":\"closed\"}'
|
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
|
# 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
|
## Project context
|
||||||
${CONTEXT_BLOCK}${MEMORY_BLOCK}${JOURNAL_BLOCK}
|
${CONTEXT_BLOCK}${MEMORY_BLOCK}${JOURNAL_BLOCK}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ memory check (skips if available RAM < 2000 MB).
|
||||||
|
|
||||||
**Key files**:
|
**Key files**:
|
||||||
- `predictor/predictor-run.sh` — Cron wrapper + orchestrator: lock, memory guard,
|
- `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
|
reference, creates tmux session (sonnet), monitors phase file, handles crash
|
||||||
recovery via `run_formula_and_monitor`
|
recovery via `run_formula_and_monitor`
|
||||||
- `formulas/run-predictor.toml` — Execution spec: two steps (preflight,
|
- `formulas/run-predictor.toml` — Execution spec: two steps (preflight,
|
||||||
|
|
@ -37,7 +37,7 @@ memory check (skips if available RAM < 2000 MB).
|
||||||
interactive session
|
interactive session
|
||||||
|
|
||||||
**Environment variables consumed**:
|
**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)
|
- `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by predictor-run.sh)
|
||||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Notifications (optional)
|
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Notifications (optional)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
||||||
build_prompt_footer
|
build_prompt_footer
|
||||||
|
|
||||||
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
# 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
|
Your role: abstract adversary. Find the project's biggest weakness, challenge
|
||||||
planner claims, and generate evidence. Explore when uncertain (file a prediction),
|
planner claims, and generate evidence. Explore when uncertain (file a prediction),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
# projects/disinto.toml.example — Template for disinto self-management
|
# projects/disinto.toml.example — Template for disinto self-management
|
||||||
#
|
#
|
||||||
# Copy to projects/disinto.toml and fill in box-specific values,
|
# 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"
|
name = "disinto"
|
||||||
repo = "johba/disinto"
|
repo = "johba/disinto"
|
||||||
|
forge_url = "http://localhost:3000"
|
||||||
repo_root = "/home/YOU/dark-factory"
|
repo_root = "/home/YOU/dark-factory"
|
||||||
primary_branch = "main"
|
primary_branch = "main"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
# projects/harb.toml.example — Template for johba/harb
|
# projects/harb.toml.example — Template for johba/harb
|
||||||
#
|
#
|
||||||
# Copy to projects/harb.toml and fill in box-specific values,
|
# 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"
|
name = "harb"
|
||||||
repo = "johba/harb"
|
repo = "johba/harb"
|
||||||
|
forge_url = "http://localhost:3000"
|
||||||
repo_root = "/home/YOU/harb"
|
repo_root = "/home/YOU/harb"
|
||||||
primary_branch = "master"
|
primary_branch = "master"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
# projects/versi.toml.example — Template for johba/versi
|
# projects/versi.toml.example — Template for johba/versi
|
||||||
#
|
#
|
||||||
# Copy to projects/versi.toml and fill in box-specific values,
|
# 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"
|
name = "versi"
|
||||||
repo = "johba/versi"
|
repo = "johba/versi"
|
||||||
|
forge_url = "http://localhost:3000"
|
||||||
repo_root = "/home/YOU/versi"
|
repo_root = "/home/YOU/versi"
|
||||||
primary_branch = "main"
|
primary_branch = "main"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# Review Agent
|
# Review Agent
|
||||||
|
|
||||||
**Role**: AI-powered PR review — post structured findings and formal
|
**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
|
**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
|
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**:
|
**Key files**:
|
||||||
- `review/review-poll.sh` — Cron scheduler: finds unreviewed PRs with passing CI
|
- `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**:
|
**Environment variables consumed**:
|
||||||
- `CODEBERG_TOKEN` — Dev-agent token (must not be the same account as REVIEW_BOT_TOKEN)
|
- `FORGE_TOKEN` — Dev-agent token (must not be the same account as FORGE_REVIEW_TOKEN)
|
||||||
- `REVIEW_BOT_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist)
|
- `FORGE_REVIEW_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_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||||
- `PRIMARY_BRANCH`, `WOODPECKER_REPO_ID`
|
- `PRIMARY_BRANCH`, `WOODPECKER_REPO_ID`
|
||||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ source "$(dirname "$0")/../lib/ci-helpers.sh"
|
||||||
REPO_ROOT="${PROJECT_REPO_ROOT}"
|
REPO_ROOT="${PROJECT_REPO_ROOT}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
API_BASE="${CODEBERG_API}"
|
API_BASE="${FORGE_API}"
|
||||||
LOGFILE="$SCRIPT_DIR/review.log"
|
LOGFILE="$SCRIPT_DIR/review.log"
|
||||||
MAX_REVIEWS=3
|
MAX_REVIEWS=3
|
||||||
REVIEW_IDLE_TIMEOUT=14400 # 4h: kill review session if idle
|
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"
|
phase_file="/tmp/review-session-${PROJECT_NAME}-${pr_num}.phase"
|
||||||
|
|
||||||
# Check if PR is still open
|
# 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
|
"${API_BASE}/pulls/${pr_num}" | jq -r '.state // "unknown"' 2>/dev/null) || true
|
||||||
|
|
||||||
if [ "$pr_state" != "open" ]; then
|
if [ "$pr_state" != "open" ]; then
|
||||||
|
|
@ -92,7 +92,7 @@ if [ -n "$REVIEW_SESSIONS" ]; then
|
||||||
done <<< "$REVIEW_SESSIONS"
|
done <<< "$REVIEW_SESSIONS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${API_BASE}/pulls?state=open&limit=20" | \
|
"${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)"')
|
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
|
[ "${current_phase}" = "PHASE:awaiting_review" ] || return 0
|
||||||
|
|
||||||
local review_comment
|
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}" \
|
jq -r --arg sha "${pr_sha}" \
|
||||||
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
||||||
if [ -z "${review_comment}" ] || [ "${review_comment}" = "null" ]; then
|
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)
|
reviewed_sha=$(sed -n 's/^SHA://p' "$phase_file" 2>/dev/null | tr -d '[:space:]' || true)
|
||||||
[ -n "$reviewed_sha" ] || continue
|
[ -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)
|
"${API_BASE}/pulls/${pr_num}" 2>/dev/null || true)
|
||||||
[ -n "$pr_json" ] || continue
|
[ -n "$pr_json" ] || continue
|
||||||
|
|
||||||
|
|
@ -196,7 +196,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
||||||
pr_branch=$(printf '%s' "$pr_json" | jq -r '.head.ref // ""')
|
pr_branch=$(printf '%s' "$pr_json" | jq -r '.head.ref // ""')
|
||||||
if [ -z "$current_sha" ] || [ "$current_sha" = "$reviewed_sha" ]; then continue; fi
|
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"')
|
"${API_BASE}/commits/${current_sha}/status" | jq -r '.state // "unknown"')
|
||||||
|
|
||||||
if ! ci_passed "$ci_state"; then
|
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
|
if "${SCRIPT_DIR}/review-pr.sh" "$pr_num" 2>&1; then
|
||||||
REVIEWED=$((REVIEWED + 1))
|
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
|
"${API_BASE}/pulls/${pr_num}" | jq -r '.head.sha // ""') || true
|
||||||
inject_review_into_dev_session "$pr_num" "${FRESH_SHA:-$current_sha}" "$pr_branch"
|
inject_review_into_dev_session "$pr_num" "${FRESH_SHA:-$current_sha}" "$pr_branch"
|
||||||
else
|
else
|
||||||
|
|
@ -227,7 +227,7 @@ while IFS= read -r line; do
|
||||||
PR_SHA=$(echo "$line" | awk '{print $2}')
|
PR_SHA=$(echo "$line" | awk '{print $2}')
|
||||||
PR_BRANCH=$(echo "$line" | awk '{print $3}')
|
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"')
|
"${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
|
# 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"
|
log " #${PR_NUM} CI=${CI_STATE} but no code files — proceeding"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check formal Codeberg reviews (not comment markers)
|
# Check formal forge reviews (not comment markers)
|
||||||
HAS_REVIEW=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
HAS_REVIEW=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${API_BASE}/pulls/${PR_NUM}/reviews" | \
|
"${API_BASE}/pulls/${PR_NUM}/reviews" | \
|
||||||
jq -r --arg sha "$PR_SHA" \
|
jq -r --arg sha "$PR_SHA" \
|
||||||
'[.[] | select(.commit_id == $sha) | select(.state != "COMMENT")] | length')
|
'[.[] | 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
|
# 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
|
# 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.
|
# 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
|
"${API_BASE}/pulls/${PR_NUM}" | jq -r '.head.sha // ""') || true
|
||||||
inject_review_into_dev_session "$PR_NUM" "${FRESH_SHA:-$PR_SHA}" "$PR_BRANCH"
|
inject_review_into_dev_session "$PR_NUM" "${FRESH_SHA:-$PR_SHA}" "$PR_BRANCH"
|
||||||
else
|
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]}"
|
PR_NUMBER="${1:?Usage: review-pr.sh <pr-number> [--force]}"
|
||||||
FORCE="${2:-}"
|
FORCE="${2:-}"
|
||||||
API="${CODEBERG_API}"
|
API="${FORGE_API}"
|
||||||
LOGFILE="${FACTORY_ROOT}/review/review.log"
|
LOGFILE="${FACTORY_ROOT}/review/review.log"
|
||||||
SESSION="review-${PROJECT_NAME}-${PR_NUMBER}"
|
SESSION="review-${PROJECT_NAME}-${PR_NUMBER}"
|
||||||
PHASE_FILE="/tmp/review-session-${PROJECT_NAME}-${PR_NUMBER}.phase"
|
PHASE_FILE="/tmp/review-session-${PROJECT_NAME}-${PR_NUMBER}.phase"
|
||||||
|
|
@ -37,7 +37,7 @@ if [ -f "$LOCKFILE" ]; then
|
||||||
fi
|
fi
|
||||||
echo $$ > "$LOCKFILE"
|
echo $$ > "$LOCKFILE"
|
||||||
status "fetching metadata"
|
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_TITLE=$(printf '%s' "$PR_JSON" | jq -r '.title')
|
||||||
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')
|
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')
|
||||||
PR_HEAD=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
|
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
|
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
|
rm -rf "$WORKTREE" "$PHASE_FILE" "$OUTPUT_FILE" 2>/dev/null || true; exit 0
|
||||||
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"')
|
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
|
||||||
CI_NOTE=""; if ! ci_passed "$CI_STATE"; then
|
CI_NOTE=""; if ! ci_passed "$CI_STATE"; then
|
||||||
ci_required_for_pr "$PR_NUMBER" && { log "SKIP: CI=${CI_STATE}"; exit 0; }
|
ci_required_for_pr "$PR_NUMBER" && { log "SKIP: CI=${CI_STATE}"; exit 0; }
|
||||||
CI_NOTE=" (not required — non-code PR)"; fi
|
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" \
|
HAS_CMT=$(printf '%s' "$ALL_COMMENTS" | jq --arg s "$PR_SHA" \
|
||||||
'[.[]|select(.body|contains("<!-- reviewed: "+$s+" -->"))]|length')
|
'[.[]|select(.body|contains("<!-- reviewed: "+$s+" -->"))]|length')
|
||||||
[ "${HAS_CMT:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: reviewed ${PR_SHA:0:7}"; exit 0; }
|
[ "${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')
|
'[.[]|select(.commit_id==$s)|select(.state!="COMMENT")]|length')
|
||||||
[ "${HAS_FML:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: formal review"; exit 0; }
|
[ "${HAS_FML:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: formal review"; exit 0; }
|
||||||
PREV_CONTEXT="" IS_RE_REVIEW=false PREV_SHA=""
|
PREV_CONTEXT="" IS_RE_REVIEW=false PREV_SHA=""
|
||||||
|
|
@ -81,7 +81,7 @@ if [ -n "$PREV_REV" ] && [ "$PREV_REV" != "null" ]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
status "fetching diff"
|
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"
|
"${API}/pulls/${PR_NUMBER}.diff" > "${REVIEW_TMPDIR}/full.diff"
|
||||||
FSIZE=$(stat -c%s "${REVIEW_TMPDIR}/full.diff" 2>/dev/null || echo 0)
|
FSIZE=$(stat -c%s "${REVIEW_TMPDIR}/full.diff" 2>/dev/null || echo 0)
|
||||||
DIFF=$(head -c "$MAX_DIFF" "${REVIEW_TMPDIR}/full.diff")
|
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")
|
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' \
|
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' \
|
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"
|
"$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' \
|
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"
|
"$PR_BODY" "$FILES" "$DNOTE" "$DIFF"
|
||||||
[ -n "$PREV_CONTEXT" ] && printf '%s\n' "$PREV_CONTEXT"
|
[ -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"
|
"$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"
|
} > "${REVIEW_TMPDIR}/prompt.md"
|
||||||
PROMPT=$(cat "${REVIEW_TMPDIR}/prompt.md")
|
PROMPT=$(cat "${REVIEW_TMPDIR}/prompt.md")
|
||||||
|
|
||||||
|
|
@ -142,7 +142,7 @@ fi
|
||||||
if [ -z "$REVIEW_JSON" ]; then
|
if [ -z "$REVIEW_JSON" ]; then
|
||||||
log "ERROR: no valid review output"
|
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}*" \
|
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
|
-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
|
matrix_send "review" "PR #${PR_NUMBER} review failed" 2>/dev/null || true; exit 1
|
||||||
fi
|
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"
|
printf '%s' "$COMMENT_BODY" > "${REVIEW_TMPDIR}/body.txt"
|
||||||
jq -Rs '{body: .}' < "${REVIEW_TMPDIR}/body.txt" > "${REVIEW_TMPDIR}/comment.json"
|
jq -Rs '{body: .}' < "${REVIEW_TMPDIR}/body.txt" > "${REVIEW_TMPDIR}/comment.json"
|
||||||
POST_RC=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
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")
|
"${API}/issues/${PR_NUMBER}/comments" --data-binary @"${REVIEW_TMPDIR}/comment.json")
|
||||||
[ "$POST_RC" != "201" ] && { log "ERROR: comment HTTP ${POST_RC}"; exit 1; }
|
[ "$POST_RC" != "201" ] && { log "ERROR: comment HTTP ${POST_RC}"; exit 1; }
|
||||||
log "posted review comment"
|
log "posted review comment"
|
||||||
|
|
@ -171,19 +171,19 @@ log "posted review comment"
|
||||||
REVENT="COMMENT"
|
REVENT="COMMENT"
|
||||||
case "$VERDICT" in APPROVE) REVENT="APPROVED" ;; REQUEST_CHANGES|DISCUSS) REVENT="REQUEST_CHANGES" ;; esac
|
case "$VERDICT" in APPROVE) REVENT="APPROVED" ;; REQUEST_CHANGES|DISCUSS) REVENT="REQUEST_CHANGES" ;; esac
|
||||||
if [ "$REVENT" = "APPROVED" ]; then
|
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)
|
"${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' | \
|
jq -r --arg l "$BLOGIN" '.[]|select(.state=="REQUEST_CHANGES")|select(.user.login==$l)|.id' | \
|
||||||
while IFS= read -r rid; do
|
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" \
|
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews/${rid}/dismissals" \
|
||||||
-d '{"message":"Superseded by approval"}' || true; log "dismissed review ${rid}"
|
-d '{"message":"Superseded by approval"}' || true; log "dismissed review ${rid}"
|
||||||
done || true
|
done || true
|
||||||
fi
|
fi
|
||||||
jq -n --arg b "AI ${RTYPE}: **${VERDICT}** — ${REASON}" --arg e "$REVENT" --arg s "$PR_SHA" \
|
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"
|
'{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" \
|
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews" \
|
||||||
--data-binary @"${REVIEW_TMPDIR}/formal.json" >/dev/null 2>&1 || true
|
--data-binary @"${REVIEW_TMPDIR}/formal.json" >/dev/null 2>&1 || true
|
||||||
log "formal ${REVENT} submitted"
|
log "formal ${REVENT} submitted"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# collect-metrics.sh — Collect factory metrics and write JSON for the dashboard
|
# 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
|
# counts vault decisions, and checks CI pass rates. Writes a JSON snapshot
|
||||||
# to the live site directory so the dashboard can fetch it.
|
# 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=$(grep '^repo ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//')
|
||||||
repo_name=$(grep '^name ' "$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)
|
# PRs merged (all time via state=closed + merged marker)
|
||||||
local prs_merged_week=0 prs_merged_month=0 prs_merged_total=0
|
local prs_merged_week=0 prs_merged_month=0 prs_merged_total=0
|
||||||
local closed_prs
|
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 "[]")
|
"${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)
|
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
|
# Issues closed
|
||||||
local issues_closed_week=0 issues_closed_month=0
|
local issues_closed_week=0 issues_closed_month=0
|
||||||
local closed_issues
|
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 "[]")
|
"${api_base}/issues?state=closed&sort=updated&type=issues&limit=50" 2>/dev/null || echo "[]")
|
||||||
|
|
||||||
if [ -n "$WEEK_AGO" ]; then
|
if [ -n "$WEEK_AGO" ]; then
|
||||||
|
|
@ -82,19 +85,19 @@ collect_project_metrics() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local total_closed_header
|
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")
|
"${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}"
|
local issues_closed_total="${total_closed_header:-0}"
|
||||||
|
|
||||||
# Open issues by label
|
# Open issues by label
|
||||||
local backlog_count in_progress_count blocked_count
|
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 | \
|
"${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")
|
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 | \
|
"${api_base}/issues?state=open&labels=in-progress&type=issues&limit=50" 2>/dev/null | \
|
||||||
jq 'length' 2>/dev/null || echo 0)
|
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 | \
|
"${api_base}/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null | \
|
||||||
jq 'length' 2>/dev/null || echo 0)
|
jq 'length' 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div>Data refreshed every 6 hours by the metrics collector.</div>
|
<div>Data refreshed every 6 hours by the metrics collector.</div>
|
||||||
<div style="margin-top:0.5rem">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -421,7 +421,7 @@
|
||||||
var card = el('div', 'project');
|
var card = el('div', 'project');
|
||||||
var nameDiv = el('div', 'name');
|
var nameDiv = el('div', 'name');
|
||||||
var nameLink = document.createElement('a');
|
var nameLink = document.createElement('a');
|
||||||
nameLink.href = 'https://codeberg.org/' + p.repo;
|
nameLink.href = 'http://localhost:3000/' + p.repo;
|
||||||
nameLink.textContent = p.name;
|
nameLink.textContent = p.name;
|
||||||
nameDiv.appendChild(nameLink);
|
nameDiv.appendChild(nameLink);
|
||||||
card.appendChild(nameDiv);
|
card.appendChild(nameDiv);
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,7 @@
|
||||||
<!-- Eight Agents -->
|
<!-- Eight Agents -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Eight agents</h2>
|
<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="agents-grid">
|
||||||
<div class="agent-card">
|
<div class="agent-card">
|
||||||
<div class="name">dev-agent</div>
|
<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>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>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>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>
|
<p><strong>Single VPS</strong> — runs on an 8 GB server. Flat cost, no scaling surprises.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -538,7 +538,7 @@ disinto/
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="/">← disinto.ai</a> ·
|
<a href="/">← disinto.ai</a> ·
|
||||||
<a href="/docs/quickstart">Quickstart</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -328,8 +328,8 @@
|
||||||
<div class="label">Prerequisites</div>
|
<div class="label">Prerequisites</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>A VPS or server</strong> — 8 GB RAM minimum (Ubuntu/Debian recommended)</li>
|
<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 forge instance</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 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>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>An Anthropic API key</strong> — with the <code>claude</code> CLI installed and authenticated</li>
|
||||||
<li><strong>tmux</strong> — for persistent dev sessions</li>
|
<li><strong>tmux</strong> — for persistent dev sessions</li>
|
||||||
|
|
@ -343,13 +343,13 @@
|
||||||
Clone the factory
|
Clone the factory
|
||||||
</div>
|
</div>
|
||||||
<p>Clone disinto onto your server. This is the factory — the code that runs your agents.</p>
|
<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
|
cd ~/disinto
|
||||||
cp .env.example .env</code></pre>
|
cp .env.example .env</code></pre>
|
||||||
<p>Edit <code>.env</code> with your tokens:</p>
|
<p>Edit <code>.env</code> with your tokens:</p>
|
||||||
<pre><code><span class="comment"># Required</span>
|
<pre><code><span class="comment"># Required</span>
|
||||||
CODEBERG_TOKEN=your_codeberg_token
|
FORGE_TOKEN=your_codeberg_token
|
||||||
REVIEW_BOT_TOKEN=your_review_bot_token
|
FORGE_REVIEW_TOKEN=your_review_bot_token
|
||||||
|
|
||||||
<span class="comment"># Woodpecker CI</span>
|
<span class="comment"># Woodpecker CI</span>
|
||||||
WOODPECKER_TOKEN=your_woodpecker_token
|
WOODPECKER_TOKEN=your_woodpecker_token
|
||||||
|
|
@ -365,14 +365,14 @@ CLAUDE_TIMEOUT=7200</code></pre>
|
||||||
<span class="step-num">2</span>
|
<span class="step-num">2</span>
|
||||||
Initialize your project
|
Initialize your project
|
||||||
</div>
|
</div>
|
||||||
<p><code>disinto init</code> sets up everything: clones the repo, creates the project config, adds Codeberg labels, and installs cron jobs.</p>
|
<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 https://codeberg.org/you/your-project</code></pre>
|
<pre><code>bin/disinto init http://localhost:3000/you/your-project</code></pre>
|
||||||
<div class="expected">
|
<div class="expected">
|
||||||
<div class="label">Expected output</div>
|
<div class="label">Expected output</div>
|
||||||
<code>=== disinto init ===
|
<code>=== disinto init ===
|
||||||
Project: you/your-project
|
Project: you/your-project
|
||||||
Name: 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
|
Branch: main
|
||||||
Created: /home/you/disinto/projects/your-project.toml
|
Created: /home/you/disinto/projects/your-project.toml
|
||||||
Creating labels on you/your-project...
|
Creating labels on you/your-project...
|
||||||
|
|
@ -406,7 +406,7 @@ Done. Project your-project is ready.</code>
|
||||||
<ol>
|
<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 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>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>
|
</ol>
|
||||||
<pre><code><span class="comment"># Create CLAUDE.md in your project</span>
|
<pre><code><span class="comment"># Create CLAUDE.md in your project</span>
|
||||||
cat > ~/your-project/CLAUDE.md <<'EOF'
|
cat > ~/your-project/CLAUDE.md <<'EOF'
|
||||||
|
|
@ -434,7 +434,7 @@ git push</code></pre>
|
||||||
<span class="step-num">4</span>
|
<span class="step-num">4</span>
|
||||||
File your first issue
|
File your first issue
|
||||||
</div>
|
</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>
|
<pre><code><span class="comment"># Title: Add health check endpoint</span>
|
||||||
<span class="comment"># Label: backlog</span>
|
<span class="comment"># Label: backlog</span>
|
||||||
<span class="comment"># Body:</span>
|
<span class="comment"># Body:</span>
|
||||||
|
|
@ -523,7 +523,7 @@ git log --oneline -5</code></pre>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="/">← disinto.ai</a> ·
|
<a href="/">← disinto.ai</a> ·
|
||||||
<a href="/docs/architecture">Architecture</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -661,7 +661,7 @@
|
||||||
<p>
|
<p>
|
||||||
<strong>Bash scripts and Claude.</strong> No Kubernetes, no microservices,
|
<strong>Bash scripts and Claude.</strong> No Kubernetes, no microservices,
|
||||||
no SaaS dependencies. Runs on an 8GB VPS.
|
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.
|
<strong>Woodpecker CI</strong> pipeline and it starts building.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -691,8 +691,8 @@
|
||||||
<div class="cta-links">
|
<div class="cta-links">
|
||||||
<a href="/docs/quickstart">Quickstart</a>
|
<a href="/docs/quickstart">Quickstart</a>
|
||||||
<a href="/docs/architecture">Architecture</a>
|
<a href="/docs/architecture">Architecture</a>
|
||||||
<a href="https://codeberg.org/johba/disinto">Browse the source</a>
|
<a href="http://localhost:3000/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/issues">See active issues</a>
|
||||||
<a href="/dashboard">Live dashboard</a>
|
<a href="/dashboard">Live dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -702,7 +702,7 @@
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="/docs/quickstart">quickstart</a>
|
<a href="/docs/quickstart">quickstart</a>
|
||||||
<a href="/docs/architecture">architecture</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>
|
<a href="/dashboard">dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="under-hood">
|
<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.
|
which `supervisor-run.sh` consumes atomically on each run.
|
||||||
|
|
||||||
**Environment variables consumed**:
|
**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)
|
- `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
|
- `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
|
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Matrix notifications + human input
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Supervisor Agent
|
# 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.
|
`supervisor-poll.sh` detected an issue it couldn't auto-fix.
|
||||||
|
|
||||||
## Priority Order
|
## Priority Order
|
||||||
|
|
@ -19,7 +19,7 @@ Before acting, read the relevant best-practices file:
|
||||||
- Memory issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/memory.md`
|
- Memory issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/memory.md`
|
||||||
- Disk issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/disk.md`
|
- Disk issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/disk.md`
|
||||||
- CI issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/ci.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`
|
- Dev-agent issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/dev-agent.md`
|
||||||
- Review-agent issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/review-agent.md`
|
- Review-agent issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/review-agent.md`
|
||||||
- Git issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/git.md`
|
- Git issues → `cat ${FACTORY_ROOT}/supervisor/best-practices/git.md`
|
||||||
|
|
@ -32,10 +32,10 @@ source ${FACTORY_ROOT}/lib/env.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This gives you:
|
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)
|
- `wpdb -c "SELECT ..."` — Woodpecker Postgres (uses $WOODPECKER_DB_PASSWORD)
|
||||||
- `woodpecker_api "/repos/$WOODPECKER_REPO_ID/pipelines"` — Woodpecker REST API (uses $WOODPECKER_TOKEN)
|
- `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_REPO_ROOT` — path to the target project repo
|
||||||
- `$PROJECT_NAME` — short project name (for worktree prefixes, container names)
|
- `$PROJECT_NAME` — short project name (for worktree prefixes, container names)
|
||||||
- `$PRIMARY_BRANCH` — main branch (master or main)
|
- `$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
|
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.
|
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
|
2. Read the referenced source files in `$PROJECT_REPO_ROOT` to understand which change
|
||||||
actually depends on which
|
actually depends on which
|
||||||
3. Edit the issue that has the incorrect dep to remove the `#NNN` reference from its
|
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)
|
`## 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
|
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
|
```bash
|
||||||
# Read current body
|
# 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)
|
# Edit (remove the circular ref, keep other deps)
|
||||||
NEW_BODY=$(echo "$BODY" | sed 's/- #XXX/- None/')
|
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)
|
### Stale dependencies (P3)
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,13 @@
|
||||||
- Modifying pipeline configs in `.woodpecker/` directory
|
- Modifying pipeline configs in `.woodpecker/` directory
|
||||||
|
|
||||||
## Known Issues
|
## 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.
|
- `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.
|
- 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`.
|
- CI images take hours to rebuild. Never run `docker system prune -a`.
|
||||||
|
|
||||||
## Lessons Learned
|
## 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.
|
- 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.
|
- `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`
|
- Clean worktree: `cd $PROJECT_REPO_ROOT && git worktree remove /tmp/${PROJECT_NAME}-worktree-<N> --force`
|
||||||
- Remove `in-progress` label if agent died without cleanup:
|
- Remove `in-progress` label if agent died without cleanup:
|
||||||
```bash
|
```bash
|
||||||
codeberg_api DELETE "/issues/<N>/labels/in-progress"
|
forge_api DELETE "/issues/<N>/labels/in-progress"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dangerous (escalate)
|
## Dangerous (escalate)
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
**Trust closed state.** If a dependency issue is closed, the code is on the primary branch. Period.
|
**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:
|
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
|
- PRs don't always mention the issue number in title/body
|
||||||
- Searching last N closed PRs misses older merges
|
- Searching last N closed PRs misses older merges
|
||||||
- The dev-agent closes issues after merging, so closed = merged
|
- 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.
|
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
|
### 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)
|
### 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.
|
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.
|
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
|
### 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.
|
**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"
|
source /home/debian/dark-factory/lib/load-project.sh "$PROJECT_TOML"
|
||||||
PHASE_FILE="/tmp/dev-session-${PROJECT_NAME}-<ISSUE>.phase"
|
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)
|
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_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"
|
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
|
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
|
## 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
|
- Woodpecker `git` step fails with exit code 128
|
||||||
- Multiple pipelines fail in quick succession with the same error
|
- Multiple pipelines fail in quick succession with the same error
|
||||||
- Retriggers make it WORSE by adding more clone attempts
|
- 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).
|
- 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.
|
- If >3 pipelines are pending/running, do NOT create more work.
|
||||||
|
|
||||||
## OAuth Tokens
|
## API Tokens
|
||||||
- OAuth tokens expire ~2h. If Codeberg is down during refresh, re-login required.
|
- API token is in `.env` as `FORGE_TOKEN` — loaded via env.sh.
|
||||||
- API token is in `~/.netrc` — read via `awk` in env.sh.
|
- Review bot has a separate token (`$FORGE_REVIEW_TOKEN`) for formal reviews.
|
||||||
- Review bot has a separate token ($REVIEW_BOT_TOKEN) for formal reviews.
|
- With local Forgejo, tokens don't expire. For remote forges, check provider docs.
|
||||||
|
|
||||||
## Lessons Learned
|
## Lessons Learned
|
||||||
- Retrigger storm on 2026-03-12: supervisor + dev-agent both retriggered during rate limit, caused 5+ failed pipelines. Added cooldown awareness.
|
- 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
|
## Architecture
|
||||||
- `review-poll.sh` (cron */10) → finds open PRs with CI pass + no review → spawns `review-pr.sh`
|
- `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
|
- `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)
|
- Skips WIP/draft PRs (`[WIP]` in title or draft flag)
|
||||||
|
|
||||||
## Safe Fixes
|
## Safe Fixes
|
||||||
|
|
@ -27,4 +27,4 @@
|
||||||
- Review bot must output JSON — prevents self-narration collapse
|
- Review bot must output JSON — prevents self-narration collapse
|
||||||
- DISCUSS verdict should be treated same as REQUEST_CHANGES by dev-agent
|
- DISCUSS verdict should be treated same as REQUEST_CHANGES by dev-agent
|
||||||
- Error comments must NOT include `<!-- reviewed: SHA -->` — would falsely mark as reviewed
|
- 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 ──────────────────────────────────────────────────────────────
|
# ── Open PRs ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
echo "## Open PRs (${PROJECT_NAME})"
|
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 "$_open_prs" | jq -r '.[] | "#\(.number) [\(.head.ref)] \(.title) — updated \(.updated_at)"' 2>/dev/null || echo "No open PRs or query failed"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Backlog + In-Progress ─────────────────────────────────────────────────
|
# ── Backlog + In-Progress ─────────────────────────────────────────────────
|
||||||
|
|
||||||
echo "## Issue Status (${PROJECT_NAME})"
|
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 "?")
|
_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=$(codeberg_api GET "/issues?state=open&labels=in-progress&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=$(codeberg_api GET "/issues?state=open&labels=blocked&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 "Backlog: ${_backlog_count}, In-progress: ${_in_progress_count}, Blocked: ${_blocked_count}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ echo ""
|
||||||
# ── Blocked Issues ────────────────────────────────────────────────────────
|
# ── Blocked Issues ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
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)
|
_blocked_n=$(echo "$_blocked_issues" | jq 'length' 2>/dev/null || echo 0)
|
||||||
if [ "${_blocked_n:-0}" -gt 0 ]; then
|
if [ "${_blocked_n:-0}" -gt 0 ]; then
|
||||||
echo "$_blocked_issues" | jq -r '.[] | " #\(.number): \(.title)"' 2>/dev/null || echo " (query failed)"
|
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"
|
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"
|
# Usage: codeberg_count_paginated "/issues?state=open&labels=backlog&type=issues"
|
||||||
# Returns total count across all pages (max 20 pages = 1000 items).
|
# Returns total count across all pages (max 20 pages = 1000 items).
|
||||||
codeberg_count_paginated() {
|
codeberg_count_paginated() {
|
||||||
local endpoint="$1" total=0 page=1 count
|
local endpoint="$1" total=0 page=1 count
|
||||||
while true; do
|
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}))
|
total=$((total + ${count:-0}))
|
||||||
[ "${count:-0}" -lt 50 ] && break
|
[ "${count:-0}" -lt 50 ] && break
|
||||||
page=$((page + 1))
|
page=$((page + 1))
|
||||||
|
|
@ -244,7 +244,7 @@ mkdir -p "$_RETRY_DIR"
|
||||||
# Function: run all per-project checks for the currently loaded project config
|
# Function: run all per-project checks for the currently loaded project config
|
||||||
check_project() {
|
check_project() {
|
||||||
local proj_name="${PROJECT_NAME:-unknown}"
|
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
|
# P2: FACTORY STOPPED — CI, dev-agent, git
|
||||||
|
|
@ -366,8 +366,8 @@ check_project() {
|
||||||
if [ "${CHECK_PIPELINE_STALL:-true}" = "true" ]; then
|
if [ "${CHECK_PIPELINE_STALL:-true}" = "true" ]; then
|
||||||
status "P2: ${proj_name}: checking pipeline stall"
|
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")
|
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=$(codeberg_api GET "/issues?state=open&labels=in-progress&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
|
if [ "${BACKLOG_COUNT:-0}" -gt 0 ] && [ "${IN_PROGRESS:-0}" -eq 0 ]; then
|
||||||
DEV_LOG="${FACTORY_ROOT}/dev/dev-agent.log"
|
DEV_LOG="${FACTORY_ROOT}/dev/dev-agent.log"
|
||||||
|
|
@ -408,14 +408,14 @@ check_project() {
|
||||||
if [ "${CHECK_PRS:-true}" = "true" ]; then
|
if [ "${CHECK_PRS:-true}" = "true" ]; then
|
||||||
status "P3: ${proj_name}: checking PRs"
|
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
|
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
|
[ -z "$PR_JSON" ] && continue
|
||||||
PR_SHA=$(echo "$PR_JSON" | jq -r '.head.sha // ""')
|
PR_SHA=$(echo "$PR_JSON" | jq -r '.head.sha // ""')
|
||||||
[ -z "$PR_SHA" ] && continue
|
[ -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')
|
MERGEABLE=$(echo "$PR_JSON" | jq -r '.mergeable // true')
|
||||||
if [ "$MERGEABLE" = "false" ] && ci_passed "$CI_STATE"; then
|
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"
|
[ "$AGE_MIN" -gt 30 ] && p3 "${proj_name}: PR #${pr}: CI=${CI_STATE}, stale ${AGE_MIN}min"
|
||||||
fi
|
fi
|
||||||
elif ci_passed "$CI_STATE"; then
|
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")
|
jq -r --arg sha "$PR_SHA" '[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | length' 2>/dev/null || echo "0")
|
||||||
|
|
||||||
if [ "${HAS_REVIEW:-0}" -eq 0 ]; then
|
if [ "${HAS_REVIEW:-0}" -eq 0 ]; then
|
||||||
|
|
@ -454,7 +454,7 @@ check_project() {
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
status "P3: ${proj_name}: checking for circular dependencies"
|
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
|
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"
|
PARSE_DEPS="${FACTORY_ROOT}/lib/parse-deps.sh"
|
||||||
|
|
@ -524,7 +524,7 @@ check_project() {
|
||||||
if [ -n "${DEP_CACHE[$dep]+x}" ]; then
|
if [ -n "${DEP_CACHE[$dep]+x}" ]; then
|
||||||
DEP_INFO="${DEP_CACHE[$dep]}"
|
DEP_INFO="${DEP_CACHE[$dep]}"
|
||||||
else
|
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
|
[ -z "$DEP_JSON" ] && continue
|
||||||
DEP_STATE=$(echo "$DEP_JSON" | jq -r '.state // "unknown"')
|
DEP_STATE=$(echo "$DEP_JSON" | jq -r '.state // "unknown"')
|
||||||
DEP_CREATED=$(echo "$DEP_JSON" | jq -r '.created_at // ""')
|
DEP_CREATED=$(echo "$DEP_JSON" | jq -r '.created_at // ""')
|
||||||
|
|
@ -646,8 +646,8 @@ Instructions:
|
||||||
_sess_issue="${_sess#dev-"${proj_name}"-}"
|
_sess_issue="${_sess#dev-"${proj_name}"-}"
|
||||||
[[ "$_sess_issue" =~ ^[0-9]+$ ]] || continue
|
[[ "$_sess_issue" =~ ^[0-9]+$ ]] || continue
|
||||||
|
|
||||||
# Check Codeberg: is the issue still open?
|
# Check forge: is the issue still open?
|
||||||
_issue_state=$(codeberg_api GET "/issues/${_sess_issue}" 2>/dev/null \
|
_issue_state=$(forge_api GET "/issues/${_sess_issue}" 2>/dev/null \
|
||||||
| jq -r '.state // "open"' 2>/dev/null || echo "open")
|
| jq -r '.state // "open"' 2>/dev/null || echo "open")
|
||||||
|
|
||||||
_should_cleanup=false
|
_should_cleanup=false
|
||||||
|
|
@ -671,7 +671,7 @@ Instructions:
|
||||||
_has_open_pr=0
|
_has_open_pr=0
|
||||||
_pr_page=1
|
_pr_page=1
|
||||||
while true; do
|
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 "[]")
|
2>/dev/null || echo "[]")
|
||||||
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
|
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
|
||||||
_pr_match=$(printf '%s' "$_pr_page_json" | \
|
_pr_match=$(printf '%s' "$_pr_page_json" | \
|
||||||
|
|
@ -689,7 +689,7 @@ Instructions:
|
||||||
_has_closed_pr=0
|
_has_closed_pr=0
|
||||||
_pr_page=1
|
_pr_page=1
|
||||||
while true; do
|
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 "[]")
|
2>/dev/null || echo "[]")
|
||||||
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
|
_pr_page_len=$(printf '%s' "$_pr_page_json" | jq 'length' 2>/dev/null || echo 0)
|
||||||
_pr_match=$(printf '%s' "$_pr_page_json" | \
|
_pr_match=$(printf '%s' "$_pr_page_json" | \
|
||||||
|
|
@ -771,7 +771,7 @@ if [ -d "$PROJECTS_DIR" ]; then
|
||||||
[ -f "$project_toml" ] || continue
|
[ -f "$project_toml" ] || continue
|
||||||
PROJECT_COUNT=$((PROJECT_COUNT + 1))
|
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"
|
source "${FACTORY_ROOT}/lib/load-project.sh" "$project_toml"
|
||||||
|
|
||||||
check_project || flog "check_project failed for ${project_toml} (per-project checks incomplete)"
|
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
|
build_prompt_footer
|
||||||
|
|
||||||
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
# 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.
|
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.
|
Fix what you can. Escalate what you cannot. Do NOT ask permission — act first, report after.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Vault Agent
|
# 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
|
`vault-poll.sh` because one or more actions in `vault/pending/` need
|
||||||
classification and routing.
|
classification and routing.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue