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:
parent
27b7a84980
commit
d26ff8870d
512 changed files with 40724 additions and 17843 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
318
.claude/skills/buglog/bug-record.md
Normal file
318
.claude/skills/buglog/bug-record.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue