📝 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:
@@ -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 fundamental tension
|
||||
|
||||
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)
|
||||
## Target shape in reliquary
|
||||
|
||||
```
|
||||
plugins/gitea/
|
||||
@@ -46,11 +35,12 @@ plugins/gitea/
|
||||
├── README.md
|
||||
└── skills/gitea/
|
||||
├── SKILL.md
|
||||
├── labels.default.json # ships with plugin
|
||||
└── scripts/
|
||||
├── __init__.py
|
||||
├── client.py
|
||||
├── client.py # flat layout — no package
|
||||
├── config.py
|
||||
├── models.py
|
||||
├── ping.py # standalone connectivity check
|
||||
├── fetch_ticket.py
|
||||
├── create_ticket.py
|
||||
├── post_comment.py
|
||||
@@ -60,85 +50,152 @@ plugins/gitea/
|
||||
└── 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.
|
||||
- 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.
|
||||
## Decisions (resolved during design review)
|
||||
|
||||
**Pros:**
|
||||
- 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.
|
||||
### 1. Module layout: flat sibling imports + PEP 723 per file
|
||||
|
||||
**Cons:**
|
||||
- `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/`.
|
||||
Each CLI entrypoint carries an inline-deps block:
|
||||
|
||||
### 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
|
||||
# /// 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
|
||||
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.
|
||||
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.
|
||||
|
||||
### Phase 3 — rewrite the skill markdown
|
||||
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.
|
||||
Invocation: `uv run ${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/fetch_ticket.py 42`.
|
||||
|
||||
### Phase 4 — documentation
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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`.
|
||||
- **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.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
- 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).
|
||||
### 3. Where Gitea lives — infer from git remote, override via env
|
||||
|
||||
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
|
||||
|
||||
| Phase | Effort | Risk |
|
||||
|---|---|---|
|
||||
| Copy + PEP 723 conversion | ~1h | Low — mechanical |
|
||||
| Parameterize workpad path + labels | ~1-2h | Medium — touches 4-5 files |
|
||||
| Skill markdown rewrite | ~30min | Low |
|
||||
| README + plugin manifest | ~20min | Low |
|
||||
| Manual end-to-end test (real Gitea + new project) | ~1h | This is where the bugs are |
|
||||
| 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: **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.).
|
||||
|
||||
Reference in New Issue
Block a user