naudachu 9479babfe9 feat: implement the wiki field in the issue domain
format.md listed `wiki:` among the domain fields, between depends and
origin, and issue.py had never heard of it. The field fell into extra and
rendered with the foreign keys — sorted in after the sync fields, which
the same document forbids one line below the table. Written without
brackets it parsed as a single string, and nothing but a text editor
could set it.

Implemented rather than de-documented: page_ls.py --titles already
prints these titles, so the field was designed and only unwired.
DOMAIN_KEYS and LIST_KEYS learn it, Issue carries it, and issue_new.py
gets a repeatable --wiki flag.

Titles only, as the format says: no path, no sub_url, no lookup. The
tracker has no field for it, so it is never sent and a pull does not
bring it back — format.md now says so.

Closes #32

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:55:10 +05:00
2026-08-10 19:37:08 +05:00
2026-08-07 18:08:11 +05:00

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-runner agent Subagent on Haiku that runs the scripts and reports back a receipt — the mechanical half, off your main context
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
      ▲
      │ calls
tea-runner      EXECUTION  runs the scripts, reports a receipt — no opinions

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
  • 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 the project root's .claude/settings.local.json and takes effect immediately — no restart needed.

Once per project, not once per checkout: a git worktree shares its main checkout's pin. Both the hook and the scripts find it from inside a worktree, so don't run /tea:auth there — it would leave a second pin in a directory that disappears with the branch.

/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. The hook and the scripts look it up the same way — one search order, in skills/auth/scripts/pin.py.

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.

The tea-runner agent

The skills carry meaning; the scripts carry work. tea-runner is a subagent on Haiku that does the second half in its own context and hands back a receipt — what ran, what it touched, what failed, verbatim.

Delegate a batch: pull a milestone and rebuild the index, push the three issues you just wrote, bootstrap the label set, post a comment from a file you prepared. Spawning it for a single pull.py 42 costs more than running the command yourself; the saving is in the loop, the retry, and reading somebody else's stderr.

It cannot decide anything. No Edit, no Write, no --force, no closing or retitling, no raw tea, no pushing beyond the set it was handed. A missing type, a failed validation, an unpushed dependency come back as a question, not as a guess. The tea-guard hook applies to it exactly as it does to the main session — the pinned login is enforced on every call it makes.

Project layout

.claude-plugin/
  plugin.json                plugin manifest
  marketplace.json           marketplace catalog (makes `/plugin install` work)
agents/
  tea-runner.md              subagent (Haiku) that executes the scripts
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,
                                 body checkboxes
      issue_new.py               create a local issue from its type template
      issue_check.py             validate against the format
      issue_ac.py                list the body's checkboxes; tick one
      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, then drops the local file
      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.

An origin: local file is the issue — the store, and the only copy. Anything with origin: gitea is a working copy of something the tracker already has, and it is deleted as soon as a push confirms the tracker is up to date:

  • Identity is a slug (wire-sqlc-appclick.md), never a tracker number. Numbers live in a gitea: field.
  • origin: local is a complete state. An issue that never leaves your machine is valid and finished — but it is not permanent: pushing ends it.
  • A successful push deletes the local file (--update too) and prints the number and URL it now lives at. Only after a confirmed response: a failed call leaves the file exactly where it was. Get it back with pull.py <n> — same slug, same depends:, even after a rename in Gitea.
  • Pulling overwrites the body: a fetch, not a merge. It is also how a pushed issue comes back.
  • Nothing tracks drift, and there is no second copy to drift. A file that is still here has not been pushed.
S
Description
Development lifecycle with the usage tea (gitea cli tool) as a issue storage.
Readme 1.3 MiB
Languages
Python 96.3%
Shell 3.7%