docs(ISSUE-WP-0006): Forgejo-only language and projection boundary
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:
parent
f9d276dadf
commit
ee9b85215d
34 changed files with 410 additions and 263 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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/"
|
||||||
|
|
|
||||||
51
INTENT.md
51
INTENT.md
|
|
@ -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-core’s 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.
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -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"
|
||||||
|
|
|
||||||
39
README.md
39
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
25
ROADMAP.md
25
ROADMAP.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
54
SCOPE.md
54
SCOPE.md
|
|
@ -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-core’s 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 emitter’s log; not a
|
`issue_id` is the **backend** issue identity for the emitter’s 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. INTENT’s UUID-only phrasing can steer readers to hide
|
key should be UUID. INTENT’s 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 @@ INTENT’s “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 |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
"""
|
"""
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue