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>
3.9 KiB
| id | status | status_updated | status_note | component | severity | timestamp | last_seen | occurrences | files | tags | related | legacy | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| bug-1818 | fixed | 2026-09-24 | harness | unknown | 2026-09-16T08:20:00Z | 2026-09-19T09:00:00Z | 1 |
|
|
true |
bug-1818: Live 3-node HA E2E validation (controller-status spec, Requirement 12 delta, task 7.6)…
Description
Live 3-node HA E2E validation (controller-status spec, Requirement 12 delta, task 7.6): cmd_ctrlstatus (xce_test client) printed electedTerm: unknown / electedAt: unknown for the new primary-election-term/time fields on EVERY standby query, both before and after a real primary kill/failover (confirmed after ~4 heartbeat intervals, ruling out a convergence-timing gap) -- even though the same query correctly showed non-stale, converged term/committedLsn/appliedLsn values for the same standby at the same moment.
Environment
Not recorded (migrated from buglog.json).
Reproduction
Not recorded (migrated from buglog.json).
Root cause
cmd_ctrlstatus gated the new electedTerm/electedAt print lines on NULLSTR(status->primaryName), reusing the same condition the pre-existing Primary: line already uses -- per the delta design's own Locked Decision 3, which assumed primaryName would be "known" on a standby that has learned the primary. But primaryName is UNCONDITIONALLY empty on every non-primary responder in this build: ctrlstatus_fill (src/controller/ctrlstatus.c) only self-reports the primary's own name when the responder IS the primary (ha->primary_host is vestigial/never written, a pre-existing behavior from Requirement 2 / commit ade0081, predating this delta). So Requirement 2.3's "standby knows the primary" branch is dead code via the plain ctrlstatus command, and gating the new fields on the same condition made them permanently unreachable on any standby -- even though status->elected_at_ms (populated unconditionally by ctrlstatus_fill, task 7.4, and correctly heartbeat-learned by task 7.3) was present and correct the whole time. Caught only by a real live cluster run; all unit tests (which test ctrlstatus_fill directly, not cmd_ctrlstatus's print gate) passed throughout, since the bug was entirely in the untested CLI display layer.
Fix
FIXED (2026-09-16, controller-status Requirement 12 delta). Changed cmd_ctrlstatus's gate from NULLSTR(status->primaryName) to status->elected_at_ms == 0. This is both necessary and sufficient: elected_at_ms stays exactly 0 until a real promotion stamps it (either promotion road, ha_do_promote / raft_cb_become_leader) or a heartbeat carries a learned value from an actual primary (ha_consume_heartbeat_committed) -- unlike term, which can legitimately bump from mere vote/candidacy activity before any primary is actually elected, so term == 0 would have been the wrong gate (verified via a dedicated candidacy-in-progress test case: elected_at_ms == 0 but term != 0 must still print unknown). ctrlstatus_fill itself was NOT touched -- it correctly remained an unconditional, no-role-branch read (Locked Decision 2), unaffected by this bug. Verified via a standalone scratch program (against the real ctrlstatus_fmt.h header, 4 cases: standby/bug case, never-elected, candidacy-in-progress, primary/regression) plus a repeat live 3-node E2E run confirming the fix converges end-to-end after a real primary kill. design.md's Locked Decision 3 and its dependent traceability/service-contract text were corrected in the same commit to document this live-E2E finding, mirroring the spec's existing Req 10/11 delta convention for recording live findings.
Verification
Not recorded (migrated from buglog.json).
References
Not recorded (migrated from buglog.json).