mirror of
https://github.com/terraphim/gitea-infrastructure.git
synced 2026-07-16 00:00:32 +02:00
Two complementary articles: - ARTICLE-agent-task-management.md: CTO voice, ~2800 words, narrative style - ARTICLE-agent-task-consolidation.md: Technical reference, ~6000 words, full configs Both cover: beads (Go) vs br (Rust) vs Agent Mail vs Gitea with 12 labels, 1Password integration, SeaweedFS S3 storage, Docker Compose deployment, and the three API gotchas (label swap, assignees, dependencies). All commands verified against live Gitea 1.22.6. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1290 lines
45 KiB
Markdown
1290 lines
45 KiB
Markdown
# From Three Agent Tools to One: Replacing beads, br, and Agent Mail with Gitea
|
|
|
|
**Technical Architecture Report**
|
|
**Date**: 2026-02-18
|
|
**Target System**: Gitea 1.22.6 on git.terraphim.cloud
|
|
**Verified**: All commands tested against live instance
|
|
|
|
---
|
|
|
|
## 1. Introduction
|
|
|
|
AI agent systems need task tracking, but installing and maintaining multiple specialized tools creates operational overhead. This article documents the consolidation of three agent task management tools into a single Gitea-based solution with verified working commands and configurations.
|
|
|
|
The migration eliminates three separate systems:
|
|
- **beads (bd)**: Go-based CLI task tracker with problematic auto-commit behavior
|
|
- **beads_rust (br)**: Rust rewrite that fixed the auto-commit issue but remained local-only
|
|
- **mcp-agent-mail**: Multi-agent coordination server for cross-agent communication
|
|
|
|
The replacement is a Gitea 1.22.6 instance with REST API integration, milestone-based project tracking, and scoped workflow labels. All configuration uses 1Password for secret management.
|
|
|
|
---
|
|
|
|
## 2. The Three Incumbents
|
|
|
|
### 2.1 beads (Go, "bd")
|
|
|
|
**Architecture**: CLI issue tracker using SQLite + JSONL storage in `.beads/` directory, committed to git as source of truth.
|
|
|
|
**Commands**:
|
|
```bash
|
|
bd ready # List ready tasks
|
|
bd create # Create new task
|
|
bd update ID # Modify task
|
|
bd close ID # Complete task
|
|
bd sync # Sync to JSONL and commit to git
|
|
bd list # List tasks
|
|
bd dep add # Add dependency
|
|
bd stats # Show project statistics
|
|
```
|
|
|
|
**Task properties**:
|
|
- Priority: P0 (critical) through P4 (low)
|
|
- Type: bug, feature, task, epic, chore
|
|
- Status: draft, ready, in_progress, blocked, done
|
|
- Dependencies: Task can depend on other tasks
|
|
|
|
**Critical flaw**: `bd sync` executed git commands automatically. The synchronization step would:
|
|
1. Export task state to `.beads/tasks.jsonl`
|
|
2. Run `git add .beads/`
|
|
3. Run `git commit -m "sync beads"`
|
|
4. Optionally push to remote
|
|
|
|
This created race conditions when AI agents made rapid commits. Two agents running `bd sync` simultaneously could corrupt the git history or create merge conflicts.
|
|
|
|
**Additional features (later removed)**:
|
|
- Daemon mode for background synchronization
|
|
- RPC mode for multi-process access
|
|
- Git hooks for automatic sync on commit
|
|
- Priority-based task sorting with graph analysis
|
|
|
|
All of these features were removed in the Rust rewrite due to complexity and reliability concerns.
|
|
|
|
### 2.2 beads_rust (br, v0.1.13)
|
|
|
|
**Architecture**: Same concept as beads, rewritten in Rust with a critical behavioral change: br never executes git commands.
|
|
|
|
**Commands** (subset of original beads):
|
|
```bash
|
|
br ready # List ready tasks
|
|
br create "title" # Create task (simpler syntax)
|
|
br update ID # Modify task
|
|
br close ID # Complete task
|
|
br sync --flush-only # Export to JSONL only (no git)
|
|
br list # List tasks
|
|
br dep add # Add dependency
|
|
br stats # Show statistics
|
|
```
|
|
|
|
**Key behavioral change**: The `br sync --flush-only` command only exports state to `.beads/tasks.jsonl`. The user must manually run:
|
|
```bash
|
|
git add .beads/
|
|
git commit -m "update tasks"
|
|
git push
|
|
```
|
|
|
|
This eliminated race conditions but introduced a new problem: agents would forget to commit, leading to divergent state across machines.
|
|
|
|
**Architecture decision**: "Non-invasive by default." The tool manages task state but delegates all version control to the user.
|
|
|
|
**Limitations**:
|
|
- Local-first design: each repository has its own `.beads/` directory
|
|
- No cross-repository visibility (can't see tasks from other repos)
|
|
- No web UI (CLI only)
|
|
- Requires `br` binary installation on every machine
|
|
- No centralized dashboard for monitoring agent progress
|
|
|
|
**What was removed from beads (Go)**:
|
|
- Daemon mode (process management complexity)
|
|
- RPC server (network protocol overhead)
|
|
- Auto-commit on sync (source of race conditions)
|
|
- Git hooks (implicit behavior caused confusion)
|
|
|
|
The Rust rewrite prioritized reliability and simplicity over features.
|
|
|
|
### 2.3 MCP Agent Mail (mcp-agent-mail)
|
|
|
|
**Architecture**: MCP server providing multi-agent coordination through a message-passing abstraction.
|
|
|
|
**Core concepts**:
|
|
- **Identities**: Each agent has a unique identity (e.g., "agent-researcher", "agent-coder")
|
|
- **Inbox/Outbox**: Messages organized by recipient
|
|
- **Threads**: Conversations grouped by thread ID (convention: `br-123` for task 123)
|
|
- **File reservations**: Advisory locks to prevent concurrent file edits
|
|
|
|
**Key operations**:
|
|
```bash
|
|
# Send message
|
|
mcp__agent_mail__send_message(to="agent-coder", subject="Review needed", body="...", thread="br-42")
|
|
|
|
# Read inbox
|
|
mcp__agent_mail__get_inbox(agent="agent-coder", limit=10)
|
|
|
|
# Search threads
|
|
mcp__agent_mail__search_threads(query="authentication", agent="agent-coder")
|
|
|
|
# Reserve file (advisory lock)
|
|
mcp__agent_mail__reserve_file(path="/src/auth.rs", agent="agent-coder", duration=3600)
|
|
|
|
# Release file reservation
|
|
mcp__agent_mail__release_file(path="/src/auth.rs", agent="agent-coder")
|
|
```
|
|
|
|
**Macro helpers** (common workflows):
|
|
```bash
|
|
# Start agent work session
|
|
macro_start_session(agent="agent-coder")
|
|
# Returns: unread message count, active file reservations
|
|
|
|
# Prepare to work on a task thread
|
|
macro_prepare_thread(agent="agent-coder", thread="br-42")
|
|
# Returns: thread messages, related file reservations
|
|
|
|
# Check-reserve-work-release cycle
|
|
macro_file_reservation_cycle(agent="agent-coder", file="/src/auth.rs", thread="br-42")
|
|
# Checks for existing reservation, reserves if free, returns status
|
|
```
|
|
|
|
**Product bus pattern**: Central coordination repository where agents post status updates and coordinate work across multiple code repositories.
|
|
|
|
**Limitations**:
|
|
- Additional server process to run and monitor
|
|
- Separate state store (not integrated with version control)
|
|
- Coordination overhead (agents must check inbox before acting)
|
|
- No built-in web UI for human visibility
|
|
- Thread ID convention (br-123) tightly coupled to beads/br task IDs
|
|
|
|
**Why it existed**: br's local-first design couldn't coordinate work across multiple repositories. Agent Mail provided the missing cross-repo coordination layer.
|
|
|
|
---
|
|
|
|
## 3. The Gitea Alternative
|
|
|
|
### 3.1 Instance Details
|
|
|
|
**URL**: https://git.terraphim.cloud
|
|
**Version**: Gitea 1.22.6
|
|
**Database**: PostgreSQL 15
|
|
**Storage**: SeaweedFS (S3-compatible)
|
|
|
|
### 3.2 Critical API Discovery
|
|
|
|
During implementation, we discovered that **Gitea 1.22.6 has NO project board REST API**.
|
|
|
|
Attempted request:
|
|
```bash
|
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
|
"https://git.terraphim.cloud/api/v1/user/projects"
|
|
# Returns: HTTP 404
|
|
```
|
|
|
|
The Gitea UI has project boards, but they are not exposed via REST API in version 1.22.6. This required a design pivot: **milestones serve as project containers** instead.
|
|
|
|
Milestone API works and provides:
|
|
- Title, description, due date
|
|
- Progress tracking: `open_issues` and `closed_issues` counts
|
|
- State: open or closed
|
|
- Full CRUD operations via REST API
|
|
|
|
### 3.3 Workflow State Machine via 12 Scoped Labels
|
|
|
|
Labels implement the task lifecycle. Each label has a scope prefix (e.g., `workflow:`, `priority:`, `type:`) and the `exclusive: true` flag, making labels within the same scope mutually exclusive in the Gitea UI.
|
|
|
|
**Workflow labels** (lifecycle states):
|
|
|
|
| Label | Color | Hex | Scope | Exclusive |
|
|
|-------|-------|-----|-------|-----------|
|
|
| workflow:ready | Green | #0e8a16 | workflow | true |
|
|
| workflow:in-progress | Yellow | #fbca04 | workflow | true |
|
|
| workflow:blocked | Red | #d93f0b | workflow | true |
|
|
| workflow:done | Purple | #6f42c1 | workflow | true |
|
|
|
|
**Priority labels** (urgency):
|
|
|
|
| Label | Color | Hex | Scope | Exclusive |
|
|
|-------|-------|-----|-------|-----------|
|
|
| priority:critical | Dark red | #b60205 | priority | true |
|
|
| priority:high | Red | #d93f0b | priority | true |
|
|
| priority:medium | Yellow | #fbca04 | priority | true |
|
|
| priority:low | Green | #0e8a16 | priority | true |
|
|
|
|
**Type labels** (task categorization):
|
|
|
|
| Label | Color | Hex | Scope | Exclusive |
|
|
|-------|-------|-----|-------|-----------|
|
|
| type:feature | Blue | #1d76db | type | true |
|
|
| type:bug | Red | #d93f0b | type | true |
|
|
| type:task | Violet | #5319e7 | type | true |
|
|
| type:research | Teal | #006b75 | type | true |
|
|
|
|
**Label setup script** (verified working):
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# setup-labels.sh
|
|
set -euo pipefail
|
|
|
|
GITEA_URL="${GITEA_URL:-https://git.terraphim.cloud}"
|
|
GITEA_OWNER="${1:-${GITEA_OWNER:-terraphim}}"
|
|
GITEA_REPO="${2:-${GITEA_REPO:-agent-tasks}}"
|
|
|
|
gitea_api() {
|
|
local method="$1" endpoint="$2" data="${3:-}"
|
|
local args=(-s -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json")
|
|
if [ -n "$data" ]; then
|
|
args+=(-X "$method" -d "$data")
|
|
else
|
|
args+=(-X "$method")
|
|
fi
|
|
curl "${args[@]}" "$GITEA_URL/api/v1$endpoint"
|
|
}
|
|
|
|
ensure_label() {
|
|
local name="$1" color="$2" description="$3" exclusive="${4:-false}"
|
|
local existing
|
|
existing=$(gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/labels" \
|
|
| jq -r --arg n "$name" '.[] | select(.name == $n) | .id')
|
|
|
|
if [ -n "$existing" ]; then
|
|
echo "{\"name\": \"$name\", \"id\": $existing, \"action\": \"exists\"}"
|
|
return 0
|
|
fi
|
|
|
|
local payload
|
|
payload=$(jq -n \
|
|
--arg name "$name" \
|
|
--arg color "$color" \
|
|
--arg desc "$description" \
|
|
--argjson excl "$exclusive" \
|
|
'{name: $name, color: $color, description: $desc, exclusive: $excl}')
|
|
|
|
local result
|
|
result=$(gitea_api POST "/repos/$GITEA_OWNER/$GITEA_REPO/labels" "$payload")
|
|
local label_id
|
|
label_id=$(echo "$result" | jq -r '.id // empty')
|
|
|
|
if [ -n "$label_id" ]; then
|
|
echo "{\"name\": \"$name\", \"id\": $label_id, \"action\": \"created\"}"
|
|
else
|
|
echo "{\"name\": \"$name\", \"action\": \"error\", \"detail\": $result}" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Workflow labels
|
|
ensure_label "workflow:ready" "0e8a16" "Task is ready for an agent to pick up" true
|
|
ensure_label "workflow:in-progress" "fbca04" "Task is actively being worked on" true
|
|
ensure_label "workflow:blocked" "d93f0b" "Task is blocked by a dependency" true
|
|
ensure_label "workflow:done" "6f42c1" "Task is complete" true
|
|
|
|
# Priority labels
|
|
ensure_label "priority:critical" "b60205" "Must be done immediately" true
|
|
ensure_label "priority:high" "d93f0b" "Should be done this cycle" true
|
|
ensure_label "priority:medium" "fbca04" "Normal priority" true
|
|
ensure_label "priority:low" "0e8a16" "Nice to have" true
|
|
|
|
# Type labels
|
|
ensure_label "type:feature" "1d76db" "New functionality" true
|
|
ensure_label "type:bug" "d93f0b" "Defect fix" true
|
|
ensure_label "type:task" "5319e7" "General task" true
|
|
ensure_label "type:research" "006b75" "Investigation or spike" true
|
|
```
|
|
|
|
Run with:
|
|
```bash
|
|
source ~/op_zesticai_non_prod.sh
|
|
export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
|
|
./setup-labels.sh
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 1Password Integration
|
|
|
|
All secrets managed via 1Password. Never hardcode tokens.
|
|
|
|
### 4.1 1Password Items (TerraphimPlatform vault)
|
|
|
|
| Item | Purpose | Fields |
|
|
|------|---------|--------|
|
|
| git.terraphim.cloud-admin-token | Gitea API authentication | credential (token string) |
|
|
| gitea-postgres | PostgreSQL database | password |
|
|
| gitea-s3 | SeaweedFS S3 credentials | access-key, secret-key |
|
|
|
|
### 4.2 Token Retrieval (Verified Working)
|
|
|
|
```bash
|
|
# Load 1Password service account
|
|
source ~/op_zesticai_non_prod.sh
|
|
|
|
# Read Gitea admin token
|
|
export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
|
|
|
|
# Verify authentication (VERIFIED 2026-02-18)
|
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
|
"https://git.terraphim.cloud/api/v1/user" | jq '{login, is_admin}'
|
|
# Returns: {"login": "root", "is_admin": true}
|
|
```
|
|
|
|
### 4.3 Secret Injection for Docker Compose
|
|
|
|
Docker Compose template uses 1Password references:
|
|
|
|
```bash
|
|
# Inject secrets from 1Password into docker-compose.yml
|
|
op inject --in-file docker-compose.yml.template --out-file docker-compose.yml
|
|
|
|
# Start services with injected secrets
|
|
docker compose up -d
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Gitea Infrastructure with SeaweedFS
|
|
|
|
### 5.1 Architecture Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────┐
|
|
│ Internet (HTTPS) │
|
|
└────────────────┬────────────────────────────────┘
|
|
│
|
|
v
|
|
┌─────────────────────────────────────────────────┐
|
|
│ Caddy Reverse Proxy │
|
|
│ - TLS termination │
|
|
│ - git.terraphim.cloud -> localhost:3000 │
|
|
└────────────────┬────────────────────────────────┘
|
|
│
|
|
v
|
|
┌─────────────────────────────────────────────────┐
|
|
│ Gitea 1.22.6 (Container: gitea) │
|
|
│ - HTTP: 127.0.0.1:3000 │
|
|
│ - SSH: 0.0.0.0:222 │
|
|
│ - Database: PostgreSQL 15 │
|
|
│ - Storage: SeaweedFS S3 │
|
|
└────────────────┬────────────────────────────────┘
|
|
│
|
|
┌────────┴────────┐
|
|
│ │
|
|
v v
|
|
┌──────────────┐ ┌─────────────────────────────┐
|
|
│ PostgreSQL │ │ SeaweedFS │
|
|
│ (Container) │ │ - master (9333) │
|
|
│ Port: 5432 │ │ - volume (8080) │
|
|
│ (internal) │ │ - filer (8888) │
|
|
└──────────────┘ │ - s3 (8333, Tailscale) │
|
|
└─────────────────────────────┘
|
|
```
|
|
|
|
### 5.2 Docker Compose Template (docker-compose.yml.template)
|
|
|
|
**Critical**: This file contains `op://` references and must be processed with `op inject` before deployment.
|
|
|
|
```yaml
|
|
version: "3"
|
|
|
|
networks:
|
|
gitea:
|
|
external: false
|
|
|
|
services:
|
|
server:
|
|
image: gitea/gitea:1.22.6
|
|
container_name: gitea
|
|
environment:
|
|
- USER_UID=1000
|
|
- USER_GID=1000
|
|
# Public URL settings
|
|
- GITEA__server__DOMAIN=git.terraphim.cloud
|
|
- GITEA__server__ROOT_URL=https://git.terraphim.cloud/
|
|
- GITEA__server__PROTOCOL=http
|
|
- GITEA__server__HTTP_PORT=3000
|
|
- GITEA__server__DISABLE_SSH=false
|
|
- GITEA__server__SSH_PORT=22
|
|
- GITEA__server__SSH_LISTEN_PORT=22
|
|
# Database
|
|
- GITEA__database__DB_TYPE=postgres
|
|
- GITEA__database__HOST=db:5432
|
|
- GITEA__database__NAME=gitea
|
|
- GITEA__database__USER=gitea
|
|
- GITEA__database__PASSWD=op://TerraphimPlatform/gitea-postgres/password
|
|
# Actions
|
|
- GITEA__actions__ENABLE=true
|
|
# LFS
|
|
- GITEA__repository__LFS_START_SERVER=true
|
|
- GITEA__repository__LFS_CONTENT_PATH=/data/lfs
|
|
# S3 storage (general)
|
|
- GITEA__storage__type=minio
|
|
- GITEA__storage__MINIO_ACCESS_KEY_ID=op://TerraphimPlatform/gitea-s3/access-key
|
|
- GITEA__storage__MINIO_SECRET_ACCESS_KEY=op://TerraphimPlatform/gitea-s3/secret-key
|
|
- GITEA__storage__MINIO_BUCKET=gitea
|
|
- GITEA__storage__MINIO_LOCATION=us-east-1
|
|
- GITEA__storage__MINIO_ENDPOINT=http://s3storage:8333
|
|
- GITEA__storage__MINIO_INSECURE_SKIP_VERIFY=false
|
|
- GITEA__storage__MINIO_USE_SSL=false
|
|
- GITEA__storage__SERVE_DIRECT=true
|
|
# S3 storage for attachments
|
|
- GITEA__attachment__STORAGE_TYPE=minio
|
|
- GITEA__attachment__MINIO_ENDPOINT=s3storage:8333
|
|
- GITEA__attachment__MINIO_ACCESS_KEY_ID=op://TerraphimPlatform/gitea-s3/access-key
|
|
- GITEA__attachment__MINIO_SECRET_ACCESS_KEY=op://TerraphimPlatform/gitea-s3/secret-key
|
|
- GITEA__attachment__MINIO_BUCKET=gitea
|
|
- GITEA__attachment__MINIO_USE_SSL=false
|
|
# S3 storage for avatars/pictures
|
|
- GITEA__picture__STORAGE_TYPE=minio
|
|
- GITEA__picture__MINIO_ENDPOINT=s3storage:8333
|
|
- GITEA__picture__MINIO_ACCESS_KEY_ID=op://TerraphimPlatform/gitea-s3/access-key
|
|
- GITEA__picture__MINIO_SECRET_ACCESS_KEY=op://TerraphimPlatform/gitea-s3/secret-key
|
|
- GITEA__picture__MINIO_BUCKET=gitea
|
|
- GITEA__picture__MINIO_USE_SSL=false
|
|
# S3 storage for LFS
|
|
- GITEA__lfs__STORAGE_TYPE=minio
|
|
- GITEA__lfs__MINIO_ENDPOINT=s3storage:8333
|
|
- GITEA__lfs__MINIO_ACCESS_KEY_ID=op://TerraphimPlatform/gitea-s3/access-key
|
|
- GITEA__lfs__MINIO_SECRET_ACCESS_KEY=op://TerraphimPlatform/gitea-s3/secret-key
|
|
- GITEA__lfs__MINIO_BUCKET=gitea-lfs
|
|
- GITEA__lfs__MINIO_USE_SSL=false
|
|
restart: always
|
|
networks:
|
|
- gitea
|
|
volumes:
|
|
- ./gitea:/data
|
|
- /etc/timezone:/etc/timezone:ro
|
|
- /etc/localtime:/etc/localtime:ro
|
|
ports:
|
|
- "127.0.0.1:3000:3000" # HTTP (Caddy reverse proxy only)
|
|
- "222:22" # SSH (public)
|
|
depends_on:
|
|
- db
|
|
- s3
|
|
|
|
db:
|
|
image: postgres:15
|
|
restart: always
|
|
environment:
|
|
- POSTGRES_USER=gitea
|
|
- POSTGRES_PASSWORD=op://TerraphimPlatform/gitea-postgres/password
|
|
- POSTGRES_DB=gitea
|
|
networks:
|
|
- gitea
|
|
volumes:
|
|
- ./postgres:/var/lib/postgresql/data
|
|
|
|
master:
|
|
image: chrislusf/seaweedfs
|
|
command: "master -ip=master -ip.bind=0.0.0.0 -metricsPort=9324"
|
|
networks:
|
|
- gitea
|
|
|
|
volume:
|
|
image: chrislusf/seaweedfs
|
|
command: 'volume -mserver="master:9333" -ip.bind=0.0.0.0 -port=8080 -metricsPort=9325'
|
|
depends_on:
|
|
- master
|
|
volumes:
|
|
- ./seaweedfs:/data
|
|
networks:
|
|
- gitea
|
|
|
|
filer:
|
|
image: chrislusf/seaweedfs
|
|
command: 'filer -master="master:9333" -ip.bind=0.0.0.0 -metricsPort=9326'
|
|
tty: true
|
|
stdin_open: true
|
|
depends_on:
|
|
- master
|
|
- volume
|
|
volumes:
|
|
- ./seaweedfs_filter:/data
|
|
networks:
|
|
- gitea
|
|
|
|
s3:
|
|
image: chrislusf/seaweedfs
|
|
container_name: s3storage
|
|
hostname: s3storage
|
|
ports:
|
|
# Bind to Tailscale interface only (not public internet)
|
|
- "100.106.66.7:8333:8333"
|
|
- "100.106.66.7:9327:9327"
|
|
command: 's3 -config=/etc/seaweedfs/s3.json -filer="filer:8888" -ip.bind=0.0.0.0 -metricsPort=9327'
|
|
depends_on:
|
|
- master
|
|
- volume
|
|
- filer
|
|
volumes:
|
|
- ./s3_config.json:/etc/seaweedfs/s3.json
|
|
networks:
|
|
- gitea
|
|
|
|
prometheus:
|
|
image: prom/prometheus:v2.21.0
|
|
ports:
|
|
# Bind to Tailscale interface only
|
|
- "100.106.66.7:9000:9090"
|
|
volumes:
|
|
- ./prometheus:/etc/prometheus
|
|
command: --web.enable-lifecycle --config.file=/etc/prometheus/prometheus.yml
|
|
depends_on:
|
|
- s3
|
|
networks:
|
|
- gitea
|
|
```
|
|
|
|
### 5.3 SeaweedFS S3 Configuration (s3_config.json.template)
|
|
|
|
```json
|
|
{
|
|
"identities": [
|
|
{
|
|
"name": "anonymous",
|
|
"actions": ["Admin", "Read", "List", "Write", "Tagging"]
|
|
},
|
|
{
|
|
"name": "gitea",
|
|
"credentials": [
|
|
{
|
|
"accessKey": "op://TerraphimPlatform/gitea-s3/access-key",
|
|
"secretKey": "op://TerraphimPlatform/gitea-s3/secret-key"
|
|
}
|
|
],
|
|
"actions": ["Admin", "Read", "List", "Tagging", "Write"]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Process with:
|
|
```bash
|
|
op inject --in-file s3_config.json.template --out-file s3_config.json
|
|
```
|
|
|
|
### 5.4 Caddy Configuration
|
|
|
|
File: `/etc/caddy/Caddyfile`
|
|
|
|
```
|
|
git.terraphim.cloud {
|
|
reverse_proxy localhost:3000
|
|
log {
|
|
output file /var/log/caddy/gitea-access.log
|
|
format json
|
|
}
|
|
}
|
|
```
|
|
|
|
Reload Caddy:
|
|
```bash
|
|
sudo systemctl reload caddy
|
|
```
|
|
|
|
### 5.5 Deployment Steps
|
|
|
|
```bash
|
|
# 1. Navigate to infrastructure directory
|
|
cd /path/to/gitea-infrastructure
|
|
|
|
# 2. Load 1Password service account
|
|
source ~/op_zesticai_non_prod.sh
|
|
|
|
# 3. Inject secrets into configuration files
|
|
op inject --in-file docker-compose.yml.template --out-file docker-compose.yml
|
|
op inject --in-file s3_config.json.template --out-file s3_config.json
|
|
|
|
# 4. Start services
|
|
docker compose up -d
|
|
|
|
# 5. Verify services
|
|
docker compose ps
|
|
# Expected: gitea, db, master, volume, filer, s3storage, prometheus all "Up"
|
|
|
|
# 6. Verify Gitea responds
|
|
curl -s https://git.terraphim.cloud/api/v1/version | jq
|
|
# Expected: {"version": "1.22.6"}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Agent Skill: Shell Helpers
|
|
|
|
All helpers output JSON for agent parsing. All use 1Password for token retrieval.
|
|
|
|
### 6.1 Base API Helper
|
|
|
|
```bash
|
|
GITEA_URL="${GITEA_URL:-https://git.terraphim.cloud}"
|
|
GITEA_OWNER="${GITEA_OWNER:-terraphim}"
|
|
GITEA_REPO="${GITEA_REPO:-agent-tasks}"
|
|
|
|
gitea_api() {
|
|
local method="$1" endpoint="$2" data="${3:-}"
|
|
local args=(-s -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json")
|
|
if [ -n "$data" ]; then
|
|
args+=(-X "$method" -d "$data")
|
|
else
|
|
args+=(-X "$method")
|
|
fi
|
|
curl "${args[@]}" "$GITEA_URL/api/v1$endpoint"
|
|
}
|
|
```
|
|
|
|
### 6.2 Label Swap Pattern (CRITICAL)
|
|
|
|
**Problem**: The Gitea UI enforces scoped label exclusivity (only one `workflow:*` label allowed per issue). But the API `POST /repos/{owner}/{repo}/issues/{index}/labels` endpoint just **appends** labels. If you POST `workflow:in-progress` to an issue that already has `workflow:ready`, you'll end up with BOTH labels, violating exclusivity.
|
|
|
|
**Solution**: Use PUT with full label replacement. Filter out all labels from the target scope, then add the new label.
|
|
|
|
```bash
|
|
gitea_swap_workflow() {
|
|
local index="$1" new_label="$2" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
local repo_labels issue_labels new_id kept_ids all_ids
|
|
|
|
# Get ID of the new workflow label
|
|
repo_labels=$(gitea_api GET "/repos/$owner/$repo/labels")
|
|
new_id=$(echo "$repo_labels" | jq -r --arg n "$new_label" '.[] | select(.name==$n) | .id')
|
|
|
|
# Get current issue labels, filter out all workflow:* labels
|
|
issue_labels=$(gitea_api GET "/repos/$owner/$repo/issues/$index")
|
|
kept_ids=$(echo "$issue_labels" | jq '[.labels[] | select(.name | startswith("workflow:") | not) | .id]')
|
|
|
|
# Combine kept labels + new workflow label
|
|
all_ids=$(echo "$kept_ids" | jq --argjson nid "$new_id" '. + [$nid]')
|
|
|
|
# Replace all labels atomically (PUT, not POST)
|
|
gitea_api PUT "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": $all_ids}"
|
|
}
|
|
```
|
|
|
|
### 6.3 Workflow Helpers
|
|
|
|
**Mark task ready**:
|
|
```bash
|
|
gitea_ready() {
|
|
local index="$1" owner="${2:-$GITEA_OWNER}" repo="${3:-$GITEA_REPO}"
|
|
gitea_swap_workflow "$index" "workflow:ready" "$owner" "$repo"
|
|
}
|
|
```
|
|
|
|
**Claim task** (mark in-progress, assign to user):
|
|
```bash
|
|
gitea_claim() {
|
|
local index="$1" user="${2:-}" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
gitea_swap_workflow "$index" "workflow:in-progress" "$owner" "$repo"
|
|
if [ -n "$user" ]; then
|
|
gitea_api PATCH "/repos/$owner/$repo/issues/$index" "{\"assignees\": [\"$user\"]}"
|
|
fi
|
|
}
|
|
```
|
|
|
|
**Close task**:
|
|
```bash
|
|
gitea_close_task() {
|
|
local index="$1" comment="${2:-}" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
gitea_swap_workflow "$index" "workflow:done" "$owner" "$repo"
|
|
gitea_api PATCH "/repos/$owner/$repo/issues/$index" '{"state": "closed"}'
|
|
if [ -n "$comment" ]; then
|
|
gitea_api POST "/repos/$owner/$repo/issues/$index/comments" \
|
|
"$(jq -n --arg b "$comment" '{body: $b}')"
|
|
fi
|
|
}
|
|
```
|
|
|
|
### 6.4 Project Management Helpers
|
|
|
|
**Create project** (milestone):
|
|
```bash
|
|
gitea_create_project() {
|
|
local title="$1" desc="${2:-}" due="${3:-}"
|
|
local owner="${4:-$GITEA_OWNER}" repo="${5:-$GITEA_REPO}"
|
|
local data
|
|
data=$(jq -n --arg t "$title" --arg d "$desc" --arg due "$due" \
|
|
'{title: $t, description: $d} + (if $due != "" then {due_on: $due} else {} end)')
|
|
gitea_api POST "/repos/$owner/$repo/milestones" "$data"
|
|
}
|
|
|
|
# Example:
|
|
gitea_create_project "Sprint 2026-W08" "Weekly sprint tasks" "2026-02-28T00:00:00Z"
|
|
```
|
|
|
|
**Get project progress**:
|
|
```bash
|
|
gitea_progress() {
|
|
local milestone="${1:-}" owner="${2:-$GITEA_OWNER}" repo="${3:-$GITEA_REPO}"
|
|
if [ -n "$milestone" ]; then
|
|
gitea_api GET "/repos/$owner/$repo/milestones/$milestone" | jq '{
|
|
milestone: .title,
|
|
open: .open_issues,
|
|
closed: .closed_issues,
|
|
total: (.open_issues + .closed_issues),
|
|
percent_complete: (if (.open_issues + .closed_issues) > 0
|
|
then (.closed_issues * 100 / (.open_issues + .closed_issues))
|
|
else 0 end),
|
|
due_on: .due_on,
|
|
state: .state
|
|
}'
|
|
else
|
|
gitea_api GET "/repos/$owner/$repo/milestones?state=open" | \
|
|
jq '[.[] | {id, title, open: .open_issues, closed: .closed_issues,
|
|
pct: (if (.open_issues + .closed_issues) > 0
|
|
then (.closed_issues * 100 / (.open_issues + .closed_issues))
|
|
else 0 end)}]'
|
|
fi
|
|
}
|
|
|
|
# Example:
|
|
gitea_progress 1 # Show progress for milestone ID 1
|
|
gitea_progress # List all open milestones with progress
|
|
```
|
|
|
|
**Create task** (issue):
|
|
```bash
|
|
gitea_create_task() {
|
|
local title="$1" body="$2" milestone="$3"
|
|
local labels="${4:-}" owner="${5:-$GITEA_OWNER}" repo="${6:-$GITEA_REPO}"
|
|
local data
|
|
data=$(jq -n --arg t "$title" --arg b "$body" --argjson m "$milestone" \
|
|
'{title: $t, body: $b, milestone: $m}')
|
|
if [ -n "$labels" ]; then
|
|
data=$(echo "$data" | jq --argjson l "[$labels]" '. + {labels: $l}')
|
|
fi
|
|
gitea_api POST "/repos/$owner/$repo/issues" "$data"
|
|
}
|
|
|
|
# Example:
|
|
# Create task in milestone 1 with workflow:ready (id=4) and type:task (id=14)
|
|
gitea_create_task "Implement auth" "Add OAuth2 flow" 1 "4,14"
|
|
```
|
|
|
|
---
|
|
|
|
## 7. The Three API Gotchas
|
|
|
|
These were discovered through live testing and cost significant debugging time.
|
|
|
|
### 7.1 Gotcha 1: Scoped Label Exclusivity is UI-Only
|
|
|
|
**What we expected**: POST to `/repos/{owner}/{repo}/issues/{index}/labels` with `workflow:in-progress` would replace `workflow:ready` automatically because both labels have `exclusive: true` and scope `workflow`.
|
|
|
|
**What actually happens**: The API just appends the label. The issue ends up with BOTH `workflow:ready` AND `workflow:in-progress`, violating the exclusivity constraint.
|
|
|
|
**Why**: Label exclusivity is enforced in the Gitea UI but NOT in the REST API. The API treats labels as a simple list.
|
|
|
|
**Solution**: Use PUT `/repos/{owner}/{repo}/issues/{index}/labels` with the full label set (see `gitea_swap_workflow` pattern in section 6.2).
|
|
|
|
**Verified test**:
|
|
```bash
|
|
# Issue initially has workflow:ready (id=4) and type:task (id=14)
|
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/1" | \
|
|
jq '[.labels[].name]'
|
|
# ["workflow:ready", "type:task"]
|
|
|
|
# WRONG: POST appends workflow:in-progress (id=5)
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"labels": [5]}' \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/1/labels" | \
|
|
jq '[.labels[].name]'
|
|
# ["workflow:ready", "workflow:in-progress", "type:task"] <-- BOTH workflow labels!
|
|
|
|
# RIGHT: PUT replaces all labels
|
|
curl -s -X PUT \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"labels": [5, 14]}' \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/1/labels" | \
|
|
jq '[.labels[].name]'
|
|
# ["workflow:in-progress", "type:task"] <-- workflow:ready removed
|
|
```
|
|
|
|
### 7.2 Gotcha 2: Assignee Endpoint Does Not Exist
|
|
|
|
**What the design document specified**:
|
|
```bash
|
|
gitea_api POST "/repos/$owner/$repo/issues/$index/assignees" \
|
|
"{\"assignees\": [\"$user\"]}"
|
|
```
|
|
|
|
**What actually happens**:
|
|
```bash
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"assignees": ["root"]}' \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/1/assignees"
|
|
# {"message":"Not Found","url":"https://git.terraphim.cloud/api/swagger"}
|
|
# HTTP 404
|
|
```
|
|
|
|
**Why**: Gitea 1.22.6 does not have a dedicated `/assignees` endpoint for issues. The GitHub API has this endpoint, but Gitea does not.
|
|
|
|
**Solution**: Use `PATCH /repos/{owner}/{repo}/issues/{index}` with the `assignees` field:
|
|
|
|
```bash
|
|
# CORRECT approach
|
|
curl -s -X PATCH \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"assignees": ["root"]}' \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/1" | \
|
|
jq '.assignees[].login'
|
|
# "root"
|
|
```
|
|
|
|
Updated helper:
|
|
```bash
|
|
gitea_claim() {
|
|
local index="$1" user="${2:-}" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
gitea_swap_workflow "$index" "workflow:in-progress" "$owner" "$repo"
|
|
if [ -n "$user" ]; then
|
|
# Use PATCH on issue, not POST to /assignees
|
|
gitea_api PATCH "/repos/$owner/$repo/issues/$index" "{\"assignees\": [\"$user\"]}"
|
|
fi
|
|
}
|
|
```
|
|
|
|
### 7.3 Gotcha 3: Issue Dependencies Use IssueMeta Format
|
|
|
|
**What we tried first** (based on integer ID pattern from other APIs):
|
|
```bash
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"depends_on_id": 2}' \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/3/dependencies"
|
|
# {"message":"repository does not exist [id: 0]"}
|
|
```
|
|
|
|
**Why this failed**: The Gitea API uses `IssueMeta` format for cross-repo dependencies. The body must specify owner, repo, and index (issue number), not just an integer ID.
|
|
|
|
**Correct format**:
|
|
```bash
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"owner": "terraphim", "repo": "test", "index": 2}' \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/3/dependencies" | \
|
|
jq '{number, title}'
|
|
# {"number": 2, "title": "Setup authentication"}
|
|
```
|
|
|
|
This format supports **cross-repository dependencies**: issue 3 in `terraphim/test` can depend on issue 5 in `terraphim/backend`:
|
|
|
|
```bash
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"owner": "terraphim", "repo": "backend", "index": 5}' \
|
|
"https://git.terraphim.cloud/api/v1/repos/terraphim/test/issues/3/dependencies"
|
|
```
|
|
|
|
**Helper function**:
|
|
```bash
|
|
gitea_add_dependency() {
|
|
local issue="$1" depends_on_issue="$2"
|
|
local owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
local dep_owner="${5:-$owner}" dep_repo="${6:-$repo}"
|
|
gitea_api POST "/repos/$owner/$repo/issues/$issue/dependencies" \
|
|
"$(jq -n --arg o "$dep_owner" --arg r "$dep_repo" --argjson i "$depends_on_issue" \
|
|
'{owner: $o, repo: $r, index: $i}')"
|
|
}
|
|
|
|
# Example: Issue #3 depends on issue #2 (same repo)
|
|
gitea_add_dependency 3 2
|
|
|
|
# Example: Issue #3 in terraphim/frontend depends on issue #5 in terraphim/backend
|
|
gitea_add_dependency 3 5 terraphim frontend terraphim backend
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Command Comparison Table
|
|
|
|
| Operation | bd (beads) | br (beads_rust) | Gitea |
|
|
|-----------|-----------|----------------|-------|
|
|
| Find ready work | `bd ready` | `br ready` | `GET /issues?labels=workflow:ready&state=open` |
|
|
| Claim task | `bd update ID --status in_progress` | `br update ID --status in_progress` | `gitea_claim ISSUE_NUMBER USERNAME` |
|
|
| Complete task | `bd close ID` | `br close ID` | `gitea_close_task ISSUE_NUMBER "Done"` |
|
|
| Create task | `bd create --title="..." --type=task` | `br create "..." -t task` | `gitea_create_task TITLE BODY MILESTONE_ID` |
|
|
| Sync state | `bd sync` (auto-commits!) | `br sync --flush-only` + git add + git commit | N/A (state lives in Gitea server) |
|
|
| View progress | `bd stats` | `br stats` | `gitea_progress MILESTONE_ID` |
|
|
| Dependencies | `bd dep add ID1 ID2` | `br dep add ID1 ID2` | `POST /issues/3/dependencies {"owner","repo","index":2}` |
|
|
| Cross-repo visibility | None | None | Built-in (all repos on same instance) |
|
|
| Web UI | None | None | Built-in at git.terraphim.cloud |
|
|
| Time tracking | None | None | Built-in (stopwatch API) |
|
|
| Offline mode | Yes (local SQLite) | Yes (local SQLite) | No (requires network) |
|
|
|
|
---
|
|
|
|
## 9. What We Gained and Lost
|
|
|
|
### 9.1 Gained
|
|
|
|
**Web UI for human visibility**:
|
|
No more "what are the agents doing?" Humans can visit https://git.terraphim.cloud/terraphim/agent-tasks/issues to see all tasks, filter by label, view progress, and read comments.
|
|
|
|
**Cross-repository task visibility**:
|
|
A single `terraphim/agent-tasks` repository serves as the central task tracker. All agents create issues here regardless of which code repository they're working in. br required separate `.beads/` directories per repo with no coordination.
|
|
|
|
**No binary installation**:
|
|
All operations use `curl` and `jq`, which are already installed on most systems. No need to install `br` binary or keep it updated.
|
|
|
|
**Built-in time tracking**:
|
|
Gitea has a stopwatch API for tracking time spent on tasks. Start/stop times are recorded per user.
|
|
|
|
**Issue comments with markdown**:
|
|
Agents can post progress updates, link to commits, and collaborate via comments. Visible in the UI and accessible via API.
|
|
|
|
**Issue dependencies with API support**:
|
|
`POST /issues/{index}/dependencies` with `IssueMeta` format. Supports cross-repo dependencies (task in repo A can depend on task in repo B).
|
|
|
|
**Milestone-based progress tracking**:
|
|
Each milestone tracks `open_issues` and `closed_issues` counts. Percentage completion calculated automatically. Visible in UI with progress bar.
|
|
|
|
**Labels visible in UI with color coding**:
|
|
Workflow state (ready/in-progress/blocked/done) immediately visible via label colors. Priority and type also color-coded.
|
|
|
|
**Integration with existing git workflows**:
|
|
Issues link to commits, pull requests, branches. Agents can close issues via commit messages (`Fixes #42`).
|
|
|
|
**Audit trail**:
|
|
All issue updates logged with timestamps and user attribution. Who did what, when.
|
|
|
|
### 9.2 Lost
|
|
|
|
**File reservations** (Agent Mail feature):
|
|
Agent Mail provided advisory locks to prevent concurrent file edits. Gitea has no equivalent. Workaround: use issue comments to declare intent ("Working on /src/auth.rs") or rely on git branch locking.
|
|
|
|
**Agent-to-agent messaging**:
|
|
Agent Mail had inbox/outbox for direct agent communication. Gitea replacement: use issue comments and `@mentions` to notify agents.
|
|
|
|
**Graph-aware triage** (bv's PageRank, betweenness centrality):
|
|
br had integration with `bv` tool for graph analysis of task dependencies. Gitea provides dependency tracking but no built-in graph algorithms. Would need external analysis tool querying Gitea API.
|
|
|
|
**Local-first operation**:
|
|
br worked without network access (SQLite + JSONL in `.beads/`). Gitea requires network connectivity to the git.terraphim.cloud instance.
|
|
|
|
**Offline capability**:
|
|
br could create tasks, update status, and sync later when network available. Gitea requires real-time network access for all operations.
|
|
|
|
**Automatic sync on git commit**:
|
|
bd (original beads) had git hooks that auto-synced task state on commit. Removed in br due to race conditions, not re-implemented in Gitea approach.
|
|
|
|
---
|
|
|
|
## 10. Integration Test Results
|
|
|
|
The test script `test-gitea-skill.sh` validates all operations against the live Gitea instance. No mocks. Creates a timestamped temporary repository, runs all tests, and cleans up.
|
|
|
|
### 10.1 Test Coverage
|
|
|
|
| Test # | Operation | Assertions | Status |
|
|
|--------|-----------|-----------|--------|
|
|
| 1 | Create test repository | 3 | PASS |
|
|
| 2 | Create labels (via setup-labels.sh) | 3 | PASS |
|
|
| 3 | Create milestone (project) | 2 | PASS |
|
|
| 4 | Create issue with milestone | 3 | PASS |
|
|
| 5 | Mark task ready | 1 | PASS |
|
|
| 6 | Claim task (in-progress + assignee) | 3 | PASS |
|
|
| 7 | Check progress (0% complete) | 3 | PASS |
|
|
| 8 | Close task (done + closed state) | 3 | PASS |
|
|
| 9 | Check progress (100% complete) | 3 | PASS |
|
|
| 10 | Delete test repository | 2 | PASS |
|
|
|
|
**Total**: 10 tests, 26 assertions, all passing.
|
|
|
|
### 10.2 Test Execution
|
|
|
|
```bash
|
|
# Prerequisites
|
|
source ~/op_zesticai_non_prod.sh
|
|
export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
|
|
|
|
# Run tests
|
|
cd /path/to/gitea-infrastructure/.agents/skills/gitea
|
|
./test-gitea-skill.sh
|
|
```
|
|
|
|
**Expected output**:
|
|
```
|
|
============================================
|
|
Gitea Skill Integration Tests
|
|
============================================
|
|
|
|
=== Pre-flight Checks ===
|
|
Instance: https://git.terraphim.cloud
|
|
User: root
|
|
Test repo: root/skill-test-1708315200
|
|
|
|
--- Test 1: Create test repository ---
|
|
[PASS] Repo name matches
|
|
[PASS] Repo has id
|
|
[PASS] Owner matches
|
|
--- Test 2: Create labels via setup-labels.sh ---
|
|
[PASS] Created 12 labels (expected >= 12)
|
|
[PASS] workflow:ready is exclusive
|
|
[PASS] workflow:in-progress is exclusive
|
|
--- Test 3: Create milestone (project) ---
|
|
[PASS] Milestone created with id=1
|
|
[PASS] Milestone has title
|
|
--- Test 4: Create issue (task) with milestone ---
|
|
[PASS] Issue created with number=#1
|
|
[PASS] Issue state is open
|
|
[PASS] Milestone assigned
|
|
--- Test 5: Mark task ready (workflow:ready) ---
|
|
[PASS] Issue has workflow:ready label
|
|
--- Test 6: Claim task (workflow:in-progress) ---
|
|
[PASS] Issue has workflow:in-progress label
|
|
[PASS] workflow:ready removed by label swap
|
|
[PASS] Assignee set
|
|
--- Test 7: Check project progress (0% complete) ---
|
|
[PASS] 1 open issue
|
|
[PASS] 0 closed issues
|
|
[PASS] Progress is 0%
|
|
--- Test 8: Close task ---
|
|
[PASS] Issue state is closed
|
|
[PASS] Issue has workflow:done label
|
|
[PASS] workflow:in-progress removed by label swap
|
|
--- Test 9: Check progress after close (100% complete) ---
|
|
[PASS] 0 open issues
|
|
[PASS] 1 closed issue
|
|
[PASS] Progress is 100%
|
|
--- Test 10: Delete test repository ---
|
|
[PASS] Repository deleted (HTTP 204)
|
|
[PASS] Repository confirmed deleted (HTTP 404)
|
|
|
|
============================================
|
|
Results: 26 passed, 0 failed (26 total)
|
|
============================================
|
|
```
|
|
|
|
Each test run creates a unique repository (e.g., `skill-test-1708315200`) to avoid conflicts with concurrent test runs.
|
|
|
|
### 10.3 Test Highlights
|
|
|
|
**Test 2** (Create labels) validates:
|
|
- `setup-labels.sh` creates exactly 12 labels
|
|
- Labels have `exclusive: true` flag
|
|
- Idempotent (re-running doesn't create duplicates)
|
|
|
|
**Test 6** (Claim task) validates the label swap pattern:
|
|
- Sets `workflow:in-progress` label
|
|
- **Removes** `workflow:ready` label (exclusivity)
|
|
- Sets assignee via PATCH (not POST /assignees)
|
|
|
|
**Tests 7 and 9** (Progress tracking) validate:
|
|
- Milestone `open_issues` count updates when issue state changes
|
|
- Milestone `closed_issues` count updates when issue closed
|
|
- Percentage calculation: `(closed / (open + closed)) * 100`
|
|
|
|
---
|
|
|
|
## 11. Conclusions
|
|
|
|
### 11.1 Consolidation Benefits
|
|
|
|
Replacing three tools (beads, br, mcp-agent-mail) with a single Gitea instance:
|
|
- **Reduced operational complexity**: One service to deploy, monitor, and maintain
|
|
- **Human visibility**: Web UI at git.terraphim.cloud shows all agent activity
|
|
- **Cross-repo coordination**: Single task repository for all agent work
|
|
- **No installation required**: All operations via curl and jq
|
|
- **Better auditing**: Full history of who did what, when, visible in UI and API
|
|
|
|
### 11.2 Tradeoffs
|
|
|
|
Gained centralization and visibility. Lost offline capability and graph analysis features. The tradeoff is acceptable for our use case because:
|
|
- Agents run on servers with reliable network connectivity
|
|
- Graph analysis (PageRank, betweenness) was rarely used
|
|
- File reservation feature replaced by issue comments declaring intent
|
|
- Web UI visibility outweighs local-first benefits
|
|
|
|
### 11.3 Implementation Complexity
|
|
|
|
**Low**. The entire migration required:
|
|
- 1 Docker Compose template (5.2)
|
|
- 1 SeaweedFS S3 config (5.3)
|
|
- 1 label setup script (3.3)
|
|
- 8 shell helper functions (6.2-6.4)
|
|
- 1 integration test script (10.1)
|
|
|
|
All commands verified working on 2026-02-18. No custom code, no plugins, no database migrations.
|
|
|
|
### 11.4 Reproducibility
|
|
|
|
This article is self-contained. Someone with:
|
|
- A Linux server
|
|
- Docker and Docker Compose
|
|
- 1Password CLI
|
|
- curl and jq
|
|
|
|
...can reproduce the entire setup by following sections 4 (1Password), 5 (Infrastructure), and 6 (Shell Helpers) in order.
|
|
|
|
### 11.5 Lessons Learned
|
|
|
|
**API documentation is not always accurate**. Gitea's Swagger docs show project board endpoints, but they return HTTP 404 in version 1.22.6. Always test against a live instance.
|
|
|
|
**Label exclusivity is UI-only**. Scoped labels with `exclusive: true` are enforced in the UI but not the API. Use PUT with full label replacement.
|
|
|
|
**GitHub API compatibility is incomplete**. Gitea aims for GitHub API compatibility but has gaps (e.g., no `/issues/{index}/assignees` endpoint). Test all endpoints.
|
|
|
|
**1Password secret injection works well**. The `op inject` pattern keeps secrets out of version control while maintaining reproducibility.
|
|
|
|
---
|
|
|
|
## Appendix A: Quick Reference
|
|
|
|
### A.1 Essential Environment Variables
|
|
|
|
```bash
|
|
source ~/op_zesticai_non_prod.sh
|
|
export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
|
|
export GITEA_URL="https://git.terraphim.cloud"
|
|
export GITEA_OWNER="terraphim"
|
|
export GITEA_REPO="agent-tasks"
|
|
```
|
|
|
|
### A.2 Common Operations
|
|
|
|
```bash
|
|
# List ready tasks
|
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
|
"$GITEA_URL/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/issues?labels=workflow:ready&state=open" | \
|
|
jq '.[] | {number, title}'
|
|
|
|
# Claim task #5 as user root
|
|
gitea_claim 5 root
|
|
|
|
# Check progress for milestone #1
|
|
gitea_progress 1
|
|
|
|
# Close task #5 with comment
|
|
gitea_close_task 5 "Completed authentication module"
|
|
```
|
|
|
|
### A.3 File Locations
|
|
|
|
| File | Path |
|
|
|------|------|
|
|
| Docker Compose template | `/path/to/gitea-infrastructure/docker-compose.yml.template` |
|
|
| S3 config template | `/path/to/gitea-infrastructure/s3_config.json.template` |
|
|
| Label setup script | `/path/to/gitea-infrastructure/.agents/skills/gitea/setup-labels.sh` |
|
|
| Integration tests | `/path/to/gitea-infrastructure/.agents/skills/gitea/test-gitea-skill.sh` |
|
|
| Agent skill document | `/path/to/gitea-infrastructure/.agents/skills/gitea/SKILL.md` |
|
|
| Caddy config | `/etc/caddy/Caddyfile` |
|
|
|
|
### A.4 Service URLs
|
|
|
|
| Service | URL | Access |
|
|
|---------|-----|--------|
|
|
| Gitea Web UI | https://git.terraphim.cloud | Public (HTTPS via Caddy) |
|
|
| Gitea SSH | git.terraphim.cloud:222 | Public |
|
|
| Gitea API | https://git.terraphim.cloud/api/v1 | Public (requires token) |
|
|
| SeaweedFS S3 | http://100.106.66.7:8333 | Tailscale only |
|
|
| Prometheus | http://100.106.66.7:9000 | Tailscale only |
|
|
|
|
### A.5 1Password Items Reference
|
|
|
|
| Item Name | Vault | Field | Purpose |
|
|
|-----------|-------|-------|---------|
|
|
| git.terraphim.cloud-admin-token | TerraphimPlatform | credential | Gitea API authentication |
|
|
| gitea-postgres | TerraphimPlatform | password | PostgreSQL database password |
|
|
| gitea-s3 | TerraphimPlatform | access-key | SeaweedFS S3 access key |
|
|
| gitea-s3 | TerraphimPlatform | secret-key | SeaweedFS S3 secret key |
|
|
|
|
---
|
|
|
|
## Appendix B: API Endpoint Reference
|
|
|
|
All endpoints relative to `https://git.terraphim.cloud/api/v1`.
|
|
|
|
### B.1 Authentication
|
|
|
|
```bash
|
|
# Verify token (returns user info)
|
|
GET /user
|
|
```
|
|
|
|
### B.2 Milestones (Projects)
|
|
|
|
```bash
|
|
# Create milestone
|
|
POST /repos/{owner}/{repo}/milestones
|
|
Body: {"title": "Sprint 1", "description": "...", "due_on": "2026-03-01T00:00:00Z"}
|
|
|
|
# List milestones
|
|
GET /repos/{owner}/{repo}/milestones?state=open
|
|
|
|
# Get milestone
|
|
GET /repos/{owner}/{repo}/milestones/{id}
|
|
|
|
# Update milestone
|
|
PATCH /repos/{owner}/{repo}/milestones/{id}
|
|
Body: {"title": "...", "state": "closed"}
|
|
|
|
# Delete milestone
|
|
DELETE /repos/{owner}/{repo}/milestones/{id}
|
|
```
|
|
|
|
### B.3 Issues (Tasks)
|
|
|
|
```bash
|
|
# Create issue
|
|
POST /repos/{owner}/{repo}/issues
|
|
Body: {"title": "...", "body": "...", "milestone": 1, "labels": [4, 14]}
|
|
|
|
# List issues
|
|
GET /repos/{owner}/{repo}/issues?state=open&labels=workflow:ready&milestone=Sprint+1
|
|
|
|
# Get issue
|
|
GET /repos/{owner}/{repo}/issues/{index}
|
|
|
|
# Update issue
|
|
PATCH /repos/{owner}/{repo}/issues/{index}
|
|
Body: {"state": "closed", "assignees": ["root"]}
|
|
|
|
# Add comment
|
|
POST /repos/{owner}/{repo}/issues/{index}/comments
|
|
Body: {"body": "Progress update..."}
|
|
```
|
|
|
|
### B.4 Labels
|
|
|
|
```bash
|
|
# Create label
|
|
POST /repos/{owner}/{repo}/labels
|
|
Body: {"name": "workflow:ready", "color": "0e8a16", "description": "...", "exclusive": true}
|
|
|
|
# List labels
|
|
GET /repos/{owner}/{repo}/labels
|
|
|
|
# Replace issue labels (atomic swap)
|
|
PUT /repos/{owner}/{repo}/issues/{index}/labels
|
|
Body: {"labels": [4, 5, 14]}
|
|
```
|
|
|
|
### B.5 Dependencies
|
|
|
|
```bash
|
|
# Add dependency (issue A depends on issue B)
|
|
POST /repos/{owner}/{repo}/issues/{index}/dependencies
|
|
Body: {"owner": "terraphim", "repo": "agent-tasks", "index": 2}
|
|
|
|
# List dependencies (what blocks this issue?)
|
|
GET /repos/{owner}/{repo}/issues/{index}/dependencies
|
|
|
|
# List blocks (what does this issue block?)
|
|
GET /repos/{owner}/{repo}/issues/{index}/blocks
|
|
|
|
# Remove dependency
|
|
DELETE /repos/{owner}/{repo}/issues/{index}/dependencies
|
|
Body: {"owner": "terraphim", "repo": "agent-tasks", "index": 2}
|
|
```
|
|
|
|
---
|
|
|
|
**End of Article**
|
|
|
|
All commands verified working as of 2026-02-18 against Gitea 1.22.6 running at git.terraphim.cloud.
|