Files
marketplace/README.md
T
naudachu 091dceec1d refactor: split issue domain from Gitea transport
An issue was a Gitea row that happened to be cached locally: its identity
was the tracker's number (42.md), its dependencies were tracker numbers
(depends: [#12]), and a local issue existed only as a draft that push
deleted on success. Nothing could be planned or tracked without a tracker.

Split into layers, with knowledge flowing one way:

  skills/issue  DOMAIN  what an issue is: format, validation, dep graph
        ^               offline; stdlib imports only, no subprocess
        | imports
  skills/sync   BRIDGE  map.py    md <-> Gitea JSON, pure, no I/O
                        _gitea.py login pin, api, pagination, filters
  skills/use    REFERENCE  tea CLI docs for non-issue entities

skills/issue never imports skills/sync. Delete the sync layer and the
domain keeps working.

Identity is now a slug derived from the title (wire-sqlc-appclick.md) and
is stable across retitles and pushes. Tracker numbers live in a `gitea:`
field, never in a file name and never in `depends:`; the pair is indexed
in .remote.json, which is a cache over the files, not a second source of
truth.

Behavior changes:

- Pushing is additive. The file is never deleted; it gains gitea:/url:/
  synced: and origin: flips from local to gitea. `origin: local` is a
  durable state, not a pending one.
- Pushes go in topological order so dependencies get numbers first.
- The dependency graph is computed offline from `depends:` metadata; body
  prose is passed through unchanged in both directions rather than being
  rewritten between slugs and #N.
- `origin` is domain-owned (whether work exists elsewhere is a fact about
  the work); the handle and how to reach it stay with sync.

Script moves:

  issue_get.py   -> sync/pull.py
  issue_push.py  -> sync/push.py
  issue_list.py  -> sync/remote.py
  issue_index.py -> issue/issue_index.py
  _tea.py        -> split into issue/issue.py, sync/map.py, sync/_gitea.py

New: issue/issue_new.py, issue/issue_check.py, issue/issue_tree.py, and
sync/comment.py — comment posting was the last issue operation still
hand-rolled through raw `tea api`.

references/issue-format.md moves to skills/issue/references/format.md;
label hex colors move out of it into map.py, since a color is how a
tracker paints a chip, not what an issue is.

Verified: offline path end to end (new, check, tree, index, push
--dry-run) and read-only against Gitea (remote listing, pull with
mapping, comment guard). Write paths of push.py and comment.py are not
exercised here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 23:37:32 +05:00

140 lines
6.0 KiB
Markdown

# tea — Claude Code plugin for the Gitea CLI
A Claude Code plugin that gives Claude a reference for the `tea` CLI and enforces a hard rule: every `tea` command runs under the login **the operator chose**, never one Claude picked.
## What it ships
| Piece | What it does |
|---|---|
| `/tea:auth` skill | Prompts you to pick a Gitea login and pins it to the project |
| `/tea:issue` skill | Issues as units of work — create, read, grep, validate, walk the dependency graph. Entirely offline |
| `/tea:sync` skill | Moves issues between the local store and Gitea — pull, push, comment |
| `/tea:use` skill | Tea CLI reference for everything that is not an issue — loads command docs on demand |
| `tea-guard` hook | PreToolUse hook that blocks or rewrites every `tea` invocation |
## The layering
An issue is a unit of work first and a Gitea row second. Those are two layers,
and knowledge flows one way:
```
skills/issue DOMAIN what an issue is: format, validation, dependency graph
▲ offline — no tracker, no network, stdlib only
│ imports
skills/sync BRIDGE md <-> Gitea JSON, then over the wire
```
Delete `skills/sync` and the domain layer keeps working — issues that live only
on your machine are first-class, not drafts waiting to be uploaded. That is the
point of the split: you can plan, write, validate, and track work without a
tracker, and publish only what you choose to.
## Prerequisites
- **Claude Code** — CLI, desktop app, or IDE extension
- **Python 3** — required by the `tea-guard` hook (`python3` must be on `$PATH`)
- **`tea`** — Gitea's official CLI. Install with `brew install tea` (macOS) or from [gitea.com/gitea/tea/releases](https://gitea.com/gitea/tea/releases)
- At least one login configured: `tea logins add` (interactive — run it in a terminal, not via Claude)
## Installation
This is a Claude Code plugin — install it through the plugin marketplace, not by hand-editing `settings.json`.
1. Register this repo as a marketplace:
```
/plugin marketplace add https://git.noodles.cam/claude-skills/tea.git
```
Already have a local clone? Point at the directory instead:
```
/plugin marketplace add /path/to/tea
```
2. Install the plugin:
```
/plugin install tea@tea
```
The skills (`/tea:auth`, `/tea:issue`, `/tea:sync`, `/tea:use`) and the `tea-guard` hook load immediately. Use `/plugin` to enable, disable, or update it later.
> The marketplace registration is written to `extraKnownMarketplaces` and the plugin to `enabledPlugins` in your settings automatically — you don't edit those by hand. There is **no** top-level `"plugins"` settings key; if you've added one from older instructions, remove it.
## First use
Run `/tea:auth` once per project. Claude will list your available Gitea logins and ask you to pick one. The choice is written to `.claude/settings.local.json` and takes effect immediately — no restart needed.
```
/tea:auth
```
After that, just ask Claude to do something with issues or Gitea — it loads the
right skill automatically. `/tea:auth` is only needed for the tracker side;
`/tea:issue` works without any login at all.
## How the login guard works
Every `tea` invocation Claude writes must carry the literal placeholder `--login "$GITEA_LOGIN"`. The `tea-guard` hook intercepts the Bash call before it runs, looks up the pinned login from `.claude/settings.local.json`, and rewrites the command to use it.
Claude is **blocked** from:
- running `tea` without `--login` at all
- naming a login itself (e.g. `--login myaccount`)
- using any variable other than `$GITEA_LOGIN`
This prevents silent fallback to the machine's default login (often a personal account) when working in a project that belongs to a different identity.
`tea logins list` and `tea --version / --help` are exempt — they don't touch Gitea data.
## Project layout
```
.claude-plugin/
plugin.json plugin manifest
marketplace.json marketplace catalog (makes `/plugin install` work)
hooks/
hooks.json registers the PreToolUse hook
tea-guard.sh the guard (Python 3, no deps)
skills/
auth/SKILL.md /tea:auth skill
issue/ /tea:issue — the domain layer, offline
SKILL.md
references/format.md canonical issue format (identity, types, templates)
scripts/ Python 3, stdlib only, no network:
issue.py domain module: slug identity, parse/render,
validation, taxonomy, dependency graph
issue_new.py create a local issue from its type template
issue_check.py validate against the format
issue_tree.py draw the dependency graph
issue_index.py rebuild tmp/issues/INDEX.md
sync/ /tea:sync — the bridge to Gitea
SKILL.md
scripts/
map.py md <-> Gitea JSON, pure functions, no I/O
_gitea.py transport: login pin, tea api, pagination, filters
pull.py Gitea -> tmp/issues/
push.py tmp/issues/ -> Gitea (additive; never deletes)
remote.py discovery listing to stdout
comment.py post or edit a comment
use/ /tea:use — tea CLI reference (non-issue entities)
SKILL.md
references/tea/ command docs
```
## Local issue store
Issues live in `tmp/issues/` (gitignore it) as flat markdown with one metadata
field per line — so `grep -l 'labels:.*type/bug' tmp/issues/*.md` works without
a parser.
It is **the store, not a cache of Gitea**:
- Identity is a slug (`wire-sqlc-appclick.md`), never a tracker number. Numbers
live in a `gitea:` field.
- `origin: local` is a durable state. An issue that never leaves your machine is
complete and valid.
- Pushing is additive — the file gains `gitea:` / `url:` / `synced:` and stays
put. Pulling overwrites the body: a fetch, not a merge.
- Nothing tracks drift. `synced:` tells you how old your copy is.