feat(buglog): status field, buglog skill, status-driven gitea-sync
The buglog had no status field, so "is this bug open?" meant reading free text, and gitea-sync closed any issue whose `fix` text was non-empty -- including "NOT FIXED"/"PENDING"/"PARTIALLY FIXED" ones (bug-1855). - Backfill `status` (+ status_updated) on all 1,193 entries. The 85 entries whose text said not-fixed/pending/out-of-scope were checked against HEAD and git history; the rest are `fixed`. Result: 36 open, 10 partial, 2 investigating, 5 cannot-reproduce, 4 wontfix, 1 duplicate. Insert-only diff: existing lines are untouched. - .claude/rules/buglog-status.md defines the status values (kept out of openwolf.md, which `openwolf update` overwrites). - scripts/buglog.py + buglog skill: add (next free id), status, note, recur, list, show, triage. Edits only the touched entry's lines; a full json.dumps rewrite churns ~1,200 lines of mixed \u-escaping. - gitea-sync (script + skill): state and a single bug:<status> label come from `status`; reopen supported; issues shared by several entries follow the most-open one; only status labels are swapped; per-entry write-back with gitea_synced_status so unchanged entries cost no API calls; list-typed `fix` no longer crashes; spec issues close on done phases (spec.json has no implementation approval); --since no longer floods Gitea after the backfill. - Disable OpenWolf buglog auto-detection: its hook numbers new entries len(bugs)+1, which collides with existing ids (bug-1856, upstream). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8330997c5a
commit
19104f1940
7 changed files with 3333 additions and 171 deletions
34
.claude/rules/buglog-status.md
Normal file
34
.claude/rules/buglog-status.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Buglog status field
|
||||
|
||||
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".
|
||||
- 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`.
|
||||
- `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:
|
||||
`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`
|
||||
64
.claude/skills/buglog/SKILL.md
Normal file
64
.claude/skills/buglog/SKILL.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
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
|
||||
triggers:
|
||||
- /buglog
|
||||
- log a bug
|
||||
- open bug list
|
||||
- mark bug fixed
|
||||
---
|
||||
|
||||
# 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`.
|
||||
|
||||
Status values and when to use each: `.claude/rules/buglog-status.md`.
|
||||
|
||||
## Before fixing a bug
|
||||
|
||||
Search for prior art first (project rule): `openwolf bug search "<error text>"`,
|
||||
or `python3 scripts/buglog.py list --grep '<regex>'`.
|
||||
|
||||
## Commands
|
||||
|
||||
| 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>"` |
|
||||
| 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`) |
|
||||
| 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> ...` |
|
||||
|
||||
`status` requires `--note` for every status except `fixed`, and always
|
||||
rewrites the note, so a stale note never outlives its status.
|
||||
|
||||
## 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`.
|
||||
|
|
@ -24,6 +24,21 @@ Sync state (issue numbers) is stored in source files for idempotency.
|
|||
/gitea-sync --dry-run # preview without making changes
|
||||
```
|
||||
|
||||
## Preferred: run the script
|
||||
|
||||
`scripts/gitea-sync.py` implements every step below over the Gitea REST API
|
||||
and needs no MCP server (token from `GITEA_TOKEN`, or the gitea MCP entry in
|
||||
`~/.claude.json`). Use it whenever it can reach Gitea; follow the manual MCP
|
||||
procedure only when it can't:
|
||||
|
||||
```bash
|
||||
python3 scripts/gitea-sync.py --dry-run --bugs --since 2026-09-01 # preview
|
||||
python3 scripts/gitea-sync.py --bugs --since 2026-09-01
|
||||
python3 scripts/gitea-sync.py --spec <name>
|
||||
```
|
||||
|
||||
Keep the script and this procedure in step when changing either.
|
||||
|
||||
## Procedure
|
||||
|
||||
### 0. Parse arguments
|
||||
|
|
@ -31,7 +46,10 @@ Sync state (issue numbers) is stored in source files for idempotency.
|
|||
- `--dry-run`: only report what would change, don't call MCP write tools
|
||||
- `--spec <name>`: sync only one spec directory
|
||||
- `--bugs`: sync bugs only (skip specs)
|
||||
- `--since <YYYY-MM-DD>`: only sync bugs with `last_seen >=` this date
|
||||
- `--since <YYYY-MM-DD>`: create issues only for bugs with `last_seen >=` this
|
||||
date; for already-synced bugs (`gitea_issue` set), also mirror those whose
|
||||
`status_updated >=` this date, so a status change on an old bug still
|
||||
reaches Gitea without flooding it with old never-synced bugs
|
||||
|
||||
### 1. Ensure labels exist
|
||||
|
||||
|
|
@ -49,6 +67,16 @@ for any missing ones:
|
|||
| `spec` | `3498DB` | Spec-tracked issue |
|
||||
| `bug` | `E74C3C` | Bug-tracked issue |
|
||||
| `bug:fixed` | `27AE60` | Bug resolved |
|
||||
| `bug:partial` | `F39C12` | Partly fixed, remainder open |
|
||||
| `bug:investigating` | `E67E22` | Real failure, root cause not found yet |
|
||||
| `bug:cannot-reproduce` | `95A5A6` | Repro attempted and failed |
|
||||
| `bug:wontfix` | `7F8C8D` | Not a bug / accepted limitation |
|
||||
| `bug:duplicate` | `BDC3C7` | Duplicate of another bug |
|
||||
| `bug:asan` | `8E44AD` | Sanitizer finding |
|
||||
| `bug:crash` | `C0392B` | Crash (SEGV, double-free, UAF) |
|
||||
| `bug:race` | `D35400` | Race / TOCTOU |
|
||||
| `bug:ha` | `2980B9` | HA / Raft / failover |
|
||||
| `bug:data-loss` | `922B21` | Data loss |
|
||||
| `blocked` | `E74C3C` | Blocked on dependency |
|
||||
|
||||
### 2. Sync Specs
|
||||
|
|
@ -95,30 +123,72 @@ For each spec directory in `.kiro/specs/` (or just `--spec <name>`):
|
|||
}
|
||||
```
|
||||
|
||||
d. **Phase state updates** (for already-synced specs): Check `spec.json`
|
||||
approvals. If requirements AND design are approved → close the
|
||||
requirements_design issue via `mcp__gitea__issue_write` method `update`
|
||||
with `state: "closed"`. If implementation approved → close
|
||||
implementation and validation issues. If all approved → close milestone
|
||||
via `mcp__gitea__milestone_write` method `update`.
|
||||
d. **Phase state updates** (for already-synced specs). `spec.json`
|
||||
`approvals` only covers requirements/design/tasks — there is no
|
||||
implementation approval — so use `phase` for the later issues:
|
||||
|
||||
"Done" phases: `implementation-complete`, `implemented`, `superseded`
|
||||
(no tool distinguishes the first two).
|
||||
|
||||
| Condition | Action |
|
||||
|-----------|--------|
|
||||
| `approvals.requirements.approved` AND `approvals.design.approved`, or phase is done | close `requirements_design` |
|
||||
| phase is done | close `implementation`, `validation` and the milestone |
|
||||
| phase is not done | reopen `implementation`/`validation`/milestone if closed |
|
||||
|
||||
The same rules give the initial state when the issues are first created.
|
||||
|
||||
Close issues via `mcp__gitea__issue_write` method `update` with
|
||||
`state: "closed"`, the milestone via `mcp__gitea__milestone_write`
|
||||
method `update`.
|
||||
|
||||
### 3. Sync Bugs
|
||||
|
||||
Read `.wolf/buglog.json`. For each entry:
|
||||
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".
|
||||
|
||||
1. **Skip** if:
|
||||
- Entry has `gitea_issue` field (already synced) AND bug was already fixed
|
||||
when synced (no state change needed).
|
||||
- Filter: if `--since <date>` provided, skip entries with
|
||||
`last_seen < <date>`.
|
||||
**Status → Gitea state / label** (`status` values are defined in
|
||||
`.claude/rules/buglog-status.md`):
|
||||
|
||||
| `status` | Gitea state | Status label |
|
||||
|----------|-------------|--------------|
|
||||
| `open` | open | — |
|
||||
| `partial` | open | `bug:partial` |
|
||||
| `investigating` | open | `bug:investigating` |
|
||||
| `fixed` | closed | `bug:fixed` |
|
||||
| `cannot-reproduce` | closed | `bug:cannot-reproduce` |
|
||||
| `wontfix` | closed | `bug:wontfix` |
|
||||
| `duplicate` | closed | `bug:duplicate` |
|
||||
| missing (hook-created entry) | 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
|
||||
user-reported issue whose cause was split into several bugs, like #46). Such
|
||||
an issue follows its "most open" entry, in this order: `open`,
|
||||
`investigating`, `partial`, (no status), `cannot-reproduce`, `duplicate`,
|
||||
`wontfix`, `fixed`.
|
||||
|
||||
For each entry (after the `--since` filter):
|
||||
|
||||
1. **Skip** if it has `gitea_issue` AND `gitea_synced_status == status`
|
||||
(already mirrored, nothing changed).
|
||||
|
||||
2. **Create issue** for entries without `gitea_issue`:
|
||||
Use `mcp__gitea__issue_write` method `create`.
|
||||
|
||||
**Title**: `[<bug-id>] <first sentence of error_message>` (max 120 chars)
|
||||
**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).
|
||||
|
||||
**Body**:
|
||||
```
|
||||
**Status:** <status> (<status_note>, if present)
|
||||
<"Duplicate of <duplicate_of>" / "Repro attempts: <repro_attempts>", if present>
|
||||
|
||||
## Error
|
||||
<error_message>
|
||||
|
||||
|
|
@ -126,10 +196,11 @@ Read `.wolf/buglog.json`. For each entry:
|
|||
<root_cause>
|
||||
|
||||
## Fix
|
||||
<fix> (or "*Not yet fixed*" if empty)
|
||||
<fix> (if `fix` is an array, join the elements with blank lines;
|
||||
"*Not yet fixed*" if empty)
|
||||
|
||||
## Affected Files
|
||||
- <file> (split on commas)
|
||||
- <file> (split on commas), or each element of `files`
|
||||
|
||||
## Tags
|
||||
`tag1` `tag2` ...
|
||||
|
|
@ -141,41 +212,62 @@ Read `.wolf/buglog.json`. For each entry:
|
|||
*Synced from `.wolf/buglog.json`*
|
||||
```
|
||||
|
||||
**Labels**: Start with `["bug"]`. If `fix` is populated, add `bug:fixed`.
|
||||
Map known tags to labels: `asan`/`ubsan` → create `bug:asan` label,
|
||||
`SEGV`/`double-free`/`UAF` → `bug:crash`, `race`/`toctou` → `bug:race`,
|
||||
`ha`/`raft`/`election`/`failover` → `bug:ha`, `data-loss` → `bug:data-loss`.
|
||||
**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**: `"closed"` if `fix` is populated, `"open"` otherwise.
|
||||
**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. **Close fixed bugs**: For entries WITH `gitea_issue` where `fix` is now
|
||||
populated (was empty before — we track by checking if the Gitea issue is
|
||||
still open). Use `mcp__gitea__issue_write` method `update` with
|
||||
`state: "closed"`.
|
||||
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`.
|
||||
|
||||
4. **Write `gitea_issue`** back to the buglog entry: add
|
||||
`"gitea_issue": <issue_number>` to the entry in `.wolf/buglog.json`.
|
||||
Read the full file, modify the matching entry, write back with
|
||||
`json.dumps(data, indent=2)`.
|
||||
**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):
|
||||
|
||||
```bash
|
||||
python3 scripts/buglog.py set <bug-id> \
|
||||
gitea_issue=<issue_number> 'gitea_synced_status="<status>"'
|
||||
```
|
||||
|
||||
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"`.
|
||||
|
||||
### 4. Report
|
||||
|
||||
Print summary: "Specs: N synced, M skipped | Bugs: X created, Y closed, Z skipped"
|
||||
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.
|
||||
|
||||
## 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/fix state changed.
|
||||
unless their phase changed (specs) or `status != gitea_synced_status` (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** (~13K lines). Always use `--since` filter for
|
||||
practical runs (e.g., `--since 2026-07-18`). Without `--since`, sync ALL bugs.
|
||||
- **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.
|
||||
- **Private repo**: `nexa/xce` is private. MCP auth must have access.
|
||||
|
||||
## Gitea MCP Tool Reference
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue