The plugin is issues and nothing else now. `skills/page` (the page-tree domain) and `skills/wiki` (its bridge to a Gitea wiki) are gone, and with them the issue domain's `wiki:` field — page titles were the only thing that tied the two domains together, and a field the tracker has no column for never came back from a pull anyway. What is left is the shape AGENTS.md already claimed for the rest of the repo: one domain, one bridge, one transport. The docs, the plugin manifest, and tea-runner's skill table now say so too, and test_payload_root walks the one script directory that remains. Also removes openspec/config.yaml; nothing in the repo referenced it. 378 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
Issue format
Canonical format for every issue in this project, whether it ever reaches a tracker or not. Designed to be unambiguous for both humans and LLMs: fixed English section headers in a fixed order, verifiable acceptance criteria, one issue = one deliverable.
Nothing here depends on Gitea. How these files are mapped onto a tracker is the
sync layer's business — see /tea:sync.
Identity
An issue is one file, tmp/issues/<id>.md, and id is a slug: lowercase
ASCII, digits, single dashes, derived from the title. The slug is the
identity. It is stable for the life of the issue — a retitled issue keeps its
slug; an issue pushed to a tracker, deleted locally and fetched back a month
later keeps it too. Tracker numbers are a foreign key stored in a field, never
the name of anything.
tmp/issues/wire-sqlc-appclick.md
A slug never contains a dot, which is how the store tells an issue from the
files parked beside it (<id>.comments.md).
Stability is a promise the format makes, so something has to keep it once the
file is gone. That is the sync layer's problem and its answer is a marker in the
body — see /tea:sync; the domain neither writes nor reads it, and it never
appears in the file on disk.
Metadata block
One field per line, lists inline, so plain grep works without a parser:
---
id: wire-sqlc-appclick
state: open
labels: [type/task, tech/sql]
assignees: [naudachu]
milestone: v0.2
depends: [migrate-schema]
origin: gitea
branch: feat/wire-sqlc
gitea: claude-skills/tea#42
remote-updated: 2026-08-09T18:24:01Z
synced: 2026-08-09T18:40:00Z
url: https://git.noodles.cam/claude-skills/tea/issues/42
---
# Wire sqlc into the appclick repo layer
## Summary
…
| Field | Owner | Meaning |
|---|---|---|
id |
domain | slug; equals the file name |
state |
domain | open or closed |
labels |
domain | see namespaces below; exactly one type/* |
assignees |
domain | logins; may be empty |
milestone |
domain | title, or none |
depends |
domain | ids this issue depends on — the authoritative graph |
origin |
domain | local, or the name of a tracker this also lives in |
gitea |
sync | the handle in that tracker: owner/repo#N |
branch |
sync | the tracker's branch link (Gitea ref); push fills an empty one with the current git branch, and never overwrites a filled one |
url, synced, remote-updated, comments |
sync | bookkeeping |
Domain fields render first, in the order above; sync fields follow, sorted.
origin is domain-owned on purpose: whether a piece of work exists anywhere
but here is a fact about the work. Where that is, and how to reach it, is the
sync layer's business — the domain carries gitea: and the rest through
load/save verbatim and never reads them. That passthrough is why one file can
represent a local issue and a synced one without a second format.
origin: local is a complete state, not a pending one. An issue that never
leaves this machine is valid and finished work; pushing it is optional and
nothing here treats it as a draft.
It is not a permanent state, and it is what the file's fate depends on:
origin: |
what the file is | what a push does to it | what eviction does to it |
|---|---|---|---|
local |
the issue itself — the only copy there is | creates it in the tracker, then deletes the file | nothing, ever — in any state, named or not |
| a tracker | a working copy of something the tracker already has | updates the tracker, then deletes the file | removes it once state: closed |
A successful push deletes tmp/issues/<id>.md (and <id>.comments.md), on
create and on --update alike. What is in the store is what has not left this
machine; everything else is fetched again when it is needed. The rule, its
safety conditions, and how the slug survives are /tea:sync's to state.
A closed issue is evicted from the store by issue_evict.py — same trade,
one condition more: the work is done and it exists somewhere else. An
origin: local issue is never evicted, because there is nowhere to fetch it
back from. The store is a working set, not an archive; pull.py <n> fetches a
closed issue again whenever it is wanted.
The id never changes across that round trip, which is why depends: in other
issues keeps working. That is the format's promise; the mechanism is not.
Language rules
- Issue title: English, imperative mood, no type prefix — the type lives in
the label, not the title. Good:
Fix tea-guard crash on empty settings file. Bad:fix: crash,[bug] crash,Крашится гвард. - Section headers: the exact English literals below, as
##headings, in the given order. Do not translate, rename, or reorder them. - Body prose (text inside sections): Russian.
Label namespaces
Four namespaces classify an issue. Two are exclusive (at most one label from the namespace), two are free-form:
| Namespace | Exclusive | Purpose |
|---|---|---|
type/* |
yes | What kind of work; primarily its business value. Mandatory, exactly one. |
severity/* |
yes | Business impact. At most one; apply when the impact is known. |
tech/* |
no | Technology the issue is bound to. Any number. |
comp/* |
no | System component of this repo. Any number; no preset — project-specific. |
type/* — mandatory, exactly one
| Label | Meaning |
|---|---|
type/bug |
Something behaves incorrectly in existing code |
type/task |
Implementation of new functionality |
type/refactor |
Internal restructuring: file moves, architecture; behavior must not change |
type/test |
Writing or fixing tests |
type/feature |
Container: several issues delivering one unit of business value |
type/draft |
Idea captured for later; not ready for work |
severity/* — at most one
severity/low, severity/medium, severity/high, severity/showstopper,
severity/critical.
tech/* — any number
Technology-bound labels, e.g. tech/sql (pgx, sqlc, sql-migrate — persistent
storage), tech/obs (grafana, loki, prometheus, alloy — observability),
tech/postgres.
comp/* — any number
Components of this repo's system, e.g. comp/appclick. No preset list —
derive from the project.
Label colors are not part of the format: a hex code is how a tracker paints a chip, not what an issue is. They live in
skills/sync/scripts/map.pyand are applied on push.
Dependencies
depends: in the metadata block is the graph, and it holds ids:
depends: [migrate-schema, add-pool-cfg]
An optional ## Depends on section, placed right after ## Spec, carries the
human explanation — one reference per line, with a reason where it helps:
## Depends on
- migrate-schema — нужна схема БД из этого issue
- add-pool-cfg
The section is prose and is passed to and from a tracker unchanged; only
depends: is walked when the graph is computed. Keeping them consistent is on
you — issue_check.py warns when the section names an id that depends: does
not list. Omit the section when there are no dependencies; never write an empty
one.
A type/feature container writes the same relation under ## Issues instead
(see the template below). Same direction, same rule: every id named there also
belongs in that issue's depends:. The warning names whichever of the two
sections the reference actually came from.
Draw the graph with issue_tree.py. The reverse direction is a grep:
grep -ln 'depends:.*migrate-schema' tmp/issues/*.md
Shared rules
## Summaryis always the first section;## Acceptance criteriais always present (exception:type/draft). These two are the anchors every reader (human or LLM) relies on.## Specis mandatory in every type. Its value is a repo path (docs/specs/auth.md), a URL, or the literalnonewhen no spec exists. Never omit the section and never invent a link —noneis an explicit, valid answer.- Acceptance criteria are
- [ ]checkboxes; each item is an objectively checkable condition, not an aspiration. - A checkbox is item markup, not a property of one section:
- [ ]unticked,- [x]ticked, and it means the same under## Issuesas under## Acceptance criteria. An item that wraps continues on an indented line and is still one item. A- [ ]inside a ``` code fence is an example of the markup, not state. Tick them withissue_ac.py, which reads the whole body on exactly these rules and rewrites one character; progress (3/7) is counted off the body and is never a metadata field. - Code references use the
path/file.ext:lineform; related issues by id. - Screenshots are allowed but their content must be duplicated as text — an LLM reading these files cannot see images.
- If acceptance criteria grow past ~5 unrelated items, split the issue (or
promote it to a
type/featurecontainer with child issues).
Template: type/bug
## Summary
Что сломано и где проявляется, одно-два предложения.
## Spec
`docs/specs/auth.md`, URL — или `none`.
## Steps to reproduce
1. …
2. …
## Expected
Что должно было произойти.
## Actual
Что происходит на самом деле: вывод команды, лог.
## Environment
Только релевантное: версии, ОС, конфигурация.
## Acceptance criteria
- [ ] баг не воспроизводится по шагам выше
- [ ] добавлена проверка на регрессию (если применимо)
Template: type/task
## Summary
Что нужно сделать, одно-два предложения.
## Spec
Ссылка или `none`.
## Motivation
Какую проблему пользователя/системы это решает.
## Acceptance criteria
- [ ] проверяемое условие
- [ ] …
## Constraints
Что НЕ входит в объём; технические рамки. (опционально)
Template: type/refactor
## Summary
Что перестраиваем и в каких файлах (`path/file:line`).
## Spec
Ссылка или `none`.
## Motivation
Чем плохо текущее состояние: дублирование, связность, читаемость.
## Invariants
Что НЕ должно измениться: поведение, публичные API, форматы данных.
## Acceptance criteria
- [ ] проверяемое условие (тесты зелёные, старый путь удалён, …)
Template: type/test
## Summary
Что покрываем тестами и где (`path/file:line`).
## Spec
Ссылка или `none`.
## Motivation
Зачем: регрессия после бага, пробел в покрытии, флаки-тест.
## Test cases
- сценарий → ожидаемый результат
- …
## Acceptance criteria
- [ ] перечисленные кейсы покрыты и зелёные
- [ ] тесты проходят в CI
Template: type/feature
A container: one unit of business value delivered by several child issues.
Child issues carry their own type/* (task, bug, test, …) and know
nothing about the container.
The container depends on its children, never the reverse. Every child id
goes in the container's own depends: and, as prose, in its ## Issues
section; a child's depends: is for that child's real dependencies and must
not point back at the container. Keep implementation detail in the children;
the feature body stays at business level.
That direction is not a convention picked at random. "The container is closed
when its children are closed" is a dependency relation. "This child belongs
to that feature" is a membership relation, and membership has no place in a
dependency graph. Pointed the other way the two rules contradict each other:
the moment the container listed a child that already depended on it,
issue_check.py would report ERROR cycle. With the edge going down, the
graph reads as nesting — issue_tree.py draws the container as the root with
its children beneath it — and the check is green.
So the container's metadata block carries the children:
depends: [wire-sqlc-appclick, add-pool-cfg]
and its body repeats them for a human:
## Summary
Бизнес-ценность одним-двумя предложениями.
## Spec
Ссылка или `none`.
## Motivation
Какую проблему пользователя/системы это решает.
## Issues
- [ ] wire-sqlc-appclick — краткое описание части
- [ ] add-pool-cfg — краткое описание части
## Acceptance criteria
- [ ] все дочерние issues закрыты
- [ ] проверяемое условие уровня фичи (например, e2e-сценарий работает)
Template: type/draft
A parking spot for ideas that are not fleshed out yet. Minimal structure, no acceptance criteria required. Before implementation starts, a draft MUST be promoted: relabeled to a concrete type and rewritten into that type's template.
## Summary
Идея одним-двумя предложениями.
## Spec
Ссылка или `none` (для драфтов обычно `none`).
## Notes
Свободные заметки: что известно, открытые вопросы, варианты.
Containers beyond type/feature
- Milestone — a set of issues with an optional time bound. Locally it is
just the
milestone:field; a tracker-side milestone must already exist for a push to attach the issue to it. - Project — a set of issues tracked by status columns (Backlog, ToDo, InProgress, Ready, Done). Not represented in this format and not reachable through the Gitea API — web UI only.