# Porting the `gitea` skill + scripts into reliquary Hands-on design for moving dirigent's `gitea` skill (which is a thin wrapper around `scripts/gitea/*.py` in the dirigent repo) into a reliquary plugin where the scripts and the skill ship as one unit. ## Current state in dirigent ``` dirigent/ ├── .claude/skills/gitea/SKILL.md # 123 LOC — thin wrapper, points at scripts └── scripts/gitea/ ├── CLAUDE.md # agent reference for scripts (~127 LOC) ├── README.md # human setup guide (~178 LOC) ├── __init__.py ├── client.py # 186 LOC — GiteaClient REST wrapper ├── close_ticket.py # 39 LOC ├── config.py # 68 LOC — env loading ├── create_ticket.py # 115 LOC ├── fetch_ticket.py # 113 LOC ├── list_tickets.py # 56 LOC ├── models.py # 66 LOC — Pydantic models ├── post_comment.py # 69 LOC ├── setup_labels.py # 43 LOC └── start_ticket.py # 163 LOC ``` Total: ~919 LOC of Python + ~430 LOC of docs. The skill itself does almost nothing — it tells the agent to *read* `scripts/gitea/CLAUDE.md` and *run* `uv run python -m scripts.gitea.`. Everything load-bearing lives in the dirigent project root: the venv, `uv.lock`, `pyproject.toml`, and `.env`. ## Target shape in reliquary ``` plugins/gitea/ ├── .claude-plugin/plugin.json ├── README.md └── skills/gitea/ ├── SKILL.md ├── labels.default.json # ships with plugin └── scripts/ ├── client.py # flat layout — no package ├── config.py ├── models.py ├── ping.py # standalone connectivity check ├── fetch_ticket.py ├── create_ticket.py ├── post_comment.py ├── start_ticket.py ├── list_tickets.py ├── close_ticket.py └── setup_labels.py ``` Single SKILL.md, no separate CLAUDE.md. No `__init__.py`. Each entrypoint is a self-contained PEP 723 script. ## Decisions (resolved during design review) ### 1. Module layout: flat sibling imports + PEP 723 per file Each CLI entrypoint carries an inline-deps block: ```python # /// script # requires-python = ">=3.11" # dependencies = ["httpx", "pydantic>=2", "python-dotenv"] # /// ``` Imports become flat — `import client`, `from client import GiteaClient`, `from fetch_ticket import render_issue_markdown`. The script's own directory is on `sys.path` when run as a file, so no package context is needed. Invocation: `uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/fetch_ticket.py 42`. Tradeoff: `uv run` builds an isolated env per script on first call (slow once, cached after). Worth it for hermetic execution against any project regardless of its own Python setup. ### 2. CWD-relative, not script-relative The dirigent scripts derive `PROJECT_ROOT` from `Path(__file__).resolve().parent.parent.parent`. After the port that resolves to the *plugin* directory, which is wrong for every git/workpad/env operation. All scripts must use `Path.cwd()` for: - git operations (`subprocess.run([...], cwd=Path.cwd())` or just no `cwd` arg) - workpad/ticket file writes - `.env` discovery (call `dotenv.load_dotenv()` with no args — finds `.env` in CWD and walks up) `Path(__file__)` is only used to locate sibling resources shipped with the plugin (e.g. `labels.default.json`). ### 3. Where Gitea lives — infer from git remote, override via env `config.py` resolves connection settings in this order: 1. **Parse `git remote get-url origin`.** Handle both forms: - `git@host:owner/repo.git` → `base_url=https://host`, `owner`, `repo` - `https://host/owner/repo(.git)?` → same fields 2. **Env vars override inferred values.** Any of `GITEA_URL`, `GITEA_OWNER`, `GITEA_REPO` set in env (or `.env`) wins over what's parsed from the remote. Covers mirrors, multi-remote setups, separate API host. 3. **`GITEA_TOKEN` is always required** — never inferable. No host sniffing — the script doesn't try to guess whether the remote is Gitea, GitHub, or anything else. It just makes the API call; the connectivity check surfaces a wrong host as HTTP 404 or a malformed response. If `git remote get-url origin` fails (not a git repo, no origin) and any of URL/owner/repo isn't set in env, exit with a clear error listing what's missing. ### 4. Workpad resolution lives in the skill, not the script Scripts take a required `--workpad ` flag. They do not resolve the workpad themselves. The skill resolves it before invoking: 1. `WORKPAD_FOLDER` env var (or `.env`-loaded value). 2. If unset, ask the user — or accept a value already established in conversation. No `/docs/workpad` auto-fallback in this skill — the workpad skill itself handles project-level placement; gitea just consumes the resolved path. ### 5. Tickets folder is independently overridable The workpad's `tickets/` subfolder is *not* the only valid home for per-ticket scratch. In some projects, tickets live outside the workpad entirely. Resolution: 1. `--tickets-dir ` flag if passed (skill can populate from `GITEA_TICKETS_DIR` env). 2. Else `/tickets//`. When `--tickets-dir` is given, write directly to `//context.md` — no `workpad/` join. `mkdir(parents=True, exist_ok=True)` on demand. ### 6. Labels: plugin default + project override - `plugins/gitea/skills/gitea/labels.default.json` ships with the plugin and contains the de-dirigent-ed default set. - `.gitea-labels.json` in CWD, if present, fully replaces the default (not merged — keeps semantics simple). - `setup_labels.py` loads whichever applies, creates missing labels, skips existing ones (case-insensitive name match). The label description `"Created from dirigent workpad"` becomes `"Created from workpad"`. ### 7. Connectivity check becomes a script The SKILL's Step 1 inline `python -c` doesn't survive the package rename. Replace with: ```bash uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/ping.py ``` `ping.py` does what `test_connection()` did: load config, hit `/repos/{owner}/{repo}`, print `OK` or a categorized error (missing env, 401, 404, connection refused) and exit non-zero on failure. ## Concrete port checklist ### Phase 1 — copy + flatten 1. Copy `dirigent/scripts/gitea/*.py` → `plugins/gitea/skills/gitea/scripts/`. 2. Delete `__init__.py`. 3. Rewrite every `from .client import …` → `from client import …`. Same for `.config`, `.models`, `.fetch_ticket`. 4. Add PEP 723 header to each CLI entrypoint (`fetch_ticket.py`, `create_ticket.py`, `post_comment.py`, `start_ticket.py`, `list_tickets.py`, `close_ticket.py`, `setup_labels.py`, `ping.py`). Library files (`client.py`, `config.py`, `models.py`) don't need it. ### Phase 2 — CWD + remote inference 5. Replace `PROJECT_ROOT = Path(__file__)...` in `start_ticket.py` with `Path.cwd()` usage. Remove the constant. 6. Switch `config.py` `.env` loading from custom code to `dotenv.load_dotenv()` (no path arg). 7. Add `git_remote_origin()` helper in `config.py`: runs `git remote get-url origin`, parses SSH/HTTPS forms, returns `(base_url, owner, repo)` or `None`. Use it as the base in `load_config()`; env vars override. 8. Drop the "See scripts/gitea/README.md" hint in the missing-env error and replace with skill-aware wording. ### Phase 3 — parameterize workpad + tickets + labels 9. Add `--workpad ` (required) and `--tickets-dir ` (optional) to `fetch_ticket.py` and `start_ticket.py`. Default tickets dir = `/tickets/`. 10. Add `--workpad ` to `create_ticket.py` (for `--from-goal` path resolution — though goals are typically given as explicit paths, so this may be unnecessary; decide during implementation). 11. Build `labels.default.json` from the current hardcoded list, de-dirigent-ed. 12. `setup_labels.py`: load `.gitea-labels.json` from CWD if present, else `Path(__file__).parent.parent / "labels.default.json"`. ### Phase 4 — skill markdown + manifest 13. Rewrite `SKILL.md` from scratch: - Inline capability summary for each script (no separate CLAUDE.md). - Step 0 (read CLAUDE.md) deleted. - Step 1 (connectivity) calls `ping.py`. - All invocations use `uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/.py`. - Workpad resolution paragraph: env → ask, then pass via `--workpad`. - Keep the error-recovery table; update path references. 14. `plugin.json`: name `gitea`, version `0.1.0`, author Gabor Körber, description matches the marketplace tone of other plugins in this repo. 15. `README.md` at plugin root: install, token setup, env var reference (only `GITEA_TOKEN` mandatory now), example session. ### Phase 5 — manual end-to-end 16. Real Gitea + a non-dirigent project. Test: `ping`, `setup_labels` (with project override file present and absent), `list_tickets`, `fetch_ticket`, `start_ticket` (cwd-relative git!), `post_comment`, `close_ticket`. This is where bugs surface. ## What we are not doing - No `python -m` invocation, no shared venv, no project-level dependency installation. - No host sniffing — the script doesn't decide whether a remote is "Gitea-shaped" before calling. - No merge between project label file and plugin default — override is total replacement. - No PR-related endpoints. Issues only, same as the dirigent scripts. - No workflow glue (write-plan-and-comment, ticket-to-branch-with-implementation, etc.). That's a separate sibling plugin, addressed below. ## Sibling "mini plugin" — future scope, not in this port A separate plugin (working name: `gitea-workflow` or similar) layers opinionated flows on top of the API skill: - **kanban skill** — list/move issues across columns, mark in-progress, query "what am I working on right now". - **workpad-flow skill** — fetch ticket → write plan in workpad → post plan back as comment → branch via `start_ticket` → final commit with `closes #N`. - **bulk skill** — relabel many issues, close stale ones, milestone grooming. Out of scope for this port. The core `gitea` plugin stays a thin, opinion-light API wrapper so the workflow plugin can be swapped or extended without touching the API surface. ## Estimated work | Phase | Effort | Risk | |---|---|---| | Phase 1 (copy + flatten + PEP 723) | ~45min | Low — mechanical | | Phase 2 (CWD + remote inference) | ~1h | Medium — `git remote` parsing has edge cases | | Phase 3 (workpad/tickets/labels params) | ~1h | Low | | Phase 4 (SKILL.md + manifest + README) | ~45min | Low | | Phase 5 (end-to-end test) | ~1h | This is where bugs hide | Total: **~4.5h focused work**. Highest-risk pieces: `start_ticket` (touches git state + workpad + API in one call) and the remote-URL parser (SSH alias forms, port numbers, trailing `.git`, etc.).