📝 Refine gitea skill port design with resolved decisions

Folds in decisions made during the design review: flat sibling imports
+ PEP 723, CWD-based paths, git-remote inference with env overrides,
workpad/tickets parameterization, project label overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 19:51:51 +02:00
parent 4f67b63918
commit c5b189a73c
@@ -27,18 +27,7 @@ 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.<module>`. Everything load-bearing lives in the dirigent project root: the venv, `uv.lock`, `pyproject.toml`, and `.env`. The skill itself does almost nothing — it tells the agent to *read* `scripts/gitea/CLAUDE.md` and *run* `uv run python -m scripts.gitea.<module>`. Everything load-bearing lives in the dirigent project root: the venv, `uv.lock`, `pyproject.toml`, and `.env`.
## The fundamental tension ## Target shape in reliquary
The dirigent skill assumes:
1. **The Python scripts live in the project being worked on**, importable as `scripts.gitea.*` from project root.
2. **The project has uv set up and a `.env` with `GITEA_*` vars.**
3. **The user is in dirigent itself** — the skill says `dirigent workpad`, references `docs/workpad/tickets/`, and the labels list (🐛 bug, ✨ feature, 📋 workpad…) is curated for dirigent.
To turn this into a **reusable plugin skill**, none of those assumptions hold. The scripts must live with the plugin, work from anywhere, and adapt to any project's workpad layout.
## Two viable port shapes
### Option A — Scripts ship inside the plugin (recommended)
``` ```
plugins/gitea/ plugins/gitea/
@@ -46,11 +35,12 @@ plugins/gitea/
├── README.md ├── README.md
└── skills/gitea/ └── skills/gitea/
├── SKILL.md ├── SKILL.md
├── labels.default.json # ships with plugin
└── scripts/ └── scripts/
├── __init__.py ├── client.py # flat layout — no package
├── client.py
├── config.py ├── config.py
├── models.py ├── models.py
├── ping.py # standalone connectivity check
├── fetch_ticket.py ├── fetch_ticket.py
├── create_ticket.py ├── create_ticket.py
├── post_comment.py ├── post_comment.py
@@ -60,85 +50,152 @@ plugins/gitea/
└── setup_labels.py └── setup_labels.py
``` ```
Invocation pattern changes: Single SKILL.md, no separate CLAUDE.md. No `__init__.py`. Each entrypoint is a self-contained PEP 723 script.
- Skill expands `${CLAUDE_PLUGIN_ROOT}` to locate scripts. ## Decisions (resolved during design review)
- Each call becomes `uv run --no-project --with httpx --with pydantic --with python-dotenv python ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/fetch_ticket.py 42` — or, better, an inline-deps PEP 723 header at the top of each script so they're self-installing.
- Env vars come from `.env` in the *current working directory* (the project the user is in), not the plugin dir. `python-dotenv` already supports this.
- Workpad path comes from the `workpad` skill's resolution rules (env var → just recipe → `/docs/workpad`), not hardcoded.
**Pros:** ### 1. Module layout: flat sibling imports + PEP 723 per file
- Plugin is fully self-contained — install it once, use it in any repo.
- Versioned together with the skill markdown.
- No requirement that the host project has uv configured.
**Cons:** Each CLI entrypoint carries an inline-deps block:
- `uv run --no-project --with httpx ...` is slow on first run (env build). Mitigation: PEP 723 headers + `uv` cache make second-run startup fast.
- The scripts must be reworked to take a workpad root as an arg (or env var) rather than hardcoding `docs/workpad/`.
### Option B — Scripts vendored into each project (rejected) ```python
# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "pydantic>=2", "python-dotenv"]
# ///
```
Keep the dirigent shape: ship a "scaffolder" that copies scripts into the user's repo. This is what `superpowers`-style skills sometimes do. 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.
**Why reject:** updates don't propagate, every project carries duplicate code, and the scripts become "the user's code now" which makes version skew permanent. 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 <path>` 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 <path>` flag if passed (skill can populate from `GITEA_TICKETS_DIR` env).
2. Else `<workpad>/tickets/<n>/`.
When `--tickets-dir` is given, write directly to `<tickets-dir>/<n>/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 ## Concrete port checklist
If we go with Option A: ### 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 1copy + decouple from dirigent ### Phase 2CWD + remote inference
1. Copy `scripts/gitea/` contents → `plugins/gitea/skills/gitea/scripts/`. 5. Replace `PROJECT_ROOT = Path(__file__)...` in `start_ticket.py` with `Path.cwd()` usage. Remove the constant.
2. Drop `__init__.py` if scripts are run as files; keep it if we want `python -m`. 6. Switch `config.py` `.env` loading from custom code to `dotenv.load_dotenv()` (no path arg).
3. Add PEP 723 inline-dep blocks to each CLI entrypoint (`fetch_ticket.py`, `create_ticket.py`, etc.): 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.
```python 8. Drop the "See scripts/gitea/README.md" hint in the missing-env error and replace with skill-aware wording.
# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "pydantic>=2", "python-dotenv"]
# ///
```
This makes each script runnable in isolation via `uv run script.py` without a project context.
4. Rewrite imports: `from scripts.gitea.client import ...` → `from client import ...` (relative within the scripts/ dir). Or run as `python -m` with `PYTHONPATH` set.
### Phase 2 — parameterize project-specific behavior ### Phase 3 — parameterize workpad + tickets + labels
5. Replace hardcoded `docs/workpad/tickets/` with a CLI flag `--workpad <path>` defaulting to env var `WORKPAD_FOLDER`, defaulting to `./docs/workpad`. Honor the `workpad` skill's resolution order. 9. Add `--workpad <path>` (required) and `--tickets-dir <path>` (optional) to `fetch_ticket.py` and `start_ticket.py`. Default tickets dir = `<workpad>/tickets/`.
6. Move the hardcoded label-emoji list out of `setup_labels.py` into a JSON/YAML file shipped with the plugin (`labels.default.json`) so users can override per-project. 10. Add `--workpad <path>` 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).
7. Audit any other dirigent-isms: references to "dirigent" in error messages, the README mentioning dirigent's workpad, etc. 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 3 rewrite the skill markdown ### Phase 4 — skill markdown + manifest
8. `Step 0: Read Current Script Capabilities` currently points at `scripts/gitea/CLAUDE.md`. Replace with **inline** capability summary in the SKILL itself — the skill *is* the reference now; the scripts are an implementation detail. 13. Rewrite `SKILL.md` from scratch:
9. Replace every `uv run python -m scripts.gitea.X` example with `uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/X.py`. - Inline capability summary for each script (no separate CLAUDE.md).
10. Connectivity check stays — just relocate the inline `python -c` snippet's imports to whatever the new module layout is. - Step 0 (read CLAUDE.md) deleted.
11. Error-recovery table stays as-is. It's good. - Step 1 (connectivity) calls `ping.py`.
- All invocations use `uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/<name>.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 4documentation ### Phase 5manual end-to-end
12. README at plugin root: install instructions + Gitea token setup. Lift most of `scripts/gitea/README.md`, drop the dirigent-specific bits. 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.
13. The skill markdown should be self-contained — don't make the agent open a second file unless absolutely necessary.
## Things to NOT do in the port ## What we are not doing
- **Don't keep both `CLAUDE.md` and `SKILL.md` for the scripts.** That split made sense in dirigent because the scripts had a life of their own outside the skill. Inside a plugin they don't. One file: `SKILL.md`. - No `python -m` invocation, no shared venv, no project-level dependency installation.
- **Don't ship Pydantic v2 + httpx as hard plugin dependencies installed in the user's project venv.** Use PEP 723 inline deps so uv builds an isolated env per invocation. - No host sniffing — the script doesn't decide whether a remote is "Gitea-shaped" before calling.
- **Don't hardcode `closes #N` / `fixes #N` keywords** — those are Gitea behavior, not script behavior. They should stay in the skill markdown as guidance, not in code. - No merge between project label file and plugin default — override is total replacement.
- **Don't copy `setup_labels.py`'s default label set verbatim.** It's tuned for one user's taste. Externalize. - 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.
## Open question — sibling skill in a "mini plugin" ## Sibling "mini plugin" — future scope, not in this port
The user mentioned wanting **another skill in a custom mini plugin** that builds on this. That's not designed yet — likely candidates given the workpad ecosystem: A separate plugin (working name: `gitea-workflow` or similar) layers opinionated flows on top of the API skill:
- A `gitea-workpad-flow` skill that ties together: "fetch a ticket → write plan in workpad → post plan back as comment → branch via `start_ticket`". This is workflow glue, not API access. - **kanban skill** — list/move issues across columns, mark in-progress, query "what am I working on right now".
- A `gitea-bulk` skill for grooming: relabel many issues, close stale ones, etc. - **workpad-flow skill** — fetch ticket → write plan in workpad → post plan back as comment → branch via `start_ticket` → final commit with `closes #N`.
- A `gitea-pr` skill if/when Gitea's PR API gets used (the current scripts only touch issues). - **bulk skill** — relabel many issues, close stale ones, milestone grooming.
These should be a separate plugin so the core `gitea` plugin stays purely a thin API wrapper. The workflow-glue skill *depends on* the API skill but is opinionated about flow — exactly the kind of thing that should be swappable. 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 ## Estimated work
| Phase | Effort | Risk | | Phase | Effort | Risk |
|---|---|---| |---|---|---|
| Copy + PEP 723 conversion | ~1h | Low — mechanical | | Phase 1 (copy + flatten + PEP 723) | ~45min | Low — mechanical |
| Parameterize workpad path + labels | ~1-2h | Medium — touches 4-5 files | | Phase 2 (CWD + remote inference) | ~1h | Medium — `git remote` parsing has edge cases |
| Skill markdown rewrite | ~30min | Low | | Phase 3 (workpad/tickets/labels params) | ~1h | Low |
| README + plugin manifest | ~20min | Low | | Phase 4 (SKILL.md + manifest + README) | ~45min | Low |
| Manual end-to-end test (real Gitea + new project) | ~1h | This is where the bugs are | | Phase 5 (end-to-end test) | ~1h | This is where bugs hide |
Total: **half a day of focused work** for a complete port. The `start_ticket` flow is the most likely to surface issues because it touches git state, the workpad, and the API in one call. 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.).