Issue Tracking
How work is organised in the project’s Forgejo issue tracker — a self-hosted Forgejo during reconstruction, moving to Codeberg at public launch (the GitHub repo stays a code-only mirror; issues live only on the canonical Forgejo). This document is the convention; the planning documents it references remain the content source of truth.
Tooling note: the
fjCLI (forgejo-cli) can create/edit/search/close issues and manage labels, but has no milestone or project commands — milestones and the roadmap board are managed via the web UI or the REST API (ortools/trackerctl, which wraps the API). Pointfjat the repo with-r <owner>/<repo>, and pass-H <base-url>when the instance isn’t plain HTTPS on the default port (a self-hosted dev instance may serve plain HTTP on a custom port; a shell wrapper can inject it). Codeberg needs no-H.
1. Principles
- Documents own content; tickets own workflow state. A ticket carries open/closed, assignee, order, and links — not the authoritative description. The authoritative description lives in the planning document the ticket was minted from (see §4). Tickets backlink to it; they do not duplicate prose that would then drift.
- Just-in-time decomposition. Only the active release and the next release are exploded into leaf tickets. Everything beyond that exists as epic tracker issues (§3) or as entries in a planning document — not as tickets. Leaf tickets for work two-plus releases out are noise: they get rewritten before anyone touches them and they pollute search.
- Order is explicit. Forgejo has no priority/rank field, and issue-number order is brittle. Execution order lives in per-release tracker issues (§5). “Do the next ticket for vX.Y” resolves to the first unchecked box in that release’s tracker.
- Close-out keeps three artifacts in sync. Code (PR), the planning document, and the ticket are all updated in one ritual (§6).
2. The tiers
priority-bucket milestone (gate-v0.5, gate-v1.0, v0.x, v1.x, v2.x+)
└── epic tracker issue kind/epic — ordered task list of its children, links to epics/NN-*.md
└── leaf ticket one actionable unit of work
└── standalone leaf ticket small items with no epic (most ROADMAP.md entries)
bucket tracker issue kind/release-tracker — ordered list of the epics + standalone tickets for one bucketForgejo has no native epic type; the “epic” and “release tracker” tiers are ordinary issues whose
body is a Markdown task list referencing child issue numbers (- [ ] #42 …). Forgejo renders
those as live links with a progress bar. Sub-issue / dependency links in the web UI are a welcome
nicety but the task-list body is what the CLI can read, so it is authoritative for order.
3. When to create an epic tracker issue vs. leaf tickets
- A roadmap theme or feature that is more than one actionable unit of work → an epic tracker
issue (
kind/epic), body = ordered task list, linked to itsepics/NN-*.mddocument once that exists. Explode it into leaf tickets only when its release becomes active or next. - A small, self-contained unit (most
ROADMAP.mdentries) → a standalone leaf ticket, no epic. - Far-future work (two-plus releases out) → leave it as a
kind/epicplaceholder under the version milestone with no leaf tickets, or just as a planning-document entry. Do not decompose it.
4. Labels
Labels carry the axes that fj issue search can filter on (-l/--labels, -s/--state). Create
them once with fj repo labels create.
source/* — which planning document owns this work (drives the close-out ritual, §6)
| Label | Origin document | On completion |
|---|---|---|
source/v1x-backlog | docs/project/ROADMAP.md | move the entry to a ### Done block under its version |
source/features | FEATURES.md | update the feature’s (landed: …) annotation |
source/roadmap | PROJECT-DETAILS.md §6.2 or a roadmap doc | close; no doc edit beyond roadmap status |
source/epic-task | epics/NN-*.md | tick the task’s acceptance criteria in the epic file (existing AGENTS.md workflow) |
source/triage | none — a bug or idea filed directly | close; add a regression test if it was a bug |
kind/* — the shape of the issue
kind/epic, kind/release-tracker, kind/feature, kind/bug, kind/chore, kind/docs.
area/* — the part of the system
area/statemgmt, area/agent, area/nats, area/server, area/schema, area/bootstrap,
area/cli, area/security, area/policy, area/observability, … (add as needed; keep the set
small).
roadmap-backlog
Umbrella label on every issue minted from ROADMAP.md, so they are distinguishable from regular
tracker traffic. Renamed from the original v1x-backlog to drop the version pin (the underlying
document was renamed V1X-BACKLOG.md → docs/project/ROADMAP.md earlier). The paired source
label source/v1x-backlog still carries the legacy name; renaming it is tracked as a v0.x
ROADMAP entry. (Equivalent umbrella labels may be added for other bulk imports.)
Version
Version is not a label and not part of the title — it is the milestone, where each
milestone corresponds to a priority bucket (gate-v0.5, gate-v1.0, v0.x, v1.x, v2.x+).
Re-slotting work between buckets is a milestone change, nothing else. (A version label may be
added only if CLI filtering by bucket becomes necessary; the milestone is canonical.)
5. Milestones, tracker issues, and the roadmap board
One milestone per priority bucket (
gate-v0.5,gate-v1.0,v0.x,v1.x,v2.x+). Defined intools/trackerctl/config/milestones.yamland applied bytrackerctl sync-milestones. The milestone gives the progress bar and an optional due date; every leaf ticket and epic for that bucket is assigned to it. Seedocs/project/VERSIONING.mdfor what each bucket means and what gates each milestone.One bucket tracker issue per priority bucket — title
<bucket> — tracker, labelkind/release-tracker, assigned to that milestone. Its body is an ordered checklist of the bucket’s leaf issues, generated bytrackerctl gen-tracker --version <bucket>fromtools/trackerctl/config/release-order.yaml(the canonical execution order — entries listed there first, anything unlisted appended inROADMAP.mdfile order). Example:Execution order for gate-v1.0. "Next item" = first unchecked box. - [x] #9 Schema versioning via `golang-migrate` - [ ] #10 Reactor engine + event lifecycle tracking ← next - [ ] #20 `state.apply.skip` event taxonomy + wiring - [ ] #19 Replay protection on agent commandsReprioritising = editing
release-order.yamland re-runninggen-tracker(it preserves ticked boxes; the rest of the body is regenerated, so don’t hand-edit it or keep notes there). Order entries so everyBlocked bytarget precedes its dependents. “Do the next<bucket>item” = first unchecked box.One Forgejo Project (“Roadmap”) — board columns
Backlog / Next release / In progress / In review / Done— as a human-facing dashboard across milestones. Maintained via the web UI; not driven byfj. The milestone + labels + tracker issues remain the machine-readable layer.
6. Ticket lifecycle
Creating a leaf ticket:
- Title = the feature/work name, no version prefix. For bulk imports, a stable greppable prefix is
fine (e.g. backlog entries keep their
####heading text). - Body = the originating document section copied verbatim (What / Why / Acceptance / References),
plus: a backlink to the document heading anchor, the epic/task it was deferred from (if any), and
Blocked by #Nlines for each dependency. - Apply labels: one
source/*, onekind/*, one or morearea/*, plusroadmap-backlog(or the relevant umbrella).fj issue createcannot set labels at creation time — create, thenfj issue edit <n> labels. - Assign the milestone (priority bucket) (web UI / API).
- Slot it into the execution order: add its title to
tools/trackerctl/config/release-order.yamlunder the right priority bucket and re-runtrackerctl gen-tracker --version <bucket>; add it to the epic tracker’s list too if it has a parent epic. (Most leaf issues, and the per-bucket tracker itself, are created in bulk bytrackerctl gen-issues/gen-trackerrather than one at a time — this step is for issues added later.)
Picking up “the next bucket item”:
- Open
<bucket> — tracker; take the first unchecked box, say#15. - Open
#15; read Acceptance + References. - Check
Blocked by— if any referenced issue is still open, stop and report; do not proceed out of order. - If the work maps to an
epics/NN-*.mdtask, follow theAGENTS.mdepic-task workflow (present a plan, get explicit approval) before writing code. - Implement.
Closing out:
- PR with
Closes #15in the description (auto-closes the ticket on merge). - Check the box in the release tracker (and the epic tracker, if any).
- Update the originating document per the
source/*row in §4 — e.g. move aROADMAP.mdentry to that version’s### Doneblock with a “landed in PR #x” note.
Three artifacts — code, planning document, ticket — one ritual. If you find them out of sync, the planning document wins on content and the ticket wins on state; reconcile toward those.
7. First adoption
Do not mint every backlog/feature/roadmap item at once. The full set of priority-bucket
milestones (gate-v0.5, gate-v1.0, v0.x, v1.x, v2.x+, per docs/project/VERSIONING.md)
exists in tools/trackerctl/config/milestones.yaml so they are visible and assignable — but
creating tickets is gated separately: trackerctl gen-issues --versions gate-v0.5 only
decomposes that bucket. Bring up gate-v0.5 first: run trackerctl sync (labels + milestones),
then trackerctl gen-issues --versions gate-v0.5 to mint the v0.5 blockers under ## gate-v0.5,
then trackerctl gen-tracker --version gate-v0.5 to build the gate-v0.5 — tracker. Decompose
gate-v1.0 and the broader v0.x bucket when work starts on each (gen-issues --versions gate-v1.0 then gen-tracker --version gate-v1.0, etc.).
8. Tooling and the test → production cutover
The label set, milestones, and the leaf issues are not hand-clicked — they are generated by
tools/trackerctl from configuration in the repo:
tools/trackerctl/config/labels.yaml— the label set (trackerctl sync-labels).tools/trackerctl/config/milestones.yaml— every priority-bucket milestone (gate-v0.5,gate-v1.0,v0.x,v1.x,v2.x+) (trackerctl sync-milestones).docs/project/ROADMAP.md— the leaf-issue source.trackerctl gen-issues --versions <bucket>creates them (one bucket at a time);trackerctl reconcile-issuespushes any later edits (re-slotting, label fixes) onto the already-created issues — it updates milestone +source/*/kind/*/ umbrella labels and never touchesarea/*.tools/trackerctl/config/release-order.yaml— execution order per bucket (trackerctl gen-tracker --version <bucket>, which creates/updates the<bucket> — trackerissue).
All operations are idempotent and host-parameterized (--host, --repo, --apply,
FORGEJO_TOKEN); gen-issues and reconcile-issues take --versions to scope to one or more
buckets, gen-tracker takes --version for the tracker issue. See tools/trackerctl/README.md.
Cutover model: (b) clean regeneration. When the project is announced, the production tracker is
built by re-running trackerctl against the production Forgejo — not by migrating the internal test
repo. Consequences:
- The internal server is a rehearsal; it does not need to be kept pristine.
- Issue numbers differ between servers. Nothing that must survive the cutover may hard-code
#N. Canonical execution order therefore lives inconfig/release-order.yaml(regenerable into the tracker issue bygen-tracker), not in scattered issue bodies. - Anything created directly on the test server that should exist on prod must be reflected back into the config files / backlog doc, or it will not be regenerated.
- The Codeberg/GitHub mirrors carry git only — no labels, milestones, or issues — so the cutover targets exactly one canonical Forgejo instance.