From c5b189a73cb031f43b459206d9467ceac12a4ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Tue, 19 May 2026 19:51:51 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Refine=20gitea=20skill=20port=20?= =?UTF-8?q?design=20with=20resolved=20decisions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../designs/2026-05-11-17-gitea-skill-port.md | 199 +++++++++++------- 1 file changed, 128 insertions(+), 71 deletions(-) diff --git a/docs/workpad/designs/2026-05-11-17-gitea-skill-port.md b/docs/workpad/designs/2026-05-11-17-gitea-skill-port.md index df329a4..da9f831 100644 --- a/docs/workpad/designs/2026-05-11-17-gitea-skill-port.md +++ b/docs/workpad/designs/2026-05-11-17-gitea-skill-port.md @@ -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.`. 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) +```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 ` 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 -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 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 β€” 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 2 β€” parameterize project-specific behavior -5. Replace hardcoded `docs/workpad/tickets/` with a CLI flag `--workpad ` 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 β€” 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 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. +### 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 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. +### 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. -## 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`. -- **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. +- 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. -## 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. -- 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). +- **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. -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 | 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.).