Issue tracking
We use Gitea milestones + issues as a lightweight checklist + memory aid. No PR auto-linking, no Project boards, no status labels — just open vs. closed and “is this in the right milestone?” One source of truth, one URL, zero ceremony.
Where it lives
| What | Where |
|---|---|
| Repo | https://git.rizoma.sa/rizoma/wiqaia |
| All milestones | https://git.rizoma.sa/rizoma/wiqaia/milestones |
| Open issues | https://git.rizoma.sa/rizoma/wiqaia/issues?state=open |
| Critical-path filter | https://git.rizoma.sa/rizoma/wiqaia/issues?state=open&labels=critical-path |
| Bootstrap script | scripts/gitea-bootstrap-issues.py |
Conventions
Milestones map 1:1 to the plan
Every plan milestone (M01–M10) is a Gitea milestone with the same name. When you open M01 — Foundations in Gitea, you see every open + closed issue that belongs to that plan milestone, plus a single progress bar — useful for “how close are we to M01 actually being done?”
If you find yourself wanting to create a Gitea milestone that doesn’t exist in apps/docs/src/routes/plan/, stop. Either:
- the work belongs in an existing plan milestone (most likely — drop it there), or
- the plan itself needs a new milestone (rare — edit the plan doc first, then add the matching Gitea milestone).
The Gitea side mirrors the plan; the plan is the canonical artifact.
Labels — minimal by design
Ten labels, no more without good reason:
| Label | Meaning |
|---|---|
cloud | GCP / Terraform / GKE / networking / observability |
backend | NestJS / Postgres / API |
ai | Tier-0/Tier-1/Tier-2 + eval + prompts |
frontend | React / dashboards / 3D twin |
edge | Go supervisor / Python edge apps |
integration | Fire panel / NVR drivers / 3D model |
docs | Plan docs / runbooks / handbooks |
contracts | packages/contracts (protos + DTOs) |
critical-path | Must-have for the Hajj operational window |
needs-human | Physical / external / accountability — not AI-writable |
Deliberately no status labels. Open issues are “to do”, closed issues are “done”. Anything in flight is implied by recent commits. We tried adding status:in-progress once; it became a thing-to-remember-to-update and got out of sync within a day.
needs-human items are decomposed
When an issue is gated on external input (fire-panel make/model, NVR access, a license purchase), the unblocked work gets its own issue separate from the gated piece. So we have:
- #94 “Confirm fire-panel make/model at Arafat site” (
needs-human, blocked on Saudi Airlines) - #95 “BACnet/IP fire-panel adapter (against bacpypes + recorded traces)” (NOT blocked — we can build the generic driver today)
- #97 “Test fire-panel adapter against actual Saudi Airlines panel” (
needs-human, blocked on physical access + #94)
The pattern matters: it stops us from saying “we can’t start fire panel work until we know the make/model” when actually 80% of the driver code is independent of the panel model.
Day-to-day usage
Pick something to work on
Open the milestone you’re on (M01 today). Skim open issues. Anything not labeled needs-human that you can start right now. Comment with what you’re starting if multiple people might pick the same one — otherwise just go.
Close an issue
When the work lands in master, close the issue manually in the Gitea UI (or via API). Add a one-comment summary if there’s anything load-bearing about how it was done that doesn’t belong in the commit message. Don’t close from a commit message — we deliberately don’t use the Closes #N auto-close magic.
Add a new issue
Two paths:
One-off: create it in the Gitea UI. Pick the right milestone, slap the right track label on it, optionally
critical-path.Bulk: edit
scripts/gitea-bootstrap-issues.py, addissue(...)entries, re-run the script. It’s idempotent (skips by title), so you can append + re-run safely. This is how the initial 140 issues landed.
The bootstrap script reads its token from ~/.gitea-token (a Personal Access Token with write:issue scope, generated under Settings → Applications in Gitea).
Mark an old completed-before-tracking item as done
When seeding work that was already finished (like the 9 closed M01 issues for things we’d done before Gitea tracking existed), include a Completed YYYY-MM-DD line in the issue body. Gitea sets closed_at to when you closed the issue, not when the work happened — the body comment is how the actual completion date survives.
Why no PR auto-linking?
For solo work (me + Claude), it’s overhead with no payoff — there’s no “ready for review” stage between commit and merge. When the team grows and we have multiple humans reviewing each other’s PRs, we’ll revisit. The trigger to add Closes #N auto-linking is: “we’re now linking issues to specific branches anyway, and forgetting to close issues by hand.”
Why no project board?
Project boards (Kanban-style To Do / In Progress / Done) earn their keep at ~30+ active issues across multiple people. With one or two contributors, the milestone view is the project board — “open in this milestone” is the to-do column, “closed in this milestone” is the done column. Adding a Project on top would just be another thing to keep in sync with reality.
Resyncing the issue list with the plan
If a plan milestone gets significantly rescoped (an issue gets dropped, a new deliverable gets added), the Gitea side is allowed to drift briefly. Catch it up by:
- Editing
scripts/gitea-bootstrap-issues.pyso theISSUESlist reflects the new reality. - Re-running the script — it creates anything new (since it’s idempotent by title).
- Closing or deleting any obsolete issues by hand in the UI.
The script is append-only-safe; it never modifies or deletes existing issues. Manual UI work is the escape hatch for that.
Related runbooks
- Maintaining the docs — the docs themselves live in this same repo.
- Cost tracker — running spend visibility, mirrors what’s in budget alerts.