myxce/.claude/skills/buglog/bug-record.md
Rongsong Shen d26ff8870d
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>
2026-09-28 03:40:11 +00:00

14 KiB

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.

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.


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):

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:

---
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.