The buglog was one 27K-line JSON file whose fields covered only symptom,
root cause and fix, as free text: 2% of entries named the code version a
bug was found on, 12% the build profile, 36% a way to reproduce, and 58% of
the file was OpenWolf hook auto-detections ("Missing guard clause",
"Function not marked async" on .md files).
- .claude/skills/buglog/bug-record.md: the record standard. Eight
sections (Summary as the title, Description, Environment, Reproduction;
Root cause, Fix, Verification, References once fixed), each with the
content it must contain -- e.g. Environment needs the OS, the libc the
failing binary loads (from its ELF interpreter: glibc 2.42 in the nix
shell vs 2.40 on the host), toolchain, build profile and commit.
- .wolf/bugs/<id>.md: 500 records migrated from buglog.json; every field
verified to round-trip. They carry legacy: true and an explicit "not
recorded" where the old log had no such field; component derived from
file paths, severity from keywords (else unknown), fix_commits from
hashes in status_note/fix that git resolves (91 of 463 fixed).
- .wolf/buglog-archive.json: the 710 hook auto-detections, verbatim.
- .wolf/buglog.json is now an index generated from the records, keeping
the fields OpenWolf's hooks and `openwolf bug search` read.
- scripts/buglog.py rewritten: new (template + Environment collected from
the machine and --binary), check, status (refuses a status the record
doesn't support; fixed needs Root cause, Fix, Verification and
--commit hashes git resolves), note, recur, set, show, list, triage,
env, index. 29 unittest cases in scripts/tests/test_buglog.py.
- scripts/git-hooks/pre-commit runs `check --staged`: incomplete staged
records, a legacy record hand-changed to fixed without the fix-time
sections, a new record claiming legacy: true, or a stale index refuse
the commit.
- scripts/gitea-sync.py reads the records: issue body = the record's
sections plus linked fix commits (origin remote, GITEA_COMMIT_REPO to
override), and a body hash so an issue's text is refreshed when the
record changes.
- bug-1866 rewritten to the full standard as the worked example.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
12 KiB
name: gitea-sync description: Sync cc-sdd specs and bug records (.wolf/bugs/*.md) to Gitea milestones/issues in nexa/xce argument-hint: [--dry-run] [--spec ] [--bugs] [--since ] [--ids ] triggers:
- /gitea-sync
- sync to gitea
- gitea sync
- synchronize specs to gitea
Gitea Sync Skill
Sync cc-sdd specs (.kiro/specs/) and bug records (.wolf/bugs/*.md) to
Gitea issues in nexa/xce. Files are ground truth — Gitea is a read-only mirror.
Sync state (issue numbers) is stored in source files for idempotency.
Invocation
/gitea-sync # sync all specs + all bugs
/gitea-sync --spec admin-config-tool # sync one spec
/gitea-sync --bugs --since 2026-07-18 # sync recent bugs only
/gitea-sync --dry-run # preview without making changes
Preferred: run the script
scripts/gitea-sync.py implements every step below over the Gitea REST API
and needs no MCP server (token from GITEA_TOKEN, or the gitea MCP entry in
~/.claude.json). Use it whenever it can reach Gitea; follow the manual MCP
procedure only when it can't:
python3 scripts/gitea-sync.py --dry-run --bugs --since 2026-09-01 # preview
python3 scripts/gitea-sync.py --bugs --since 2026-09-01
python3 scripts/gitea-sync.py --spec <name>
Keep the script and this procedure in step when changing either.
Procedure
0. Parse arguments
--dry-run: only report what would change, don't call MCP write tools--spec <name>: sync only one spec directory--bugs: sync bugs only (skip specs)--since <YYYY-MM-DD>: create issues only for bugs withlast_seen >=this date; for already-synced bugs (gitea_issueset), also mirror those whosestatus_updated >=this date, so a status change on an old bug still reaches Gitea without flooding it with old never-synced bugs--ids <bug-N,bug-M,...>: also sync these bugs regardless of dates (OR'd with--since). Needed because bulk triage has bumpedlast_seen/status_updatedon most of the buglog, and for a never-synced bug--sinceonly checkslast_seen— a recently fixed older bug is otherwise unreachable. Always--dry-runfirst:--sincealone can match hundreds of bugs.
1. Ensure labels exist
These labels must exist in nexa/xce. Use mcp__gitea__label_read to list
existing labels, then mcp__gitea__label_write with method create_repo_label
for any missing ones:
| Name | Color | Description |
|---|---|---|
phase:requirements |
6C5CE7 |
Spec requirements phase |
phase:design |
6C5CE7 |
Spec design phase |
phase:tasks |
6C5CE7 |
Spec tasks phase |
phase:implementation |
F39C12 |
Spec implementation phase |
phase:complete |
27AE60 |
Spec complete |
spec |
3498DB |
Spec-tracked issue |
bug |
E74C3C |
Bug-tracked issue |
bug:fixed |
27AE60 |
Bug resolved |
bug:partial |
F39C12 |
Partly fixed, remainder open |
bug:investigating |
E67E22 |
Real failure, root cause not found yet |
bug:cannot-reproduce |
95A5A6 |
Repro attempted and failed |
bug:wontfix |
7F8C8D |
Not a bug / accepted limitation |
bug:duplicate |
BDC3C7 |
Duplicate of another bug |
bug:asan |
8E44AD |
Sanitizer finding |
bug:crash |
C0392B |
Crash (SEGV, double-free, UAF) |
bug:race |
D35400 |
Race / TOCTOU |
bug:ha |
2980B9 |
HA / Raft / failover |
bug:data-loss |
922B21 |
Data loss |
blocked |
E74C3C |
Blocked on dependency |
2. Sync Specs
For each spec directory in .kiro/specs/ (or just --spec <name>):
-
Read
spec.json. If it hasgitea.milestone_idandgitea.issueswith all three keys (requirements_design,implementation,validation), skip creation — the spec is already synced. Check phase states and update issue open/closed state if needed (step 2d). -
If not synced: a. Create milestone: Use
mcp__gitea__milestone_writemethodcreate. Title = spec name. Description = one-line summary + phase + blockers. b. Create three phase issues under that milestone viamcp__gitea__issue_writemethodcreate:Key Title Labels Initial State requirements_design[{spec}] Requirements & Designspec, phase:requirements, phase:design closed if both approved, else open implementation[{spec}] Implementationspec, phase:implementation open validation[{spec}] Validation & Closurespec, phase:complete open Body format for each issue (build from spec files):
Requirements & Design body: Phase status, requirements summary table (extract
## Requirements Summaryfromrequirements.md), blockers list, link to spec files.Implementation body: Tasks generated/ready status, task checklist (extract
- [ ]/- [x]fromtasks.mdif it exists), blockers list.Validation body: Acceptance gate checklist, deferred future-spec items.
c. Write sync state back to
spec.json:"gitea": { "milestone_id": <id>, "issues": { "requirements_design": <number>, "implementation": <number>, "validation": <number> } }d. Phase state updates (for already-synced specs).
spec.jsonapprovalsonly covers requirements/design/tasks — there is no implementation approval — so usephasefor the later issues:"Done" phases:
implementation-complete,implemented,superseded(no tool distinguishes the first two).Condition Action approvals.requirements.approvedANDapprovals.design.approved, or phase is doneclose requirements_designphase is done close implementation,validationand the milestonephase is not done reopen implementation/validation/milestone if closedThe same rules give the initial state when the issues are first created.
Close issues via
mcp__gitea__issue_writemethodupdatewithstate: "closed", the milestone viamcp__gitea__milestone_writemethodupdate.
3. Sync Bugs
Read the bug records, .wolf/bugs/<id>.md (format and rules:
.claude/skills/buglog/bug-record.md). Each record's front-matter status is
the source of truth for open/closed — never infer it from the Fix text or
tags: migrated records often quote "NOT FIXED" or "PENDING" from earlier
stages. .wolf/buglog-archive.json (retired hook auto-detections) is never
synced.
Status → Gitea state / label (status values are defined in
.claude/rules/buglog-status.md):
status |
Gitea state | Status label |
|---|---|---|
open |
open | — |
partial |
open | bug:partial |
investigating |
open | bug:investigating |
fixed |
closed | bug:fixed |
cannot-reproduce |
closed | bug:cannot-reproduce |
wontfix |
closed | bug:wontfix |
duplicate |
closed | bug:duplicate |
| missing / unknown | open | — ; report it in the summary as "needs status" |
An issue carries exactly one status label. When changing it, remove only the
other bug:<status> labels and add the new one; never replace the issue's
whole label set, which would wipe labels a person added.
Shared issues: several records can point at one gitea_issue (e.g. a
user-reported issue whose cause was split into several bugs, like #46). Such
an issue follows its "most open" record, in this order: open,
investigating, partial, (no status), cannot-reproduce, duplicate,
wontfix, fixed.
Title: [<bug-id>] <summary> — the record's # title (max 120 chars).
Body:
**Status:** <status> — <status_note, if any>
**Component:** <component> · **Severity:** <severity>
**Fix commits:** [`<sha>`](<GITEA_URL>/<commit repo>/commit/<sha>), … (if any)
**Duplicate of:** / **Repro attempts:** (if set)
## <each record section that has content: Description, Environment,
Reproduction, Root cause, Fix, Verification, References, Legacy notes>
## Affected Files
- `<file>` for each of `files`
## Tags
`tag1` `tag2` ...
---
*Synced from `.wolf/bugs/<id>.md`*
Commit links point at GITEA_COMMIT_REPO, default the origin remote's
owner/repo (fixes land on a fork branch before they reach nexa/xce).
Labels: ["bug"] + the status label, bug:crash/bug:data-loss from
severity, and known tags: asan/ubsan → bug:asan,
SEGV/double-free/UAF → bug:crash, race/toctou → bug:race,
ha/raft/election/failover → bug:ha, data-loss → bug:data-loss.
For each record (after the --since/--ids filter):
-
Skip if it has
gitea_issue,gitea_synced_status == statusandgitea_synced_hashequals the hash of the title + body it would write now. -
Create issue for records without
gitea_issue, then close it if the state is closed (state: "closed"may be ignored on create). No milestone. -
Update changed issues:
- text changed (hash differs):
updatethe issue's title and body, so an issue created while the bug was open gets its Root cause, Fix, Verification and fix commits once it is fixed. - status changed:
updatethe state and swap the status label (aggregate status for shared issues), and post a one-line comment with the new status,status_noteand fix commits. This also reopens bugs whose status went back toopen/investigating.
- text changed (hash differs):
-
Write sync state back to the record's front matter immediately after each issue is created/updated (a failure part-way through must not lose an issue number, or the next run creates a duplicate issue):
python3 scripts/buglog.py set <bug-id> gitea_issue=<n> \ 'gitea_synced_status="<status>"' 'gitea_synced_hash="<hash>"'For records without a valid
status, recordgitea_synced_status="unknown".
4. Report
Print summary: "Specs: N synced, M skipped | Bugs: X created, Y closed, R reopened, F refreshed, Z skipped"
and list any records with no valid status so they can be triaged.
Important Rules
- Files are ground truth. Never read Gitea state to update local files. The gitea_issue/milestone_id fields in local files only store sync mappings.
- Idempotent. Items with existing
gitea_issueorgiteastate are skipped unless their phase changed (specs) or their status or text changed (bugs). - Labels: Check existing labels before creating. Use label IDs (not names)
in issue create calls — fetch with
mcp__gitea__label_readfirst. - Rate limiting: Batch issue creation — create issues sequentially (a few per turn) to stay within API limits.
- ~400 records have never been synced (older bugs). A run without
--since/--idscreates an issue for every one of them; use the filters unless that is the intent, and always--dry-runfirst. - Private repo:
nexa/xceis private. MCP auth must have access.
Gitea MCP Tool Reference
| Action | Tool | Method |
|---|---|---|
| List labels | mcp__gitea__label_read |
list_repo_labels |
| Create label | mcp__gitea__label_write |
create_repo_label |
| List milestones | mcp__gitea__milestone_read |
list |
| Create milestone | mcp__gitea__milestone_write |
create |
| List issues | mcp__gitea__issue_read |
get (single) or use list |
| Create issue | mcp__gitea__issue_write |
create |
| Update issue | mcp__gitea__issue_write |
update |
For listing issues by milestone, use mcp__gitea__issue_read with method: "get"
and state: "all". The Gitea MCP may have limited filtering — for bulk listing,
check the milestone's open_issues/closed_issues counters.
Note: issue state: "closed" may not work on create — if the issue is created
as "open", immediately update it with method: "update" + state: "closed".