c5b189a73c
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>
202 lines
11 KiB
Markdown
202 lines
11 KiB
Markdown
# 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.<module>`. 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 <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 |
|
|
|---|---|---|
|
|
| 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.).
|