docs(ISSUE-WP-0006): Forgejo-only language and projection boundary
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 34s

Prefer Forgejo as the self-hosted forge product. Keep Gitea only as the
Gitea-compatible API identifier (module backends/gitea, type string
gitea). FORGEJO_TOKEN is preferred; GITEA_* remains a deprecated alias.

INTENT/SCOPE quote ACT-ADR-005: issue-core is not the fleet ops claim
queue. Connector docs describe repo work record → hub index → optional
Forgejo projection, not activity-core → issue-core → harness.

Assistant: grok
Assistant-Session: 01a09dc6-3f0d-7c93-8b11-8e83c0623d49
This commit is contained in:
tegwick 2026-09-14 04:52:58 +02:00
parent f9d276dadf
commit ee9b85215d
34 changed files with 410 additions and 263 deletions

View file

@ -1,4 +1,4 @@
**Purpose:** Authoritative task lifecycle manager for the Coulomb org. Backend-agnostic CLI + REST ingestion endpoint for tasks from activity-core's IssueSink. Pluggable backends (Gitea, SQLite, GitHub). Renamed from issue-facade on 2026-05-17. **Purpose:** Authoritative task lifecycle manager for the Coulomb org. Backend-agnostic CLI + REST ingestion endpoint for tasks from activity-core's IssueSink. Pluggable backends (Forgejo, SQLite, GitHub). Renamed from issue-facade on 2026-05-17.
**Domain:** infotech **Domain:** infotech
**Repo slug:** issue-core **Repo slug:** issue-core

View file

@ -2,7 +2,7 @@
## Repo Identity ## Repo Identity
**Purpose:** Authoritative task lifecycle manager for the Coulomb org. Backend-agnostic CLI + REST ingestion endpoint for tasks from activity-core's IssueSink. Pluggable backends (Gitea, SQLite, GitHub). Renamed from issue-facade on 2026-05-17. **Purpose:** Authoritative task lifecycle manager for the Coulomb org. Backend-agnostic CLI + REST ingestion endpoint for tasks from activity-core's IssueSink. Pluggable backends (Forgejo, SQLite, GitHub). Renamed from issue-facade on 2026-05-17.
**Domain:** infotech **Domain:** infotech
**Repo slug:** issue-core **Repo slug:** issue-core

View file

@ -1,24 +1,55 @@
# Agent Integration Guide # Agent Integration Guide
**Issue Core for Autonomous Coding Agent Coordination** **Issue Core for external tracker projection** — not the fleet ops queue.
## Purpose ## Purpose
The **Issue Core** capability provides a standardized interface for autonomous coding agents to operate on **external issue trackers** (GitHub, GitLab, Gitea/Forgejo). Instead of agents learning platform-specific APIs, they use a unified abstraction. The **Issue Core** capability provides a standardized interface for autonomous
coding agents to operate on **external issue trackers** (Forgejo, GitHub,
GitLab, Jira). Instead of agents learning platform-specific APIs, they use a
unified abstraction. The self-hosted forge is **Forgejo** only; the HTTP API
is Gitea-compatible (`issue_core.backends.gitea`, backend type `gitea`).
**Fleet note (2026-07-20):** Internal work originates as repo work records (ADR-001 / work-record canon), not as issue-core issues. Use this guide when an agent must project to or work inside a third-party tracker. See `INTENT.md` and `docs/uuid-external-id-mapping.md`. **Fleet note (2026-07-20 / ISSUE-WP-0006):** Internal work originates as repo
work records (ADR-001 / work-record canon), not as issue-core issues. Use this
guide when an agent must project to or work inside a third-party tracker. See
`INTENT.md` and `docs/uuid-external-id-mapping.md`.
### Happy path (projection, not automation loop)
```
1. Author work record in repo (ADR-001)
│ fix-consistency
v
state-hub index (UUIDv7 write-back)
│ optional project / link
v
2. issue-core ──► Forgejo (or GitHub / Jira later)
3. mapping: work-record UUID ↔ (backend=forgejo, external_id)
stored backend type string is currently `gitea`
(Forgejo Gitea-compatible API)
```
Internal scheduled automation (FI/Binky, mail, …) does **not** follow
`activity-core → issue-core → harness`. That path is **legacy / external
tickets only**. Claim/execute for fleet ops is activity-core **`ops_run`**
(ACT-ADR-005). See rein-aharness `docs/task-intake.md` and ISSUE-WP-0006.
### When to use a tracker (and when not to) ### When to use a tracker (and when not to)
**Fleet claim/execute** runs on work records (repo files + state-hub), not on **Fleet claim/execute** runs on work records (repo files + state-hub) or on
Forgejo by default. Use issue-core when: `ops_run` for scheduled automation — not on Forgejo by default. Use
issue-core when:
- A counterparty or OSS workflow lives in Gitea/GitHub/Jira - A counterparty or OSS workflow lives in Forgejo/GitHub/Jira
- You need to **project or link** a work record (`issue project` / `issue map`) - You need to **project or link** a work record (`issue project` / `issue map`)
- You must update or comment on an **external** issue already in a tracker - You must update or comment on an **external** issue already in a tracker
Do **not** treat issue-core as the org task board or as origin of intake/tasks. Do **not** treat issue-core as the org task board, the ops claim queue, or
Findings → `kind: intake` + promotion; optional later projection. the origin of intake/tasks. Findings → `kind: intake` + promotion; optional
later projection.
## Current Status: Production-Ready with Manual Setup ## Current Status: Production-Ready with Manual Setup
@ -31,7 +62,7 @@ Findings → `kind: intake` + promotion; optional later projection.
- Milestone operations - Milestone operations
- Comment threads - Comment threads
**Gitea Backend** (Production-Ready) **Forgejo Backend** (Production-Ready; Gitea-compatible API)
- Complete API integration - Complete API integration
- Rate limiting and error handling - Rate limiting and error handling
- State mapping (open/in_progress/blocked → open/closed) - State mapping (open/in_progress/blocked → open/closed)
@ -76,18 +107,19 @@ pip install -e .
### 2. Backend Configuration (One-Time Setup) ### 2. Backend Configuration (One-Time Setup)
**For Gitea Projects:** **For Forgejo Projects:**
```bash ```bash
# Configure Gitea backend # Configure Forgejo backend (type string `gitea` = Gitea-compatible API)
export GITEA_API_TOKEN="your-token-here" export FORGEJO_TOKEN="your-token-here"
# Deprecated aliases: FORGEJO_API_TOKEN, GITEA_API_TOKEN, GITEA_TOKEN
issue backend add my-project gitea issue backend add my-project gitea
# Prompts for: # Prompts for:
# - Gitea URL: https://gitea.example.com # - Forgejo URL: https://forgejo.example.com
# - Owner: your-org # - Owner: your-org
# - Repo: your-project # - Repo: your-project
# - Token: (reads from GITEA_API_TOKEN) # - Token: (reads FORGEJO_TOKEN, then deprecated GITEA_* aliases)
# Verify connection # Verify connection
issue backend test my-project issue backend test my-project
@ -187,8 +219,8 @@ from datetime import datetime, timezone
backend = GiteaBackend() backend = GiteaBackend()
backend.connect({ backend.connect({
'base_url': 'https://gitea.example.com', 'base_url': os.environ.get('FORGEJO_URL', 'https://forgejo.example.com'),
'token': os.environ['GITEA_API_TOKEN'], 'token': os.environ.get('FORGEJO_TOKEN') or os.environ['GITEA_API_TOKEN'],
'owner': 'myorg', 'owner': 'myorg',
'repo': 'myproject' 'repo': 'myproject'
}) })
@ -236,10 +268,10 @@ import os
# Initialize backend # Initialize backend
backend = GiteaBackend() backend = GiteaBackend()
backend.connect({ backend.connect({
'base_url': os.environ['GITEA_URL'], 'base_url': os.environ.get('FORGEJO_URL') or os.environ['GITEA_URL'],
'token': os.environ['GITEA_API_TOKEN'], 'token': os.environ.get('FORGEJO_TOKEN') or os.environ.get('FORGEJO_API_TOKEN') or os.environ['GITEA_API_TOKEN'],
'owner': os.environ['GITEA_OWNER'], 'owner': os.environ.get('FORGEJO_OWNER') or os.environ['GITEA_OWNER'],
'repo': os.environ['GITEA_REPO'] 'repo': os.environ.get('FORGEJO_REPO') or os.environ['GITEA_REPO']
}) })
# Query issues # Query issues
@ -421,14 +453,14 @@ results = read_agent_messages(42, 'implementation_complete')
```bash ```bash
# Pull all issues to local backup # Pull all issues to local backup
issue backend add backup local issue backend add backup local
issue sync pull gitea-remote backup issue sync pull forgejo-remote backup
# Work offline with local backend # Work offline with local backend
issue backend set-default backup issue backend set-default backup
issue create "Offline work item" --label=offline issue create "Offline work item" --label=offline
# Sync back when online # Sync back when online
issue sync push backup gitea-remote issue sync push backup forgejo-remote
``` ```
### Conflict Handling ### Conflict Handling
@ -509,20 +541,20 @@ Create a setup script for each project:
# setup-issue-tracking.sh # setup-issue-tracking.sh
cat > .issue-core-config << EOF cat > .issue-core-config << EOF
GITEA_URL=https://gitea.example.com FORGEJO_URL=https://forgejo.example.com
GITEA_OWNER=myorg FORGEJO_OWNER=myorg
GITEA_REPO=myproject FORGEJO_REPO=myproject
GITEA_TOKEN_FILE=~/.secrets/gitea-token FORGEJO_TOKEN_FILE=~/.secrets/forgejo-token
EOF EOF
# Load config and configure backend # Load config and configure backend
source .issue-core-config source .issue-core-config
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE) export FORGEJO_TOKEN=$(cat $FORGEJO_TOKEN_FILE)
issue backend add $(basename $(pwd)) gitea <<INPUT issue backend add $(basename $(pwd)) gitea <<INPUT
$GITEA_URL $FORGEJO_URL
$GITEA_OWNER $FORGEJO_OWNER
$GITEA_REPO $FORGEJO_REPO
INPUT INPUT
issue backend set-default $(basename $(pwd)) issue backend set-default $(basename $(pwd))
@ -642,7 +674,7 @@ issue backend add myproject gitea
issue backend test myproject issue backend test myproject
# Reconfigure with correct token # Reconfigure with correct token
export GITEA_API_TOKEN="new-token" export FORGEJO_TOKEN="new-token"
issue backend remove myproject issue backend remove myproject
issue backend add myproject gitea issue backend add myproject gitea
``` ```
@ -650,7 +682,7 @@ issue backend add myproject gitea
### "Issue not found" ### "Issue not found"
```python ```python
# Gitea uses backend_id, not number # Forgejo uses backend_id, not number
issue = backend.get_issue_by_number(42) # Correct issue = backend.get_issue_by_number(42) # Correct
# issue = backend.get_issue("42") # Wrong - needs backend_id # issue = backend.get_issue("42") # Wrong - needs backend_id
``` ```

View file

@ -6,14 +6,14 @@ metadata:
version: 0.2.1 version: 0.2.1
type: connector type: connector
description: > description: >
Backend-agnostic connector to external issue trackers (Gitea/Forgejo, SQLite). Backend-agnostic connector to external issue trackers (Forgejo, SQLite).
Maps work-record UUIDs to external issue ids. Not the origin of fleet work records. Maps work-record UUIDs to external issue ids. Not the origin of fleet work records.
# What problems this capability solves # What problems this capability solves
purpose: purpose:
primary: External tracker projection and ops (work-record-aware connector) primary: External tracker projection and ops (work-record-aware connector)
problems_solved: problems_solved:
- Direct API calls to Gitea/GitHub/GitLab (credential sprawl) - Direct API calls to Forgejo/GitHub/GitLab (credential sprawl)
- No durable link from work-record UUID to tracker issue - No durable link from work-record UUID to tracker issue
- Platform-specific agent code for tracker CRUD - Platform-specific agent code for tracker CRUD
- Offline SQLite cache / backend sync for tracker data - Offline SQLite cache / backend sync for tracker data
@ -21,7 +21,7 @@ purpose:
# When agents should use this capability # When agents should use this capability
usage_rules: usage_rules:
MUST_USE_INSTEAD_OF: MUST_USE_INSTEAD_OF:
- "Direct Gitea API calls (requests.post to /api/v1/repos/...)" - "Direct Forgejo API calls (requests.post to /api/v1/repos/...)"
- "GitHub CLI (gh issue create/list/...)" - "GitHub CLI (gh issue create/list/...)"
- "GitLab CLI (glab issue create/list/...)" - "GitLab CLI (glab issue create/list/...)"
- "Python libraries (PyGithub, python-gitlab) for routine tracker ops" - "Python libraries (PyGithub, python-gitlab) for routine tracker ops"
@ -67,7 +67,7 @@ integration:
required: true required: true
method: manual # v1.0 - auto in v1.1 method: manual # v1.0 - auto in v1.1
steps: steps:
- "Export GITEA_API_TOKEN environment variable" - "Export FORGEJO_TOKEN environment variable (GITEA_API_TOKEN is a deprecated alias)"
- "Run: issue backend add myproject gitea" - "Run: issue backend add myproject gitea"
- "Provide: URL, owner, repo when prompted" - "Provide: URL, owner, repo when prompted"
- "Run: issue backend set-default myproject" - "Run: issue backend set-default myproject"
@ -115,8 +115,9 @@ efficiency:
credentials: credentials:
method: environment_variables method: environment_variables
variables: variables:
- GITEA_API_TOKEN - FORGEJO_TOKEN (preferred; FORGEJO_API_TOKEN also accepted)
- GITEA_URL (optional with config) - GITEA_API_TOKEN / GITEA_TOKEN (deprecated aliases)
- FORGEJO_URL (optional with config; GITEA_URL deprecated alias)
security: security:
- "Tokens never in code or logs" - "Tokens never in code or logs"
@ -259,7 +260,7 @@ support:
solution: "Run: issue backend add <name> <type>" solution: "Run: issue backend add <name> <type>"
- problem: "Authentication failed" - problem: "Authentication failed"
solution: "Check GITEA_API_TOKEN is set and valid" solution: "Check FORGEJO_TOKEN is set and valid (legacy GITEA_API_TOKEN still accepted)"
- problem: "Command not found: issue" - problem: "Command not found: issue"
solution: "Run: pip install -e capabilities/issue-core/" solution: "Run: pip install -e capabilities/issue-core/"

View file

@ -9,11 +9,12 @@ state-hub, under a closed kind registry (`workplan`, `task`, `intake`,
`the-custodian/canon/standards/work-record-types_v0.1.md` and the founder- `the-custodian/canon/standards/work-record-types_v0.1.md` and the founder-
reviewed architecture draft (`WorkOrchestrationArchitectureDraft.md` v0.2). reviewed architecture draft (`WorkOrchestrationArchitectureDraft.md` v0.2).
External issue trackers (Forgejo / Gitea, GitHub, Jira, …) remain necessary External issue trackers (Forgejo, GitHub, Jira, …) remain necessary
when a human counterparty, open-source workflow, or third-party process lives when a human counterparty, open-source workflow, or third-party process lives
there. They are **not** the fleet coordination substrate and are **not** a there. They are **not** the fleet coordination substrate and are **not** a
work-record kind. Canon is explicit: *issue-core issues become external work-record kind. Canon is explicit: *issue-core issues become external
projections only.* projections only.* The self-hosted forge product is **Forgejo** only
(ACT-ADR-005); Gitea is not a supported second product or migration target.
issue-core exists so that, **when an external tracker is actually in use**, issue-core exists so that, **when an external tracker is actually in use**,
the fleet has one backend-agnostic surface to project, query, and update the fleet has one backend-agnostic surface to project, query, and update
@ -32,9 +33,11 @@ calls with no stable back-reference to the work record.
issue-core was originally built and documented as a **task landing zone**: a issue-core was originally built and documented as a **task landing zone**: a
single place where humans, activity-core, and agents filed work via CLI / single place where humans, activity-core, and agents filed work via CLI /
REST, with Gitea as the default store. That framing contradicted file-first REST, with the self-hosted forge as the default store (product language then
work records and produced a live incident (`daily-todo-md-stale-review` said Gitea; the fleet forge is **Forgejo**). That framing contradicted
IssueSink → Forgejo issues that were never fleet work). file-first work records and produced a live incident
(`daily-todo-md-stale-review` → IssueSink → Forgejo issues that were never
fleet work).
Architecture draft §4.2 and the work-record types standard (CUST-WP-0060) Architecture draft §4.2 and the work-record types standard (CUST-WP-0060)
retargeted purpose: **connector, not origin**. Capability (CRUD, backends, retargeted purpose: **connector, not origin**. Capability (CRUD, backends,
@ -51,13 +54,16 @@ execution loop.
- **Tracker CRUD** on configured backends: create, read, update, close / - **Tracker CRUD** on configured backends: create, read, update, close /
reopen, comment (and related label / assignee / milestone operations). reopen, comment (and related label / assignee / milestone operations).
- **Backends:** local SQLite (offline store / cache), Gitea (Forgejo- - **Backends:** local SQLite (offline store / cache), **Forgejo** (Gitea-
compatible in deployment). Further backends (GitHub, GitLab, Jira) are compatible API; Python module `issue_core.backends.gitea`, backend type
product growth, not yet implemented. string `gitea`). Further backends (GitHub, GitLab, Jira) are product
growth, not yet implemented. Gitea is not a second supported product.
- **CLI** (`issue` / `issue-core`) and **Python library** for direct backend use. - **CLI** (`issue` / `issue-core`) and **Python library** for direct backend use.
- **REST** (`issue serve`, optional `[api]` extra): intentional create - **REST** (`issue serve`, optional `[api]` extra): intentional **external
(`POST /issues/`), list/get/claim (`GET`/`PATCH /issues/`). tracker** create (`POST /issues/`), list/get (`GET`/`PATCH /issues/`).
- **Backend↔backend sync** via CLI (e.g. Gitea ↔ SQLite) — *not* the same as REST claim/list is for tracker issues already on a backend — **not** the
fleet ops claim queue (see below).
- **Backend↔backend sync** via CLI (e.g. Forgejo ↔ SQLite) — *not* the same as
work-record boundary sync below. work-record boundary sync below.
- **Optional intentional ingestion** of TaskSpec payloads for clients that - **Optional intentional ingestion** of TaskSpec payloads for clients that
deliberately create tracker issues (not the fleet path for internal findings). deliberately create tracker issues (not the fleet path for internal findings).
@ -104,10 +110,23 @@ connector must add **no coordination load** to the internal loop.
Emitter policy: activity-core **ACTIVITY-WP-0022** (from ISSUE-WP-0004-T05 / Emitter policy: activity-core **ACTIVITY-WP-0022** (from ISSUE-WP-0004-T05 /
CUST-WP-0060). CUST-WP-0060).
- **Not the fleet ops claim queue.** Internal scheduled automation
(FI daily brief, Binky rhythm, mail intake, …) **claim and execute** via
activity-core **`ops_run`** ([ACT-ADR-005](../activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md)).
issue-core does **not** provide that queue. `POST /issues/` creates or
links an **external tracker** issue only. The default activity-core sink
remains state-hub progress + `ops_run`**not** REST to issue-core for
Binky/FI. Do not poll issue-core as the primary automation loop.
ACT-ADR-005: *issue-cores correct role is a connector facade over
external trackers (Forgejo, GitHub, Jira, …). It is not the origin of
work records and not the default internal ops queue. Gitea is out of
scope for this fleet; the self-hosted forge is Forgejo.*
- **Not a second autonomy or budget model.** `lane`, `tags`, and budgets live - **Not a second autonomy or budget model.** `lane`, `tags`, and budgets live
on the work record. Projections carry only what external collab needs on the work record. Projections carry only what external collab needs
(title, body, agreed labels); they must not invent parallel lane/budget (title, body, agreed labels); they must not invent parallel lane/budget
semantics on Gitea labels without explicit rules. semantics on Forgejo labels without explicit rules.
- **Not a project manager, spawn audit trail, event bus, notification - **Not a project manager, spawn audit trail, event bus, notification
system, or workflow engine.** Plans and dependencies are workplan tooling; system, or workflow engine.** Plans and dependencies are workplan tooling;
@ -151,8 +170,8 @@ cancel`) and tracker **`IssueState`** (`open` / `in_progress` / `blocked` /
+---+------+------+--+ +---+------+------+--+
| | | | | |
v v v v v v
Gitea/ SQLite GitHub Forgejo SQLite GitHub
Forgejo cache (planned) cache (planned)
``` ```
**Primary fleet path (coordination):** **Primary fleet path (coordination):**
@ -205,3 +224,7 @@ cancel`) and tracker **`IssueState`** (`open` / `in_progress` / `blocked` /
connector decision. connector decision.
- `the-custodian/canon/architecture/adr-001-workplans-as-repo-artefacts.md`. - `the-custodian/canon/architecture/adr-001-workplans-as-repo-artefacts.md`.
- activity-core **ACTIVITY-WP-0022** — IssueSink default policy (emitter side). - activity-core **ACTIVITY-WP-0022** — IssueSink default policy (emitter side).
- activity-core **ACT-ADR-005** — ops runs vs work records; issue-core is not
the ops claim queue.
- `workplans/ISSUE-WP-0006-forgejo-only-projection-boundary.md` — Forgejo-only
product language and projection boundary.

View file

@ -22,7 +22,7 @@ help: ## Show issue core capability help
@echo " issue-backend-list List configured backends" @echo " issue-backend-list List configured backends"
@echo " issue-backend-detect Auto-detect backend from repository" @echo " issue-backend-detect Auto-detect backend from repository"
@echo " issue-backend-set-local Configure local SQLite backend" @echo " issue-backend-set-local Configure local SQLite backend"
@echo " issue-backend-set-gitea Configure Gitea backend" @echo " issue-backend-set-gitea Configure Forgejo backend (Gitea-compatible API)"
@echo "" @echo ""
@echo "Synchronization:" @echo "Synchronization:"
@echo " issue-sync Sync with remote backend" @echo " issue-sync Sync with remote backend"

View file

@ -1,10 +1,10 @@
# Issue Core — External Tracker Connector # Issue Core — External Tracker Connector
**A backend-agnostic connector to third-party issue trackers (Gitea/Forgejo, GitHub, …), not the fleet's internal work-origin or coordination substrate.** **A backend-agnostic connector to third-party issue trackers (Forgejo, GitHub, …), not the fleet's internal work-origin or coordination substrate.**
## Purpose ## Purpose
**issue-core** is a standardized abstraction layer for talking to external issue-tracking backends (Gitea, GitHub, GitLab, local SQLite cache). Instead of each agent or automation implementing platform-specific APIs, they use one consistent CLI and Python interface — and, going forward, a durable **work-record UUID ↔ external issue id** mapping when a tracker projection is switched on. **issue-core** is a standardized abstraction layer for talking to external issue-tracking backends (Forgejo, GitHub, GitLab, local SQLite cache). Instead of each agent or automation implementing platform-specific APIs, they use one consistent CLI and Python interface — and, going forward, a durable **work-record UUID ↔ external issue id** mapping when a tracker projection is switched on. The self-hosted forge is **Forgejo** only (ACT-ADR-005); the HTTP API is Gitea-compatible.
### Fleet coordination vs external trackers ### Fleet coordination vs external trackers
@ -32,7 +32,7 @@ For agent coordination *inside* the Coulomb fleet, use work records + state-hub,
✅ **Fully Implemented:** ✅ **Fully Implemented:**
- Complete CRUD operations (issues, labels, users, milestones, comments) - Complete CRUD operations (issues, labels, users, milestones, comments)
- Gitea backend (production-ready with full API integration) - Forgejo backend (Gitea-compatible API; production-ready)
- Local SQLite backend (offline work with sync capability) - Local SQLite backend (offline work with sync capability)
- CLI with JSON output for machine parsing - CLI with JSON output for machine parsing
- Python API for programmatic access - Python API for programmatic access
@ -72,15 +72,20 @@ Clients authenticate with `Authorization: Bearer <key>` or `X-API-Key: <key>`.
See `SCOPE.md` "TaskSpec payload" for the request schema, or visit See `SCOPE.md` "TaskSpec payload" for the request schema, or visit
`http://<host>:<port>/docs` once the server is running for live OpenAPI docs. `http://<host>:<port>/docs` once the server is running for live OpenAPI docs.
Internal scheduled automation (FI/Binky, mail, …) does **not** poll this
server. Claim/execute is activity-core **`ops_run`** (ACT-ADR-005).
`POST /issues/` is external tracker create/link only.
### Configuration (One-Time Setup) ### Configuration (One-Time Setup)
**For Gitea-backed projects:** **For Forgejo-backed projects:**
```bash ```bash
# Set your Gitea token # Preferred token name (CLI also accepts FORGEJO_API_TOKEN)
export GITEA_API_TOKEN="your-token-here" export FORGEJO_TOKEN="your-token-here"
# Deprecated aliases still work: GITEA_API_TOKEN, GITEA_TOKEN
# Configure backend # Configure backend (type string `gitea` = Forgejo Gitea-compatible API)
issue backend add myproject gitea issue backend add myproject gitea
# Prompts for: URL, owner, repo (reads token from environment) # Prompts for: URL, owner, repo (reads token from environment)
@ -185,7 +190,7 @@ for issue in issues:
┌───────┴────────┐ ┌───────┴────────┐
│ │ │ │
┌──────▼─────┐ ┌──────▼──────┐ ┌──────▼─────┐ ┌──────▼──────┐
│ Local │ │ Gitea │ Local │ │ Forgejo
│ (SQLite) │ │ (REST API) │ │ (SQLite) │ │ (REST API) │
└────────────┘ └─────────────┘ └────────────┘ └─────────────┘
``` ```
@ -200,7 +205,7 @@ for issue in issues:
| Backend | Status | Features | | Backend | Status | Features |
|---------|--------|----------| |---------|--------|----------|
| **Gitea** | ✅ Production | Full API, rate limiting, state mapping | | **Forgejo** (Gitea-compatible API) | ✅ Production | Full API, rate limiting, state mapping |
| **Local SQLite** | ✅ Production | Offline work, fast queries, sync support | | **Local SQLite** | ✅ Production | Offline work, fast queries, sync support |
| **GitHub** | 🚧 Planned (v1.1) | Full API integration | | **GitHub** | 🚧 Planned (v1.1) | Full API integration |
| **GitLab** | 🚧 Planned (v1.2) | Full API integration | | **GitLab** | 🚧 Planned (v1.2) | Full API integration |
@ -209,7 +214,7 @@ for issue in issues:
### 1. External tracker ops (when a backend is in use) ### 1. External tracker ops (when a backend is in use)
Agents or humans work issues that already live in Gitea/GitHub (e.g. OSS Agents or humans work issues that already live in Forgejo/GitHub (e.g. OSS
inbound, customer Jira, or an intentional projection): inbound, customer Jira, or an intentional projection):
```bash ```bash
@ -227,7 +232,7 @@ Fleet-internal task claim/execute should use workplan task status in the repo
### 2. Human collaboration through a tracker UI ### 2. Human collaboration through a tracker UI
When the counterparty only uses GitHub/Gitea, project or update the external When the counterparty only uses GitHub/Forgejo, project or update the external
issue and keep comments there. Mapping design (UUID ↔ external id) will tie issue and keep comments there. Mapping design (UUID ↔ external id) will tie
that projection back to the work record — see `docs/uuid-external-id-mapping.md`. that projection back to the work record — see `docs/uuid-external-id-mapping.md`.
@ -238,14 +243,14 @@ Work offline with local backend, sync when online:
```bash ```bash
# Setup local backup # Setup local backup
issue backend add backup local issue backend add backup local
issue sync pull gitea-production backup issue sync pull forgejo-production backup
# Work offline # Work offline
issue backend set-default backup issue backend set-default backup
issue create "Offline tracker note" --label=offline issue create "Offline tracker note" --label=offline
# Sync back # Sync back
issue sync push backup gitea-production issue sync push backup forgejo-production
``` ```
## CLI Commands Reference ## CLI Commands Reference
@ -318,7 +323,7 @@ issue-core/
│ │ ├── models.py # Issue, Label, User, etc. │ │ ├── models.py # Issue, Label, User, etc.
│ │ └── interfaces.py # IssueBackend, SyncableBackend │ │ └── interfaces.py # IssueBackend, SyncableBackend
│ ├── backends/ │ ├── backends/
│ │ ├── gitea/ # Gitea backend implementation │ │ ├── gitea/ # Forgejo backend (Gitea-compatible API)
│ │ └── local/ # SQLite backend implementation │ │ └── local/ # SQLite backend implementation
│ └── cli/ # Click-based CLI │ └── cli/ # Click-based CLI
│ ├── commands.py # Issue operations │ ├── commands.py # Issue operations
@ -358,7 +363,7 @@ issue map detach --uuid <work-record-uuid>
``` ```
Mappings live in `~/.config/issue-tracker/mappings.db` (bookkeeping), independent Mappings live in `~/.config/issue-tracker/mappings.db` (bookkeeping), independent
of whether CRUD targets Gitea or local SQLite. of whether CRUD targets Forgejo or local SQLite.
## Roadmap (summary) ## Roadmap (summary)
@ -412,7 +417,7 @@ The Issue Core is designed to be extensible:
- [ ] Integration tests with mock API - [ ] Integration tests with mock API
- [ ] Documentation - [ ] Documentation
See existing backends (Gitea, Local) as reference implementations. See existing backends (Forgejo / `gitea` module, Local) as reference implementations.
## Why "Facade" / connector? ## Why "Facade" / connector?
@ -421,7 +426,7 @@ The **Facade Pattern** still describes the *implementation* style:
> *"Provide a unified interface to a set of interfaces in a subsystem."* > *"Provide a unified interface to a set of interfaces in a subsystem."*
> — Gang of Four, Design Patterns > — Gang of Four, Design Patterns
Instead of agents learning different APIs for GitHub (`gh`), GitLab (`glab`), Gitea, JIRA, etc., they use one consistent interface. The facade does not replace issue trackers — and it does **not** replace work records as the origin of fleet work. It connects the fleet to trackers at the boundary. Instead of agents learning different APIs for GitHub (`gh`), GitLab (`glab`), Forgejo, JIRA, etc., they use one consistent interface. The facade does not replace issue trackers — and it does **not** replace work records as the origin of fleet work. It connects the fleet to trackers at the boundary.
## License ## License

View file

@ -28,7 +28,7 @@ replacing workplans/tasks.
✅ **Complete:** ✅ **Complete:**
- Core CRUD operations (100%) - Core CRUD operations (100%)
- Gitea backend (production-ready) - Forgejo backend (Gitea-compatible API; production-ready)
- Local SQLite backend (fully functional) - Local SQLite backend (fully functional)
- CLI with JSON output - CLI with JSON output
- Python programmatic API - Python programmatic API
@ -80,8 +80,8 @@ def detect_git_remote() -> Optional[Dict[str, str]]:
Returns: Returns:
{ {
'platform': 'gitea' | 'github' | 'gitlab', 'platform': 'forgejo' | 'github' | 'gitlab',
'base_url': 'https://gitea.example.com', 'base_url': 'https://forgejo.example.com',
'owner': 'myorg', 'owner': 'myorg',
'repo': 'myproject' 'repo': 'myproject'
} }
@ -90,15 +90,15 @@ def detect_git_remote() -> Optional[Dict[str, str]]:
def parse_remote_url(url: str) -> Optional[Dict[str, str]]: def parse_remote_url(url: str) -> Optional[Dict[str, str]]:
""" """
Parse various git remote URL formats: Parse various git remote URL formats:
- https://gitea.example.com/owner/repo.git - https://forgejo.example.com/owner/repo.git
- git@gitea.example.com:owner/repo.git - git@forgejo.example.com:owner/repo.git
- https://github.com/owner/repo - https://github.com/owner/repo
""" """
``` ```
**Tests:** `tests/test_detection.py` **Tests:** `tests/test_detection.py`
- Test various URL formats (HTTPS, SSH, with/without .git) - Test various URL formats (HTTPS, SSH, with/without .git)
- Test platform detection (Gitea, GitHub, GitLab) - Test platform detection (Forgejo, GitHub, GitLab)
- Test edge cases (subgroups, custom domains) - Test edge cases (subgroups, custom domains)
**Effort:** 2-3 days **Effort:** 2-3 days
@ -112,7 +112,8 @@ def parse_remote_url(url: str) -> Optional[Dict[str, str]]:
def load_backend_from_env() -> Optional[Dict[str, Any]]: def load_backend_from_env() -> Optional[Dict[str, Any]]:
""" """
Load backend config from environment variables: Load backend config from environment variables:
- GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO - FORGEJO_URL, FORGEJO_TOKEN, FORGEJO_OWNER, FORGEJO_REPO
(deprecated aliases: GITEA_URL, GITEA_TOKEN / GITEA_API_TOKEN, …)
- GITHUB_TOKEN (with auto-detection) - GITHUB_TOKEN (with auto-detection)
- GITLAB_URL, GITLAB_TOKEN - GITLAB_URL, GITLAB_TOKEN
""" """
@ -149,10 +150,10 @@ issue config auto
{ {
"backend": { "backend": {
"type": "gitea", "type": "gitea",
"url": "https://gitea.example.com", "url": "https://forgejo.example.com",
"owner": "myorg", "owner": "myorg",
"repo": "myproject", "repo": "myproject",
"token_source": "env:GITEA_TOKEN" // or "file:/path/to/token" "token_source": "env:FORGEJO_TOKEN" // or "file:/path/to/token"; GITEA_* deprecated
}, },
"sync": { "sync": {
"enabled": true, "enabled": true,
@ -247,7 +248,7 @@ if issue backend show "$backend_name" &>/dev/null; then
fi fi
# Offer existing values as defaults # Offer existing values as defaults
read -p "Gitea URL [$CURRENT_URL]: " url read -p "Forgejo URL [$CURRENT_URL]: " url
url="${url:-$CURRENT_URL}" url="${url:-$CURRENT_URL}"
read -p "Repository owner [$CURRENT_OWNER]: " owner read -p "Repository owner [$CURRENT_OWNER]: " owner
@ -403,7 +404,7 @@ class LockManager:
**Storage:** Store claims in issue metadata or separate tracking table. **Storage:** Store claims in issue metadata or separate tracking table.
**For Gitea backend:** **For Forgejo backend:**
```json ```json
// In issue.sync_metadata // In issue.sync_metadata
{ {
@ -788,7 +789,7 @@ def sync_with_strategy(
### Phase 1 Success ### Phase 1 Success
- [ ] Agent can attach to any repo's tracker with zero manual config - [ ] Agent can attach to any repo's tracker with zero manual config
- [ ] Environment-only setup works: `GITEA_TOKEN=xxx issue list` - [ ] Environment-only setup works: `FORGEJO_TOKEN=xxx issue list`
- [ ] Auto-detection accuracy: >95% for common platforms - [ ] Auto-detection accuracy: >95% for common platforms
### Phase 1.5 Success (mapping) ### Phase 1.5 Success (mapping)

View file

@ -55,7 +55,7 @@ A **Capability Implementation** is a concrete realization of a Capability Family
**Characteristics:** **Characteristics:**
- Provides **concrete functionality** (code, CLI, API) - Provides **concrete functionality** (code, CLI, API)
- May implement **multiple backend variants** (Gitea, GitHub, local) - May implement **multiple backend variants** (Forgejo, GitHub, local)
- Has **maturity levels** (experimental, beta, production) - Has **maturity levels** (experimental, beta, production)
- Can be **composed with other capabilities** - Can be **composed with other capabilities**
@ -255,7 +255,7 @@ metadata:
implementation: issue-core implementation: issue-core
version: 1.0.0 version: 1.0.0
description: > description: >
Unified interface for issue tracking across Gitea, GitHub, GitLab. Unified interface for issue tracking across Forgejo, GitHub, GitLab.
Enables agent coordination via standardized issue operations. Enables agent coordination via standardized issue operations.
purpose: purpose:
@ -315,7 +315,7 @@ purpose:
- "Unified issue management for polyglot platform environments" - "Unified issue management for polyglot platform environments"
problems_solved: problems_solved:
- "Direct API calls to GitHub/GitLab/Gitea (avoids credential sprawl)" - "Direct API calls to GitHub/GitLab/Forgejo (avoids credential sprawl)"
- "Inconsistent issue tracking access patterns" - "Inconsistent issue tracking access patterns"
- "Platform-specific code in agents" - "Platform-specific code in agents"
- "Offline/online workflow synchronization" - "Offline/online workflow synchronization"
@ -323,7 +323,7 @@ purpose:
# When to use this capability # When to use this capability
usage_rules: usage_rules:
MUST_USE_INSTEAD_OF: MUST_USE_INSTEAD_OF:
- "Direct Gitea/GitHub/GitLab API calls" - "Direct Forgejo/GitHub/GitLab API calls"
- "Platform-specific CLIs (gh, glab)" - "Platform-specific CLIs (gh, glab)"
- "Python libraries (PyGithub, python-gitlab)" - "Python libraries (PyGithub, python-gitlab)"
@ -361,7 +361,7 @@ integration:
required: true required: true
method: manual # auto-detection planned for v1.1 method: manual # auto-detection planned for v1.1
steps: steps:
- "Export GITEA_API_TOKEN environment variable" - "Export FORGEJO_TOKEN environment variable (GITEA_API_TOKEN is a deprecated alias)"
- "Run: issue backend add myproject gitea" - "Run: issue backend add myproject gitea"
- "Provide: URL, owner, repo when prompted" - "Provide: URL, owner, repo when prompted"
- "Run: issue backend set-default myproject" - "Run: issue backend set-default myproject"
@ -416,8 +416,8 @@ efficiency:
credentials: credentials:
method: environment_variables method: environment_variables
variables: variables:
- GITEA_API_TOKEN - FORGEJO_TOKEN # preferred; GITEA_API_TOKEN is a deprecated alias
- GITEA_URL # optional with config - FORGEJO_URL # optional with config; GITEA_URL deprecated alias
security: security:
- "Tokens never in code or logs" - "Tokens never in code or logs"
@ -736,7 +736,7 @@ metadata:
``` ```
Project A needs GitHub issues Project A needs GitHub issues
Project B needs GitLab issues Project B needs GitLab issues
Project C needs Gitea issues Project C needs Forgejo issues
→ Pattern: "We need issue tracking" → Pattern: "We need issue tracking"
→ Family: "issue-tracking" → Family: "issue-tracking"

View file

@ -25,7 +25,7 @@ aspirational lists when deciding “does issue-core already do X?”
| Concept | Notes | | Concept | Notes |
| --- | --- | | --- | --- |
| `Issue` | id, number, title, description, state, timestamps, labels, assignees, milestone, comments, backend_id/type, `sync_metadata` | | `Issue` | id, number, title, description, state, timestamps, labels, assignees, milestone, comments, backend_id/type, `sync_metadata` |
| `IssueState` | `open`, `closed`, `in_progress`, `blocked` (Gitea maps non-closed → open on the wire) | | `IssueState` | `open`, `closed`, `in_progress`, `blocked` (Forgejo Gitea-compatible API maps non-closed → open on the wire) |
| `Label` / priority / type | Priority and type are primarily **label conventions** (`priority:high`, `bug`, …), not separate backend columns everywhere | | `Label` / priority / type | Priority and type are primarily **label conventions** (`priority:high`, `bug`, …), not separate backend columns everywhere |
| `User`, `Milestone`, `Comment` | First-class models on the backend interface | | `User`, `Milestone`, `Comment` | First-class models on the backend interface |
| `Priority`, `IssueType` enums | Helpers for label-derived classification | | `Priority`, `IssueType` enums | Helpers for label-derived classification |
@ -46,7 +46,7 @@ aspirational lists when deciding “does issue-core already do X?”
| Backend | Module | Role | | Backend | Module | Role |
| --- | --- | --- | | --- | --- | --- |
| **Local SQLite** | `issue_core.backends.local` | Offline store; hard delete supported; bulk ops; sync-capable | | **Local SQLite** | `issue_core.backends.local` | Offline store; hard delete supported; bulk ops; sync-capable |
| **Gitea** | `issue_core.backends.gitea` | Remote REST; **Forgejo-compatible** in railiance01 deploy; no true delete (close-as-delete); rate-limit aware; sync-capable | | **Forgejo** | `issue_core.backends.gitea` | Remote REST via **Forgejo (Gitea-compatible API)**; backend type string remains `gitea`. Gitea is not a second product. No true delete (close-as-delete); rate-limit aware; sync-capable |
**Not implemented as code:** GitHub, GitLab, Jira backends (optional deps may be **Not implemented as code:** GitHub, GitLab, Jira backends (optional deps may be
declared in `pyproject.toml`; no backend package under `issue_core/backends/`). declared in `pyproject.toml`; no backend package under `issue_core/backends/`).
@ -64,7 +64,9 @@ declared in `pyproject.toml`; no backend package under `issue_core/backends/`).
- JSON-friendly output for agents (`--format=json` on list/show paths). - JSON-friendly output for agents (`--format=json` on list/show paths).
- Backend configs: `~/.config/issue-tracker/` (default backend + named configs). - Backend configs: `~/.config/issue-tracker/` (default backend + named configs).
- Mapping store: `~/.config/issue-tracker/mappings.db` (independent of CRUD backend). - Mapping store: `~/.config/issue-tracker/mappings.db` (independent of CRUD backend).
- Gitea token typically from env (`GITEA_API_TOKEN` / config); never commit secrets. - Forgejo token from env: **`FORGEJO_TOKEN`** (preferred) or `FORGEJO_API_TOKEN`;
legacy **`GITEA_API_TOKEN` / `GITEA_TOKEN`** are deprecated aliases the CLI
still accepts. Never commit secrets.
### 1.4b Work-record mapping (shipped v0.2.x) ### 1.4b Work-record mapping (shipped v0.2.x)
@ -103,7 +105,7 @@ also `sync_metadata.mapping` and a row in `mappings.db`.
### 1.6 Synchronization (CLI) ### 1.6 Synchronization (CLI)
- Bidirectional pull/push between configured backends (e.g. Gitea ↔ local). - Bidirectional pull/push between configured backends (e.g. Forgejo ↔ local).
- Uses `get_issues_modified_since` / conflict hooks where backends implement them. - Uses `get_issues_modified_since` / conflict hooks where backends implement them.
- Conflict handling is basic (operator / force flags) — not a full merge engine. - Conflict handling is basic (operator / force flags) — not a full merge engine.
@ -111,7 +113,7 @@ also `sync_metadata.mapping` and a row in `mappings.db`.
| Artifact | Provides | | Artifact | Provides |
| --- | --- | | --- | --- |
| PyPI package | Built wheel/sdist; Makefile targets publish to Coulomb Gitea/Forgejo registry | | PyPI package | Built wheel/sdist; Makefile targets publish to Coulomb Forgejo registry |
| Docker image | Built and published by this app repo; production consumes an immutable digest | | Docker image | Built and published by this app repo; production consumes an immutable digest |
| `rapp-issue-core` | Authoritative railiance01 runtime package: manifests, private Service, ExternalSecret references, NetworkPolicy, rollout, rollback, and live evidence | | `rapp-issue-core` | Authoritative railiance01 runtime package: manifests, private Service, ExternalSecret references, NetworkPolicy, rollout, rollback, and live evidence |
| `docs/package-release.md` | Release notes for packaging | | `docs/package-release.md` | Release notes for packaging |
@ -148,7 +150,7 @@ Things this repo **owns** and may grow, consistent with the connector role.
backends when tracker use is intentional. backends when tracker use is intentional.
- List and filter (state, labels, assignee, milestone, text search as backends allow). - List and filter (state, labels, assignee, milestone, text search as backends allow).
- Label, assignee, and milestone management through the backend interface. - Label, assignee, and milestone management through the backend interface.
- Delete where the backend allows (SQLite hard delete; Gitea effectively close). - Delete where the backend allows (SQLite hard delete; Forgejo effectively close).
### 2.2 Connector / mapping (owned direction) ### 2.2 Connector / mapping (owned direction)
@ -164,7 +166,9 @@ Things this repo **owns** and may grow, consistent with the connector role.
### 2.3 Surfaces ### 2.3 Surfaces
- CLI for humans and agents on a shell. - CLI for humans and agents on a shell.
- REST for intentional automation and worker claim/list/close. - REST for intentional **external tracker** create/link and worker
list/update/close of tracker issues. This is **not** the fleet ops claim
queue (ACT-ADR-005 / activity-core `ops_run`).
- Python library for embedding. - Python library for embedding.
- Optional future NATS consumer **only** for intentional external projection - Optional future NATS consumer **only** for intentional external projection
(never silent fleet work origin). (never silent fleet work origin).
@ -204,6 +208,24 @@ activity-core (and peers) must not treat `POST /issues/` as the always-on
default for every matched rule. Emitters own that policy; issue-core keeps default for every matched rule. Emitters own that policy; issue-core keeps
accepting authenticated POSTs without advertising itself as a landing zone. accepting authenticated POSTs without advertising itself as a landing zone.
Follow-up: `activity-core` **ACTIVITY-WP-0022** (from ISSUE-WP-0004-T05). Follow-up: `activity-core` **ACTIVITY-WP-0022** (from ISSUE-WP-0004-T05).
Default activity-core sink remains **state-hub progress + `ops_run`**, not
REST to issue-core for Binky/FI.
### 3.3b Ops claim queue (ACT-ADR-005) — not this repo
Internal scheduled automation **claim/execute** belongs to activity-core
**`ops_run`** ([ACT-ADR-005](../activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md)).
issue-core does **not** provide the fleet ops claim queue. Do not poll
`GET /issues/` as “the automation queue.”
ACT-ADR-005: *issue-cores correct role is a connector facade over
external trackers (Forgejo, GitHub, Jira, …). It is not the origin of
work records and not the default internal ops queue. Gitea is out of
scope for this fleet; the self-hosted forge is Forgejo.*
`POST /issues/` = external tracker create/link only. rein-aharness
`poll --source=issue-core` is **legacy / external ticket** path only
(see ISSUE-WP-0006, REINAH-WP-0005-T06).
### 3.4 Project management ### 3.4 Project management
@ -246,8 +268,8 @@ Emitters record who/what spawned an external issue (e.g. activity-core
| --- | --- | --- | | --- | --- | --- |
| Human / agent shell | CLI | Tracker admin; `project` / `map` for work-record links | | Human / agent shell | CLI | Tracker admin; `project` / `map` for work-record links |
| Library consumers | Python API + `MappingService` | Same backends + mapping store without shell | | Library consumers | Python API + `MappingService` | Same backends + mapping store without shell |
| Workers (e.g. harness) | REST GET/PATCH | List / claim / close external issues when in the loop | | Workers (e.g. harness) | REST GET/PATCH | List / update / close **external tracker** issues when that tracker is in use — **not** the fleet ops claim queue |
| activity-core IssueSink | REST POST | **Optional, intentional** external issues only — not default for internal findings; may pass `work_record_uuid` | | activity-core IssueSink | REST POST | **Optional, intentional** external issues only — not default for internal findings or Binky/FI; may pass `work_record_uuid`. Default sink is state-hub progress + `ops_run` (ACT-ADR-005) |
| Mapping-aware emitters | CLI or POST `/issues/` with UUID | Project/link work-record UUID ↔ external id | | Mapping-aware emitters | CLI or POST `/issues/` with UUID | Project/link work-record UUID ↔ external id |
### 4.3 TaskSpec contract (`POST /issues/`) ### 4.3 TaskSpec contract (`POST /issues/`)
@ -287,14 +309,16 @@ Retained for intentional emits and backward compatibility:
``` ```
`issue_id` is the **backend** issue identity for the emitters log; not a `issue_id` is the **backend** issue identity for the emitters log; not a
work-record UUID. (`github` appears in the response enum for forward work-record UUID. The wire value `gitea` is the Forgejo (Gitea-compatible
compatibility; no GitHub backend is shipped yet.) API) backend type string — not a second Gitea product. (`github` appears in
the response enum for forward compatibility; no GitHub backend is shipped
yet.)
### 4.4 Credential routing (operators / agents) ### 4.4 Credential routing (operators / agents)
| Need | Owner | | Need | Owner |
| --- | --- | | --- | --- |
| Gitea/Forgejo API token for backend | OpenBao / operator path (`warden route`) | | Forgejo API token for backend (`FORGEJO_TOKEN`; legacy `GITEA_*` alias) | OpenBao / operator path (`warden route`) |
| `ISSUE_CORE_API_KEY` for REST | Shared secret via deploy secrets (e.g. ExternalSecret) | | `ISSUE_CORE_API_KEY` for REST | Shared secret via deploy secrets (e.g. ExternalSecret) |
| SSH certs | ops-warden only | | SSH certs | ops-warden only |
@ -309,7 +333,7 @@ issue_core/
core/ # models, IssueBackend ABC, factory, MappingService core/ # models, IssueBackend ABC, factory, MappingService
backends/ backends/
local/ # SQLite issue store local/ # SQLite issue store
gitea/ # Gitea/Forgejo REST gitea/ # Forgejo REST (Gitea-compatible API)
cli/ # Click: issue, project, map, backend, sync, serve cli/ # Click: issue, project, map, backend, sync, serve
api/ # FastAPI: ingest + query (+ auth, schemas) api/ # FastAPI: ingest + query (+ auth, schemas)
tests/ tests/
@ -326,7 +350,7 @@ tests, image construction, and Forgejo image publication.
| Language | Python 3.8+ | | Language | Python 3.8+ |
| CLI | Click | | CLI | Click |
| HTTP | FastAPI + Pydantic v2 + uvicorn (`[api]` extra) | | HTTP | FastAPI + Pydantic v2 + uvicorn (`[api]` extra) |
| HTTP client (Gitea) | requests | | HTTP client (Forgejo) | requests |
| Issue store (local) | SQLite (`issues.db`) | | Issue store (local) | SQLite (`issues.db`) |
| Mapping store | SQLite (`mappings.db`, separate file) | | Mapping store | SQLite (`mappings.db`, separate file) |
| Tests | pytest | | Tests | pytest |
@ -350,3 +374,5 @@ tests, image construction, and Forgejo image publication.
(issue-core issues = external projections only) (issue-core issues = external projections only)
- `the-custodian/research/WorkOrchestrationArchitectureDraft.md` §4.2 - `the-custodian/research/WorkOrchestrationArchitectureDraft.md` §4.2
- activity-core IssueSink / **ACTIVITY-WP-0022** — emitter-side default policy - activity-core IssueSink / **ACTIVITY-WP-0022** — emitter-side default policy
- activity-core **ACT-ADR-005**`ops_run` is the fleet ops claim queue; not issue-core
- `workplans/ISSUE-WP-0006-forgejo-only-projection-boundary.md` — Forgejo-only + projection boundary

View file

@ -2,9 +2,10 @@
# Render issue-core backends.json from environment, then start the API. # Render issue-core backends.json from environment, then start the API.
# #
# The backend structure (host/owner/repo/default) is non-secret and supplied # The backend structure (host/owner/repo/default) is non-secret and supplied
# via the BACKENDS_TEMPLATE env (a ConfigMap), with the Gitea token injected # via the BACKENDS_TEMPLATE env (a ConfigMap), with the Forgejo token injected
# from GITEA_BACKEND_TOKEN (an ExternalSecret-materialized Secret). The token # from FORGEJO_BACKEND_TOKEN (preferred) or GITEA_BACKEND_TOKEN (deprecated
# is never baked into the image or committed to Git. # alias; ExternalSecret-materialized Secret). The token is never baked into
# the image or committed to Git.
set -eu set -eu
CONFIG_DIR="${HOME}/.config/issue-tracker" CONFIG_DIR="${HOME}/.config/issue-tracker"
@ -14,12 +15,13 @@ mkdir -p "${CONFIG_DIR}"
# Substitute the token placeholder using python (always present in the image) # Substitute the token placeholder using python (always present in the image)
# to avoid shell-escaping issues with the secret value. # to avoid shell-escaping issues with the secret value.
FORGEJO_BACKEND_TOKEN="${FORGEJO_BACKEND_TOKEN:-}" \
GITEA_BACKEND_TOKEN="${GITEA_BACKEND_TOKEN:-}" \ GITEA_BACKEND_TOKEN="${GITEA_BACKEND_TOKEN:-}" \
BACKENDS_TEMPLATE="${BACKENDS_TEMPLATE}" \ BACKENDS_TEMPLATE="${BACKENDS_TEMPLATE}" \
python - "${CONFIG_DIR}/backends.json" <<'PY' python - "${CONFIG_DIR}/backends.json" <<'PY'
import json, os, sys import json, os, sys
tmpl = json.loads(os.environ["BACKENDS_TEMPLATE"]) tmpl = json.loads(os.environ["BACKENDS_TEMPLATE"])
token = os.environ.get("GITEA_BACKEND_TOKEN", "") token = os.environ.get("FORGEJO_BACKEND_TOKEN") or os.environ.get("GITEA_BACKEND_TOKEN", "")
for cfg in tmpl.values(): for cfg in tmpl.values():
if isinstance(cfg, dict) and cfg.get("token") == "__FROM_ENV__": if isinstance(cfg, dict) and cfg.get("token") == "__FROM_ENV__":
cfg["token"] = token cfg["token"] = token

View file

@ -21,7 +21,7 @@
| Side | Key | | Side | Key |
| --- | --- | | --- | --- |
| Work record | **UUIDv7** (bookkeeping); canonical id optional denorm for UX | | Work record | **UUIDv7** (bookkeeping); canonical id optional denorm for UX |
| External | `(backend, external_id)` where backend is `sqlite` \| `gitea` \| … | | External | `(backend, external_id)` where backend is `sqlite` \| `gitea` (Forgejo) \| … |
## Outward status table (task kind + pass-through) ## Outward status table (task kind + pass-through)

View file

@ -87,7 +87,7 @@ without naming UUIDv7 or the human-facing id.
**Why it matters:** Mapping implementation and CLI (`issue project **Why it matters:** Mapping implementation and CLI (`issue project
ISSUE-WP-…`) will almost always start from the **canonical name**; storage ISSUE-WP-…`) will almost always start from the **canonical name**; storage
key should be UUID. INTENTs UUID-only phrasing can steer readers to hide key should be UUID. INTENTs UUID-only phrasing can steer readers to hide
canonical ids or treat Gitea numbers as peer identity to human task names. canonical ids or treat Forgejo numbers as peer identity to human task names.
**Recommendation:** In INTENT “Why / Mapping”, say: **Recommendation:** In INTENT “Why / Mapping”, say:
@ -215,7 +215,7 @@ Framework makes `lane` mandatory on every work record; tags and budgets are
normative. normative.
INTENT is silent — **acceptable** if projections never invent parallel INTENT is silent — **acceptable** if projections never invent parallel
lane/budget models on issues. Soft risk if Gitea labels start carrying a lane/budget models on issues. Soft risk if Forgejo labels start carrying a
second autonomy model (`lane:green` as issue labels without mapping rules). second autonomy model (`lane:green` as issue labels without mapping rules).
**Recommendation:** Mapping/projection: lane, tags, budget stay on the work **Recommendation:** Mapping/projection: lane, tags, budget stay on the work
@ -231,7 +231,7 @@ INTENTs “What it is” lists responsibilities that mix **shipped** and
| Responsibility in INTENT | Shipped? | | Responsibility in INTENT | Shipped? |
| --- | --- | | --- | --- |
| Projection / CRUD on backends | **Yes** (SQLite, Gitea) | | Projection / CRUD on backends | **Yes** (SQLite, Forgejo) |
| Mapping UUID ↔ external id | **No** — design only | | Mapping UUID ↔ external id | **No** — design only |
| Two-way boundary sync | **No** — CLI backend sync ≠ work-record boundary sync | | Two-way boundary sync | **No** — CLI backend sync ≠ work-record boundary sync |
| CLI + REST | **Yes** | | CLI + REST | **Yes** |
@ -295,7 +295,7 @@ defaults, rewriting AGENT_INTEGRATION examples — those are separate work.
| Connector role | Yes | Yes | Yes | | Connector role | Yes | Yes | Yes |
| Not work origin | Yes | §3.1 | Yes | | Not work origin | Yes | §3.1 | Yes |
| Mapping | Target | Design + not shipped | Yes if INTENT softens “What it is” | | Mapping | Target | Design + not shipped | Yes if INTENT softens “What it is” |
| Backends | SQLite + Gitea (+ planned) | Same | Yes | | Backends | SQLite + Forgejo (+ planned) | Same | Yes |
| REST TaskSpec | Optional intentional | Full contract | Yes | | REST TaskSpec | Optional intentional | Full contract | Yes |
| State-hub runtime events from package | Downstream mention | Explicitly not shipped | Mild aspirational gap in older SCOPE; current SCOPE honest | | State-hub runtime events from package | Downstream mention | Explicitly not shipped | Mild aspirational gap in older SCOPE; current SCOPE honest |

View file

@ -15,7 +15,7 @@ make package-check
Publish to the Coulomb organization registry: Publish to the Coulomb organization registry:
```bash ```bash
TWINE_USERNAME=<gitea-user> \ TWINE_USERNAME=<forgejo-user> \
TWINE_PASSWORD=<package-token> \ TWINE_PASSWORD=<package-token> \
make publish-forgejo make publish-forgejo
``` ```

View file

@ -11,13 +11,13 @@ kinds in `the-custodian/canon/standards/work-record-types_v0.1.md`
## Problem ## Problem
When collaboration requires a third-party tracker (Forgejo/Gitea, GitHub, When collaboration requires a third-party tracker (Forgejo, GitHub,
Jira, …), the fleet still needs a stable link between: Jira, …), the fleet still needs a stable link between:
| Side | Identity today | Owner | | Side | Identity today | Owner |
| --- | --- | --- | | --- | --- | --- |
| Internal work record | UUIDv7 written back as `state_hub_*_id` (workplan/task/intake/…) | repo file + state-hub | | Internal work record | UUIDv7 written back as `state_hub_*_id` (workplan/task/intake/…) | repo file + state-hub |
| External issue | Backend issue id / number (+ URL) | Gitea, GitHub, … via issue-core | | External issue | Backend issue id / number (+ URL) | Forgejo, GitHub, … via issue-core |
Without an explicit mapping: Without an explicit mapping:
@ -95,7 +95,7 @@ mapping:
work_record_uuid: <uuidv7> # primary internal key work_record_uuid: <uuidv7> # primary internal key
work_record_id: "ISSUE-WP-0004-T01" # canonical name, optional denorm work_record_id: "ISSUE-WP-0004-T01" # canonical name, optional denorm
work_record_kind: task # task | intake | workplan | … work_record_kind: task # task | intake | workplan | …
backend: gitea # gitea | github | gitlab | jira | sqlite backend: gitea # forgejo product; type string `gitea` | github | gitlab | jira | sqlite
external_id: "176" # backend-native issue id/number external_id: "176" # backend-native issue id/number
external_url: "https://…/issues/176" external_url: "https://…/issues/176"
target_repo: "coulomb/example" # optional routing hint target_repo: "coulomb/example" # optional routing hint
@ -123,7 +123,7 @@ issue-core only:
```yaml ```yaml
# on a task / intake YAML block (illustrative — exact key names TBD with canon) # on a task / intake YAML block (illustrative — exact key names TBD with canon)
external_tracker: external_tracker:
backend: gitea backend: forgejo # product; stored type may be `gitea`
issue_id: "176" issue_id: "176"
issue_url: "https://…" issue_url: "https://…"
mapped_by: issue-core mapped_by: issue-core
@ -132,7 +132,7 @@ external_tracker:
Alternatively a single string field if canon prefers flat keys: Alternatively a single string field if canon prefers flat keys:
```text ```text
issue_core_external_ref: "gitea:coulomb/example#176" issue_core_external_ref: "forgejo:coulomb/example#176"
``` ```
Canon PR owns the field name; issue-core owns the mapping authority. Canon PR owns the field name; issue-core owns the mapping authority.
@ -142,7 +142,7 @@ Canon PR owns the field name; issue-core owns the mapping authority.
Recommended v1: Recommended v1:
1. **Local SQLite** table `work_record_issue_map` (even when primary CRUD 1. **Local SQLite** table `work_record_issue_map` (even when primary CRUD
backend is Gitea) — mapping is fleet bookkeeping, not a Gitea concept. backend is Forgejo) — mapping is fleet bookkeeping, not a Forgejo concept.
2. **Issue.sync_metadata.mapping** (or top-level) echo for convenience when 2. **Issue.sync_metadata.mapping** (or top-level) echo for convenience when
the issue is loaded: the issue is loaded:
```json ```json
@ -154,7 +154,7 @@ Recommended v1:
} }
} }
``` ```
3. Optional Gitea label / body footer for human visibility in the tracker UI 3. Optional Forgejo label / body footer for human visibility in the tracker UI
(non-authoritative). (non-authoritative).
### API / CLI (sketch only) ### API / CLI (sketch only)
@ -255,7 +255,7 @@ increment. Until enabled, connector load is zero beyond idle process cost.
documenting multi-tracker projection is future work. documenting multi-tracker projection is future work.
- **SQLite-only offline:** mapping table lives with local DB; sync of - **SQLite-only offline:** mapping table lives with local DB; sync of
issues and mappings to remote is coordinated (mapping rows are not issues and mappings to remote is coordinated (mapping rows are not
pushed as Gitea native objects). pushed as Forgejo native objects).
## Implementation sketch (later workplan) ## Implementation sketch (later workplan)

View file

@ -12,7 +12,8 @@ This directory contains working examples of autonomous agents using the Issue Co
2. **Configure backend** (one-time setup): 2. **Configure backend** (one-time setup):
```bash ```bash
export GITEA_API_TOKEN="your-token" export FORGEJO_TOKEN="your-token"
# Deprecated alias: GITEA_API_TOKEN
issue backend add myproject gitea issue backend add myproject gitea
# Enter: URL, owner, repo when prompted # Enter: URL, owner, repo when prompted
issue backend set-default myproject issue backend set-default myproject
@ -20,10 +21,11 @@ This directory contains working examples of autonomous agents using the Issue Co
3. **Set environment variables** for scripts: 3. **Set environment variables** for scripts:
```bash ```bash
export GITEA_URL=https://gitea.example.com export FORGEJO_URL=https://forgejo.example.com
export GITEA_TOKEN=your-token export FORGEJO_TOKEN=your-token
export GITEA_OWNER=your-org export FORGEJO_OWNER=your-org
export GITEA_REPO=your-repo export FORGEJO_REPO=your-repo
# Deprecated aliases: GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO
``` ```
## Examples ## Examples
@ -212,7 +214,7 @@ issue backend add myproject gitea
### "Authentication failed" ### "Authentication failed"
```bash ```bash
# Check token is valid # Check token is valid
curl -H "Authorization: token $GITEA_TOKEN" $GITEA_URL/api/v1/user curl -H "Authorization: token $FORGEJO_TOKEN" $FORGEJO_URL/api/v1/user
``` ```
### "No issues found" ### "No issues found"

View file

@ -9,10 +9,10 @@ This agent demonstrates agent-human collaboration:
4. Agent can ask questions and wait for answers 4. Agent can ask questions and wait for answers
Usage: Usage:
export GITEA_URL=https://gitea.example.com export FORGEJO_URL=https://forgejo.example.com
export GITEA_TOKEN=your-token export FORGEJO_TOKEN=your-token
export GITEA_OWNER=your-org export FORGEJO_OWNER=your-org
export GITEA_REPO=your-repo export FORGEJO_REPO=your-repo
python human_in_loop.py python human_in_loop.py
""" """
@ -40,11 +40,11 @@ class HumanInLoopAgent:
self.backend = None self.backend = None
def connect(self): def connect(self):
"""Connect to backend.""" """Connect to Forgejo backend."""
base_url = os.environ['GITEA_URL'] base_url = os.environ.get('FORGEJO_URL') or os.environ['GITEA_URL']
token = os.environ['GITEA_TOKEN'] token = os.environ.get('FORGEJO_TOKEN') or os.environ.get('FORGEJO_API_TOKEN') or os.environ['GITEA_TOKEN']
owner = os.environ['GITEA_OWNER'] owner = os.environ.get('FORGEJO_OWNER') or os.environ['GITEA_OWNER']
repo = os.environ['GITEA_REPO'] repo = os.environ.get('FORGEJO_REPO') or os.environ['GITEA_REPO']
self.backend = GiteaBackend() self.backend = GiteaBackend()
self.backend.connect({ self.backend.connect({

View file

@ -9,10 +9,10 @@ This agent monitors issue health and sends alerts:
- Reports on project velocity and bottlenecks - Reports on project velocity and bottlenecks
Usage: Usage:
export GITEA_URL=https://gitea.example.com export FORGEJO_URL=https://forgejo.example.com
export GITEA_TOKEN=your-token export FORGEJO_TOKEN=your-token
export GITEA_OWNER=your-org export FORGEJO_OWNER=your-org
export GITEA_REPO=your-repo export FORGEJO_REPO=your-repo
python monitoring_agent.py [--stale-days=7] [--check-interval=3600] python monitoring_agent.py [--stale-days=7] [--check-interval=3600]
""" """
@ -42,11 +42,11 @@ class MonitoringAgent:
self.backend = None self.backend = None
def connect(self): def connect(self):
"""Connect to backend.""" """Connect to Forgejo backend."""
base_url = os.environ['GITEA_URL'] base_url = os.environ.get('FORGEJO_URL') or os.environ['GITEA_URL']
token = os.environ['GITEA_TOKEN'] token = os.environ.get('FORGEJO_TOKEN') or os.environ.get('FORGEJO_API_TOKEN') or os.environ['GITEA_TOKEN']
owner = os.environ['GITEA_OWNER'] owner = os.environ.get('FORGEJO_OWNER') or os.environ['GITEA_OWNER']
repo = os.environ['GITEA_REPO'] repo = os.environ.get('FORGEJO_REPO') or os.environ['GITEA_REPO']
self.backend = GiteaBackend() self.backend = GiteaBackend()
self.backend.connect({ self.backend.connect({

View file

@ -11,10 +11,10 @@ Demonstrates a CI/CD-like pipeline with multiple specialized agents:
Each agent monitors for issues in their stage and advances them through the pipeline. Each agent monitors for issues in their stage and advances them through the pipeline.
Usage: Usage:
export GITEA_URL=https://gitea.example.com export FORGEJO_URL=https://forgejo.example.com
export GITEA_TOKEN=your-token export FORGEJO_TOKEN=your-token
export GITEA_OWNER=your-org export FORGEJO_OWNER=your-org
export GITEA_REPO=your-repo export FORGEJO_REPO=your-repo
# Run all agents in parallel (in separate terminals) # Run all agents in parallel (in separate terminals)
python multi_agent_pipeline.py --agent=coder python multi_agent_pipeline.py --agent=coder
@ -50,11 +50,11 @@ class BaseAgent:
self.backend = None self.backend = None
def connect(self): def connect(self):
"""Connect to backend from environment.""" """Connect to Forgejo backend from environment."""
base_url = os.environ['GITEA_URL'] base_url = os.environ.get('FORGEJO_URL') or os.environ['GITEA_URL']
token = os.environ['GITEA_TOKEN'] token = os.environ.get('FORGEJO_TOKEN') or os.environ.get('FORGEJO_API_TOKEN') or os.environ['GITEA_TOKEN']
owner = os.environ['GITEA_OWNER'] owner = os.environ.get('FORGEJO_OWNER') or os.environ['GITEA_OWNER']
repo = os.environ['GITEA_REPO'] repo = os.environ.get('FORGEJO_REPO') or os.environ['GITEA_REPO']
self.backend = GiteaBackend() self.backend = GiteaBackend()
self.backend.connect({ self.backend.connect({

View file

@ -9,10 +9,10 @@ This agent demonstrates a basic workflow:
4. Report completion and close the issue 4. Report completion and close the issue
Usage: Usage:
export GITEA_URL=https://gitea.example.com export FORGEJO_URL=https://forgejo.example.com
export GITEA_TOKEN=your-token export FORGEJO_TOKEN=your-token
export GITEA_OWNER=your-org export FORGEJO_OWNER=your-org
export GITEA_REPO=your-repo export FORGEJO_REPO=your-repo
python simple_task_executor.py python simple_task_executor.py
""" """
@ -39,16 +39,22 @@ class SimpleTaskExecutor:
self.backend = None self.backend = None
def connect(self): def connect(self):
"""Connect to Gitea backend from environment variables.""" """Connect to Forgejo backend from environment variables."""
base_url = os.environ.get('GITEA_URL') base_url = os.environ.get('FORGEJO_URL') or os.environ.get('GITEA_URL')
token = os.environ.get('GITEA_TOKEN') token = (
owner = os.environ.get('GITEA_OWNER') os.environ.get('FORGEJO_TOKEN')
repo = os.environ.get('GITEA_REPO') or os.environ.get('FORGEJO_API_TOKEN')
or os.environ.get('GITEA_TOKEN')
or os.environ.get('GITEA_API_TOKEN')
)
owner = os.environ.get('FORGEJO_OWNER') or os.environ.get('GITEA_OWNER')
repo = os.environ.get('FORGEJO_REPO') or os.environ.get('GITEA_REPO')
if not all([base_url, token, owner, repo]): if not all([base_url, token, owner, repo]):
raise ValueError( raise ValueError(
"Missing required environment variables: " "Missing required environment variables: "
"GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO" "FORGEJO_URL, FORGEJO_TOKEN, FORGEJO_OWNER, FORGEJO_REPO "
"(deprecated aliases: GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO)"
) )
self.backend = GiteaBackend() self.backend = GiteaBackend()

View file

@ -1,9 +1,10 @@
schema_version: open-reuse.integration.v0.1 schema_version: open-reuse.integration.v0.1
id: issue-core-gitea id: issue-core-gitea
name: issue-core Gitea Backend name: issue-core Forgejo Backend
description: > description: >
Pluggable remote backend that maps the issue-core unified issue model onto the Pluggable remote backend that maps the issue-core unified issue model onto the
Gitea issues API for external-tracker projection and synchronization. Forgejo issues API (Gitea-compatible) for external-tracker projection and
synchronization. Gitea is not a second supported product.
status: registered status: registered
owner: issue-core owner: issue-core
@ -13,9 +14,9 @@ local:
system: issue-core system: issue-core
upstream: upstream:
name: Gitea name: Forgejo (Gitea-compatible API)
project_url: https://github.com/go-gitea/gitea project_url: https://codeberg.org/forgejo/forgejo
homepage: https://about.gitea.com/ homepage: https://forgejo.org/
version_policy: gitea-api-v1 version_policy: gitea-api-v1
monitor: monitor:
releases: true releases: true
@ -29,14 +30,14 @@ reuse:
- plugin - plugin
risk_level: medium risk_level: medium
rationale: > rationale: >
Gitea REST API is wrapped behind the RemoteBackend interface; local task Forgejo (Gitea-compatible) REST API is wrapped behind the RemoteBackend
lifecycle semantics remain stable across backend swaps. interface; local task lifecycle semantics remain stable across backend swaps.
boundary: boundary:
type: adapter type: adapter
local_adapter: issue_core.backends.gitea.backend.GiteaBackend local_adapter: issue_core.backends.gitea.backend.GiteaBackend
local_interface: issue_core.core.interfaces.RemoteBackend local_interface: issue_core.core.interfaces.RemoteBackend
reused_surface: Gitea /api/v1 issues, labels, milestones, comments reused_surface: Forgejo (Gitea-compatible) /api/v1 issues, labels, milestones, comments
contracts: contracts:
- issue-core.backend.v1 - issue-core.backend.v1
fragility_points: fragility_points:

View file

@ -2,12 +2,12 @@
issue-core external issue-tracker connector issue-core external issue-tracker connector
Backend-agnostic CLI, library, and optional REST surface for operating on Backend-agnostic CLI, library, and optional REST surface for operating on
third-party issue trackers (Gitea/Forgejo, local SQLite). Not the origin of third-party issue trackers (Forgejo via Gitea-compatible API, local SQLite).
fleet work records (see INTENT.md / SCOPE.md). Not the origin of fleet work records (see INTENT.md / SCOPE.md).
Shipped: Shipped:
- Unified issue model and IssueBackend plugin architecture - Unified issue model and IssueBackend plugin architecture
- Local SQLite + Gitea backends; bidirectional sync CLI - Local SQLite + Forgejo backends; bidirectional sync CLI
- CLI (`issue` / `issue-core`) and optional FastAPI REST (`issue serve`) - CLI (`issue` / `issue-core`) and optional FastAPI REST (`issue serve`)
""" """

View file

@ -7,5 +7,5 @@ API regardless of the underlying issue tracking system.
Available Backends: Available Backends:
- local: SQLite-based local backend for offline use - local: SQLite-based local backend for offline use
- gitea: Gitea API backend for GitHub-compatible systems - gitea: Forgejo backend (Gitea-compatible API)
""" """

View file

@ -1,12 +1,11 @@
""" """
Gitea Backend Forgejo backend (Gitea-compatible API).
A backend implementation for Gitea issue tracking systems. Talks to the self-hosted Forgejo HTTP API. Gitea is not a second supported
This backend provides integration with Gitea API for remote issue management. product. The backend type string remains `gitea`.
Features: Features:
- Full Gitea API integration - Full Forgejo / Gitea-compatible API integration
- GitHub-compatible operations
- Remote synchronization - Remote synchronization
- Authentication support - Authentication support
- Rate limiting compliance - Rate limiting compliance

View file

@ -1,8 +1,9 @@
""" """
Gitea Backend Implementation Forgejo backend (Gitea-compatible API).
Provides integration with Gitea API for remote issue tracking. Talks to the self-hosted Forgejo HTTP API, which is Gitea-compatible.
This backend adapts the Gitea API to our unified issue model. Gitea is not a second supported product. The backend type string remains
`gitea` for compatibility.
""" """
import requests import requests
@ -16,7 +17,7 @@ from ...core.models import Issue, Label, User, Milestone, Comment, IssueState, P
class GiteaAPIError(Exception): class GiteaAPIError(Exception):
"""Gitea API specific errors.""" """Forgejo (Gitea-compatible API) errors."""
pass pass
@ -26,7 +27,7 @@ class GiteaRateLimitError(GiteaAPIError):
class GiteaBackend(RemoteBackend, SyncableBackend): class GiteaBackend(RemoteBackend, SyncableBackend):
"""Gitea API backend for remote issue tracking.""" """Forgejo backend using the Gitea-compatible HTTP API."""
def __init__(self): def __init__(self):
self.base_url: Optional[str] = None self.base_url: Optional[str] = None
@ -44,7 +45,7 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
supports_webhooks=True, supports_webhooks=True,
supports_real_time=False, supports_real_time=False,
max_labels_per_issue=None, max_labels_per_issue=None,
max_assignees_per_issue=10 # Gitea typical limit max_assignees_per_issue=10 # Forgejo / Gitea-compatible typical limit
) )
@property @property
@ -56,7 +57,7 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
return self._capabilities return self._capabilities
def connect(self, config: Dict[str, Any]) -> None: def connect(self, config: Dict[str, Any]) -> None:
"""Connect to Gitea API.""" """Connect to Forgejo (Gitea-compatible API)."""
self.base_url = config['base_url'].rstrip('/') self.base_url = config['base_url'].rstrip('/')
self.token = config['token'] self.token = config['token']
self.owner = config['owner'] self.owner = config['owner']
@ -71,10 +72,10 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
# Test connection # Test connection
if not self.test_connection(): if not self.test_connection():
raise GiteaAPIError("Failed to connect to Gitea API") raise GiteaAPIError("Failed to connect to Forgejo (Gitea-compatible API)")
def disconnect(self) -> None: def disconnect(self) -> None:
"""Disconnect from Gitea API.""" """Disconnect from Forgejo."""
self.session.close() self.session.close()
self.base_url = None self.base_url = None
self.token = None self.token = None
@ -195,20 +196,20 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
if issue.milestone: if issue.milestone:
data['milestone'] = int(issue.milestone.backend_id) if issue.milestone.backend_id else None data['milestone'] = int(issue.milestone.backend_id) if issue.milestone.backend_id else None
# Gitea expects numeric label IDs on issue create/update. Name-only # Gitea expects numeric label IDs on issue create/update. Name-only
# labels are preserved in issue-core metadata but omitted from the API # labels are preserved in issue-core metadata but omitted from the API
# payload until a label-resolution step exists. # payload until a label-resolution step exists.
label_ids = [] label_ids = []
for label in issue.labels: for label in issue.labels:
if not label.backend_id: if not label.backend_id:
continue continue
try: try:
label_ids.append(int(label.backend_id)) label_ids.append(int(label.backend_id))
except (TypeError, ValueError): except (TypeError, ValueError):
continue continue
if label_ids: if label_ids:
data['labels'] = label_ids data['labels'] = label_ids
return data return data
# Issue CRUD Operations # Issue CRUD Operations

View file

@ -12,6 +12,23 @@ from .utils import (
echo_error, echo_warning, echo_info, confirm_action echo_error, echo_warning, echo_info, confirm_action
) )
# Preferred FORGEJO_*; GITEA_* remain deprecated aliases (ISSUE-WP-0006).
_FORGE_TOKEN_ENV_NAMES = (
'FORGEJO_TOKEN',
'FORGEJO_API_TOKEN',
'GITEA_API_TOKEN',
'GITEA_TOKEN',
)
def _forge_token_from_env():
"""Return (env_name, token) for the first set Forgejo/legacy token var."""
for name in _FORGE_TOKEN_ENV_NAMES:
value = os.getenv(name)
if value:
return name, value
return None, None
@click.group() @click.group()
def backend_group(): def backend_group():
@ -46,14 +63,19 @@ def add_backend(ctx, name, backend_type):
'db_path': str(db_path) 'db_path': str(db_path)
} }
elif backend_type == 'gitea': elif backend_type == 'gitea':
base_url = click.prompt('Gitea base URL (e.g., https://git.example.com)') base_url = click.prompt('Forgejo base URL (Gitea-compatible API)')
owner = click.prompt('Repository owner/organization') owner = click.prompt('Repository owner/organization')
repo = click.prompt('Repository name') repo = click.prompt('Repository name')
# Check for API token in environment variable first env_name, env_token = _forge_token_from_env()
env_token = os.getenv('GITEA_API_TOKEN')
if env_token: if env_token:
click.echo(f"Using API token from GITEA_API_TOKEN environment variable") if env_name in ('GITEA_API_TOKEN', 'GITEA_TOKEN'):
click.echo(
f"Using API token from {env_name} "
"(deprecated alias; prefer FORGEJO_TOKEN)"
)
else:
click.echo(f"Using API token from {env_name} environment variable")
token = env_token token = env_token
else: else:
token = click.prompt('Access token', hide_input=True) token = click.prompt('Access token', hide_input=True)

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "issue-core" name = "issue-core"
description = "External issue-tracker connector (Gitea/Forgejo, SQLite) with work-record UUID mapping — not the fleet work origin" description = "External issue-tracker connector (Forgejo, SQLite) with work-record UUID mapping — not the fleet work origin"
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
license = {text = "MIT"} license = {text = "MIT"}

View file

@ -1,7 +1,7 @@
--- ---
id: capability.infotech.issue-tracking id: capability.infotech.issue-tracking
name: External Issue Tracker Connector name: External Issue Tracker Connector
summary: Backend-agnostic CLI/Python connector to Gitea/Forgejo (and SQLite cache) summary: Backend-agnostic CLI/Python connector to Forgejo (and SQLite cache)
with work-record UUID ↔ external issue mapping. Not the fleet work origin. with work-record UUID ↔ external issue mapping. Not the fleet work origin.
owner: issue-core owner: issue-core
status: draft status: draft
@ -29,10 +29,10 @@ external_evidence:
confidence: medium confidence: medium
basis: scope_vs_intent_and_consumer_expectations basis: scope_vs_intent_and_consumer_expectations
satisfied_expectations: satisfied_expectations:
- Gitea + local SQLite backends - Forgejo + local SQLite backends
- CLI with JSON output; project/map commands - CLI with JSON output; project/map commands
- work_record_uuid optional on TaskSpec - work_record_uuid optional on TaskSpec
- credential handling via env vars (GITEA_API_TOKEN, ISSUE_CORE_API_KEY) - credential handling via env vars (FORGEJO_TOKEN preferred; GITEA_API_TOKEN deprecated alias; ISSUE_CORE_API_KEY)
broken_expectations: [] broken_expectations: []
out_of_scope_expectations: out_of_scope_expectations:
- Fleet task origin / intake promotion (work-record canon) - Fleet task origin / intake promotion (work-record canon)

View file

@ -4,7 +4,7 @@ domain: helix_forge
capabilities: capabilities:
- id: capability.infotech.issue-tracking - id: capability.infotech.issue-tracking
name: Universal Issue Tracking Coordination name: Universal Issue Tracking Coordination
summary: Unified Python/CLI interface for issue tracking across Gitea, GitHub, and GitLab, preventing summary: Unified Python/CLI interface for issue tracking across Forgejo, GitHub, and GitLab, preventing
direct platform API usage and credential sprawl for coordinating agents. direct platform API usage and credential sprawl for coordinating agents.
vector: D4 / A2 / C2 / R1 vector: D4 / A2 / C2 / R1
domain: infotech domain: infotech

View file

@ -53,7 +53,7 @@ class TestCLICommands:
], input='https://git.example.com\ntestorg\ntestrepo\n') ], input='https://git.example.com\ntestorg\ntestrepo\n')
assert result.exit_code == 0 assert result.exit_code == 0
assert 'Using API token from GITEA_API_TOKEN environment variable' in result.output assert 'Using API token from FORGEJO_TOKEN environment variable' in result.output
assert 'Backend \'test-gitea\' added successfully' in result.output assert 'Backend \'test-gitea\' added successfully' in result.output
# Verify save_backend_configs was called with correct data # Verify save_backend_configs was called with correct data
@ -205,8 +205,13 @@ class TestEnvironmentTokenDetection:
@patch('os.getenv') @patch('os.getenv')
def test_gitea_token_detection(self, mock_getenv): def test_gitea_token_detection(self, mock_getenv):
"""Test GITEA_API_TOKEN environment variable detection.""" """Deprecated GITEA_API_TOKEN alias is still accepted."""
mock_getenv.return_value = 'test-env-token' def _getenv(name, default=None):
if name == 'GITEA_API_TOKEN':
return 'test-env-token'
return default
mock_getenv.side_effect = _getenv
from issue_core.cli.backend_commands import add_backend from issue_core.cli.backend_commands import add_backend
@ -220,4 +225,5 @@ class TestEnvironmentTokenDetection:
], input='https://git.example.com\ntestorg\ntestrepo\n') ], input='https://git.example.com\ntestorg\ntestrepo\n')
assert result.exit_code == 0 assert result.exit_code == 0
assert 'Using API token from GITEA_API_TOKEN environment variable' in result.output assert 'Using API token from GITEA_API_TOKEN' in result.output
assert 'deprecated alias; prefer FORGEJO_TOKEN' in result.output

View file

@ -77,7 +77,7 @@ class TestGiteaBackend:
backend = GiteaBackend() backend = GiteaBackend()
backend.session = mock_session backend.session = mock_session
with pytest.raises(GiteaAPIError, match="Failed to connect to Gitea API"): with pytest.raises(GiteaAPIError, match="Failed to connect to Forgejo"):
backend.connect(self.test_config) backend.connect(self.test_config)
def test_url_construction_fix(self): def test_url_construction_fix(self):
@ -98,29 +98,29 @@ class TestGiteaBackend:
called_url = mock_request.call_args[1]['url'] if 'url' in mock_request.call_args[1] else mock_request.call_args[0][1] called_url = mock_request.call_args[1]['url'] if 'url' in mock_request.call_args[1] else mock_request.call_args[0][1]
assert called_url == 'https://git.example.com/api/v1/repos/owner/repo' assert called_url == 'https://git.example.com/api/v1/repos/owner/repo'
def test_gitea_payload_omits_name_only_labels(self): def test_gitea_payload_omits_name_only_labels(self):
"""Gitea issue payloads only include numeric label IDs.""" """Gitea issue payloads only include numeric label IDs."""
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
issue = Issue( issue = Issue(
id="", id="",
number=0, number=0,
title="Test issue", title="Test issue",
description="Test description", description="Test description",
state=IssueState.OPEN, state=IssueState.OPEN,
created_at=now, created_at=now,
updated_at=now, updated_at=now,
labels=[ labels=[
Label(name="priority:low"), Label(name="priority:low"),
Label(name="source:rule", backend_id="not-a-number"), Label(name="source:rule", backend_id="not-a-number"),
Label(name="existing", backend_id="42"), Label(name="existing", backend_id="42"),
], ],
) )
payload = self.backend._unified_issue_to_gitea(issue) payload = self.backend._unified_issue_to_gitea(issue)
assert payload["labels"] == [42] assert payload["labels"] == [42]
assert payload["title"] == "Test issue" assert payload["title"] == "Test issue"
@patch('issue_core.backends.gitea.backend.requests.Session') @patch('issue_core.backends.gitea.backend.requests.Session')
def test_test_connection_success(self, mock_session_class): def test_test_connection_success(self, mock_session_class):
"""Test test_connection method works correctly.""" """Test test_connection method works correctly."""

View file

@ -49,8 +49,8 @@ be an alternative origin.
This workplan brings issue-core's own docs, scope, and (where cheap) code This workplan brings issue-core's own docs, scope, and (where cheap) code
in line with that decision. It does not remove issue-core's existing in line with that decision. It does not remove issue-core's existing
capability — Gitea/GitHub backend CRUD stays useful — it retargets *what capability — Forgejo/GitHub backend CRUD stays useful — it retargets *what
issue-core is for*. issue-core is for*. (Product language: Forgejo only; see ISSUE-WP-0006.)
## Task: Rewrite INTENT.md — connector, not landing zone ## Task: Rewrite INTENT.md — connector, not landing zone

View file

@ -35,7 +35,7 @@ Close the gap between:
| Layer | State at start of this WP | | Layer | State at start of this WP |
| --- | --- | | --- | --- |
| **INTENT** | Work-record-aligned connector (2026-07-22) | | **INTENT** | Work-record-aligned connector (2026-07-22) |
| **SCOPE §1 shipped** | Models, SQLite/Gitea, CLI, REST TaskSpec, backend sync | | **SCOPE §1 shipped** | Models, SQLite/Forgejo, CLI, REST TaskSpec, backend sync |
| **SCOPE §1.8 / target** | Mapping store/API, boundary sync, work_record_uuid, dual-lifecycle rules | | **SCOPE §1.8 / target** | Mapping store/API, boundary sync, work_record_uuid, dual-lifecycle rules |
Deliver a **minimum viable connector core**: durable mapping + project/link Deliver a **minimum viable connector core**: durable mapping + project/link
@ -137,14 +137,14 @@ state_hub_task_id: "027871b0-696d-5ab7-b18c-b26167e97813"
Per `docs/uuid-external-id-mapping.md`: Per `docs/uuid-external-id-mapping.md`:
- Table `work_record_issue_map` (or equivalent) on local backend / dedicated - Table `work_record_issue_map` (or equivalent) on local backend / dedicated
store used even when CRUD backend is Gitea store used even when CRUD backend is Forgejo
- Uniqueness: active `(backend, external_id)` and active - Uniqueness: active `(backend, external_id)` and active
`(work_record_uuid, backend)` `(work_record_uuid, backend)`
- Domain service `MappingService`: upsert, resolve by UUID, by canonical id - Domain service `MappingService`: upsert, resolve by UUID, by canonical id
(if provided), by external id, detach (if provided), by external id, detach
- Unit tests for idempotent project and uniqueness - Unit tests for idempotent project and uniqueness
**Acceptance:** Tests pass without a live Gitea; store survives reconnect; **Acceptance:** Tests pass without a live Forgejo; store survives reconnect;
SCOPE §1.8 “mapping store” can move toward shipped with an honest partial SCOPE §1.8 “mapping store” can move toward shipped with an honest partial
note until CLI/API land. note until CLI/API land.
@ -208,7 +208,7 @@ Implement **outward-only** application of T03 rules for mapped records
- Do **not** write work-record files - Do **not** write work-record files
- Inward sync remains out of scope for this task (document as next) - Inward sync remains out of scope for this task (document as next)
**Acceptance:** Policy tests + one integration-style test on local/Gitea mock; **Acceptance:** Policy tests + one integration-style test on local/Forgejo mock;
SCOPE distinguishes backend sync vs work-record boundary sync clearly. SCOPE distinguishes backend sync vs work-record boundary sync clearly.
## Task: SCOPE inventory refresh after implementation ## Task: SCOPE inventory refresh after implementation

View file

@ -4,18 +4,20 @@ type: workplan
title: "Forgejo-only forge + projection boundary (not ops queue)" title: "Forgejo-only forge + projection boundary (not ops queue)"
domain: infotech domain: infotech
repo: issue-core repo: issue-core
status: ready status: finished
owner: grok owner: grok
topic_slug: issue-core topic_slug: issue-core
priority: medium priority: medium
created: "2026-08-03" created: "2026-08-03"
updated: "2026-08-03" updated: "2026-09-14"
depends_on: [] depends_on: []
related: related:
- ACT-ADR-005 - ACT-ADR-005
- ACTIVITY-WP-0022 - ACTIVITY-WP-0022
- ISSUE-WP-0004 - ISSUE-WP-0004
- ISSUE-WP-0005 - ISSUE-WP-0005
- REIN-A-0002
- REINAH-WP-0005
state_hub_workstream_id: "4d465264-cbe1-56ba-84ed-bd580b649b76" state_hub_workstream_id: "4d465264-cbe1-56ba-84ed-bd580b649b76"
--- ---
@ -41,7 +43,7 @@ and so rein-aharness stops treating issue-core as the primary automation queue
```task ```task
id: ISSUE-WP-0006-T01 id: ISSUE-WP-0006-T01
status: todo status: done
priority: high priority: high
state_hub_task_id: "927eee42-e22d-5c0d-a4f8-dcd36c53c8e9" state_hub_task_id: "927eee42-e22d-5c0d-a4f8-dcd36c53c8e9"
``` ```
@ -65,7 +67,7 @@ compatibility notes remain.
```task ```task
id: ISSUE-WP-0006-T02 id: ISSUE-WP-0006-T02
status: todo status: done
priority: high priority: high
state_hub_task_id: "a1d69a48-833b-5e5e-8286-14f5aba627f3" state_hub_task_id: "a1d69a48-833b-5e5e-8286-14f5aba627f3"
``` ```
@ -87,7 +89,7 @@ Explicit section:
```task ```task
id: ISSUE-WP-0006-T03 id: ISSUE-WP-0006-T03
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "75eef8a3-abd7-50dd-9171-3488fda84da5" state_hub_task_id: "75eef8a3-abd7-50dd-9171-3488fda84da5"
``` ```
@ -107,7 +109,7 @@ as the primary automation loop.
```task ```task
id: ISSUE-WP-0006-T04 id: ISSUE-WP-0006-T04
status: todo status: done
priority: low priority: low
state_hub_task_id: "5be0bce4-1816-599f-a98b-4fa1565a8b0e" state_hub_task_id: "5be0bce4-1816-599f-a98b-4fa1565a8b0e"
``` ```
@ -117,14 +119,32 @@ No code change required in issue-core if rein owns the doc cut.
**Done when:** cross-link in ISSUE-WP-0006 and REIN-A-0002. **Done when:** cross-link in ISSUE-WP-0006 and REIN-A-0002.
**Closed 2026-09-14:** REIN-A-0002 was completed as **REINAH-WP-0005**. T06
there already marked issue-core poll as legacy/external. Cross-links added:
- this workplan `related: REIN-A-0002`, `REINAH-WP-0005`
- `~/rein-aharness/workplans/REINAH-WP-0005-ops-run-claim-loop.md` T06 note
+ `related: ISSUE-WP-0006`
- `~/rein-aharness/docs/task-intake.md` (legacy/external section)
- State Hub message to `rein-aharness`
## Acceptance ## Acceptance
- [ ] No Gitea-as-product planning - [x] No Gitea-as-product planning
- [ ] Projection boundary clear - [x] Projection boundary clear
- [ ] Ops claim queue explicitly out of scope for issue-core - [x] Ops claim queue explicitly out of scope for issue-core
## Out of scope ## Out of scope
- Implementing ops_run (ACTIVITY-WP-0026) - Implementing ops_run (ACTIVITY-WP-0026) — lives in activity-core / REINAH-WP-0005
- Fixing Forgejo PAT for rest sink (only if projection needs it later) - Fixing Forgejo PAT for rest sink (only if projection needs it later) — ISSUE-WP-0003 residual / deploy
- GitHub/Jira connectors - GitHub/Jira connectors — product growth, not this workplan
No new residual: those items already have live work records elsewhere.
## Close note (2026-09-14)
Product language is **Forgejo** only. The HTTP API is Gitea-compatible;
module `issue_core.backends.gitea` and backend type `gitea` remain as
identifiers. `FORGEJO_TOKEN` is preferred; `GITEA_*` is a deprecated alias.
INTENT/SCOPE quote ACT-ADR-005: issue-core is not the ops claim queue.