📝 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)
Keep the dirigent shape: ship a "scaffolder" that copies scripts into the user's repo. This is what `superpowers`-style skills sometimes do.
**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.
## Concrete port checklist
If we go with Option A:
### Phase 1 — copy + decouple from dirigent
1. Copy `scripts/gitea/` contents → `plugins/gitea/skills/gitea/scripts/`.
2. Drop `__init__.py` if scripts are run as files; keep it if we want `python -m`.
3. Add PEP 723 inline-dep blocks to each CLI entrypoint (`fetch_ticket.py`, `create_ticket.py`, etc.):
```python ```python
# /// script # /// script
# requires-python = ">=3.11" # requires-python = ">=3.11"
# dependencies = ["httpx", "pydantic>=2", "python-dotenv"] # 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 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.
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.
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.
7. Audit any other dirigent-isms: references to "dirigent" in error messages, the README mentioning dirigent's workpad, etc.
### Phase 3 — rewrite the skill markdown Invocation: `uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/fetch_ticket.py 42`.
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.
9. Replace every `uv run python -m scripts.gitea.X` example with `uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/X.py`.
10. Connectivity check stays — just relocate the inline `python -c` snippet's imports to whatever the new module layout is.
11. Error-recovery table stays as-is. It's good.
### Phase 4 — documentation 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.
12. README at plugin root: install instructions + Gitea token setup. Lift most of `scripts/gitea/README.md`, drop the dirigent-specific bits.
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 ### 2. CWD-relative, not script-relative
- **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`. 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.
- **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.
- **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.
- **Don't copy `setup_labels.py`'s default label set verbatim.** It's tuned for one user's taste. Externalize.
## Open question — sibling skill in a "mini plugin" 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)
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: `Path(__file__)` is only used to locate sibling resources shipped with the plugin (e.g. `labels.default.json`).
- 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. ### 3. Where Gitea lives — infer from git remote, override via env
- A `gitea-bulk` skill for grooming: relabel many issues, close stale ones, etc.
- A `gitea-pr` skill if/when Gitea's PR API gets used (the current scripts only touch issues).
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. `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
### 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 <path>` (required) and `--tickets-dir <path>` (optional) to `fetch_ticket.py` and `start_ticket.py`. Default tickets dir = `<workpad>/tickets/`.
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).
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/<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 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 ## 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.).