Replace fetch_issue.py with four scripts around a flat, greppable cache in tmp/issues/. Planning stays offline and issues reach Gitea in one push: - issue_get.py: fetch by key or by filter (--milestone/--label/-q). The list endpoint carries issue bodies, so a whole milestone costs one request per 50 issues. Gitea silently ignores an unresolvable milestones= filter and returns the entire backlog, so the milestone is resolved up front and every returned issue is re-checked locally. --deps walks the dependency graph downwards via the structured sections plus native dependencies and writes tree-<slug>.md. - issue_push.py: validate a local draft against the canonical format, create missing labels with the right colors and exclusivity, POST, delete the draft. - issue_list.py: discovery to stdout, writes nothing. - issue_index.py: rebuild INDEX.md from what is on disk. Files use one metadata field per line with inline lists so plain grep works without a parser. This is a cache and a drafting area, not a mirror: no drift tracking, no sync back. Projects are not fetchable — the projects API is 404 on Gitea 1.26; documented alongside the milestone caveat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
name, description
| name | description |
|---|---|
| use | Reference docs for the `tea` CLI — Gitea's command-line client. Load when the user asks about Gitea repos, issues, pulls, releases, actions, or other Gitea entities, to look up the right `tea` command and flags. Always write the login as the literal placeholder --login "$GITEA_LOGIN" — the tea-guard hook substitutes the operator-pinned login; set it with /tea:auth. |
/tea:use — tea CLI reference
Reference material for the tea CLI (Gitea's official command-line client).
Use these docs to look up commands, flags, filters, and output fields before
running tea via Bash.
Login: always write the placeholder, never a name (enforced)
Every tea invocation that touches Gitea MUST carry the login as the literal
placeholder --login "$GITEA_LOGIN" (or -l "$GITEA_LOGIN"). Do not
substitute an actual login name yourself.
The tea-guard PreToolUse hook enforces this and resolves it:
- no
--login→ blocked. --login "$GITEA_LOGIN"→ the hook reads the operator's pinned login from.claude/settings.local.json(env.GITEA_LOGIN) at call time and rewrites the command to use that literal before it runs.--login <some-name>or any other variable → blocked. You may not choose the login; only the operator does (via/tea:auth).- no login pinned → blocked with a pointer to run
/tea:auth.
Why: without an explicit login tea silently falls back to the machine's
default (possibly the user's personal account), and a login you pick may be
the wrong identity. Pinning is the operator's decision; the hook guarantees it.
The pin takes effect immediately — no restart. Only tea logins list and
tea --version/--help are exempt from the guard.
How to use
- Identify the entity in the request: issues, pulls, labels, milestones, releases, times, repos, branches, actions, webhooks, comments, notifications, etc.
- Find the matching command in the index below.
- Run it via Bash with the placeholder login, e.g.
tea issues list --login "$GITEA_LOGIN" --repo owner/repo --state open. (The hook rewrites"$GITEA_LOGIN"to the operator-pinned login.)
tea auto-detects owner/repo from $PWD inside a git repo; otherwise pass
--repo owner/repo (or -r). Login is not auto-detected — it is pinned
per-project by the operator (see /tea:auth) and injected by the guard.
Config lives in $XDG_CONFIG_HOME/tea.
Issues: work on local files, not on live tea calls
Never run tea issues <n> -o json or tea api .../issues/<n> to read an
issue — the full JSON payload (avatars, nested user objects, every comment
body) lands in your context whether you need it or not. Use the scripts in
<skill-base-dir>/scripts/. They pull issues into a flat, greppable cache
under $PWD/tmp/issues/ and print only a compact index.
This is a cache, not a mirror: nothing tracks drift, nothing syncs back.
Refetch when you need current data; the fetched: field tells you the age.
| Script | Network | What it does |
|---|---|---|
issue_get.py <key…> or issue_get.py --milestone M | --label L | -q TEXT |
yes | fetch issue(s) → tmp/issues/<n>.md (+ <n>.comments.md, tree-<slug>.md) |
issue_list.py [--state] [--label] [--milestone] [-q TEXT] |
yes | discovery: one line per issue to stdout, writes nothing |
issue_push.py <draft…>|--all [--keep] [--dry-run] |
yes | validate a draft, create labels, POST the issue, delete the draft |
issue_index.py |
no | rebuild tmp/issues/INDEX.md (auto after get/push) |
tmp/issues/INDEX.md table of everything cached — read this first
tmp/issues/42.md metadata block + `# Title` + body
tmp/issues/42.comments.md comments (only with --comments)
tmp/issues/tree-40.md dependency map (only with --deps)
tmp/issues/drafts/<slug>.md issues not yet created in Gitea
Key forms for <key>: 42, #42, owner/repo#42, or a full issue URL. Repo
defaults to the current directory's git remote (add --repo owner/repo
outside one). No --login on any script call: they resolve the operator's pin
from .claude/settings.local.json themselves — same source as the tea-guard
hook. No pin → exit with a pointer to /tea:auth.
Fetching a whole set: milestone, label, search
Do not loop issue_get.py over numbers to pull a group — pass the filter. The
list endpoint carries the issue bodies, so a milestone costs one request per
50 issues, not one per issue:
python3 <skill-base-dir>/scripts/issue_get.py --milestone 6 # id or title
python3 <skill-base-dir>/scripts/issue_get.py --milestone v0.2 --deps
python3 <skill-base-dir>/scripts/issue_get.py --label type/bug --label comp/hooks --state all
python3 <skill-base-dir>/scripts/issue_get.py -q sqlc --limit 20
Filters AND together; --state defaults to open; --limit defaults to 100.
Keys and filters are mutually exclusive. --comments stays single-issue —
loop over the numbers when a whole thread set is needed.
Two traps this handles for you:
- Gitea silently ignores an unresolvable milestone filter and returns the
whole backlog. The script resolves the milestone first (exits listing the
real ones if it does not exist) and re-checks every returned issue locally.
Never trust a raw
tea api ...issues?milestones=Xcall for this. - Projects are not fetchable. The projects API is not exposed (404 on
Gitea 1.26 for
repos/…/projects,orgs/…/projects,projects/{id}). Use milestones or labels; project columns live in the web UI only.
After a filtered fetch, INDEX.md carries a milestone column, and the cache
is greppable by it: grep -l 'milestone: v0.2' tmp/issues/*.md.
Working a feature as one document
--deps walks the dependency graph downwards — the structured
## Depends on and ## Issues sections plus Gitea's native dependencies.
Prose #N mentions are ignored on purpose, or the walk would drag in half the
backlog. Comments are not fetched during a walk (loop over the numbers if you
need them).
python3 <skill-base-dir>/scripts/issue_get.py 40 --deps # feature + children
Read tree-40.md once for the shape (a filtered fetch writes one forest,
tree-<slug>.md), then grep the files as one document:
grep -ln 'depends:.*#42' tmp/issues/*.md # who depends on #42 (upwards)
grep -l 'labels:.*type/bug' tmp/issues/*.md # all cached bugs
grep -A3 '## Acceptance criteria' tmp/issues/4*.md
grep -c '^- \[ \]' tmp/issues/42.md # open checkboxes
Metadata is written one field per line with inline lists (labels: [a, b])
precisely so plain grep works without a parser.
Creating issues: draft locally, push once
During planning write drafts to tmp/issues/drafts/<slug>.md — no network, no
tea call. A draft is the metadata block with labels: only, plus the
canonical body:
---
labels: [type/task, tech/sql]
---
# Wire sqlc into the appclick repo layer
## Summary
...
When the plan is agreed, issue_push.py validates the format (exactly one
type/*, English title without a type prefix, ## Summary / ## Spec /
## Acceptance criteria present), creates missing labels with the right
colors and exclusivity, POSTs, prints the URL and deletes the draft — the
issue lives in Gitea now. --keep writes tmp/issues/<n>.md instead;
--dry-run validates without touching the network. Guided procedure:
/tea:issue.
Index
- tea CLI overview — global flags, common options, output formats
- ENTITIES — issues, pulls, labels, milestones, releases, times, repos, branches, actions, webhooks, comment
- HELPERS — open, notifications, clone, api
- MISC — whoami, admin
- SETUP — logins, logout, ssh-keys
- ISSUE FORMAT — canonical issue format: label
namespaces (
type/*,severity/*exclusive;tech/*,comp/*free), typesbug|task|refactor|test|feature|draft, templates, dependencies, title and language rules. MANDATORY whenever creating or editing an issue; the/tea:issueskill is the guided procedure for it.
Rich payloads — write to $PWD/tmp/ first, then tea api
Entity subcommands (tea comment, tea issues create, tea pulls create, …)
are built for humans at a TTY. With a large or formatted body they can hang
silently — an empty-looking positional arg triggers $EDITOR fallback, or a
scope/confirm prompt waits on a TTY that doesn't exist. The harness eventually
kills the process (e.g. exit 144 = 128 + SIGURG on macOS).
Rule: for any non-trivial body (multi-line, or containing markdown / code
fences / backticks / pipes / tables), bypass entity commands. Save the full
request payload to $PWD/tmp/ first, then POST via tea api.
Issue creation is already wrapped: use issue_push.py (above) instead of
hand-rolling the JSON. The procedure below covers everything else — comments,
pulls, releases, and PATCHes to existing issues.
Procedure
- Ensure the target dir exists:
mkdir -p tmp/{kind}where{kind}iscomment,issue,pull,release, etc. - Write the complete request body as JSON to
$PWD/tmp/{kind}/<slug>.json. One file = one request. Use a quoted heredoc to avoid shell expansion:Newlines inside the body must be encoded asmkdir -p tmp/comment cat > tmp/comment/issue-60.json <<'EOF' {"body": "## Heading\n\nMulti-line markdown with `code`, | tables |, and ```fences```."} EOF\nin the JSON string. If composing programmatically, pipe throughjq -Rs '{body: .}' < body.md > tmp/comment/issue-60.json. - POST with
tea api, passing the file with-d @<path>:tea api --login "$GITEA_LOGIN" \ -X POST -d @tmp/comment/issue-60.json \ repos/{owner}/{repo}/issues/60/comments - Keep the file.
tmp/should be gitignored; the saved payload is useful for retries, edits (PATCH), and debugging failed posts.
Common endpoints
| Action | Method + endpoint |
|---|---|
| Comment on issue/PR | POST repos/{owner}/{repo}/issues/{n}/comments |
| Edit comment | PATCH repos/{owner}/{repo}/issues/comments/{id} |
| Create issue | POST repos/{owner}/{repo}/issues |
| Edit issue/PR body or title | PATCH repos/{owner}/{repo}/issues/{n} |
| Create PR | POST repos/{owner}/{repo}/pulls |
| Create release | POST repos/{owner}/{repo}/releases |
Short single-line bodies (e.g. tea comment 42 "lgtm" --login "$GITEA_LOGIN")
are still fine via entity commands. Always the placeholder, never a login name.
Tips
- Pass
-o jsonfor structured output when parsing programmatically. - Use
--fields, -fto narrow columns. - Pagination:
--page, -p <n>and--limit, --lm <n>(defaults 1 / 30). - If a
teacommand is blocked bytea-guard: either you forgot--login "$GITEA_LOGIN", you wrote a literal login name instead of the placeholder (not allowed — let the guard substitute), or no login is pinned (run/tea:auth).