A discussion leaves behind a directory of markdown somewhere outside this repo, and the only durable home for it is the Gitea wiki. Getting it there by hand means re-deriving the same three things every time: what each file should be called, where it goes, and whether the page already exists. Two skills wrap that, along the split the repo already uses. `skills/page` is domain, offline, stdlib-only, and knows nothing about Gitea. It imports a directory into a space under `tmp/wiki/`, titles every file, records the result in `.pages.json`, and writes the index. `skills/wiki` is the bridge — `wikimap.py` translates, and the transport is `_gitea.py`, the same one the issue side uses. There is no second transport, and `tea` has no wiki subcommand to offer one. The Gitea wiki is flat, and that fact shapes everything There are no directories. A title of `a/b` is stored as one file named `a%2Fb.md`, and Gitea escapes it by rules of its own: space becomes `-`, `/` becomes `%2F`, and a literal `-` forces a trailing `.-` marker so the two stay distinct. `Chain decisions — DC` under two levels of prefix comes back as `Simple-Chains%2FParked%2FChain-decisions-%E2%80%94-DC`. So `sub_url` is the identity, it is read back from whatever the API returned, and it is never constructed. One built by hand that is almost right does not fail — it creates a second page and abandons the first. And a real subdirectory committed into a wiki's git repository is a ghost: the file exists, the API and the web UI do not see it. `folder/page.md` in this repo's own wiki is one. Nothing here clones a wiki repo. A title is a decision, not a derivation Titles come from the first heading, because there is no mechanical route from `03-q-01-do-we-know-the-chain-participant-by-name.md` to `Q-01. Do We Know the Chain Participant by Name`. But they are derived exactly once. A re-import replaces bodies and keeps titles, so editing a heading cannot rename a published page — which would not rename it, it would publish a second one. `--retitle` opts in. It finds the prior entry by `source` rather than by path, because the path is derived from the title and a retitle moves it; looked up by path the page would read as new and the next push would duplicate it. The old file goes, `sub_url` comes along, and `pushed` is cleared — a rename can leave the body byte-identical, and push decides by body hash alone, so a stale hash would skip the rename forever. Ordering is a `NN-` file-name prefix and never reaches the title. `00-` means "this is the directory's own page", and that page is named for the directory, not for its own heading: a child's title has to extend its parent's exactly, and `ideas/00-intro.md` opens with "Ideas for chain business requirements". Path collisions are reported and never resolved. Picking a winner is how a discussion loses a document. The index is navigation, not decoration Nothing draws a tree from flat titles. `page_index.py` writes one as an ordinary page, nested by title depth rather than by manifest path order — those disagree, since on disk `Top/System.md` sorts before `Top/Ideas/Scale.md` while in the hierarchy System is a child and Scale a grandchild. A parent with no page of its own still gets a node, so its children are not hidden. Links use `sub_url` when there is one and Gitea's `[[Title|label]]` syntax when there is not, so the order is push, rebuild, push. The same stances as the issue store, for the same reasons Pull overwrites, push is additive and never deletes, change detection is one hash and there is no drift model. A page with no `sub_url` has never been published, and that is a durable state. Issues gain a `wiki:` field holding page titles — titles, not URLs, so the reference stays in the domain. It already round-trips as a foreign key; this documents it. Verified against a live Gitea 1.26.1: create, update with a message, unchanged-skip, prefix-filtered pull, byte-identical round trip, and the per-page revision history carrying the operator's own words. The probe pages were deleted afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 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. Source spec: the project wiki (Issues-Workflow).
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, and an issue pushed to a tracker keeps it too. Tracker numbers are a
foreign key stored in a field, never the name of anything.
tmp/issues/wire-sqlc-appclick.md
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]
wiki: [Simple Chains/Ideas/Chain core]
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 |
wiki |
domain | page titles this issue is written up in; may be empty. Titles, not URLs — a title is a name for a document and stays in this layer, a URL is tracker bookkeeping. /tea:page owns what those titles mean; page_ls.py --titles prints them |
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 durable state, not a pending one. An issue that never
leaves this machine is complete and valid. Pushing is optional and additive.
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.