TOGTools-v0.10.1
What's new
Changelog
[v0.10.1] (2026-10-04) - WoW Forever recognised again
Bug Fixes
- WoW Forever was treated as Classic Era, so opening a profession window threw
ProfCapture.lua:412: attempt to call a nil value(GetTradeSkillLine). Reported by the operator from a Forever client, opening TOGProfessionMaster's Crafting tab. Root cause:addon.DeriveGameVersiononly recognised Forever whenWOW_PROJECT_ID == WOW_PROJECT_MAINLINE, but Forever reports project id 18 (FastGuildInvite observed "Project ID: 18, Build: 16001" in-client on build 1.60.1.70124,FGI_Compatibility.lua:22-38; the forever tree'sProjectConstants.luanames only 1 and 2). So Forever at Interface 16001 was derived as Vanilla,isRetailwas false, and every Forever/Retail branch in the addon was dead there -- ProfCapture took the ClassicGetTradeSkillLinepath, which Forever does not have. Other consumers that were wrong on Forever for the same reason, among them: Quest DB refused to run at all (QuestDB.lua:71gates onisForever, "only runs on the WoW Forever client");ProfCapture:ReadTrainerreadGetTrainerServiceInfoin Classic return order (ProfCapture.lua:182-198); I'd Hit That skipped Forever's ranged-hit modifier (IdHitThat.lua:275); Smack offered its six combat-log triggers, which it hides onisRetail120Plus(Smack.lua:142-147); and/togt vctried VersionCheck instead of saying it is not available. The load-time call now counts project id 18 as a retail-codebase client too. New spec: "is derived from Forever's own project id, 18" (Tests/version_spec.lua). Not verified in game. Location:Version.lua.
Process
CHANGELOG.mdarchived under the release-body ceiling. With the entry above it measured 121,457 characters, over the 120,000 working ceiling (GitHub refuses a release body over 125,000). The oldest section, v0.8.6, moved whole to the top ofCHANGELOG_ARCHIVE.md, ahead of v0.8.5, with the editing tools. The trailing pointer now says "v0.8.6 and earlier".
[v0.10.0] (2026-09-30) - Event Trace developer tab
New Features
- Event Trace: a live stream of every event the WoW client fires. New developer tab (shown with Developer Tools on) over the stream !!TOGT v0.2.5 captures from the first line of addon code (
!!TOGT/EventStream.lua). Lines arrive as they happen, like a chat window: sequence number, local time with milliseconds, seconds from login (negative before it), event name, arguments. The view is aScrollingMessageFrame(the widget chat windows use; AceGUI has none) keeping the newest 5,000 lines on screen, with the mouse wheel scrolling 3 lines, or 20 with Shift. Filter narrows the view and the copy to lines containing the text. Copy opens every captured line, not only the on-screen ones, in a read-only box for Ctrl+A / Ctrl+C: an AceGUIMultiLineEditBoxwhoseOnTextChanged(which AceGUI fires only for the user's own typing) puts the text back, so nothing can be deleted by accident. Counts lists every event fired since load and how often, excluded ones included. Pause, a confirmed Clear, a Feed on tick box, and an editable exclude list round it out. Feed on is OFF by default and saved, so it stays as left through reloads (operator: "we don't want a memory leak, this should only work when i want it to"); unticking it stops the stream at once and frees every line. The Copy box shows the whole stream 1,000 lines to a page, with Previous / Next, because one edit box cannot hold it. That was measured in game on 2026-09-30. The whole stream in one box showed BLANK at 9,511 lines. It was blank again at 5,099 lines after the box's byte cap was lifted (SetMaxBytes(0), as the AceGUI report in WoWInterface thread 55511 did) and !!TOGT began escaping control bytes. 1,000-line pages showed and copied. Later the same day a single box showed at 1,691 lines and was blank at 1,992. AddOn Studio'sEditBox:SetTextpage documents one cause: "SetText fails when the text contains characters that aren't renderable (e.g. control chars, invalid UTF8). In that case the EditBox is cleared". So !!TOGT v0.2.5 now also escapes every byte that is not valid UTF-8, and never cuts an argument mid-character. Whether that alone lets one box hold a long stream is not tested in game, so Copy stays paged. Needs !!TOGT v0.2.5; with an older !!TOGT the tab says there is no event stream. Memory only: nothing is saved, so a/reloadstarts a fresh stream. The operator asked for it on 2026-09-30 ("an in game log capture ... capture them into a 'memory' log and display them in TOGTools to help analysis"; "it's just a livestream we capture"; "this is to capture the wow api, so we can see what's going on in it"). It replaces a GUILD-chat login probe drafted earlier the same day for FastGuildInvite's question (inbox thread15b4d7a3), which never shipped. - Settings in
db.global.eventTrace(enabled,capture,exclude), which !!TOGT reads fromTOGTools_DBatPLAYER_LOGIN.EventTrace:SetExcludeapplies a new list to the live stream at once, and empty text restores !!TOGT's default.SetCapture(true)restarts a stopped stream andSetCapture(false)calls !!TOGT'sStop(). The module is inmoduleregistry_spec'sGATE_EXEMPT, with the reason: the gate is !!TOGT's. 15 specs inTests/eventtrace_spec.luaload the REAL!!TOGTfiles from the sibling folder, not a stand-in;Modules/EventTrace/EventTrace.luais at 100% line coverage. Locations:Modules/EventTrace/EventTrace.lua,GUI/EventTraceTab.lua,TOGTools.lua, every.toc,Tests/eventtrace_spec.lua,Tests/moduleregistry_spec.lua.
Bug Fixes
- Quest DB threw a load error on the Classic client:
Attempt to register unknown event "QUEST_DATA_LOAD_RESULT"(24x). Reported by QuestDB (inbox threadb0734afe) from the user's_classic_client. The event exists only in the live and Forever trees (QuestLogDocumentation.lua); it is not in the classic or classic_era docs.Modules/QuestDB/QuestDB.lualoads on every flavour and registered it unguarded at file load, andRegisterEventthrows on an event the client does not have. The registration is nowpcall'd; the walk only runs on Forever (UnavailableReason), so nothing is lost elsewhere. New spec: "loads on a client that does not have the reply event" (Tests/questdb_spec.lua), whose frame stub throws on anyRegisterEventas the client does. Not verified in game. Location:Modules/QuestDB/QuestDB.lua.
[v0.9.6] (2026-09-29) - Diagnostics explains Lua errors
New Features
- Explain a Lua error. Players bring errors to whoever they know, whether or not the addon is
theirs: the prompt for this was a Retail
LibOpenRaid.lua:1222: attempt to compare a secret number value (execution tainted by 'Details')brought to the TOG author. New pure-logicaddon.ErrorInterpreter(Interpretreturns a result table,Explainreturns the display text) reads a singlepath:line: reasonline or a whole BugSack / Blizzard error-window paste. It works out what the error means (16 rules: secret value, blocked / forbidden action, attempt to call / index / compare / concatenate / arithmetic, bad argument, stack overflow, script ran too long, SavedVariables too large, missing library, XML from another game version, and more), which addon folder owns the file (it also handles a cut-off...face/AddOns/path, a path pasted without itsInterface/AddOns/prefix, and[string "@..."]), and whether the file is inside a library bundled with another addon (Foo/Libs/LibBar/). It then says who to report it to. For Blizzard code or a standalone library addon (Lib*,Ace*,Ace3), the report goes to the game'stainted by 'X'marker first, otherwise the first real addon in the stack (skipping BugGrabber/BugSack). The stack scan stops atLocals:. In the Diagnostics tab: a new Explain entry in the row right-click menu, and a plain left-click on a row does the same (added after the user could not find how to get a row into the window). There is also an Explain an error button that opens a paste window updated as you type. The window shows the addon's installed title, version and author throughGetAddOnMetadata, and says whether it is a Pimptasty addon (authorPimptastyor aTOG*folder). The label is "Pimptasty", not "TOG", because not all of the user's addons are TOG ones. For a Pimptasty addon, the Discord invite (EI.DISCORD, the same link the README uses) appears in a read-only copy field right under the reading: click it and press Ctrl+C, since addons cannot write the clipboard. It was briefly a separate button; the user asked "why can't we just have the copy box appear in the text". The field is the newaddon.UI.CopyField, which selects all on click and puts the text back if typed over. Its select-all hook on AceGUI's pooled edit box acts only while TOGTools holds the widget. The field is shown only for Pimptasty addons, so the explanation pane is rebuilt on each render (AceGUI's List layout leaves room for a hidden child). The Explain window snaps to the main window's right edge with LibAceGUIWidgets'DockWindow(user request). It is docked by default; dragging it away undocks it, dropping it near the edge re-docks it, and the choice is kept per character inchar.frames.explainDock. An AceGUI Frame moves from its title bar's mouse handlers and never fires theOnDragStopthat DockWindow listens to, so the title bar'sOnMouseUpcallsh:Moved(). The main window's frame goes back to AceGUI's shared pool on close, and the Explain window must let go of it first, or it would dock to whichever addon's window reused that frame. A newMainWindow.beforeReleasehook calls the library'sW:UndockWindow(MINOR 38, requested on thread2472500eacf8; that MINOR also lets go of a released AceGUI frame by itself). On an older library, it pins the Explain window and re-points its dock at a private hidden frame. Output is ASCII only. Location:Modules/Diagnostics/ErrorInterpreter.lua(new, added to all seven TOCs),GUI/DiagnosticsTab.lua,GUI/MainWindow.lua. Spec:Tests/errorinterpreter_spec.lua, which uses the Details error verbatim; line coverage is 100%. After Peer Review's reply to this session's self-audit (thread4e63f914), there were three fixes, each with a spec. The stack scan skips the error's own message line, so a reason quoting a file ("... in Foo/Bar.lua") no longer names Foo as the caller; that spec was written first and failed first. The Explain window interprets once per keystroke and passes the result toExplainandWantsAddonLoad, which now accept either text or a result. It used to interpret the same text three times. And the "Pimptasty addon" test isEI.IsOurs, a substring match on the TOC Author line, because co-authored addons carry the name inside a longer line. - Diagnostics keeps each error's original message (
msg, new entries only, capped at 500 characters, not part of the dedup key), because the compactsourcedrops the folders where an embedded library shows. Rows saved before this, and blocked-action rows, are rebuilt into the game's own wording for Explain. Location:Modules/Diagnostics/Diagnostics.lua. Spec:Tests/diagnostics_spec.lua.
Improvements
- One copy box, from the widget library. The user asked for the "copy" popup to live in
LibAceGUIWidgets so every addon can use it. That library method,
W:ShowCopyBox(text, opts)(one button, text selected, read-only), was requested on the library's inbox (thread737274f20c2f) and built there at MINOR 38 (library v0.2.8). TOGTools now copies through a single helper,addon.UI.ShowCopy(text, prompt, parent), which callsShowCopyBoxwhen the installed library has it and otherwise builds the same box from the library's existingShowDialog(one button, pre-filled, selected). So nothing in TOGTools has to change when the library ships it. The hand-rolledStaticPopupDialogs["TOGTOOLS_DIAG_COPY"]is gone; the Diagnostics row Copy uses the helper.parentis passed on purpose: the library dialog takes its parent's strata, so parenting to UIParent would open the box behind the FULLSCREEN_DIALOG windows. Location:GUI/UI.lua,GUI/DiagnosticsTab.lua. Spec:Tests/uishowcopy_spec.lua(both paths). - Diagnostics is its own main tab, for every player, not a developer-only Logs sub-tab. User
report, 2026-09-29, with a screenshot of Settings > Modules: the Diagnostics tab did not appear
and there was no toggle for it. I had made it visible only when Settings > General > Developer
Tools was on, so no player could reach it, and neither could the user with Developer Tools off.
It briefly became a Log category, and then the user asked: "it needs to be it's own main tab, and
not a subtab". It now registers in
addon.modules["diagnostics"](no longeraddon.logCategories), withtabKey/configKey/label/WINDOW_SIZEinGUI/DiagnosticsTab.lua, the usual automatic Settings > Modules toggle backed bydiagnostics.enabled(default on), andshowWhenDevso it stays visible under Developer Tools with the toggle off. The gate (Diagnostics.IsWanted, which is alsorecordingEnabled) isIsModuleEnabled("diagnostics")OR Developer Tools. Switching it on runsDiagnostics:Wake()(broker, fallback capture, draining the companion's early buffer), from the module toggle'sOnModuleToggledand from the Developer Tools setting, replacing the startup code the Settings file used to carry. The toast, the Titan broker and/togt diagopen the main tab./togt diagno longer insists on Developer Tools: it refused players even after the tab was surfaced, which I missed when I changed the gate./togt logs diagnosticsstill gets there, and/togt diagis now in/togt help. Known cost: with it on by default, every player records errors (capped at 500 distinct entries) and sees the new-error pop-up. Location:Modules/Diagnostics/Diagnostics.lua,GUI/DiagnosticsTab.lua,GUI/Settings.lua,GUI/LogsTab.lua,SlashCommands.lua,TOGTools.lua(default). Specs:Tests/diagnostics_spec.lua(the recording gate, the main-tab registration, Open, Wake) andTests/logregistry_spec.lua(Diagnostics exemptions removed). - Explain sends a missing-dependency error to the Addon Load tab. User request: "if the error
is the missing dep error, it needs to have some way to click to open up the addon load tab".
New
AddonLoad:GetMissingDeps(name)answers one addon's missing required dependencies, the same way the tab works them out. Explain asks it for the culprit and every addon in the stack, names what each is missing under a new "Missing addons" heading, and shows an Open Addon Load button (EI.WantsAddonLoad). It does the same for an error that is itself about a dependency (a newdepsrule) or a missing library. Location:Modules/AddonLoad/AddonLoad.lua,Modules/Diagnostics/ErrorInterpreter.lua,GUI/DiagnosticsTab.lua. Specs:Tests/addonload_spec.lua,Tests/errorinterpreter_spec.lua. - The Discord copy field is the library's. The user asked for the library's copy box with its
copy icon. LibAceGUIWidgets built an inline widget,
LAGW-CopyField(MINOR 38, inbox thread513d5779bb63), andaddon.UI.CopyFieldcreates it whenAceGUI:GetWidgetVersion("LAGW-CopyField") >= 2. Version 1 errored on every Create ("FontString:SetText(): Font not set", its label had no font); the user hit it in game, it was reported (thread63629c29099f) and fixed as Version 2. On an older library,UI.CopyFielddraws the same box itself: the library'sCOPY_ICONwith its numbers (LEFT +1,-1, 14px, inset 20, the 1.0/0.6 tint), and on release it hides the icon and puts AceGUI's insets (0, 0, 3, 3) back on the pooled edit box. Location:GUI/UI.lua.
Documentation
README and the CurseForge page checked claim by claim against the code (user request: "ensure we have addon functionality parity with the documents"). Corrected where the documents said something the code does not do:
- Both promised a history-retention setting.
retentionDaysis only a default (TOGTools.lua), andGUI/Settings.luahas no control for it, so both now say "older than 90 days". - The Gratz achievement triggers were described as Retail-only.
Modules/Gratz/Gratz.luaregisters the achievement events on every flavour, so both now say "a version of WoW that has achievements". - The README's Gratz and Smack Say/Yell rule, and the page's Smack Emote note, left out that
Midnight and Forever block automatic Say/Yell/Emote everywhere (
Compat.lua, viaVersion.luaisRetail120Plus). - Bloodlust was listed as working on Classic Era.
Modules/Smack/Smack.luasays it never lands there. - The gathering summary's sources were described as "the best". They are the three most gathered
by item count (
GatheringLog.lua). - The custom date filter was described as a single day.
GUI/DateRangePicker.luapicks a day or a range, labelled "Custom...". - Diagnostics "records nothing" when switched off now adds "unless Developer Tools is on".
- Quest DB was described as a Forever-only tab. It shows on every flavour behind Developer Tools and only works on Forever.
- Walk recipe sources was described as covering "every known recipe". It covers the TOG profession database and needs ProfessionDB, which both Requirements sections now list as optional.
- Switching Login Digest off from its tab hides the tab. Both documents now say so.
- The Addon Load status list now says "include", since unlisted client reasons show raw.
Added what the code has and neither document mentioned: Settings > Clear Data, the verbose debug switch, the minimap right-click to settings and its per-character hide, the gear icon, Login Digest's Print test digest, Smack's Manual trigger, and the missing slash aliases (
versioncheck,db,logs bank/gbank/whisper, plus the README's aliases now copied into the page).- Both promised a history-retention setting.
Stale in-game help text. The Mailbox help said the threshold "lives in the addon settings panel", but it is the slider on the tab (
GUI/MailboxTab.lua). The Logs help still called Trade "coming in a follow-on patch" (GUI/LogsTab.lua). Both Gathering descriptions in Settings left out gas and pickpocketing (GUI/Settings.lua). Two code comments were also out of date.
[v0.9.5] (2026-09-28) - I'd Hit That on WoW Forever; Name Prefix matches a first or last name
Bug Fixes
- I'd Hit That said "not applicable" on WoW Forever. Player report, 2026-09-28 (screenshot of
the tab: "Hit chance was removed as a stat in Warlords of Draenor"). I gated the whole module on
gv.isRetail, which is true on Forever because it is the Retail client, and never checked whether Forever has hit. It does: its own character sheet computes melee, ranged and spell hit asGetCombatRatingBonus(CR_HIT_*) + Get*HitModifier()(wow-ui-source-foreverBlizzard_UIPanels_Game/Camelot/PaperDollFrameStats.lua:489-496). The engine now asksNO_HIT_STAT = gv.isRetail and not gv.isForevereverywhere it used to askisRetail, and the tab shows its settings on Forever. Forever has no weapon-skill API in its generated docs, so the existing no-skill path is used (Blizzard's miss table, glancing "not known"); ranged hit is read the way Forever's sheet reads it (GetRangedHitModifier+CR_HIT_RANGED, no scope tooltip scan), Forever only. Every one of those calls isSecretWhenUnitStatsRestricted(PlayerScriptDocumentation.lua), so each read goes throughstat(): a secret value marks the rebuild unreadable, the previous numbers are put back and the player half stays dirty so the next refresh retries. Retail proper is unchanged (still always-hits, no stat reads). Not verified in a Forever client, including the HUD's placement on Forever's target frame. Location:Modules/IdHitThat/IdHitThat.lua,GUI/IdHitThatTab.lua. Spec:Tests/idhitthat_spec.lua("WoW Forever", three cases). - Name Prefix's "Hide prefix if nickname matches character name" never matched on Forever.
Player report, 2026-09-28: character "Vishi Swaz", nickname "Vishi". Forever allows a space in a
character name (a first and a last name), and the test was an exact, case-sensitive
nick == UnitName("player"). NewnickMatchesNamematches the whole name or any one word of it (first or last name, operator 2026-09-28), ignoring case; a partial word does not match. The player also suggested per-character checkboxes; not built, as the name match covers the report. Location:Modules/NamePrefix/NamePrefix.lua,GUI/NamePrefixTab.lua(checkbox description). Spec:Tests/nameprefix_spec.lua.
[v0.9.4] (2026-09-28) - Quest DB developer tool; Item DB opens on WoW Forever and walks its whole item range; Profession Capture on Forever
New Features
- Quest DB developer tool (WoW Forever only). QuestDB inbox thread 76745964: LibQuestDB has no
offline source for Forever's own quests (QuestieDB's Forever TOC carries the Vanilla set, and
Forever's QuestV2 DB2 has no titles), so this asks the live client. It walks the ids
LibQuestDB:GetCoverageanswers"uncovered"for, not every non-"shipped"id, because"unknown"spans the whole range. Each quest is requested withC_QuestLog.RequestLoadQuestByIDand read onQUEST_DATA_LOAD_RESULT(both confirmed inwow-ui-source-foreverQuestLogDocumentation.lua:1219,:1336). The walk is paced by replies, not a ticker: at most 10 requests outstanding, and each reply sends the next. A_fillingflag guards re-entry, because the event is declaredSynchronousEvent. The cost: a request the server never answers holds its slot. When all ten are held, the tab says "Waiting", and Pause then Fill gaps re-sends them. Saved asdb.global.questDB.quests[id] = "title\31level\31groupSize\31elite\31questType\31obj\30obj...\31tip\30tip...", the format QuestDB's generator reads. A field the client does not return is stored empty. Separators are stripped from client text. Off Forever, or without LibQuestDB loaded, the tab says why and nothing runs. The walk's ceiling is the end of LibQuestDB's last coverage run (QuestDB:TopID). I first shipped a fixed 65000 with a comment claiming the ids sat below it, without checking. Forever's coverage runs to 99234 and most of its ids are above 73000. The first in-game walk therefore stopped at cursor 65001, marked itself complete, and saved 64 quests. A store in that state resumes from its cursor on the next Build / Resume; the operator's resumed walk finished at cursor 99235 with 833 quests (QuestDB, thread 76745964). That first walk did show that objectives and tooltip text come back for a quest the player does not hold. Location:Modules/QuestDB/QuestDB.lua,GUI/QuestDBTab.lua, all seven TOCs (QuestDBadded toTOGTools_Camelot.tocOptionalDeps), thequestDBdefaults inTOGTools.lua, andC_QuestLogin.luacheckrc/.luarc.json. Spec:Tests/questdb_spec.lua; ungated exemption recorded inTests/moduleregistry_spec.lua.
Bug Fixes
Item DB tab threw "SetPoint would result in anchor family connection" on open (WoW Forever). ItemDB inbox thread a2110cf1; the operator saw it 3x opening the window from the minimap button. The failing call is AceGUI's Flow layout anchoring the tab's first group to the TabGroup content (
AceGUI-3.0.lua:717); the client refuses that when the content already depends on the group. AceGUI pools widget frames for every addon andReleaseclears only the released frame's own points, so a group another frame still anchors onto can come back fromAceGUI:Create. I did not find the stray anchor in TOGTools' own code (everySetPointonto a widget frame inGUI/was read), so which addon leaves it is not verified. NewUI.AnchorPath(from, target)walks a region's points breadth-first and returns the chain, andUI.CreateUntangled(AceGUI, type, container)refuses a pooled widget the container already depends on: it is hidden, kept out of the pool for the session, and a fresh one is taken; the first time, the chain is printed once in chat so the frame holding the stray anchor is named. The Item DB tab's four groups use it. Not verified in game. Location:GUI/UI.lua,GUI/ItemDBTab.lua. Spec:Tests/uianchor_spec.lua.Profession Capture's "Walk recipe sources" looked like it did nothing. On Forever it walked all 2,511 ProfessionDB recipes, got 0 answers, and said nothing (operator's screenshot, 2026-09-27). Blizzard only calls
C_TradeSkillUI.GetRecipeSourceTextfor a recipe shown in the open profession window, and only for an unlearned one (wow-ui-source-foreverBlizzard_ProfessionsRecipeSchematicForm.lua:633-634). A second walk with the trainer and the Blacksmithing window open also answered 0. The operator's screenshot shows why: the window listed only learned recipes, because its "Show unlearned" filter (Blizzard_Professions.lua:1222) was off.retailKnownasks for a source only for an unlearned recipe, so it had nothing to ask about. Changes:- Capture never writes the profession window's filters. I first forced "Show learned" and
"Show unlearned" on, the way the trainer's are, and that locked the WoW Forever profession
window (K) so nothing in it could be clicked (operator, 2026-09-28; it cleared with TOGTools
disabled). Cause: Forever's
Professions.GenerateCraftingDataProviderforces "Show unlearned" back off every time it rebuilds the list (wow-ui-source-foreverBlizzard_ProfessionsTemplates/Camelot/Blizzard_Professions.lua:17-20), each change re-lists and firesTRADE_SKILL_LIST_UPDATE, and my handler turned it on again, so the two fought without end.OpenTradeSkillFilters,RestoreTradeSkillFilters,TradeSkillFiltersOnand theTRADE_SKILL_CLOSEregistration are gone; the list the window shows is read as it is. CaptureKnownmerges a Retail read into the stored profession, so a narrower list (learned only) keeps the recipes an earlier read listed instead of overwriting them.WalkRecipeSourcesalso counts every source the window capture read. These are keyed by the client's own recipe id, which covers ids ProfessionDB lacks.- A walk keeps every source an earlier walk found, and reports
answeredNow(what this pass added). - The button prints the result in chat.
- The walk now also records
sourceTypefromC_TradeSkillUI.GetRecipeInfo, labelled withBATTLE_PET_SOURCE_<n>, the same numbers and labels Blizzard's own Sources filter uses (Blizzard_Professions.lua:1255-1272). The operator's SavedVariables showGetRecipeInfoanswered for all 2,496 unlearned recipes with no window open, whileGetRecipeSourceTextanswered none. The walk was already calling it and threw that field away.retailKnownkeepssourceTypetoo. A recipe counts as answered when it has either a source type or source text, and is never counted twice. - Forever gives no source at all -- measured. The operator's field dump for unlearned
recipe 7928 (2026-09-28) lists every
GetRecipeInfokey: nosourceTypeor anything like it, andGetRecipeSourceTextreturned nil. The forever tree'sTradeSkillUIDocumentation.luahas no other source call, only the window's source-type filter, which would mean writing the window's filters (see above). The walk now keepsskillLineAbilityIDper recipe (4414 for 7928), the client's SkillLineAbility row, so an offline build can join it to how the recipe is learned, and counts those aswalk.linked. A link is not counted as an answer. When nothing answers, the chat line now says the client gives no recipe sources and how many links were saved, instead of printing the sample dump.
Not verified in game, apart from the Forever field dump above. Location:
Modules/ProfCapture/ProfCapture.lua(CaptureKnown,OnEvent,WalkRecipeSources),GUI/ProfCaptureTab.lua. Spec:Tests/profcapture_spec.lua.- Capture never writes the profession window's filters. I first forced "Show learned" and
"Show unlearned" on, the way the trainer's are, and that locked the WoW Forever profession
window (K) so nothing in it could be clicked (operator, 2026-09-28; it cleared with TOGTools
disabled). Cause: Forever's
Item DB walk stopped at ID 240,000, below WoW Forever's items. ItemDB inbox thread 500e4ce2: Forever (build 1.60.1.69913) has 4,862 item IDs above 240,000, up to 286,427.
DEFAULT_TOP_IDis now 320,000, andraiseTopIDlifts a storedtopIDbelow it on everyStartandFillGaps, so an existing walk (stored 240,000, cursor 240,001, even marked complete) resumes from its cursor into the new range instead of keeping the old ceiling.FillGapsraises before placing the cursor past the ceiling, so it still skips the walk. The comment claiming the ceiling was "editable via the tab" was wrong (the tab only displays it) and is replaced. Location:Modules/ItemDB/ItemDB.lua. Spec:Tests/itemdb_spec.lua("walk ceiling").wow-version-replication.ps1could delete a live file from every replica install. Peer review259f99e3(TOGProfessionMaster): a save-by-replace raises Deleted and Created/Changed for one save, in either order, and when Deleted ran lastSync-Fileremoved the file from every other WoW install while the source still had it. A delete is now honoured only when the source file is really gone; otherwise it is copied as a change. Dev-only; the script does not ship. Location:wow-version-replication.ps1(Sync-File).
[v0.9.3] (2026-09-27) - Gratz: sister-guild level-ups, and no more roster polling
New Features
- Guild level-up profiles also fire for sister-guild members. Operator request, 2026-09-27.
LibGuildRoster (already a required dependency in all seven TOCs) reports sister members' level-ups
from its periodic
/whosightings, sister roster copies and groupedUNIT_LEVELasOnSisterMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source). Gratz routes it throughfireGuildLevelUp, so the level band, every-Nth-level interval, cooldown, skip-self and channel rules apply unchanged;[guild]is the sister guild's name (the key'sFaction-prefix stripped). The line goes to our guild chat and crosses to the sister guilds through the existing GreenWall relay. Location:Modules/Gratz/Gratz.lua("Guild level-ups",onMemberLevelChanged,guildNameFromKey). - "Sister guilds" checkbox on Level up (guild) profiles. Stored as
profile.sisterGuilds;nilmeans on, so profiles saved before this release get it, and new profiles default to on. Unticked keeps a profile to the home guild. Location:GUI/GratzTab.lua,Gratz:NewProfileDefaults.
Improvements
- The 60 s guild roster poll is gone. Gratz asked the server for the whole roster every 60 s
(
C_GuildInfo.GuildRoster) and compared every row while a guild level-up profile was enabled; the operator called that too expensive for one addon. Home-guild dings now come from LibGuildRoster'sOnHomeMemberLevelChanged(same signature), which is free: every library user's ownPLAYER_LEVEL_UPannounced on the guild addon channel ('comm'), plusUNIT_LEVELon a grouped guildmate ('unit'). GuildRoster searched every flavour tree and the web and found no server message announcing another player's level. Removed:scanGuildLevels,guildLevelUpQualifies,requestGuildRoster,guildTrackerArmed, theGUILD_ROSTER_UPDATEregistration and the login ticker. Baseline, online-at-both-readings and cross-source dedupe are now the library's rules (itsTests/sisterlevel_spec.lua); the Gratz specs that pinned them for the old scan went with it. Contracts2f198123and9c178c96. - A second guard behind the library's: a decrease, no change or a malformed call says nothing, and a home-guild key arriving on the sister callback is refused, so the two can never gratz one person twice.
wow-version-replication.ps1now replicates into WoW Forever._classic_beta_(Battle.net productwow_classic_beta, which readsTOGTools_Camelot.toc) added to the version list, and a destination is kept when the version folder exists rather than itsInterface\AddOnsfolder: a fresh client has none yet, so the old test skipped Forever silently. The AddOns folder is created with the addon folder. Verified with-DryRun(Forever listed as a target) and by the live watcher filling_classic_beta_\Interface\AddOns\TOGTools. Dev-only; the script does not ship.- Addon-list title in the brand colours, on all seven TOCs. Fleet request
ff41f4c4(DeltaSync, at the operator's direction). The version tag used|c999999FF, which is alpha-first (a pale blue, not grey), and neither colour code was closed. Now|cffFF8000TOG Tools|r |cff999999<Version>|r. A newTests/toc_spec.luacase pins the exact title per flavour. Not yet seen in the client's AddOns list. Locations: every.toc,Tests/toc_spec.lua.
Bug Fixes
- Guild Log read Blizzard's event buffer twice per query on Classic.
requestQueryarms a 1.5 s fallback ingest for Retail, whereGUILD_EVENT_LOG_UPDATEno longer fires; on Classic the event arrives too, and both ran, so every query re-hashed all 100 buffer entries twice (the identical pairs every ~30 s in the operator's 2026-09-27 debug trace). The event handler now cancels the pending fallback before ingesting. Location:Modules/GuildLog/GuildLog.lua. Specs:Tests/guildlog_spec.luacounts ingest passes on both paths; the Classic spec was run red with the cancel line removed (1 failed) and green with it restored. /togt glcheckwrote to the saved Guild Log.GuildLog:Diagnose's instrumented ingest walktinserted every new buffer row into the bucket, with noid, unsorted and unpruned, and the next real ingest would not re-sort them (it sorts only when it appends). The walk now counts what the real ingest would append ("would append / deduped") and writes nothing. Location:Modules/GuildLog/GuildLog.lua. Found by the self-audit's coverage pass.- One level-up could post twice in the same channel. Since LibGuildRoster MINOR 27 a grouped
guildmate's ding reaches the party trigger (Gratz's own
UNIT_LEVEL) and the guild trigger (the library's'unit'source) at once, so a party profile and a guild profile both set to Guild chat said it twice.sendToChannelsnow keeps one line per (player, level, channel) for the session, first profile wins; profiles aimed at different channels still each post. A repeated identical ding is therefore no longer re-gratzed after its cooldown. Peer review, inbox thread0f319d11. Location:Modules/Gratz/Gratz.lua(_dingSent,tryFire'sdingKey). The spec for profile order and two cooldown specs moved to the achievement trigger, which the rule does not touch. - Guild Log line coverage 53% -> 100% (437/437): specs for
/togt gldump,/togt glcheck, the default-rank lookup, the login query,Requery, theC_Timer.Afterfallback and the sub-tab refresh.Gratz.lua404/404. Full suite 1126 passed, 0 failed (desk, 2026-09-27, after the TOC title spec).
Known limits
- Needs LibGuildRoster MINOR 27 (GuildRoster 1.1.0), released before this. On an older
library neither callback fires and guild level-up gratz go quiet; registering an unfired name is
harmless under CallbackHandler. Seen in game 2026-09-27 (Classic Era): a home guildmate's ding
to 15 was gratzed in guild chat. Which library source fired it was not captured (the order
against GRM's own announcement proves nothing: two pollers on different schedules); with debug on,
every callback now prints
[gratz] home|sister level-up: ... (source=...)before any filter. Sister-guild dings, the grouped path, the "Sister guilds" checkbox and the no-library-addon re-check are not yet seen in game. - The 60 s re-check moved INTO LibGuildRoster, shared by every addon (operator, 2026-09-27,
contract
7e447fb6). The library re-reads the home roster about once a minute while any addon holdsRequestLevelRefresh, keepsmember.levelcurrent for every member, and firesOnHomeMemberLevelChangedwith source'roster'under the same rules. Gratz holds it exactly while the module is on and a guild level-up profile is enabled:Gratz:SyncLevelRefresh, called fromOnEnable,OnModuleToggled,SaveProfileandDeleteProfile(the tab's form and "On" checkbox both go throughSaveProfile). Feature-detected, so an older library is a no-op. So a guildmate who runs no library addon is still seen, as the old poller saw them, at one request per client however many addons ask. - Sister dings can be minutes late (the library's
/whocadence), or missed if the member logs off first. Seen in game 2026-09-27: a sister member's ding to 40 was never reported, because no reading of them preceded it (a first reading is a baseline). LibGuildRoster MINOR 27 now forwards a player's own ding to the sister guilds throughGreenWallAPI.SendMessage(version 2) and firesOnSisterMemberLevelChangedsource'comm'on receipt (contract58cbd95c), so a sister member running the standalone GuildRoster addon plus GreenWall 1.14.0+ is gratzed at once. Not verified in a client. - Sister gratz volume is untested. Every TOGTools user with a guild level-up profile gratzes a
sister-guild ding, and existing profiles have "Sister guilds" on. Peer review (thread
0f319d11) asks for one in-game check with two TOGTools users online; the player notes say it can be switched off.
[v0.9.2] (2026-09-25) - Addon Load names the missing dependency and shows each addon's .toc
New Features
- "Dep Missing" now names the dependency. A row the client refuses with a
DEP_*reason readsDep Missing: LibX, LibY, listing every required dependency that is not installed or will not load. Until now the tab only said that something was missing.AddonLoad:GetDatareads each addon's## DependenciesthroughC_AddOns.GetAddOnDependencies(the call Blizzard's own AddOn list tooltip uses,Blizzard_AddOnList/AddonList.lua:849in the Era tree) and resolves each one:C_AddOns.DoesAddOnExistfalse -> "Not installed";IsAddOnLoaded-> "Loaded"; otherwise the client's reason fromGetAddOnInfo(dep)(Disabled, Wrong Version, ...). A dependency that is installed, loadable and not yet loaded (load-on-demand) counts as fine. Location:Modules/AddonLoad/AddonLoad.lua(depState,collectDeps). - Hover a row to see its .toc. An addon cannot open a
.tocfile, so the tooltip reads it back through the client: title, folder name,## Notes, status,## Versionand## Author(C_AddOns.GetAddOnMetadata), the interface number (C_AddOns.GetAddOnInterfaceVersion), and the required and optional dependency lists (GetAddOnOptionalDependencies), each dependency coloured green or red with its state. Wired through RowList'sonRowEnter/onRowLeave, which the translator passes to LibAceGUIWidgets unchanged. Location:GUI/AddonLoadTab.lua(showRowTooltip).
Improvements
- Every new API is feature-detected on
C_AddOns. A flavour without the dependency calls shows the bare "Dep Missing" label and an empty list, never an error.GetAddOnInfoon a dependency name ispcall'd: whether it raises or returns for an uninstalled name is not verified on every flavour, so a raise, or aMISSINGreason, reads as "Not installed". - Sort priority keys on the bare label, so
Dep Missing: LibXstill sorts with the failures. Tests/addonload_spec.luacovers the module at 149/149 lines. Eleven dependency specs, plus three for paths no spec had run before: the module's ownADDON_LOADED/PLAYER_LOGINtiming capture used when!TOGTis absent, andRefreshMemory. Not verified in a client.
[v0.9.1] (2026-09-25) - LibGraph-2.0 and LibDBIcon-1.0 become required dependencies instead of embedded copies
Improvements
- LibGraph-2.0 is now the standalone "LibGraph-2.0 Revived" addon, not a vendored file. All
seven TOCs add
LibGraph-2.0to## Dependenciesand stop loadinglibs\LibGraph-2.0.lua, which is deleted..pkgmetaaddslibgraph-2-0-revivedtorequired-dependencies, so CurseForge installs it alongside TOGTools; that slug is the one FastGuildInvite already ships with. The TOC line decides whether TOGTools loads and the.pkgmetaline decides what gets installed, so both are needed. - Why the embedded copy was wrong, not just duplicated. LibGraph finds its line and bar
textures by parsing
debugstackfor the directory its.luasits in, and expects the.tgafiles beside it (LibGraph-2.0/LibGraph-2.0/*.tgain the standalone addon). TOGTools shipped the.luaflat inlibs\with no textures at all. The standalone copy also carries the maintained fixes and a minor (92069) that out-ranks older embedded copies.GUI/LogGraph.luareads the library throughLibStub("LibGraph-2.0")at file scope and needs no code change. - LibDBIcon-1.0 (and the LibDataBroker-1.1 it carries) is a required dependency on six of seven
flavours. The Era, TBC, Wrath, Cata, Mists and Mainline TOCs add
LibDBIcon-1.0to## Dependencies, drop the## OptionalDeps: LibDataBroker-1.1, LibDBIcon-1.0line and stop loadinglibs\LibDataBroker-1.1.lua/libs\LibDBIcon-1.0.lua;.pkgmetaaddslibdbicon-1-0torequired-dependencies. The standalone addon (Funkeh, v12.0.3) loads LibStub, CallbackHandler-1.0 and LibDataBroker-1.1 from its ownembeds.xml, so one dependency covers both librariesGUI/MinimapButton.luaasks LibStub for. Why: an embedded copy only reaches the players of the addon that shipped it, so every LibDBIcon update meant a release of every addon with a minimap button; as a CurseForge dependency it updates without one. Kept against: the embedded copy's zero install cost.TOGTools_Camelot.tocstill embeds both files, because the standalone's TOC lists no16001and an out-of-date required dependency would keep TOGTools from loading on WoW Forever (inferred from how the client treats out-of-date addons, not observed on a Forever client). The embedded copy was already MINOR 56, the same as upstream v12.0.3, so it was not refreshed. .luarc.json(TOGTools and !!TOGT) setsdiagnostics.libraryFilesanddiagnostics.ignoredFilestoDisable, so vendored code inlibs\stops filling the Problems panel whenever one of its files is open in a tab. TOGTools'workspace.libraryalso points at the sibling../LibGraph-2.0/LibGraph-2.0folder soLogGraph.luakeeps its type information.GUI/MinimapButton.lua: the unused_selfargument of the LDBOnClickis now_.Tests/toc_spec.lua:INTENTIONAL_OMISSIONSlistslibs\LibDataBroker-1.1.luaandlibs\LibDBIcon-1.0.luafor every flavour except Forever, so the cross-flavour parity check pins the Forever-only embed instead of failing on it.- Docs:
README.mdanddocs/Curseforge_Description.htmllist LibGraph-2.0 and LibDBIcon-1.0 under Requirements (LibDBIcon auto-installed everywhere but Forever, which keeps a built-in copy). They no longer call LibDataBroker/LibDBIcon optional. Smack's big-hit trigger now notes environmental damage, a detail kept from the v0.8.6 notes that roll off Recent Updates.
[v0.9.0] (2026-09-24) - Profession Capture developer tool; every table now drawn by LibAceGUIWidgets
New Features
Profession Capture (developer tool). Built for ProfessionDB (LibProfessionDB-1.0), inbox contract
e2868011. The client's DBC says which recipes exist; only a live client standing at the NPC can say which ones a player can obtain, and where. A walker turns on Capture in the newProfession Capturetab (Developer Tools only), visits trainers and recipe vendors and opens their own profession windows. The SavedVariables go back to ProfessionDB. NewModules/ProfCapture/ProfCapture.luaandGUI/ProfCaptureTab.lua, listed in all seven TOCs. Passive: it reads on window events (TRAINER_SHOW/TRAINER_UPDATE,MERCHANT_SHOW/MERCHANT_UPDATE,TRADE_SKILL_SHOW/TRADE_SKILL_UPDATE/TRADE_SKILL_LIST_UPDATE,CRAFT_SHOW/CRAFT_UPDATE,LOOT_OPENED) and sends no request of its own. Gated three ways:devTools, the module toggle (IsModuleEnabled("profcapture")), andprofCapture.capture, which defaults off.enabledstays on by default becauseMainWindowhides a disabled module's tab, and that would hide the switch.- Two API shapes, by
gameVersion.isRetail. Read from the Blizzard trees underF:\Blizzard API Docs. On the Retail client, including WoW Forever,GetTrainerServiceInforeturnsname, type, texture, reqLevel, subText, category(wow-ui-source-foreverBlizzard_TrainerUI/Mainline/Blizzard_TrainerUI.lua:425), and the spell id comes fromC_TooltipInfo.GetTrainerService(i).id. On every Classic client it returnsname, subText, type, isExpandedwith header rows. That order is the same in the classic_era, classic_anniversary and classic (Mists) trees. The spell id comes from a hiddenGameTooltip'sSetTrainerService+GetSpell. Vendors useC_MerchantFrame.GetItemInfoorGetMerchantItemInfo. Recipe lists useC_TradeSkillUI(GetFilteredRecipeIDs,GetRecipeInfo,GetRecipeSchematic) orGetTradeSkillInfo/GetCraftInfo. - The trainer filter. On show, every service filter (
available,unavailable,used) is turned on and the player's own is saved;TRAINER_CLOSEDputs it back. A read is kept only when all three are on, so a filtered list never overwrites a complete one. If Blizzard's trainer frame re-applies its own filter afterTRAINER_SHOW, the nextTRAINER_UPDATEturns ours back on and the re-list that follows is read. There is no timer. - Not verified in a client. Nothing here has run in game. Unverified points: that
SetTrainerServiceTypeFilterstill applies afterTRAINER_CLOSED; what an enchant'sGetCraftItemLinkcarries on Classic Era (Era's craft frame calls onlyGetCraftItemLink, neverGetCraftRecipeLink--Blizzard_CraftUI/Vanilla/Blizzard_CraftUI.xml:571-574-- so the spell id is read from it as a fallback, and a recipe with neither lands inunkeyed; Era's trade-skill frame does callGetTradeSkillRecipeLink,Blizzard_TradeSkillUI.xml:11); and thatGetFilteredRecipeIDsreturns every row while the player's profession-window filters are set. It honours those filters, so a filtered list is a partial list.
- Two API shapes, by
Recipe-source walk: the NPC-free route, Retail client only (WoW Forever included). A Walk recipe sources button in the tab.
ProfCapture:WalkRecipeSources()takes every recipe spell id LibProfessionDB ships (GetProfessions/GetRecipes). For each one it asksC_TradeSkillUI.GetRecipeSourceText(id): the text the Professions window shows under an unlearned recipe (Blizzard_ProfessionsRecipeSchematicForm.lua:632-638in the forever and live trees). The pass is synchronous: local calls, no timer and no travelling. Result:profCapture.walk = { recipes = { [spellID] = { prof, learned?, source? } }, total, answered, build, locale, ts }. The same text is also recorded during a recipe-list capture, assourceon an unlearned recipe and asnextRecipeID+nextRankSourceon a learned one.ProfessionDBis an OptionalDep of the Camelot and Mainline TOCs so it loads first. No Classic client has this call. The writblizzard-apiindex finds it only in the forever and live trees, and the classic_eraTradeSkillUIDocumentation.luahas no recipe-info or source function. So Classic has no walk. Not verified in a client: whether the call answers for a profession the walking character lacks, or with no profession window open. The walk'sansweredcount settles that on the first click. Specs: 5 more examples, covering an unsupported client, ProfessionDB missing, no store, a full walk with counts and purge, and the source text in a recipe-list capture. The Profession Capture tab shows the walk's "answered" line only on a client that has the call, since "0 of 0" on Classic read as a failed walk.SavedVariables shape, schema 1 (
TOGTools_DB.global.profCapture), for ProfessionDB's parser. Every record carriesbuild("<version>.<build>"fromGetBuildInfo, e.g.1.60.1.69977),localeandts(server time). Fields shown?may be absent.profCapture = { enabled = true, capture = false, trainers = { [npcID] = { npcID, name?, zone?, subzone?, mapID?, faction?, trainerType?, rank?, maxRank?, collapsedHeaders?, -- Classic only: how many headers were collapsed (rows hidden) seen, -- how many complete reads this NPC has had build, locale, ts, services = { { -- array, in the trainer's own order spellID?, name, subText?, category?, type, -- "available"|"unavailable"|"used" reqLevel?, skill?, skillRank?, cost?, abilities? = { "<ability name>", ... }, }, ... }, } }, vendors = { [npcID] = { npcID, name?, zone?, subzone?, mapID?, faction?, seen, build, locale, ts, recipes = { [itemID] = { -- Recipe-class (9) items only name?, price?, stack?, numAvailable?, -- nil = unlimited stock extendedCost?, } }, } }, known = { ["Name-Realm"] = { [professionID or profession name] = { name, skill, maxSkill, professionID?, -- professionID on Retail childProfessionID?, childSkill?, childMaxSkill?, collapsedHeaders?, build, locale, ts, recipes = { [spellID] = { name, learned, difficulty?, -- "optimal"|"medium"|"easy"|"trivial" itemID?, minMade?, maxMade?, numSkillUps?, categoryID?, source?, -- Retail: unlearned recipe's source text nextRecipeID?, nextRankSource?, -- Retail: learned recipe's next rank reagents = { { itemID, count }, ... }, } }, unkeyed? = { <recipe as above>, ... }, -- Classic rows with no readable spell id } } }, drops = { ["<itemID>:<sourceKind>:<sourceID>"] = { itemID, sourceKind?, sourceID?, -- from the loot source GUID, e.g. "Creature", 570 count, first, zone?, subzone?, mapID?, build, locale, ts, -- ts = last seen } }, walk? = { -- Retail only; the latest Walk recipe sources pass, replaced whole total, answered, build, locale, ts, recipes = { [spellID] = { prof, learned?, source? } }, }, }Two limits the parser should know. A trainer or vendor record is replaced whole on each complete read, so it holds the latest visit rather than a merge. Service
typebelongs to the character who opened the trainer. On Classic a service whose tooltip yields no spell id (Classic Era'sGetSpellon a trainer-service tooltip is not verified) is still recorded, withoutspellID: a raisingSetTrainerServiceorGetSpellis caught. Specs:Tests/profcapture_spec.lua, 34 examples covering both API shapes, the filter handling, the gate, a failing spell-id tooltip, vendors, both recipe frames, drops, the walk, and the event wiring. Full suite: 1075 passed, 0 failed;ProfCapture.lua376/376 lines covered.
Improvements
Every list now draws through LibAceGUIWidgets. The eleven tabs' tables (the six logs, Diagnostics, Item DB, Addon Load, Gratz, Smack) are drawn by LibAceGUIWidgets-1.0's
RowList, MINOR 34. The library added what they relied on at TOGTools' request (inbox threadc571dee6):- the header sort/filter menu, with cascading filters,
filterGroupand a cap; SetSearch,autoFitand hyperlinks in cells;Detachfor AceGUI's recycled parents;col.align,col.gapBeforeandcol.sortDescDefault.
GUI/RowList.luawent from ~1100 lines to a translator that maps the options the tabs were written against onto the library's, so no tab changed:format(entry)becomesformat(value, entry);sortKeybecomessortValue;- a RIGHT/CENTER
justifybecomesalign; headerTipDescbecomes the library'sheaderTip;tiebreakKeybecomes atiebreak(a, b)that checksname, then the key, then falls through to SetData order.
addon.UI.OpenColumnMenuinGUI/UI.luais removed, because nothing else called it.LibAceGUIWidgetsis a hard dependency in all seven TOCs (Tests/toc_spec.luapins it). Release order: this version must not ship before LibAceGUIWidgets MINOR 34 is published. The operator has settled that it is released first. As a backstop,RowList.LibraryIsCurrent()checksLibStub.minors['LibAceGUIWidgets-1.0'] >= 34. On an older library the first list built prints one chat line saying LibAceGUIWidgets is out of date, instead of silently drawing broken tables.Hyperlinks are opt-in per list, not on for every list. A link click also runs the row's own
OnMouseDown, so a list withonRowClickcould open its row menu and the chat-name menu together (Peer Review, audit threadcf6cc085). The six lists whose cells render item or player links passhyperlinks = true: Gathering, Guild Bank, Mail, Trade, Whisper and Item DB. Gratz, Smack, Diagnostics, Addon Load and Guild Log do not. The Gathering Log'ssortType = "date"option was removed; nothing reads it now.Visible differences:
- headers are the library's gold rather than TOGTools' orange, because the accent is session-global and TOGTools does not set it;
- the menu reads "Sort ascending/descending" rather than "Oldest > Newest";
- sorting is numeric-aware and case-insensitive;
- a search also scrolls the list to the top.
Specs:
Tests/rowlist_spec.luais rewritten. It pins every translation rule, and it runs the old tiebreak guarantees (findings rev-B and rev-J) end to end through the real library. A formatter's nil still draws as a blank cell rather than "nil" (the librarytostrings the result, and the translator maps nil to""). Full suite: 1075 passed, 0 failed, against LibAceGUIWidgets' working tree as it stood after Peer Review's fixes (threadaf2ebd61);GUI/RowList.lua56/56 lines covered. Not verified in a client: nothing has been drawn in game on LibAceGUIWidgets MINOR 34 yet.- the header sort/filter menu, with cascading filters,
The seven TOCs read as four blocks. Libraries, core, shared GUI and modules now sit under
#comment headings. The modules block no longer has a blank line between each engine and its tab. Load order is unchanged (Tests/toc_spec.luapins it across all seven).Test harness pin moved to WoWAPITesting
3d492c7(from7932484, ten days of Adoption-log entries).Tests/itemdb_spec.luadrops its ownstrsplitstand-in, because the harness'sstrsplitnow keeps empty fields and honours the piece limit as the client does. The spec-localIsInGroup/UnitInParty/UnitHealth/CombatLogGetCurrentEventInfoassignments in the Gratz, Smack and IdHitThat specs stay: they drive per-test scenarios, not a missing API. Full suite on the new pin: 1075 passed, 0 failed.Retail TOC declares
110207, 120100(was110207, 120008). LibAceGUIWidgets is now a hard dependency, so every Interface number TOGTools declares must be one the library declares too, or a client on that build skips the library as out of date and TOGTools with it. The library lists120100and never listed120008; LibAceGUIWidgets found120008in no TOC but TOGTools' own and no 12.0.8 patch anywhere, with 12.1 (120100) the current Retail patch (inbox threadc571dee6).Tests/toc_spec.luastill pins one 11.x floor and exactly one 12.x build. Not verified: this box does not record the Retail client's build, so120100is the library's evidence, not a reading of the installed client.
Documentation
- README and CurseForge description brought to parity. Both now describe the same features in the same detail. The README gains what only the CurseForge page had: the Name Prefix GRM duplicate-name toggle, the per-log details (Take-All merge, C.O.D., trade enchant slot, guild and bank history kept past the game's own window, date presets), Gratz placeholders and cooldown, Smack's channels and cooldown, I'd Hit That's layout options, and the Addon Load status badges. The CurseForge page gains what only the README had: a Settings section and the Developer Tools commands. Both gain a Developer Tools section (Diagnostics, Item DB, Profession Capture) and list LibAceGUIWidgets v0.2.4 (MINOR 34) under Requirements.
[v0.8.9] (2026-09-23) - Sister-guild lines cross at once through GreenWall 1.14.0's relay; older GreenWalls wait for a key press
New Features
- GreenWall 1.14.0+ relays Gratz and Smack lines with no key press. At TOGTools' request
(inbox contract
898b1e4c, the operator's directive that it be an API for ANY addon), GreenWall 1.14.0 addedGreenWallAPI.SendChat(addon, text, chatType)(API.lua,GreenWallAPI.version == 2). It carries the line as addon messages: a WHISPER to one GreenWall user per co-guild, who re-posts it as a GUILD/OFFICER addon message inside their guild. Classic has no CHANNEL addon messages, and SendAddonMessage has no hardware restriction. NewrelayThroughApiinCompat.luacalls it frombridgeThroughGreenWallwhengw.configexists and the API is version 2+, and returns before the key-press queue. It does not post to the sender's own guild, so the real guild send inaddon.Chat.Sendstays. The result table is read:okwithtruncatedprints "was cut short";no-peersis debug-only, because no co-guild member who could have seen it is online;not-configured,refused,too-long,secretand any unknown reason are printed with the quoted line, like every other lost relay.pcall'd, so a raise prints "GreenWall reported an error" and never takes down the handler that already spoke in guild. Older GreenWalls (no API, or version 1) fall through to the unchanged key-press queue below.Compat.luanow namesaddonNamefrom...for the API's addon-id assert. Specs:Tests/gratz_spec.lua"on GreenWall 1.14.0+ (GreenWallAPI version 2)", 10 examples against a stand-in of the documented contract: fire-time relay with no key press and no CHANNEL send; OFFICER; no age drop; silentno-peers; each refusal reason told; truncation told; a raising SendChat; not calling it beforegw.config; version-1 fallback; party/raid/say not bridged. Full suite: 1037 passed, 0 failed. GreenWall 1.14.0 shipped (GreenWall commit3893010); its shippedAPI.luaandRelay.luawere re-read before release and match the stand-in:version = 2, theSendChat(addon, text, chatType)signature and the five reason strings. Not verified in a client: no whisper-then-guild round trip has been seen in game.
Bug Fixes
Gratz and Smack lines never reached the sister guilds: the v0.8.8 GreenWall relay was dropped by the client every time. I built the v0.8.8 bridge on GreenWall's channel send (
gw.config.channel.<guild|officer>:send), which ends inSendChatMessage(segment, "CHANNEL", nil, number)(GreenWallChannel.lua:366-370). A numbered CHANNEL send is hardware-event restricted indoors and outdoors (warcraft.wiki.ggAPI_SendChatMessage), so an event-driven Gratz or Smack line was lost silently. A typed line works only because GreenWall'sParseTexthook runs inside the Enter key press. Addon messages on a custom channel are no way round it: the wiki says they are disabled on Classic for exactly this reason.Fix:
bridgeThroughGreenWallno longer sends. It queues{msg, chanType, key, t}and shows a hidden relay frame built atCompat.luaload; the frame'sOnKeyDown(flushHeld) sends every held line through GreenWall inside the player's next key press. GreenWall's send is synchronous while connected (Channel.lua:277-382), so the CHANNEL send happens inside that press. The frame callsSetPropagateKeyboardInput(true)beforeEnableKeyboard(true)so every key still reaches the game; propagation is combat-restricted, so a load in combat (a /reload mid-fight) arms the frame onPLAYER_REGEN_ENABLEDand lines wait until then. Lines older than 120 s are dropped rather than sent late; at most 5 are held, oldest dropped. Either drop is printed to the player throughaddon.lib:Print(reportRelayProblem), quoting the line and the reason, because a relay that never arrives looks identical to one that did from the sender's own chat, so a debug-only drop was invisible to the one person who could notice it (peer review, 2026-09-23). GreenWall is resolved again at send time, and each held line goes throughsendIfConnected, which asks the channelis_connected()(GreenWallChannel.lua:125-138, a freshGetChannelNameeach call) before sending. That check is load-bearing: a GreenWall that is loaded but off its bridge channel does not refuse a send, it queues the segment and returns 0 without raising (Channel.lua:325-326, 352-381), and the line would then go out on a later flush outside any key press, where the client drops it. A disconnected channel, missing channel objects, and a GreenWall error caught by thepcallare each printed throughreportRelayProblemwith their own reason. The error notice says the line may not have reached the sister guilds rather than that it was not passed:tl_flushdrains its queue FIFO (Channel.lua:354-376), so a raise on a segment GreenWall queued earlier leaves ours queued, and GreenWall's next send can still deliver it (peer review, 2026-09-23). GreenWall also cuts a long line rather than splitting it: the segmentopcode#guild_id##messagegoes throughstrsub(..., 1, GW_MAX_MESSAGE_LENGTH)(Channel.lua:322; 255 inConstants.lua:79), so the tail is lost with no error. A typed line is cut the same way;greenWallCutsLinenow tells the player when a relayed line was cut short, since the home guild saw it whole. That notice goes through the same printer, so it is namedreportRelayProblemrather than for the not-relayed case alone. Location:Compat.lua.Not verified in a client: that an insecure
OnKeyDowncounts as the hardware event for a CHANNEL send on Era, and that the frame still receives keys in combat. Needs an in-game test.Specs:
Tests/gratz_spec.lua"the GreenWall bridge", rewritten. The v0.8.8 specs passed because their GreenWall stand-in recorded the call and never asked the client whether it would deliver it. The stand-in now makes GreenWall's real CHANNEL send through the harness chat gate, and carriesis_connected()plus GreenWall's queue-without-raising behaviour when disconnected: one example pins that a send with no key press lands inwow.dropped, and the rest pin hold-at-fire, relay-on-key-press, relay-once, officer routing, the non-bridged types, GreenWall absent, no send function, a GreenWall error, 120 s expiry, the 5-line cap, GreenWall gone at press time, GreenWall disconnected at press time (nothing relayed and nothing left in its queue), a GreenWall with nois_connected(still relayed: the feature-detect fallback), a line one character over GreenWall's limit (relayed, and the player told it was cut; one at the limit is not reported), and the in-combat load arming on regen; the expiry, cap, error, gone, disconnected and an Officer-line example also assert the player-facing notice, and the error example asserts it does not claim the line was not passed.newWorldexposes the relay frame (frames[beforeCompat + 1]) andw.press().A guild line sent while GreenWall was loaded but not yet set up was dropped from the relay without a word.
bridgeThroughGreenWallreturnedfalsesilently whenevergreenWallChannel(key)orrelayFramewas nil. GreenWall'sgwglobal exists from itsGlobals.lua:36, butgw.configis only built in itsADDON_LOADEDhandler (GreenWall.lua:294), so a Gratz or Smack line in that window was never held and never reported -- the one silent path left after every other relay failure was made visible. Now silence is kept only for GreenWall not installed (_G.gwnil); otherwise the line goes throughreportRelayProblemand nothing is held. One reason per condition: no relay frame (noCreateFrameat load) is "the key-press relay could not be set up", which is TOGTools' failure and not GreenWall's; "GreenWall was not ready yet" only whengw.configis missing; a config lacking the channel orGW_MTYPE_CHATis a GreenWall whose shape changed, reported as "GreenWall is not available", since waiting would not fix it and "not ready" would repeat on every line. Per the peer-review ruling on threadc54b54b4: fix it, do not measure the window. Location:Compat.lua. Specs:Tests/gratz_spec.lua"tells the player when GreenWall is loaded but not set up yet, and holds nothing", "does not call a set-up GreenWall with a changed shape 'not ready'" and "tells the player when there is no relay frame, without blaming GreenWall" (newWorld({ noRelayFrame = true })hidesCreateFramewhileCompat.lualoads).The relay limits were restated in two test names and nothing tied the CurseForge page to them. Two specs were named "older than two minutes" and "at most five lines" -- a name is compared to nothing, so it would drift from
RELAY_MAX_AGE/RELAY_MAX_HELDsilently (peer review, thread5ca12a4d).Compat.luanow publishes both asaddon.Chat.RELAY_MAX_AGEandaddon.Chat.RELAY_MAX_HELD; the specs are named after the constants and derive their time advance, line count and expected "within N minutes" text from them. A new example, "the CurseForge page states the relay limits the code enforces", readsdocs/Curseforge_Description.htmland asserts its "after two minutes" / "at most five" wording matches the constants, so changing either side fails the suite. This entry restates the values on purpose: a dated record must not move with the code. Also added "still relays a line held exactly RELAY_MAX_AGE", pinning that the age check is strict (>). Coverage ofCompat.lua(harnesscoverage.lua): every relay line is covered; the 47 uncovered lines (67-83, 601-681) are AceGUI widget helpers, which need the client.README.md(which ships) also said "two minutes" and nothing pinned it (peer review, thread6d00c25e). It keeps the figures, because a player reading it from the AddOns folder may not have the page open, and the page example (renamed "the CurseForge page and README state the relay limits the code enforces") now asserts the same wording in both files (peer review, threade3c168f4). README still points at the page's "Sister guilds hear it too" bullet for the notices, and the example asserts that bullet exists. The example also accepts "one minute" for a 60 s limit. The no-relay-frame notice is defensive only -- Ace3'sAceEvent-3.0.luacallsCreateFrameat file scope whenever Ace3 runs at all, so a client without it errors in Ace3 first -- which is now commented at the guard (citing the Ace3 line by its text, not a line number) and is why the page does not list it.
Documentation
- The v0.8.8 entry below says Smack "stays omitted" from
TOGTools_Camelot.toc. That was never true: the same entry's Smack fix lists Smack in both_Mainlineand_Camelot, and the shipped Camelot TOC loads it. Smack runs on WoW Forever. Left in place because released entries are not edited; corrected here.
[v0.8.8] (2026-09-20) - Gratz and Smack guild lines now cross GreenWall to the sister guilds; WoW Forever support
New Features
WoW Forever ("Camelot") support: a seventh TOC, and a flavour derivation that knows the retail client can report a Classic-band number. From FastGuildInvite's onboarding notes, inbox thread
7480082e. Forever (Battle.net productwow_classic_beta) is the RETAIL client --WOW_PROJECT_ID == WOW_PROJECT_MAINLINE, retailC_*namespaces, Menu API, secret values, the live tree -- with vanilla content at## Interface: 16001. The client reads<Addon>_Camelot.tocbefore_Mainline, soTOGTools_Camelot.tocisTOGTools_Mainline.tocwith the Interface line and Title changed (Smack stays omitted: same client, same 12.0+ block).The TOC alone would have been WRONG for this addon, and this is the part FGI's note could not know:
Version.luaderives every flavour flag fromselect(4, GetBuildInfo()), and 16001 sits inside the Vanilla band -- so on ForeverisVanillawould be true andisRetailfalse, routing every API branch down the Classic path on a client that has none of those APIs.addon.DeriveGameVersion(tocVersion, isMainlineClient)now takes the project-id test as a second argument (passed in, so the function stays pure): a mainline client under 100000 isisForever = true,isRetail = true,isRetail120Plus = true, every Classic flag false. The same number from a Classic client stays Vanilla, and a real retail number never becomes Forever.isRetail120Plusis true there because Forever runs the 12.x codebase; not verified in a Forever client (none on this box, andF:\Blizzard API Docshas no Forever tree) -- the cost of being wrong that way is a skipped automated /say, where the other way is a secret-value error.WOW_PROJECT_ID/WOW_PROJECT_MAINLINEare declared in.luarc.jsonand.luacheckrc, verified present on every Classic tree (Blizzard_FrameXMLBase/<Flavour>/Constants.lua).!!TOGTships its own_CamelotTOC in the same pass (its v0.2.3). TOGTools hard-depends on it, and without one the client fell back to!!TOGT.toc(no 16001), so TOGTools could not load on Forever at all. The file was first sent to!!TOGT's inbox (thread0a9abbb39cb5) because writ refused a write into that repository from this session; the user then spanned the two workspaces as one project (writ workspace span, 2026-09-20 -- their words to Writ: "TOGTools and !!TOGT is basically one addon but it HAS to be two for how wow works") and the TOC, changelog and player notes were written there from this seat. Both must release together.Specs:
Tests/version_spec.lua-- new "WoW Forever" block of 6 (mainline + 16001 is Retail and not Vanilla; takes the 12.0+ gates; the same number from a Classic client stays Vanilla; a real retail number never becomes Forever; never set on any Classic client; derived at load fromWOW_PROJECT_ID), andloadAtnow sets the project id explicitly on every load rather than inheriting it.Tests/toc_spec.lua-- the seventh manifest inTOCS, Forever's Smack omission, the VersionCheck rule extended to it, and a{16001, 16002}band pinning the one number the client advertises. Full suite: 1013 passed, 0 failed.
Bug Fixes
A Gratz (or Smack) sent to Guild or Officer stayed in the home guild; the sister guilds on the GreenWall confederation never saw it. Reported by users on 2026-09-20. Root cause, read in GreenWall's source rather than assumed: GreenWall's entire outbound relay is a hook on the chat edit box's
ParseText(GreenWall.lua:223-270,GreenWall_ParseText), which pushes the typed line onto the bridge channel withgw.config.channel.guild:send(GW_MTYPE_CHAT, msg).CHAT_MSG_GUILDis received but only logged (GreenWall.lua:326-330, "Messages will be forwarded by the ChatEdit_ParseText hook"). An addon'sSendChatMessagenever goes through the edit box, so nothing we said was ever relayed.GreenWallAPI.SendMessagewas not the fix either -- it sendsGW_MTYPE_EXTERNAL, addon data dispatched to registered handlers, not chat.Fix, in
Compat.lua:addon.Chat.Send-- the one choke point Gratz and Smack both use -- now mirrors the typed-line call after the real send, for exactly the two types GreenWall bridges (GUILD->gw.config.channel.guild,OFFICER->gw.config.channel.officer,GW_MTYPE_CHAT). GreenWall's receive side already drops the sender's own guild (Channel.lua:408), so the home guild hears the real send once and each co-guild hears the relay once. Feature-detected on GreenWall's own globals through_G(they are another addon's, not WoW API, so they stay off the linter globals list): with GreenWall absent it is a table lookup and nothing else; with GreenWall loaded but unconfigured it is a logged no-op (guild_iddefaults to'',tl_flushrefuses while not connected). The call ispcall'd and the failure written toaddon:Debug, because the real guild send has already gone out by then and a GreenWall-internal change must not take down the event handler that just spoke. Location:Compat.lua(bridgeThroughGreenWall,addon.Chat.Send).Specs:
Tests/gratz_spec.lua, new "the GreenWall bridge" block, 7 examples: GUILD goes to the guild channel as chat (not EXTERNAL) after the real send; OFFICER to the officer channel; party / raid / say are never bridged; GreenWall absent sends normally; no send function means no relay; a GreenWall-internal raise leaves the send and the fire count standing and is logged; a combat-queued line is bridged at the flush, not at queue time. Full suite: 999 passed, 0 failed.Not verified in a client yet. The call shape is the one GreenWall's own hook makes, checked against the fork's source on this box; the first gratz across a live confederation is the test.
A plain item (no suffix, enchant or gems) in the Guild Bank log was signed by its WHOLE LINK, not its id, so the same movement could be stored twice. TOGBankClassic's contract, inbox thread
55e84c617342(2026-09-17).linkSiginModules/GuildBankLog/GuildBankLog.luamatched|Hitem:(%d+):/item:(%d+):-- both REQUIRE a colon after the id -- and a plain item's link has none (|cffffffff|Hitem:858|h[Minor Healing Potion]|h|r; LibItemDB emitsitem:%dwhen every optional field is empty). Both patterns missed and the fallback returned the link itself. So one movement carried twoitemSigs depending on whose item cache answered the link lookup -- the id when the viewer's cache gave a coloned link, the link string when LibItemDB's did -- and two dedupe keys for one row. Fix:|Hitem:(%d+)/^item:(%d+), so|Hitem:858|h,|Hitem:858:...,item:858anditem:858::::::863all sign858. A native Blizzard bank link always carries colons, so native rows' sigs and ids do not move.Stored rows re-key once. A row the old fallback signed has
itemSig == itemLinkexactly -- that equality is the tell, and nothing else produces it (an id or a caller-supplied sig never equals the link). New one-shotresignItemSigs(bucket, guildKey), marker_sigsRekeyed, next to its two siblings: re-signs such rows under the fixedlinkSig, regenerates theirid(the sig is part ofrowId), then collapses any pair that now shares a dedupe key in original array order, backfillingnamefrom the dropped copy. It runs from BOTH write paths -- aftermigrateOccOrdinalsiningestTab, and at the top ofInjectEntry-- because on Classic Era nothing ever ingests and every row arrived through injection. Theidchange is invisible to TOGBankClassic:InjectEntrynever returns it and nothing reads it back yet.Specs:
Tests/guildbanklog_spec.lua-- twolinkSigexamples (colon-less link and bare item string, each alongside its coloned twin, all signing858) and a new "colon-less sig repair" block of 5: a stored whole-link row is re-signed and re-id'd throughInjectEntry; the same movement re-delivered under a coloned link now answersduplicate; a movement already stored under both sigs collapses to the named copy; an id-signed row and a caller-supplied sig are untouched; the marker is honoured. Full suite: 1006 passed, 0 failed.Smack did not load on Retail, while the README and CurseForge page had said since v0.8.5 that it works there. Both are mine. v0.8.2 omitted
Modules\Smack\Smack.luaandGUI\SmackTab.luafromTOGTools_Mainline.toc("Retail 12.0+ blocks automated public chat") and pinned that inTests/toc_spec.lua'sINTENTIONAL_OMISSIONS. v0.8.5 then readSmack.lua:134'sisRetail120Plusgate, concluded the tool works on Retail 11.x and Midnight, and corrected both player documents to say so -- without ever opening the manifest. So for a month Retail players were told about a tab that was not in their build. The code was the right half: only Say/Yell/Emote are blocked on 12.0+, Party/Raid/Guild and the hotkey work, the secret-value guards are in place, andTests/smack_spec.luadrives every flavour. Fix: Smack is listed inTOGTools_Mainline.tocandTOGTools_Camelot.toc(between Gratz and ItemDB, matching the Vanilla order the parity spec checks), andINTENTIONAL_OMISSIONSis empty -- with the story left in its comment so the next omission is added together with a check that the player docs agree. Found by the docs-accuracy pass this release, not by a report. Full suite: 1013 passed, 0 failed..pkgmetaignored"!TOGT"-- one bang, the companion's pre-rename folder name -- so the defensive entry named a folder that no longer exists. Raised in !!TOGT's peer-review board on 2026-08-14 ("theirs would not fire in the one situation it exists for"), read while retiring that board for !!TOGT's v0.2.3. Now"!!TOGT". Verified withwow-version-replication.ps1 -DryRun: the seven globs load and the WOULD list is exactly the release set. Location:.pkgmeta.
Improvements
- The Mailbox tab's sister-guild tooltip now says BOTH officers must list each other.
GuildRoster 0.8.0 made the sister sync mutual (inbox thread
586d309279d2, from the user's 2026-09-15 directive: "we don't want one guild stealing info from another one without permission"): a guild that lists yours without your officer listing them back is refused.Mailbox:DescribeSisterStatusnamed one officer in both setup states, which sends a player to do half the job. "No sister guild is configured" now says an officer of each guild lists the other and that a one-sided listing gets nothing; "configured, no roster pulled yet" says their officer must list your guild back, that their side refuses until then (/gr's status line says so), and that the next automatic round brings the roster once both sides list each other. The old "one sighting on the wire" bootstrap sentence is gone with the bootstrap it described. Location:Modules/Mailbox/Mailbox.lua. Specs:Tests/mailbox_spec.lua-- a new example asserts "each" / "mutual" / "list your guild back" across both states, and the bootstrap example now asserts "next automatic round" and the absence of "one sighting". Full suite: 1007 passed, 0 failed.
Documentation
README.mdand the CurseForge description brought up to the addon's actual state. Forever added to every supported-versions list, with the one-line explanation that it gets the Retail version of each tool; the GreenWall bridge described under Gratz, Smack and (as an optional dependency) Requirements; the sister-guild mailbox text now says the link is mutual; I'd Hit That and/togt vcsay "Retail or Forever"; Smack's Midnight paragraph names Forever alongside it. The slash-command table was checked againstSlashCommands.luaand matches (the undocumented aliases --/togt db,/togt versioncheck,/togt logs bank|gbank|diag-- stay undocumented on purpose;dbanddiagare developer-gated). The v0.8.8 CurseForge entry carries the Smack packaging fix under Fixed, because Retail players were told about the tab by name.
[v0.8.7] (2026-09-15) - The sister-guild mail autocomplete moves into the Guild Roster library
Improvements
The Send Mail To: box's sister-guild suggestions are now LibGuildRoster's, not TOGTools'. The user's direction, given in the GuildRoster session on 2026-09-15 and delivered here as inbox thread
427ea01a1362: "i added a feature to TOGTools i want to rip out and put in here. The mailbox autocomplete for sister guilds. it shouldn't live in an addon, it belongs in this library." GuildRoster 0.8.0 / MINOR 19 now owns the hook (InstallMailAutocomplete, at PLAYER_LOGIN and re-checked on every MAIL_SHOW), the candidate read (GetSisterRecipients), the matching rule (MergeRecipients, verbatim) and the ONE account-wide switch (IsMailAutocompleteEnabled/SetMailAutocomplete, in/grand/guildroster mail on|off). Until this rip-out both wrappers were chained on the same box -- clean output, because both dedupe, but one redundant merge per keystroke and two switches for one hook, which is the "one concept, two spellings" finding by definition.Removed from
Modules/Mailbox/Mailbox.lua:GetSisterRecipients,MergeRecipients,InstallAutocomplete, thesisterAutocompleteOn/otherPriority/_installedSourcelocals and bothInstallAutocomplete()calls in the event handler. The stale-mail watcher is untouched. Removed fromTOGTools.lua: thedb.global.mailbox.sisterAutocompletedefault; a copy already in a player's SavedVariables is cleared once atOnInitialize, because a key with no default is one AceDB keeps forever. Kept:GetSisterStatus(now#lib:GetSisterRecipients(), feature-detected) andDescribeSisterStatus, since the tab's tooltip still has to explain an empty list; the "too old" sentence now names 0.8.0. New, and thin:Mailbox:IsSisterAutocompleteEnabled()(nil when the library has no switch) andMailbox:SetSisterAutocomplete(bool), which forward to the library. The tab's checkbox is a second handle on the library's switch, not a second switch --GUI/MailboxTab.luareads and writes through those two, and on a library older than MINOR 19 draws a "needs Guild Roster 0.8.0 or later" note in the checkbox's place rather than an OFF box that does nothing. The help line and tooltip say where the switch lives.Specs: the 20
MergeRecipients/GetSisterRecipients/InstallAutocompleteexamples left with the code -- they run in GuildRoster'sTests/mailauto_spec.luaagainst the library's copy.Tests/mailbox_spec.luanow pins the other direction: PLAYER_LOGIN and MAIL_SHOW leaveSendMailNameEditBox.autoCompleteSourceexactly as they found it and the three removed methods are nil (so a second wrapper cannot quietly come back);GetSisterStatuscounts the library's recipients and reports 0 on a library that predates the autocomplete even when it holds rosters; the checkbox handle reads and writes the library's switch and never adbcopy, and answers nil / refuses the write with no switch to reach; the defaults table carries nosisterAutocompleteandOnInitializeclears a saved one. Suite: 992 passed, 0 failed. Verified in game by the user the same day. Location:Modules/Mailbox/Mailbox.lua,GUI/MailboxTab.lua,TOGTools.lua,Tests/mailbox_spec.lua.
Process
docs/AUDIT.mdanddocs/DEPENDENCY_CONTRACTS.mdare deleted. Both were boards that writ's inbox replaced on 2026-09-10; the user's direction on 2026-09-15: "you shouldn't be doing anything in audit.md. once it was migrated to the inbox, you could delete it" and "you can remove dependency_contracts.md as well, as long as you migrated it to the inbox". The audit board's open items had already gone to the inbox and the todo list on 2026-09-11; its last open item, round 5 finding H (GuildBankLogoccas a position in a mutating buffer), was falsified in game today -- the user's three-deposit test showed the third identical stack recorded, so the ordinal is stable and no dedupe-key change is needed. The contracts board's three sections were all closed (TOGBankClassic pushes viaInjectEntry, delivered; the level-change callback, absorbed into Gratz; sister-guild ownership and now the mail autocomplete, delivered by GuildRoster) and their records are filed as inbox threads42e8d2c03719(TOGBankClassic) and5e886a380126(GuildRoster). Both files remain in git history.CLAUDE.mdnow points at the inbox instead; the code comments and specs that cited the boards by path (GuildBankLog.lua,Mailbox.lua,guildbanklog_spec.lua,logregistry_spec.lua) cite the inbox thread or say the finding is settled; the watcher spec no longer listsdocs/AUDIT.md.
Older releases (v0.8.6 and earlier) are archived in CHANGELOG_ARCHIVE.md.
All Relations
- All Relations
- Embedded Library
- Optional Dependency
- Required Dependency
- Tool
- Incompatible
- Include

