feat(buglog): one Markdown record per bug, checked against a record standard

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>
This commit is contained in:
Rongsong Shen 2026-09-28 03:40:11 +00:00
parent 27b7a84980
commit d26ff8870d
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
512 changed files with 40724 additions and 17843 deletions

View file

@ -1,34 +1,34 @@
# Buglog status field
# Bug records and their status
Kept separate from `openwolf.md` because `openwolf update` overwrites that file.
OpenWolf itself ignores this field (search, pre-write hints and the hook-created
entries don't read or set it), so the convention is enforced only by this rule.
- Every `.wolf/buglog.json` entry carries a `status`, and it is the only source
of truth for whether a bug is open. Never infer it from `fix`: many entries
have a populated `fix` that says "NOT FIXED", "PENDING" or "PARTIAL".
- Each bug is a Markdown record, `.wolf/bugs/<id>.md`, written to the standard
in `.claude/skills/buglog/bug-record.md` (what Description, Environment,
Reproduction, Root cause, Fix and Verification must contain). Create and
change records with the `buglog` skill (`scripts/buglog.py`), which checks
them against that standard.
- `.wolf/buglog.json` is an index generated from the records; never edit it.
`.wolf/buglog-archive.json` holds retired OpenWolf hook auto-detections.
- The front-matter `status` is the only source of truth for whether a bug is
open. Never infer it from the Fix text or tags: migrated records often
quote "NOT FIXED" or "PENDING" from earlier stages.
- Values:
- `open`: defect confirmed and still in the code. Intermittent test
failures that do still recur count as `open`, not `cannot-reproduce`.
- `partial`: part fixed; `status_note` says what remains.
- `investigating`: real failure observed, root cause not found yet or
waiting for a reproduction.
- `fixed`: fix committed; cite the commit in `fix` or `status_note`.
- `fixed`: fix committed; `fix_commits` lists the commit(s) and the
Verification section says how the fix was proven.
- `cannot-reproduce`: reproduction was attempted and failed; record what was
tried in `repro_attempts` (e.g. "0/40 runs, 2026-08-01").
- `wontfix`: not a bug, accepted limitation, or deferred with no plan.
- `duplicate`: same defect as another entry; set `duplicate_of`.
- Whenever you log a bug or change its state, set `status` and
`status_updated` (YYYY-MM-DD), plus `status_note` unless the status is
`fixed` with the commit already in `fix`. Put them right after `"id"`.
- A `cannot-reproduce` bug that recurs: bump `occurrences`/`last_seen` and set
`status` back to `investigating`.
- Use the `buglog` skill (`scripts/buglog.py`) to add entries and change
status: `add`, `status`, `note`, `recur`, `list`, `triage`. It picks the
next free id and edits only the touched entry's lines. Don't re-serialize
the whole file with `json.dumps`/`jq`; that rewrites ~1,200 unrelated lines.
- Open-bug list:
- `duplicate`: same defect as another record; set `duplicate_of`.
- Change status only with `python3 scripts/buglog.py status <id> <status>`
(`--note` for every status except `fixed`; `--commit <sha>` for `fixed`).
It sets `status_updated` and refuses a change the record doesn't support.
- A `cannot-reproduce` bug that recurs: `buglog.py recur <id>` bumps
`occurrences`/`last_seen` and sets `status` back to `investigating`.
- Open-bug list: `python3 scripts/buglog.py list --open`, or from the index:
`jq -r '.bugs[] | select(.status | IN("open","partial","investigating")) | "\(.id) [\(.status)] \(.status_note // .error_message[0:90])"' .wolf/buglog.json`
- Entries with no `status` (hand-written without it, or created by an OpenWolf
hook) need triage:
`jq -r '.bugs[] | select(.status == null) | .id' .wolf/buglog.json`
- Records with no valid status need triage: `python3 scripts/buglog.py triage`.

View file

@ -1,7 +1,7 @@
---
name: buglog
description: Log a new bug, change a bug's status (fixed / partial / cannot-reproduce / ...), record a recurrence, or list open bugs in the OpenWolf buglog (.wolf/buglog.json). Use whenever you log or fix a bug, or are asked for the open-bug list.
argument-hint: add | status <id> <status> | note <id> | recur <id> | list [--open] | show <id> | triage
description: Log a new bug, change a bug's status (fixed / partial / cannot-reproduce / ...), record a recurrence, or list open bugs. Bugs are Markdown records in .wolf/bugs/<id>.md checked against a record standard. Use whenever you log or fix a bug, or are asked for the open-bug list.
argument-hint: new | status <id> <status> | note <id> | recur <id> | list [--open] | show <id> | check
triggers:
- /buglog
- log a bug
@ -11,12 +11,23 @@ triggers:
# Buglog Skill
Every operation goes through `scripts/buglog.py` (run from the repo root). It
edits only the lines of the entry it touches, re-parses the file before
saving, and writes atomically. **Never** rewrite `.wolf/buglog.json` with
`json.dumps`/`jq`/the Write tool: entries mix raw UTF-8 and `\uXXXX` escapes,
so a re-serialize rewrites ~1,200 unrelated lines. Never read the file whole
either (~27K lines); use `list`/`show`.
Each bug is one Markdown file, `.wolf/bugs/<id>.md`: short machine fields in
front matter, `# <id>: <summary>`, then the sections of the record standard.
**Read `bug-record.md` (next to this file) before writing a record**: it says
what every section must contain (e.g. Environment needs the OS, the libc the
failing binary loads, toolchain, build profile, commit and deployment).
- `.wolf/buglog.json` is an **index generated** from the records (for
OpenWolf's hooks, `openwolf bug search` and `jq`). Never edit it; every
`buglog.py` write regenerates it, and `python3 scripts/buglog.py index` does
it after a hand edit of a record.
- `.wolf/buglog-archive.json` holds the retired OpenWolf hook auto-detections
(710 entries, template messages like "Missing guard clause"). They are
not bug reports; leave them there.
- Records migrated from the old JSON log carry `legacy: true` and
`_Not recorded (migrated from buglog.json)._` where the old log had no such
field. They are exempt from the reporting sections, but changing one to
`fixed`/`partial` still needs the fix-time sections.
Status values and when to use each: `.claude/rules/buglog-status.md`.
@ -29,36 +40,35 @@ or `python3 scripts/buglog.py list --grep '<regex>'`.
| Task | Command |
|------|---------|
| Log a new bug | `python3 scripts/buglog.py add --error "<symptom, exact error text>" --root-cause "<cause>" --file <path>[,<path>] --tags scheduler,asan --status open --note "<one line>"` → prints the new id |
| Log a bug you already fixed | same, with `--fix "<what changed>, commit <sha>" --status fixed` |
| Mark fixed | `python3 scripts/buglog.py status <id> fixed --note "<sha>"` |
| Mark partly fixed | `python3 scripts/buglog.py status <id> partial --note "<what remains>"` |
| Log a new bug | `python3 scripts/buglog.py new --summary "<symptom in component, ≤100 chars>" --component <c> --severity <s> [--binary <failing binary>] [--files a,b] [--tags x,y] [--related bug-N]` → prints the file path. Environment is filled in from this machine (pass `--binary` so libc and build profile come from that binary). Then edit the file: replace every `TODO` in Description, Environment (`Deployment`) and Reproduction, and run `check <id>`. |
| Environment block only | `python3 scripts/buglog.py env [<binary>]` |
| Check records | `python3 scripts/buglog.py check <id> …` / `check --all` |
| Mark fixed | Fill in Root cause, Fix and Verification (`- Test:` RED before/GREEN after, `- Suites:`, `- Live:`), then `python3 scripts/buglog.py status <id> fixed --commit <sha> [--commit <sha2>]`. Refused, and nothing written, until the record meets the standard; each `--commit` must be a real commit. |
| Mark partly fixed | `python3 scripts/buglog.py status <id> partial --note "<what remains>"` (Root cause + Fix required) |
| Couldn't reproduce | `python3 scripts/buglog.py status <id> cannot-reproduce --repro "0/40 runs, 2026-09-24" --note "<what was tried>"` |
| Real failure, cause unknown | `python3 scripts/buglog.py status <id> investigating --note "<next step>"` |
| Not a bug / accepted | `python3 scripts/buglog.py status <id> wontfix --note "<why / who decided>"` |
| Duplicate | `python3 scripts/buglog.py status <id> duplicate --duplicate-of <other-id> --note "<same defect as ...>"` |
| Add findings to an entry | `python3 scripts/buglog.py note <id> "<text>"` (appends an `UPDATE (date):` paragraph to `fix`) |
| Add findings | `python3 scripts/buglog.py note <id> "<text>" [--section "Root cause"]` (appends `UPDATE (date): …`; default section Description) |
| Seen again | `python3 scripts/buglog.py recur <id> [--note "<where>"]` (bumps occurrences/last_seen; reopens a `fixed`/`wontfix`/`cannot-reproduce` bug) |
| Open-bug list | `python3 scripts/buglog.py list --open` (add `--tag scheduler`, `--grep`, `--since YYYY-MM-DD`) |
| One entry | `python3 scripts/buglog.py show <id>` |
| Entries missing a status | `python3 scripts/buglog.py triage` |
| Any other single-line field | `python3 scripts/buglog.py set <id> key=<json> ...` |
| One record | `python3 scripts/buglog.py show <id>` |
| Front-matter field | `python3 scripts/buglog.py set <id> key=<json> ...` |
`status` requires `--note` for every status except `fixed`, and always
rewrites the note, so a stale note never outlives its status.
Components: controller, scheduler, worker, lib, harness, apiserver, xcecli,
xceadmin, autoscaler, build, config, tooling. Severities: data-loss, crash,
hang, wrong-result, leak, security, noise.
## Rules
- **Ids**: let `add` pick the id (one past the highest existing). Don't number
entries by hand or by entry count. OpenWolf's post-write hook numbers by
entry count, which collides with existing ids; its auto-detection is turned
off in `.wolf/config.json` for that reason (`openwolf.buglog.auto_detect`).
- **Write the symptom first**: `--error` holds the observed error text or
behavior, not a status banner ("OPEN, NOT FIXED --" goes in `status`).
- **Fixing a bug in code**: after the fix commit, run `status <id> fixed
--note <sha>`. If only part is fixed, use `partial` with what remains.
- **Arrays** (`tags`, `related_bugs`, `files`) can only be set at `add` time;
to change them later, edit that entry's lines by hand and re-check with
`python3 -c "import json; json.load(open('.wolf/buglog.json'))"`.
- **Gitea**: status changes reach Gitea on the next `/gitea-sync`, which opens,
closes or reopens issues from `status`.
- **Ids**: let `new` pick the id (one past the highest in records + archive).
- **Editing sections**: edit the Markdown file directly (Edit tool), keeping
the `## <Section>` headings; then `check <id>` and `index`. Status changes
go through `status`, never a hand edit of `status:`.
- **Enforcement**: the pre-commit hook (`scripts/git-hooks/pre-commit`; install
once per clone with `ln -sf ../../scripts/git-hooks/pre-commit
.git/hooks/pre-commit`) runs `check --staged`: it refuses staged records
that miss the standard, a legacy record hand-changed to fixed/partial without
the fix-time sections, and a stale `buglog.json`.
- **Gitea**: `/gitea-sync` mirrors status, and refreshes an issue's title and
body whenever the record's text changes; fix commits are linked.

View file

@ -0,0 +1,318 @@
# Bug Record Standard
Each bug is one file, `.wolf/bugs/<id>.md`. `scripts/buglog.py` creates
records, changes their status and checks them against this standard; the
pre-commit hook refuses a commit whose staged records do not meet it. See
[Storage and enforcement](#storage-and-enforcement).
Every bug record is read by two audiences: a person deciding what to work on,
and an agent that has to reproduce, fix or verify the bug with no other
context. So a record uses **few fields**, and each field is plain Markdown
text that must satisfy the rules in its section below. The rules say what the
text must contain, how to collect it, and what does not count.
Write every field so that someone who has never seen the bug, on a different
day and a different machine, could act on it.
## Fields at a glance
| # | Field | When required | One-line rule |
|---|-------|---------------|---------------|
| 1 | **Summary** | always | The `# <id>: <summary>` title: one line, ≤ 100 characters, symptom + component. |
| 2 | **Description** | always | What happened vs. what should happen, with the exact error text and the impact. |
| 3 | **Environment** | always | OS, libc, toolchain, build profile, deployment, and the exact code version. |
| 4 | **Reproduction** | always | Steps someone else can run, how often it fails, and where the evidence is kept. |
| 5 | **Root cause** | status `fixed` or `partial` | The mechanism, in code terms, that produces the symptom. |
| 6 | **Fix** | status `fixed` or `partial` | What was changed and why this change removes the cause. |
| 7 | **Verification** | status `fixed` | Proof the fix works, and that the same test failed before the fix. |
| 8 | **References** | status `fixed` | Fix commits, related bugs, specs or docs. |
Machine fields stay short and live in the front matter, outside these
sections: `id`, `status`, `status_updated`, `status_note`, `component`,
`severity`, `timestamp`, `fix_commits`, and a few more. Their values are defined in `.claude/rules/buglog-status.md`
and in [Machine fields](#machine-fields).
---
## 1. Summary
**Must include**
- The observable symptom, not the suspected cause.
- The component or binary where it shows (`xce_controller`, `xcecd_virtual`,
`xce_scheduler --sim`, `gitea-sync`, …).
**Rules**
- Written as the record's title, `# <id>: <summary>`; the summary itself is
one line, ≤ 100 characters, no trailing period. It becomes the Gitea issue title.
- No status words (`OPEN`, `FIXED`, `NOT FIXED`, `PRIORITY: LOW`). Status lives in `status`.
**Not acceptable**: `Missing guard clause` · `Incorrect value in code` ·
`crash` · `bug in ha.c`.
**Good**: `xcecd_virtual leaks worker strings on every finished job (LeakSanitizer at exit)`
## 2. Description
**Must include**
- **Observed**: what happened, with the exact error text, log line, assertion
or sanitizer headline in a code block, copied, not paraphrased.
- **Expected**: what should have happened instead.
- **Impact**: who or what is affected and how badly: crash, hang, data loss,
wrong result, leak (and its growth rate), noise only.
- **How it was found**: user report, HA soak, ASAN run, audit, code review,
unit test, or a spec task (name it).
**Include when relevant**
- The first bad state you can see, when the visible symptom comes much later.
- Which other bugs showed up in the same run.
**Rules**
- Keep it to the facts known at the time you log the bug. Later findings go
in Root cause, or in a dated `UPDATE (YYYY-MM-DD):` line at the end of this
section. Never rewrite history silently.
- No guesses stated as facts. Label a suspicion as `Suspected:`.
## 3. Environment
The goal: another person can tell whether their machine and build are
affected, and can recreate the same conditions.
**Must include**
- **OS**: distribution and version, and the kernel.
- Kernel: `uname -srm`.
- Distribution: `/etc/os-release` `PRETTY_NAME`. If that file is missing,
say so and give what identifies the system instead. For example, this dev
host has no `/etc/os-release`. It is a Docker container (`/.dockerenv`
exists) on a NixOS kernel (`/proc/version` shows `#1-NixOS`).
- Container or VM, when it applies.
- **libc**: name and version of the C library **the failing binary actually
loads**, not the host's.
- Get it from the binary:
`readelf -l <binary> | grep 'program interpreter'` → `/nix/store/…-glibc-2.42-47/lib/ld-linux-x86-64.so.2`.
- Why it matters: on this host `ldd --version` outside the Nix shell reports
glibc **2.40**, but every XCE binary built in the dev shell runs on glibc
**2.42**. The host value is the wrong answer.
- **Toolchain**: compiler and version (`gcc --version`) plus Meson, and Go or
Python for those components, taken from the same shell that built the binary.
- **Build**: profile and directory: `asan` (`xce.build`), `debug`
(`build.dbg`) or `release` (`build.release`); and the sanitizer options in
force when the failure was seen (e.g. `ASAN_OPTIONS=detect_leaks=0`).
- **Code version**: the commit the failing binary was built from, and the
branch (`git rev-parse --short HEAD` at build time; add `-dirty` if the tree
had uncommitted changes). For a bug seen on a deployed cluster, the version
of **each** component involved (controller, scheduler, worker, apiserver,
xceadmin) when they differ.
- **Deployment**: topology (single controller, 3-node HA, multihost), worker
kind (`xcecd_virtual` or real root `xcecd`), and host names when more than
one machine is involved.
**Include when relevant**
- Config set (`src/configs` or a fixture directory) and any non-default values.
- CPU count and memory, for races, timeouts and load-dependent bugs.
- systemd and cgroup version, for worker process or accounting bugs.
- Anything else that ran on the machine at the same time (parallel builds,
other soaks), for timing-sensitive failures.
**Not acceptable**: `Linux` · `local` · `latest code` · `ASAN build` with no
commit · the host's `ldd --version` for a Nix-built binary.
**Collect it with** (Nix-built binaries):
```bash
uname -srm; cat /etc/os-release 2>/dev/null | grep PRETTY_NAME || echo "no /etc/os-release"
[ -f /.dockerenv ] && echo "docker container"
readelf -l <binary> | grep -o '/nix/store/[^]]*ld-linux[^]]*' # libc actually loaded
nix develop ./src --command bash -c 'gcc --version | head -1; meson --version'
git rev-parse --short HEAD; git diff --quiet || echo dirty
```
## 4. Reproduction
**Must include**
- **Steps**: numbered commands someone else can paste, starting from a clean
checkout at the commit in Environment. Name the test binary, soak mode,
replay file or script and its arguments.
- **Frequency**: `deterministic`, or measured as `N/M runs` with the date
(e.g. `2/4 force-snapshot runs, 2026-09-26`). Never `sometimes`.
- **Evidence**: where the logs, cores, replay files or captures are kept. Use
a path that survives the session: copy them out of `/tmp` and out of
per-session scratch directories first. If they could not be kept, say so.
**Include when relevant**
- A smaller reproduction (unit test, single command) once you have one. Keep
the original steps too.
- Conditions that stop it reproducing (e.g. passes with `--jobs 1`).
**When it does not reproduce**: record what was tried, how many runs, on
which commit and date; this is what `status cannot-reproduce --repro` stores.
## 5. Root cause *(fixed or partial)*
**Must include**
- The mechanism in code terms: the function(s) and file(s), the state or
ordering that goes wrong, and why that produces the symptom in Description.
- Since when: the commit or change that introduced it when known
(`git log -S`, `git bisect`), otherwise `pre-existing, origin unknown`.
- Scope: other call sites or components with the same defect, checked or
not checked.
**Rules**
- Describe the cause, not the fix. `Added a NULL check` is a fix, not a cause.
- If part of the explanation is still unproven, mark that part `Unconfirmed:`.
## 6. Fix *(fixed or partial)*
**Must include**
- What changed, per file or function, in a few lines.
- Why this change removes the cause in Root cause.
- For `partial`: what is left and where it is tracked.
**Include when relevant**
- Alternatives that were rejected, and why.
- Behaviour changes, compatibility or migration notes (wire format, WAL,
snapshot, config).
**Rules**
- A proposal that is not implemented yet is labelled `Proposed:` and needs
status `open` or `investigating`, never `fixed`.
## 7. Verification *(fixed)*
**Must include**
- **Test added or changed**: the test name and file, and a statement that it
**failed before the fix** (RED) and passes after (GREEN). If no automated
test is possible, say why and describe the manual check.
- **Suites run**: which suites, on which build profile, with results
(e.g. `worker suite 12/12, asan`).
- **Live check** for HA, persistence, scheduler or worker-process bugs: the
soak or deploy run used, with its mode, rounds and result.
**Not acceptable**: `tests pass` · `verified` · `should be fixed now`.
## 8. References *(fixed)*
**Must include**
- Fix commit(s), short SHA, on a pushed branch. They live in the front-matter
field `fix_commits`, set by `buglog.py status <id> fixed --commit <sha>`,
which refuses a hash that is not a commit in this repository.
**Include when relevant**
- Related bugs, with the relationship: `regression of`, `split from`,
`split into`, `same root cause as`, `duplicate of`.
- Spec, steering or design documents that changed or explain the design.
- The Gitea issue number (filled in by `gitea-sync`).
---
## Machine fields
Front matter: one `key: <JSON value>` per line (valid YAML), between `---` lines.
| Field | Values |
|-------|--------|
| `id` | `bug-<n>`; must match the file name. `buglog.py new` assigns it. |
| `status` | see `.claude/rules/buglog-status.md`; change it with `buglog.py status` |
| `status_updated`, `status_note` | set by `buglog.py status`; a note is required for every status except `fixed` |
| `component` | `controller` · `scheduler` · `worker` · `lib` · `harness` · `apiserver` · `xcecli` · `xceadmin` · `autoscaler` · `build` · `config` · `tooling` |
| `severity` | `data-loss` · `crash` · `hang` · `wrong-result` · `leak` · `security` · `noise` |
| `timestamp` | when the bug was first seen, ISO-8601 UTC, real time (no placeholder `T00:00:00Z`) |
| `last_seen`, `occurrences` | updated by `buglog.py recur` |
| `files`, `tags`, `related` | JSON lists; old status words are not tags (`fixed`, `partially-fixed`, `closed-by-…`) |
| `fix_commits` | short SHAs, required for `fixed` |
| `duplicate_of`, `repro_attempts` | required for `duplicate` / `cannot-reproduce` |
| `legacy` | `true` on records migrated from the old JSON log |
| `gitea_issue`, `gitea_synced_status`, `gitea_synced_hash` | written by `gitea-sync.py` only |
A value of `TODO…` counts as empty: `buglog.py new` writes the template with
`TODO` markers, and `check` lists each one still unfilled.
## Template
`buglog.py new` writes this, with Environment already filled in:
````markdown
---
id: "bug-1872"
status: "open"
status_updated: "2026-09-27"
status_note: "newly logged"
component: "scheduler"
severity: "hang"
timestamp: "2026-09-27T20:05:00Z"
files: []
tags: []
related: []
---
# bug-1872: <symptom in component, ≤ 100 chars>
## Description
**Observed:**
```
<exact error / log / sanitizer text>
```
**Expected:** <…>
**Impact:** <…>
**Found by:** <user report | soak | ASAN | audit | review | test | spec task …>
## Environment
- OS: <distribution + version | "no /etc/os-release" + what it is>; kernel <uname -srm>; <container/VM>
- libc: <name version, from the binary's interpreter>
- Toolchain: gcc <…>, meson <…>
- Build: <asan|debug|release> (<dir>), <sanitizer options>
- Code: <branch> @ <short sha>[-dirty]
- Deployment: <topology>, <worker kind>, <hosts>
## Reproduction
1. <command>
2. <command>
- Frequency: <deterministic | N/M runs, date>
- Evidence: <durable path | "not kept">
## Root cause
<mechanism; since when; scope>
## Fix
<what changed and why it removes the cause>
## Verification
- Test: <name> in <file>: RED before, GREEN after
- Suites: <suite results, profile>
- Live: <soak mode, rounds, result | n/a because …>
## References
- Related: <bug-id (relationship)>, …
- Docs: <path>, …
````
## Example
`.wolf/bugs/bug-1866.md` is a complete record written to this standard
(a worker leak: exact sanitizer text, Environment from `buglog.py env`, a
reproduction on the discovery commit, RED/GREEN verification, fix commit).
## Storage and enforcement
- **Records**: `.wolf/bugs/<id>.md`, one per bug. People and agents read and
edit them directly; keep the `## <Section>` headings.
- **Index**: `.wolf/buglog.json` is generated from the records (for OpenWolf's
hooks, `openwolf bug search` and `jq`). Never edit it; `buglog.py` rewrites
it on every write, and `buglog.py index` after a hand edit.
- **Archive**: `.wolf/buglog-archive.json` keeps the 710 retired OpenWolf hook
auto-detections (template messages such as "Missing guard clause", ids
between bug-002 and bug-1672) verbatim. They are not bug reports and are
not in the index.
- **Migrated records** (`legacy: true`, 2026-09-27) show
`_Not recorded (migrated from buglog.json)._` where the old log had no such
field. They are exempt from the reporting sections; changing one to
`fixed` or `partial` still needs the fix-time sections.
- **Enforcement**:
- `buglog.py status` refuses a status whose requirements the record does
not meet, and writes nothing.
- `buglog.py check <id>|--all` lists every unmet rule.
- The pre-commit hook (`scripts/git-hooks/pre-commit`, installed per clone
with `ln -sf ../../scripts/git-hooks/pre-commit .git/hooks/pre-commit`)
runs `check --staged`: it refuses staged records that miss the standard,
a legacy record hand-changed to fixed/partial without the fix-time
sections, and a stale index.

View file

@ -1,6 +1,6 @@
---
name: gitea-sync
description: Sync cc-sdd specs and OpenWolf buglog to Gitea milestones/issues in nexa/xce
description: Sync cc-sdd specs and bug records (.wolf/bugs/*.md) to Gitea milestones/issues in nexa/xce
argument-hint: [--dry-run] [--spec <name>] [--bugs] [--since <date>] [--ids <ids>]
triggers:
- /gitea-sync
@ -11,7 +11,7 @@ triggers:
# Gitea Sync Skill
Sync cc-sdd specs (`.kiro/specs/`) and OpenWolf buglog (`.wolf/buglog.json`) to
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.
@ -54,7 +54,7 @@ Keep the script and this procedure in step when changing either.
with `--since`). Needed because bulk triage has bumped `last_seen` /
`status_updated` on most of the buglog, and for a never-synced bug
`--since` only checks `last_seen` — a recently fixed older bug is otherwise
unreachable. Always `--dry-run` first: `--since` alone can match ~1100 bugs.
unreachable. Always `--dry-run` first: `--since` alone can match hundreds of bugs.
### 1. Ensure labels exist
@ -149,9 +149,12 @@ For each spec directory in `.kiro/specs/` (or just `--spec <name>`):
### 3. Sync Bugs
Read `.wolf/buglog.json`. Each entry's `status` field is the source of truth
for open/closed — **never infer it from `fix`**: many entries have a populated
`fix` that says "NOT FIXED", "PENDING" or "PARTIALLY FIXED".
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`):
@ -165,114 +168,93 @@ for open/closed — **never infer it from `fix`**: many entries have a populated
| `cannot-reproduce` | closed | `bug:cannot-reproduce` |
| `wontfix` | closed | `bug:wontfix` |
| `duplicate` | closed | `bug:duplicate` |
| missing (hook-created entry) | open | — ; report it in the summary as "needs status" |
| 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 entries can point at one `gitea_issue` (e.g. a
**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" entry, in this order: `open`,
an issue follows its "most open" record, in this order: `open`,
`investigating`, `partial`, (no status), `cannot-reproduce`, `duplicate`,
`wontfix`, `fixed`.
For each entry (after the `--since` filter):
**Title**: `[<bug-id>] <summary>` — the record's `#` title (max 120 chars).
1. **Skip** if it has `gitea_issue` AND `gitea_synced_status == status`
(already mirrored, nothing changed).
**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)
2. **Create issue** for entries without `gitea_issue`:
Use `mcp__gitea__issue_write` method `create`.
## <each record section that has content: Description, Environment,
Reproduction, Root cause, Fix, Verification, References, Legacy notes>
**Title**: `[<bug-id>] <first sentence of error_message>` (max 120 chars),
after dropping any leading status banner such as "OPEN, NOT FIXED --" or
"PRIORITY: LOW --" (status lives in the state and labels).
## Affected Files
- `<file>` for each of `files`
**Body**:
```
**Status:** <status> (<status_note>, if present)
<"Duplicate of <duplicate_of>" / "Repro attempts: <repro_attempts>", if present>
## Tags
`tag1` `tag2` ...
## Error
<error_message>
---
*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`).
## Root Cause
<root_cause>
**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`.
## Fix
<fix> (if `fix` is an array, join the elements with blank lines;
"*Not yet fixed*" if empty)
For each record (after the `--since`/`--ids` filter):
## Affected Files
- <file> (split on commas), or each element of `files`
1. **Skip** if it has `gitea_issue`, `gitea_synced_status == status` and
`gitea_synced_hash` equals the hash of the title + body it would write now.
## Tags
`tag1` `tag2` ...
2. **Create issue** for records without `gitea_issue`, then close it if the
state is closed (`state: "closed"` may be ignored on create). No milestone.
## Related Bugs
- <related>
3. **Update changed issues**:
- text changed (hash differs): `update` the 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: `update` the state and swap the status label (aggregate
status for shared issues), and post a one-line comment with the new
status, `status_note` and fix commits. This also **reopens** bugs whose
status went back to `open`/`investigating`.
---
*Synced from `.wolf/buglog.json`*
```
**Labels**: `["bug"]` + the status label from the table above. Map known
tags to labels: `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`.
**State**: from the table above. `state: "closed"` may be ignored on
`create` — create as open, then `update` to closed if needed.
**Milestone**: None (bugs span specs).
3. **Update changed issues**: for entries WITH `gitea_issue` whose `status`
differs from `gitea_synced_status` (or that have no `gitea_synced_status`
yet), `update` the issue's state and swap its status label per the table
(using the aggregate status for shared issues).
This both closes newly fixed bugs and **reopens** bugs whose status went
back to `open`/`investigating` (e.g. a `cannot-reproduce` bug that recurred).
Post a one-line comment with the new status and `status_note`.
**One-time repair**: issues synced before the `status` field existed were
closed whenever `fix` was non-empty, so entries with `gitea_issue` and no
`gitea_synced_status` whose status is `open`/`partial`/`investigating` are
probably wrongly closed — step 3 reopens them.
4. **Write sync state back** to the buglog entry immediately after each
issue is created/updated (not once at the end — a failure part-way through
must not lose an issue number, or the next run creates a duplicate issue), with the surgical helper —
never re-serialize the whole file (`json.dumps`/`jq` rewrite ~1,200
unrelated lines because entries mix raw UTF-8 and `\uXXXX` escapes):
4. **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):
```bash
python3 scripts/buglog.py set <bug-id> \
gitea_issue=<issue_number> 'gitea_synced_status="<status>"'
python3 scripts/buglog.py set <bug-id> gitea_issue=<n> \
'gitea_synced_status="<status>"' 'gitea_synced_hash="<hash>"'
```
Run it from the repo root. It edits only that entry's lines and refuses to
write anything that no longer parses. For entries without a `status`,
record `gitea_synced_status="unknown"`.
For records without a valid `status`, record `gitea_synced_status="unknown"`.
### 4. Report
Print summary: "Specs: N synced, M skipped | Bugs: X created, Y closed, R reopened, Z skipped"
and list any entries with no `status` (hook-created entries) so they can be triaged.
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_issue` or `gitea` state are skipped
unless their phase changed (specs) or `status != gitea_synced_status` (bugs).
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_read` first.
- **Rate limiting**: Batch issue creation — create issues sequentially
(a few per turn) to stay within API limits.
- **Buglog is large** (~1,200 entries, ~27K lines). Always use `--since` filter
for practical runs (e.g., `--since 2026-07-18`). Without `--since`, sync ALL
bugs. Filter with `jq`, don't read the file whole.
- **~400 records have never been synced** (older bugs). A run without
`--since`/`--ids` creates an issue for every one of them; use the filters
unless that is the intent, and always `--dry-run` first.
- **Private repo**: `nexa/xce` is private. MCP auth must have access.
## Gitea MCP Tool Reference

View file

@ -269,11 +269,13 @@ several minutes per mode.
## Step 5 — Record and report
- Log every genuinely new root-caused bug with the `buglog` skill:
`python3 scripts/buglog.py add --error ... --root-cause ... --file ...
--tags <component>,... --fix "<what changed>" --status fixed` (or `open` /
`partial` with `--note` for what remains). It assigns the id; don't pick one
by hand. For a violation that an existing entry already covers, update that
entry instead: `scripts/buglog.py status <id> ...` / `note <id> ...`.
`python3 scripts/buglog.py new --summary "<symptom in component>"
--component <component> --severity <s> --binary <failing binary>`, fill in
the record per `.claude/skills/buglog/bug-record.md`, and once the fix is
committed `scripts/buglog.py status <id> fixed --commit <sha>` (or
`partial --note "<what remains>"`). It assigns the id; don't pick one by
hand. For a violation that an existing record already covers, update that
record instead: `scripts/buglog.py status <id> ...` / `note <id> ...`.
- Append durable, non-obvious learnings to `.wolf/cerebrum.md`'s Key
Learnings section (anchor the edit near an existing dated entry to avoid
the "## Key Learnings" heading collision — the file has one per dated