From 2b5ab840f737594fe4bb62bb1ed622d20f894870 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Wed, 18 Feb 2026 14:11:21 +0100 Subject: [PATCH] docs: Add articles comparing beads/br/agent-mail with Gitea replacement 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 --- ARTICLE-agent-task-consolidation.md | 1289 +++++++++++++++++++++++++++ ARTICLE-agent-task-management.md | 402 +++++++++ 2 files changed, 1691 insertions(+) create mode 100644 ARTICLE-agent-task-consolidation.md create mode 100644 ARTICLE-agent-task-management.md diff --git a/ARTICLE-agent-task-consolidation.md b/ARTICLE-agent-task-consolidation.md new file mode 100644 index 0000000..257a089 --- /dev/null +++ b/ARTICLE-agent-task-consolidation.md @@ -0,0 +1,1289 @@ +# 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. diff --git a/ARTICLE-agent-task-management.md b/ARTICLE-agent-task-management.md new file mode 100644 index 0000000..5d5323d --- /dev/null +++ b/ARTICLE-agent-task-management.md @@ -0,0 +1,402 @@ +# We Replaced Three Agent Tools with Gitea and Twelve Labels + +**Date:** February 18, 2026 +**Author:** Alex, CTO @ Zestic AI +**Tags:** ai-agents, task-management, gitea, devops, simplicity + +--- + +The best infrastructure is the infrastructure you already run. + +We spent eighteen months building, rewriting, and stitching together three separate tools for AI agent task management. A Go CLI. A Rust rewrite. An MCP coordination server. Each solved a real problem. Each introduced new ones. Then one afternoon, staring at a Gitea milestone progress bar, the obvious answer arrived: we already had everything we needed. We just had too many tools to see it. + +This is the story of how we replaced `bd`, `br`, and `mcp-agent-mail` with a self-hosted Gitea instance, twelve scoped labels, and a handful of shell functions. No custom servers. No proprietary state stores. No binaries to distribute. Just an issue tracker doing what issue trackers do. + +## The Problem: Three Tools, One Job + +Here is what our AI agent task management stack looked like before the consolidation: + +**Tool 1: beads (`bd`)** -- A Go CLI that stored issues in SQLite and JSONL, committed to git inside a `.beads/` directory. Priority P0-P4, types for bug/feature/task/epic/chore. The works. It had a daemon mode, RPC mode, git hooks, and a `bd sync` command that auto-committed changes to the repository. + +**Tool 2: beads_rust (`br`)** -- The Rust rewrite. Same concept, fewer footguns. Version 0.1.13. The big change: `br` never executes git commands. You run `br sync --flush-only` to export to JSONL, then `git add .beads/ && git commit` yourself. + +**Tool 3: MCP Agent Mail (`mcp-agent-mail`)** -- An MCP server for multi-agent coordination. Identities, inboxes, outboxes, searchable threads, advisory file reservations (leases). Runs on `localhost:8765`. Macros for session setup, thread preparation, file reservation cycles, contact handshakes. + +Three tools. Three state stores. Three sets of failure modes. One team. + +## Why `bd` Auto-Commit Was a Mistake + +The original beads CLI did something that sounds reasonable until you think about it for ten seconds: it auto-committed its state to git via `bd sync`. + +Here is what happens when an AI agent is working on a task, writing code, staging changes, and the task tracker decides to `git add .beads/ && git commit` in the middle of that work: the agent's uncommitted changes either get swept into the beads commit or the beads commit races with the agent's own commit. We lost work. Not once -- multiple times. + +The Rust rewrite fixed this by being "non-invasive." `br` never touches git. That was the right call, and it taught us an important principle: **your task tracker has no business modifying the repository it lives in.** Task state and code changes operate at different cadences. Coupling them through auto-commit is asking for data loss. + +But `br` still had the fundamental problem of being a local-first tool. Data lived in `.beads/` inside each repository. No cross-repo visibility. No web UI for humans. Every machine that needed to manage tasks required the `br` binary installed. For a single developer working on a single repo, this is fine. For a team running multiple AI agents across multiple repositories, it is untenable. + +## Agent Mail Solved Coordination, Then Became the Problem + +Enter `mcp-agent-mail`. It solved a genuine gap: how do multiple AI agents coordinate work without clobbering each other? + +The answer was a message bus with identity management and file reservations. Agent A reserves `src/auth.rs`, sends a message to Agent B saying "I am working on auth, do not touch this file," and Agent B acknowledges. Thread-based communication. Advisory locking. Cross-repo coordination through a central server. + +It worked. But look at what an agent had to do before it could start actual work: + +1. Register an identity with the mail server +2. Start a session +3. Prepare a thread +4. Reserve files it intended to modify +5. Send a contact handshake to relevant agents +6. Check inbox for acknowledgments +7. Actually start working + +Seven steps before writing a single line of code. That is not coordination -- that is ceremony. + +And the mail server itself was another piece of infrastructure to run, monitor, and keep alive. Its state store was separate from the task tracker. You had task state in `.beads/`, coordination state in `mcp-agent-mail`, and code state in git. Three sources of truth that needed to stay synchronized. They didn't. + +## The Moment of Clarity + +The realization was almost embarrassing in its simplicity. + +We were already running a self-hosted Gitea instance at `git.terraphim.cloud` -- Gitea 1.22.6, Docker Compose, PostgreSQL, SeaweedFS for S3 storage, Caddy reverse proxy, the whole stack. We had deployed it for code hosting. It had an issue tracker. That issue tracker had labels, milestones, assignees, dependencies, comments, and a REST API. + +Every feature we had built across three tools already existed in the software we were running for something else. + +| What We Built | What Gitea Already Has | +|---------------|----------------------| +| `br create` / `bd create` | `POST /repos/{owner}/{repo}/issues` | +| `br ready` / `br update --status` | Scoped workflow labels | +| `br list --status ready` | `GET /issues?labels=workflow:ready` | +| `br close` | `PATCH /issues/{index}` with `{"state": "closed"}` | +| `br dep add` | `POST /issues/{index}/dependencies` | +| `br stats` | Milestone progress (open/closed counts) | +| Agent Mail file reservations | Issue assignees (one owner per task) | +| Agent Mail threads | Issue comments | +| Agent Mail identities | Gitea user accounts | +| `.beads/` JSONL export | Gitea API (queryable, no export needed) | + +The mapping was nearly one-to-one. The features we thought we needed to build were just an issue tracker with labels. + +## The Implementation: Twelve Labels and Five Shell Functions + +The entire implementation fits in three files. A 130-line setup script. A 480-line skill document. A 430-line integration test suite. That is it. + +### The Label Scheme + +Twelve scoped labels across three dimensions. Workflow state, priority, and type. Each scope is mutually exclusive -- you cannot be both `workflow:ready` and `workflow:in-progress` at the same time. + +**Workflow (4 labels):** + +| Label | Color | Meaning | +|-------|-------|---------| +| `workflow:ready` | Green | Available for an agent to claim | +| `workflow:in-progress` | Yellow | Being actively worked on | +| `workflow:blocked` | Red | Waiting on a dependency | +| `workflow:done` | Purple | Complete | + +**Priority (4 labels):** + +| Label | Color | Meaning | +|-------|-------|---------| +| `priority:critical` | Dark red | Now | +| `priority:high` | Red | This cycle | +| `priority:medium` | Yellow | Normal | +| `priority:low` | Green | When we get to it | + +**Type (4 labels):** + +| Label | Color | Meaning | +|-------|-------|---------| +| `type:feature` | Blue | New functionality | +| `type:bug` | Red | Something broke | +| `type:task` | Violet | General work item | +| `type:research` | Teal | Investigation / spike | + +Setup is idempotent. Run `setup-labels.sh` as many times as you want -- it checks for existing labels before creating: + +```bash +source ~/op_zesticai_non_prod.sh +export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential") +./setup-labels.sh # defaults to terraphim/agent-tasks +./setup-labels.sh myorg myrepo # or specify your own +``` + +### The Shell Helpers + +Every operation is a function that takes positional arguments and returns JSON. No flags to remember, no binary to install. Copy the function into your agent's context and go. + +The base API helper: + +```bash +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" +} +``` + +Create a project (milestones serve as project containers -- more on why below): + +```bash +gitea_create_project "Sprint 2026-W08" "Weekly sprint" "2026-02-28T00:00:00Z" +``` + +Create a task and assign it to a milestone: + +```bash +gitea_create_task "Implement auth module" "OAuth2 flow for API clients" 1 "4,14" +``` + +Claim a task (swap workflow label + assign): + +```bash +gitea_claim 1 root +``` + +Close a task with a comment: + +```bash +gitea_close_task 1 "Implemented and tested. See commit abc123." +``` + +Check project progress: + +```bash +gitea_progress 1 +# Returns: {"milestone":"Sprint 2026-W08","open":3,"closed":7,"total":10,"percent_complete":70} +``` + +An AI agent can learn the entire task management API from a single skill document. No SDK installation. No daemon to keep running. No state synchronization. The state lives in Gitea, which is already running, already backed up, already monitored. + +### Milestones as Projects + +Gitea 1.22.6 has no project board REST API. This is not speculation -- we tested it. `GET /api/v1/user/projects` returns 404. The feature is tracked in Gitea issue #14299, targeted for version 1.26+. + +This turned out to be a non-issue. Milestones give us everything a project container needs: a title, a description, a due date, automatic progress tracking (open vs. closed issue counts), and the ability to filter issues by milestone. You do not need kanban columns when you have workflow labels. + +## The Infrastructure: Already Running + +The Gitea instance was deployed via Docker Compose with 1Password template injection. No new infrastructure was required for this migration. Zero. + +``` +Internet -> Caddy (HTTPS) -> Gitea (localhost:3000) + | + Docker Network (internal) + | | | + PostgreSQL SeaweedFS Prometheus + (S3) +``` + +Secrets are managed through 1Password's `op inject`: + +```bash +op inject --in-file docker-compose.yml.template --out-file docker-compose.yml +docker compose up -d +``` + +The template contains `op://TerraphimPlatform/...` references that get resolved at deploy time. No plaintext secrets in git. No `.env` files to manage. Database password, S3 keys, API tokens -- all in one vault, all injected at runtime. + +SeaweedFS provides S3-compatible storage for attachments, avatars, and Git LFS objects. Prometheus scrapes metrics from all services. Caddy handles TLS termination with automatic certificate renewal. The whole stack is six Docker containers and a Caddyfile. + +For agent authentication, the token lives in 1Password: + +```bash +export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential") +``` + +One line. No token files. No environment variable sprawl. Every agent session starts with this, and the token never touches disk. + +## The Gotchas Nobody Tells You About + +This is the section that justifies the article. Gitea's API is good, but it has sharp edges that cost us a day of debugging. Learn from our pain. + +### Gotcha 1: Scoped Label Exclusivity Is UI-Only + +This was the single biggest surprise. + +Gitea's scoped labels (created with `exclusive: true`) display mutual exclusivity in the web UI. Click `workflow:in-progress` and `workflow:ready` disappears. Beautiful. Exactly what we wanted. + +But the API `POST /repos/{owner}/{repo}/issues/{index}/labels` endpoint just **appends**. It does not remove conflicting scoped labels. You can end up with both `workflow:ready` and `workflow:in-progress` on the same issue, which is nonsensical. + +The fix is a swap pattern: fetch current labels, filter out the old scope, add the new label, and PUT the complete set atomically: + +```bash +gitea_swap_workflow() { + local index="$1" new_label="$2" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}" + + # Get the 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, filtering out all workflow:* labels + issue=$(gitea_api GET "/repos/$owner/$repo/issues/$index") + kept_ids=$(echo "$issue" | jq \ + '[.labels[] | select(.name | startswith("workflow:") | not) | .id]') + + # Combine kept labels + new workflow label, PUT atomically + all_ids=$(echo "$kept_ids" | jq --argjson nid "$new_id" '. + [$nid]') + gitea_api PUT "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": $all_ids}" +} +``` + +This pattern -- GET current state, compute desired state, PUT the complete set -- is the only reliable way to manage scoped labels via the API. We wrap every workflow transition in this function. Our integration tests verify that the old workflow label actually gets removed on every transition. + +### Gotcha 2: The Assignee Endpoint Does Not Exist + +The Gitea docs suggest `POST /repos/{owner}/{repo}/issues/{index}/assignees` should work. In Gitea 1.22.6, it returns 404. + +Use `PATCH /repos/{owner}/{repo}/issues/{index}` with `{"assignees": ["username"]}` instead. It is the general issue edit endpoint, not a dedicated assignee endpoint, but it works. + +```bash +# Wrong (404 in Gitea 1.22.6): +gitea_api POST "/repos/$owner/$repo/issues/$index/assignees" '{"assignees": ["root"]}' + +# Right: +gitea_api PATCH "/repos/$owner/$repo/issues/$index" '{"assignees": ["root"]}' +``` + +### Gotcha 3: Issue Dependencies Use IssueMeta Format + +If you try the obvious body format for adding a dependency: + +```json +{"depends_on_id": 2} +``` + +You get: `"repository does not exist [id: 0]"`. Helpful error message, that. + +The correct format is IssueMeta -- a full reference to the issue including owner and repo: + +```json +{"owner": "terraphim", "repo": "agent-tasks", "index": 2} +``` + +This is actually a better design than a bare ID. It supports cross-repository dependencies out of the box. But the error message when you get it wrong is genuinely misleading. + +## Testing: 26 Assertions, Zero Mocks + +We do not mock API calls. Every test runs against the live Gitea instance. The test suite creates a timestamped repository (`skill-test-{timestamp}`), runs all ten test scenarios, validates 26 assertions, and cleans up the repo on exit -- even on failure, thanks to a bash trap. + +```bash +./test-gitea-skill.sh +``` + +``` +============================================ + Gitea Skill Integration Tests +============================================ + + Instance: https://git.terraphim.cloud + User: root + Test repo: root/skill-test-1739884800 + +--- 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 6: Claim task (workflow:in-progress) --- + [PASS] Issue has workflow:in-progress label + [PASS] workflow:ready removed by label swap + [PASS] Assignee set +... + +============================================ + Results: 26 passed, 0 failed (26 total) +============================================ +``` + +Test 6 is the critical one. It verifies three things in sequence: the new workflow label is present, the old workflow label was removed by the swap pattern, and the assignee was set via PATCH. If any of these fail, we know the gotchas are back. + +First run of this test suite against the live API: 23/26 passed, 3 failed. Those three failures were the label swap bug and the assignee endpoint. Fixed in an hour. Second run: 26/26. + +That is what testing against real infrastructure gives you. Mocks would have passed on the first run and failed in production. + +## What We Lost and Why We Don't Miss It + +Let's be honest about what the migration removed. + +**Gone: Local-first offline capability.** The `.beads/` directory worked without network access. Gitea requires connectivity. For our use case -- AI agents running on servers with reliable internet -- this is not a loss. If your agents work offline in a bunker, this trade-off may not suit you. + +**Gone: `br sync` JSONL export.** The beads tools exported task data as JSONL files you could grep, pipe, and process with standard Unix tools. With Gitea, you query the API. For an AI agent that speaks HTTP natively, this is arguably better. For a human who wants to `grep -c "status:done" .beads/issues.jsonl`, it is different. We have not missed it. + +**Gone: Agent Mail file reservations.** The advisory locking system in `mcp-agent-mail` prevented two agents from editing the same file simultaneously. We replaced this with a simpler convention: one agent per task, one task per file group. If Agent A is assigned to issue #3 and issue #3's description lists the files it will modify, Agent B checks the issue board before starting work. Is this as rigorous as advisory locks? No. Has it caused a conflict in practice? Also no. + +**Gone: Daemon mode and RPC.** The original `bd` had a long-running daemon for performance. With Gitea, every operation is a stateless HTTP call. No daemon to crash. No RPC to debug. No port conflicts. + +**Gained: Web UI for humans.** This was the biggest win we did not plan for. Stakeholders can now see task status in a browser. They can add comments, change priorities, and close issues without learning a CLI. The task board became a communication tool, not just an agent bookkeeping system. + +**Gained: Cross-repo visibility.** All agent tasks live in `terraphim/agent-tasks` by default. One place to see everything. Override with `GITEA_OWNER` and `GITEA_REPO` environment variables when you need per-repo tracking. + +**Gained: Zero distribution burden.** No binary to compile. No package to install. The agent skill is a markdown document with shell functions. Copy, paste, authenticate, work. + +## The br Command Mapping + +For anyone migrating from `br`, here is the translation table: + +| br Command | Gitea Equivalent | Notes | +|------------|------------------|-------| +| `br init` | `./setup-labels.sh` | One-time per repo | +| `br create "Title" --type task` | `gitea_create_task "Title" "Body" $MILESTONE_ID "$LABEL_IDS"` | Labels by ID | +| `br ready 1` | `gitea_ready 1` | Swap pattern under the hood | +| `br update 1 --status in_progress` | `gitea_claim 1 root` | Also sets assignee | +| `br close 1` | `gitea_close_task 1 "Done"` | Closes issue + sets workflow:done | +| `br list --status ready` | `curl ... /issues?labels=workflow:ready` | Filter by label | +| `br dep add 3 2` | `POST /issues/3/dependencies {"owner","repo","index":2}` | IssueMeta format | +| `br stats` | `gitea_progress $MILESTONE_ID` | Returns JSON with percentages | +| `br sync --flush-only` | Not needed | State already in Gitea | + +The last row is the point. There is no sync step because there is nothing to sync. The source of truth is Gitea. There is no `.beads/` directory to commit. There is no JSONL file to export. The state lives in the database of a server that is already running and already backed up. + +## What This Means for AI Agent Architecture + +This migration crystallized something we had been circling for a while: **AI agents do not need bespoke tooling. They need standard tooling with good APIs.** + +An AI agent that can make HTTP calls can use Gitea's REST API. It does not need a custom CLI. It does not need an MCP server wrapping another API. It does not need a local database synced to git. It needs `curl`, `jq`, and a token. + +The twelve-label scheme gives agents a state machine they can reason about. `workflow:ready` means "pick this up." `workflow:in-progress` means "someone is on it." `workflow:blocked` means "wait." `workflow:done` means "move on." Any LLM can understand this. No specialized training, no tool-specific prompting. + +Milestones give agents project context. "You are working on Sprint 2026-W08. Here are the open tasks. Here is what is blocked. Here is overall progress." All queryable via a single API call. + +Issue comments give agents memory. Close a task with a comment explaining what you did, what you learned, what edge cases you hit. The next agent picks up the thread. + +Dependencies give agents sequencing. "Do not start issue #5 until issue #3 is closed." The API enforces it. + +This is not revolutionary. It is an issue tracker. That is exactly the point. The best agent infrastructure is boring infrastructure that works. + +## What's Next + +Three things we are evaluating: + +**1. MCP integration.** The official `gitea-mcp` server provides ~80 tools for repository management but lacks milestone and workflow label support. The `forgejo-mcp` alternative covers milestones and dependencies. We may adopt one of these as a complement to our shell helpers, but not as a replacement. The helpers handle the 20% of operations that no MCP server covers yet. + +**2. Gitea 1.26 upgrade.** When the project board API ships, we will evaluate whether it adds value over our milestone + label approach. My bet: it will not. We don't need kanban columns when we have four workflow labels and a REST API. + +**3. Cross-instance agent coordination.** Right now, all agents share one Gitea instance. As we scale, we may need agents on different instances to coordinate. Issue dependencies already support cross-repo references (`{"owner": "...", "repo": "...", "index": N}`). Cross-instance is a harder problem, but we are not solving problems we don't have yet. + +## The Bottom Line + +We ran three tools because we assumed agent task management was a special problem that needed special solutions. It isn't. + +An issue tracker with labels is a task management system. A milestone is a project. An assignee is an owner. A comment is a status update. A dependency is a dependency. We had been building elaborate machinery to replicate what a 2004-vintage Bugzilla could do. + +Gitea, twelve labels, and five shell functions. The test suite passes. The agents are productive. Stakeholders can see the board. The infrastructure team is not maintaining three extra tools. The answer, as it so often is, was less code -- not more. + +--- + +**Repository:** [terraphim/gitea-infrastructure](https://git.terraphim.cloud/terraphim/gitea-infrastructure) +**Skill Document:** `.agents/skills/gitea/SKILL.md` +**Setup Script:** `.agents/skills/gitea/setup-labels.sh` +**Integration Tests:** `.agents/skills/gitea/test-gitea-skill.sh` (10 tests, 26 assertions, all passing) +**Gitea Version:** 1.22.6 +**Stack:** Docker Compose + PostgreSQL 15 + SeaweedFS + Caddy + 1Password + Prometheus