Ants Projects Hub
← Ants Terminal

Changelog

Ants Terminal

Every release, newest first — 187 in total.

v0.7.113

0.7.113

A tidier, faster Roadmap window. Everything lines up in columns, it remembers what you had open, and big sections open much faster. Also: the Flatpak build can now run git and search tools on your computer, the status bar shows Claude Code's context use again, and several security hardening fixes.

v0.7.112

0.7.112

What's new

  • It updates itself. If you use the single-file download (the AppImage), Ants now tells you on the menu bar when a new version is out. One click downloads it, checks that it really came from this project, and swaps it in. Restart straight away, or carry on and get the new version next time you open it. If you installed from your system's package manager, updates keep arriving that way, as before.
  • A welcome window for new users. The first time you open Ants it explains what it can do and offers one-click setup. Anything that would change one of your own files shows you the exact change first. You can reopen it any time from Help → Show Welcome.
  • Tool updates without closing the terminal. Claude Code now talks to Ants through a small helper program. When the tools improve, you reconnect Claude Code (type /mcp) and it picks them up; the terminal and your other sessions keep running. The welcome window sets this up for you.
  • Tabs you can tell apart. The View menu can give each tab its own colour.
  • A clearer project to-do window. Sub-headings are indented under their parent, each tab remembers its own filters, and the title says where the list comes from.
  • Safer typing into several panes. "Broadcast" typing now reaches only the panes in the tab you are looking at, starts switched off, and shows a red marker in the bottom bar while it is on.
  • Smoother and steadier. Saving your tabs in the background is quicker, and several actions that could make the window pause no longer do: saving everything that has scrolled past, the code checker, and typing in the to-do window.
  • Works on more computers. The single-file download can now connect securely on systems where it previously could not, so update checks work there too.
  • Safety fixes. The activity log hides anything that looks like a password or key, and a mistyped backup location can no longer delete the file that was there.

v0.7.111

0.7.111 — hotfix

Hotfix. Restores the Mageia package, which 0.7.110 could not build.

Fixed

  • The Mageia package builds again (ANTS-5304) A self-check compared the way Ants lowercases a roadmap id against the way its database does it, using accented letters as the example. Most Linux distributions lowercase only English letters there; Mageia's build also lowercases accented ones, so the two disagreed and the package build stopped before it finished. The check now uses only the characters a roadmap id can actually contain, so it gives the same answer everywhere. No change to the program itself — this is a packaging fix.

v0.7.111-rc2Pre-release

0.7.111 RC2 — Patron preview

Respin of v0.7.111-rc1 with cherry-picked fixes: a3134d7e

v0.7.111-rc1Pre-release

0.7.111 RC1 — Patron preview

This release shipped without written notes.

v0.7.110

0.7.110

Added

  • A performance report for contributors: tools/perf-report.sh runs every benchmark and compares each number against a saved baseline, flagging what got slower (ANTS-5133) Development tooling only; nothing ships in the user binary. How-to: docs/qa/perf-harness.md.

  • The roadmap repair step can now remove leftover detail lines at the end of an item's description (ANTS-4507) roadmap_log op:"repair_trailers" takes strip_runs:true. It removes the Kind / Source / Layman / Lanes lines a description stored by an older version still ends in, but only where they repeat values the item already stores. Where they disagree, nothing is removed and the item is listed for review. Each removal is kept in the item's history.

  • apply_edits reports each edit's own replacement count in edit_replacements, and an optional per-edit expect_count skips an edit whose match count differs, before it touches the file. (ANTS-4838)

Changed

  • The Project Audit dialog's code is split into five source files; the dialog behaves exactly as before. (ANTS-1044) src/auditdialog.cpp keeps the dialog's core, and the check catalogue, Debt Sweep tab, AI triage and export each move to their own file. Tests read the class through one source list, and new checks keep the files in step with it.

  • MCP rate limiting remembers each caller folder's resolved path for 30 seconds, so repeat tool calls skip a filesystem lookup on the window's thread (ANTS-5090)

  • The etag option on MCP tools now states that a not-modified check needs the same arguments as the call that issued the etag (ANTS-4859)

  • On a store-backed project, a roadmap append that moves .roadmap-counter marks it counter_mirrored: true, since the store allocates the id and the file only mirrors it. (ANTS-4969)

  • Every MCP tool description now fits the 800-byte wire budget; the longer prose moved to each tool's detail, served by tool_info, and a test checks every registered tool. (ANTS-5152)

  • Switching back to a Claude tab resumes its task lists instead of re-reading the transcript (ANTS-5050) The task and background-task trackers keep each recently read transcript's parse position, so a tab switch back, or a second pane on the same session, carries on from where reading stopped.

  • workspace_search, cited_by and co_change_family now read ripgrep's output line by line instead of holding it whole (ANTS-5127) co_change_family also keeps only its strongest max_sites sites while it scans, instead of every candidate. Results and counts are unchanged.

  • Searching the terminal's history is more than twice as fast. (ANTS-2000) Each search reads history lines directly instead of one cell at a time. What a search finds is unchanged.

  • The test-audit window opens without the pause it used to have (ANTS-5126) Opening the window, and editing its dimensions field, ran a content scan over every test file before drawing the panel. The panel now asks only for the file grouping it displays, and the scan runs when an audit is actually dispatched.

  • The periodic session save now skips tabs that have not changed (ANTS-5030) Every thirty seconds each tab's whole history was re-serialised, compressed and written even when nothing had happened in it. A tab is now written only when something it stores has actually changed.

  • The diff viewer and clipboard guard build clean under the project's clang-tidy check set (ANTS-5129) Internal tidy-up with no change to behaviour: helper linkage, two byte-cap constants, one avoidable string copy, and the size of an internal enum.

  • The Independent Review and Cold-eyes dialogs search the project once per review round instead of twice (ANTS-5125)

  • CI's memory-checked test run skips the benchmark tests, as the local pre-push check already does (ANTS-5014) The benchmarks cost 285 s of a 1121 s serial sanitized run (measured 2026-09-11). Two correctness tests labelled perf for their large fixtures still run in CI's Release job.

  • The AI review windows keep one copy of the code that stops their requests on close (ANTS-5009) LlmDispatcher now only schedules jobs. ReviewDialogBase's runner creates the LlmClients and aborts them when the window closes. The dispatcher's own copy never ran, so nothing changes for the user.

  • Streaming AI answers are parsed without re-copying the buffer for every line (ANTS-5005)

  • Cold-eyes re-reviews now run cold and keep a round-by-round log (ANTS-2011) A re-review no longer tells the reviewer what was fixed, so a fix that did not hold shows up again. The results view lists each round: the lanes it checked and how many findings it produced.

  • Audit: the Contract-Doc ↔ Code Drift check now reports standards and specs as two separate lanes (ANTS-3849) One category held both, and the specs half outnumbered the standards half by more than fifteen to one — over 80% of the whole report. Nobody reads a category that size, so the real findings in it were invisible. Splitting suppresses nothing: both lanes still report every finding, and the small, readable half can now be acted on by itself.

Fixed

  • The remote-control grab-image command saves a relative path inside the test artifact folder, and its reply names where the file really is (ANTS-5132)

  • Links and highlights stay correct on busy output when trigger rules are set (ANTS-5078) With a trigger rule that colours or links matching text, a burst of output could leave clickable links and highlight colours on the wrong place on lines printed later in the same burst. Those lines are now rechecked when the screen is next drawn.

  • Exporting from the terminal's right-click menu tells you when it fails (ANTS-5078) Export Scrollback as Text or HTML and Share Block could leave a half-written file on a full disk and say nothing. They now write to a temporary file that replaces the old one only when everything was written, and show a message in the status bar when an export fails.

  • Right-click command actions always act on the command you clicked (ANTS-5078) With shell integration, right-clicking a command offers Copy, Re-run, Fold and Share. If new output arrived while that menu or the Share dialog was open, the action could land on the neighbouring command, so Re-run could type a different one. Each action now remembers which command you clicked, and does nothing if that command is gone.

  • The verify-config trust prompt lists every gate and can remember a repo for good (ANTS-5082) When Claude asks to run a repo's own verify commands, the prompt now lists how many gates would run and their names, and "Trust this repo" has a checkbox (on by default) to ask again if the file changes. The trust file keeps the date each entry was first trusted instead of resetting it on every save, and Ants warns when that file can be read by other users.

  • Claude can no longer freeze the window by asking for a terminal's whole history (ANTS-5219) Claude's scrollback tool took any line count and read all of it on the window's thread. It now returns at most 10,000 lines, trims to the same size limit as reading a tab's text, and marks a reply that was cut so Claude knows it did not get everything.

  • The review dialogs no longer build every lane's brief at once (ANTS-5082) Starting a review built the full brief for every lane up front and held them all in memory while only a couple ran at a time. Each brief is now built when its lane actually starts, which keeps memory inside the review budget.

  • A background audit started by Claude no longer leaves its thread behind (ANTS-5080) An audit Claude starts in the background is run on its own thread. That thread was cleaned up only through the object that tracks such audits, so if that object went away mid-audit the thread was never freed. The thread now cleans itself up when the audit finishes.

  • Pasting a large screenshot no longer freezes the window while it saves (ANTS-5077) Pasting an image saved it as a PNG on the window's own thread, so a large screenshot froze every tab until the file was written. The image is now saved in the background, and its path is pasted once the file exists.

  • Long unbroken lines no longer leave tens of megabytes cached per tab (ANTS-5077) The drawing cache kept a fixed number of text runs, however long each run was. A line with no spaces is one run as wide as the row, so a few tabs of such output could hold tens of megabytes after it stopped. The cache now has a budget for the amount of text it keeps, and very long runs are drawn without being cached.

  • Closing a tab whose shell is stuck no longer risks freezing the whole window (ANTS-5077) When a tab's background reader was still busy two seconds after the tab closed, it was forcibly killed, which can freeze the window if it held a lock at that moment. It is now left to finish on its own and clean itself up.

  • A clickable link that wraps off the bottom row stays clickable on both lines (ANTS-5076) A link printed near the end of the last row wraps and scrolls the screen. Its recorded start row did not move with the scroll, so the link was dropped from both lines. It now keeps a span on each.

  • A large paste now reaches the shell whole, closing marker included (ANTS-5075) A paste bigger than the terminal's write queue used to lose its tail, and could lose the bracketed-paste end marker, leaving the shell stuck in paste mode. The paste is now fed to the shell in slices as it reads, and keys typed during a long paste arrive after it.

  • The audit drift checks no longer freeze the window, and audit_run keeps them under its time limit (ANTS-5067) The Audit dialog runs its spec, contract-doc and changelog drift checks on a background thread, so terminals keep updating and Cancel works while a check runs. A result that arrives after Cancel or a new Run is discarded. audit_run now counts these checks in its aggregate time cap; one still running at the deadline is reported timed_out and marks the run partial.

  • CI's Release job finishes again, and fails red instead of silently cancelling when it runs long. (ANTS-5188) cppcheck now runs in its own CI job, multi-threaded with a cache of its analysis. The Release job's build and test steps each have a time guard, its compile cache is saved even when a run stops early, and its tests run two at a time.

  • doc_citations no longer checks a bare line number against a file named in an earlier paragraph. (ANTS-4923) A line reference like :120 on its own now borrows a file only from a citation in the same paragraph. After a blank line or a heading it is reported as unresolved, instead of a confident result about whatever file the previous section happened to mention.

  • doc_citations no longer reports citations inside a review loop-log row as stale. (ANTS-4918) A citation on a Cold-eyes loop-log table row is still listed, marked loop_log_row:true and counted, but only:"stale" leaves it out, since a landed loop-log row is never edited. A line reference that follows a quoted path from another project is now foreign_path too, instead of a missing_file warning that could never be cleared.

  • Re-importing a roadmap no longer rewrites the wording of each item's plain-English summary (ANTS-4955) Summaries are now stored without their closing full stop and written back with one, whichever route saved them. Each roadmap file gains the stop on summaries that lacked it at its next render: a one-time diff.

  • A line such as ```json inside an open code block no longer ends the block, so the headings and text after it stay treated as code by the outline, spec log, feedback and changelog tools (ANTS-4987) Matches the CommonMark rule that a closing fence carries nothing after its backticks. Files with Windows line endings still close their blocks, and the code-block mask now shares the same rule as the other readers.

  • The MCP socket answers a malformed request line with a JSON-RPC parse error instead of closing silently after five seconds (ANTS-5089)

  • A remote-control client that stops reading a large reply is disconnected after 30 seconds instead of holding the socket open (ANTS-5093)

  • The Test Audit dialog drops a collected report when its chunk now covers different test files, so stale findings are not filed (ANTS-5102)

  • The status bar and tab dot follow a Claude session that was started before its first message, instead of showing it as idle for the whole session (ANTS-5156) The per-tab tracker now keeps looking for the session's transcript until Claude Code creates it, and no longer borrows another project's transcript in the meantime.

  • indie_review_partition says why it ignored a .indie-review/partition.json (map_rejected with a reason, such as a missing "version": 1) and no longer reports that file as the source it used. (ANTS-4846)

  • New tab shells no longer inherit a dead Claude Code session's identity variables, which switched transcript saving off in a claude started there; other CLAUDE_CODE_ settings still pass through. (ANTS-4541)

  • The per-tab thinking-level chip reads the last prompt a person typed instead of stopping at a tool reply, so it no longer shows Unknown on almost every live tab. (ANTS-1892)

  • invariant_check's scope note now says on every reply that specs are matched by path, so a spec citing the module only by symbol is known to be missing even when another spec matched. (ANTS-4972)

  • apply_edits flags a batch that applied some edits and skipped others with partial:true (would_be_partial:true on a dry run), instead of a bare ok:true. (ANTS-4856)

  • roadmap_log op:"set_body" accepts a replacement body up to 65536 characters, so long bodies can be written back whole; amend_body keeps its 4096-character fragment cap. (ANTS-4841)

  • co_change_family, docs_index, feedback_query and session_orient now declare the compact argument they already honoured, so a caller can pass compact:false to them. (ANTS-4657)

  • roadmap_query's tool detail now says when parseable_bullets and its warning appear, and that a store-served read never carries them, so their absence can be read correctly on either backend. (ANTS-4964)

  • roadmap_query answers a migrated project whose store holds no items from the store, with zero items, instead of falling back to the file and telling the caller to Read it. (ANTS-4861)

  • roadmap_query names the kind or source filter when it empties a result, instead of blaming the ID filter and calling the roadmap malformed. (ANTS-4971)

  • Roadmap writes leave a roadmap file untouched when its content has not changed, and list it under files_unchanged rather than files_written. (ANTS-5016)

  • workspace_search no longer warns about short terms when the whole regex is one word-bounded group, such as \b(TODO|FIXME|TBD|XXX)\b. (ANTS-5139)

  • file_outline lists gtest TEST, TEST_F and TEST_P blocks as Suite.Case, and no longer reports locals declared inside a test body as functions. (ANTS-5019)

  • mutation_probe refuses a baseline that collected no tests (pytest exit 4 or 5) as baseline_did_not_run, naming the test command, instead of reporting the suite as red. (ANTS-4852)

  • file_outline lists out-of-line C++ constructors and destructors, so read_region can fetch them by name, including a bare ~ClassName. (ANTS-5021)

  • mutation_probe, verify_changes and the test-results cache read ctest's all-passing summary ("100% tests passed out of N"), so a green run counts as green and require_green_baseline can pass. (ANTS-4996)

  • feedback_query flags a shipped id with no recorded ship date as possibly_stale_binary, marked stale_check:"no_shipped_date", instead of leaving the check silent. (ANTS-4843)

  • roadmap_query says when it applied a different body cap than the max_body_bytes asked for, with body_cap_clamped and the effective value, on an id or ids fetch. (ANTS-4981)

  • ants-terminal --remote waits for a slow first byte of the reply, up to its overall deadline, instead of printing "no response" while the server is still answering. (ANTS-5138)

  • workspace_search resolves a glob or exclude_glob containing / against lane when one is set, instead of returning no matches. (ANTS-5117)

  • A refused MCP call narrowed with fields no longer lists every requested field as unmatched, so the refusal's own code is the only cause shown. (ANTS-4979)

  • find_definition finds C functions whose return type, name and parameter list sit on separate lines, the style common in older C code such as DOOM. (ANTS-4828)

  • find_definition no longer lists a wrapped ternary arm (: sym(a);) or a stream insertion (out << sym(i);) as a declaration of the symbol. (ANTS-4924)

  • An offloaded reply's per-row preview now samples each object row's own JSON, so every head shows what its row is instead of the same {"v": prefix. (ANTS-5153)

  • tools/perf-report.sh -R <regex> --save-baseline now keeps every other benchmark's saved numbers instead of replacing the whole baseline. (ANTS-5137)

  • The git diff reader ends a file's hunks at a merge-conflict section instead of assigning its lines to the file before it. (ANTS-5111)

  • The debug log now rotates when it passes its size limit during a session, instead of growing until Ants restarts (ANTS-5110)

  • read_log refuses a path that is not a regular file, so a named pipe can no longer freeze it (ANTS-5110)

  • Review Changes no longer lists a repository's whole history on every refresh when it has no remote, and a crashed git command no longer refreshes the window twice (ANTS-5109)

  • A line break in a changelog entry's summary, id or dated headline stays on that line and can no longer create a fake heading (ANTS-5108)

  • project_query no longer refuses every query for the rest of the session after 64 slow queries (ANTS-5107) A query worker that was left running past its deadline and later finishes now frees its slot.

  • project.read refuses anything that is not a regular file, so a named pipe can no longer hang a query (ANTS-5107)

  • Two open Ants windows no longer corrupt each other's saved tabs (ANTS-5106) Each process now writes its own temporary file before replacing a saved session or the tab order.

  • A failed tab-order save (for example on a full disk) keeps the previous tab order instead of replacing it with a cut-off file (ANTS-5106)

  • read_region clips an oversized first line to its byte limit instead of returning a huge single-line file whole (ANTS-5103)

  • An automatically sized results page always includes at least one row, so following next_offset can no longer loop forever (ANTS-5103)

  • codebase_index reports a failed cache write instead of ignoring it (ANTS-5103)

  • The AI client reports an error for a successful reply it cannot read, instead of an empty answer that looks like success (ANTS-5105) A proxy login page or any other body that is neither a stream nor a chat completion used to show nothing in the AI chat and read as a clean review lane.

  • Build-error fix hints recognise GCC's curly quotes, so they work with this machine's compiler output (ANTS-5102)

  • verify_changes no longer loses ctest's failed-test list when one large chunk of output arrives at once (ANTS-5102)

  • An Independent Review lane now stops at its time limit even when the AI endpoint keeps trickling data, and a reply over 10 MiB is cut off (ANTS-5101)

  • The debt sweep's automatic fix no longer corrupts bytes that are not valid UTF-8 elsewhere in the file (ANTS-5101)

  • spec_conformance refuses a pattern that does not compile instead of passing every no-match row against it (ANTS-5100)

  • doc_lint's table-of-contents fix recognises code blocks the same way the check that reports the gap does (ANTS-5099) It used its own simpler rule, which could insert a contents row in the wrong place.

  • docs_index reports a failed cache write instead of ignoring it (ANTS-5099)

  • indie_review_fold_in's narrative mode honours dry_run and no longer writes ROADMAP.md on a preview (ANTS-5097)

  • verify_changes no longer serves a cached pass when git status fails or times out (ANTS-5097) A failed status used to hash as a clean tree; that snapshot is now marked uncacheable.

  • session_message returns at most 200 messages per inbox call and refuses a message_id that is not a positive whole number (ANTS-5098) A limit of 0 or less used to return the whole mailbox.

  • changelog_log add_batch accepts at most 200 entries per call (ANTS-5098)

  • A roadmap flip_batch or annotate_batch where no item resolves in the roadmap database now reports failure under its own name instead of success (ANTS-5095) The review-kind warning on append_batch also names the ids the batch assigned rather than the headlines.

  • mutation_probe writes the mutated file and the restored original atomically, so a full disk or crash cannot leave a source file cut short (ANTS-5096)

  • build_target_for validates its cmake_path argument, and file_outline accepts at most 100 paths per call (ANTS-5096)

  • Editing a roadmap item's plain-language summary, source or evidence through roadmap_log can no longer break a line in ROADMAP.md or split an evidence path (ANTS-5094) The single-field edit now cleans values the way adding an item does.

  • roadmap_query's report mode refuses a since or until date it cannot read instead of quietly reporting a different period (ANTS-5094)

  • The Claude permissions window says when its settings file cannot be read, and the transcript window no longer shows an empty User line for each tool result (ANTS-5092)

  • The Background Tasks window reads each task's output from the exact path it checked, and reads a finished task's output only once (ANTS-5092)

  • The roadmap window's kind filter covers every roadmap kind, and a roadmap file over 8 MiB is no longer cut off (ANTS-5088) Perf, security, feature and six other kinds can now be filtered, and a kind filter no longer hides them. The live roadmap is read up to 64 MiB.

  • Publishing the roadmap refuses a path that leaves the project before creating any folder, and never writes through a symlink that points outside it (ANTS-5087) A dry run no longer creates folders either.

  • On a pass-headings roadmap, a hand-added pass block is protected from being deleted by the next roadmap write (ANTS-5087) The write now refuses and names the block, as it already did for ordinary bullets the database has never imported.

  • Roadmap migration and export rebuild match item ids with non-ASCII letters the same way the roadmap database does (ANTS-5087) The database folds only A-Z when comparing ids, while the migration and the rebuild also lowercased letters such as Ä, so those ids could link to the wrong item or none.

  • An older Ants build meeting a roadmap database from a newer build now refuses at once instead of waiting for the write lock (ANTS-5086) The new roadmap database file is also created owner-only before any roadmap data is written into it.

  • Since-last-run audits no longer re-count header findings as new or grow the recorded finding list every run (ANTS-5085) A finding a tool reports in an unchanged file that the previous run also had is carried forward once, not added again.

  • Suppressing an audit finding no longer rewrites the whole rule-quality history file (ANTS-5085) The history is saved at the end of a run and when the audit window closes, and only when something was recorded.

  • Audit context filters skip very large referenced files, and the review false-positive ledger cache is thread-safe (ANTS-5085)

  • Audit auto-fix repairs files with Windows line endings, and the audit cache folder is always created private (ANTS-5085) Auto-fix used to treat every planned repair in a CRLF file as out of date; it now applies them and keeps each line's ending. The SARIF and HTML exports and the gitleaks config writer create the .audit_cache folder owner-only from the start.

  • Audit window: the Since baseline filter and changed-lines scope no longer hide every finding, and batch AI triage is throttled (ANTS-5084) A run with Since baseline on keeps the changed-line data that filter needs. When git cannot say what changed (a short history, a timeout, a failure), the status line says so and the changed-lines filter is skipped instead of hiding everything; a repository with fewer commits than the window counts every line as changed. Batch AI triage sends two batches at a time through one shared network client, refreshes the list once at the end, and stops reading a reply past 10 MiB.

  • Audit window: project detection no longer walks build trees, and exports, labels and the changed-files filter are correct (ANTS-5084) Opening the audit window lists only the top few folder levels when detecting Docker and Terraform files. The signal banner shows one percent sign. A finding containing an HTML comment opener can no longer blank the HTML report. A tool that failed to start is labelled a tool issue, not a timeout. A failed export says so. A changed oo.cpp no longer matches src/foo.cpp.

  • Audit auto-fix skips findings suppressed after the run, and the contract-drift lanes keep every finding (ANTS-5083) Auto-fix now checks suppressions when it runs, skips findings with no line before reading any file, and does not read very large files. The per-check findings cap no longer applies to lanes that are uncapped on purpose.

  • A suppression that could not be saved no longer hides the finding, and a new audit run re-reads source lines (ANTS-5083) The audit dialog now checks the suppression file write before hiding the finding and says so when the save fails. Each run clears the cached source lines, so an edited file is shown as it is now.

  • The themed stylesheet header comment now matches the rules it builds (ANTS-5082)

  • A mistyped colour in a custom theme now uses the default colour instead of turning black, and an oversized theme file is skipped (ANTS-5082)

  • The About dialog no longer describes a GPU renderer the terminal no longer has (ANTS-5082)

  • Clicking Re-review while a review is still running no longer wipes that round's failures, so a failed lane can't be mistaken for a clean one (ANTS-5082)

  • A full disk can no longer replace the verify trust list with a cut-off file, and a damaged trust file is kept aside instead of being overwritten (ANTS-5082)

  • A global hotkey that the desktop refuses to register now shows the 'Global hotkey unavailable' message instead of failing silently (ANTS-5081)

  • The global hotkey now works on desktops that start their shortcut service on demand (ANTS-5081)

  • A tab's tooltip keeps naming the tool Claude is using instead of being reset to a generic state on every redraw (ANTS-5081)

  • The Public/Private repository badge recovers when the gh tool is missing or stops responding, instead of never updating again (ANTS-5080)

  • Restoring the window position through KWin no longer leaks a helper process or leaves the loaded script behind when a step cannot start (ANTS-5081)

  • The Review Changes button no longer shows the previous tab's git state after a quick tab switch, and a stuck git check can no longer freeze it for the session (ANTS-5080)

  • Start-up cleanup of old MCP sockets now removes only real sockets owned by you (ANTS-5080)

  • Exporting scrollback now tells you when the file could not be written, instead of reporting success (ANTS-5079)

  • Moving or centring the window through KWin no longer leaves a stray process and temp file behind when the helper cannot start (ANTS-5079)

  • Connecting to an SSH host no longer risks a crash if the new tab closes straight away (ANTS-5079)

  • Copying a very large selection with formatting no longer builds hundreds of megabytes of styled text (ANTS-5078) Formatted copy now groups characters that share a style, and above a size limit copies plain text only.

  • Claude running in a split pane now lights its tab's status dot, and closing a pane or a split tab releases what it was tracking (ANTS-5079) Split panes were never registered with the Claude and background-task trackers, and closing one left its shell listed in them.

  • A search pattern that can match an empty spot no longer hides the real matches later on the same line (ANTS-5078)

  • Jumping to a search match or a bookmark now moves the scroll bar too (ANTS-5078)

  • Right-clicking with a very large selection no longer pauses while the whole selection is copied just to measure it (ANTS-5078)

  • A terminal recording no longer garbles a character whose bytes arrived in two pieces (ANTS-5078)

  • A background image keeps its shape instead of being stretched to fit the window (ANTS-5077)

  • A command's duration never shows as negative when the system clock steps backwards (ANTS-5077)

  • Drawing the terminal no longer builds a highlight cache entry for every line when no highlight rules are set (ANTS-5077)

  • In broadcast mode, Ctrl key combinations and accepted suggestions now reach every pane (ANTS-5077) Ctrl+C, Ctrl+arrows and accepting an autocomplete suggestion went only to the focused pane, and did not clear a highlighted selection.

  • A pasted screenshot's path works at the prompt even when the paste folder has spaces, and the saved file is private to you (ANTS-5077) The saved screenshot's path is now quoted like a copied image file's, the file is readable only by you, and nothing is announced when saving fails.

  • A tab whose shell failed to start no longer behaves as if a shell were running (ANTS-5077)

  • Turning on the underline debug log no longer slows output, and the log stops growing past a fixed size (ANTS-5076)

  • Resizing the window no longer glues together words that were split across two lines (ANTS-5076) When a line wrapped exactly at a space, resizing dropped that space and the words on either side ran together.

  • Clickable links disappear when the text under them is cleared or overwritten (ANTS-5076) A link stayed clickable over blank space after the screen or line was cleared, and redrawing the same link over and over kept piling up copies of it.

  • Sixel images decode faster, so a crafted image can no longer stall the window as long (ANTS-5076) Each pixel was set through a slow general-purpose call; the decoder now writes pixels directly and skips drawing past the image edge.

  • A garbled terminal control code no longer spills its leftover characters onto the screen (ANTS-5075) When a program sent a control code with a stray byte in it, the terminal gave up on the code and printed the rest of it as text. It now discards the whole code, the way other terminals do.

  • Clearing the screen now always clears a text selection, even when the clear arrives in two pieces (ANTS-5075) The terminal looked for the clear-screen code inside each chunk of output it read, so a code split across two chunks was missed and the old selection stayed highlighted.

  • A shell opened on a desktop session with a very large environment now receives every variable, including the one Claude Code uses to find the terminal (ANTS-5075) The terminal copied its environment into a fixed-size list and stopped when the list filled, so the variables added last were lost. The list is now sized to fit.

  • Opening a long Claude transcript no longer freezes the window while the whole file is decoded (ANTS-5089) The transcript viewer shows only the last 2000 entries, but it decoded every entry in the file first, on the window's own thread. It now decodes just the entries it shows and only counts the rest.

  • Claude's status shows up for a Claude session started from a subfolder or subshell (ANTS-5089) Ants found the Claude session's transcript by looking at the shell's current folder. If Claude was started after changing folders in a subshell, that folder belonged to a different project, so no transcript was found and the status stayed blank. Ants now looks at the folder Claude itself is running in first.

  • Reading terminal text over remote control no longer copies the part it throws away (ANTS-5093) When a get-text reply was over its size limit, Ants copied the whole dropped part just to count its lines. It now counts them from what it already holds.

  • Tabs without Claude running no longer make Ants read every process's details every two seconds (ANTS-5089) To spot Claude Code in a tab, Ants checks the shell's child processes every two seconds, and when none was Claude it also read the details of every process on the system. That full scan now runs once every five checks for a tab with no Claude running, and on every check while one is.

  • The Claude transcript viewer and session summaries no longer drop messages longer than 64 KiB (ANTS-5089) Ants read Claude Code's transcript one record at a time but stopped at 64 KiB, so a long tool result or a pasted file was split into pieces that could not be read and was silently left out. Records up to 16 MiB are now read whole, and a larger one is skipped cleanly instead of in fragments.

  • Saving a large answer with many rows to disk no longer slows down sharply as the row count grows (ANTS-5104) When a big MCP answer is saved aside, Ants builds a short per-row preview that has to fit a size budget. It rechecked the size by rebuilding the whole preview after every row, so the cost grew with the square of the row count. It now keeps a running total and builds each row once, with the same preview as before.

  • Ants no longer rewrites Claude Code's settings.json on every launch, and no longer replaces a hooks value it cannot read (ANTS-5104) When the Ants session hook is already in place, starting Ants now leaves ~/.claude/settings.json untouched instead of rewriting it. A "hooks" value that is not an object, or a "SessionStart" value that is not a list, is now refused with a warning instead of being silently replaced.

  • Calls to tool names that do not exist no longer grow the token usage counter, and each MCP call does less bookkeeping (ANTS-5104) Every made-up tool name used to add its own permanent entry to the per-session token counter. They now share one "(unknown tool)" entry, still counted as failures. The tokens-saved chip also reads its total directly instead of rebuilding and sorting the full report on every call.

  • The tokens-saved chip shows 1M instead of 1000K for counts just under a million (ANTS-5104) A count that rounds up to the next unit (999,950 and above) now moves to that unit, so it reads 1M rather than 1000K, and the same for M to B.

  • Reading a large saved MCP answer page by page no longer garbles characters at page edges (ANTS-5104) read_spill could end a page in the middle of a multi-byte character, which showed as a replacement character and threw the page length off. Pages now end on a whole character, and each page reads only its own part of the saved file instead of the whole file.

  • Suspending the computer or changing its clock no longer lets two audits run on one project (ANTS-5090) The short read cache, the "already running" guard for audit_run and indie_review_dispatch, and the async audit job list now measure elapsed time on a monotonic clock. A clock step backward could keep a cached read fresh, and a suspend could free a guard slot while its sweep was still running.

  • A "too busy, retry shortly" answer is no longer remembered and repeated for a cached MCP verb (ANTS-5090) When the dispatch queue was full, the refusal for a cached read verb was stored in the short-lived read cache, so the next call got the same refusal after the queue had drained. The refusal is no longer cached.

  • Large answers from background MCP verbs no longer stall the Ants window while they are prepared (ANTS-5072) The reply transforms (etag, field projection, compaction, hints, offload and the data wrap) now run on the dispatch worker for verbs that already run there. The window thread keeps only the cache insert, the usage record and the socket write. Measured before the change: a 4 MiB reply cost about 92-95 ms on the window thread.

  • changelog_query finds an id cited only in a dated topic's headline or prose (ANTS-5145) A dated topic heading written by changelog_log op:add_subsection is now an entry of its own: its text is the headline and its body the prose under it, so an id lookup reports an id cited there instead of calling it missing.

  • Remote-control and MCP tab tools act on the window you last used, not on a hidden first window (ANTS-5121) Delivered by the shared listener (ANTS-5144): each request goes to the most recently active visible window, whose tools act on that window.

  • Closing a New Window window no longer cuts MCP, Claude hooks and remote control off for every other window (ANTS-5144) Each window used to take the three socket paths from the window before it, and closing it removed the socket files. Now one listener per path serves the whole process: requests go to the most recently active visible window, and a hook event goes to the window whose tabs track its session. A second Ants process no longer takes the remote-control socket from the first.

  • The sanitizer build builds again (ANTS-5143) The perf benchmarks are no longer built when ANTS_SANITIZERS is on, so a full build-asan build and its test suite complete, and the pre-push hook's ASan leg runs again.

  • changelog_query returns every entry for an id lookup (ANTS-5147) An id or ids lookup now ignores offset and limit, as its design and roadmap_query do, so offset:10 no longer returns an empty page for an id that is present.

  • A malformed extended-colour code no longer sets text attributes (ANTS-5136) An SGR 38, 48 or 58 introducer whose selector is neither 2 nor 5 now abandons the rest of the sequence. Before, ESC[38;1m applied bold and ESC[48;3m italic.

  • changelog_query refuses an id or ids of the wrong JSON type (ANTS-5146) A number, boolean or object passed as id or ids now refuses bad_args instead of returning the whole changelog.

  • changelog_log add_subsection refuses a date it cannot parse (ANTS-5148) A date that is not a valid yyyy-MM-dd date now refuses bad_args, so every dated heading it writes is one changelog_query can read.

  • changelog_query reads entries under dated topic headings (ANTS-5071) A ### <date> <Category> — <headline> heading, the form changelog_log op:add_subsection writes, now sets the category named after a valid date. Its bullets are returned, found by id lookups and counted in version_index. ANTS-3533 § 3 and INV-10 were amended first.

  • plan_template's includes_tests:false now drops each task's test file line and test-first steps (ANTS-5068) The option was documented and parsed but never read, so false returned the same skeleton. The remaining steps are numbered from 1; the default output is unchanged.

  • The window no longer freezes while a --remote search, tree walk or git command runs, and the roadmap store is no longer used from two threads at once (ANTS-5073) Remote-control socket routes whose MCP twin already runs off the GUI thread now run on the same MCP dispatch worker, with the reply written later. ANTS-5051 closes with it. indie_review_dispatch still freezes the window for a review.

  • A synchronous audit_run no longer freezes the window for the whole sweep (ANTS-5035) It replies from a completion slot instead of joining its worker on the GUI thread. A second synchronous call for the same project while a sweep runs is now refused with already_running.

  • Closing a second Ants window no longer makes Claude commands in the remaining window refuse (ANTS-5142)

  • The window no longer stalls while Claude writes to its transcript. (ANTS-5050) The task and background-task trackers now read only the newly appended part of the conversation log instead of re-reading up to 16 MiB of it every time Claude writes. Measured on a 65 MiB transcript, one update went from 258 ms of frozen window to 0.09 ms. Whenever the log cannot be resumed safely — truncated, replaced or rewritten — the full read still happens, so nothing is missed.

  • Leftover text styling is cleared when a foreground program exits. (ANTS-5135) A program interrupted while printing dimmed text used to leave the dim attribute set, and on distributions whose shell prompt carries no colour reset of its own — openSUSE's default among them — every later prompt and everything typed stayed dim. Ants now clears the character attributes when a program hands the foreground back to the shell. Shell builtins keep their styling, since they never leave the shell's own process group.

  • A malformed colour escape no longer leaves the text after it dim (ANTS-5130) An extended-colour sequence too short to carry its colour had its own selector run as an attribute code, switching dim on for every cell that followed until the next reset. Such a sequence is now abandoned whole.

  • Closing Review Changes now cancels its background git probes before they are torn down (ANTS-5128) Closing the window while a probe was still running destroyed it mid-flight, which ran the probe's completion handler against the half-closed window. The probes are now stopped first, on every way the window can close — the Close button, the parent window, and quitting the app.

  • The Review Changes window no longer freezes on a huge diff: it shows the first 1 MiB with a note, skips redrawing when nothing changed, and ignores an older refresh that finishes after a newer one (ANTS-5059)

  • The roadmap window reads its "last touched" dates in the background and can no longer put a date on the wrong card when the roadmap changes mid-read (ANTS-5047)

  • The audit window's Debt Sweep tab scans in the background, so the window stays usable while it runs and after every fix or allow click (ANTS-5057)

  • The audit window no longer freezes after a run while it looks up who last changed each flagged line; it asks git once per file in the background and fills the names in when they arrive (ANTS-5040)

  • Saving roadmap changes no longer re-adds up the whole change history for every row it writes, and a history size that cannot be measured now refuses the write instead of switching the size limit off (ANTS-5046)

  • A push interrupted during its Qt 6.2 compile check now stops that check's container, and the next check does not trust the half-built tree (ANTS-5124)

  • The "Close tab?" question box now matches the Ants theme, can be resized and remembers its size (ANTS-5123)

  • The build no longer prints its GCC warnings, and the tests they pointed at still check what they read (ANTS-5114)

  • project_query refuses a file too large for its memory limit before reading it, and its Lua callbacks no longer leak memory when a query runs out of room (ANTS-5070)

  • Code searches stop reading ripgrep's output past a size limit and say the answer is partial, instead of holding all of it in memory (ANTS-5052)

  • The Claude status tick stops reading saved conversations once none of the rest can be the live one (ANTS-5048)

  • project_query stops building a result as soon as it is over its size limit, so a small Lua snippet can no longer exhaust memory (ANTS-5069)

  • Independent Review and Cold-eyes never walk into build and dependency folders (ANTS-5058)

  • The test-audit partition never walks into build folders, walks the project once for all patterns, and keeps a project that sits inside a folder named build (ANTS-5062)

  • doc_citations reads each quoted document once per call, inside its read budget and size limit (ANTS-5054)

  • The subsystem lane cache is locked, so the window and the MCP worker can read it at the same time safely (ANTS-5074)

  • The Compiler Warnings audit check can finish, and no longer leaves a build folder behind in /tmp (ANTS-5039)

  • A verify_changes gate that times out stops everything it started, with SIGTERM first so a build can stop cleanly (ANTS-5063)

  • A timed-out or cancelled audit check, and an audit_run tool past its time cap, stop every process the tool started (ANTS-5038)

  • The review engines share one locked file cache with a size limit, so the window and the MCP worker no longer write one cache at once (ANTS-5056)

  • recent_errors finds every missing-include hint in one project scan instead of one scan per symbol (ANTS-5053)

  • recent_errors reads a line the terminal wrapped as one line, so an error wider than the pane is reported whole (ANTS-5061)

  • co_change_family clips a long match in one pass, so a very long line no longer stalls it (ANTS-5066)

  • Closing a tab while its Background Tasks window is open no longer risks a crash; the window closes with the tab. (ANTS-5049)

  • The test-result tools now name the tests that failed, not just how many failed. (ANTS-5064)

  • The link checker no longer reports false broken links in a document too large for it to read; it skips that document instead of reading part of it. (ANTS-5055)

  • The test-audit summary returns one page of reports when no page size is given, instead of all of them. (ANTS-5065)

  • An audit of recent changes checks the whole project when git fails, instead of reporting a clean result. (ANTS-5043) This covers a repository with no branch called main, a mistyped tag, and a git call that times out.

  • Dialogs remember their size when you close them with the close button or Esc. (ANTS-5037)

  • Filtering or sorting audit results no longer lowers how serious findings look. (ANTS-5041) A line that two tools both flag is now ranked higher, as intended.

  • Scrolling back through a command's output stays smooth even when that output is huge. (ANTS-5027) The command name pinned at the top while you scroll is now built from only as much text as fits, instead of rereading the whole output on every redraw.

  • Closing a window now asks first when a program such as an editor is still running in any of its tabs, as closing one tab does. (ANTS-5120) The question offers the same "don't ask again" choice as the tab one, and turning it off applies to both.

  • A program that floods the terminal with output no longer holds up Ctrl+C and other keys, and a finished shell is reported once. (ANTS-5026) Ants stops reading as soon as it falls behind and resumes when it catches up, instead of queueing output without limit.

  • Terminal output can raise at most ten desktop notifications a minute per tab. (ANTS-5033) A program or file that prints notification codes in a loop no longer starts a notification process for each one.

  • Closing one of several windows closes its tabs and ends their programs, including the original window, which used to keep them running out of sight. (ANTS-5118) The last window still keeps its tabs for the next start. The tabs close a moment after their window, so a logout that closes every window still saves them all.

  • A second window (File → New Window) opens with one fresh tab instead of reopening every saved tab, and a restart brings back the tabs of every open window. (ANTS-5032) Saved tabs are restored once per run, into the first window. Each window now saves the whole run's tab list rather than only its own, so the last window to save no longer drops the others' tabs.

  • Changing View -> Opacity updates terminals that are already open (ANTS-5034)

  • audit_run reports a tool that ran out of time as timed out, not crashed, and ignores its half-finished output (ANTS-5044)

  • A saved tab that cannot be restored keeps its session file, restores all or nothing, and very large scroll histories save only what can be restored (ANTS-5031)

  • Re-running a command reads the right line after the scroll history has filled up (ANTS-5029)

  • Opening a link from the suspicious-link warning is safe while output keeps streaming (ANTS-5028)

  • Dialogs keep remembering their size after a second window closes (ANTS-5036)

  • Audit runs and project queries no longer leak memory, and the everyday checks now catch new leaks (ANTS-3847) Each audit tool's process kept itself alive and was never freed, and a refused project.read or project.list skipped its cleanup. Both are fixed. The sanitized test runs before a push, in tools/ci-parity.sh and on CI now check for leaks, so a new one fails the run instead of going unnoticed.

  • Closing Ants while a Claude tool call waits on the window no longer hangs (ANTS-5113) At shutdown, a tool call already waiting for the window thread used to wait forever while the window waited for it. Shutdown now releases that call without running it, so Ants exits.

  • The verify.json trust prompt opens on the main thread (ANTS-5025) verify_changes runs off the GUI thread, so the "Trust .ants/verify.json?" prompt used to be built and run on a background thread, which Qt does not allow. It now always opens on the main thread. If Ants is shutting down, no prompt is shown and the untrusted commands are not run.

  • indie_review_dispatch replies instead of hanging Ants (ANTS-5024) The verb's worker now receives the project root the provider has already resolved on the GUI thread. It used to resolve the root itself, which asked the GUI thread for every tab's cwd while the GUI thread was waiting for that worker, so every call froze Ants for good.

  • Ants's own git checks can no longer make your git commit fail with "index.lock: File exists" (ANTS-4999) Every read-only git check Ants runs now skips git's optional index lock. That covers the Review button's two-second check, the git MCP verbs, audit scoping and the Claude git-context hook. A hook that is already installed picks this up when reinstalled from Settings.

  • The AI review job pool reports a finished round exactly once, even when a job completes the moment it starts (ANTS-5000) Before, each nested step announced the same round again. Only test code reached this path; live AI requests always finish later.

  • Four end-to-end checks now fail when the typed command never runs (ANTS-5013) The ANSI colour, CJK, scrollback and resize cases looked for text the typed command already contained, so the terminal's echo satisfied them. Each now looks for text only running the command can produce.

  • AI answers from a provider that ignores streaming now appear, and a server's own error message now reaches you (ANTS-5007) LlmClient keeps the raw reply until an SSE line proves it is a stream, then reads a plain JSON answer or error body whatever the HTTP status.

  • Reaching the AI answer size cap now ends the download (ANTS-5008) The request is aborted at LlmClient::kMaxBytes and reported as a truncated answer, even while the server holds the connection open.

  • Closing an AI-backed window mid-request no longer crashes the app (ANTS-5002) An AI client destroyed with a request in flight now shuts down quietly. The audit dialog's AI triage could reach this when closed mid-answer.

  • Long AI answers that arrive all at once are delivered whole (ANTS-5015) An answer longer than one parsing pass, sent in a single burst by a fast or local AI server, used to come back cut short with no warning.

  • Review dialogs report a failed AI request as a failure, not a clean review (ANTS-5003) A lane whose request failed now shows the error in its tab and is named in the status line, and it is left out of the results. Test audit's resume now retries such a chunk instead of treating it as reviewed.

  • The review dialogs' Dispatch button is disabled while a round runs (ANTS-5004) A second click mid-round used to wipe the collected reports and pay for every lane again.

  • Dispatching a review with no lanes says so instead of reporting a finished review (ANTS-5006)

  • The sanitizer CI job's test step has room to finish again (ANTS-5011) Passing runs were finishing within a minute of the 16-minute limit, and three were cut off at it. The limit is now 22 minutes, re-measured from recent runs, and the job's overall cap rises to match.

  • The end-to-end test cases wait for the screen instead of sleeping a fixed second (ANTS-5012) A slow instance, such as the sanitizer build in CI, no longer fails them. Each case now polls for its expected text for up to about 15 seconds.

  • The blocked-roadmap-write message now describes the rule the code actually enforces (ANTS-4749) A roadmap write blocked for a missing plain-language summary told you the summary had to be the first line of your note. It never did — the key may lead any line of the note. Following the old wording cost nothing; reading it as exhaustive led you to conclude a fix would not work when it would.

Security

  • Files that hold your project's content or your terminal's output are no longer written when Ants cannot make them readable by you alone; settings and other small files log a warning instead (ANTS-5151) Covers audit exports and caches, saved reports, the debug log, and session logging and recordings, which now say in the status bar why they did not start. The roadmap database warns rather than refusing, so every project keeps working.

  • Resuming a Claude session quotes the session id in the command it types (ANTS-5080)

  • The command that asks Claude to review an audit quotes its prompt, so an unusual results path cannot run shell code (ANTS-5079)

  • The optional key-press debug log no longer records what you type, so passwords typed at a prompt stay out of it (ANTS-5077)

  • A remote-control command name can no longer add fake lines to the debug log (ANTS-5093) The remote-control socket wrote the command name it received straight into the debug log, so a name containing a newline could add a line that looked like a real log entry, and an escape sequence could affect a terminal showing the log. Control characters are now written as visible escape codes.

  • OpenAI service-account and admin keys are now scrubbed before text is sent to an AI model (ANTS-5104) The secret scrubber recognised OpenAI project keys and the old key format but not the sk-svcacct- and sk-admin- keys OpenAI now issues. Both are now replaced with a [REDACTED] marker.

  • A large MCP answer saved to disk is discarded if Ants cannot make the file private (ANTS-5104) The saved copy holds the answer itself. If its permissions cannot be set to owner-only, the file is removed and the answer is returned directly instead.

  • Resetting the terminal no longer lets a program get around the clipboard-write and user-variable limits (ANTS-5122)

  • Re-run Last Command shows the command and asks before running it, unless prompt signing proves the command came from your shell (ANTS-5029)

  • Audits no longer take tool options from the audited project's .audit-config.json, so a downloaded project cannot use them to write files or run code (ANTS-5045)

  • Batch AI triage and indie_review_dispatch remove passwords and keys from source text before sending it to the AI endpoint (ANTS-5042)

  • SSH bookmark extra arguments are checked the way ssh reads them, including spaced, quoted and combined option forms, config files and library-loading options (ANTS-5060)

  • A keyless AI request sent over plain http to another machine now warns that it is not encrypted, and still sends (ANTS-5010) The AI chat, the review dialogs, the audit dialog's triage and the indie_review_dispatch reply each show the same warning naming the host. Localhost and https endpoints are unaffected.

  • The indie_review_dispatch MCP tool now runs the same AI send checks as the rest of the app (ANTS-5018) It refuses an API key over plain http to a remote host, private and link-local IP addresses, and credentials embedded in the URL, and it no longer follows redirects.

v0.7.110-rc1Pre-release

0.7.110 RC1 — Patron preview

This release shipped without written notes.

v0.7.109

0.7.109

Added

  • Roadmap items can now be searched by where they came from (ANTS-4985) roadmap_query gained a source filter — a case-insensitive prefix, or a list of them — so "how much review work is still open?" is one call. Each item now reports its source as a field alongside its kind and lanes. The provenance was always recorded and nothing could read it.

  • Filing a review's fix under the wrong label now says so (ANTS-4989) Adding a roadmap item whose origin is a review but whose kind is a plain fix returns an advisory suggesting review-fix or audit-fix. It never refuses the write — the label may be deliberate — and it names which origin it matched.

  • A roadmap publish that overwrites unrecoverable text now keeps a copy of the file and names it (ANTS-4947) discarded_backup_paths[] rides the lost-text arm of a store-backed roadmap write. A lost line is text the render reproduces in no styling, so the file on disk was its only copy and the capped twenty-line discarded_text echo was all a caller had to retype from. The copies go under the shared data directory, never beside the project, so they cannot dirty git status. Restyled and repunctuated drift keeps nothing — that text survives in the render.

  • similar_code accepts query as an alias for shape (ANTS-4951) shape wins when both are sent, as every other alias here does.

  • roadmap_migrate reports the note population it capped, and lets you raise the cap (ANTS-4559) notes_summary maps each note code to its total count over all notes, tallied before the row cap and never truncated, so a truncated notes[] can still name what it dropped. max_notes moves the row bound, default 200 and clamped to [1, 2000]; the envelope echoes the effective value.

  • roadmap_query accepts q as an alias for query (ANTS-4927) Unrecognised, q was dropped and the call answered with the whole list, which reads as a search result. Follows the existing filter/status and max_results/limit aliases; query wins when both are sent.

  • apply_edits accepts old_text/new_text and a batch-level path (ANTS-4936) The spelling roadmap_log's op:"amend_body" uses now works here too, alongside the native Edit tool's old_string/new_string. A path given once at the top level supplies every edit that omits one; a per-edit path still wins.

  • doc_lint can repair a drifted table of contents (ANTS-3669) Pass fix:true and the one auto-fixable finding is corrected in place: a missing Contents entry is inserted in document order, a duplicated one is removed. Every other line is left exactly as it was — sub-entries, free-text rows and entries pointing at nothing are all preserved, because a rebuild-from-headings would quietly delete them. dry_run:true shows what would change without touching the file. Everything else the verb reports stays report-only.

  • doc_lint — every deterministic document check in one call (ANTS-3663) Runs doc_integrity, doc_citations, doc_dedup, doc_symbols and spec_lint over one enumeration and one shared read, returning a single findings list in a total order so two runs diff cleanly. Narrow it with checks[]; an unknown name is refused rather than read as "all". spec_lint is applied only to documents under the project's specs directory, so a README is no longer flooded with findings about invariants it does not have. Reports only — nothing is written.

  • search the roadmap by kind of work (ANTS-4836) Every item records what kind of work it is, and nothing could select on it — so "which review fixes are still open?" meant fetching every open item and sorting by hand. roadmap_query now takes a kind, composing with status, section and keyword. An unrecognised kind is refused with the list of real ones rather than quietly returning everything, which would have read as "nothing of that kind exists".

  • read a long roadmap item's latest note in one call (ANTS-4904) A long-lived item is an append-only progress log, so what a resuming session wants is its last paragraph — and the body could only be kept from the head, with a fixed 1 KiB tail. Asking for more raised the head with it until the reply spilled and the preview dropped the body altogether: three calls and a hand-picked byte offset to read one closing paragraph, and an offset guessed too high missed the note silently. body_from_end on a targeted id fetch keeps the END instead, sized by max_body_bytes, and every truncated body now reports body_bytes — its true size — so what is missing is a number rather than a guess.

  • project_settings op:"get" — the read, named like one (ANTS-4903) Asking what a project has declared meant calling op:"detect", whose name and documented purpose are to propose settings for a project that may have none — so the answer arrived with an inert suggestion block attached, and a session reading the op as write-shaped could skip the check entirely. op:"get" returns the declaration and what is still unset, and no proposal. Both are read-only and share one implementation, so they cannot disagree about what is declared.

  • UnrealScript (.uc) files are outlined, indexed and searchable for definitions (ANTS-4902) file_outline reported unknown with no symbols for .uc, so a project whose whole runtime surface is UnrealScript had to read those files whole. It now outlines as brace-family — classes, structs, consts and every function spelling the language uses, including the modifiers no other brace language has. codebase_index and find_definition admit the extension in the same change, which the three lists' own in-step rule requires. var members are the known gap.

  • spec_lint reports how many invariants it recognised, so a spec it read none of is visible (ANTS-4894) A spec whose invariants are written in a form the format standard does not define parses to zero, every per-invariant check then runs over an empty set, and the envelope was byte-comparable with one that checked thirteen. invariants_found is emitted on every run — zero is the alarm — and the hint that says invariant_no_test always runs now points at it rather than reading as evidence that any invariants were seen.

Changed

  • The roadmap format standard now requires every item to record where it came from (ANTS-4985) Source: moves from optional to required (format v1.2), matching what the writing tool has always enforced, and the standard now states that an item's kind and its origin are separate questions — a review legitimately produces work of any kind.

  • A narrowed response names the fields it does carry, not just the ones it doesn't (ANTS-4930) fields_available now accompanies fields_unmatched, so a misspelled field name is visible rather than inferred, and a field that is simply not populated on this call can be told from one this backend never carries.

  • The body-scrub warning names the text it removed (ANTS-4938) body_scrubbed_tool_xml now carries removed_fragments, so a caller can confirm the strip from the envelope instead of re-reading the stored body. It reported a count alone, and the re-read it asked for usually found nothing missing.

  • workspace_search states that caller_cwd picks any tree to search (ANTS-4928) Pointing caller_cwd at another project, or at a path outside any repo, searches that tree. That was documented only inside the lane argument, so the verb read as project-scoped.

  • project_query names file enumeration where the tool is chosen (ANTS-4929) Its one-line hint now covers listing files by name via project.list, and project.list leads the API list. Previously the hint described counting inside file contents, so the enumeration case was never found.

  • a refused absolute path now names the way to read that file (ANTS-4899) A file handed to a session from elsewhere on the machine was refused by file_outline and read_region with nothing else to go on, so the fallback was reading the whole thing raw — the one thing those tools exist to avoid, and worst on the large documents where it matters. The boundary has not moved: there was already a way to read such a file, and the refusal now names it. Reads only, absolute paths only, and only for a file that exists — a relative ../ is still a traversal and is still answered with nothing.

  • workspace_search now says what its enclosing-symbol annotation managed (ANTS-4901) Asking for the annotation over files whose language has no outline returned every row unannotated and said nothing, which is indistinguishable from the documented case of a match sitting above the first symbol — except that here no row in the file can ever be annotated. The envelope now carries enclosing_annotated and, when it applies, enclosing_symbol_unavailable naming those files. Both ride the opt-in.

  • The spec standard fixes one review-log table shape, and the spec skeleton now carries it (ANTS-3682) Review logs had drifted into many incompatible column layouts because the spec skeleton handed authors a placeholder where a table should be. The skeleton now ships the header row, and § 5.7 fixes the columns: one count per review question, n/a where a genre does not ask one, and the outcome cell last. Existing logs keep the shape they have.

  • roadmap_migrate's re-run refusals carry the project's registration as fields (ANTS-4893) Its description names the dry run as the read-only way to ask "is this project already migrated?", but both re-run guards fire before the preview is built — so on a project whose stored export_slug was not derived from its directory name, that question came back as a refusal with the answer only in the message. The refusal now carries project_id, store_backed, stored_export_slug and stored_project_name. It still refuses: a preview that reached a different verdict from the real call would be worse than an unanswerable one.

Fixed

  • Creating a level-2 roadmap section no longer swallows the level-3 sections below it (ANTS-4848) roadmap_log op:"create_section" could adopt trailing subsections, producing a section whose later appends landed somewhere unexpected. Already in 0.7.108; recorded here late.

  • roadmap_log's description now names every op that refuses on a pass-headings roadmap (ANTS-4863) It said only create_section refuses; amend_body refuses too, so a caller planned around a limit the tool did not actually have. Already in 0.7.108; recorded here late.

  • doc_integrity no longer reports false duplicate headings in a mirrored standard (ANTS-4878) Heading numbers were counted across the MIRROR boundary, so every standard carrying a project delta plus its upstream copy reported duplicates that were not there. Already in 0.7.108; recorded here late.

  • roadmap_query no longer reports one project's storage backend to another caller (ANTS-4885) The backend witness was cached on the roadmap's path alone, so a second caller could be told which backend a different project was using. Already in 0.7.108; recorded here late.

  • spec_lint no longer blames a document for a check it did not run (ANTS-4889) Its single-file path reported test coverage as unchecked and attributed that to the spec, while the same spec passed the same check on the other path. Already in 0.7.108; recorded here late.

  • roadmap_migrate now names the second format when a roadmap mixes two (ANTS-4492) A mixed-format roadmap was classified by majority with nothing saying the other format was present, so the items written in it were silently reinterpreted. Already in 0.7.108; recorded here late.

  • An unknown MCP tool now reports the same code in the envelope and in telemetry (ANTS-2173) The refusal envelope said unknown_tool while telemetry recorded tool_not_found, so the two could not be correlated. Already in 0.7.108; recorded here late.

  • The changelog and test-audit parsers now use the shared CommonMark fence rules (ANTS-4404) Both hand-rolled their own "is this a code block?" test, and both got it wrong in ways that can swallow the rest of a document — a line quoting fence syntax opened a block that never closed. Measured before changing anything: neither fault currently bites on this project's own files, so this closes a latent defect of a class that has already caused two real bugs.

  • ants.notify() now shows a notification instead of doing nothing (ANTS-4273) The plugin API's notification signal reached the plugin manager and stopped there — the call was accepted, raised no error and had no effect. It now goes to the desktop notification service, falling back to the status bar when that does not take it. Plugin authors: the "currently a no-op" note is gone from PLUGINS.md.

  • The Ctrl+Shift+V paste handler guards the clipboard pointer Qt documents as nullable (ANTS-3831) All three paste branches dereferenced QClipboard::mimeData() without a check. A null now makes the paste a no-op instead of undefined behaviour. No crash was reproduced — the documented trigger does not apply to this call site — so this closes a documented contract, not an observed bug.

  • A test run can no longer write to the developer's real git repository (ANTS-3841) Both test-bundle entry points now scrub the git environment variables that redirect which repository git acts on, and the shell tests that run git do the same. Without it, a session with GIT_DIR exported had its git-fixture tests operate on the real repo — one such run rewrote local main onto fixture commits and left stray branches behind.

  • The pre-push gate now passes from a git worktree (ANTS-3842) Git hands a worktree's hooks an absolute GIT_DIR, which reached the test suite and pointed its git fixtures at the real repository, so the gate failed for a reason that appeared nowhere in the diff. Fixed by the same scrub as ANTS-3841.

  • The pre-push ASan gate now resolves a pending CMake regeneration and gates on the real edge count (ANTS-4536) A changed CMakeLists.txt leaves a pending regen that hides every pending build step behind it, and the gate stood the sanitizer leg down whenever it saw one — so the leg went dark on exactly the pushes most likely to need it. The hook now runs the regen (a CMake re-run, not a build) and measures what it reveals.

  • The pre-push interrupt-marker skip now states how old the marker is (ANTS-4943) The marker that disables the sanitizer leg after a killed build never expires, and its skip message read identically whether it was written minutes or days earlier. It now reports the marker's age, so a long-dark gate is visible in a run that still ends "push allowed".

  • ANTS_PREPUSH_NO_ASAN can now be used for the push it documents (ANTS-4883) The hook's own test inherited the variable from the caller, so exporting the documented escape hatch made the suite the hook runs go red — the push was refused either way. The test now scrubs the hook's tunables and sets only what each case exercises.

  • Three MCP flags whose false is the answer now survive terse mode (ANTS-4956) file_in_sync, store_backed and markdown_rewritten were emitted correctly and then dropped as dead weight, because terse responses are on by default. file_in_sync was therefore present exactly when the roadmap was in sync and absent when it had drifted; markdown_rewritten is always false and so never appeared at all.

  • roadmap_migrate op:"init" no longer writes a link that cannot resolve (ANTS-4962) The generated roadmap names the format standard in prose. A project bootstrapping one has no docs/standards/ to link to, so the link was dead in the file the project is told is its source of truth.

  • feedback_query's path help no longer names a folder holding no feedback file (ANTS-4980) It points at derivation instead. The corpus root is configurable, so a literal path in the help is wrong on any machine that moved it.

  • changelog_log op:add writes at the top of a mixed changelog instead of into its tail (ANTS-4563) A ## [Unreleased] whose dated topics lead over a legacy flat tail fell between two guards that answered its shape in opposite directions, and the entry landed in the gap — appended into the tail, reported ok:true, measured on one project at roughly ten thousand lines below the newest entry. Both guards now read one classifier, and op:"add" routes such a section to the dated-topic form at the top, saying so with routed_to_subsection. A direct call to the flat writer refuses the new mixed_section rather than burying anything.

  • spec_lint says whether a project ADOPTED the global spec-format standard or adopted none (ANTS-4926) sections_source prefixing a hit with ~global/ said which standard answered and nothing more. A conforming project keeps no local copy — the standard forbids one and sends deltas to docs/standards/spec-format-overrides.md — so adoption looked identical to a project with no format standard at all, and a caller dropping section findings on that prefix dropped real ones. sections_source_reason now separates the states, with sections_source_hint on the three arms a caller can misread. Resolution is unchanged: the overrides file is reported, never read.

  • The MCP usage figures now count a call by what actually happened to it (ANTS-4457) Two errors cancelled each other out badly. A call that returned "nothing changed since last time" — the cheapest possible result, where Ants sends no data at all — was filed under failures, which is the column meant for wasted effort. And a call a tool rejected for bad input was filed under successes, because only rejections made by the dispatcher itself were counted. Both are now filed correctly, and the diagnostic trace names the rejection instead of reporting "ok".

  • Ants no longer re-reads its settings file about eight times a second (ANTS-4457) A background check that watches for the Claude "Switch model?" prompt re-opened and re-parsed the whole settings file on every one of its polls, and those polls run continuously by default whether or not Claude is even running. It now reuses the copy already held in memory, which is refreshed whenever the file changes on disk — so changing a setting still takes effect as before.

  • Claude status no longer gets stuck needing a tab switch to wake up (ANTS-4457) Ants finds the Claude session's log file to follow what Claude is doing. It looked exactly once, right after spotting Claude start — and Claude often has not written that file yet at that moment. When the first look came up empty, Ants never looked again for the rest of the session, so the status stayed blank and every update from Claude was ignored. Switching tabs and back was the only cure. It now keeps looking until it finds the file, and stops as soon as it does.

  • Permission prompts and failed tool calls now reach the status bar (ANTS-4457) Ants had working code to show when Claude is waiting for you to approve something, and when a tool call failed — but it never asked Claude to tell it about those two things, so neither ever happened. Both are now requested. If you installed the Claude hooks before this version, Settings will stop saying "Hooks installed" until you press the button again; that is accurate rather than a new fault, because the two new ones genuinely are not set up yet.

  • Resetting the terminal no longer throws away your theme and scrollback setting (ANTS-4456) Some programs reset the terminal when they finish, and reset does it on purpose. That used to snap the text colours back to the built-in ones and shrink your scrollback back to the default — while the cursor and selection colours stayed on your theme, so you were left with a mismatch. A reset now clears the screen and history as it should, and leaves your settings alone.

  • Selection highlight no longer runs past the end of Chinese, Japanese, Korean or emoji text (ANTS-4456) Characters that take up two columns were being counted as three when the highlight behind them was drawn. The error added up, so a selection containing several of them spilled further and further to the right of the text it belonged to.

  • Choosing a new bold or italic font now takes effect straight away (ANTS-4456) Text already on screen kept drawing in the previous font until something else forced a redraw — resizing the window, or changing the font size — so the setting looked like it had been ignored.

  • Opening a file by name from the terminal can no longer consult the wrong process (ANTS-4456) The terminal looks up your shell's current folder to resolve a short filename. The record of which shell to ask was being updated on one thread and read on another without coordination, so just after a shell exited the lookup could land on an unrelated program that had been given the same identifier.

  • A terminal whose shell has gone away no longer pins a CPU core (ANTS-4456) When a write to the terminal failed for good — the shell reaped, the connection torn down — the code that retries queued keystrokes kept being woken, kept failing, and never stopped, burning a whole CPU core silently for as long as the window stayed open. It now gives up on the first unrecoverable error. Keystrokes discarded that way are also reported in the log, as they already were on the two other paths that can drop them.

  • Sending a multi-line scratchpad now submits the text, and only the text (ANTS-4456) Sending from the scratchpad sent the Enter before the paste confirmation was answered. The shell received a bare Enter first, running whatever was already typed at the prompt, and the scratchpad text then arrived without being submitted; cancelling the confirmation did not take the Enter back. The Enter now travels with the paste, so it is sent when the paste is and not at all when it is cancelled. Multi-line sends always took this path, since a newline is what raises the confirmation.

  • Sixel decoding is now bounded by a whole-payload work budget (ANTS-4456) Displaying a file containing a crafted Sixel image could hang the terminal. The decoder now stops and reports an inline error when a payload's work exceeds a budget derived from the image it declares, alongside the existing dimension and image-size caps. Ordinary images, including multi-colour ones that redraw a row once per colour, are unaffected.

  • session_orient counts findings that carry no Proposed ID slot (ANTS-4941) A feedback finding with no **Proposed ID:** line at all is a different state from one whose slot is blank, and the session-start summary could see only the second. Files whose sole pending input is slotless now list, under their own suspected_untagged_count.

  • doc_citations caps an ambiguous quotation's candidate list (ANTS-4939) The citation path has capped its candidates since ANTS-3636; the quote path returned every same-named file in the tree. All three ambiguous arms now share one emitter and report candidates_total and truncated_candidates.

  • a checker that printed its help text is no longer reported as 92 findings (ANTS-3846) When a code checker refuses to run it prints its usage banner and exits successfully, and the audit read that banner as its output — so one report announced 92 findings whose contents were lines like "--help Display available options". A tool that analysed nothing looked exactly like a tool that found problems, which is worse than the tool being missing: missing reads as missing, this read as coverage. Such a run is now reported as an incomplete tool. The markers are matched only at the start of a line and only near the top of the output, so a genuine finding that mentions usage is still a finding.

  • one refusal code for a bad kind, whichever route reported it (ANTS-4750) Setting an unrecognised kind of work was refused under one name through roadmap_log's own argument and a different one through op:"amend_field", so a caller that handles the first and re-prompts for a valid kind silently mis-handled the second. Both now report bad_kind, and the refusal lists the real kinds — taken from the same source the check itself uses, so the two cannot drift.

  • a batch of changelog entries now reads in the order it was written (ANTS-4854) Each entry is inserted at the top of its category, so applying a batch front-to-back left the last entry on top and the release notes came out backwards from the list their author wrote. The entries are now applied in reverse, which puts them on the page in input order. This replaces an earlier contract of being byte-identical to the same entries added one call at a time — a property nothing checks, where the order on the page is one every reader sees. The reports back to the caller are unchanged and still follow the input list.

  • "where is this defined?" no longer lists local variables as definitions (ANTS-4880) A function-local variable whose value continues on the next line was reported with the same kind as the real definition, so a symbol appeared to have several homes and a reader could be sent to any of them. Such lines were already reported as declarations when they fitted on one line; they are now treated the same way when they wrap. They are still listed — knowing where a name is declared is the point — they just stop claiming to be the definition.

  • a feedback corpus in its own folder no longer causes a second, empty record (ANTS-4900) The guard that stops a derived feedback file landing where no maintainer sweep reads it only looked UP the tree. Move the corpus into a folder of its own — which is what happens once a machine carries two dozen of these files — and it sits BELOW instead, so the derivation read as "this is the first file in a new corpus": the read answered "nothing filed yet" and the write created an empty second record and reported success, while the real file went on sitting one directory down. The derivation root's children are now probed too, and the refusal names both the corpus it found and the setting that would point derivation at it.

  • cross-project id resolution survives the feedback corpus moving into its own folder (ANTS-4905) feedback_query resolves a foreign-prefix id against the project whose roadmap owns that prefix, and found that project by scanning the feedback file's own directory for sibling projects. That held while the corpus WAS the shared root; once it moved into a folder of its own, the scan found no projects and every id fell back to foreign_repo — measured at 64 of 64 from a contributor project. It now searches the corpus directory and then its parent, nearer first. Ownership is still decided the same way, so a prefix nobody owns is still reported as foreign.

  • test bundles no longer read the developer's live settings (ANTS-4898) Both bundle mains sandboxed XDG_DATA_HOME so a test could not open the real roadmap store, and left XDG_CONFIG_HOME alone — so every test process read ~/.config/ants-terminal/config.json. Setting one real setting (the shared feedback corpus root) turned twelve tests red, and their writes went to the real corpus rather than the temporary directory each of them asserts against. Both mains now redirect the config directory too, per process; a test that wants its own config still overrides it.

  • session_orient's feedback backlog now follows the configured corpus root (ANTS-4896) The feedback_pending block scanned the parent of the project root and nothing else, while feedback_query had consulted claude.mcp_feedback_root since ANTS-4471 — so moving the shared corpus to a folder of its own left the session-start summary reporting nothing waiting rather than saying it had looked somewhere else. It now scans both roots, names the declared corpus as shared_root, and carries searched — the directories actually read — so an empty result says where it looked. The scan moved into its own builder, which is what let it be tested at all: the bundle verb refuses without a window.

  • spec_lint checks a project against its own spec standard, not a mirrored copy of the global one (ANTS-4895) Where a project keeps a verbatim mirror of the global spec standard beside its own, the mirror was consulted first and the project's own required-section list was never read — so specs were graded against a rulebook the project had deliberately replaced. A project shipping no standard of its own is unaffected.

  • changelog_log's schema no longer tells callers that summary is ignored under add_from_roadmap (ANTS-4888) It has overridden the cited bullet's headline since ANTS-4360, with category, id and the Layman body still inherited, but the schema still described it as ignored. A session read that, took the roadmap headline into its release notes, and hand-edited the file afterwards — the round-trip the op exists to remove. Three descriptions corrected; the behaviour was already right and is unchanged.

  • roadmap verbs run from a git worktree are served by the project's store instead of patching the worktree's copy (ANTS-4887) The store keys a project on its main checkout, so a worktree was a path with no entry: reads answered from the worktree's ROADMAP.md, which is generated output, and a write patched that file, reported ok:true, and was erased by the next render from the main checkout. A worktree now resolves to its main checkout — read out of the .git file git already writes, with no subprocess — provided that checkout really holds the project's roadmap. Anything else is left exactly as it was.

  • project_conventions no longer states a convention whose source document is not in your project (ANTS-4460) The rule table is curated and cites Ants Terminal's own documents, so on another project it stated this project's conventions as that project's — one report was told to write a tests/features/<name>/ conformance test by a project that keeps unit tests elsewhere. A rule whose cited document is not present under the caller's root is now dropped and listed in conventions_dropped, with a warning naming the missing documents and sources_resolved saying how many were found. The only previous signal was a per-row exists:false, which compact:true drops by design.

  • spec_log op:append_loop refuses instead of writing an empty row and discarding the caller's cells (ANTS-4886) The verb finds the log by its ## Cold-eyes loop log heading. Against a file whose table sat under a different heading it fell through to the bullet arm, which renders from label and body alone — so a call passing only cells wrote the literal row - ** ** — , dropped every cell, and returned ok:true. It then appended the heading it had failed to find, below the existing table, so the next call wrote into that stub and the file ended up with two logs. It now refuses in all three cases, and still creates the section for a spec that genuinely has no loop log yet.

  • roadmap verbs called from a project subdirectory are now served by the roadmap store (ANTS-4884) The store-vs-markdown dispatch asked the store by caller_cwd, and a project is keyed on its canonical root — so from a subdirectory the lookup matched nothing and the verb fell back to reading and splicing ROADMAP.md, which is an output of the store. Reads answered from a file that could lag the store; writes reported ok:true and were discarded by the next render. Both dispatchers, the render's containment check and the project-row lookups now resolve the caller's directory to the project root first.

  • roadmap_log's id allocator now applies its store floor when called from a project subdirectory (ANTS-4882) The floor that stops a migrated project reissuing an id the store already holds asked the store by caller_cwd. A project is keyed on its canonical root, so from a subdirectory the lookup matched nothing and the floor did not apply. Both op:"append" and op:"append_batch" now ask by the directory the roadmap was resolved under, which is the registered root in every layout the resolver supports.

  • doc_citations counts each quotation once when the document contains a fenced code block (ANTS-4697) A blank line after a closing code fence used to open a scan window of its own, so every quotation in the paragraph below it was counted twice — inflating quotes_checked and the quote_counts buckets that a reviewer reads.

  • A fields=-narrowed MCP reply that matched nothing now still says it succeeded (ANTS-4877) Asking a verb for a field it does not carry returned only the list of names it could not find, with no ok — indistinguishable from a call that produced no envelope at all. Such a reply now carries ok. A request where at least one name matched is unchanged: the matched field already proves the call worked.

  • Mirrored standards no longer carry dead links (ANTS-4875) A link a mirrored standard cannot carry — the owner's review history, a foundation document, a skeleton — is now copied down as plain text instead of as a link, so it no longer 404s for a reader on GitHub. The path is still named. tools/check-standard-mirrors.sh does the de-link inside the same step that feeds its drift check, so the copy stays derivable from its owner and no mirror is hand-edited.

  • The published 0.7.107 RPM now builds on Fedora 44, via a backport carried in the spec. (ANTS-4870) Fedora's package repository was stuck one release behind because 0.7.107 could not build there, while openSUSE, Leap and Mageia were unaffected. The already-released 0.7.107 now carries the fix as a packaging patch, so Fedora users no longer have to wait for the next release. The patch applies only to 0.7.107 and older and is skipped from 0.7.108 onward, which carries the fix directly.

  • cut-rc.sh hotfix --continue resumes when the emergency release is already tagged. (ANTS-4872) An emergency release interrupted between tagging and writing its notes back to the main branch could not be finished by re-running the command; it refused, and the last steps had to be done by hand. It now recognises its own tag — the one sitting on the work in progress — and completes the remaining steps. A tag of that name made by anything else is still refused.

  • cut-rc.sh cycle no longer skips its second phase after an interrupted release cut. (ANTS-4871) The weekly cadence command would report success having done nothing, for the same reason new-rc used to refuse: it read the notes already moved into the version's own section as an empty changelog. It now looks for that moved work — but only where the version is not already released, so the ordinary run still skips cleanly instead of failing.

  • cut-rc.sh new-rc resumes after an interrupted run instead of refusing. (ANTS-4869) Cutting a release candidate first moves the pending notes into the version's own section, then builds. If the build was interrupted, re-running said there was nothing to release — reading the move it had already made as an empty changelog. It now recognises its own completed work and carries on, and still refuses a genuinely empty cut.

  • cut-rc.sh now names the release phase it abandoned when a pre-commit hook refuses one of its commits. (ANTS-4865) The run already stopped, but said nothing: the only text on screen was the hook's own refusal, which says nothing about the release, and a run piped through tail loses the exit status as well. It now reports which phase aborted, what did not happen (no tag, nothing pushed), and that the staged files are left as written so you can see what had been changed.

Security

  • Harder to forge the boundary around tool results (ANTS-4457) Results Ants sends to Claude are wrapped in a marker so Claude can tell where the data ends. Text inside the data that looked like that end-marker was being blanked out — but only in its plainest form. A slightly dressed-up version slipped through, which a file or a commit message could use to make the rest of its content look like it came from Ants rather than from the file. Both forms are now blanked.

  • Project folder paths from Claude's session files are now checked (ANTS-4457) Ants reads Claude's own records to work out where each project lives on disk. One of the three places it reads was checked for paths that climb out of their folder, and the other two were not — including the one it consults first. All three are now checked, and a project whose location cannot be trusted is skipped rather than shown.

  • Session logs and session recordings are now readable only by you (ANTS-4456) Turning on session logging, or hitting Record Session, saves a file holding everything the terminal printed — which can include keys or tokens a command happened to display. Those files, and the folders holding them, were being created with the system default that lets any other account on the machine read them. Both are now locked to your account only. Files saved by an earlier version keep the permissions they were created with; delete them or tighten them by hand if that matters to you. The three "Export..." menu items are unchanged, since you choose where those go and may mean to share them.

v0.7.109-rc1Pre-release

0.7.109 RC1 — Patron preview

This release shipped without written notes.

v0.7.108

0.7.108

Added

  • Roadmaps written as numbered passes can now use the database (ANTS-4803) A project whose roadmap is organised as numbered pass headings could be copied into the database but not written back out, so it could use none of the database features. It can now be written back out, which makes the database usable for those projects.

  • Review corroboration now spots the same problem found in several different files (ANTS-4814) When reviewers each cover a different part of the codebase they rarely cite the same line, so finding one recurring problem in several places used to read as no agreement at all. Those are now reported, grouped by the function they sit in.

  • Cross-lane review corroboration now reports near misses, and can group them on request (ANTS-4817) Two reviewers who find the same problem but quote different lines of it were previously reported as no agreement at all, with nothing to say why the result was empty. Those groups are now listed as near misses, and an optional tolerance merges them into one finding naming the span. Exact matching is unchanged unless that tolerance is asked for.

  • changelog_log and changelog_query can name a nested component changelog (ANTS-4812) A project that ships a second changelog for a bundled, separately versioned component can now write and read it through the verbs, with an optional path. Previously only the root CHANGELOG.md was reachable, so the nested file had to be edited by hand.

  • Shell scripts can be outlined, so Claude can see their shape without reading them whole (ANTS-4826) Asking for the outline of a .sh file used to return nothing at all. It now lists the functions and the settings at the top, and recognises a script with no file extension — a git hook or a launcher — from its first line. Those scripts also become visible to the code search and review tools, which had been skipping them.

  • Writing a note instead of a filename in a roadmap entry's Evidence line now warns you (ANTS-4527) That field is split on commas, so a sentence written there was silently stored as several fragments. The write still succeeds — it tells you rather than refusing.

  • Documented how doc_integrity really behaves (ANTS-3652) It answers a path that does not exist with a clean pass rather than an error, and silently stops after a fixed number of documents. The per-verb notes now say so.

  • Committed a linter configuration for the helper scripts (ANTS-4801) ruff.toml pins the checks to the ones that catch code that misbehaves, leaving formatting alone. Two real findings in the roadmap survey script were fixed with it.

  • spec_lint checks a spec's Invariants against its Tests section (ANTS-4623) Catches a spec that declares ten contracts and remembers to test eight — the two sections restate one set in different words, so they share no token to search for and could disagree indefinitely. Coverage written in notation that cannot be expanded, such as INV-1..7, is reported as unverifiable rather than as a pile of gaps.

  • find_definition resolves C++ data members (ANTS-3668) Asking where a struct field or an m_-prefixed member is declared used to come back empty, though the answer was sitting in a header. Measured on this project's own docs, the unresolved data-member share fell from 26% to 7%, which also lifts doc_symbols' signal-to-noise.

  • doc_symbols gains an only= row filter (ANTS-3689) only:"unresolved" (or "not_checked") drops the resolved rows nobody reads, which is what let a single dense document spill past the response cap. counts stays whole-document and symbols_filtered_out reports what was withheld; an unrecognised value refuses rather than widening.

  • Shell scripts are checked automatically, in CI and locally (ANTS-4518) The project ships a lot of load-bearing shell — git hooks, build and release tooling, test drivers — and nothing checked any of it, so a committed hook had silently lost a suppression its installed copy carried with no gate to say so. The gate blocks rather than reporting, because the surface is clean at the severity it runs. It covers the two hooks that carry no .sh extension, which are the scripts the original finding was about.

  • Review lanes over a test tree are labelled as such (ANTS-4806) A computed partition groups by directory, so the tests become a lane like any other — and a code review's scope excludes them while a test review's is exactly them. The lane is labelled rather than dropped, so each consumer takes what it wants instead of the tool choosing.

  • Review lanes report how many lines they cover, not just how many files (ANTS-4804) A lane holding one very large file counted as one file and tripped no size threshold, so the biggest lanes were the ones least likely to be flagged. Six projects reported it independently. Each lane now carries a line count and an oversized signal alongside the existing file-count one, which keeps its meaning.

  • A way to rewrite a roadmap entry's text from scratch (ANTS-4808) Editing an entry's prose could only replace a matched piece of it, so text that could not be matched had no route back — one project carried a kilobyte of garbled prose in a shipped entry for that reason. roadmap_log op:"set_body" replaces the whole body. It records the previous text in history and reports how much it replaced, and it refuses an old_text rather than ignoring one, since a caller who sends it believes the write is bounded by it.

  • An .editorconfig recording the layout this project actually uses (ANTS-4790) Spaces, four wide, LF, UTF-8, trimmed trailing whitespace and a final newline, with two deliberate carve-outs: markdown keeps trailing whitespace because there it is a line break, and the roadmap round-trip vectors keep both rules off because they back a byte-identical comparison. It does not declare a shell style — no setting makes shfmt agree with these scripts, so arming that tool would mean reformatting every one of them.

  • A spell-check vocabulary file, so typos is usable on this tree (ANTS-4789) A whole-tree run was almost entirely false positives — identifier fragments, VT100 notation, git SHA prefixes and namespace aliases read as misspellings. The config names the project's vocabulary rather than silencing the rule, so genuine misspellings still report.

  • A committed clang-tidy check set, so the C++ analyser actually analyses (ANTS-4787) With no configuration clang-tidy selects an empty check set: it reads every file, reports nothing, and exits successfully, which is indistinguishable in a log from a clean tree. This project's own audit_run had been invoking it that way. It now runs against a declared set of checks.

  • Conformance test holding the committed review partition to every source file (ANTS-4794) The build now fails when a file under src/ belongs to no lane in .indie-review/partition.json, when a lane names a path that has been renamed away, or when two lanes claim the same file. The reviewable-file set comes from the review engine's own walk rather than a second copy of its filters, so a change to what counts as reviewable moves the test with it.

  • Every source file now belongs to a review lane (ANTS-4793) A committed .indie-review/partition.json declares 32 lanes covering all 313 src/ files exactly once. The module map's prefix rule had left 189 files and 59,257 lines — mainwindow.cpp among them — in no lane at all, so a full-codebase review silently skipped a third of the tree.

  • Release gate: a CHANGELOG bullet that merely repeats its roadmap headline is now reported (ANTS-4759) A defect item's roadmap headline states the problem, so a summary copied from it puts the bug in the release notes where the fix belongs. tools/check-shipped-coverage.sh now flags any [Unreleased] bullet whose bold summary is byte-identical to the item's stored headline — 14 of 56 were, across seven commits, and all 14 have been rewritten to say what shipped. Advisory like its sibling check, so it cannot block a release over wording.

  • spec_lint accepts fields= and compact (ANTS-4663) A corpus run previously spilled and had to be parsed by hand.

  • roadmap_log op:"amend_headline" rewords a headline on a store-backed project (ANTS-4683) A headline was effectively immutable after creation while body notes accumulated a correction the headline went on contradicting.

  • roadmap_log op:"render" publishes the store's canonical ROADMAP.md (ANTS-4509) Normalising a file used to require inventing an unrelated write purely to trigger the render, which left a roadmap bullet nobody wanted.

  • A read-only way to ask whether a project is in the roadmap store and in sync (ANTS-4488) Previously only a dry-run answered it, as a side effect, at the cost of its whole note list.

  • Large MCP read bodies spill to a scratch file instead of flooding the reply (ANTS-2094) The response carries the head plus a pointer, so a big read no longer costs its full size in context.

  • roadmap_query mode:"headline_only" returns lean rows (ANTS-1881) {id, status, headline_oneline, section_slug} per bullet, skipping the body — roughly a tenth the payload on a dense section.

  • feedback_log op:"set_format_version" corrects a file's format marker (ANTS-4707) A feedback file declaring a version that was never defined could only be fixed by the hand edit the assign-id pipeline exists to avoid. The marker is a claim about the content, so the write happens only where the content already matches — stamping 2 needs the inline-**Proposed ID:** model, stamping 1 needs its absence, and either way round refuses with a hint saying which. Refuses a version above what the build can read, and refuses a file with no marker at all: building that shape is migrate_v2's job. An already-correct marker reports no change, so a corpus sweep is re-runnable; dry_run previews.

  • The pre-push docs-only set is checked against ci.yml's paths-ignore, and the skip decision is recorded (ANTS-4726) The hook's docs-only path list was a hand-maintained twin of the workflow's paths-ignore that nothing compared, so drift would let it skip a gate CI then runs. A static test now asserts the two name the same paths in both directions, and that the regex is anchored. The hook also prints the ref range and the paths its skip was decided on — previously a wrong skip and a right one printed the same line and exited 0. The misclassification that prompted this is still unexplained and the item stays open; the log is what a recurrence needs.

  • doc_citations resolves a backticked path::symbol to the symbol's line (ANTS-4748) Only where the path resolves and the symbol is unique in that file; the entry carries resolved_via:"symbol", since its start_line was looked up rather than authored. An overload, a name that has moved, or an unresolvable path resolves to nothing and is counted as unchecked — a false ok is worse than the admitted silence ANTS-4743 added. The unqualified suffix of a qualified definition is matched too, which is how the corpus writes these; two definitions sharing a suffix are ambiguous and refused.

  • invariant_check accepts paths as an alias for files (ANTS-4744) invariant_check now accepts paths as an alias for files, the spelling its sibling multi-path verbs use, so the argument name a caller carries across works here too. files wins when both are sent, and the refusal for neither names both spellings.

  • invariant_check says why a zero match can be wrong (ANTS-4742) invariant_check returning zero matches now says why it can be wrong. Matching is by file path only, so a spec naming the module by symbol was reported as no spec at all — the reading that sends a session to write against no contract. The zero case now carries path_match_only and a hint naming the fallback search. Matching itself is unchanged.

  • feedback_query does the stale-binary comparison itself (ANTS-4741) feedback_query now does the stale-binary comparison instead of telling you to make a second call for it. A shipped id whose ship date is not older than the running server binary's build date comes back with possibly_stale_binary, alongside the build date and commit. A same-day ship sets it, because the ship date carries no time.

  • roadmap_migrate op:"init" bootstraps a project that has no ROADMAP.md (ANTS-4740) roadmap_migrate op:"init" bootstraps a project that has no roadmap file at all. It writes a conforming skeleton with one ## Backlog section and then registers the project the ordinary way, replacing the hand-authoring step a greenfield project used to need — where a mis-cased trailer label would migrate cleanly while silently dropping fields. Refuses roadmap_exists rather than overwriting; dry_run:true returns the skeleton without writing.

  • A document can opt out of spec_lint's required-sections check (ANTS-4739) A document may carry spec-lint: no-required-sections to opt out of the required-sections check -- for a mixed corpus holding pre-standard specs, build plans and ledgers that can never conform. The number of exempted documents comes back as sections_exempt_docs, so an exemption is never mistaken for a clean pass.

  • spec_lint can match a required heading by prefix, so a descriptive suffix passes (ANTS-4738) A format standard may write its marker as required-sections: prefix, which matches a numbered heading on its number and name and lets a descriptive suffix pass. Exact matching stays the default.

  • roadmap_query can list WHICH items shipped in a date window, not just how many (ANTS-4715) shipped_since / shipped_until narrow mode:"bullets" and mode:"headline_only" to items the store dates inside a half-open window, so the release-time question — what shipped this cycle that the changelog does not mention — can be asked through the verb. It refuses rather than ignores the arguments wherever the branch that would honour them is not the one that runs, and reports shipped_undated when the store holds shipped items with no date, since those can appear in no window at all.

  • apply_edits takes match_wrapped, applying an edit whose old differs from the file only in where its line breaks fall (ANTS-4723) Opt-in and fallback-only — consulted just where the verbatim search found nothing, so it can turn a refusal into an edit and never changes a call that already succeeds. Uniqueness is enforced on it as on the verbatim pass, and it declines a span whose continuation lines carry column alignment or open a list item. When it fires the envelope says so, so a re-flowed span is never silent. Same rule and spelling as workspace_search's match_wrapped.

  • Release gate: work that shipped but reached no CHANGELOG entry is now reported (ANTS-4714) tools/check-shipped-coverage.sh reads the roadmap store's shipped dates and lists every item closed since the last public tag that no CHANGELOG bullet cites. The existing gate only checked the opposite direction — that entries claiming to have shipped really had — so work that shipped and was never written down was invisible. The first run found 19, one closed the same day as the release it was missing from.

Changed

  • amend_body's not-found refusal now shows the text it searched (ANTS-4807) The refusal echoes the stored body's tail and its length, and says plainly that this is the text being matched against and not what roadmap_query returns. A caller comparing their search string to that echoed tail gets in one call what three rounds of guesswork could not.

  • Right-clicking a tab now shows which colour that tab is currently set to. (ANTS-4862) The palette listed 26 colours and marked none of them, so there was no way to read the current setting with the menu covering the tab. The active entry is now ticked and bold; a colour picked from the custom dialog marks "Custom colour..." and shows itself as that entry's swatch.

  • Adding a test that reads a source file now rebuilds one file, not the whole bundle (ANTS-4797) Each test's file-path setting is attached to that test alone rather than to every file in its bundle. Measured on one bundle: adding a setting rebuilt 54 files before, and rebuilds 1 now.

  • cppcheck's advisory useStlAlgorithm class is suppressed, with its reason recorded (ANTS-4782) It asked for hand-written loops to become <algorithm> calls, printed in every run and was never acted on. Suppressed in both the workflow and the local parity script, each stating why and how to re-enable it.

  • The standards mirror gate now names the links it cannot check (ANTS-4825) A link out of a mirrored standard that points outside the mirror set is skipped by rule. The gate now names each such target and why, instead of reporting a bare "links resolve" over a tree where doc_integrity finds broken links. Both tools were right; only the reporting disagreed.

  • The document-integrity check accepts the same compact setting as its sibling tools (ANTS-4858) It was the only one of that group that ignored the setting, so asking for a smaller reply there did nothing. Replies stay full-size unless you ask, because some of its empty fields carry meaning.

  • The project-setup check separates settings you can fill in from ones you cannot (ANTS-4815) A project that deliberately keeps no docs or specs folder was listed as missing them forever, with no way to clear the list short of creating folders it did not want.

  • CI now drops a superseded build on branches and PRs (ANTS-4799) Pushing again to the same branch cancels the run the previous push started. Runs on main are never cancelled, so a cancelled build there still means something went wrong.

  • Symbol resolution answers many names in one tree walk, and skips lines that cannot match (ANTS-3680) doc_symbols over the whole docs/specs + docs/standards corpus went 41.8 s to 3.4 s with identical output. A single find_definition went 410 ms to 81 ms on this tree; 200 names resolved together went 15.6 s to 0.35 s. New lib entry point SymbolQuery::findDefinitions, which returns exactly what the per-name call returns.

  • session_orient offers project settings for any non-standard layout (ANTS-3353) A project whose code sits where Ants expects but whose roadmap, changelog or specs live somewhere unusual now hears that it can pin those paths — previously only a project with misplaced SOURCE was told. The suggestion carries every path it found, not just the source directories.

  • A gap in a spec's invariant numbering now reads as evidence, not a defect (ANTS-3684) A spec that continues a parent spec's numbering keeps its ids, and renumbering would break the citations — so the gap is real and the document is correct. The finding is now a candidate and names that cause, so it can be triaged without opening the parent spec.

  • CI jobs show readable names in the GitHub Actions UI (ANTS-4796) The three jobs carried no name, so Actions labelled each by its internal yaml key.

  • The cppcheck gate reads tests/ as well as src/ (ANTS-4788) Findings under tests/ were previously seen by no run. cppcheck cannot parse this project's test macros, so its syntax-error rule is suppressed under that directory; without that the widening reports a parse failure per affected file. The gate stays informational and does not fail a build. tools/ is not in scope because it carries no C or C++ source.

  • Two per-query tokenisers compile their search pattern once per process rather than once per call (ANTS-4776) wrapmatch.cpp and findsources.cpp hold their constant QRegularExpression as static const.

  • The roadmap high-water scan is 41% faster on a large roadmap (ANTS-4426) maxDeclaredId — the scan behind roadmap_query's file_ahead_of_store witness and the allocator's floor — ran a regular expression once per line of the file, 62,918 times on this project's 4.96 MB roadmap. It is now a character check with identical semantics: the scan drops from 22.6 ms to 13.3 ms, and a cache-miss roadmap read on a migrated project from ~26.5 ms to ~16.6 ms. Verified by verdict diff over 400 markdown files and 234,692 lines, with no verdict moved.

  • A roadmap note with no line breaks of its own is wrapped to match the corpus (ANTS-4532) It was written verbatim, so a caller who did not pre-wrap left one very long line in a file where every other body is wrapped. Prose carrying no newline has no structure to preserve, so it is wrapped at the width used around it; a note that brought its own line breaks is still written exactly as given. The wrap runs after the trailer-key guard, never before it — moving a word to the start of a line could otherwise manufacture a declaration the caller never wrote.

  • apply_edits says a transport timeout means UNKNOWN, not failed (ANTS-4733) Measured: a batch reported a timeout, the files showed nothing written, and the write then landed — so the obvious recovery of looking and re-sending races a write still in flight. The description now says to re-read before deciding, and why the unique-old form is the safe one.

  • Three source-scrape tests now anchor on the construct they mean, not on the first match (ANTS-4727) Each opened its search window on a string that occurs several times, and landed on the intended site only because of where the translation units sit in the build's source list. Two of the three would have kept passing from the wrong place. Each is now scoped to the function or branch under test.

  • Two unverified ANTS-1881 combinator claims are now checked against the code (ANTS-4725) Both hold. The section etag keys on the section's bytes rather than on mode, so an etag taken under one mode is answered unchanged under another — the row now records that edge. And the list path's mode echo is gated, which is what § 2.4's byte-identical back-compat claim rests on.

  • A source-scrape test now anchors inside the branch it measures (ANTS-4724) Inv2BypassesStatusAndPagination anchored on a string that also matches an earlier else if, so its byte window opened above the branch it names — a test that passes from the wrong position gives no signal when the thing it names moves. Re-anchored on a needle that occurs once, and proved by mutation in both directions.

Fixed

  • Session preamble now reports when the installed hooks have drifted from the repo (ANTS-4516) Fixes to the helper hooks could be written, tested and green while the installed copies stayed a month stale, with nothing saying so. One preamble line now names the stale hooks and the command that installs them. It reports rather than writing to the hook directory at launch.

  • spec_log op:append_loop no longer corrupts a table-form review log (ANTS-3685) It appended a bullet after the last table row whatever shape the log was in. It now detects a table, takes the row's cells, checks them against the header's column count, and refuses rather than writing a row that breaks the table. Row order is inferred too, so a row lands at the end the log actually runs from. Shipped under ANTS-4364 and ANTS-4353.

  • Tab-colour menu now ticks the colour the tab is already on (ANTS-4864) The active entry gets a tick drawn into its swatch plus a checkmark on its label, so the current colour is findable in a 27-row menu. Qt's own checked state was rendering as a frame around the swatch, which looked like every other swatch.

  • Typing in the scrollback search box no longer freezes the window. (ANTS-2000) Each keystroke re-scanned the entire scrollback -- 50,000 lines by default -- so typing a word could lock the window for seconds. The search now waits for a brief pause in typing and scans once. Pressing the regex toggle still searches immediately.

  • Updating the compiler no longer leaves precompiled headers that can crash the build. (ANTS-2107) The build now records which compiler built its precompiled headers and discards them when that changes. Previously a compiler upgrade left large incompatible files in place, which warned and then crashed the compiler part-way through a build, and the only remedy was knowing to delete them by hand.

  • The test-audit dialog's "Per-finding" fold-in now works. (ANTS-4445) Choosing Per-finding reported a failure every time: nothing ever collected the findings it needed. The reviewer's reports are now read into findings and folded in with roadmap IDs allocated. When a report carries no findings in the expected form, the dialog says so and points at Narrative mode instead of reporting a failure.

  • Dismissing an audit finding as a false positive now actually hides it. (ANTS-4444) A dismissed finding was remembered, but it kept appearing in the results list, the summary counts, the HTML and SARIF exports and the roadmap fold-in, and was sent to the AI again on every triage. Every one of those places now checks the learned-false-positive list, and does so live, so a dismissal takes effect without re-running the audit.

  • A plugin that reaches its memory limit can no longer take the terminal down with it. (ANTS-4442) Sending an event to a plugin already at its 10 MB budget crashed the whole terminal, which is exactly what the plugin documentation promises cannot happen. The event data is now prepared inside a protected call, so the plugin is told it ran out of memory and the terminal carries on.

  • SECURITY.md now lists the link schemes the terminal actually accepts. (ANTS-4453) Hyperlinks are limited to http, https and mailto. The document promised ftp and file as well, which the code has never accepted at these sites.

  • Opening a file whose path contains a percent sign now passes the right path to the editor. (ANTS-4784) Ctrl-clicking a file:line in terminal output built the editor argument with a form that could substitute into the path itself. The same form was corrected wherever an untrusted path, lane name or caller-supplied string is placed into a message before another value.

  • Files that Claude Code changes are tracked again when the hooks are installed. (ANTS-4451) The hook path read the wrong field names from the event, so no file was ever recorded and nothing downstream fired. The transcript path was unaffected.

  • Audit dedup keys and learned false-positive fingerprints no longer collide on a file path containing a percent sign. (ANTS-4452) Both keys are built from the file path first. The string-building form used could read a %2 inside the path as a placeholder, letting two unrelated findings hash to the same key -- so dismissing one could silence the other.

  • roadmap_query now answers check_sync on every reply, including the section-inventory fallback. (ANTS-4860) Asking whether the roadmap file matches the store could previously come back with the field simply absent, which is also how the reply looks when nobody asked. The fallback reply now says the check did not run and why.

  • Documentation now says where review lanes actually come from (ANTS-4785) The subsystem map described itself as the source of the review lanes, which stopped being true when every source file was given a lane in a committed override. Both files now state which is which, and which one to edit to change a lane.

  • A code fence inside one roadmap bullet now ends with that bullet (ANTS-4823) roadmap_log's walkers bind a fence to the body it was opened in, so a hand-written or legacy bullet that opens a fence and never closes it can no longer make every bullet below it unwritable. A fence opened in section prose still masks what follows, unchanged.

  • Projects whose item IDs start with a digit can name their own IDs again (ANTS-4849) An ID like 3D_E-0682 was rejected by the one setting that exists to let you choose an ID, while the rest of the same tool accepted it. The only route left quietly depended on a counter file being correct. A plain number with no letters is still rejected, because nothing could tell it apart from an automatic ID.

  • A preview of a batch file edit no longer claims it wrote the files (ANTS-4834) Asking "what would this change?" replied as though the change had already happened, so a caller checking that reply could conclude the work was done and skip doing it. Previews now say what they would do. The matching tool for roadmap edits was fixed the same way earlier.

  • Asking for a roadmap section's items now tells you which sub-section each one is in (ANTS-4824) Every item came back labelled with the section you asked for, so a nested one looked like it lived in its parent. Filing a new item next to one of them would then have put it in the wrong place without saying so.

  • Cutting a release now reports the date it stamped on the heading (ANTS-4833) That date is what release-note tooling searches for, and it was the one thing the preview could not show you — left out, it quietly uses the machine's idea of today.

  • A roadmap note that quotes code-block markers is handled correctly again (ANTS-4450) The check that stops such a note running away and swallowing the entries below it had drifted apart from the reader that enforces the same rule. It could both miss a runaway note and insert a stray backslash into a note that was perfectly fine.

  • The roadmap tool's set_body operation is listed among its operations again (ANTS-4842) It worked and was documented, but was missing from the list callers read to check what exists — so sessions concluded it was unavailable and left a garbled entry unrepaired.

  • The spell checker's settings now cover the noise they were written for (ANTS-4800) Findings dropped from 131 to 4, and the four that remain are documented as deliberate. Verified the checker still catches ordinary misspellings.

  • A roadmap entry can show its own format in a code block without that example becoming real data (ANTS-4526) Keys written inside a fenced block are now read as illustration, matching what already happened for keys quoted inline. Tilde-fenced blocks are covered too, which they were not before.

  • Roadmap entries below a certain point can be updated again (ANTS-4823) One entry contained an unfinished code block, which made the roadmap tool refuse every entry after it.

  • Both guides quote the README's real version line (ANTS-4540) They described it with wording the README stopped using, so a reader searching for it found nothing.

  • The contributor guide and bump recipe name the tools that still exist (ANTS-4530) Both routed the version bump to a /bump command removed in August, and credited tagging to the wrong tool. They now name cut-release and packaging/cut-rc.sh.

  • The debt-sweep engine's design doc matches the shipped code again (ANTS-3510) Its detector list, scan order, apply-fix contract, dry-run path and perf budget had fallen behind several months of changes. The two places that listed detectors by name now point at the engine's own roster, so adding one no longer needs a doc edit.

  • find_definition tags a brace-initialised member declaration, so both spellings of one field agree (ANTS-4821) A brace only opens a body when a parameter list, a lambda capture list or a class-key precedes it; otherwise it initialises the declarator it abuts. QTimer *m_t{nullptr}; now matches int m_x = 0;.

  • The Review Changes dialog refreshes when a file is saved in place (ANTS-2229) Closed by the watcher rewrite already shipped under ANTS-3509: edits to an existing file, and edits anywhere in a subdirectory, now refresh the diff without a manual Refresh.

  • A spec that quotes the test-clause marker parses whole (ANTS-3676) The spec reader took the first occurrence of the marker wherever it appeared, so an invariant naming it in backticks was cut in half at the mention. It now skips a marker inside an inline code span. spec_lint reads this parser, so its findings were least reliable on the specs that discuss spec format.

  • The closer-length rule now reaches every reader that tracks fences itself (ANTS-4820) The docs index, file outline, spec lint, spec log and feedback-file scanners each kept their own fence loop and still ended a block on a short run. A document quoting code-block syntax no longer leaks its sample headings into the outline, the docs index, or a feedback file's untriaged delta.

  • Code fences now honour CommonMark's closer-length rule (ANTS-3678) A short run of backticks or tildes no longer ends a block opened with a longer one, so a document that quotes fence syntax keeps its sample text — headings included — inside the block. Affects every reader built on fenceMask, read_region's section resolution among them.

  • The bash veto no longer blocks commands that merely mention a git diff (ANTS-4517) The rule matched its phrase anywhere in the command, so an echo, a heredoc or a sed replacement carrying it was vetoed while running no git at all. The match now anchors to a command position; a later stage of a pipeline or a compound is still a real invocation and still vetoed.

  • roadmap_query section= now returns a heading's nested sections too (ANTS-4819) A level-2 slug answers with its level-3 children's items, matching the descendants mode:"section_index" already rolled into that slug's counts. Store-backed projects previously got an empty read for any parent heading while the index promised its children's bullets.

  • The citation scanner appears in the project's own code map (ANTS-3687) docs/subsystems.md listed all five of its siblings and not it, so the code map answered "no such thing" to a session asking where the citation checker lives.

  • Build and release workflows are visible to the review splitter (ANTS-4805) The walk never descended into .github at all, so a project's CI files appeared in no review lane and in no coverage report either — an absent lane and a clean lane look identical downstream. This session's own sweep found four real workflow defects, two security-relevant, on files no partition would have offered a reviewer.

  • A registered project is no longer told the store holds no record of it (ANTS-4802) Only one roadmap dialect is served from the store, while the migration reads three — so a project could be registered, hold hundreds of rows, and still be told there was no row for it, which sends the reader to re-run the migration, the one action that cannot help. The refusal now asks the store first, and the migration says what a non-served format costs rather than reporting a bare flag.

  • A spec id one tool hands out is now accepted by the tool that reads specs (ANTS-4810) invariant_check returns a spec's id as its filename stem, and on projects whose specs carry a topic suffix spec_query rejected that exact id — breaking the two-call lookup the coding workflow prescribes.

  • The roadmap summary view answers the in-sync question instead of ignoring it (ANTS-4818) Asking whether ROADMAP.md matches the store was accepted and silently dropped in report mode, while the documented rule says a missing answer means the question was never asked — so a caller who did ask read the silence as their own omission.

  • A roadmap entry's reply says which of its lines the tool wrote (ANTS-4813) An entry's text mixes an author's prose with lines composed from stored fields, and the two are edited by different operations. Nothing distinguished them, so passing a line back to be edited failed on text that had just been read. The reply now names the composed lines.

  • Lanes that count zero now say whether they are empty or unmeasured (ANTS-4816) A lane of packaging files, YAML or shell counted zero, because the count admits only file types the code index can read — and zero read exactly like an empty directory while being what the size warnings key on. Such a lane now reports how many files were there and not counted.

  • A project keeping all its code in one directory gets review lanes again (ANTS-4811) Every partitioner grouped by top-level directory and returned nothing unless grouping produced more than one group, so a project whose modules all live in one package got an empty partition that looked exactly like having no subsystems. Such a directory is now split by size, and the reply says which stage produced the lanes so "the map yielded nothing" is distinguishable from "there is no map".

  • Review lanes no longer cover files the repository ignores (ANTS-4809) The partition walk knew the conventional exclusions but never asked git about the project's own, so one project got fourteen of seventeen lanes over an ignored directory of scraped data. A lane is an instruction to read what it names, so this mattered more than the noise.

  • indie_review_partition now reports which files no review lane covers (ANTS-4786) It could only answer that for a partition it had computed itself. On the declared path — the normal one here and on every migrated project — the reply listed lanes and said nothing about what they left out. The envelope now carries the uncovered count, a per-suffix breakdown, a bounded sample of paths, and a reason saying whether the files were dropped by a filter or simply never named.

  • remotecontrol_state.cpp includes the Qt header it uses instead of relying on the module precompiled header (ANTS-4774) The translation unit built only because the shared Qt PCH happened to supply QElapsedTimer; it now compiles on its own terms.

  • The command palette builds and positions itself on every reveal, not only when shown through a typed pointer (ANTS-4775) It now overrides the virtual setVisible(bool) rather than shadowing the non-virtual QWidget::show(), which could otherwise yield an unbuilt, unpositioned and unfocused palette.

  • indie_review_partition now reports the source files its computed walk left out of every lane, so a review can no longer read as full coverage while skipping part of the tree (ANTS-4771) The computed partition admits only suffixes the codebase index can outline, which is narrower than "source" on purpose. On a mixed Python and shell project that silently dropped every shell file. The envelope now carries unassigned_count, unassigned_by_suffix and a hint whenever it hands back a computed partition.

  • The mirrored standards now include the C++, Qt and Python spellings that coding.md names as owning how this project's code is written (ANTS-4764) A reader with only the checkout could open coding.md, read that the language spellings live elsewhere, and have no way to follow it. What stays out of the mirror set is now a stated decision rather than an omission: a skeleton is a template to copy, not a rule to conform to, and docs/standards/README.md records the boundary.

  • The public copies of the shared standards now carry every sibling they link to, so their cross-references resolve for a reader who has only the checkout (ANTS-4761) The mirrored half of a standard is a verbatim copy, so a link in it pointing at a global file this repo did not carry simply dead-ended. The mirror set is now closed under its own links, and tools/check-standard-mirrors.sh fails on a link out of a mirrored half that this repo does not carry.

  • Two false-positive compiler warnings no longer appear on every build (ANTS-4544) GCC reported a possible null dereference on a line that checks for null, which it loses sight of when optimising. Scoped suppression, same as the three earlier instances of this class. Reproduced before being silenced and confirmed gone after.

  • The roadmap store's history budget counts the bytes it says it counts (ANTS-4503) Three units were in play — SQLite counted characters, the incoming size counted UTF-16 code units, and every name for the budget said bytes. They agree on plain ASCII and diverge on emoji status markers, em dashes and non-Latin text, so a real store could hold several times the history the cap names. All four measurement sites now count UTF-8 bytes. On such a store the effective budget is smaller, which is the documented intent, and reaching it still writes no history and never aborts the operation.

  • Asking for fields= no longer switches compaction on as a side effect (ANTS-4673) The two shared one allowlist, so narrowing a reply also applied a transform nobody asked for — which folded away spec_lint's flag saying a check had never run.

  • An odd number of backticks no longer inverts code-span masking for the rest of a body (ANTS-4598) Everything after the stray backtick was treated as the opposite of what it was.

  • counter_advanced_past no longer names a position the allocator has passed (ANTS-4579) The one field a caller would predict the next id from was knowably false.

  • op:append_batch reports the counter fields op:append already emitted (ANTS-4551) Their absence read as evidence that the counter had not moved.

  • A dry run on a store-backed project reports the id it would allocate (ANTS-4592) ANTS-4508's would_be_id never reached that path.

  • A roadmap_log dry run no longer looks like it reserved an id (ANTS-4508) It returned the next id under the same key the real write uses, so a caller reading that field took it for a reservation. Nothing is reserved, and the id is wrong if any other write intervenes.

  • roadmap_migrate's routine re-import is no longer thousands of tokens of noise (ANTS-4408) Every call returned about a thousand notes whether or not anything changed.

  • A rolled-back roadmap_migrate no longer describes writes that did not happen (ANTS-4144) It emitted its full note list regardless, spending thousands of tokens on a transaction that was undone.

  • A project that has allocated no ids is not reported as diverged (ANTS-4430) Its zero high-water mark was indistinguishable from the divergence that blocks writes.

  • A migrated project no longer reports a false divergence (ANTS-4410) roadmap_migrate left the store's high-water mark at zero, which reads as the state that means "do not write".

  • The store-divergence warning no longer fires forever (ANTS-4406) It compared two different quantities, so it warned on every project and both of its numbers were wrong.

  • roadmap_query names the backend that actually answered (ANTS-4143) It answered from the store while the envelope named ROADMAP.md, so a caller could not tell which document it had read.

  • Specs no longer cite the pre-split remotecontrol.cpp or the deleted isFieldProjectionTool (ANTS-4711) Roadmap-query code is attributed to the translation unit that holds it, and the stale line pins are dropped rather than re-derived — every one landed on unrelated code or past the file's end. The file was corrected only where the subject moved: dispatch() and the rc* helpers those specs cite really did stay put. Instructions to enrol a verb in isFieldProjectionTool now say there is nothing to enrol in, ANTS-4524 having removed the fields= allowlist; two statements asserting the deleted symbol still exists are gone, which is what invited re-adding it. Loop-log rows are untouched — they record what a review pass found on a date. CLAUDE.md's own applicationDirPath citation is corrected with them.

  • changelog_query's bad_mode refusal names the version argument (ANTS-4754) A caller after one version's entries reaches for mode:"unreleased", reads an accepted-mode list that does not contain it, and concludes the verb cannot filter by version — when version:"Unreleased" has done exactly that all along and composes with every mode. The refusal now says so and gives a value that works. No filter was added; the capability was never missing, only unfindable from the reply that turned it down.

  • workspace_search explains a zero caused by its own hidden-path filter (ANTS-4753) A glob naming a dot-directory (.github/workflows/*.yml) returned no matches and no reason, because include_hidden defaults to false and ripgrep prunes the hidden directory during the walk — indistinguishable from the string genuinely being absent. Such a zero now carries hidden_paths_skipped:true and a hint naming include_hidden:true, on the match list and on the count_only, files_only and matches_only replies alike. A call that returns rows is unchanged. A lane naming a hidden directory is searched as before and does not flag it.

  • The Arch and Debian packages now declare ripgrep, so their test suites run instead of failing (ANTS-4717) The guard that catches this class was reading only the CI workflow, so the same missing declaration was still live in two package recipes. It now reads every recipe that runs the test suite, and matches a declaration rather than a mention.

  • doc_citations says whether a zero is a clean run or a silent one (ANTS-4743) doc_citations returning zero now says whether that is clean or silent. It recognises only path:line citations, and the spec-format standard tells authors to cite the symbol instead — so a conforming spec corpus scanned to zero and read as checked. Every reply now names the forms it recognises, and a zero over a document holding citation-shaped text reports how many it could not read, with a hint not to record the run as a passing check.

  • spec_lint counts are taken over the whole scan, not after the cap (ANTS-4737) spec_lint counts are now taken over the whole scan, and max_findings trims only the returned list. Previously a capped run under-reported the total, so two runs at different caps could not be compared -- one measurement read 39 where the truth was 81. A new findings_total gives the uncapped figure.

  • mutation_probe stops a batch before it outlives the transport timeout (ANTS-4736) mutation_probe now stops a batch before it outlives the MCP transport's read timeout. Previously the loop kept mutating the source after the caller had been told the call timed out, so a session could see an unexplained red suite and commit a file the probe still held. The deadline is measured from the slowest run so far, not predicted from timeout_sec (a worst-case cap), so batches that fit are unaffected; skipped mutations report not_run and the reply names the arithmetic. transport_budget_sec (default 60, 0 disables) tunes it.

  • read_region no longer returns a short Python function body while reporting it complete (ANTS-4735) Symbol mode brace-matched every function, and { opens nothing in Python — so a body containing a dict literal ended at that dict's closing brace, dropping everything after it, with truncated:false saying the read was whole. In py mode the extent now comes from the flat outline, which is what indentation already encodes.

  • doc_citations resolves a bare basename naming a markdown document where the codebase index cannot (ANTS-4728) A skill citing a standard as commits.md — a file living under standards/ — matched none of the resolver's three routes in a tree with no codebase index, so the quotation came back unverified and, in an aggregate, looked like a pass. The caller now supplements the basename map with a bounded markdown scan, and only where the index was absent or truncated. A basename with two matches stays ambiguous rather than being guessed.

  • roadmap_query's section_etag_match row now matches what the code does (ANTS-4732) The row said the shortcut was ignored under mode:"headline_only"; in fact the section argument alone selects it, and it is honoured. The row now says so, and carries the edge that follows: the etag keys on the section's bytes, so varying mode between calls can be answered unchanged for a payload shape you did not ask for. No code change — the code was already right.

  • section_index now warns when it returns no sections, and says which of the two reasons applies (ANTS-4710) Asking a project for its roadmap sections used to give the same empty answer whether the file was unreadable or simply had none. The warning names the case, using whether any id-bearing bullet parsed to tell them apart.

  • check_sync emits sync_checked on both arms (ANTS-4730) The field the schema tells every caller to read first was emitted only where it would be false, so a caller doing the documented check read the healthy answer as "nobody looked" — defeating exactly the misread the field was added to prevent. Its absence now means only that check_sync was not requested. Two tests asserting the old shape were updated with the reason recorded.

  • discarded_external_edits no longer fires on the renderer's own format marker (ANTS-4729) The one flag documented as the only place a silently discarded hand-edit surfaces was firing once per not-yet-marked project, on a header the render itself adds — and a high-trust flag with a routine false positive is one sessions learn to skim. The boolean is now derived from the file's own classified lines; the total count is deliberately unchanged, since deciding which differences are cosmetic is not this check's judgement.

  • Removed a dead duplicate lambda that made every build of the Claude-integration library warn (ANTS-4687) A second, never-called copy of a helper sat in a different method from the live one. Dead since it was written.

  • roadmap_query's schema and spec now state that mode:"headline_only" with ids also carries input_index (ANTS-4712) Both described a strict four-key shape while the ids path has always emitted a fifth. The key is kept rather than removed: results come back in document order, so dropping it would reintroduce the mis-pairing it exists to prevent, for exactly the leanest caller. A test now locks the ordering that produces it.

  • read_regions refuses a non-object region item by naming the shape it wanted, not the selector rule (ANTS-4721) Passing a [start, end] pair per item — a natural reading of the regions alias — was refused with "exactly one of {line range, symbol, section} required", which diagnoses a choice the caller never made and cannot act on: an array has nowhere to hold a selector. The refusal now names the expected shape, what arrived, and the item's index, and stays per-item so one bad item does not cost the batch its good ones.

  • file_outline no longer reads a # comment inside a fenced code block as a markdown heading (ANTS-4722) A path comment labelling a fenced source listing was emitted as a level-1 heading. With sizes:true that phantom took its extent to the end of the file, so it claimed most of the document and every real section below read as its child with its own size wrong — turning the answer to "where is the natural seam?" into a comment. max_heading_level was no escape, since the phantom was level 1.

  • A multi-line old that misses now reports where it nearly matched, instead of a bare not_found (ANTS-4723) The near-miss report previously covered single-line misses only, so the commonest multi-line cause — a hard wrap falling elsewhere — was indistinguishable from the text being absent. It now reports the location and names match_wrapped in its hint, so the fix is one re-send rather than a re-read. This matters most mid-batch, where earlier edits have already landed and the file has moved under the caller.

  • Releasing now updates the package repositories instead of only the recipe in git (ANTS-4716) Promoting a release pinned the new tag in the OBS recipe and pushed it, but nothing told OBS to re-fetch — so the repositories kept rebuilding the sources they already had, while the git side looked correct. That is how package users ended up two releases behind twice. The promote step now runs the submit itself, and says so loudly if it cannot.

Security

  • The AI triage request is scrubbed for secrets before it is sent. (ANTS-4448) This path built and posted its own request rather than going through the shared client, so it never got that client's scrub. The prompt embeds the finding's source snippet -- and for a secrets or gitleaks finding, that snippet is the credential. The status line now says when something was redacted, so a scrubbed prompt is not silent.

  • Credentials in a git remote URL no longer reach SARIF exports or the wire. (ANTS-4448) A remote like https://user:token@host/owner/repo.git was written verbatim into SARIF provenance and read back out of it by last_audit_summary. The userinfo is now stripped on both the write and the read, so SARIF files captured before this change stop leaking too. An scp-style remote (git@host:owner/repo.git) is left alone.

  • Roadmap and changelog lookups stay inside the project they were called from. (ANTS-4447) The search for a project's ROADMAP.md or CHANGELOG.md walks up from the calling directory. It now stops at the enclosing git repository before walking rather than after each step, so a call made from a directory that is not in a repository no longer reaches a different project's files.

  • The release workflow defaults to read-only and grants write on one job (ANTS-4795) release.yml declared contents: write for the whole workflow; it now defaults to contents: read and grants write on the single job that creates the release and uploads the AppImage, mirroring ci.yml. Same effect today, but a job added later inherits read rather than a write grant nobody decided to make.

  • Release-workflow values now reach the shell through env:, never template expansion (ANTS-4792) All thirteen remaining ${{ }} expansions into run: bodies in release.yml are gone; the tag and RC flag are passed as environment variables. ANTS-4772 had closed the direct inputs.tag path but the same value was written to $GITHUB_OUTPUT and read back into the script body at eight further sites. A new conformance test enumerates .github/workflows/ and fails on any expansion reaching a shell body, so a workflow added later is covered on the day it lands.

  • CI and release checkouts no longer leave the git credential in the runner workspace (ANTS-4773) All four actions/checkout steps set persist-credentials: false. Neither workflow invokes git, so nothing depended on the persisted token, and it can no longer reach an uploaded artifact via .git/config.

  • The release workflow passes its tag input through env: instead of expanding it into the shell (ANTS-4772) A workflow_dispatch tag is now read as "$INPUT_TAG" rather than substituted by the Actions template engine before the shell parses the line, so a crafted tag can no longer become shell source in a runner holding contents: write.

v0.7.108-rc2Pre-release

0.7.108 RC2 — Patron preview

Respin of v0.7.108-rc1 with cherry-picked fixes: 0086e07b

v0.7.108-rc1Pre-release

0.7.108 RC1 — Patron preview

This release shipped without written notes.

v0.7.107

0.7.107

Added

  • feedback_query reports a format version it does not recognise (ANTS-4702) A feedback file claiming a version that has never existed was accepted in silence. It is still read, and the envelope now says the marker is unrecognised.

  • read_region takes max_line_bytes to clip long lines (ANTS-4700) Reading part of a document whose weight sits in a few very long lines returned a truncated head and none of the rows you were reading toward. Clipping per line is applied before the size cap, so it makes room rather than competing with it.

  • A prose-heavy migration explains its item count (ANTS-4694) Migrating a large milestone document could report only a handful of items, which read as though most of the file had been skipped. Nothing is skipped — the rest is stored as prose and tables — and the report now says so.

  • feedback_log can record a whole triage in one write (ANTS-4671) The new assign_id_batch fills many findings' id slots at once, where before each one cost a separate read and write of the file.

  • The tab colour menu now offers 25 colours, including greys and black (ANTS-4689) Right-click a tab to pick from 25 presets instead of 14. New: Orchid, Indigo, Sand, Olive, Lime and Mint fill gaps between the existing shades, and a grey ramp -- Silver, Gray, Slate, Charcoal, Black -- is grouped at the bottom of the list. They use the same gradient as every other tab colour.

  • roadmap_log op:"amend_field" edits an item's Layman, Kind, Source, Lanes or Evidence (ANTS-4667) These could be set when an item was filed and never changed afterwards. Trying through amend_body failed with the text reported missing, because those lines are assembled when the roadmap is written out rather than stored. That error now points at the new command.

  • roadmap_log's envelope could report when its XML scrub actually removed something. (ANTS-4572) A safety net quietly cleans up a mistake, so nobody learns they made it.

  • read_region gives no positive signal that a read did NOT spill once fields + compact narrow the envelope away. (ANTS-4567) A deliberately cheap probe can come back saying only "ok", which is indistinguishable from the probe being wrong.

  • invariant_check cannot see a spec that cites the file by basename, so the wrong spec comes back looking like the answer. (ANTS-4566) Asking which design document governs a file returns a confidently wrong one when the right one names the file informally.

  • roadmap_migrate accepts fields= and compact, so its envelope can be narrowed (ANTS-4429) A no-op re-sync returned ~25 KB with no way to ask for less — the schema sets additionalProperties: false, so fields= was not merely absent, it was refused. Both args are consumed by the dispatcher after the handler returns, so they narrow the report and never the migration. Closes the last of ANTS-4144, ANTS-4408 and ANTS-4429, the older half of which ANTS-4649 had already settled.

  • feedback_log op:"set_title" — rename a feedback file's heading after a project rename (ANTS-4646) The filename was always derived correctly; the H1 was writable by no op, so a rename left the title contradicting it and the only route was a hand edit, against the file's own "don't hand-edit" instruction. title is the project name, not the whole heading: the existing prefix is preserved verbatim, since this renames a project rather than normalising a corpus. Re-runnable — an already-correct title reports no change and writes nothing.

  • invariant_check states that the ROADMAP was not consulted (ANTS-4645) Every reply now carries roadmap_scanned:false and a scope_note naming roadmap_query and task_priors. The envelope was confident and complete-looking on a non-zero answer, and a project rebuilt a feature its own roadmap had already specified.

  • doc_citations quotes: widen attribution from the line to the paragraph — coverage goes from 26 to 111 of 249 checkable quotations. (ANTS-4638) The checker only links a quote to its source when both sit on one line, which hard-wrapped prose almost never does.

  • Feedback lookups can be pointed at a shared corpus directory (ANTS-4471) When feedback_query could not find a project's feedback file it said only "not found", even though it already knew how to list nearby files — it was looking in one directory, and for a project whose feedback lives on a different part of the disk that directory is empty. A new claude.mcp_feedback_root setting names the shared folder, searched in addition to the usual one, and a miss now reports which directories were searched plus how to widen it. Empty by default, meaning the previous behaviour is unchanged.

  • roadmap_query can now answer "is my roadmap file in sync with the database?" (ANTS-4462) Pass check_sync:true to have the roadmap rendered from the database and compared against the file on disk — the same comparison a write already runs before it overwrites, so the two can never disagree. Returns file_in_sync plus a breakdown of how many lines differ and which text would be lost. The existing warning only fired when the file mentioned an item the database had never seen, so a status changed by hand looked perfectly healthy; this catches that. Opt-in and never cached: it costs a full render (~204 ms on a 2,267-item project), which is why it is a question you ask once rather than something every query pays for.

  • Every roadmap_log flip/annotate envelope names the path that wrote (ANTS-4464) The store-backed and markdown paths declare different fields on purpose — a store has no line numbers — but nothing said which had run, so a caller reading note_line got nothing with no way to tell why, and identical calls changed shape across a migration. A new write_path ("render" or "patch") makes the absence a statement. The store path also now emits the post_bullets echo that return:"headline_only" promises and only the other path supplied.

  • roadmap_log gains op:"annotate_batch" (ANTS-4470) Adds a note to N roadmap items in one read and one atomic commit, where N separate annotates cost N reads of a large ROADMAP.md, N commits, and raced the file watcher. Takes flip_batch's existing locators[] unchanged, so there is no second shape to learn; a to_status is refused rather than ignored, and a locator with no note is skipped without costing the batch its other closures.

  • Build-time Qt-version guard: refuse a tree whose objects predate a Qt upgrade (ANTS-4625) A Qt point upgrade could silently poison an incremental build. RPM preserves upstream header mtimes, so newer headers can be older than your objects; ninja is mtime-based, rebuilds nothing, and links the new libraries against stale objects. Measured on 6.11.1 → 6.11.2: 859 of 3838 tests failing with heap corruption. The build now compares the installed Qt against the version the tree was configured with, and refuses with the clean-rebuild command. A reconfigure alone never fixed this — it regenerates build.ninja but invalidates no objects.

  • Cross-session mailbox: session_message, so Claude sessions on different projects can leave each other notes (ANTS-4622) Mail is addressed to a PROJECT by its export_slug, never to a session: a session is its shell PID, which is ephemeral and reused, so mail addressed to one is undeliverable the moment that session ends. Three ops — send, inbox and ack — with confirmation held as two states in one nullable column, and unread mail surfaced by session_orient at the start of the next session on that project. Complements the *_Ants_MCP_Feedback.md corpus rather than replacing it: that channel asks for work to be FILED and ends in a roadmap id, this one asks for a person to KNOW something and ends in a receipt. Adds the store's tenth table and moves the schema to version 3.

  • A gate that markdown tables, code fences and blockquotes survive the store round trip (ANTS-4616) Once a project is store-backed the whole-file re-render fires on any write, so every element type a roadmap body can carry has to survive it. Nested lists were measured; these three were assumed. All four new cases passed on the first run — no defect was found, and the assumption is now a gate.

  • roadmap_migrate op:"deregister" removes a project from the shared store (ANTS-4617) Trying a risky operation on a throwaway copy is the careful thing to do, and it permanently polluted the shared database — there was no inverse of migrate. deregister removes a project and everything hanging off it in one transaction. It refuses while the project's folder still exists unless you pass confirm:true, because the database holds history the roadmap file does not; a project whose files are gone needs no confirm.

  • roadmap_log op:"render" publishes the store to ROADMAP.md on demand (ANTS-4614) After adopting the database, the tidy-up a migration computed could not be written to the file until some unrelated edit happened to trigger it — so the only route was to invent a roadmap bullet nobody wanted, and a clean git status after migrating was indistinguishable from the migration never having run. render publishes it directly: no locator, no semantic change, idempotent, dry_run previewable, and it runs every gate the ordinary write ops run.

  • roadmap_query mode:section_index can be narrowed by section name (ANTS-4610) Resolving the one slug a roadmap_log write needs used to cost the whole index — 40 section objects for one string, with neither fields nor compact able to cut it. query now filters the index by section headline or slug, and the reply carries sections_considered / sections_filtered_out so an empty result is legible. whole_word and regex compose, as on the bullets path.

  • A project can declare its own roadmap ID format in .ants/project.json (ANTS-3771) Add an id_format object with a prefix, a pattern, or both, and the roadmap reader stops guessing. prefix decides what a new ID is called and refuses one written with the wrong prefix; pattern says which part of a bold heading is actually the ID, so a bullet written **AX1. Geometric occlusion** is reported as item AX1 with "Geometric occlusion" as its headline, instead of the whole sentence being treated as the ID. Set it with project_settings op:"set" id_format:{…}. A project that declares nothing is completely unaffected, and a heading the pattern does not match keeps exactly the ID it had before — nothing loses its identity. Unblocks the one-time converter for checklist-format roadmaps.

  • roadmap_query says which ids it GUESSED from a bold prose lead-in (ANTS-4575) A bullet now carries id_inferred: true when the reader adopted its id from a bold phrase instead of reading an id the author wrote in brackets — so a session planning work on a checklist-style roadmap can tell a real id from a guessed one. Gated: absent when the id was declared, so a roadmap whose ids are all bracketed sends exactly the same bytes as before. It is not the existing synthetic flag, which marks an id invented from a content hash when there was no text to take one from; the two can never both fire on one bullet.

  • roadmap_query now says when a roadmap file mixes two bullet styles (ANTS-4604) A roadmap carrying both checkbox and emoji bullets reported only the first style it found, so it looked identical to a file written entirely in that style. It now adds mixed with a count of each, and only when the file really carries both.

  • roadmap_log op:"repair_trailers" — recover the trailer values migration cut short (ANTS-4585) A one-off, per project, with a dry run. Values cut at a hard line wrap or at the full stop inside e.g. / config.yaml are recovered by re-parsing the prose that still holds them, since the legacy inline run was never stripped. A value is written only where the stored one is a strict prefix of the re-parse: measured across 16 projects, seven items hold a column newer than its prose, and an unguarded pass would revert those to superseded text. 824 layman values, 58 source and 54 lanes are repairable.

  • A daily audit of what is actually published (ANTS-4588) Checks the current release against reality rather than against the last build's exit code: that the release carries its AppImage, .zsync and permanent alias, that the permanent URL serves a real ELF binary (fetched as bytes, not inferred from a listing), and that every package repository publishes the release's version. Written because two channels broke on the same day while both reported success, and because a per-run check cannot catch a job that dies before reaching it. A fetch that could not run reports as unchecked rather than as drift.

  • A permanent direct-download link for the latest AppImage (ANTS-4586) A stable release now also publishes the same binary under a version-less name, so github.com/milnet01/ants-terminal/releases/latest/download/Ants_Terminal-x86_64.AppImage downloads the current version and never needs updating anywhere. Previously the filename embedded the version, so no permanent direct URL could exist and every link a site or README could offer was a page to navigate. The versioned filename is unchanged and RC builds are excluded, since latest never resolves to a prerelease.

  • README.md's numeric claims are verified against the code on every push (ANTS-4584) tools/check-readme-claims.sh checks the version banner against CMakeLists.txt, the MCP tool count against claudeintegration.cpp, and the colour-theme count against themes.cpp — the tool count in particular had no verifier but a person remembering. It runs from the pre-push hook ahead of the docs-only skip, so a README-only push cannot exempt itself; every tenth push additionally raises the prose for a human read.

  • workspace_search gains match_wrapped:true — find a quotation that is hard-wrapped in the file. (ANTS-4547) A run of whitespace in the pattern matches a run of whitespace and markdown blockquote markers in the text, so a sentence spanning a line break is found and line reports where the span starts. Prose here wraps at ~70 columns, so a line-oriented search missed text that was present and exact — and a review gate that dismisses a finding whose quote it cannot locate then ships the real defect. Two-sided: paste the quotation with its own newlines and > markers and it still matches unmarked text. Literal patterns only; off by default.

  • The roadmap can now answer "when" — dates are recorded going forward, and the past is backfilled from git (ANTS-4501) roadmap_query mode:"report" shipped with nothing to count: the item table's date columns existed and nothing ever wrote them, so every "closed this week" was computed over an empty set. Two changes fix that. Every roadmap write now stamps when an item was created, when it last changed, and — only on the move into shipped — when it closed; a later edit to a closed item no longer moves its closure date, and reopening one clears it. And roadmap_log op:"backfill_dates" walks the project's own git history once to date everything filed before today: 1525 revisions in 4 seconds on this project, dating all 2179 items with none left over. It never overwrites a date that is already there, so it is safe to re-run and a hand correction survives.

Changed

  • roadmap_query and roadmap_log now document that they spell one dialect two ways (ANTS-4606) roadmap_query reports github-task-list where roadmap_log reports gfm for the same format, so comparing the two envelopes read as a disagreement that was not one. Both spellings stand — unifying them would silently break a caller already keying on either, and the roadmap store is machine-global, so that caller may be another project's session. Each verb's detail now says so and tells you to map both spellings.

  • workspace_search's exclude_glob now names its use for scoping a cold review lane (ANTS-4568) A subagent told which files it may read cannot honour that through an unscoped search: one project-wide match returns context lines from the documents it was forbidden. The description now says so, so a caller reaches for the argument instead of trusting the instruction.

  • workspace_search's glob says how to name several paths at once (ANTS-4672) It takes one pattern where its neighbour exclude_glob takes a list, and nothing said that several paths are written as docs/{coding,testing}.md. The description now says both.

  • Seven more MCP verbs now run off the GUI thread (ANTS-4682) The five test_audit_* verbs, caller_cwd_info and project_query no longer hold the window while they work. The other seven inline verbs stay on the GUI thread on purpose, and each now records why in the code.

  • fields= and response compaction are now separate per-verb answers, so spec_lint and feedback_query are no longer compacted unless asked (ANTS-4524) A single predicate granted both, so a verb added to the list for fields= silently began compacting its replies for every caller — which folded away spec_lint's flag saying a check had never run. They are now two columns of one table, and adding a verb makes you answer each. An explicit compact:true is still honoured wherever the schema declares it; only the unasked-for default is per-verb. Partial: making fields= universal is the remaining half, and the blocker that half named is now gone.

  • feedback_query ignores fields=, so a duplicate check pays for the whole delta it was passed to avoid. (ANTS-4665) Asking the feedback reader for just a couple of small numbers returns the entire backlog anyway.

  • compact_resolved retires a legacy tracking heading once every id in it has shipped (ANTS-4646) A v1 "Tracked in ROADMAP" heading survived migration with no verb able to touch it — append_tracking refuses on a v2 file, and assign_id needs a finding those ids do not have. It is retired under the gate compact_resolved already applies, all-or-nothing per heading. It is deliberately KEPT on a fully condensed file carrying no inline proposed ids, where that line is the project's only record of what it reported.

  • roadmap_migrate collapses repeated notes instead of restating one fact hundreds of times (ANTS-4649) A 357-item migration returned 357 identical "field defaulted" objects — about 6 KB — beside two envelope fields that already said the same thing, and both the preview and the real call emitted it. Repeated notes are now merged by (code, detail, source), each merged row carrying a count and up to three sample lines. Measured on the 250-item fixture: 500 notes become 2 rows and 197 bytes, and the answer is now complete where before it was capped at 200 rows and still truncated.

  • a ripgrep start failure now names its cause instead of asking whether ripgrep is installed (ANTS-4650) "rg failed to start (is ripgrep installed?)" could only ever name one cause, and it has been emitted both for ripgrep genuinely absent and for ripgrep present and working — so in one of those cases it sent the reader to the wrong repair every time. The three rg-backed verbs now share one classifier that distinguishes a vanished search root, rg missing from PATH (naming the PATH searched), rg still starting when the wait expired (a loaded host — retry, do not fall back to grep), and rg refused by the OS.

  • A 5-second git-spawn budget in eight test fixtures reddens build-asan at random. (ANTS-4651) One shared, env-overridable budget replaces twelve hard 5-second literals across eight fixtures, and a timeout now says which command blew it and after how long — distinctly from a helper that could not start at all.

  • Large-result previews shrink each row's text sample before dropping it (ANTS-4474) When a preview could not fit a sample of every row it discarded all of them, leaving row sizes but no hint of what any row contained — usually the only part that says what the row is. The sample width now steps down (60 → 40 → 24 → 12) and the narrowest that still covers every row wins, reported as rows_preview_head_chars. Only when no width fits is the preview omitted.

  • A migrated project whose file was never rendered is write-locked, and the render that would free it is gate-refused. (ANTS-4628) The roadmap's "plain-English summary" rule now applies to the items an edit touches, instead of to the whole project. Editing one item no longer demands that every other item in the project already carry a summary — which had made some projects impossible to edit at all, including the one whose repair was itself blocked by the rule.

  • Split the roadmap-write translation unit, which was at its line cap (ANTS-4620) The file holding the roadmap write operations had reached the 6,000-line ceiling the project sets, leaving no room for the next change. It is now two files — one for single-item writes, one for batch and section writes — with the batch and section operations moved out. No behaviour changes; two tests that named the old file by name were widened so they still cover both halves.

  • Declare that roadmap_log op:"append" writes ants-v1 into any roadmap dialect (ANTS-4605) Appending to a github-task-list roadmap has always written an ants-v1 emoji bullet, leaving the file mixed, and nothing said so — the standard actively claimed the opposite ("projects that prefer GFM stay on GFM"). Now declared in roadmap-format.md § 3.10.2 and in the verb's own op:"append" reference. Behaviour is unchanged: the store re-renders the file in one dialect, so the mixed state is transient. op:"flip" does not do this and never did — it applies per the bullet's own format.

  • discarded_edit_lines now comes with a breakdown and the lost text itself (ANTS-4615) One count mixed harmless reformatting with sentences that were deleted, so nobody could tell the difference — a measured report of 84 was 24 restyled bullets and one sentence that no longer existed. A drifted write now also reports discarded_restyled_lines, discarded_text_lines and discarded_text[] naming the lost lines. The total is unchanged: this adds a breakdown rather than suppressing anything.

  • roadmap_log's bad_section refusal now carries ranked near-miss candidates (ANTS-4556) A mistyped slug threw the composed body away and left one route: a section_index call answering every slug in the file. The refusal now ranks the real slugs against what you typed (capped at 10), reusing read_region's ranker rather than growing a second copy. It still refuses — a near miss is a hint, never a resolution.

  • roadmap_query: body is the item's notes, no longer the whole rendered bullet (ANTS-4557) Two projects reported opposite things about this field and both were right: the database column holds notes, while the tool rebuilt the bullet and handed back its title line and metadata block too. The field is now the notes alone — the title is already returned separately. The metadata lines stay, because nothing else carries their text.

  • roadmap_log: dry_run now says what a green preview is, and is not, evidence for (ANTS-4548) The preview is not a simulation — it performs the store write inside a transaction and rolls it back, so every refusal the real call would hit reaches it too. It stops short of the commit and of the write to disk, and the tool description now says so instead of claiming the preview cannot differ from the result.

  • workspace_search: a lane that escapes the project root now names the route out (ANTS-4569) The refusal was correct but terminal. Both the lane schema description and the bad_path envelope now say that to search a different tree you set caller_cwd to that tree's root and confine with glob — so a refused call carries its own repair.

Fixed

  • Roadmap card columns now line up across sections whatever the font (ANTS-4718) The shared column grid was not actually shared: a section whose rows carry a kind grew that column past its reserved width when the text did not fit, while a section without one kept the reserved width, so the two sat on different grids. It only showed where fonts render the kind text wider — never on a typical desktop. The grid is now measured across every card and applied uniformly, and no text is truncated to achieve it.

  • changelog_log add_subsection's docstring described a guard the code does not implement (ANTS-4562) It claimed a refusal against any [Unreleased] carrying flat category blocks, and told you to empty the section first. The guard actually tests ORDER: a section led by a dated topic is accepted however many flat blocks trail below. The remedy is to lead with a dated topic, and a legacy tail no longer reads as a blocker.

  • roadmap_migrate no longer advises a re-run that a known open defect says destroys correct rows (ANTS-4564) Its hint called re-ingesting "routine and idempotent", while ANTS-4507 has established that parse(render(x)) is not identity — so items_updated can be a re-parse artefact and applying it overwrites a good row. Both the hint and the description now say so and point at updated_items[].

  • doc_citations resolves a quotation inside a blockquote (ANTS-4706) A quote written as an indented > block was reported out of date even when it matched its source exactly, and the reported text carried the block markers so copying it into a search found nothing.

  • An offload envelope always reports how many rows exist (ANTS-4705) A summarised result could say its preview was shortened without ever reporting the total it was shortened against — the figure that decides where to read next.

  • doc_citations no longer blames a nearby document for a stale quote (ANTS-4696) A quotation credited to a document cited without its file extension was reported out of date against an unrelated file mentioned earlier in the same paragraph. It now says the target was not recognised, and names it.

  • Publishing the roadmap reports punctuation changes separately (ANTS-4695) A render that adjusted the punctuation of hand-written summary lines counted it as cosmetic restyling, so the field a caller checks before allowing an overwrite read zero while their sentences changed.

  • roadmap_migrate finds a roadmap declared in .ants/project.json (ANTS-4693) A project keeping its roadmap somewhere other than the usual filename could be read and written but not adopted into the database. The other verbs already honoured the declared path; this one now does too.

  • An offloaded result keeps the body's own top-level fields (ANTS-4692) When a large roadmap_query answer was parked to disk, fields like source and file_in_sync vanished with it — the safety information a session is told to read before writing. They now survive, and anything dropped for size is named in preserved_omitted.

  • A roadmap_log dry run no longer leaves a counter file behind (ANTS-4691) A preview that refused still created .roadmap-counter, so pointing a dry run at a repository left an untracked file in it. Both the single-append and batch paths are fixed.

  • A narrowed reply keeps the note explaining how to read it (ANTS-4698) Asking a verb for only certain fields could drop the warning that says a zero means "this roadmap's format wasn't recognised" rather than "nothing left to do". That warning now always survives.

  • Asking the roadmap for a few rows using the search tool's word now works (ANTS-4701) roadmap_query calls its row cap limit where workspace_search calls it max_results. Sending the wrong one used to be ignored and answered with every row; it is now accepted as an alias.

  • token_usage says so when it cannot read the figures, instead of reporting zero (ANTS-4684) Asked while the window is closing, it reported no savings rather than saying it could not look -- a wrong number where an error was owed.

  • A roadmap item's title can be corrected again (ANTS-4668) roadmap_log op:"amend_headline" refused outright on any project whose roadmap lives in the database, which is all of them -- so a title was fixed only by refiling the item under a new number and losing every reference to the old one. It now edits the title in place.

  • The window no longer freezes while Claude runs an MCP command (ANTS-2132) Ants answered every MCP request on the thread that draws the window, so the whole window was frozen for as long as the request took — and with several Claude sessions live, their requests queued behind each other on that one thread, which is why the freezes had no pattern. Eligible verbs now run on a background worker and the window keeps painting. Requests still run one at a time, so nothing that could not previously overlap now does. Measured: a 10 ms timer fired once across a 200 ms verb before, ~20 times after. Two verbs are NOT covered yet and still block for a whole sweep — audit_run and indie_review_dispatch (ANTS-4682).

  • fields= is documented universal but allowlisted, so every other verb drops it AND cannot report it. (ANTS-4524) fields= is honoured on every verb now, and declared on every schema, so a caller who narrows a response gets what they asked for instead of the full payload in silence. Compaction stays per-verb on purpose: it has a default, and a default nobody asked for is what folded away a flag saying a check had never run.

  • changelog_log op:release echoes the whole closed section back with no way to suppress it, so it fails on a large [Unreleased]. (ANTS-4561) Closing a big changelog section returns the entire section as its reply, which is too large to receive.

  • spec_lint's surfaces hint states a FALSE cause: the check does have an input, and it is a hard-coded layout. (ANTS-4679) A tool tells you a check can never be switched on. It can -- the tool just only looks in one place for the thing that switches it on.

  • spec_lint names a check in skipped[] while returning that same check's findings, so the field cannot be read either way. (ANTS-4666) The spec checker reports a check as "did not run" in the same breath as reporting what that check found.

  • roadmap_query's sync_checked:false is folded away by default compaction -- ANTS-4673 in a second verb, already live. (ANTS-4677) A flag meaning nobody checked whether the file matches the database is quietly deleted from the reply, so it reads as checked and fine.

  • spec_lint's skip entries and their hint fields share no token, so a hinted skip reads as unexplained. (ANTS-4676) A tool says it skipped a check and does explain why, but the explanation is labelled so differently that readers cannot tell the two go together.

  • doc_citations quotes:true detects a quoted span per LINE while matching folds newlines, so a hard-wrapped quotation is never checked and lands in no bucket. (ANTS-4664) The quotation checker silently ignores any quote that runs onto a second line, and reports the document as clean.

  • Neither quotation checker survives a hard wrap, so a true quotation reads as unverifiable. (ANTS-4607) Two tools that check whether a quote is real fail when the quote is split across lines, which is how this project writes everything.

  • An unknown roadmap section now suggests near misses on database-backed projects too (ANTS-4591) The "did you mean?" hint had reached only the file-backed path, so every migrated project got a bare refusal and a full section listing as its only route. The schema now also says the hint exists — documenting it first was the original request, and probing the field before writing about it is what found it missing.

  • A refused preview no longer blames an item id that exists nowhere (ANTS-4593) A dry run creates its candidate row inside the transaction and rolls it back, so a render-gate refusal listed an id nobody could look up. The preview's own row is now reported separately, and when it is the only offender the message says the problem is the arguments rather than the roadmap.

  • A truncated first-call file digest now says so under a findable name (ANTS-4560) On a project with no module map the digest is a flat file list, and the only flag describing it was named for the lane digest — while the obvious-looking files_truncated describes the index cap and is correctly false. A reporter read those and concluded a large project had no renderer. source_files_truncated now travels alongside.

  • doc_citations accepts the ~global sentinel its sibling verbs already took (ANTS-4565) It was the one verb of the doc-checking set that refused it, so checking a document under ~/.claude/ meant hand-rolling the citation pass. The basename fallback is unavailable under that root and the description now says so.

  • A narrowing argument sent to a verb that ignores it now says so (ANTS-4578) fields, compact, offload and etag_match were exempt from the unknown-argument advisory on every verb, while the dispatcher honours them only for the verbs that support them — so asking an unsupporting verb to narrow its reply did nothing and reported nothing. The exemption is now decided per call.

  • A roadmap note naming a C++ symbol is no longer read as a trailer declaration (ANTS-4608) A double colon is a scope operator, not a heading. Writing about a symbol whose scope name ends in Source, Kind, Lanes, Layman or Evidence was refused; the quieter half was a scoped symbol inside a value silently truncating it at that point.

  • A spilled MCP reply now still names the argument it ignored (ANTS-4626) The unknown-argument advisory was attached to the response body and then discarded when a large body was spilled to a handle — so it reached the caller only when the reply was small, which is backwards: a filter that was silently ignored is what makes a reply large in the first place.

  • A repeated MCP refusal no longer comes back as ok:true (ANTS-4446) The ETag short-circuit built its {ok:true, unchanged:true} reply before parsing the response, so it never learned the body was a refusal — and the same function attached an etag to refusals in the first place, which is what put a replayable etag in the caller's hands. It now parses first and returns a refusal verbatim.

  • a nested project's feedback file is no longer created where nothing reads it (ANTS-4647) Omitting path derives the file into the parent of caller_cwd, which is the shared corpus only for a project sitting directly under it. A repo nested inside another workspace got a file in that workspace — invisible to every maintainer sweep, and starting an empty parallel record beside the workspace's real one — and was told it succeeded. It now refuses with the ancestor corpus's files as candidates. A configured claude.mcp_feedback_root redirects the write there instead, and a genuinely new corpus with no feedback file anywhere above still creates its first file.

  • project_settings op:detect omits its source counts when no walk ran, instead of reporting 0 (ANTS-4648) A project with a settings file short-circuits before the directory walk, so both counts were still their initialiser — and a ~3000-line project was reported as holding no source at all. They are omitted now, with counts_computed saying which shape the reply is.

  • invariant_check retries a zero with the path's suffixes, so a spec that cites a module by name is no longer invisible (ANTS-4644) A spec cites a module the way its author writes it, so the project-relative path the verb's own description prescribed was routinely the one form that matched nothing — and matched_count:0 beside specs_scanned:247 read as "nothing governs this file". On a zero that did look, the scan now retries with each path's suffixes, longest first; the bare basename is a separate later tier, used only when every fuller form has failed. fallback_match rides on every reply, and matched_as names the form that actually matched. Measured over this repo's 247 specs: 29 source files that returned a confident zero now return their governing specs.

  • doc_integrity broken_link: a ~/-prefixed target is resolved as a relative path and reported as a missing file. (ANTS-4643) Links written with the home-directory shortcut are reported as pointing at nothing, with a message that sends you looking for a file that is there.

  • doc_integrity broken_link: skip a link whose enclosing span is a verbatim quotation of another document. (ANTS-4642) Quoting another document word for word drags its links along, and the checker reports them as broken.

  • doc_integrity heading_sequence: suppress a numbering gap the document itself accounts for, and say that it did. (ANTS-4641) A file that explains why it skips numbers 2, 3, 10 and 13 still gets flagged for skipping them.

  • doc_citations quotes: a table row's LAST backticked path wins attribution, which is the wrong one in an impact table. (ANTS-4640) In a table listing which documents a change affects, the quote gets checked against the affected file instead of the source.

  • doc_citations quotes: resolve a bare basename against the scanned document's own directory, and stop making non-document tokens targets. (ANTS-4639) A quote pointing at a sibling document in the same folder is reported as having no source at all.

  • doc_citations quotes: skip a quotation sitting in a loop-log table row — 271 of 520 in one corpus (52%) are historical by design. (ANTS-4637) The quote checker flags wording that a review deliberately replaced, so most of what it reports is not a problem.

  • roadmap_query: a freshly migrated project no longer warns that its file is ahead of its own store (ANTS-4636) idHighWater() reads the id_prefix row, and migration does not write one — only an id-allocating append does. A migrated-but-never-appended project therefore reported a store high-water of 0 while holding every id in its file, so file_ahead_of_store was pinned on permanently and read as "the migration silently wrote nothing". The mark is now the higher of that counter and the ids the items actually hold. Reported by Album_Builder on a 357-item project.

  • Confirmed that file_ahead_of_store's baseline firing is a real defect, not a fixture artefact (ANTS-4633) Album_Builder reported the same flag on a real 357-item project whose ids were minted under a previous directory name, which is this item's own hypothesis confirmed in the wild: a high-water lookup keyed on a prefix re-derived from the path finds no rows and reports 0. The fix is tracked as ANTS-4636.

  • roadmap_log: op:"append_batch" now reconciles .roadmap-counter on the store path, as op:"append" already did (ANTS-4635) The cache stayed stale for a whole batch until some later single append's scan repaired it, so anything reading it to predict the next id got a wrong answer that looked right. Reported by Games_Hub, who measured three batches allocating GHUB-0113 through GHUB-0118 with the counter still reading 110. The reconciliation is now a shared helper both paths call, and the batch envelope reports counter_advanced_to / counter_advanced_past when the cache moves.

  • roadmap_log: a store-backed dry run reports would_be_id / would_be_ids, never id / ids (ANTS-4634) ANTS-4508 established that a preview must not report the id under the real write's key — a caller reading one field takes it for a reservation, and nothing is reserved. That rename landed on the markdown branch only, so every migrated project, which takes the store branch, still got id back from a dry run. Reported by finbreak and reproduced directly. A test in roadmap_write_half had pinned the store branch to the old behaviour; it now asserts the new key and the absence of the old one.

  • roadmap_log reports the status it just committed, not the one it read (ANTS-4466) On a store-backed project, op:"annotate" described the stale file it read rather than the result the render had committed — so after a git checkout -- reverted ROADMAP.md, the envelope said the item was still planned while the write had correctly restored shipped. Nothing was lost; the report was wrong, in exactly the case where a caller confirming a write most needs it right. A new file_status names the file's value when the two disagree, so a divergence is visible rather than silently resolved.

  • Roadmap id allocation reads the store's id columns instead of scraping the rendered roadmap (ANTS-4631) A migrated project's ROADMAP.md is a rendered output of the store, but the allocator still floored its next id to a \bPFX-NNNN\b scan of that file's whole text — so it re-read the store's own answer back through prose, which cannot tell an allocated id from a documented example. One deliberately absurd ANTS-9999 inside a body was read as the high water: the next append issued ANTS-10000 and burned ~5,370 ids, and every roadmap_query reply reported the file ahead of a store that was exactly in sync, pinning on the one indicator that would reveal a real divergence. The store path now asks two columns and reads no file. The two callers with no database to ask — an un-migrated project, and the file-vs-store witness — count only lines that declare an id, a top-level list item outside a fence.

  • roadmap_query elides a long body BEFORE spilling it, so the middle is unreachable by any argument. (ANTS-4630) A very long roadmap note can now be read right through. Before, its middle was cut out and the suggested fix could not reach it.

  • changelog_log op:"add" double-wraps a summary that already carries its own emphasis or id. (ANTS-4629) Writing a changelog entry whose text already carried bold or its own reference number produced a mangled line, and the reply gave no sign of it. That is now refused with an explanation, and every write reports the line it actually wrote.

  • render_gate_unmet now names a remedy that works — repair every offender in one flip_batch (ANTS-4434) The render's Layman gate is per project and is evaluated after the mutation, so repairing one item at a time is refused by whichever offenders remain and rolled back — a single repair commits only when it is the last one outstanding, and a project with two or more could not be repaired one item at a time at all. The refusal previously named the offenders and no way out. It now names the route that works: one flip_batch carrying every id from gate_failures, each locator with a note whose first line declares the Layman: trailer and to_status set to the status those items already hold. No gate change, so ANTS-3758 INV-5 is untouched. Locked by a new RoadmapWriteHalf case that asserts both halves — each single repair refused while the other is outstanding, and one batch repairing both. ANTS-4434 stays open for the bulk case, where the offender count is 585 and a batch of hand-written summaries is not a remedy.

  • session_orient now honours fields=, narrowing the largest response in the session (ANTS-4624) The verb joined the projection allowlist in 0.7.105 but never gained the matching fields schema property, so the argument reached the dispatcher as a string, failed its array check, and was skipped in silence — the full bundle came back on every narrowed call. The property is now declared, and the test that should have caught it derives its count from the allowlist instead of hardcoding one.

  • roadmap_migrate now declares its op and confirm arguments (ANTS-4621) op:"deregister" and confirm:true were documented in the verb's description but declared in neither its schema properties nor any enum. The schema sets additionalProperties:false, so a strictly validating MCP client refused the call outright — and on the permissive path the ignored_args advisory named op as ignored on the very call that argument had just steered.

  • amend_body no longer collapses an aligned or nested block when a match spans it (ANTS-4612) A wrapped match re-flows the lines it spanned into one, which is right for a hard wrap and destroys a column-aligned table or a nested list — and every repair attempt ran the same pass, so the damage accumulated. Such a span now refuses body_match_wrapped_block and writes nothing. Ordinary hard wraps are unaffected, including a bullet whose continuation sits at a deeper hanging indent.

  • feedback_log no longer derives a stray feedback file from a dotted project leaf (ANTS-4613) Omitting path from a project like ~/.claude created <home>/.claude_Ants_MCP_Feedback.md and reported success — a hidden-looking file that could never match the naming convention, so a session quietly started a second feedback file nobody reads while the real one went untouched. A dotted leaf now refuses with the sibling files as candidates. Only the leaf counts: a project under a dotted parent still derives normally.

  • roadmap_query's no_roadmap_loaded refusal now says where it looked (ANTS-4611) Passing an explicit caller_cwd and getting back "no ROADMAP.md detected for the active tab" sent callers to diagnose tab misrouting. The refusal now reads "no ROADMAP.md under " and carries resolved_root; the tab is named only where the tab supplied the path. The verdict is unchanged, and the schema now says the code is an answer — this project keeps no roadmap — rather than a failed call.

  • roadmap_log body scrub now strips leaked OPENING tags, not just closing ones (ANTS-4609) Peeling a closing tag off the edge of a body exposed the opening half of the same pair, alone on its line with its value, and the scrub stopped there — so a bare <compact>true line reached ROADMAP.md under an ok:true envelope. Only a lone scalar after the tag counts as leakage, so markup followed by prose still survives.

  • a trailer value your own text got wrong is no longer reported as a database fault (ANTS-4577) A body declaring an unrecognised Kind: refused under store_failed, so a caller branching on the code read an engine fault for their own text — and a retry is the wrong answer to both readings. It now answers bad_kind, which is what the same value in an argument has always answered.

  • find_definition now resolves C++ definitions whose return type wraps onto its own line (ANTS-4603) Every definition anchor matched a single line, so the common shape for a long return type resolved to nothing — which reads as "no such symbol" for a function that is plainly there. 30 such definitions in this tree, including 14 of RoadmapStore's.

  • roadmap_log's store-only ops now name the project they refused (ANTS-4602) repair_trailers and backfill_dates reported the store holds no row for "" on every refusal, naming neither the project nor the file — the shared lookup filled its out-params only when it succeeded.

  • A throwaway temporary folder can no longer register itself as a real project (ANTS-4600) The roadmap database is shared by every project on the machine, so anything recorded in it as a project stays there. A session's temporary scratch folder had been recorded that way: 33 items that were copies of another project's, under a folder that was deleted minutes later. Every machine-wide count has been reporting one project too many ever since. Migrating from a folder under the system temporary directory is now refused, and says which folder to use instead. The already-recorded copy is removed separately.

  • roadmap_query: the include_body schema now says a trailer line can be composed from a column (ANTS-4599) The store-backed record is built by rendering the item and re-parsing it, so the body carries a trailer line for every column whose key the stored prose does not declare. The field answers what the bullet says in ROADMAP.md, not whether the stored body declares that field — and the schema had promised the second. Behaviour unchanged; locked by INV-6.

  • A Lanes: list is no longer cut short at a dotted lane name (ANTS-4597) The lanes capture stopped at the first full stop, which for this key is usually a dot inside a filename, so every member after it was lost — remotecontrol.cpp, AuditDialog, RoadmapDialog, … was read as the single lane remotecontrol. The value now ends at its sentence stop instead, where a dot inside a token does not count, and the list is split from the finished text. Measured over the roadmap store: 15 bullets parse differently, 5 gain whole members, nothing loses one.

  • The layman capture stops at the first period, not the trailing one. (ANTS-4596) A plain-English roadmap summary containing "e.g.", "etc.", a decimal like 41.5, or a filename like .deb was cut off at that dot when the roadmap was read. The capture now runs to the end of the line and removes only the sentence's own closing period, which is what the two neighbouring trailer keys already did.

  • bullet_ambiguous no longer advises narrowing by the locator that just failed (ANTS-4574) When an id matched two bullets and neither carried an anchor, the refusal said "narrow with anchor or id" — naming two routes that were both closed. It now names the id as the ambiguous one and points at the headline locator, or says outright that no locator can address them.

  • Roadmap render no longer doubles the full stop on a trailer value that already ends in one (ANTS-4582) Kind:, Source: and Lanes: now get their terminating period only when the value lacks one. The period belongs to the format, not the value (roadmap-format.md § 3.5 names Evidence: as the one key rendered without one), so a value supplied as a sentence was rendering with two.

  • Package-manager users were two releases behind (ANTS-4587) All four repositories (openSUSE Tumbleweed and Leap 16.0, Fedora 44, Mageia 10) served 0.7.103 while the current release was 0.7.105. The _service file in git correctly pinned v0.7.105, but the OBS project's own copy still pinned v0.7.103 — the submit step was never run for 0.7.104 or 0.7.105, so OBS kept rebuilding the source archive it already had and reported every build as succeeded.

  • A bullet listing four lanes no longer stores three (ANTS-4553) Reported against finbreak, where a bullet's prose listed four lanes while the stored column held three. Same cause as ANTS-4542: the list was truncated at the line wrap rather than diverging from a second source. Values already truncated in existing stores are not repaired by this change and are tracked as ANTS-4585.

  • A hard-wrapped roadmap trailer value keeps the text below the wrap (ANTS-4542) Every trailer pattern stopped its capture at the line break, so a value wrapped mid-phrase lost its tail: Lanes: MainWindow, stored one lane and dropped TerminalWidget. from the next line, and the render then re-emitted the short value terminated with a full stop, which reads as a correct declaration. A wrapped value now continues onto the following line, stopping at its own sentence-terminating full stop, a blank line, a line opening another declaration, or four lines. No-op on generated content, which is always emitted unwrapped.

  • A release can no longer be published with no downloadable file (ANTS-4583) v0.7.105 shipped with no AppImage: its build hit a 25-minute job timeout (GitHub reports that as "cancelled") inside the first step, so nothing was built or uploaded, while the release page existed because the tag created it. The ceiling is now 60 minutes — sized against the slowest observed run, 22m36s, not the fastest — and a new final step re-reads the release and fails the run unless both the AppImage and its .zsync are attached.

  • roadmap_query: an item's notes come back with their own indenting (ANTS-4558) Notes read back through the tool were flush-left even where the file had lists and indented commands, so text read out and written back lost its structure. The block is now dedented by its shared indent only.

  • Roadmap items keep their indenting: a nested sub-bullet is no longer re-attached to the bullet above it (ANTS-4554) Reading a bullet stripped the indent off every line of its notes, so writing the file back put them all at one level — which in Markdown makes an indented sub-point belong to the wrong parent. The shared indent is now removed and anything deeper is kept, so lists and command lines survive a write. Items imported before this change are already flat; their original indenting is only in git history.

  • roadmap_log: deleting a Kind: or Source: line from an item's notes no longer fails with a database error (ANTS-4576) Removing a trailer declaration from a body used to clear the column, and four of the five columns cannot be empty — the caller got a raw NOT NULL constraint failed. What un-declaring means is now the column's own contract: the plain-English line clears, lanes and evidence empty to a list with nothing in it, and kind and source keep their value, which the roadmap file then states canonically. An amend_body new_text is guarded like a note, and a recognised kind is canonicalised rather than refused.

  • roadmap_query's duplicate_ids[] now reports an authored id shared by two bullets, whatever its shape. (ANTS-4546) On a roadmap whose bullets lead with a bold span, two bullets sharing that span both carry it as their id — and roadmap_log then refuses bullet_ambiguous, so a silence on the read side was paid for at write time. The detector had been keyed to the canonical [PROJ-NNNN] shape to stop the reader's own content-hash nonces over-reporting, and that key excluded the authored ids too; it now excludes only the nonces, which is what it was for. Read the field as "this id addresses more than one bullet". A roadmap whose ids are all canonical is unaffected.

  • A roadmap_log note is prose — a trailer keyword named mid-sentence no longer rewrites that column. (ANTS-4549) op:"annotate", op:"flip" and op:"flip_batch" append the note to the bullet's body, and the trailer columns are then re-derived from that body — so a bare Kind: in a sentence was read as a declaration and its following words written to the column. Caught on one report only because the store's CHECK constraint refused an invalid kind; an accepted one would have written silently, and the other four columns have no CHECK. A note may now name a trailer key only with the label first on its line, which is how the render writes a declaration; mid-line it refuses body_shadowed, quoting the text and naming both remedies. Position rather than a value check, so the one deliberate use — a Layman: note filling that column on a migrated item, which the render gates on — keeps working.

  • roadmap_log op:"amend_body" amends a phrase that straddles a hard-wrapped line break, in one call. (ANTS-4550) old_text is now exact apart from its whitespace runs, which span a line break. The wrapped pass runs only after the exact one finds nothing, so no previously-succeeding call changed, and the body_match_ambiguous guard applies to whichever pass matched. This removes the multi-call hazard ANTS-4097 could only echo. Shares one matching rule (src/wrapmatch.cpp) with workspace_search, so the two verbs cannot answer one quotation differently.

  • ROADMAP.md's version banner no longer silently reverts after a release (ANTS-4529) The bump recipe hand-edited a version number into ROADMAP.md, which is generated from the roadmap store — so the next roadmap write of any kind re-rendered the file and discarded the edit, putting the banner back to the previous release and re-opening version drift long after the release commit had looked clean. The banner now states no version at all: the number duplicated CMakeLists.txt's project(VERSION), and the sentence carrying it already links CHANGELOG.md, which lists every shipped version with its date. ROADMAP.md leaves both the bump recipe and the version-drift gate, so there is nothing left to keep in step.

  • The sanitizer CI job finishes again, and a timeout now reads as a failure (ANTS-4533) The memory-safety half of CI had been quietly giving up part-way through for two days — cancelled at its 30-minute cap on 36 of the last 40 runs. It did not look like a failure, because a job killed by a timeout is reported as cancelled rather than failed, so nothing went red and nothing notified. Three causes, all measured: the long steps are now wrapped so an overrun genuinely fails; the compiler cache is saved even when the job does not finish (a cancelled job skipped the save, which froze the cache and made every later build slower, which made the timeout likelier); and the sanitized test run is parallel instead of serial. Measured after the fix: 922s to 704s on the tests, and a first fully green run.

  • Re-importing the roadmap no longer piles metadata lines up inside entries (ANTS-4506) The migration now drops the trailing run of trailer-only lines from what it stores, because that block is the renderer's own output rather than anything an author wrote. Measured before the fix: one re-import moved 599 entry bodies, 458 gaining a Kind: line and 97 a Source: line. A trailer line sitting ABOVE authored prose is the author's and stays, and a line is only dropped when it is the sole declaration of its key — the condition that stops an entry rewriting its own metadata. The indentation half of the round-trip report is ANTS-4528 and is still open.

  • A roadmap entry's metadata can now be corrected by editing the file (ANTS-4505) The renderer used to keep a trailer line (Kind:, Source:, Layman:, Lanes:, Evidence:) out of its own output only when the entry's text repeated that value exactly. Because the renderer appends its block at the END of an entry and the reader takes the last declaration, a value read wrong once won for ever: correcting the real line in ROADMAP.md was silently ignored. Suppression is now presence at the start of a line — whatever the value — so the file wins and the store follows on the next import. A mid-sentence mention still declares nothing, and an unrecognised Kind: value cannot displace a recognised one.

v0.7.107-rc2Pre-release

0.7.107 RC2 — Patron preview

Respin of v0.7.107-rc1 with cherry-picked fixes: 143fa73a (ANTS-4718 — roadmap card column grid is now genuinely shared across sections; the previous RC fails the openSUSE/Mageia package build).

v0.7.107-rc1Pre-release

0.7.107 RC1 — Patron preview

This release shipped without written notes.

v0.7.106

0.7.106

Added

  • ignored_args is suppressed on refusals, so a wrong-argument call never learns its args were wrong. (ANTS-4525) When a call fails, the tool stops telling you that you also used the wrong option names.

  • mutation_probe mutates every occurrence with no way to state how many were intended. (ANTS-4521) A test-quality check can quietly change more places than you meant, and still report success.

  • roadmap_query's bad_mode refusal names the id/ids[] route (ANTS-4511) mode:"by_id" is the obvious guess for a single-item lookup, and none of the accepted mode names answers it — item lookup is an argument, not a mode. The hint fires on every bad mode, not just that spelling.

  • roadmap_migrate states that it never rewrites ROADMAP.md (ANTS-4482) markdown_rewritten is on every response and is always false. The file being byte-identical after a migration is expected, not a sign it did not run.

  • roadmap_migrate says whether the project will be served from the store (ANTS-4490) store_backed answers it on every response: only an ants-v1 roadmap is served from the store afterwards, so a github-task-list project migrating successfully now says so rather than leaving the caller to notice which fields a later query does not carry. sections_unchanged accompanies sections_written, so 0 written reads as an idempotent re-run rather than as a dead counter.

  • roadmap_migrate names which items it would update, and which fields (ANTS-4479) updated_items lists the id and the changed column names for each item items_updated counted, capped at 200 with updated_items_truncated. Under dry_run that turns a bare count into a plan you can review before running it.

  • apply_edits says where the occurrences are when an edit matches more than once. (ANTS-4473) An ambiguous skip was correct and silent about location, so recovering from it meant re-reading the file. It now carries candidates:[{line,text}] — the same field a not_found near-miss already returns — plus match_count for the true total and candidates_truncated when the ten-entry list is capped. Recover by extending old with surrounding context, by naming the occurrence with the start_line/end_line form, or with replace_all:true.

  • roadmap_log and changelog_log accept each other's word for "add an entry" (ANTS-4475) One spells it append and the other add, and each already accepted the other's word for the batch version. Both now accept both. Error messages still name the canonical form.

  • section_index can return just the slugs, which no longer overflow on a large roadmap (ANTS-4467) Pass slugs_only:true to get a flat list of section slugs. On roadmaps big enough to overflow the reply budget the full form returned only the first handful of sections — and slugs are exactly what roadmap_log needs to file a bullet.

  • a refused cross-project feedback-file read now names the call that works (ANTS-4421) The refusal is correct — the file lives outside the project — but it was a dead end. It now names the folder to use as the working directory.

  • searching with HTML entities now warns instead of silently finding nothing (ANTS-4420) A pattern written as &lt;h1&gt; rather than <h1> found nothing and read as a confident answer; it now asks whether the literal characters were meant.

  • a failed text edit now points at the line that nearly matched (ANTS-4418) When an edit misses only because of spacing, apply_edits reports the line number and that line's exact text, so the retry needs no re-reading and no guessing at the whitespace.

  • an empty code map now explains why it is empty (ANTS-4419) Instead of a bare "empty" flag, codebase_index says whether the project was never registered, whether its declared source folders hold no code, or whether there genuinely is none — and names the folders that do hold code.

  • Feedback v2 gains an "awaiting reporter" triage state (ANTS-3631) assign_id takes a third disposition, awaiting, writing _(awaiting reporter — <question>)_. It stays un-triaged on purpose so the question reaches the reporting session's feedback_query delta, and it is classified before the id and closure tests so a question quoting an id is not mistaken for an assignment. feedback_query emits awaiting[]; session_orient separates the maintainer's outbox from their inbox. Layman: When I triage another project's bug report and need to ask them something, there is now a way to ask it that they will actually see.

  • build_target_for — which build target owns a source file (ANTS-3745) New read-only MCP verb. Parses CMakeLists.txt and answers with the owning target, the cmake --build --target line, and the ctest -R filter for the source's gtest suites. Replaces an awk over CMakeLists.txt and a --gtest_list_tests launch per bundle; the bundle mapping is not guessable from the path. Layman: Ask "which test program do I rebuild after editing this file?" and get the exact build and test commands back.

  • spec_lint falls back to the global spec-format standard (ANTS-4080) A project carrying only a delta was linted against nothing, and the skip read as a clean structural result. The four candidate paths are now retried under ~/.claude/ through the same ~global re-rooting doc_integrity uses; sections_source prefixes a global hit with ~global/. Projects shipping their own standard are unaffected. Layman: The spec checker now falls back to the shared standard when a project has no copy of its own, and says which one it used.

  • spec_lint: a document can declare a deliberate invariant-id floor (ANTS-3784) A spec that continues a sibling's numbering can now say so with an <!-- invariant-id-base: N --> line outside fenced code; missing numbers below N are counted as suppressed instead of reported. Measured over docs/specs/: 39 gap findings became 28, one document moved. Layman: The spec checker kept flagging deliberately skipped numbers as mistakes; a spec can now say the skip is on purpose.

  • roadmap_query now says which backend answered — source: "store" | "markdown" (ANTS-4402) The store has been primary for roadmap reads since ANTS-3793, while ANTS-4141's standing workaround has roadmap edits made by hand to ROADMAP.md — so a hand edit lands in a file no reader reads, and the envelope gave no way to tell: it named ROADMAP.md as its path, returned ok:true, and served the store. file_ahead_of_store (with file_highest_id and store_high_water) fires when the file holds ids the store has never seen. One-directional by design: it proves staleness, never freshness, because a status flip or a body edit moves no id. Reconciling the two backends remains ANTS-4141.

  • co_change_family — every edit site of one settings field, in one call (ANTS-3368) Adding a setting by copying an existing one is a ten-file lockstep change, and the easiest sites to miss are the ones that are not spelled like the field: the text key in the config file ("claude.mcp_enabled") and the helper names built from it (setClaudeMcpEnabled, m_claudeMcpEnabled, setMcpEnabled). Ask for one example field and this lists them all, grouped by file, each line tagged with what it is. Measured on this repo's own claude.mcp_enabled family: it spans 11 files under src/, where searching for the whole name finds 6 — and one of those 6 is found only by a comment, while the two lines you actually have to copy in that file match nothing at all.

  • changelog_log op:normalize now tidies stray prose, not just category order (ANTS-3381) A paragraph left stranded between the category blocks of [Unreleased] is folded into the entry above it, in the same one-shot tidy that puts the categories in order. Every fold is listed back to you as moves[], under dry_run as well as on a real write, so you can see what would be absorbed before it is. A line separated from its entry by a sub-heading is left alone and still reported. Completes the half ANTS-3495 deferred.

  • cited_by — one call that says which document cites which anchor (ANTS-3716) Give it the names a change touched and it reports which documents mention which of them, with an occurrence count and a first line per pair — plus the anchors nothing cites, which is the half a caller cannot infer from an absence. One ripgrep run per anchor, so each match belongs to its anchor by construction. Replaces a per-anchor search loop a review pass otherwise runs by hand four times a round. Scope defaults to the project's docs directory, README.md and CLAUDE.md.

  • doc_integrity catches a skill that calls an MCP tool it never granted itself (ANTS-3719) A Claude Code skill lists the tools it may use in its frontmatter and names the ones it needs in its instructions. When the two drift the skill cannot be run as written — and the skill that hit this twice was a review skill, so the check meant to catch everything else was itself broken. A new ungranted_tool finding reports any mcp__ants__ verb the body calls that the allowed-tools list omits. It fires only on files that actually carry that frontmatter key, so ordinary documentation never sees it. doc_integrity also accepts the ~global / ~claude-config root that search and outline already take, which is what lets it read the skill tree at all.

Changed

  • A spilled read_region emits a headless rows_preview that costs ~1k tokens and conveys nothing. (ANTS-4519) A big read sends back a long list of empty rows that tells you nothing but still costs money.

  • roadmap_migrate's description now states that it never writes ROADMAP.md, and which roadmaps are served from the store. (ANTS-4482) A byte-identical file and a clean git status immediately after a migration are expected — the file is first re-rendered by the next roadmap_log write — and only an ants-v1 roadmap is served from the store afterwards, so a github-task-list project migrates successfully while roadmap_query keeps answering from markdown. The description also records that the store is machine-global rather than per-project. (ANTS-4490)

  • roadmap_migrate's selection hint says that re-running is the routine way to reconcile a drifted store. (ANTS-4480) It read "Use ONCE per project", which two sessions read as one-shot-and-dangerous — one nearly skipped the re-run that reconciled its drift, the other hand-edited a 616 KB generated ROADMAP.md instead. The hint now says re-ingesting is routine and idempotent, names dry_run as the preview, and states that the verb never writes ROADMAP.md.

  • focused_test now points at mutation_probe (ANTS-4472) Three sessions in a row hand-wrote the mutation loop that verb replaces, because nothing surfaced it. The session-start list is full to within 10 bytes of its cap, so the pointer lives on focused_test instead — which is where a session running tests will be looking anyway.

  • roadmap_query reads ROADMAP.md once per call, not once per branch (ANTS-4431) A cold section= query opened and read the whole roadmap twice — the heading index and the per-section ETag each built their own file reader, in the same function, so neither could reuse the other's memoised body. One reader is now created per call and every branch shares it, folding five readers into one.

  • roadmap_log op:append no longer reads ROADMAP.md to check for near-duplicates (ANTS-4426) On a migrated project the ANTS-2043 advisory took its records from the whole 3.8 MiB file; it now takes them from the roadmap store, so the append path reads no document body at all. No answer changes — a file carrying a bullet the store has never imported is refused outright, and that is the only input on which the two sources could differ.

  • Roadmap reads dispatch before loading the file, so a migrated project no longer pulls 3 MiB of ROADMAP.md to ask which backend serves it (ANTS-3863) The read seam's markdown parameter became RoadmapSource::RoadmapText, a move-only provider that reads lazily and only as far as the caller's path needs: the dispatch spends the detector's window — at most 300 non-blank lines or 1 MiB, whichever comes first — and the whole body is read only on the unmigrated path that was always going to parse it. Thirty call sites converted; no verb's response shape, refusal code or message changes.

  • the session startup tool list now spells out that read_regions batches (ANTS-4422) It read as a plural of read_region, so sessions made several calls where one would do.

  • the roadmap write refusal now says where backticks must go (ANTS-4424) "Wrap the key in backticks" was true only if they sit immediately around the key; a key inside a longer code span is still read as metadata, so a caller who already had backticks was refused twice with the same advice.

  • Roadmap dialog — the filter bar collapses to three controls (ANTS-4412) Sixteen checkboxes across two rows became Status: all ▾, Kind: all ▾ and Reset filters, with the density combo moved up to the search row. The reset button is enabled exactly when something narrows the list, so a collapsed control cannot hide why the list looks short. Keyboard access is unchanged. Layman: The cluttered row of options at the top of the roadmap window is now three tidy buttons.

  • Audit: the Contract-Doc ↔ Code Drift lane no longer reports any head:value citation (ANTS-3849) The 2026-08-13 guard covered a path-shaped head only (remotecontrol.cpp:2540); it now covers any head, so a symbol:line citation (applyTheme:3118) and a JSON field-and-value fragment (sections_checked:false) are skipped too. Source spells the head and the literal separately, so the joined form can never resolve. Measured before shipping: 128 of the 129 distinct heads still reaching the guard resolve in source, so the finding carried no information about its head. 175 of 1,175 findings, 14.9%; verdicts diffed old-vs-new over the whole corpus with 0 newly firing.

Fixed

  • A filename mentioned in an entry's Source line is no longer reported as a missing file. (ANTS-4502) The import treated a token as a path if it carried EITHER a directory separator OR a filename-shaped ending, and either half alone is a bad test. The ending alone flags every bare filename and every §4.4-shaped fragment: 11 of 12 notes on one project, every file existing one directory down. The separator alone flags every prose slash: measured over this project's 1,781 entries it accepts 97 tokens of which 2 are real paths, the rest being slash-command names, id pairs and phrases like precision/convenience. Requiring both gives 1 candidate, 1 real, 0 false reports. A path with no extension — a bare directory — is no longer checked, which is the whole cost.

  • A roadmap entry that quotes the format no longer has its own example read as real data. (ANTS-4504) The guard against a quoted trailer key was three fixed-length lookbehinds, so it only saw a backtick one to three characters before the label. An entry whose example wrapped across two lines had the second key parsed as a real declaration — ANTS-3808 gained a Lanes: value it never wrote. Every key is now matched against a masked copy of the body in which inline code spans are blanked, with values taken from the original text so a real value keeps its own backticks. Span boundaries come from the project's existing CommonMark primitive rather than a fourth hand-rolled pairing.

  • roadmap_log dry_run returns the next id under the same key the real write uses, so it reads as a reservation. (ANTS-4508) The preview tells you which number your entry will get, but does not hold it — and the wording makes it look held.

  • read_region section= and file_outline cannot see a heading inside a blockquote. (ANTS-4520) A heading written inside a quoted block is invisible to the tool, so the one section you were told to read cannot be addressed by name.

  • read_regions refuses conflicting alias arrays instead of picking one (ANTS-4512) Sending both paths and regions silently used one and then reported a shape error against the other. It now refuses bad_op_combo naming both, and a successful call echoes items_key.

  • read_log no longer reads as a git-log reader in the session bootstrap (ANTS-4510) It sat between git_state and model_switch_stats described as a "filtered log tail", and now says Ants debug-log tail (NOT git log).

  • session_orient honours fields= (ANTS-4523) It had accepted and silently dropped the argument — ~16k tokens for a three-integer answer, on the documented first call. Narrowing trims the payload, not the work: the eager codebase_index refresh still runs.

  • Bash guard-hook now routes a --stat diff to git_state op:diff (ANTS-2169) It had named get_git_status and roadmap_query, neither of which can return changed lines, so obeying the block led to a dead end and then to the bypass token. The status/log branch keeps get_git_status.

  • roadmap_log now reports the hand-edits its re-render overwrote (ANTS-4462, ANTS-4465) On a migrated project every roadmap_log op rewrites the whole ROADMAP.md from the store, so anything typed into the file by hand — the preamble above the first heading especially, which no verb can write — was silently put back. The write now renders the store as it stood before the change, compares that against the file, and reports discarded_external_edits with a line count (would_discard_* on a dry run). It reports rather than refuses, so one hand-edit cannot block every roadmap op on a project. Expect one true report per project on the first write after a migration, where the render canonicalises formatting the migration kept verbatim.

  • A roadmap migration no longer overwrites a stored field with a value it invented (ANTS-4498) Where a roadmap file does not declare an item's Kind: or Source:, the import supplies a default. Re-running a migration then wrote that invented value over whatever the database already held, so re-importing a project could quietly replace real information with placeholders — on this project's own roadmap, 384 items were exposed to it. A defaulted value is now withheld from any field that already holds one, and the migration reports each withheld write. The effect is that a re-run can only add information, which is what makes it safe to run without taking a backup first.

  • The roadmap's id counter cache no longer drifts behind the database (ANTS-4141) Filing an item on a migrated project allocates its number from the database and left the on-disk counter untouched, so the counter fell further behind with every item — measured at 99 behind on this project. Anything still reading it, such as a fresh clone or a hand-written entry, would then pick a number already taken. It is now refreshed on each write, as a best-effort cache update that cannot fail the write itself.

  • A roadmap item that quotes an example above its own details no longer imports the example's values (ANTS-4497) Four of the five detail lines an item carries were read from the FIRST place they appeared in the item, so an item quoting a sample block — or whose title happened to contain one of the labels — was filed under the sample's values instead of its own. All five now take the last one written, which is the rule the import contract already specified and which only one of them followed.

  • The first roadmap item filed after a migration no longer reuses an id the database already holds (ANTS-4493) When a project is migrated, items with no id of their own are given one. Those ids live only in the database, and the code that hands out the next id was reading only the roadmap file — so the next item filed took a number already in use. It now checks the database too. counter_advanced_past is reported alongside counter_advanced_to, so the response says what the number was skipped over rather than only where it landed.

  • A long roadmap headline no longer splits its item in two when the file is rewritten (ANTS-4484) A headline that wrapped across two lines was stored with the line break inside it, and written back out with the second half flush against the left margin — where markdown reads it as a new bullet, so the item visibly split and the trailers below it were then read as belonging to the fragment. The headline is now joined into one line both when it is read into the store and when it is written back, so an existing roadmap with the damage in it is repaired by the next write.

  • roadmap_migrate's preview reports the real project id instead of 0 (ANTS-4478) A dry run over an already-migrated project answered project_id: 0 beside counts that could only have been computed against that project's real rows. It now reports the store's id for that root on both paths; 0 means only that the root is not registered yet, so a preview answering project_id > 0 reads as "already migrated".

  • roadmap_migrate no longer reports a resolvable path as missing when the provenance is a sentence (ANTS-4481) A Source: value is prose far more often than a bare path, and the whole value was being resolved as one filename — so every item whose provenance mentioned a real file was reported unresolved. The unit is now the whitespace-delimited token, as the import contract always said. A token that ends a sentence also gets its punctuation stripped on a retry, and the note points at the line carrying the path rather than at the bullet's first line.

  • codebase_index can see HTML pages, so a website project's code map is no longer near-empty. (ANTS-4425) file_outline has outlined HTML since ANTS-4361 while the index gate did not admit it, so on a site of ~40 HTML/CSS/JS files the index counted 9 — the .js and .py — and every page was invisible, to codebase_index and to the indie_review computed partition alike. Both now key on one shared predicate. CSS stays out until it has an outline mode, because counting files the outline cannot read is the same drift in the other direction.

  • project_layout finds an AppStream metainfo file kept one level under packaging/ or pkg/. (ANTS-4439) Every probe was single-level, so a project packaging for more than one distro — packaging/obs/ beside packaging/flatpak/ — reported no AppStream metadata at all, and a release path keyed on that field silently skipped its release-notes sync. The legacy *.appdata.xml spelling is now accepted too, probed after every *.metainfo.xml candidate so the current name always wins. Flat candidates are still tried first, so no project whose file already resolved resolves differently.

  • changelog_log op:"add_subsection" no longer refuses a dated changelog because of a legacy category tail at the bottom. (ANTS-4489) The guard classified ## [Unreleased] by first match, so any Keep-a-Changelog category heading anywhere in the section made the whole section "flat" — refusing a section of ~500 dated topics on the strength of six legacy headings 10,000 lines below them. It now classifies by position: a dated topic above the first flat heading means a dated section with a legacy tail, and the insert (which lands at the top) is accepted. The refusal that remains reports what it found — MIXED, with both populations and their line ranges — instead of asserting the section is flat, and no longer advises the op that created the mixture.

  • file_outline says how many symbols a max_symbols cut left out (ANTS-4469) Truncation reported only that it had happened, so there was no way to tell one missing symbol from four hundred. It now emits symbols_dropped, the same field the byte cap already used.

  • spec_query declares its mode argument, and rejects a misspelled one (ANTS-4468) mode was accepted but never declared, so every call passing it was reported as having an ignored argument. A misspelled mode also fell through to the list behaviour and answered a drift question with a spec list; it now refuses and names the valid values.

  • roadmap_log dry runs no longer report the write as done (ANTS-4463) A preview returned files_written and note_appended:true for a write that did not happen, so a caller checking either field read a dry run as a completed one. Those names are now absent on a dry run and the values arrive as would_write / note_would_append, matching what changelog_log already documents.

  • SARIF export labels suppressions with status, the v2.1.0 field name (ANTS-4454) The suppression object wrote state, an earlier-draft spelling that no v2.1.0 validator accepts — strict consumers rejected the object and lenient ones read no status at all. The feature spec and its conformance test carried the same error and are corrected with it.

  • doc_symbols now reports its qualified unresolved-span count (ANTS-4443) The classifier tested the scope-stripped needle for ::, which it can never contain, so every qualified non-call span was filed as bare and the counter was structurally always zero. It tests the span.

  • Lua: registering an event handler from inside a handler no longer corrupts memory (ANTS-4441) LuaEngine::fireEvent iterated the live handler vector inside m_handlers while lua_pcall ran a handler, so a plugin calling ants.on() from a handler could reallocate that vector (same event) or rehash the QHash (new event) out from under the loop. It now iterates a copy taken before the dispatch begins — which also fixes the contract: a handler registered during a dispatch runs from the next dispatch, never the current one.

  • A duplicate id_fold refused a whole project as a bare SQL constraint, naming neither bullet. (ANTS-4428) roadmap_migrate now refuses a duplicate id before the insert, naming both path:line sites and the folded identity, instead of aborting the whole project with a bare UNIQUE constraint failed: item.project_id, item.id_fold.

  • a section search that matches nothing now says the search emptied it (ANTS-4423) It previously reported that no item carried an ID and suggested two settings that cannot help. The same fix shipped for whole-file searches in July and had never been applied to single-section ones.

  • roadmap_query no longer returns a body fragment as a bullet's headline (ANTS-4417) On the **ID** headline bullet shape (id bolded, headline plain), the headline search ran past the bullet's first line and adopted whatever the body bolded next — so a listing could read "already received" or "fixed 2026-08-02" for open items. Also stopped a latent migration fault: the same offset is the boundary the roadmap import strips at, so importing such a project would have dropped real text.

  • mutation_probe's green-baseline gate no longer passes when it cannot tell (ANTS-4401) require_green_baseline tested for red and read everything else as green, so a baseline whose output could not be parsed, or that ran no tests at all, satisfied it. Both now refuse with baseline_unreadable, naming which of the two it hit. Layman: A safety check meaning "stop if the tests are already broken" quietly did nothing when it couldn't tell whether they were.

  • spec_lint's skip hint no longer states a false cause on an empty walk (ANTS-4080) A run that matched no document reported "no required-sections block was found", which is a claim about the format standard that the run never tested. It now says no document was checked.

  • Roadmap migration no longer loses a quarter of the roadmap to a line of prose that quotes fence syntax (ANTS-4403) The migration walk decided code-fence extents with its own startsWith("```") test instead of the shared MarkdownScan rule, so a multi-backtick inline span — the standard way to write *about* fences — opened a fence nothing closed and masked the rest of the file. Bullets below it were never recorded and headings never opened a section, with no note: the plan was well-formed and simply short. On this project's own ROADMAP.md one such line at 31,081 dropped the plan from 2,040 items to 1,559 and from 218 sections to 135 — 481 items, 24% of the roadmap. It was also the whole of the 446 "orphaned" store rows that had been read as a policy question about rows the file no longer explains; the dry run now reports 0. The walk takes MarkdownScan::fenceMask(), so the fence rule is stated once.

  • roadmap_log no longer deletes bullets the store has never imported (ANTS-4141) The store-backed write path renders the whole roadmap from the store on every op, which assumes the store holds everything the file does. Where it does not, a bullet filed by hand between two verb calls was simply absent from the render and the publish removed it with no trace. RoadmapWrite::commitAndRender() now compares the dry render's id set against the ids the files it would rewrite hold today and refuses render_would_drop, naming them, instead of publishing. It sits at the one seam all eight ops share, so none can bypass it, and a dry_run preview reports the same refusal.

  • Re-importing a roadmap no longer aborts on an id the store and the file both claim (ANTS-4142) A migration re-run refused the whole project with UNIQUE constraint failed: item.project_id, item.id_fold when a bullet's hand-filed id had already been synthesised by an earlier run for a different, id-less bullet. Two causes: matching walked the plan once in document order, so the weaker id-less key could consume a row the id itself named (now two passes, id-bearing first); and allocation took the store's high-water instead of the source file's highest id rather than the higher of the two, so it could re-issue an id the file was already using.

  • feedback_log op:compact_resolved no longer lists the same id twice (ANTS-3739) An id named twice on one **Proposed ID:** line now reports once, matching op:assign_id.

  • feedback_query again reports mapped_ids for a fully-condensed feedback file (ANTS-3744) A file tidied down to its ## Tracked in ROADMAP … pointer line returned no ids, so the reporting project could not see whether its own findings had shipped. Inline ids still win where a file has them.

  • Audit: the Contract-Doc ↔ Code Drift lane no longer reports path:line citations (ANTS-3849) A remotecontrol.cpp:2540-shaped token can never resolve — the path index holds no line numbers — so half the category was noise by construction. 1,146 of 2,315 findings on this repo. Citation staleness is doc_citations' job.

v0.7.106-rc1Pre-release

0.7.106 RC1 — Patron preview

This release shipped without written notes.

v0.7.105

0.7.105

Added

  • The four shared standards now carry their full text in-repo, gated against drift (ANTS-4133) docs/standards/{coding,documentation,testing,commits}.md keep their project-specific delta and now mirror their global owner verbatim below it; security.md joins them under the same markers. This repo is public, and a pointer to ~/.claude/standards/… left the rule simply absent for anyone reading on GitHub. tools/check-standard-mirrors.sh (--check / --write) compares each mirrored region against its owner and tools/hooks/pre-commit refuses a commit that has drifted; a checkout without the global tree skips and passes.

  • spec_lint resolves the test surface a spec cites, instead of reading it (ANTS-4127) A spec's Test: clause claims an invariant is locked by something real, and nothing checked that the thing it names exists — three specs in this corpus name test directories that never have, two of them claiming to have shipped. spec_lint now resolves a cited tests/features/<name> against disk and against CMakeLists.txt, adding test_surface_absent, test_surface_unresolved and test_surface_unwired, plus surfaces_resolved / surfaces_checked on the envelope. The bucket is chosen by the spec's own Status:, so only a shipped one yields a finding and a draft's forward reference does not; a wildcard (tests/features/audit_*) names a family and is not resolved. The engine still opens no file — both filesystem facts are injected by the verb layer, gathered once per run — and an empty set means skip, never fail.

  • Qt 6.2 floor guard now runs before every push (ANTS-4131) (ANTS-4131) New tools/qt62-guard.sh compiles the whole project against the oldest supported Qt inside a container, and the pre-push hook now runs it whenever a push touches compilable source. Both the toolchain layer and the build tree are cached, so a check that used to take about 25 minutes now takes 5-7 seconds — and rejects a too-new Qt call in about one. This is the failure that broke CI three times on ANTS-4108 and could not be caught on this machine at all.

  • spec_conformance — an MCP verb that RUNS the regex patterns a spec prescribes against the | input | expected | table beside them (ANTS-4108) A cold read passes a wrong pattern; running it does not. A row whose actual result differs from expected comes back as a finding, a ```regex fence with no table beside it as a candidate, and a per-case timing as an observation — which is what catches a fixture that returns before the pattern under test ever runs. Executes patterns only, never fenced code: a pattern applied to a string cannot reach the filesystem or the network whatever it contains. pcre2 only; any other engine tag refuses per fence rather than substituting one. max_cases outside [1,1000] refuses the call rather than clamping, so a partial run can never read as a complete one. Nothing in this repository uses the convention yet, so it reports nothing here today — ANTS-4128 (the re.search call form) and ANTS-4127 (executing fixtures) are what make it earn its keep.

  • indie_review_partition reports each lane's real size and flags lanes too big to review (ANTS-4100) A module map naming one directory produced a single lane covering a whole 21,000-line application, and nothing in the response said so. Each lane now carries a measured file count, and one over 30 files is marked too_coarse with a hint to split by cohesion. The threshold is set above this project's own largest lane so a good partition never trips it.

  • doc_integrity takes a single path, so a post-fix sweep cannot be scoped to the files a run actually edited. (ANTS-4106) After fixing five files you can only re-check one folder or the whole tree, so unrelated old problems get blamed on your change.

  • Archive a released version from the roadmap store, and rename a section (ANTS-4070) roadmap_log op:"rotate_minor" moves a closed minor's headings and everything under them into docs/roadmap/<M>.<N>.md, and op:"retitle_section" renames a heading. Both re-derive the section's internal address the way a re-read of the file would, so the roadmap still matches itself afterwards. Rotation refuses a minor that still has unfinished work in it, and previews with dry_run.

  • The roadmap store records which roadmap dialect each project was migrated from, and reports it when the file later disagrees (ANTS-3815) The database now remembers whether a project's roadmap was written in Ants' own format, a GitHub task list, or pass headings, instead of working it out from the file every time. If the file is later rewritten in a different dialect behind the database's back, Ants says so plainly and points you at re-running the migration, rather than quietly guessing which of the two to believe. Projects migrated before this release carry no recorded dialect and behave exactly as they did.

    This is also the store's first schema-version bump (1 → 2), so an existing store is upgraded in place on first open, keeping all its data. An older Ants will refuse to open a store this version has upgraded — relaunch the newer one.

  • roadmap_migrate — the MCP verb that loads a project into the roadmap store (ANTS-3855) The migration engine has been complete and unreachable since ANTS-3756: schema, read half, load half, archives, render, export, section provenance, consumer cutover and write half all shipped, and every invoker was a test. This is the production entry point. One project per call, resolved from caller_cwd; optional project_name / export_slug default from the leaf directory (slug derived, a supplied one validated verbatim and never rewritten). dry_run:true reports the real run's counts and rolls back — it does open the store, and creates an empty schema if none exists, because those counts are a diff against existing rows. Re-running an unchanged project is idempotent; re-running with a different name or slug is refused rather than silently applied. Refusals: no_project, no_roadmap, case_ambiguous, not_utf8, format_mismatch, bad_args, slug_collision, store_failed, migrate_failed (the last three codes are new to the taxonomy). The handler is deliberately thin — it resolves the root, stamps the clock once and names RoadmapStore::defaultPath() — with all store work in a free RoadmapMigrateVerb::run(storePath, req) seam in its own translation unit, so the feature test drives it against a temp store instead of the developer's real one. 11 feature tests, INV-1..INV-10.

  • Roadmap write half — roadmap_log's eight ops mutate the store, then re-render. (ANTS-3809) On a project migrated to the roadmap store, all eight roadmap_log ops now mutate the store and re-render, instead of splicing markdown. Unmigrated projects, and migrated ones whose roadmap is a GFM or pass-headings dialect, are unaffected. Adds the render_gate_unmet, render_failed, store_failed, locator_unsupported and body_shadowed refusals.

Changed

  • Dropped 52 stale file references from the remote-control code, cutting build work (ANTS-4125) When this area was split into a dozen smaller files, the code that used each helper moved out but the line naming that helper stayed behind — so the main file still pulled in 48 helpers it never touched. Every candidate was removed and then re-compiled to prove it was genuinely unused; the five that turned out to be needed were put back. No behaviour change. (Also closes ANTS-4123, the single-file version of the same thing.)

  • test_audit_partition's dimensions:"auto" no longer includes the five test-STYLE dimensions. (ANTS-4111) They are still accepted explicitly via csv:, and now appear in dimensions_skipped[] with a reason. dimensions_active[] is what /test-audit seeds subagents from, so returning all 18 briefed reviewers on exactly what the skill had removed.

  • Split the remote-control implementation into eleven source files (ANTS-3833) The MCP verb implementation was one 24,752-line file — slow to rebuild, and awkward for anything that reads it. It is now eleven files, one per verb family, with no change to what any verb does. A new conformance test keeps the split honest: it checks the files stay in cut order, none grows back past 6,000 lines, and nothing goes back to reading just one of them.

Fixed

  • The audit window now recognises Qt projects that keep their code in the top folder (ANTS-4124) Two separate pieces of code decided whether a project was a Qt one, and they had already drifted apart once — that drift was the bug fixed in the previous release. They are now one piece. The window inherits the better version as a result: a Qt project whose source sits in the top folder rather than a src/ subfolder is now detected, so it gets the correct analysis settings instead of being treated as a plain C++ project.

  • The push safety check no longer starts a sanitizer build it cannot finish in time (ANTS-4118) Pushing from an automated session could be cut off part-way through the second half of the check, failing the push and leaving the sanitizer build tree half-done. The check now measures how much work is pending first and, if that is more than a couple of minutes' worth, says so and skips that half rather than starting it (the cloud build still covers it). If it does get interrupted, it marks the tree and the next run tells you the one command that repairs it. It also now tells you up front how to skip that half, instead of only mentioning it once you already had.

  • The file map now shows top-level data blocks in JavaScript and similar files (ANTS-4090) A quick map of a file listed its functions but skipped plain top-level values — which, in a file that keeps big blocks of text or styling in a named value, are the largest parts of it. One reported file showed a 250-line hole. Those entries now appear, labelled as data rather than code, with only their name shown so a 95-line block never lands in the reply. You can also pull one up by name now.

  • Opening one roadmap item by ID now returns its whole body, not the ends with the middle cut out (ANTS-4091) A long item was trimmed to its first and last part with the middle replaced by a marker — and the middle is where a resume plan lives. Asking for a specific item by ID now returns it in full (up to 16 KB), because naming the item has already kept the reply small; asking for many at once still trims, so a wide query cannot balloon. The trim marker also now says how to read the omitted part.

  • roadmap_query mode:"bundles" no longer labels a group with punctuation and filler (ANTS-4088) The "what should I work on next" view opened with labels like - be bookmarked or 57 blocks duplication: — leftover quotes, stray hyphens, bare numbers and filenames scraped out of the item titles. Group labels are now cleaned the same way the grouping itself already cleaned them, so they read as words.

  • The bug scanner no longer goes blind on non-Qt C++ projects (ANTS-4094) Ants told the scanner to expect Qt code, always — because Ants itself is a Qt app. On a project that isn't, that setting makes the scanner treat the word emit as special. Any file using emit as an ordinary variable name then failed to read at all, so the scanner found nothing in it and the report still looked complete. One real project lost every finding in its largest file this way: 10,000 lines, zero results. It now reports 59.

    Ants now checks whether a project actually uses Qt before turning that setting on, so Qt projects keep the Qt-aware scan and everyone else stops losing whole files.

  • The audit's changelog-coverage check now recognises tests linked by ticket number (ANTS-4099) On a project that ties each release note to its tests by ticket number, every entry was reported as untested even though all of them had tests. The check was comparing the wording of the release note against the title of each test contract — but a release note is written for users and a test title for developers, so the better the note read, the worse it matched. When an entry ends with its ticket number, that number is now looked for in the tests directly. Entries without one are matched by wording as before, and an entry whose ticket appears nowhere is still reported.

  • The audit's spec-drift check no longer flags a test contract for naming its own test file (ANTS-4098) A per-feature contract that says "covered by test_min_size.py" was reported as referring to something that does not exist, even with that file sitting in the same folder. The check compared the name against the contents of every file in the project, and a file's name is not written inside itself. It only ever worked here by luck, because this project's build file happens to list every test by name. The check now also looks at the list of files that exist, so naming a real file is enough. A name that matches nothing at all is still reported.

  • spec_log set_status now reports the value it replaced, and its schema no longer teaches one project's Status vocabulary to every project (ANTS-4114) The verb writes the caller's status string verbatim and validates no vocabulary — but its schema offered "accepted (2026-06-03)" as the example, which is Ants' dated lifecycle. A project whose standard defines its own permitted values got a spec its own lint gate then failed, minutes after the verb reported success. set_status envelopes (and dry_run previews) now carry previous_status, the replaced value joined across its continuation lines, so a vocabulary mismatch is visible at the call; the schema states that no vocabulary is imposed and points at that field. No value is refused — the permitted sets live as prose in each project's own standard, in shapes that share no grammar, so an extractor would sometimes refuse a correct write.

  • Review lanes now cover hand-written shader code instead of the compiled arrays beside it (ANTS-4096) A shaders folder was partitioned into 22 generated byte-array headers and none of the 19 shader sources next to them: the generated-file filter only recognised name prefixes, and shader files were missing from the project's indexable-file list entirely (find_definition and file_outline had both supported them for months). The same fallback also suggested 38 nonsense lane merges built from its own boilerplate text; those are gone.

  • indie_review_corroborate now resolves citations that name a file without its directory (ANTS-4095) Reviewers cite the filename a brief showed them, so a report saying d_main.c:1049 for a file at linuxdoom-1.10/d_main.c resolved to nothing and the whole cross-check silently returned "no agreement". A unique basename now resolves; an ambiguous one still does not, since guessing between two same-named files would invent agreement. The response also reports how many citations it saw versus resolved, so a parse failure can no longer pass for a clean result.

  • audit_falsepos_log's ledger suppresses nothing in audit_run — the writing verb and the reading sweep use different files. (ANTS-4105) You mark a false alarm as dismissed and the next scan reports it again, because the note was filed where nothing reads it.

  • changelog_log's interleaved-prose advisory fires on trailing HTML comments and on the continuation bodies its own body parameter writes. (ANTS-4103) The changelog tool warns that your file is malformed — about text the same tool wrote, in the shape it was designed to write.

  • project_settings op:detect under-reports itself — it suggests more keys than documented, and says "no override needed" while five keys sit undeclared. (ANTS-4093) The setup tool tells you there's nothing to configure when five settings are still unset.

  • project_settings op:detect suggests gitignored directories as source_roots, including a 2 GB state directory. (ANTS-4092) The tool that is supposed to stop the guessing suggested pointing the code index at 2 GB of session transcripts.

  • roadmap_log op:amend_body is single-line by design, so two successful calls can leave a wrapped paragraph incoherent with nothing in the envelope to show it. (ANTS-4097) Fixing two lines of the same paragraph one at a time can produce a sentence that contradicts itself, and both edits report success.

  • roadmap_log op:append renders a non-conforming bullet on a pass-headings roadmap — Status: todo, unindented body, no separator. (ANTS-4117) On one project the roadmap tool writes entries in the wrong shape, so every append needs fixing by hand afterwards.

  • roadmap_log's id locator matches zero bullets on a roadmap whose ids are bold (**LOTTO-0019**), while roadmap_query resolves the same id fine. (ANTS-4109) Marking items done silently does nothing on some roadmaps, and still reports success.

  • Test chunks are bounded by bytes as well as file count. (ANTS-4113) A suite whose tests live in a few very large files returned one chunk no reviewer could read against the full dimension list. The applied budget is echoed as chunk_byte_budget.

  • The cpp_exit and system_shell_out pre-pass patterns only fire on C/C++ files. (ANTS-4112) They were matching English prose inside a quoted string in a shell script, and Python's canonical sys.exit(main()) — half the pre-pass signal on one project.

  • doc_citations says which counts keys are overlays, so the status buckets can be summed. (ANTS-4087) counts mixes statuses with overlays on the ok subset, so it never summed to count and a clean pass read as half-verified. counts_overlay_keys names them.

  • feedback_query reports "nothing filed here yet" as a result, not an error. (ANTS-4104) A derived path with no file returns ok:true / found:false with the candidates and hint intact; an explicitly passed path that does not resolve still refuses.

  • spec_lint no longer reports an invariant id gap for a number a sibling spec owns. (ANTS-4110) On a project that numbers invariants once across the corpus this fired on every neighbouring id. The scheme is detected from ownership, and suppressions are counted in id_gaps_suppressed rather than being silent.

  • Sub-lettered invariant ids (INV-3b) parse as invariants of their own. (ANTS-4107) Their text used to be absorbed into the preceding invariant's body, and invariant_no_test could not see them at all — so an invariant split by a review could ship with no test surface and pass the lint.

  • spec_query / spec_lint find an Invariants section whose heading does not begin with the word. (ANTS-4115) ## 5. Correctness invariants matched nothing, so every invariant under it was invisible — a whole project's specs returned an empty contract. Also picks up four Ants specs (## 4. Contract — invariants, ### 2.5 Invariants).

  • doc_integrity no longer reports template placeholder links as broken (ANTS-4102) A skeleton file's deliberately-unfilled <ID>-<topic>.md links are placeholders, not paths, so the check reported the same non-problem on every run of any project that keeps a template.

  • roadmap_log reports the roadmap file it actually wrote, in its real name (ANTS-4116) On a project whose roadmap is lowercase roadmap.md, the reply named "ROADMAP.md" — a file not in the repo — so there was no way to confirm where the change landed.

  • file_outline reports a C++ namespace as a namespace, not as a class (ANTS-4101) Asking the file map for a file's classes no longer hands back the namespace they live in.

  • apply_edits accepts the native Edit tool's old_string/new_string spelling (ANTS-4089) Every session reaches this verb holding the key names the built-in Edit tool uses, so the first call was refused. Both spellings now work; old/new still win if a call sends both.

  • Roadmap entries writing their work-type or owners in bold are read correctly (ANTS-4077) An entry writing **Kind:** fix. or **Lanes:** core. in bold — a style the summary line has always accepted — was read as declaring nothing, so its work-type or owners were lost. Twenty-eight entries were affected. An entry that merely quotes one of those labels while writing about the roadmap format is still correctly ignored.

  • Roadmap items marked partial import as in-progress rather than not-started (ANTS-4071) A pass-headings project marking a block partial means the work has begun. The importer had no entry for the word and filed it as planned, which says the opposite about work a reader would act on.

  • A roadmap headline quoting code no longer loses everything after the quote (ANTS-4066) A headline that quotes a bold marker inside backticks — a C signature like runMain(int argc, char **argv), or a bullet discussing the roadmap format itself — was cut short at those two asterisks, and the rest of the title was dropped without a word. Twelve entries across all projects were affected. The reader now recognises backticked code as literal text.

  • Roadmap import no longer loses a declared Kind:, invents a Source:, or defaults a field in silence (ANTS-4086, ANTS-4063) The markdown→store import read Kind: only at the start of a line, so the 99 bullets in this project that write the trailer inline were read as declaring nothing and quietly filed as implement. It also wrote Source: planned. back into the file for every bullet that had never declared one, turning "this said nothing" into a statement the next import believed. Both are fixed: the label is matched anywhere in the bullet (guarded against quoted mentions, case-sensitive now, last match wins), a source the import supplied is no longer rendered, four further Kind: spellings map to their canonical value, a path cited by Source:/Evidence: that no longer exists is reported rather than ignored, and every defaulted field raises a note with a per-field tally on the migration report. The re-migration this unblocked is verified over every bullet — 1,434 declare a kind, 0 mismatch the store.

  • The roadmap database can now be brought up to date instead of refusing to open (ANTS-3781) A roadmap database written by an older build had no route forward: it fell through to the create-tables step and died on "table already exists", a message naming neither its version nor the one the running build expects. There is now an upgrade ladder — one step per version, validated end to end before a single statement runs, applied inside one transaction so a failure leaves nothing half-done, and every refusal names both versions. The database's layout number and the backup file's record-format number were also one and the same, so any change to the database would have spoiled every backup ever written; they are now separate numbers that move independently. No file on disk changes.

  • Tests can no longer reach the user's real data directory (ANTS-3856) Every test bundle now runs with XDG_DATA_HOME pointed at a per-process temporary directory, so a test that omits its roadmap-store path opens a throwaway store instead of the live one under ~/.local/share. A fixture project had leaked into the real store that way; it has been removed. No production code changed.

  • The roadmap database now records an auto-numbered item ID as store-generated rather than author-supplied. (ANTS-3838) Every item carries a note saying where each of its values came from. Both places that add items were labelling the ID as "the author chose this" even when the system had just picked the next number — so an item nobody named looked identical to one somebody did. Only an ID you supply yourself is now labelled as yours.

  • doc_integrity truncated a Contents list at its first WRAPPED entry. (ANTS-3836) The documentation checker reported table-of-contents entries as missing when they were in fact listed, whenever an entry ran onto a second line — it stopped reading the contents list at that point and called everything below it absent. It now reads a wrapped entry as one entry. A genuinely missing entry is still reported.

v0.7.105-rc1Pre-release

0.7.105 RC1 — Patron preview

This release shipped without written notes.

v0.7.104

0.7.104

Added

  • Roadmap edits on a migrated project now change the database, and the file is rebuilt from it (ANTS-3809) All eight roadmap-editing commands — add an item, add several, mark one done, mark several, add a note, patch a note's wording, add a section, add a table row — now write to the roadmap database and regenerate ROADMAP.md from it. Before this they still edited the file's text by hand, so on a project that had been migrated the read came from the database, the write went to the file, and the next read saw a database that never heard about the edit.

    Nothing is written unless the regenerated file is valid: the database change and the rebuild either both happen or neither does. A preview (dry_run) still commits nothing. New IDs are allocated from the database rather than the .roadmap-counter file, floored to the highest ID anywhere in the committed project so a fresh clone can never hand out an ID that is already in use.

    Two things a caller will notice. Locating a bullet by line range is now refused on a migrated project, per locator, so the rest of a batch still applies — a database has no line numbers, and the old comparison would have matched every bullet in the file. And a request that sets a field the note text would contradict is refused rather than silently stored, because the file would render one value and re-read as the other.

  • Roadmap read seam — consumers can take their bullets from the store instead of ROADMAP.md (ANTS-3793) The roadmap database has been the primary copy for a while, but nothing read it back — every part of the app still re-read and re-parsed the ROADMAP.md text file, so the database quietly drifted out of date the moment anyone edited that file. This adds the reader, and it produces exactly the same entries the text file would have, so nothing downstream can tell which one it was handed. A project that has not been imported into the database keeps using its text file, unchanged. A database that is present but broken now says so instead of silently pretending the project was never imported. Reading a project's whole roadmap out of the database takes about 10 ms; re-reading the text file takes 33 ms. Not yet switched on for the app's own screens — that is the next step.

  • file_outline and read_region understand GLSL shaders (ANTS-3800) Shader sources (.comp, .frag, .vert, .glsl and the rest of the stage extensions) are outlined and can be sliced by function name, using the same extension set find_definition and find_caller already accepted. Previously a shader reported an unknown language with no symbols, and slicing one by symbol failed outright.

  • Roadmap render — ROADMAP.md can now be generated from the roadmap store at full fidelity (ANTS-3758) The inverse of the migration: every item is written in full roadmap-format.md § 3.5 bullet form, so the generated file is the file that exists today, written by the store instead of by hand. Section order comes from the stored document order, and each section is routed back to the file it was read from, so a rotated archive cannot fold back into ROADMAP.md. Publishing is gated on every open item having a plain-English Layman: line, per project and all-or-nothing.

  • find_caller takes an optional lane to scope the scan to one subdirectory. (ANTS-3805) Without it a short generic method name such as update returns hundreds of unnarrowable callers from every class that has one. The walk itself is scoped, so callers_count and truncated describe the scoped set, and the applied lane is echoed back.

  • Roadmap migration now reads rotated archives, and records which file each section came from (ANTS-3766, ANTS-3782) The migration used to see only a project's live ROADMAP.md. Closed minor versions that /bump had rotated out into docs/roadmap/0.6.md were invisible to it — so once the roadmap file becomes machine-generated, the first regeneration would have quietly deleted them. Discovery now returns the live file plus every conforming archive beside it, detects each one's format separately, and namespaces each archive's sections by the file they came from so nothing in the live file shifts. The store gained one column recording the source file per section, which is what lets the archives later be written back where they belong instead of being folded into the main file.

  • Roadmap migration, load half — a project's roadmap can now be written into the store, atomically and repeatably (ANTS-3765) The reader built in ANTS-3757 could turn a ROADMAP.md into a plan and had nowhere to put it. This writes the plan into the roadmap database: one project at a time, all-or-nothing, and safe to run again after the file changes — a second run updates what changed, keeps anything you edited in the app that the file cannot express, and never duplicates or renumbers what is already there. An item you delete from the file is kept, re-filed and reported rather than thrown away, because it is far more often a rename or an archive move than a deletion. Nothing is user-visible yet; the read side that surfaces it is still to come.

  • Roadmap migration, read half — a hand-written roadmap becomes a loadable plan (ANTS-3757) New RoadmapMigrate::findRoadmap() + planFrom() in ants_core_lib (Qt6::Core only). Discovery resolves a project's roadmap case-insensitively and refuses rather than guesses on a missing, case-ambiguous or non-UTF-8 file; the pure planFrom() turns the markdown into sections, items, tables, narration, the status legend and a report, over all three roadmap formats. Nothing is written yet — the load half is ANTS-3765. Every non-blank source line lands in exactly one carrier, so nothing is silently dropped.

  • Two new debt_sweep_scan detectors — dead lint suppressions and disagreeing dependency pins (ANTS-3743) dead_suppression flags a # noqa: CODE whose code no ruff selector enables — a directive that reads as a reviewed decision and suppresses nothing. It stands down entirely unless the project states an explicit select list, so it never guesses. dep_pin_mismatch diffs every pkg==version in build scripts, CI and container files against the pin in requirements*.txt (or against the first hardcoded copy, for a package with no manifest entry) and reports only where two pins disagree — one of them is then definitely wrong.

  • cold_eyes_brief returns a per-doc section_index (ANTS-3740) Each lane doc now arrives with a map of its own headings — {heading, slug, level, start_line, end_line} — so a reviewer cites a section anchor instead of a line number (an anchor survives an edit above it) and fetches any section with read_region section=<slug>. The slug is that verb's own key, so every listed section is fetchable. Lane docs only; capped per doc, with truncated:true when a doc has more headings than the cap.

  • audit_run takes exclude_paths — skip code that is not yours (ANTS-3710) Point it at a bundled dependency folder and the audit stops reporting that folder's problems, without narrowing the sweep away from the rest of your project. The result tells you which exclusions were applied, and names any tool that could not honour them, so a partly-filtered result never reads as a fully-filtered one. Requested by the DOOM Ants session, where bundled Windows build dependencies supplied over half of every sweep's most serious findings.

  • cold_eyes_brief returns an input_hash (ANTS-3718) A fingerprint of everything a review lane reads — the brief, its docs, the small cross-reference contracts and every cited code file. A review loop can skip a lane whose fingerprint has not changed since it last came back clean, without having to read the lane to work that out. Requested by the claude-config session.

  • Mageia 10 packages, and one less thing that can break a build (ANTS-3731) Ants Terminal now builds for Mageia 10 on the openSUSE Build Service. The version number stamped into the package is now worked out when the recipe is submitted rather than by a helper running inside the build machine — that helper was unavailable on Mageia and blocked it entirely. Removing it takes a dependency away from every distribution we build for, not just Mageia.

  • openSUSE Leap 16.0 packages, and a distro-portable RPM spec (ANTS-3727) The OBS repository now publishes for openSUSE Leap 16.0 alongside Tumbleweed, so Leap users can install and update through zypper like everyone else. The spec grew %if arms for every package name that differs between distributions — Ninja, Lua, the AppStream validator and the man-page suffix — which is what made a second distribution possible at all. Fedora 44 is wired up and resolves; it turns green from the next release, which carries the Lua header fix below.

  • Install Ants Terminal on openSUSE with zypper (ANTS-3726) Ants Terminal is now built and hosted on openSUSE's own build servers, so on Tumbleweed you can add one repository and install it like any other program — and get updates automatically instead of downloading a file each time:

    sudo zypper ar https://download.opensuse.org/repositories/home:/milnet:/ants-terminal/openSUSE_Tumbleweed/ ants-terminal sudo zypper in ants-terminal

    Getting there turned up six problems in the packaging recipe that had gone unnoticed because nobody had ever built it on a clean machine — including one that would have left out the shell-integration scripts entirely. All six are fixed.

Changed

  • Roadmap consumers read the store — roadmap_query, changelog_log, feedback_query/feedback_log, roadmap_branch_drift and the Roadmap dialog (ANTS-3793) Until now nothing read the roadmap database: every tool parsed ROADMAP.md, so the database was a write-only copy. Each of those tools now asks the database first, and falls back to reading the file for any project that has not been migrated — which today is all of them, so nothing changes yet for anyone. A project served by the database also gets its own status legend in the Roadmap dialog; every other project keeps the built-in one, unchanged. When the database exists but cannot be read, the tools say so instead of quietly reading the file behind its back.

  • Roadmap reader records what a headless migration needs (ANTS-3764) RoadmapParse::BulletRecord gains five fields: the verbatim - **Status**: value, the Source: value, the leading-slot id token as written, the pass designator, and a 1-based inclusive line span. Additive — no existing caller reads a field that did not exist, so the roadmap dialog, roadmap_query and roadmap_log are unchanged.

  • debt_sweep_scan's scope_note derives its per-category detector counts from the live roster (ANTS-3743)

  • debt_sweep_scan now shows how thinly each category is checked (ANTS-3707) A zero next to a category used to read as "this area is clean". It really meant "the checks we have did not fire" — and some categories rest on a single check while others rest on seven. The result now lists the checks behind every count, so a zero can be weighed properly. Reported independently by the DOOM Ants and Fin Break sessions, both of which read a zero as an all-clear and then found real problems by hand.

Fixed

  • The render has no table renderer — a migrated table re-emits as raw JSON. (ANTS-3832) A roadmap table stored in the database now comes back out as a table. It was being written into ROADMAP.md as raw JSON, because the file generator replayed every non-item block verbatim while the migration stores a table as structured data. Cells keep a literal | in the store and gain their \| spelling only in the file, so the round-trip survives it.

  • Migrated roadmap bullets no longer render their headline and every trailer key twice (ANTS-3808) The migration stored the whole bullet — headline line included — as the item's body, while the render emitted its own headline, then that body, then the trailer again from the database columns. The migration now stores only the leftover text, and the render emits a Kind: / Source: / Lanes: / Layman: / Evidence: line only when the body does not already say the same thing. Values are compared rather than mere presence, so a stale line in the body can never silently replace the canonical one.

  • Pasting a copied image file now inserts its path, not a file:// URI (ANTS-3828) Ctrl+Shift+V branched on hasImage() only, so a screenshot on the clipboard (a raster) worked while a copied image file — which carries text/uri-list and no raster — fell through to the plain-text paste and wrote file:///home/…/shot.png verbatim. Claude Code can't resolve a file:// URI as a path, so the attachment never formed. A hasUrls() branch now inserts the bare local path; nothing is re-saved, since the file already exists. Paths outside the shell-safe character set are passed through shellQuote(), which also fixes filenames containing a space. Non-image files keep the existing text-paste behaviour.

  • Roadmap migration: an archive's own preamble does survive (ANTS-3806) A reported migration limitation — that every source file's content above its first heading collapsed into one row per project, so an old archive's own header was lost — turned out not to exist. Verified by running a two-source project end to end rather than by reading the code: each file keeps its own preamble and gets it back when the roadmap is written out again. Added a regression test that will notice if that ever stops being true, and corrected the spec and comments that had recorded the wrong explanation.

  • feedback_log op:compact_resolved now finds shipped items in other projects' roadmaps (ANTS-3802) Cross-session feedback files are written from one project about another's tooling, so their item ids belong to that other project. The compaction step looked them up only in the calling project's own roadmap and therefore almost never found them, while the query step on the same file found them all. Both now share one lookup, a missing local roadmap is no longer fatal, and the refusal says which roadmaps were searched.

  • changelog_log op:add_batch now reports created_category, per entry and as a rollup. (ANTS-3804) The batch path emitted the field nowhere while the single-op path always did, so callers read the absent boolean as false and could conclude no heading had been created when one had.

  • changelog_log op:add now refuses a feature-grouped [Unreleased] written in the format op:add_subsection itself produces. (ANTS-3803) The detector required a flush-left bold run and missed list-item ones, so the entry was appended at the end of the section instead of being refused.

  • doc_citations no longer reports a fabricated out_of_range against a file the document never named. (ANTS-3801) A citation that failed to resolve left the inherited-path antecedent pointing at the last one that did, so a following bare :NNNN was range-checked against an unrelated file. Such a continuation now reports unresolved instead.

  • spec_lint no longer reports a false invariant_no_test for every bullet-form invariant in a spec that also contains an | INV-N | table row. (ANTS-3799) The parser skipped bullet invariants entirely once any table row matched, so one withdrawn-invariants summary table cost a conforming spec eleven fabricated findings. Both forms are now parsed and merged in document order.

  • docsindex carries a third copy of the Status-field rule, with the same truncation bug. (ANTS-3786) A third piece of code reads a document's status line and, like the other two, stops at the first line — so a status written across two lines is cut short in the docs index.

  • Spec header fields may wrap: spec_log no longer corrupts a multi-line **Status:**, and spec_query no longer truncates one (ANTS-3785, ANTS-3672) A spec's **Status:** value routinely runs onto a second line — 49 of the 172 specs carrying one do, because the format standard tells authors to append review progress inline and the corpus wraps at ~80 columns. Both sides of the toolchain assumed a single line: spec_query returned the value truncated mid-sentence, and spec_log op:set_status replaced only the first line, leaving the rest behind as an orphaned paragraph while reporting success. Neither failure was visible — the file still parsed and the stray text read like a botched hand-edit. Both now share one rule (SpecParse::headerField) for where a header field ends. Also fixed: the writer could rewrite a **Status:** line quoted inside a fenced code example and report success; the field search is now bounded to the header block.

  • Re-importing a roadmap no longer duplicates entries that share a heading (ANTS-3765) When two entries in the same section had identical wording and no ID of their own, each re-import added a fresh copy of both and set the previous pair aside. Repeated often enough that grows without limit. They are now matched in order, so importing the same file twice changes nothing.

  • Seven roadmap entries shared one malformed ID and now have their own (ANTS-3769) Seven separate fixes had been written down with the same broken ID (a stray &), so none of them could be looked up and the roadmap could not be imported. Each now has its own number; no wording changed.

  • A bold label at the start of a checkbox-style roadmap line is no longer mistaken for an ID (ANTS-3773) In roadmaps written with - [x] checkboxes, a bold phrase opening a line was always read as that item's ID. That is right for a project writing **MT1**, and wrong for one writing **Phase 9E-2:** before some prose — and when two lines shared such a label they became one item, which blocked that project from importing at all. A label ending in a colon is now never an ID.

  • Roadmap store: lanes, evidence and extras are now writable (ANTS-3767) The roadmap database had a column for each of an item's lanes, evidence files and extra fields, but nothing could actually put a value in one — every write reported success and left the column empty. All three can now be written, both when an item is created and when one is edited, and a badly-shaped value is refused rather than stored.

  • dependencies.md cites build directives instead of line numbers (ANTS-3648) The authoritative version-location map told you symbol names are the stable anchor and then cited every pin by line — and every one of those line hints had already drifted, pointing a dependency sweep at the wrong places.

  • roadmap_query mode:"bundles" now flags plain-prose gates (ANTS-3389) A to-do whose body says "Wait for X to close" in ordinary prose is now marked blocked, like one using a formal blocked by marker. Reported by the MAME Curator session, whose gated items were reading as ready to start.

  • doc_integrity, doc_symbols and spec_lint returned a false ETag 304 after documents changed (ANTS-3737) These three report findings rather than content, and the ETag is a hash of the response. Editing documents without changing any finding left the response identical, so etag_match answered "unchanged" and the caller skipped the re-check — precisely when it was looking for a new problem the edits had introduced, and precisely in the common zero-findings case. Each verb now emits docs_digest, a fingerprint of the checked set, which makes the ETag track the documents themselves. (Fin Break feedback.)

  • roadmap_query truncated long bullet bodies from the head, dropping the current status (ANTS-3736) A roadmap body is an append-only progress log: the head says what the item is, the tail says where it stands. Head-only truncation served the oldest text as the answer to "what is the state of this?" — one fetch reported work as pending that had shipped three days earlier. A truncated body now keeps both ends, joined by an explicit … [body elided — tail follows] … marker. Fixed at the cache truncation site too, so a body past the 16 KiB store cap — previously unreachable at any max_body_bytes setting — also surfaces its tail. (DOOM Ants feedback.)

  • file_outline lost every symbol after a declaration whose ; was followed by a comment (ANTS-3735) The scanner decided whether a matched definition opens a body by testing the raw line's last character, so extern "C" int f(char* c); // note did not look like it ended in ;. It was read as a definition awaiting its {, and the next brace it met — an anonymous namespace { — became that phantom function's body, suppressing every function symbol until the namespace closed. On DOOM's r_vulkan.cpp that swallowed 5,742 lines: detected definition sites went 29 → 130 with the fix, 98 of them inside the namespace. workspace_search enclosing_symbol was collateral — with the outline blind, it attributed every match in that span to the last symbol it had seen, the struct VulkanState. (DOOM Ants feedback.)

  • Project search is much faster on the first search after a build (ANTS-3732) The MCP workspace_search tool ran ripgrep with a single worker thread, which switched off its parallel directory walk and made every file read wait its turn. On a spinning disk that is the slowest possible order. A wide search over 31,500 files took 24.6 seconds; it now takes a fraction of a second, and ordinary searches no longer time out when a build has just filled the disk cache.

  • Lua headers are found on distributions that do not use openSUSE's layout (ANTS-3727) luaengine.cpp included <lua5.4/lua.hpp>, which names a directory only openSUSE creates; Fedora installs the same headers directly in /usr/include. The build failed there even though CMake had located Lua and enabled the plugin system. The include is now unqualified and the directory comes from CMake, which was already supplying it. No effect on openSUSE, where the compile line has carried -I/usr/include/lua5.4 all along.

  • openSUSE package ships a stripped binary, with debug symbols in their own subpackages (ANTS-3729) The OBS project now enables debuginfo, so /usr/bin/ants-terminal is stripped and ants-terminal-debuginfo / ants-terminal-debugsource are published alongside it — a crash report from a user can now be resolved to source, where previously the symbols were discarded during the build. The rpmlint report for the package is clean (0 errors, 0 warnings); the one remaining error was a false positive in rpmlint's own branding check, which misreads hicolor-icon-theme as a themed branding package, and is filtered with the analysis recorded next to the filter.

v0.7.104-rc1Pre-release

0.7.104 RC1 — Patron preview

This release shipped without written notes.

v0.7.103

0.7.103 — hotfix

Fixed

  • Ants Terminal builds on Fedora and Mageia again (ANTS-3727) The Lua header was included by openSUSE's path layout, so on Fedora and Mageia — which put the same header somewhere else — the build stopped before it started. It now asks the build system where the header is instead of assuming. openSUSE was unaffected; this is what kept the Fedora 44 and Mageia 10 packages from being published.

v0.7.102

0.7.102

Added

  • apply_edits can now edit by line range, not just by matching text (ANTS-3711) Deleting a big block used to mean pasting the whole block back in just to say which one you meant. Now an edit can name a line range instead — "lines 200 to 276" — which is far cheaper for a large chunk. You also hand it the exact text of the first and last line as a safety check: if the file has changed underneath you and those lines no longer match, the edit is refused instead of quietly rewriting whatever moved into those positions.

  • doc_integrity catches numbered sections that run out of order (ANTS-3700) Nothing used to notice a document whose sections go 5.6, 5.8, 5.7 — the eye reads them in the intended order and every link still works. The checker now flags out-of-order, skipped and repeated section numbers. Tuned to stay quiet: 2 findings across this project's 275 documents, and both are real.

  • indie_review_partition works out review lanes from the file tree when a project has no module map (ANTS-3709) Previously a project that describes its layout in prose got back an empty list, even though the server already knew every source file, and the caller had to build the review plan by hand. It now groups the files by folder (splitting big folders into numbered chunks) and hands that back marked as worked-out rather than declared, so you can adjust it and save it as your project's own partition file.

  • audit_run says WHY a file could not be parsed (ANTS-3706) New parse_failures_detail[] gives {file, tool, reason} — the check-id and first diagnostic — next to the existing bare-path parse_failures[]. A missing include path (a config problem you can fix) now reads differently from a construct the analyser cannot handle (something to route around), without re-running the tool by hand.

  • project_settings op:detect shows what the project already declares (ANTS-3705) detect now returns declared (the six layout keys as stored) and declared_missing[] naming any declared path that no longer exists, so checking the layout no longer means opening .ants/project.json by hand. A path that has been moved or deleted is silently ignored by the reader — previously indistinguishable from never having been declared.

  • indie_review_corroborate accepts lane reports outside the project (ANTS-3713) Pass allow_outside_project:true and reports_dir may be an absolute path — the session scratchpad, /tmp — so a review sweep no longer has to write scratch files into the working tree just to use the cheap server-side read. Same opt-in name as test_audit_synthesis_prompt's. Path checks are unchanged apart from the root anchor; the default project-relative path still refuses an absolute directory.

  • workspace_search gained exclude_glob — search everywhere except a given tree (ANTS-3704) Until now you could narrow a search to one folder or one file pattern, but there was no way to say "everywhere except the docs". On a project with a lot of written documentation that meant the results were mostly prose, and the only way round it was to drop back to the raw command-line search tool. Pass exclude_glob (one pattern or a list) and those paths are left out. Write the pattern plainly — docs/**, not !docs/**.

  • doc_dedup MCP verb — find the same passage written twice across a doc set (ANTS-3660) Splits each markdown file into paragraphs, compares them as word 3-grams, and reports every pair that overlaps enough to be the same fact written twice. Groups the pairs into clusters, because five documents sharing one stanza are one thing to fix rather than ten findings. Skips fenced code (two identical samples are not duplicated prose), pointer-only lines, and generated files whose whole purpose is to repeat a template. Report-only — it never decides which copy to keep, because that is a judgement about where a fact belongs.

  • spec_lint MCP verb — the greppable half of the spec-format contract (ANTS-3662) Checks the structural defects /cold-eyes § 1e otherwise greps for by hand on every review pass: an invariant with no test-surface clause, a gap in a spec's own id sequence, a cold-eyes loop-log row with no recorded outcome, and a test clause that is a command stating nothing it should return (a candidate — never auto-fixable, and no subprocess is ever run). Tombstoned invariants are exempt. missing_section runs only when the project's format standard carries a machine-readable block, so it ships skipped with sections_checked:false rather than firing against a guessed list. Size is reported in line_count, never as a finding.

  • doc_symbols MCP verb — resolve the identifiers a doc claims exist (ANTS-3661) Checks that every function or class name a document mentions actually exists in the code. Harvests inline code spans shaped like Foo::bar() and looks each up with find_definition's resolver, skipping fenced samples, paths, language keywords, MCP verb names and doc-examples regions. It only ever reports: deciding whether a missing name is rot or something the document is about to create stays with the reader, so there is no severity and nothing auto-fixable. A name the run never got to is reported as not_checked — never as one that does not exist.

  • doc-examples regions: mark a passage's file references as illustrations so doc_citations stops reading them as rot (ANTS-3659) Documents that explain citation syntax have to write references that point nowhere. The checker read all 58 of them in one spec as broken, against a single real citation. An author can now wrap such a passage in <!-- doc-examples: begin --> / <!-- doc-examples: end --> and the scan skips it entirely. Two new response fields, unterminated_examples and examples_suppressed, keep a runaway or over-broad region visible rather than silently blanking real citations.

  • doc_citations now checks whether a cited line still mentions the symbol named beside it (ANTS-3654) When a document writes a symbol in backticks just before the place it points at, the verb now looks for that symbol in the lines it cites and reports anchor_found. only:"stale" returns those drifted citations alongside the ones that failed to resolve, so a docs sweep surfaces "this line moved" as well as "this file is gone".

  • New doc_citations tool — check that a document's "see file, line 12" pointers still point where they claim (ANTS-3636) Design docs are full of references like src/window.cpp:120, and they go stale silently as the code moves. Reviewing one used to mean opening every file by hand. This reads the document, finds every reference, and shows you the line each one points at today — plus which ones now point at a missing file, past the end of a file, or at a name that matches several files.

  • Citation scan — the path:line grammar and scrape rule as a standalone contract, split out of ANTS-3636. (ANTS-3653) The part that just finds file.cpp:42 references in a document, specified on its own so it can be built and tested without touching the filesystem.

  • changelog_query accepts section as an alias for version. (ANTS-3618) Callers arriving from roadmap_query reach for its vocabulary and passed section:"Unreleased", which was silently ignored — so every entry came back, reading as a filter that matched everything rather than one never applied. Passing both with different values now refuses bad_args.

  • Review Changes dialog shows each touched file's total line count. (ANTS-3632) The Status list now renders a dimmed (N lines) after each entry, so file size is visible before clicking in. Renames report the new path; deleted, binary, unreadable and over-64 MiB files show no suffix rather than a misleading number. Counted in 64 KiB chunks, so memory stays flat on multi-MiB files.

  • audit_dismiss — let Claude tell the audit tool "that finding isn't a real bug". (ANTS-1713) Until now only the Audit pop-up could mark a warning as a false positive so later audits stop reporting it. Claude can now record the same verdict directly. It is remembered by content rather than by line number, so it survives edits that move the code around.

  • New doc_integrity MCP verb + engine — deterministic dead-anchor / broken-link / TOC-coverage checks for markdown docs (ANTS-3601) Finds the internal-consistency rot in long contract docs that no test caught and only a careful human reader spotted: a [text](#heading) link whose heading was renamed, a [text](../file) whose target moved, or a hand-maintained Table of Contents that drifted from the sections. GitHub-compatible heading slugs, fence-aware (fenced examples ignored). Runs as the doc_integrity verb (scope a file/dir, filter by kind) and feeds the cold-eyes review pre-pass automatically.

  • New contract_doc_drift audit lane — flags back-ticked literals in docs/standards/docs/specs that no longer exist in the code (ANTS-3600) Catches the doc-vs-code drift where a contract doc quotes a file path, symbol, or config key that a rename left behind. Fence-aware (fenced examples are skipped) and path-aware (slash-paths resolve). Human-triage severity, never auto-fixed; a project-local .ants_doc_drift_allow.txt suppresses intentionally-illustrative literals. Runs in the GUI Audit dialog.

  • audit_run: richer partiality + zero-coverage surface, raised time ceilings (ANTS-3585) (ANTS-3585) A big C/C++ sweep no longer misleads a caller: incomplete_tools_detail[] says WHY each tool is incomplete ({tool, status, elapsed_ms, truncated} — truncated ⇒ cut off at the cap, false ⇒ crashed) and parse_failures[] names source files cppcheck could not parse (a C++23 TU its frontend chokes on gets zero coverage instead of being silently absent). The per-tool wall-clock cap ceiling is raised 60→300 s and the aggregate 240→900 s (default stays 30 s; high caps are meant for the async path, which survives the transport timeout). Both surfaces ship on the sync and async-poll envelopes.

  • changelog_log: add an opt-in feature-grouped-subsection insert mode for ### <DATE> <Category> — <headline> CHANGELOGs. (ANTS-3584) Some projects group their changelog by dated feature blocks rather than flat Added/Fixed headings; give changelog_log a native way to insert those instead of forcing a hand-edit.

  • read_regions: accept an optional top-level path used as the default for any item that omits its own path (per-item path still wins). (ANTS-3589) When you want several slices of ONE file, read_regions makes you repeat the same filename on every slice; let it take one filename at the top like its sibling read_region does.

Changed

  • invariant_check answers in a few hundred bytes by default (ANTS-3699) The "does any spec already cover these files?" check used to return every matched spec's full invariant text. One real call over three files came back at 267,070 characters — too big to read, so people learned to skip the step. It now returns just the list of matching specs and how many rules each has; ask for mode:"full" when you actually want the text.

  • Review tools no longer tell you not to use them (ANTS-1581) Every cold-eyes and indie-review tool used to carry a line saying the matching review command doesn't call it, so reach for it only when building your own pipeline. That advice is now backwards — those commands prefer these tools. The line is gone. One warning stays, on the tool that runs the review on your own local AI model instead of Claude: it now says plainly that this is a weaker reviewer, not a cheaper way to get the same review, and the tool picker no longer advertises it as the way to start a review.

  • apply_edits documents the per-call batch ceiling separately from the 4 MiB file cap (ANTS-3712) A several-KB batch can fail in the client's transport before the server sees it, with an error blaming escaping; the 4 MiB figure applies to the file being edited, not the request. The description now says to split the batch rather than audit quoting.

  • roadmap_log reports bytes_written as the byte delta on every op, plus file_bytes (ANTS-3702) Whole-file-rewrite ops reported the size of the file, so six short notes came back as a 459 KB write. The full size is still available.

  • audit_falsepos_log now accepts debt-sweep as a review kind (ANTS-3701) Dismissing a false alarm found during a debt sweep had to be filed under "audit" with a hand-written note, so the record of where it came from was wrong. It can now be filed under its own name.

  • doc_symbols no longer reports bare lowercase words it cannot resolve (ANTS-3692) A backticked word like counts, dry_run or truncated is almost always a JSON field name rather than a symbol anyone declared, and the checker cannot tell those from a genuine stale reference. It now stays quiet about them unless the lookup actually finds the thing, so the list you get back is the one worth reading. Names that do resolve are still reported, which keeps shell and Python functions — they are lowercase by convention. Measured over the project's own docs: 45% fewer entries, and slightly more real symbols found, because the lookup budget is now spent on the promising names first.

  • Source-scrape tests can strip comments before scanning (ANTS-3662) A test asserting that code does not do something fired on a comment that merely named the thing — the same false-positive class as the fixed-byte scrape windows ANTS-3681 replaced. tests/_support/srcgrep.h gains stripComments, which removes comments while leaving string and character literals intact.

  • Hoisted the inline-code-span scanner into MarkdownScan::codeSpans, and taught fenceMask to report an unterminated fence's opener line (ANTS-3649) A shared text-scanning helper moved into the shared module so the tools that need it all use one copy. Nothing changes for you today; it is the groundwork for the citation checker.

  • Dropped the source-scrape size ceiling from the roadmap-format test (ANTS-3633) The ceiling had been raised eleven times and never once caught the runaway it was meant to catch — it only taxed each edit to the roadmap-query code with a second round trip. The check that actually guards against reading the wrong function is kept, and its failure message now names the real cause.

  • find_sources is now labelled C/C++-only before you call it, not after. (ANTS-3619) On a Python or JS project it returns files_count:0, which is indistinguishable from a genuine "nothing calls this" — the worst possible wrong answer when checking a change's blast radius. The limit was only explained in the response. Both pre-call surfaces (the session hook menu and the tool catalog hint) now state it, and the adjacent find_definition line names its languages so the asymmetry is visible rather than inferred.

  • roadmap_query's bad_mode refusal now lists the accepted modes. (ANTS-3617) A caller who guessed a mode name wrong was told only which value was rejected, not which were valid, so finding the right spelling meant going to read the schema. Matches the accepted array its changelog_query sibling and its own bad_status already emit.

  • The /test-audit command now uses the fast server-side path, like /audit already does. (ANTS-3625) There was a quick way for the test-audit command to run its detection and chunking work inside Ants rather than the slow way, but the shortcut had never been switched on in the command's instructions. It is now — so a test-suite sweep no longer has to load the command's full manual to get started.

  • Refreshed five stale code comments in the audit runner (missing tool, dead status value, incomplete scope list, missing caveat). (ANTS-3610)

  • Headless audit_run MCP verb does not dispatch the in-process (inProcessRunner) audit lanes. (ANTS-3605) The headless audit_run MCP verb now runs the in-process doc-vs-code drift lanes (spec↔code, contract-doc, changelog↔test) that previously fired only in the GUI Audit dialog, so automated Claude/CI audits get the same coverage. A tool-less host no longer refuses an auto-detect sweep — the lanes need no external binary.

  • Hoisted the duplicated CommonMark fence-scanner into a shared MarkdownScan module (ANTS-3603) feedbackfile.cpp and speclog.cpp had verbatim copies of the fence-opener regex + opener-char helper; both now call the single src/markdownscan.{h,cpp} copy (Qt6::Core-only), which also exposes a fenceMask primitive for the doc-integrity arc. No behaviour change.

  • project_settings op:detect: also propose test_roots/changelog/docs_dir when the conventional dirs/files exist on disk, not just source_roots. (ANTS-3588) The auto-detect for project layout only fills in the source folder even when it can plainly see a tests/ dir and a CHANGELOG.md — make it offer those too so setup is one step, not three.

  • cold_eyes_brief: caution reviewers that ROADMAP/CHANGELOG are append-only HISTORY — a ✅/Resolved bullet is PAST state, a 📋 bullet is already-tracked work. (ANTS-3586) Cold-eyes reviewers keep re-reporting already-fixed bugs because a 'Resolved' roadmap line reads to them like an open problem; add one sentence to the brief telling them to verify against current source first.

Removed

  • test_audit_partition's inert quick field, which was accepted and forwarded but never read. (ANTS-3630) quick:true ran an identical full walk and pre-pass. --quick is caller-side (the grep pre-pass is the audit; only the subagent phase is skipped), so the field promised a fast path that did not exist. Callers passing it now get a schema refusal rather than a silent no-op.

Fixed

  • A "no window" safety check that handed control to code needing one (ANTS-3725) An internal guard for the no-window case passed the problem straight to a function that assumed a window existed, and threw away the caller's own answer on the way. Harmless in the running app, but it crashed any test driving these tools directly — which is why several were testing their own source text instead of their behaviour.

  • spec_log reports how much it wrote, not how big the file is (ANTS-3724) Editing a spec reported the size of the whole file as "bytes written". It now reports what the edit actually added or removed, with the full size alongside — the same two numbers the roadmap and changelog tools already give.

  • changelog_log now reports how much it added, not the size of the whole file (ANTS-3723) Adding a two-line entry used to come back as "1,150,003 bytes written", which is the entire CHANGELOG. It now reports the bytes it actually added, alongside the full file size as a separate number — the same way the roadmap tool already did, so the two can be read side by side.

  • Filing feedback whose example contains a shell comment no longer swallows the finding (ANTS-3695) A # at the start of a line in a pasted command example was being read as a document heading, which cut the finding short and hid it from the maintainer's review list — silently. Examples are now indented so that can't happen, and files already written the old way are read correctly again.

  • test_audit_partition no longer refuses a project that told it where the tests are (ANTS-3708) If a project declares its test folders in .ants/project.json, or you name the test files outright, the tool now just gets on with it instead of insisting it can't find a recognised test framework. Hand-rolled test harnesses — the kind with no framework to detect — work again. When it genuinely can't tell, the refusal now lists everything it looked for and the two ways to answer it.

  • A roadmap bullet quoting Lanes: in its prose no longer acquires a bogus lane (ANTS-3722) Writing about the trailer keys made the reader treat them as that item's real fields — so the docs describing the roadmap format were the bullets most likely to be mis-parsed. Inline Kind: x. Lanes: y. trailers still parse as before.

  • Feedback-file standard covers a project whose directory begins with a dot (ANTS-3714) The derived filename would start with a dot and never match the glob the standard calls authoritative, so that project's input would be invisible.

  • audit_run SARIF now carries level on every result (ANTS-3694) Asking severity_floor:"error" always returned nothing, because the headless writer emitted no level and the line-based parser recorded every cppcheck finding as UNKNOWN — the real severity survived only as the message's text prefix. Both are fixed, so real errors are findable again.

  • Debt sweep counts a test that names its invariant in the function name (ANTS-3693) test_INV8a_... cannot carry the hyphen, so the commonest citation shape read as uncovered. The same fix closes a false negative in the other direction: a test citing INV-80 was accepted as covering INV-8.

  • spec_query no longer runs a bullet-form invariant through a following heading (ANTS-3697) The last invariant before a subheading absorbed that heading and its prose, and test_surface inherited the over-run.

  • roadmap_log strips a stray XML tag at the start or end of a note (ANTS-3703) A malformed call could write a literal </note> into ROADMAP.md — a permanent line in an append-only file. Markup mid-sentence is prose and is left alone.

  • roadmap_query honours filter as an alias for status (ANTS-3698) The response echoes the applied lifecycle as filter, so callers sent that name back; it was silently ignored and answered with the full set, handing planning sessions already-shipped items.

  • roadmap_log resolution notes land at the end of a multi-paragraph bullet (ANTS-3696) A note used to be inserted after the body's first paragraph, leaving the rest of the problem statement and the Kind/Source trailer below it — so a well-documented item read as resolved and then went on arguing for itself.

  • spec_query could swallow an invariant from a malformed table (ANTS-3683) parseSpecBody's table-row pattern separated cells with a whitespace class that matches newlines, so on an invariants table missing its test column one match ran across two rows — returning the first invariant with the second row's text as its test surface, and dropping the second invariant entirely. Well-formed three-column tables were unaffected. Found by spec_lint, in the parser it consumes.

  • spec_query now returns test_surface for bullet-form invariants (ANTS-3665) The spec standard promises that an invariant's *Test:* clause comes back as its own test_surface field, but only the table form delivered it — so nearly every spec in the project returned the clause buried in the invariant body. Asking a spec "which of your invariants have no test?" now has an answer.

  • Test suite no longer goes red at random on a busy machine (ANTS-3658) One audit test reads the whole source tree, so it takes about 2 seconds when the files are already in memory and nearly 10 when they are not — against a 10-second limit. It now gets 60 seconds, so a failure there means a real hang rather than a busy computer.

  • MarkdownScan::fenceOpenerChar misreads a 4-backtick inline span as a fence opener. (ANTS-3655) A line that shows backticks as example text can be mistaken for the start of a code block, which makes the scanner skip the rest of the document.

  • The doc link-checker now recognises code blocks indented inside a bullet list (ANTS-3638) A code block nested under a bullet was invisible to the scanner, so its sample text was read as prose and any link-shaped line in it was reported as broken. Code blocks are now recognised relative to the bullet they sit under, exactly as Markdown defines them, while an indented block outside a list is still left alone.

  • roadmap_log no longer writes a body or note that opens a code fence it never closes (ANTS-3640) A roadmap note quoting a bare fence opener used to be written verbatim, which silently turned every bullet below it into one big code block — the next edit down there was refused with a message pointing at the innocent bullet. Unclosed openers are now escaped on the way in (balanced ones, which leave the file well-formed, are left alone), and the refusal names the line that opened the fence.

  • Review-tool descriptions no longer deny a call their own skill instructs (ANTS-3639) The "your slash-command skill does not call this tool" note was pasted onto every tool whose name started with cold_eyes_, indie_review_ or test_audit_. But /test-audit drives the whole test_audit_ family, and /indie-review instructs indie_review_partition and indie_review_corroborate by name — so sessions were told not to call verbs their skill mandates two lines later. Those seven are now exempt.

  • doc_integrity: a code span that crosses a newline is now masked end to end (ANTS-3635) The link checker masked inline code one line at a time, so the tail of a two-line … span was still harvested as a link — a C++ lambda split across lines read as a broken link. Masking now runs over the whole document. An unpaired backtick still masks nothing, so one stray tick cannot silently stop the checker on the lines below it.

  • Link checker no longer reports every section of a spec as "missing from TOC" when that spec's contents list is plain text. (ANTS-3634) (ANTS-3634) doc_integrity's TOC-coverage check matches sections by anchor slug, but it only required that a contents region exist — not that the region actually contained any #anchor links. A spec whose contents list is written as plain text (- §1 Problem) therefore offered nothing to match against, and every one of its sections was reported missing. Three specs (ANTS-1870, ANTS-2023, ANTS-2141) accounted for 23 of the 26 findings over this project's own docs — all false alarms; the documents were fine. The check now stands down when a contents region yields no anchors. A contents list that does use anchors keeps full coverage, including the existing rule that a single linkless bullet is skipped without ending the list.

  • doc_integrity no longer reports example links written inside backticks as broken. (ANTS-3623) Fenced code blocks were skipped but inline `[text](target)` spans were not, so 21 of the 22 broken-link findings over this project's docs were prose examples — 12 of them in doc_integrity's own spec, which has to contain sample links to explain what the checker does. Real links whose text is code, [a/b.md](a/b.md), are still checked.

  • A SARIF report could silently drop findings while reporting findings_truncated: false. (ANTS-3629) The document is capped at 10 000 results across all tools, but only the per-tool caps set the flag. Several tools each individually under the cap could sum past it, so results vanished from the report a reviewer treats as the complete finding set, with nothing to say so. The document-wide shed is now reported.

  • A test_audit_* contract test that asserted the opposite of the shipped behaviour and passed anyway. (ANTS-3628) It checked that the verbs were NOT caller_cwd-Required — the inverse of the truth — and passed only because the source window it searched had drifted off the lines it meant to inspect, so its assertions were vacuous and a real regression would have gone unnoticed. Rewritten to call the contract function directly, covering all five verbs; verified to fail when a verb is flipped.

  • Two stale audit_run schema descriptions that under-reported what a sweep does. (ANTS-3622) tools claimed auto-detect skipped clang-tidy (it skips only mypy — 9 tools run by default, clang-tidy among them), and paths claimed only cppcheck and clang-tidy honoured it (eight tools do; only the repo-global gitleaks and trivy ignore it). The second mattered most: believing narrowing didn't reach ruff/bandit/semgrep, a caller would skip paths and pay for a full sweep.

  • feedback_log op:compact_resolved accepts v2-or-later feedback files, not exactly v2. (ANTS-3621) The gate was != 2 while the parser gates on >= 2, so a future v3+ file was read with v2 semantics but refused as not_v2 and pointed at migrate_v2 — a no-op on an already-migrated file, leaving no way forward.

  • audit_run refuses an unrecognised formats value instead of silently producing no report. (ANTS-3626) An unrecognised entry such as ["json"] was non-empty enough to suppress the ["sarif"] default yet matched neither output branch, so a sweep that could run for 15 minutes finished with ok:true, no artifact, and no signal. Now refused as bad_args naming the accepted set, before any tool starts. An empty array still means ["sarif"].

  • The audit's "hide these known-OK warnings" list now works outside the app window too. (ANTS-3615) The .audit_allowlist.json filter only ever ran in the Audit pop-up, so an entry that hid a finding there did nothing when Claude or CI ran the same audit. It is now shared by both. The suppressions setting, which had quietly done nothing, is honoured too — and an unrecognised value now says so instead of being ignored.

  • audit_run cleans up the temporary report files it leaves when a project folder is read-only. (ANTS-3614) When the audit can't write into your project it drops its report in a temp area, and those were never cleaned up. They are now swept after a week, on the next audit that needs the fallback.

  • audit_run no longer treats a long-but-healthy sweep as a dead worker. (ANTS-3611) The safety timer that stops two audits running on the same project at once still used an old 4.5-minute limit, but audits can legitimately take up to 15 minutes. A long sweep could have its slot freed underneath it, letting a duplicate start. The timer is now derived from the real limit instead of hardcoded.

  • Aggregate concurrency cap on audit_run — a third simultaneous sweep now refuses server_busy instead of allocating. (ANTS-3612) The audit engine was meant to run through a small pool of worker lanes to cap memory, but that pooling was never built — each audit just started its own worker. Up to 16 could run at once (~30 GiB of tool memory), enough to run a 32 GiB machine out of RAM. runAudit now takes one of two process-wide slots before doing any work, so audits across different projects are bounded too.

  • docs_index no longer indexes headings or links inside fenced code blocks (ANTS-3604) A # Heading or [link](x) written inside a ``` example block was being treated as a real document heading/link. The scanner is now fence-aware, so illustrative examples in code fences no longer pollute the documentation map.

  • indie_review_orchestrate: don't fire summary-similarity suggested_merges for lanes built by the file-list directory-grouping fallback (boilerplate summaries → false merges). (ANTS-3591) When indie-review groups a file-list project by folder, every lane's description is near-identical boilerplate, so the 'you could merge these lanes' hint wrongly suggests merging tests with src.

  • audit_run/last_audit_summary: filter zero-content SARIF results (empty ruleId AND message AND artifact uri AND startLine 0) out of total_raw/total_actionable and top_findings. (ANTS-3590) A completely clean repo reported '1 actionable finding' — a blank MAJOR row with no file, rule, or message — because trivy emitted one empty placeholder result. A close-phase/audit gate could misread that as a real blocker.

  • session_orient: keep top-level ok:true when the only problem is absent-but-optional artifacts (no ROADMAP/specs/empty index) — surface them via warnings[]/notices[]. (ANTS-3587) A brand-new project with no roadmap yet gets a scary 'ok:false' from the first orientation call, as if something broke — but 'no roadmap yet' is normal.

Security

  • The cross-session feedback-file exception is now bounded by directory, not just filename. (ANTS-3616) One shared coordination file is deliberately allowed to sit outside the project root. That exception keyed on the filename suffix alone with no location check, so any path ending _Ants_MCP_Feedback.md anywhere on the filesystem qualified — and since apply_edits uses the same check, that made the naming convention a broad write permission. It now also requires the file to sit in a directory above the project. The apply_edits description, which stated the root-escape refusal with no mention of the exception, has been corrected — that omission is what led a contributor to report it as a suspected security hole.

  • audit_run's compile-DB escape validator no longer skips every non-default build tree. (ANTS-3624) It probed only build/ while clazy and clang-tidy resolve their compile database from seven build-dir names. On a tree whose DB lives in build-fast/ — this project's documented iteration workflow — the validator found no file, concluded there was nothing to validate, and the tools then consumed that DB with its include paths never checked for escapes. Both now go through the same resolver.

  • test_audit_partition now root-anchors a caller-supplied scope, closing a path-traversal read. (ANTS-3627) A scope:"path:../.." (or a files: entry with ../) was concatenated onto the project root with no anchor check, so the walk enumerated files outside the project and returned their paths in the envelope. Both branches are now checked against the canonical root and refuse bad_path; a files: list refuses as a whole rather than silently dropping the bad entry.

v0.7.102-rc1Pre-release

0.7.102 RC1 — Patron preview

This release shipped without written notes.

v0.7.101

0.7.101

Added

  • recent_errors / build_status — attach likely_fix add_include for "'X' has not been declared" diagnostics. (ANTS-3374) recent_errors and build_status now attach a likely_fix:{add_include, defines, at} hint on undeclared-symbol diagnostics ('X' has not been declared / was not declared in this scope / unknown type name 'X' / use of undeclared identifier 'X'), resolving the declaring header so the missing #include no longer needs a second lookup.

  • indie_review partition deriver: optional fallback to group a file-list ## Module map into lanes by top-level directory. (ANTS-3507) Right now the auto-reviewer only works when a project's module map lists named subsystems; a project that lists plain file paths (common) gets refused and must hand-write a partition file. Let it fall back to grouping those files into lanes by their top folder.

  • indie_review_partition now emits a sparse_partition_hint on an empty/≤1-lane partition (ANTS-3567) Symmetry with cold_eyes_partition (ANTS-1634a): when the module-map deriver yields ≤1 lane (no CLAUDE.md ## Module map, no docs/subsystems.md), the response now carries sparse_partition:true plus a hint pointing at indie_review_brief's source_paths[] ad-hoc mode (ANTS-3375) and the .indie-review/partition.json override — instead of a bare empty partition the caller can't act on.

  • changelog_query read verb — mirror roadmap_query so drift-checks stop reading the whole CHANGELOG. (ANTS-3533) Add a quick lookup for the changelog like the one the roadmap already has, so reviews don't read the whole 276K-token file.

  • Surface MCP tokens-saved as a live status-bar chip + persisted month / YTD / all-time aggregate. (ANTS-3572) A little pill on the status bar shows how many tokens the Ants MCP has saved you — live this session, plus this-month / this-year / all-time totals that survive restarts.

  • spec_query silently under-reports invariants declared inline (` (ANTS-3569) When a spec writes a rule as a sentence instead of a list or table, the quick spec-reader reports zero rules, so a caller trusting the count thinks the spec has no invariants.

  • indie_review_brief gains a source_paths[] ad-hoc-lane mode — a lightweight single-reviewer broker for a cold review of a code / dependency diff When lane is not in the auto-partition, pass source_paths[] (the changed files of a dependency bump / code diff) and the brief is synthesised over just those files — the code-review analogue of cold_eyes_brief's doc_paths[]. Paths are traversal-guarded; rejected entries surface in source_paths_rejected, and an unknown lane with no usable override now lists known_lanes for recovery. (ANTS-3375, ANTS-3493)

  • feedback_query — resolve foreign-prefix mapped ids (ANTS-*) against the canonical sibling roadmap under the shared root. (ANTS-3519) feedback_query now resolves foreign-prefix mapped ids (e.g. ANTS-* cited in a consumer project's feedback file) against the sibling roadmap that owns the prefix — the mapped_id_status entry carries the real status, a resolved_from field naming the owning project, and shipped_date when shipped, instead of a bare "foreign_repo". Cost-gated on a foreign id being present and RAM-bounded (one roadmap parse per distinct foreign prefix, discarded after).

  • find_definition is blind to GLSL/shader symbols — add a glsl language family. (ANTS-3558) In projects built around shaders (like the DOOM renderer), the "where is this defined?" tool can't see any shader code, so it wrongly reports symbols as missing.

Changed

  • roadmap_query mode:bundles clusters on headline tokens only — same kind+lane+stem items return as all-singletons, indistinguishable from no-clusters. (ANTS-3388) roadmap_query mode:bundles now groups per-module " /spec.md"-style to-dos that share the same Kind, a lane, and a template shape (previously returned as indistinguishable singletons), and always reports no_clusters_found so a caller can tell "grouped into singletons" from a real grouping.

  • The status-bar "tokens saved" badge is now scoped to the current project instead of a global all-projects total. (ANTS-3579) The pill shows the savings for the project of the tab you're looking at — its live-session number on the face, this-month/year/all-time in the tooltip — and switching tabs re-scopes it. A separate "All projects" tooltip line preserves the previous global total. Savings are attributed per Claude Code call's project folder; the per-project store is bounded (64 projects, LRU). Switching to a tab for a project you have history for (but haven't used this session) shows that project's all-time.

  • changelog_query entries[] auto-downshifts to the lean headline_only shape when a page would truncate, keeping the whole tail (mirrors roadmap_query). (ANTS-3576) When a full-detail entries page overflows the soft cap, the server projects every entry to {version, category, ids, text_oneline} and re-pages so nothing is silently dropped; the response then carries downshifted:true. Opt out with an explicit limit or include_body.

  • Auto-downshift a would-be-truncated roadmap_query / workspace_search list to its titles-only form instead of cutting off the tail. (ANTS-3543) When a result list is too big to send whole and you didn't ask for a specific page size, the server now keeps every item but drops the long descriptions (showing just the titles / file:line) rather than silently cutting off the end — so a scan for "is X tracked anywhere?" no longer misses a hidden remainder. It emits downshifted:true, and only pages if even the titles-only list overflows.

  • debt_sweep_scan self-describes its scope (detectors_run + scope_note) so total_findings:0 is not misread as "no debt". (ANTS-3564) The debt-sweep tool only looks for a narrow set of markers; when it finds nothing it should say "I only checked these things", not silently imply the project is clean.

  • Default max_match_bytes ceiling on workspace_search — clip pathological long lines by default (token-saver default-ON). (ANTS-3548) One giant machine-generated line shouldn't be able to flood a search result; trim over-long lines automatically unless asked not to.

Fixed

  • roadmap_log flip_batch: line_range locator has no ants-v1 fallback in a mixed-format roadmap (ANTS-3565 residual). (ANTS-3570) In a to-do file that mixes two bullet styles, flipping a newer emoji-style item by its line number still fails — though you can now flip it by its ID or title instead, which is the usual way.

  • feedback_log op:assign_id now honours its documented note param (ANTS-3571) assign_id{ids|closure, note} silently dropped the note — it was never read from the request. It now renders as a single - **Note:** <note> bullet under the finding's Proposed ID line (newlines folded to spaces), replaced in place on re-assign so the write stays idempotent; the envelope echoes note_written:true.

  • roadmap_query: prose/ID-less roadmaps now warn on every status filter, not just status:all (ANTS-3583) A status-filtered query (planned/in-progress/considered) against a roadmap with zero [PROJ-NNNN]-tagged bullets returned a bare count:0 with no warning — dangerously readable as "no work left". The empty path now scans the whole file for any id-bearing bullet and, when none exist, emits a machine-detectable parseable_bullets:0 plus a "format not recognised" warning identically on every filter.

  • roadmap_query mode:"headline_only" now measures the lean row for the auto-truncate cap, so more title-only bullets fit per page. (ANTS-3577) Restores ANTS-1881 INV-6: the projection to the 4-key headline shape now runs before pagination (it had drifted to projecting the page after measuring the full-size row, dropping more rows than needed).

  • read_regions requests/paths/regions aliases are stripped by the inputSchema (additionalProperties:false) before reaching the ANTS-3500 fallback. (ANTS-3568) The batch file-reader has handy synonym keys that were coded in but the tool's input rules silently throw them away, so they never work — you always hit an error unless you spell it exactly items.

  • roadmap_log flip/annotate ID-locator suggestions now rank by sibling-id prefix, not headline overlap (ANTS-3566) A bullet_not_found on an id like 3D_E-0031 no longer suggests every bullet whose title merely starts with "3"; it offers same-prefix sibling ids (or none when nothing is related).

  • roadmap_log op:amend_body and op:flip_batch locate ants-v1 emoji bullets in a mixed-format roadmap (ANTS-3565) Extends the ANTS-3561 fix to two more verbs: in a GFM-majority ROADMAP.md with appended - 📋 [ID] emoji bullets, amend_body and flip_batch can now patch/flip those bullets by id or headline (flip_batch resolves each locator against either format and can flip GFM and emoji bullets in one commit).

  • roadmap_log flip/annotate: digit-led-id bullets have no working locator — headline fallback also fails. (ANTS-3561) roadmap_log op:flip / op:annotate can now locate an ants-v1 emoji bullet (- 📋 [ID] **…**) inside a GFM-majority (mixed-format) roadmap. Previously the ants-v1 walker only ran when the file had ZERO GFM bullets, so a mixed file's appended emoji bullets were unaddressable by id or headline — bullet_not_found — even though roadmap_query read them fine.

  • Bash guard-hook no longer vetoes a grep -v ROADMAP.md / --exclude=ROADMAP.md exclusion as if it were a read of the roadmap (ANTS-2169).

  • read_region/read_regions symbol mode should resolve namespace/class-qualified names, not only the bare identifier. (ANTS-3513) When asking to read a function by name, typing its full name (like ClassName::method) fails and you must strip the prefix — the tool should accept both.

  • Malformed ids filter fails silently to the full list instead of refusing — waste + wrong result. (ANTS-3541) If a tool call asks for specific roadmap items but formats the request slightly wrong, it quietly dumps the whole list instead of saying 'that was malformed' — wasting tokens and hiding the mistake.

  • e2e run leaks a throwaway config path into the real ~/.claude/settings.json SessionStart hook. (ANTS-3562) Running the built-in end-to-end test suite quietly edits your real Claude settings to point a startup script at a temporary folder; once that folder is deleted, every later Claude session shows a "file not found" hook error until the app is relaunched and rewrites it.

  • roadmap_query zero keyword-match emits a misleading "every entry has no [PROJ-NNNN] id" warning. (ANTS-3560) When you search the roadmap and nothing matches, the tool wrongly says "none of your items have IDs" and points you at the wrong fix — when the truth is simply that your search text matched nothing.

v0.7.101-rc1Pre-release

0.7.101 RC1 — Patron preview

This release shipped without written notes.

v0.7.100

0.7.100

Added

  • User-level e2e feature case suite (ANTS-2050) A lane-partitioned QA suite that drives the app as a user through the ANTS-2049 harness: tools/e2e/cases.sh automates the terminal-basics, scrollback, resize and theme-restyle-liveness lanes (run all or one lane), wired into ctest -L e2e; docs/qa/e2e/cases.md enumerates every feature as drive→expected cases marked auto/manual/pending-hook.

  • End-to-end test harness (ANTS-2049): a --e2e launch flag + socket-only inject verbs let the app be driven as a user for testing. New --e2e flag forces the RemoteControl socket open and unlocks four socket-only inject verbs (inject-key, inject-click, resize-window, grab-image) behind an m_e2eMode gate; a generic --remote-json client passthrough carries their argument shapes. A shell driver (tools/e2e/run.sh) + 8-case smoke suite (tools/e2e/smoke.sh) launch a throwaway isolated instance, drive it, and reap it, wired as an opt-in e2e ctest label. The inject verbs are NOT MCP tools, so they are unreachable against a normal instance (they refuse e2e_disabled).

  • read_spill row-paging — page a parked list's rows cheaply instead of re-downloading the whole body (ANTS-3545) When a large list reply is parked (offloaded) and its preview shows an array of rows, read_spill now accepts row_offset/row_count to fetch the NEXT batch of rows, already parsed — instead of byte-paging (which lands mid-row) or re-fetching everything. Returns {mode:"rows", key, rows, row_offset, total_rows, truncated}; page on via row_offset + rows.size(). Completes the ANTS-3538 preview. Pages the same dominant array the preview named (shared detection). Bounded by a stat-before-read 1 MiB parse cap (refuses too_large without loading an over-cap body); non-array bodies refuse not_array (byte-page them). Byte-mode read_spill is unchanged.

  • workspace_search offset cursor — continue a truncated search instead of re-running it wider (ANTS-3547) offset:N returns the window [offset, offset+max_results); when more matches remain the reply carries next_offset, which you pass back as offset to page forward (mirrors roadmap_query) — instead of re-running the search wider, which re-scans from scratch and re-emits every match already seen. Pairs with count_only (count first, then page). Dedup applies within each page.

  • workspace_search files_only mode — return the deduplicated matched-file set with per-file hit counts, no match rows (ANTS-3549) The rows-eliminated answer to "which files reference X?": files_only:true returns {files:[{file,count}], files_count, count} and omits every match line. Much smaller than headline_only when a symbol recurs many times in one file. File set and per-file counts are complete (uncapped by max_results); count is the true total. Complements count_only (counts only) and headline_only (one line per match).

  • Structured preview on offloaded Ants-MCP replies — first rows + total, no second fetch (ANTS-3538) When a large array-shaped reply is parked to a spill file, the preview now includes the first few whole results plus the total count (head_rows / row_count), so most callers answer without a read_spill round-trip. The preview is capped and always smaller than the original (INV-9); it falls back to the byte-prefix head for non-array, tabular, over-1-MiB, or marginal bodies.

  • workspace_search count_only mode — return {count, files_count} with no match rows (ANTS-3537) A rows-eliminated existence/frequency check ("is X referenced?", "how many call-sites?") that skips serialising match bodies. count is the true total (uncapped by max_results); truncated flags only a cut-off scan. Complements headline_only and max_match_bytes.

  • cold_eyes_brief: resolve cited-code REGIONS (symbol / file:line) so cold-eyes reviewers read regions + outline, not whole files. (ANTS-3522) cold_eyes_brief now emits cited_code_regions[] ({path, lines[]}) — the exact lines from each path:line citation, grouped per file — so a cold-eyes reviewer reads a window around each cited line (outline + read_region) instead of the whole file. Additive alongside cited_code_paths; structural completeness is preserved (the reviewer still outlines the file). Cuts review-loop token cost without losing accuracy.

  • changelog_log op:normalize — one-call tidy for the [Unreleased] section (ANTS-3495) Reorders the ### category blocks under ## [Unreleased] into canonical Keep-a-Changelog order (Added/Changed/Deprecated/Removed/Fixed/Security) without moving any content — the companion fix the malformed-section advisory previously lacked. Non-destructive (bullets stay under their heading; duplicate/custom headings keep their place), with a dry_run preview and a no-write no-op when the section is already canonical.

  • session_orient now actively flags a stale MCP-server binary (ANTS-3499) When the running server's build commit is behind the project's HEAD, session_orient emits a server_build_stale block (behind_commits, built, head, hint) so a Claude Code session on an old binary is told to relaunch before trusting shipped status — instead of silently re-reporting already-fixed bugs. Advisory only; never blocks; skipped when the build commit is unknown to the repo or caller_cwd is not a git checkout.

  • More tab colours, plus a "Custom colour…" picker for any colour you like (ANTS-1374). Right-click a tab → the colour list now offers 14 preset colours (up from 7), and a new "Custom colour…" entry opens a full colour picker so you can tag a tab with any colour at all. Custom colours are remembered across restarts and stay put when you drag tabs around, exactly like the presets.

  • Prompt-jump chips in the scrollback. (ANTS-1330) Two small floating buttons (↑ / ↓) now appear above the "back to bottom" chip when you scroll up: click ↑ to jump to your previous command, ↓ for the next — the mouse companion to the existing Ctrl+Shift+Up/Down shortcuts. They only show once you've scrolled up and your shell has command markers (OSC 133) enabled.

  • Stamp the ship-date (+ short commit) into shipped feedback-file findings so a reporting session can self-check its server_build before re-reporting. (ANTS-3504) When we mark a bug fixed in another project's feedback file, also record WHICH build it shipped in. Then that project's Claude session can compare against its own running terminal and know "my copy predates this fix — restart before assuming it's still broken," instead of re-filing an already-fixed bug.

  • session_orient codebase_index lane digest is empty for repos with no ## Module map — add a capped file-path fallback digest. (ANTS-3503) The one-call session opener's code map is blank for projects that don't declare a module map, so those projects still get no 'where is the code' hint on the first call — even though the tool already knows the file list.

  • read_regions should accept requests/paths/regions as aliases for the items batch key. (ANTS-3500) The batch file-reader only accepts one exact spelling for its list of things to read; a natural guess gets rejected. Accept the obvious synonyms too.

  • feedback_query — render each assigned id's live ROADMAP status (the deferred ANTS-3448 follow-up). (ANTS-3478) Make the feedback reader show, at a glance, whether each finding's assigned task is planned / in-progress / shipped — pulled live from the roadmap — instead of just listing the IDs.

  • feedback_log op:append_tracking should refuse (or warn) on a :2 file, pointing at assign_id. (ANTS-3477) The old way of recording IDs (a tracking table) is retired; if someone tries to use it on a new-format file, it should politely refuse and point at the new way.

  • Add id_prefix override param to the three fold-in verbs (test_audit/cold_eyes/indie_review_fold_in) — parity with roadmap_log op:append. (ANTS-3498) Let a caller force a specific ID prefix on the review fold-in tools, the way the roadmap-append tool already allows.

  • feedback_log op:migrate_v2 gains backfill_from_tracking — carry inline Proposed-IDs over from the v1 tracking tables (ANTS-3474) Confidence-gated token match (heading↔tracking-item, per-id overlap-coefficient with a margin) replaces each finding's blank Proposed-ID placeholder with its already-assigned ANTS-NNNN; ambiguous/folded matches stay blank (never a wrong id). Reports backfilled[] {heading, line, id, confidence_pct}; dry_run previews. Unblocks per-finding compact_resolved on migrated files.

  • git_state op:diff hunk-header mode for clean commit splits (ANTS-3377) hunks=true returns per-file @@ hunk headers {path, hunks:[{header, old_start, old_count, new_start, new_count, lines?}]} instead of --numstat counts; staged=true diffs the index vs HEAD; include_lines attaches hunk bodies; context sets the unified-context width. Backed by the pure GitWrap::parseDiffHunks helper.

  • verify_changes orphaned-source lint (ANTS-3373) Warns when a source file added in the working tree (.cpp/.cc/.cxx/.c++/.c) is referenced by no CMakeLists.txt / *.cmake — it compiles in isolation but is silently never built. Surfaced as an advisory orphaned_sources[] array (does not fail the build gate; emitted only when non-empty). Build/vendor dirs are pruned so a generated build-tree CMakeLists never masks a real orphan.

  • session_orient's embedded codebase_index sub-object is still counts-only — extend the ANTS-3438 symbol/lane/file selectors (or a compact digest) to the first-call orientation surface. (ANTS-3468) session_orient's embedded codebase map now carries a compact per-lane source_files digest (each subsystem's non-test source paths, sorted, capped) so the first-call "query before grep" map is navigable — a session can jump straight to file_outline/read_region instead of grepping. Opt-in on the standalone codebase_index too via lane_files:true; deterministic, so it keeps the bundle's ETag-304 stable.

Changed

  • MCP read replies now teach each advisory hint once per session, then hush (ANTS-3550) The next_call_hint / leaner_call_hint nudges on read replies are educational the first time and pure recurring overhead after; a process-scoped "already-taught" latch emits each (tool, hint-kind) pair once, then omits it. Keyed per verb so a distinct verb's own tip is never hidden. On by default (token-saver); flip the claude.mcp_hint_latch config key off to get the tips on every call.

  • Cold-eyes: stop full-reading large append-only cross-ref logs (ROADMAP/CHANGELOG) — consult by search/region. (ANTS-3526) Right now every review helper is told to read the entire 25,000-line roadmap and 19,000-line changelog top to bottom — the single biggest review cost. Have them search those two big logs for the exact thing they're checking instead.

  • feedback_query mapped_id_status marks cross-repo ids foreign_repo instead of unknown. (ANTS-3518) A mapped ANTS-id whose prefix is absent from the caller project's own roadmap now resolves to "foreign_repo" (with a mapped_id_status_note) rather than the ambiguous "unknown", so a consumer session no longer misreads a shipped suggestion as never-shipped. Fuller cross-repo status resolution is tracked in ANTS-3519.

  • Faster Roadmap dialog on large histories — the bullet parser is memoized across debounced renders, so typing in the search box no longer re-parses the whole (up to 64 MiB) archive on every keystroke. (ANTS-2119)

  • Lower idle CPU — a blinking cursor (and hover/expose repaints) no longer redraw the whole terminal. paintEvent now honours the damage rectangle (ANTS-3454): it redraws only the screen rows the update actually touches. The cursor blink invalidates just its own cell, so instead of re-shaping all 40 rows every 550 ms it now repaints a single row. Full-screen updates (heavy output, resize, theme change) are unchanged, so there is no risk to normal rendering.

  • Faster text rendering under heavy output — the terminal no longer re-computes the shape of every letter on every redraw. A new shaped-run cache (ANTS-3453) remembers the glyph layout of each run of text keyed by its content and style, so an unchanged run is redrawn without re-running the (expensive) text-shaping step. Measured 1.6-1.9x on the shape-and-draw path at a ~100% hit rate; it eases the per-frame cost behind the typing freeze during heavy Claude Code output. Cache is bounded (~8 MB worst case) and cleared on font change.

  • Skip the shell-history autosuggestion scan when the input line is unchanged, cutting per-batch work during output. (ANTS-3455)

  • Preserve URL/highlight span caches across streaming frames — only the changed scrollback band is invalidated instead of a full wipe on every push. (ANTS-3452)

  • Frame-pace output-driven repaints to ~60 fps so heavy terminal output no longer starves keystrokes (typing-freeze relief). (ANTS-3451)

  • Roadmap ID counter (.roadmap-counter) is now a derived, gitignored cache instead of committed state (ANTS-3450) The next roadmap ID is now floored to the true high-water mark computed from the committed corpus (ROADMAP.md + CHANGELOG.md + docs/roadmap/ archives), so a stale, wiped, or fresh-clone-absent counter can never reissue a live or migrated ID. This removes the recurring "counter bump left out of the commit" drift entirely — git can't drift a file it no longer tracks.

  • find_sources no longer stalls on the first call after a restart (ANTS-3444) session_orient now warms the find_sources file set into the OS cache on a background thread, so the first find_sources of a session reads fast instead of paying a one-time cold-disk penalty. Completes ANTS-3444 alongside the earlier CPU cut and the 60s bridge-timeout guard.

  • MCP server-side latency: find_sources and spec_query list mode are dramatically faster (ANTS-3444) find_sources now ASCII-folds file bytes in place and searches with QByteArray::indexOf instead of decoding every file to a lowercased UTF-16 string — ~5× faster warm (106 ms → 21 ms on the full tree), identical results. spec_query list mode reads an 8 KiB header per spec instead of the whole body (title/status always sit in the first ~124 bytes), skipping the wasted full-body invariant parse.

Fixed

  • Crash when changing the colour theme from the menu (ANTS-3556) Picking a theme from View → Theme could crash the terminal on Wayland. The restyle now runs a beat after the menu closes instead of while it is still closing, so the earlier safety guard is no longer bypassed. Locked with a regression test.

  • Large-result offload never hands back a preview envelope larger than the original reply (ANTS-3540) At a head≈threshold config the fixed envelope overhead plus the head could exceed a body that only just cleared the head guard, so offload could produce a net loss. offloadBody now measures the finished envelope and fails open (returns the untrimmed body) when it would not be strictly smaller — honoring the INV-9 net-saving guarantee at every config.

  • last_audit_summary no longer mislabels a since-tag / since-last-run / branch-diff / files audit as a single-file rerun. (ANTS-3517) The narrow_run_warning suppression (ANTS-3512) only trusted the full scope; every explicit multi-file changeset scope now suppresses the false "a broader recent file may exist" alarm, since the sidecar-recorded requested scope is authoritative.

  • Audit-cache retention no longer deletes a report a kept run still references — a same-second/same-commit filename collision could reap a SARIF/HTML file that a surviving history entry pointed at. (ANTS-2119)

  • Output triggers now fire once per matching line — a non-instant trigger (notify/command/run_script) fires on every completed line that matches, instead of once per output batch and only when the batch happened to end on a newline. (ANTS-2119)

  • ANTS-2119 hardening tail (partial): 11 correctness/robustness fixes across the PTY, audit, MCP-dispatch, and Claude-integration lanes. Small, low-risk repairs the June indie-review flagged: a stale-fd guard and a documented env-buffer reserve in the PTY reader; PTY write-loss is now logged instead of silent; the audit review-report handed to Claude gets explicit owner-only file permissions; the review dispatcher enforces its reply size limit in bytes (not characters) and rejects lane names that could escape the reports directory; the drift-check helper no longer throws away the script's error output; project discovery and the brief-file cache are bounded so a corrupt file or long session can't grow memory without limit; and a dead field was removed.

  • last_audit_summary mislabels a genuine full-tree audit as a single_file/narrow rerun when findings land in one file. (ANTS-3512) The audit summary sometimes warns 'this looks like a one-file re-run, a bigger run may exist' even after a full project scan — because it guesses the scope from how many files had problems, not from what was actually asked for.

  • workflow_state arg-validation messages — name all missing required args and distinguish absent skill from malformed. (ANTS-3511) When a Claude session calls the workflow_state tool without all its required inputs, the error now tells it everything that's missing in one go, instead of making it guess twice.

  • Debt-sweep shipped_without_commit now honours the review window. (ANTS-3342) The detector re-checked every ✅ item in the whole ROADMAP against git log, re-surfacing ~433 pre-convention shipped items each sweep. It now only considers items flipped to ✅ within since..HEAD (diff ROADMAP.md over the window, added-but-not-removed ✅ lines) (ANTS-3342).

  • Debt-sweep code-quality detectors no longer flag deliberate test-fixture data. (ANTS-3344) orphan_q_unused / obsolete_qstring_idiom / dead_branch_after_return now skip the tests/features/ and tests/audit_fixtures/ subtrees, whose files embed those exact patterns as conformance inputs (ANTS-3344).

  • Review Changes dialog now refreshes live on file edits, not just commits (ANTS-3509) The dialog advertised "live — auto-refresh on git changes" but went stale until you pressed Refresh, because its QFileSystemWatcher could not see a content edit of a file inside a watched directory (Qt only surfaces add/remove/rename). It is now driven by a new raw-inotify DirTreeWatcher that watches the gitignore-aware set of working-tree directories (edits included) plus the .git metadata — one inotify fd, directories-only (≈4× fewer watches than one per tracked file), and every watch is released when the dialog closes. Git dir is resolved via git rev-parse (worktree / sub-dir cwd safe), and all probes run with GIT_OPTIONAL_LOCKS=0 so a read-only status can't rewrite the index and self-loop.

  • Silenced 4 more harmless compiler warnings from the diff/review viewer (ANTS-3505) The same false-alarm "-Wnull-dereference" warning quieted in the dialog helper (ANTS-3358) also fired 4 times in the diff/review viewer's "back to top" button positioning. Quieted the same way (a tightly-scoped, GCC-only pragma), keeping the build warning-clean. No behaviour change.

  • Silenced 33 harmless compiler warnings from the dialog-frame helper (ANTS-3358) A full optimised build printed ~33 false-alarm "-Wnull-dereference" warnings from the dialog centring/resize helper — the compiler couldn't prove a Qt widget pointer was set, even though the code already checks it. Quieted with a tightly-scoped, GCC-only pragma (the same approach as the earlier ANTS-1554 fix), so real warnings no longer get lost in the noise. No behaviour change.

  • Release version bumps now reliably update the version shown in the README and roadmap (ANTS-2163). The automatic version updater was searching for old wording that no longer matched those two files, so it quietly skipped them and the "Version X" line could show a stale number after a release. Realigned the search text, removed a dead rule for an AppImage filename that is now a wildcard, and taught the drift-check to actually validate both banners so they can't silently fall behind again.

  • indie_review_orchestrate returns no_lanes when a ## Module map is present-but-unparseable (misleading "heading absent" error). (ANTS-3481) The auto-reviewer says "you have no module map" even when the project DOES have one — it just can't read that particular list format. The error should say so instead of claiming it's missing.

  • FeedbackFile::skeleton() still births new feedback files as v1 (marker :1 + append_tracking banner). (ANTS-3476) When a brand-new project logs its first feedback, we still create the file in the OLD format with old instructions — new files should be born in the new format.

  • find_sources returns files_scanned:0 (early-bail) for a valid multi-word topic that leads with a symbol name. (ANTS-3489) The who-uses-this search sometimes scans nothing and returns no results for a reasonable query, when a plain search finds the answer instantly.

  • debt_sweep_defer renders [ANTS-] hardcoded + un-padded — misses the ANTS-3473/3480 fold-in ID treatment. (ANTS-3497) One of the auto-ID tools (debt-sweep) still writes the fixed 'ANTS' prefix without leading zeros, unlike its siblings which were fixed.

  • roadmap_query id/ids cannot locate a bullet whose ID prefix is digit-leading (e.g. [3D_E-NNNN]) — the whole project ID scheme is invisible to id-lookup. (ANTS-3492) For a project whose IDs start with a number, the roadmap lookup by ID finds nothing at all — none of its items can be fetched or flipped by ID.

  • Fold-in verbs emit un-zero-padded IDs (FIBR-82 not FIBR-0082) + inconsistent allocated_ids type — ANTS-3473 residual. (ANTS-3480) The auto-ID feature I added today gets the project's ID prefix right but drops the leading zeros (writes FIBR-82 where every other ID reads FIBR-0082), so the IDs sort and match wrong.

  • fold-in verbs stamp the project's own roadmap ID prefix instead of a hardcoded ANTS- (ANTS-3473) test_audit_fold_in / cold_eyes_fold_in / indie_review_fold_in / plan_template now sniff the dominant [PREFIX-NNNN] prefix from the project's ROADMAP.md (new RoadmapFoldIn::sniffIdPrefix), so a fold-in into e.g. a FIBR-NNNN roadmap allocates FIBR- and no longer collides with the Ants roadmap's own IDs.

  • audit_run no longer counts mypy note: (annotation-unchecked) lines as actionable findings (ANTS-3472) A deps-less mypy run over untyped helpers emitted informational note: lines that inflated total_actionable, misleading /close-phase triage into a phantom fix-pass on a tree the gated mypy reports clean. Note lines are now dropped before the finding count.

  • codebase_index now covers repo-root source files on non-standard layouts (ANTS-3390) (ANTS-3390) project_settings op:detect now suggests source_roots:["."] when a project's source sits loose at the repo root (RetroArch's retroarch.c, configuration.c, runloop.c … ~9k LoC), instead of a subdirs-only suggestion that silently dropped those files from the index. The whole-root walk also had its keys normalised — a "." source_root previously stored "./retroarch.c", so codebase_index(file_path:"retroarch.c") returned found:false; it now keys the bare repo-relative path, which also repairs flat-root ["."] projects (ANTS-3393) whose file_path lookups and tests/ role-detection were silently broken.

  • focused_test returns an MCP -32000 transport timeout when a broad changed-file (e.g. remotecontrol.cpp) maps to a large ctest set that outruns the transport deadline. (ANTS-3449) focused_test now runs ctest in parallel (-j, capped at 4 / host cores), so a broad change that falls back to the full suite finishes in ~26s instead of ~78s and no longer trips the MCP transport timeout (spurious -32000) while the tests were still running fine underneath.

  • feedback_query/feedback_log path auto-derivation doubles a trailing '_Ants' in the project leaf. (ANTS-3426) feedback_query/feedback_log no longer double a trailing "_Ants" in the auto-derived filename (fork checkouts like DOOM_Ants now resolve DOOM_Ants_MCP_Feedback.md instead of the never-existing DOOM_Ants_Ants_MCP_Feedback.md), so a path-omitted call reads the real file and cannot fork history into a wrong-named duplicate.

  • feedback_log op:append_tracking does not pipe-escape | in item/notes cells — a pipe in the text corrupts the markdown table row. (ANTS-3469) When the triage tool writes a tracking row containing a pipe character in its text, the table row breaks, and a later cleanup step can't read the row's status.

  • roadmap_log op:amend_body now reaches body text on blank-line-separated nested sub-bullets, and its body_match_not_found refusal hints when old_text sits outside the bullet or spans a hard-wrapped line break. (ANTS-3467)

  • workspace_search now hints 'did you mean regex:true?' when a regex:false pattern carrying regex metacharacters (e.g. A|B|C) returns zero matches, so an empty result isn't mistaken for 'symbol absent'. (ANTS-3466)

  • find_definition now resolves C++ type definitions (struct / class / union / enum) — a pure type with no same-named constructor previously returned zero definitions. (ANTS-3465)

  • roadmap_log note append is now retry-idempotent (ANTS-3440) A roadmap_log op:flip/annotate that committed its ROADMAP.md write but surfaced to the caller as a transport timeout used to duplicate the resolution note when retried. appendBodyNote now dedups a byte-identical trailing note, skipping the second append and reporting note_already_present:true. Applies to flip (GFM + ants-v1), annotate, and flip_batch (shared primitive).

Security

  • Kitty graphics images are now strict-base64 decoded — a corrupt image payload is rejected outright (matching the OSC 52 / iTerm2 paths) instead of silently feeding a garbage-prefixed stream to the image loader. (ANTS-2119)

  • OSC 8 hyperlink phishing warning now covers the common case — a benign-looking link label ("Download", "click here") pointing at a punycode/IDN-homograph or bare-IP destination now prompts before opening, not only when the label itself looks like a mismatched hostname. (ANTS-2119)

  • Audit window AI-review buttons now enforce the same network safety checks as the main AI client (ANTS-2121). The audit window's "AI triage" buttons talk to the AI server over their own connection. They were missing three protections the main AI client already has: refusing internal/cloud-metadata addresses (SSRF), refusing sneaky redirects, and refusing credentials hidden in the URL. All four egress checks now live in one shared validator both paths call, so the audit buttons are guarded identically.

v0.7.100-rc1Pre-release

0.7.100 RC1 — Patron preview

This release shipped without written notes.

v0.7.99

0.7.99

Added

  • feedback_log op:"assign_id" — v2 inline triage write (ANTS-3447) Fills one ### finding's **Proposed ID:** line in a shared *_Ants_MCP_Feedback.md file with the assigned ANTS-NNNN id(s) or an n/a — <reason> closure — the inline replacement for the v1 op:append_tracking table. Locates the finding over the fence-aware ### enumerator (target_not_found / target_ambiguous+candidates), replaces the placeholder/existing line or inserts one when absent, idempotent, atomic, dry_run-previewable. No roadmap read.

  • feedback_log op:"migrate_v2" — one-shot mechanical v1→v2 feedback-file migration. (ANTS-3446) Bumps the version marker to <!-- ants-mcp-feedback: 2 --> and stamps a blank - **Proposed ID:** _(maintainer to assign)_ line on every finding-shaped, below-watermark ### block that lacks one. Leaves the v1 tracking tables in place (no move/collapse), so the v1 watermark and the shipped un-triaged delta are preserved. Reports stamped/orphans/ unclassified; already_v2 no-op on a v2 file; dry_run previews; idempotent, atomic. This is the converter that lets compact_resolved (ANTS-3443) act on the all-v1 corpus.

  • feedback_log op:compact_resolved — collapse a shipped v2 finding's write-up to a roadmap-driven stub (ANTS-3443) Maintainer, v2 files only. Auto-discovers every ### finding and collapses each whose inline **Proposed ID:** id(s) are all ✅ in the live ROADMAP.md to a one-line → shipped ✅ (write-up compacted, ANTS-3443) stub that keeps the heading + the id line — never deletes (the write-up survives in git + under its ANTS-NNNN). Per-finding, idempotent, atomic, dry_run byte report; per-finding skips (no_shippable_id / already_compacted / roadmap_unresolved_ids / has_open_id) in skipped[]. Refuses not_v2 on a v1 file (run migrate_v2 first) and roadmap_unavailable when no ROADMAP.md resolves. Introduces the shared fence-aware ### -block enumerator the pending v2 feedback_query/migrate_v2 verbs will reuse.

  • find_sources emits a redirect hint on an empty result (ANTS-3435) An empty files[] now points at workspace_search / find_definition / find_caller so a symbol-led query is not mistaken for genuine absence.

  • feedback_log op:"prune_tracking" — dedup superseded maintainer tracking rows (ANTS-3442) Removes superseded duplicate maintainer tracking-table rows from the cross-session *_Ants_MCP_Feedback.md files, keeping each id's authoritative last-per-id row plus every heading/header/separator. Two-stage pass preserves mappedIds (a notes-cell-only id pins its row); idempotent, atomic, dry_run preview, optional scope_ids. The deduplication complement to compact_shipped's write-up collapse.

  • *feedback_log: compact-shipped op to trim confirmed-implemented findings in _Ants_MCP_Feedback.md (keep a stub, not a delete). (ANTS-3421) The cross-session feedback files keep growing; once a suggestion is built and the other session confirms it works, its long write-up should shrink to a one-line stub so the file stays small.

Changed

  • feedback_query / feedback_pending honour the v2 marker (marker-aware un-triaged delta) (ANTS-3448) On a <!-- ants-mcp-feedback: 2 --> (or higher) file the un-triaged delta is now the findings whose inline **Proposed ID:** is still unfilled, not the region after the last v1 tracking table. feedback_query gains format_version + suspected_untagged[]; session_orient feedback_pending picks up the v2 count with no code change. The v1 rule is retained verbatim for un-migrated files.

  • changelog_log refuses a feature-grouped [Unreleased] (dated ### topics + **Bold** category runs) with feature_grouped_section instead of inserting a mis-ordered flat category. (ANTS-3416) On changelogs that group notes by feature (newest-first), the writer now stops and asks you to hand-edit, instead of adding an entry at the wrong place that you then have to move.

Fixed

  • prune_tracking wrongly drops distinct id-less closure rows sharing a non-ANTS id-column token (e.g. (schema fix)) — confirmed data loss. (ANTS-3445) The tool that de-duplicates feedback-file bookkeeping rows can delete a real row by mistake when two unrelated rows are labelled the same non-ID tag. Fix it so only real ANTS-IDs count for de-duplication.

  • feedback path derivation tolerates checkout-leaf vs package-name case/separator mismatch (ANTS-3439) A Fin_Break checkout resolves finbreak_Ants_MCP_Feedback.md via a normalized compare; a normalized-equal sibling is floated to candidates[0] without a false all_other_projects.

  • roadmap_branch_drift scans legacy no-ID ants-v1 roadmaps (ANTS-3437) No more false scanned_bullets:0 all-clear; id-less bullets are identified by headline slug and the envelope flags roadmap_format.

  • spec_query / spec_log accept the numeric NN-topic ids that spec list mode emits (ANTS-3436) e.g. id=17-emission-model no longer refuses bad_id; the read surface accepts the identifiers it hands out.

  • read_region bare-name symbol-mode confirmed for two-word-return-type members (ANTS-3434) Verified as a downstream effect of the file_outline drop; a bare method name resolves the qualified member via the existing suffix fallback once it is kept in the outline.

  • file_outline no longer drops out-of-line C++ members with two-word return types (ANTS-3433) unsigned int / long long / unsigned char / const T& return types on a Class::method definition are outlined again (and so resolvable via read_region symbol-mode).

  • roadmap_log op:bundle_row now works end-to-end (ANTS-3432) The bundle_row op (ANTS-1691) advertised cells/header/position/sort_col but never declared them as schema properties, so with additionalProperties:false the MCP client stripped the args and every call refused missing_field. Declared the four properties; the existing handler is now reachable. Regression-locked with a schema source-grep invariant.

  • Bulk debt-sweep defer now warns before dumping a whole scan into the roadmap. (ANTS-3346) The Project Audit "Defer all to ROADMAP" button (and the debt_sweep_defer MCP verb) used to fold every finding of a scan — real, false-positive, and mechanical alike — straight in as tracked items; that is how 1,106 false positives once landed there. Deferring more than 25 findings at once now asks for confirmation in the dialog (pointing you at "Triage with AI" first) and is refused by the MCP verb unless it passes triaged:true. The guard keys on how many findings you defer, not on whether they look auto-fixable — a scan that is mostly auto-fixable can still be mostly false positives.

  • debt_sweep_scan produces unbounded output — transport timeout + token overflow. (ANTS-3345) Running the scan on a real release-sized diff either times out or returns a 130k+ character blob the assistant can't read.

  • General read/write verbs must reach *_Ants_MCP_Feedback.md outside the project root (supersede ANTS-3419 hint-only decision). (ANTS-3430) The shared feedback files live one folder ABOVE every project, so the normal read/edit tools refuse to touch them. Let those tools reach feedback files directly instead of only pointing at the special feedback commands.

  • roadmap_query by-id: max_body_bytes (ANTS-3402) is inert — a single-bullet fetch truncates the body at the 2000 default regardless of the requested cap. (ANTS-3425) Asking the roadmap tool for one item's full text still cuts it off at ~2000 characters even when you ask for more, so the important tail (like a decision paragraph) is lost and you have to read the whole 2 MB file instead.

  • audit_run no longer auto-runs mypy in its deps-less sweep (it emitted 28-31 spurious import-not-found/import-untyped warnings on every full run); mypy stays available on explicit tools:["mypy"]. (ANTS-3418) The built-in bug-scanner stopped falsely reporting dozens of 'missing library' problems on Python projects every time it ran; CI and pre-commit still run the real, fully-installed type check.

v0.7.99-rc1Pre-release

0.7.99 RC1 — Patron preview

This release shipped without written notes.

v0.7.98

0.7.98

Added

  • subsystem op:map accepts an optional name filter (ANTS-3414) Pass name to narrow the returned lane list to subsystems whose name contains that substring (case-insensitive); omit it for the full map. Previously name was ignored on op:map.

  • RoadmapDialog surfaces a duplicate-ID warning banner (ANTS-1694) When the roadmap accidentally carries the same [PROJ-NNNN] id on more than one bullet — which roadmap_query already flags — the cards view now shows a warning banner at the top, using the same canonical-ID predicate as the MCP detector.

  • roadmap_log op:"amend_body" — patch a bullet's body prose in place (ANTS-3406) Locate a bullet by id/anchor/headline, then replace an exact single-line substring of its continuation body (old_text→new_text) with a unique-match guard, dry_run preview, and leaked-XML scrubbing. Works on GFM and ants-v1 roadmaps. Fixes the cross-session gap where correcting a stale phrase in a roadmap entry meant a raw text edit.

  • roadmap_query max_body_bytes raises the body cap for a targeted id/ids fetch (ANTS-3402) A single-bullet / id-set read can now return a large multi-phase epic body (clamp [2000, 16384]); list and section queries stay at the 2000 cap.

  • roadmap_query accepts the granular lifecycle filters planned / in-progress / considered (ANTS-3400) The read verb now takes roadmap_log's own status vocabulary (planned=📋, in-progress=🚧, considered=💭) alongside active/shipped/all, and echoes an accepted list on an unknown value. section_index collapses the granular names to their aggregate.

  • Opt-in async mode for the audit_run MCP verb (ANTS-3396). (ANTS-3396) Pass async:true to start a slow sweep detached and get a job handle back instantly ({job_id, status:"running", poll_with:"audit_poll"}) instead of blocking on the ~60s MCP transport cap; poll the new audit_poll {caller_cwd, job_id} verb for completion (running/done/error/expired). The sync path is unchanged (default async:false). The job registry is bounded (16 entries, 270s reap) and root-scoped so a poll only sees its own project's jobs. Results are written to .audit_cache regardless of poll.

  • roadmap_log append accepts optional evidence:[paths], echoed by roadmap_query (ANTS-3382) op:append / append_batch take an evidence file-path array, rendered as an Evidence: line (dots in paths preserved) and echoed back by roadmap_query as an evidence array — so a bug logged from screenshots/logs records where the files are for a later session.

  • feedback_log / feedback_query: derive the feedback-file path when path is omitted (ANTS-3376) Omit path and the verb derives _Ants_MCP_Feedback.md at the shared root, so a first-time log no longer needs a filesystem hunt; the reply carries path_derived:true. On a not-found, the candidate list now floats your own project's file first, or flags that every candidate belongs to another project. The canonical per-project filename rule is documented in docs/standards/mcp-feedback-files.md (ANTS-3384).

  • feedback_query can now report which of a session's past suggestions shipped. (ANTS-3371) Pass include_tracking:true and the reply carries a compact tracking list (item, roadmap IDs, status, notes) pulled from the maintainer tables — so a contributor session sees what's done without re-reading the whole 100KB+ feedback file.

  • The session-start summary now flags the audit-warning count as stale when the saved audit predates your latest commit. (ANTS-3370) current_state / session_orient now carry open_audit_findings_count_stale, mirroring last_audit_summary's staleness signal — so a session doesn't chase warnings that were already fixed after the audit was cached.

  • Donate menu in the menu bar with GitHub Sponsors and Patreon links A new Donate menu (just left of Help) opens the project's GitHub Sponsors and Patreon pages in your browser, so supporters can find them without leaving the terminal.

  • dry_run preview on the last two mutating Ants-MCP verbs — test_audit_fold_in and debt_sweep_apply_fix (ANTS-2227) Completes dry_run parity across all 9 mutating verbs. test_audit_fold_in previews the would-be ROADMAP IDs + block without bumping the counter or writing; debt_sweep_apply_fix runs every guard and computes the patched source but skips the write, reporting would_apply. Pass dry_run:true to any of them to see the change before it happens.

  • dry_run:true preview extended to the ROADMAP fold-in verbs (ANTS-2227) indie_review_fold_in, cold_eyes_fold_in and debt_sweep_defer now accept dry_run:true, returning the would-be allocated IDs and rendered block without bumping .roadmap-counter or inserting into ROADMAP.md — so Claude can preview a fold-in before committing it. Backed by a new non-mutating RoadmapFoldIn::peekIds primitive that mirrors allocateIds' ID math.

  • dry_run:true preview on four more mutating Ants MCP verbs (ANTS-2227) apply_edits, project_settings, feedback_log and audit_falsepos_log now accept dry_run:true, returning the would-be result (carrying dry_run:true) without writing to disk — so Claude can pre-flight a multi-file edit (catching a no-match before any partial write) or any of these appends before committing. Joins roadmap_log / changelog_log / spec_log, which already supported it. Each preview shares the verb's real write code so it can't drift from the actual result.

  • Screen-reader support for the terminal area (AT-SPI / Orca). (ANTS-1078) The terminal's content is now readable by assistive technology. A QAccessibleInterface adapter (role Terminal) with a text interface exposes the visible viewport as plain text, reports the caret, and fires a throttled change event as output arrives — so a blind user's screen reader can read the terminal, not just the window chrome. Costs nothing when no screen reader is running. Core slice of the H9 accessibility bundle; richer text attributes, command-boundary batching, and selection write-back are tracked separately (ANTS-3363). Also resolves the terminal-a11y gap in ANTS-2205 (4).

  • Tabular (columnar) encoding for homogeneous-array MCP replies (ANTS-2090). Opt-in encoding:"tabular" on the list-shaped read verbs (roadmap_query, workspace_search, file_outline, find_sources, find_caller, codebase_index, docs_index) packs each top-level array-of-objects into a columnar {cols,rows} form — the per-row keys are dropped, so big list replies are 30–60% smaller. Self-guarding per array; never made larger than plain JSON; refusals/304s untouched. Decode by zipping cols with each rows row (a null cell = key absent on that element).

  • Ants MCP: project_settings op:detect no longer ranks a bundled-dependency tree (e.g. mingw-deps/, third_party/) as a project's source root — vendored dirs are discounted from the suggestion and the file total. (ANTS-3357)

  • Ants MCP: spec_query with no id/path now lists the specs directory ({mode:"list", specs, count}) instead of erroring — spec discovery without a shell ls. (ANTS-3360)

  • Ants MCP: spec_query / spec_log accept any <PREFIX>-NNNN spec id (e.g. DOOM-0009), not just ANTS-NNNN, and resolve topic-suffixed spec files (DOOM-0009-path-tracer.md) from the bare id. (ANTS-3356) Generalised the spec id routing the same way roadmap_log was (ANTS-2076); path stays the explicit override for non-standard layouts.

  • MCP dispatch silently ignores unknown / misspelled args — echo an ignored_args advisory. (ANTS-2175) If Claude passes a setting name a tool doesn't recognise (a typo, or an option that doesn't exist), the tool quietly ignores it and can return wrong-looking results. It should name the inputs it ignored so the mistake is caught on the first call instead of after several.

  • Implement INV-13 scroll-position persistence (roadmapdialog). (ANTS-1264) the Roadmap dialog is supposed to remember where

  • read_regions MCP verb — batched multi-selector read (read-side mirror of apply_edits). (ANTS-2219) Let Claude fetch several code snippets in one request instead of one call each — fewer round-trips, fewer tokens.

  • Surface feedback_query / feedback_log to contributor CC sessions (self-advertising banner). (ANTS-2226) Tell the other projects' Claude sessions they can read and add to these feedback files through Ants directly, instead of editing them by hand.

  • read_region markdown section selector — read a heading's body by slug. (ANTS-2221) Let Claude ask for one section of a doc by name instead of working out its line numbers first.

  • workspace_search enclosing_symbol — annotate each match with the function it lives in (ANTS-2220). Opt-in enclosing_symbol:true adds enclosing:"Foo::bar" to each match (the nearest-preceding symbol from the file outline), folding the usual "which function is this in?" follow-up into the search. One outline scan per distinct matched file, cached; off by default. (DOOM_Ants feedback S2.)

  • file_outline multi-path — outline several related files in one call (ANTS-2223). Pass paths:[...] instead of path to outline a header + its impl + a consumer in a single MCP round-trip; returns a files:[{path, symbols, etag, …}] array, each entry resolved and byte-capped independently. An optional etags map ({relPath: priorEtag}) 304s any unchanged file to a compact stub, so re-outlining a set after editing one file re-sends only the changed bodies. Single-path callers are unaffected. (DOOM_Ants feedback S5.)

  • workspace_search now emits a regex_advisory when a regex:true alternation contains very short un-anchored terms (e.g. tan) that substring-match inside longer words, flooding results. (ANTS-2181)

  • Claude can now ask Ants to compute an answer across your project's files and get back just the result — not the file text — via a new sandboxed project_query MCP verb. (ANTS-2093) The "code-execution" token-saver: Claude sends a small read-only Lua snippet (with project.read/list/root + string/table/math/utf8), Ants runs it safely over the project and returns only what it computes — a count, a filtered list — instead of reading every file into the conversation. Fully sandboxed: read-only, confined to the project directory, no network/exec/write, and memory/time/output-capped on a worker thread so a runaway snippet can never freeze the app. On by default (Settings → General → "Let Ants run read-only project queries for Claude"); compiled out in builds without the Lua subsystem.

  • Master on/off switch for the Ants MCP integration (ANTS-1901) (ANTS-1901) Settings → General → "Enable Ants MCP integration" (on by default) is a single master switch for the whole Ants ↔ Claude Code helper. Turn it off and Ants stops the integration: the MCP socket isn't bound at next launch, the start-of-session cheat-sheet is removed, the auto model switcher stands down, and every MCP tool call is refused with a mcp_disabled message. Turning it back on takes effect on the next Ants launch; turning it off is honoured immediately. Every dependent toggle (auto-switcher, cheat-sheet, lean replies) greys out when the master is off. Plain terms: one switch to fully turn the Ants–Claude integration on or off.

  • Added an out-of-cadence hotfix path, cut-rc.sh hotfix. (ANTS-2165) Two phases around a /bump that ship the latest public release plus a cherry-picked fix as the next public patch (and roll any in-flight RC up one number), for an urgent bug that can't wait for the next Wednesday.

Changed

  • Roadmap dialog: items now lay out in aligned columns instead of a cramped inline run. (ANTS-3392) Each roadmap item's status, type, summary and ID line up in neat columns (the dialog renders each section as a table) instead of running together into a wall of text, and section-heading counts no longer fuse into the section title.

  • roadmap_query MCP provider now forwards args verbatim (rcDelegate), retiring the hand-maintained per-arg allowlist that silently dropped new options. (ANTS-3422) The mainwindow provider that bridges the roadmap_query MCP verb to its handler used to copy each option across the boundary by hand; forgetting a line silently disabled that option (it happened five times). It now forwards the whole request, so every current and future option reaches the handler automatically.

  • Read/write MCP verbs point you at the right tool for a feedback-file path (ANTS-3419) A bad_path refusal on a *_Ants_MCP_Feedback.md path (which lives above the project root) now carries a hint redirecting to feedback_query / feedback_log — the verbs that do serve it — instead of a bare error.

  • find_sources accepts symbol as an alias for topic (ANTS-3415) The 'who uses X' verb now takes symbol (filled in only when topic is absent), so copying an identifier between find_sources and find_caller no longer fails the first time.

  • tools/ci-parity.sh now mirrors every locally-reproducible CI gate (ANTS-3410) Added --lints (cppcheck / appstream / desktop / groff / version-drift / bash+zsh+fish completions), --asan (Debug ASan/UBSan build + sanitized ctest + --version/--help smoke) and --full phases. Gates accumulate and report all failures at the end; a gate whose tool is absent is skipped with a loud warning so a local "green" never masks a gate CI still runs. The qt62-baseline job is intentionally not mirrored (needs a different distro + Qt). Motivated by ANTS-3391's wire-budget overrun staying CI-red for three commits before ANTS-3409 caught it.

  • Document the Evidence: bullet field in roadmap-format.md § 3.5 (ANTS-3382 follow-up). (ANTS-3386) The new Evidence line on roadmap items works and is documented in the live tool schema, but the human-facing roadmap-format standard doesn't list it yet.

  • roadmap_log documents that flip/annotate resolve top-level bullets only (ANTS-3403) Nested phase sub-bullets inside a narrator bullet's body are out of scope for the locators and need a direct Edit; the op descriptor now states this.

  • changelog_log malformed-section advisory now names the alternative insert path (ANTS-3401) When a feature-grouped [Unreleased] layout is detected, the advisory suggests op:add_from_roadmap into a named subsection or a hand-edit.

  • roadmap_log counter strategy auto-creates .roadmap-counter at 0 on a greenfield roadmap instead of refusing (ANTS-3397) A counter-strategy op:append / op:append_batch on a roadmap that has no .roadmap-counter AND no existing bullet ids now auto-initialises the counter at 0 and proceeds (first id allocates as -0001), rather than refusing with counter_missing and forcing a shell echo 0 > .roadmap-counter side-step that broke the all-MCP workflow. A roadmap that already carries ids but lost its counter file still refuses (counter_missing) — re-allocating from 0 would mint duplicate ids. Reported by the Contact_List CC session.

  • project_settings op:detect now suggests all first-party source dirs on a non-standard layout, always explains itself, and echoes the roots/dirs it resolved (ANTS-3369). Cross-session feedback from four projects: the layout auto-detector gave up silently on projects that don't use a plain src/ folder (it required one dominant subdir to cover ~90% of the code, so a spread layout or a small entry-point dir like app/ got nothing). It now suggests every first-party source subdir on a miss, always returns a non-empty reason, and adds would_use_roots (the roots already in effect) + excluded (the vendored/build dirs it skipped) so the output is never an opaque set of zeros.

  • read_region / read_regions / file_outline tool descriptions now warn that an MCP read does not satisfy the native Edit read-precondition (ANTS-3383). Cross-session feedback: reading a file via these verbs and then editing it fails the harness's "File has not been read yet" check, so a native Read is still required first. Documented inline in the tool help so sessions stop paying for a doubled read.

  • *feedback_query / feedback_log not_found returns sibling _Ants_MCP_Feedback.md candidates. (ANTS-3366) When Ants can't find a feedback file at the guessed name, it now lists the real ones nearby so Claude doesn't have to shell out to find them.

  • git_state defaults op to "status" when omitted. (ANTS-3365) Asking Ants for git status in one call now just works without naming the sub-command.

  • De-duplicate the KWin scripting chain in moveViaKWin / centerWindow into a shared runKWinScript helper (ANTS-2205 7b) (ANTS-2205)

  • read_regions discoverability — surface the batch reader in the SessionStart prelude + cross-link it from read_region. (ANTS-3359) When Claude needs several chunks of a big file, the cheaper "read them all in one go" tool wasn't visible up-front, so it fell back to the slower built-in reader. Make that batch reader easy to find.

  • Review-dialog and clipboard/AI size caps: honest unit naming + de-duplicated truncation tail (ANTS-2205) Renamed the clipboard and AI-command length caps from …MaxBytes to …MaxChars so the name matches what they measure (QString length units, not bytes — they never changed behaviour). Pulled the review dialogs' shared "clip-to-cap then close any open code fence" step into one BriefDispatch::clipToCapClosingFence helper and lifted the 200 KiB prompt cap to the shared base, removing three copies. Internal tidy-up; no user-visible change.

  • Extract ClaudeTranscriptWalker (claude-trackers). (ANTS-1261) the two task-list trackers have ended up as

  • cppcheck cleanup: default member initializers on 32 struct fields + 2 const-ref getters. (ANTS-3341) An /audit run flagged a batch of small code-quality warnings: several data structures had number/flag fields with no default value (a latent source of "uninitialised value" bugs if one were ever used before being filled), and two small accessor functions copied a string instead of handing back a reference. Gave every flagged field a sensible default (0 / false / the documented default) and switched the two accessors to return by reference. Verified with cppcheck that the warnings are gone and all 2314 tests still pass.

  • read_region section selector: forgiving match for a heading's short title when it carries a trailing parenthetical (ANTS-2221 follow-up). (ANTS-2234) When a Claude session wants to read one section of a spec, it naturally types the short heading title (e.g. "7. Build order"). But many headings have a descriptive bit in brackets after them ("7. Build order (cheapest-first; ...)"), and right now the short title doesn't match — the session has to type the whole thing or fall back to line numbers. This makes the section reader accept the short title as long as it unambiguously points at one heading.

  • Link with the mold linker when available (-fuse-ld=mold, auto-detected, opt-out). (ANTS-2233) Switched the build's final "linking" step to a much faster linker called mold (it was already installed but unused). Linking is the slowest, most memory-hungry part of the build, and mold is both faster and lighter on memory — so the build finishes sooner without risking the machine hanging. Falls back to the old linker automatically if mold isn't installed.

  • Parallelise the ctest presets (-j4 default/fast, -j2 workstation; debug/perf stay serial). (ANTS-2231) Running the whole test suite used to take about 78 seconds because the tests ran one after another. They now run 4 at a time, finishing in about 19 seconds — roughly 4× faster — capped at 4 so it won't bog the machine down.

  • Regression test for the per-language lineIsCode lexer (ANTS-1270 follow-up). (ANTS-2210) Add an automated test proving the audit pipeline correctly treats # comments (Python/shell) and Lua --/[[ ]] as comments, so the ANTS-1270 fix can't silently regress.

  • read_region symbol-mode returns a struct/class/union's full body (ANTS-2222). Asking for an aggregate by name used to return only its declaration line (the flat outline ends it at the first member); it now brace-matches to the closing brace and returns the whole body. A namespace (also tagged kind class) still resolves to its head — use line mode. From DOOM_Ants cross-session MCP feedback.

  • Roadmap dialog (Cozy density, the default) — the status / kind / section-count labels are now 12px (was 10–11px), keeping a readable Compact (11px) < Cozy (12px) < Comfortable (13px) ladder. (ANTS-2211)

  • Roadmap dialog (Compact density) — the smallest text (status / kind / section-count labels) is now 11px instead of 9px, for readability. (ANTS-2211)

  • Status bar stops polling while the Ants window is in the background — saves battery and scheduler wakeups on laptops. (ANTS-1363)

  • computeConfidence moved to AuditEngine so non-GUI consumers score findings without Qt6::Widgets. (ANTS-1262) The confidence formula was a pure data-transform stranded in the GUI dialog. It now lives in AuditEngine::computeConfidence (single source of truth); AuditDialog::computeConfidence forwards, so call-sites and the public static surface are unchanged.

  • Codebase/docs index refresh now scans the project tree once per call instead of two-to-three times. (ANTS-2198)

  • Roadmap pagination cut-point search is now O(n) instead of O(n log n) (ANTS-2200) Each list element is serialized once and accumulated, rather than re-serializing a growing prefix on every binary-search probe. Same output, less work on long lists.

  • MCP read responses now surface the etag re-use hint (next_call_hint) regardless of body size — repeated small read_region / file_outline slices can now 304 their re-reads. The 4 KiB gate now applies only to the leaner_call_hint. (ANTS-2180)

  • Ants MCP now offloads oversized read replies to a short preview + pointer by default Large MCP read results (roadmap_query, workspace_search, get_scrollback, …) are saved to a scratch file and returned as a short preview plus a handle, so a Claude session spends a few tokens instead of thousands and fetches the rest (via read_spill) only if it needs it. Shipped switched off in the prior release; now on by default per the "token-savers default ON" rule, and it fails open — if the scratch file can't be written, the full reply is sent as normal. A new Settings → General toggle ("Offload huge Ants MCP replies to a preview + pointer") turns it off. (ANTS-2094 fast-follow)

  • Hardened the weekly RC→release→new-RC cadence into one guarded cut-rc.sh cycle. (ANTS-2164) new-rc refuses an empty RC and auto-rolls [Unreleased]; promote refuses an empty/placeholder or stale (>14-day) RC and auto-date-stamps the CHANGELOG/metainfo/debian release notes; version-drift is now a hard gate. Stops the recurring empty-RC / half-finished-release class of incident.

Removed

  • Deleted the decayed claude_statusbar_extraction source-grep test (ANTS-1381). It froze the 0.7.74 ClaudeStatusBarController extraction with byte-for-byte signature/connect-site/LoC greps; the controller's behaviour is covered at runtime by the model_near_miss_ledger and model_auto_switch_outcome_fillin tests. Investigation found the other five "extraction" tests in the same roadmap item are NOT decayed (two carry live runtime unit tests; three are the sole guard for their behaviour) and were kept.

  • Deleted the dead IndieReviewEngine::assembleBrief zombie — a latent prompt-injection trap (ANTS-2187). The unfenced v1 brief assembler had no production caller (only doc comments + one feature test) yet inlined raw source into an LLM prompt with no fence-hardening, and its name read like the main entry point — so a future rewire would have reopened a prompt-injection hole. The live path is assembleBriefForDispatch (fence-hardened via BriefDispatch::fenceBody) / assembleBriefManifest, both already covered by their own tests. Removed the function, its declaration, the stale header contract comments, and the orphaned Inv3 shape test.

Fixed

  • roadmap_log / changelog_log output is now pre-commit-clean (ANTS-3417) Emitted continuation lines are right-stripped, so the ubiquitous trim-trailing-whitespace pre-commit hook no longer rewrites the file and aborts the commit after a roadmap/changelog append.

  • file_outline no longer drops methods with std::function<void()> params or inline const accessors (ANTS-3412) The C++ scanner's argument matcher stopped at the first ), so a parameter whose type carried inner parentheses (e.g. std::function<void()>) truncated the match and the whole method vanished from the outline; and a trailing const/noexcept on a one-line accessor (int workerCount() const { ... }) also hid it. Both are now recognised.

  • roadmap_query's query keyword filter now actually filters (ANTS-3420) The keyword search shipped in a prior release but was silently inert: the MCP dispatch layer never forwarded the query argument to the handler, so a search returned the whole roadmap instead of the matching items. The same audit fixed three sibling arguments that were also being dropped over MCP — max_body_bytes (large-body id fetch) and the per-section ETag args (include_section_etags, section_etag_match).

  • file_outline now detects extern "C" function definitions (ANTS-3351) (ANTS-3351) The C++ outliner skipped any function with an extern "C" linkage prefix, so its interior local variables were mislabelled as top-level functions. Reported against DOOM's r_vulkan.cpp (the RB_Vulkan_* entry points were missing and devs/exts locals showed up as functions).

  • RoadmapDialog section chips now roll up descendant counts, matching the MCP (ANTS-1693) Parent (level-2) section chips previously showed only their direct bullets, so they could disagree with roadmap_query's section_index active_count for the same slug. RoadmapIndex::rollupCounts is now a shared template that both the dialog and the MCP use, so a parent chip sums self + descendants.

  • roadmap_query MCP one-line description back under its 800 B wire budget (ANTS-3409) Trimmed the redundant bundles mode gloss so the reconstructed wire description is 790 B (was 814), restoring the mcp_tool_detail_field INV-5 budget. Full per-op detail is unchanged — still served via tool_info {name:"roadmap_query"}.

  • roadmap_query granular status filters now work without a section arg (ANTS-3408) status="planned" / "in-progress" / "considered" silently returned 0 on the full-file (no-section) query path — ANTS-3400 added those arms to the section= branch only. The full-file predicate now mirrors them. Root cause was section-path vs full-file-path divergence, not the roadmap format. Reported by the Contact_List CC session.

  • read_region call_sequence — suppress false-positive callees from comments and type constructors. (ANTS-3379) The 'list the steps in this function' feature sometimes lists words from comments or type names as if they were function calls.

  • similar_code — down-weight trivial one-line/one-token signatures in ranking. (ANTS-3380) The 'find code like this' search sometimes puts a tiny irrelevant one-liner at the top.

  • roadmap_query id/ids + roadmap_log flip/annotate return a silent miss (not bad_id_format) for a format-nonconforming bracket token that exists in the file. (ANTS-3387) When a project uses an ID style Ants doesn't recognise, looking it up says 'not found' with no hint that the ID shape is the problem — so it looks like the item vanished.

  • token_usage now credits the read/search verbs for the tokens they save (ANTS-3361) file_outline, read_region(s), workspace_search, codebase_index, find_definition/sources/caller and build_status previously reported ~0 tokens saved despite replacing a full-file Read or a grep. They now carry conservative per-call baselines (deliberately under-estimated and floored at 0 so the headline never over-claims).

  • workspace_search returns a soft rg_failed envelope instead of a raw transport timeout on huge match streams (ANTS-3405) The JSON parse loop is now bounded by the wall budget (not just the rg process), so a whole-repo query over large data blobs no longer blows the client's transport timer with an un-catchable -32000.

  • file_outline now emits Python class methods as Class.method (ANTS-3404) The Python outliner tracks enclosing classes by indentation, so read_region symbol mode can address a method (e.g. PlayQueue.next); top-level defs/classes stay bare.

  • file_outline no longer glues a namespace-qualified C++ return type onto the symbol name (ANTS-3399) A method like JPH::BodyID PhysicsWorld::createStaticBody(...) now outlines as PhysicsWorld::createStaticBody (was the whole return-type-plus-name), so read_region symbol mode resolves it. read_region also gained an unambiguous qualified-suffix fallback so a bare method name resolves its qualified entry.

  • audit_run no longer turns a tool's progress bar or fatal error into a fake finding (ANTS-3395) JSON-emitting tools (ruff, bandit, semgrep, gitleaks, trivy, shellcheck) are now parsed strictly as JSON, so bandit's progress bar and trivy's FATAL log line stop being counted as findings. A trivy scan that aborts is reported as a crashed/incomplete tool instead of a single misleading finding.

  • audit_run scope:"full" now skips build-output, vendored, and cache dirs (ANTS-3394) The default exclusion set (logs, data, database, node_modules, dist, build, _build, pycache, .venv, venv, env) is now passed to each whole-tree audit tool (ruff, bandit, semgrep, trivy, mypy), so a full-project sweep no longer drowns real findings in build output or downloaded media. Scoped sweeps (an explicit file list) are unaffected.

  • codebase_index + project_settings no longer index a project's committed virtualenv (ANTS-3393) On a flat-layout project with a committed venv/, codebase_index (under source_roots=["."]) and project_settings op:detect both treated the vendored virtualenv as the project's source — indexing/suggesting thousands of library files instead of the ~10 real ones. The shared isNoiseDir skip-list now covers bare venv/env/pycache (.venv was already skipped), and codebase_index's walk applies the same skip-list, so vendored / build / virtualenv trees are pruned even when "." is an explicit source root.

  • roadmap_log op:flip locates GFM bullets by the canonical headline roadmap_query reports (ANTS-3378) Flipping a GFM task-list bullet by headline now matches the de-marked-up bold span / post-em-dash prose / bold-ID label (the token roadmap_query echoes), not just the raw stored head — so the exact title copied from roadmap_query no longer returns bullet_not_found. On a genuine miss, suggestions rank by token overlap against the canonical headline and carry the bullet line.

  • Audit summaries no longer rank a scanner's progress-bar text as the top finding. (ANTS-3372) Junk lines scraped from tool output (e.g. a "Working... 100%" progress bar) are now filtered out of every audit-summary parser before findings are ranked, so a real issue lands at the top instead of terminal noise.

  • Ants MCP read verbs no longer return doctored bytes for frame-sensitive source (ANTS-2218) read_region / read_regions / workspace_search now accept raw:true, returning file content byte-for-byte inside an unforgeable nonce frame instead of the default lossy scrub that neutralises literal </ants_mcp_data> close-tags and <!--/--> comment markers. Without it, an agent reading a file that itself contains those tokens (the MCP source, a spec, HTML/markdown with comments) and building an Edit/apply_edits from the output would corrupt the file. The default (scrub) framing is unchanged, so the ANTS-1294/1670/1996 breakout protections are untouched.

  • MCP audit_run description documents the client transport-cap + last_audit_summary fallback for long sweeps (ANTS-2183) (ANTS-2183) A full-tree sweep can exceed the MCP client's ~60 s request timer (Claude Code's), surfacing transport: timed out outside the response even though the run completes server-side and writes its SARIF. The verb description now states this and points the caller at last_audit_summary (or the envelope's cache_path) to read the result, mirroring the two-tier-timeout note verify_changes already carries.

  • MCP audit_run now resolves the project's compile_commands.json for the C/C++ tools (ANTS-2182) (ANTS-2182) cppcheck full runs drive off --project=<db> (plus the missing-include suppress set as a no-DB fallback), and clazy/clang-tidy resolve their build dir via the new shared AuditEngine::resolveCompileCommands probe (build/, build-fast/, build-asan/, …) instead of a hardcoded build/ path. Fixes the ~2330-finding missingIncludeSystem/namespace-as-C noise flood and clazy's 0-findings result when the compile DB lived in a non-build/ tree — the MCP audit now matches the in-app AuditDialog / ants-audit CLI signal.

  • Fix -Wshadow in test_roadmap_viewer_archive.cpp — inner QTemporaryDir tmp shadows outer. (ANTS-2176) A test file declares the same temporary-folder variable name twice (one inside the other), which the compiler warns about. Harmless today, but warnings should be clean — rename the inner one.

  • cppcheck uninitMemberVarNoCtor residue in tests/ — 10 test-only helper-struct members lack default-member-initialisers. (ANTS-3354) Added {} default-member-initialisers to 10 cppcheck-flagged members across 6 test-helper structs (BenchResult, Fixture, Entry, PositiveCase, NegativeCase, Cand, Site). Completes the uninitMemberVarNoCtor cleanup started in ANTS-2184 — our code (src/ + tests/) is now fully clean of the warning; only third-party googletest remains. Test-only, no behaviour change.

  • cppcheck uninitMemberVarNoCtor brace-init hygiene sweep — ~52 helper-struct members in src/ lack a default-member-initializer (e.g. Hit::score, InlineImage::row/col, UrlSpan/PaintTextRun fields, RunOut::started/timedOut/crashed). (ANTS-2184) Added {} default-member-initialisers to 11 cppcheck-flagged scalar/pointer members across 8 helper structs in 7 src/ files (Marker, Hit, SpecToken, ChangelogBullet, PendingTypedef, Head, Frame, StatusHit). Forecloses the uninitMemberVarNoCtor warning class in src/; no behaviour change (all were assigned before use). Verified: 0 findings post-fix, clean build, 242 touched-subsystem tests pass.

  • Ants MCP, from DOOM cross-session feedback: read_region/read_regions symbol-mode now returns a symbol's definition rather than its forward declaration; roadmap_log/changelog_log resolve ROADMAP/CHANGELOG from a project subdirectory (walk up to the git repo root) like the read verbs; session_orient's project_settings_suggestion gained a next_step nudge. ANTS-3349 / ANTS-3350 / ANTS-3352. A residual file_outline most-vexing-parse mis-tag (ANTS-3351) remains deferred — its root cause is an undetected enclosing function, same class as ANTS-2159.

  • Debt-sweep detector false positives: brace-less if/else/for bodies no longer flagged as dead branches; Q_UNUSED of Qt-typedef params and in comments no longer flagged as orphans; shell (test_*.sh/.bash) tests now count toward invariant coverage. Also removed two duplicate #includes in mainwindow.cpp. Deeper detector-scope issues (shipped_without_commit windowing, missing_inv_test behavioural matching, fixture-dir exclusion, scan output capping) deferred to ANTS-3342..3347.

  • C-style // and /* comment introducers aren't excluded from lineHasCode's code-detection (ANTS-1270 left them; the #/-- analogue). (ANTS-2230) The audit code that decides "is this line real code or just a comment?" was taught (in an earlier fix) that # (Python/shell) and -- (Lua) start comments, so findings inside those comments get dropped. But for C/C++ it was never taught the same about // and /* — so a secret or TODO written inside a // comment in a C++ file is still reported as a real finding. This makes the audit cry wolf on commented-out lines.

  • file_outline registers typedef struct TAG_s { … } ALIAS_t; aggregates (the dominant C struct idiom). (ANTS-2228) Let Claude pull one C struct definition by name instead of reading the whole header to find it.

  • roadmap_query section=<slug> misses #### Pass N.M bullets nested under a prose-shaped ###. (ANTS-2225) When Claude asks for the to-do items in one section, don't silently skip the ones tucked under a mostly-text heading.

  • find_definition include_body / read_region symbol-mode — brace-match function bodies so a missing outline boundary can't over-read. (ANTS-2224) Stop Claude's 'show me this function' from accidentally grabbing the next function too.

  • SessionStart MCP catalog now tells you to call each verb by its full mcp__ants__<name> — the bare names it listed weren't directly callable. (ANTS-2213)

  • file_outline / read_region now capture C/C++ functions whose parameter list wraps across multiple lines (id-Software / K&R prototypes) — previously dropped. (ANTS-2212)

  • Roadmap dialog now has a deterministic keyboard tab order through its filters and controls. (ANTS-1350)

  • Audit summary reports an honest suppressed-finding count for semgrep runs instead of always zero. (ANTS-2123)

  • Debug Mode category selection is remembered across restarts instead of resetting to off every launch. (ANTS-1863)

  • Subsystem recent-changes no longer freezes the UI on slow git — the per-file git calls are now one batched invocation. (ANTS-1340)

  • Per-tab image memory budget no longer double-counts shared image buffers. (ANTS-1265) A Kitty T stores an image in m_kittyImages and pushes a COW copy into m_inlineImages; recomputeImageBudget summed both, over-counting real RAM ~2x and rejecting legitimate later transfers. It now dedups by QImage::constBits() so a shared buffer counts once.

  • Audit path rules now match findings reported with absolute paths. (ANTS-1271) applyPathRules matched project-relative globs against Finding::file, so absolute-path findings (clang-tidy/semgrep/mypy) silently no-op'd the rules. Finding::file is now normalised to project-relative before matching.

  • Audit pipeline: comment/string detection is now language-aware (#, Lua --/[[ ]], Python triple-quotes). (ANTS-1270) lineIsCode applied a C-style lexer to every file, so # TODO: "10.0.0.1" in a Python/shell file scanned as code and its IP/secret/TODO finding survived. The lexer now dispatches on file extension and the bare comment-introducer no longer reads as code.

  • Roadmap dialog: the document preamble (intro before the first section) now shows on the Full preset. (ANTS-1275) renderCardsHtml gated preamble prose on sectionExpanded, which is still false before the first ## heading, silently dropping the document intro. The collapse gate now applies only to section-intro prose.

  • Settings reports Claude hooks as installed only when all five hook events are present (previously it could show green with two missing); failures to lock down the Claude settings-file permissions are now logged instead of ignored. (ANTS-2205)

  • With two Claude tabs open, a permission prompt arriving during the brief tab-switch window is no longer attributed to the wrong tab. (ANTS-2190)

  • Audit history file is now guarded against two Ants/Claude sessions auditing the same project at once (no lost history or orphaned reports). (ANTS-2189)

  • Wedged project-query workers are now bounded and logged instead of leaking silently (ANTS-2194) A hung query snippet that can't be killed parks its worker thread; past a hard ceiling Ants refuses new background queries (and logs the count) so a repeating hang can't slowly exhaust memory unnoticed.

  • Shell launch resolves HOME before forking for signal-safety (ANTS-2204) The child's working-directory fallback no longer calls getenv() in the fork/exec window; HOME is captured pre-fork like the other launch parameters.

  • Remote-control socket gained the same re-entrancy guard as the MCP socket (ANTS-2202) A latch + consumed-line clear prevent a command from being dispatched twice should a future verb pump a nested event loop. Also corrected a misleading code comment.

  • Fold-in verbs no longer burn roadmap ID numbers when there's nothing to write under (ANTS-2201) indie_review_fold_in / debt_sweep_defer / cold_eyes_fold_in now check for an active release heading before reserving IDs, so a missing heading returns a clean no_release_heading refusal instead of consuming counter values into a permanent gap.

  • Model-switch banner detection hardened against a quoted banner in scrollback (ANTS-2197) directModelSwitchVisible now requires the tier title and the banner's 'saved as your default' tail together, so a bare quoted fragment no longer trips a continuation.

  • Model-switch ledger now hard-bounded even when outcomes never settle (ANTS-2196) A secondary ceiling (4x the soft cap) drops the oldest record regardless of pending state, so a run of never-settling pending outcomes can't grow the file unbounded.

  • Model auto-switcher: a /model typed while idle no longer starts an unrequested billable turn (ANTS-2186) The direct-switch continuation now gates on an active turn, matching every sibling path. Pre-picking a model at idle is not consent to resume work.

  • Refuse truncated DCS/APC image payloads; correct the AI command-preview byte warning. A DCS (Sixel) or APC (Kitty/iTerm2) payload that overflowed the parser's 10 MiB accumulator is now refused outright instead of being decoded from a corrupt prefix (ANTS-1663 was previously honoured only for OSC 8/52). The AI command-confirmation dialog's "N additional bytes will be executed" warning is now accurate (and never negative) for commands containing HTML-special characters.

  • roadmap_log no longer reissues a live roadmap ID when .roadmap-counter lags the file. Both op:append and op:append_batch now reconcile the counter against the highest existing [PREFIX-NNNN] id, skip past it, self-heal the counter, and surface counter_advanced_to; an id_hint that collides with a live id is refused id_taken instead of overwriting a bullet. (ANTS-2179)

  • Flatpak installs now reach Ants MCP from Claude sessions (ANTS-1900) (ANTS-1900) Inside a Flatpak sandbox the user's shell runs on the host via flatpak-spawn --host, which does not inherit the caller's environment — so the ANTS_MCP_SOCKET path that lets a Claude Code session find Ants's MCP helper never crossed the boundary. The SessionStart cheat-sheet (and every socket-gated MCP affordance) was therefore silent in Flatpak installs. src/ptyhandler.cpp now forwards --env=ANTS_MCP_SOCKET=<path> on the flatpak-spawn argv, gated on a non-empty value (omitted when MCP is disabled). Plain terms: install Ants Terminal as a Flatpak and Claude can now use the cheaper Ants tools just like a native install.

Security

  • concurrent_writer_lock predictable /tmp/ants-cwl-<pid>-<time>.dat + symlink-attack exposure. (ANTS-1380) concurrent_writer_lock test now roots its lock file in a private QTemporaryDir (0700, random name) instead of a predictable /tmp/ants-cwl--.dat, with O_NOFOLLOW on the lock open — closes a symlink-attack window on shared /tmp. Test-only; production ConfigWriteLock (user-private config dir) was never exposed.

  • --remote client now has an overall read deadline (slow-loris guard) (ANTS-1671). The per-iteration 2 s timeout on the --remote client read loop reset every iteration, so a hostile same-machine peer that won $ANTS_REMOTE_SOCKET could dribble bytes under the 1 MiB cap and hang the client forever. A 10 s cumulative deadline now bounds the total wait.

  • Untrusted project path from a Claude transcript is now validated before use (ANTS-1670). The cwd read from a (attacker-influenceable) .jsonl transcript is gated through a new absolute-path / no-..-traversal / no-NUL check before it becomes the working directory used to launch or resume a claude process; a bad value falls back to the safe path decoder.

  • MCP data-wrapper now neutralises a spoofed open tag, not just the close tag (ANTS-1670). wrapMcpData already scrubbed a literal </ants_mcp_data> (and case/whitespace variants) from payloads; it now scrubs the symmetric <ants_mcp_data …> open form too, so attacker-controlled scrollback / commit text can no longer spoof a nested wrapper-open and desync the frame a consuming model sees.

  • Plugin loader refuses a second plugin that resolves to an already-loaded name, preventing a silent overwrite via symlinked directories. (ANTS-1370)

  • Flatpak shell launch now runs flatpak-spawn with the same pre-fork sanitised environment as the direct path (consistent terminal-variable handling across the sandbox boundary). (ANTS-1994)

  • workspace_search rejects a leading ! in glob (negation that resurrects ignored trees). (ANTS-1274) ripgrep treats a !-prefixed --glob as a negation whose precedence is above .gitignore, so !.git/!node_modules/** could un-exclude an ignored directory. The glob validator now rejects a leading ! (the only resurrection vector) alongside the existing ../256-byte guards; legitimate brace/charclass globs are unaffected.

  • The "which Claude session is current" check no longer trusts a file's modified-time when a live Claude process is known, closing a same-user spoofing gap. (ANTS-2191)

  • project_query's file listing no longer discloses names of files symlinked outside the project (ANTS-2203) project.list now re-validates each entry against the project root, so a symlink whose target escapes is skipped entirely — matching project.read, which already refuses to read it.

  • debt_sweep now rejects a malformed since revision instead of passing it to git (ANTS-2199) The user-supplied git ref is shape-checked before it reaches the git diff argv, giving a clean error instead of a confusing mid-scan git failure.

  • Model auto-switcher: code-level parked guard so a config bug can't re-arm keystroke injection (ANTS-2195) The auto-switcher's only actuator is keystroke injection and is deliberately parked as unsafe. A new compile-time guard (kAutoSwitchActuatorParked) short-circuits the firing site before the /model send, so the parked decision can no longer be undone by config alone.

  • Terminal query-responses are now rate-limited to bound a \e[6n-flood amplification (ANTS-2192). DA/CPR/DSR/DECRQSS/colour/Kitty query replies fired an unconditional PTY write per request, so a program streaming a query in a tight loop forced unbounded response writes. All replies now funnel through TerminalGrid::sendQueryResponse(), a rolling 1 s cap (256/sec) mirroring the OSC 52 quota — a flood is bounded while a legitimate synchronous query is never throttled. Response content was already fixed/derived; this bounds volume only. Regression-locked by query_response_ratelimit (INV-1/2/3).

  • audit_run scoped positionals are now guarded against argv option-injection (ANTS-2185). A scoped file path beginning with - (e.g. a hostile-clone file named -rf.cpp) was appended bare to each tool's argv and parsed as a flag. toolArgv now ./-prefixes dash-leading relative paths at a single chokepoint so they are unambiguously paths; absolute and ordinary relative paths are unchanged (the since-last-run delta keeps matching them). -- was rejected because ruff/bandit/shellcheck/mypy append flags after the path list. Regression-locked by mcp_audit_run INV-18 (behavioural + wiring).

  • audit_run now secret-scrubs raw tool output before it reaches disk or the LLM (ANTS-2188). trivy's --scanners secret surfaces literal secret values; unlike gitleaks (which runs --redact) trivy's output was embedded verbatim into the .audit_cache/*.sarif notification text and returned over MCP via top_findings. Each tool's output is now run through SecretRedact::scrub at the single capture point in the runner, before it feeds either sink — closing both the on-disk artifact and the LLM envelope (OWASP LLM06). Regression-locked by mcp_audit_run INV-17.

  • Harden several input boundaries (indie-review #8 sweep). Secret-redaction now covers GitLab personal-access tokens (glpat-) and Slack app-level tokens (xapp-) before context is sent to an LLM. The repo-visibility gh repo view call now passes a -- end-of-options sentinel so a hostile clone's --prefixed origin slug can't be parsed as a flag, and the roadmap-card HTML escaper now escapes " so a quote in a ROADMAP bold-ID can't break out of an HTML attribute (CWE-79).

v0.7.98-rc1Pre-release

0.7.98 RC1 — Patron preview

0.7.98 RC1 — Patron preview (public ship target: 2026-07-09)

A large Ants-MCP / Claude-integration release. Highlights:

  • Master on/off switch for the Ants MCP integration (ANTS-1901)
  • project_query — Claude runs read-only Lua sandboxed over your project, returning just the result (ANTS-2093)
  • Screen-reader support for the terminal area (AT-SPI / Orca) (ANTS-1078)
  • dry_run preview across all 9 mutating MCP verbs (ANTS-2227)
  • audit_run async:true + audit_poll for slow sweeps past the transport timeout (ANTS-3396)
  • Flatpak installs can now reach the Ants MCP server from Claude sessions (ANTS-1900)
  • roadmap / changelog / feedback verb ergonomics + verbatim MCP arg-forwarding

v0.7.97

0.7.97 — MCP integration briefs, project auto-detect, work-bundles

A large MCP / Claude-integration milestone. Ants now answers "where do I hook into this pipeline?" (read_region call_sequence) and "show me the canonical idiom" (similar_code include_bodies) in a single call, auto-detects projects whose code isn't under src/ and offers to write their settings file, groups related to-dos into thematic work-bundles, and keeps its most-used tools instantly callable so long sessions don't drift back to raw grep.

Highlights

  • Integration briefs — read_region call_sequence:true returns the ordered steps inside a function with line anchors + the accessors a new step needs; similar_code include_bodies:true returns full matching definitions so you copy a canonical idiom in one call. (ANTS-2157 / ANTS-2156)
  • Project auto-detect — Ants notices when a project's code isn't under src/ and offers to write its .ants/project.json; new project_settings verb creates/updates it. (ANTS-2160 / ANTS-2161)
  • Work-bundles — roadmap_query mode:"bundles" groups active to-dos by theme, flagging blocked items and shipped siblings. (ANTS-1922)
  • Eager-loaded tools — the most-used MCP verbs are always loaded in Claude Code, callable instantly without a discovery step. (ANTS-2158)
  • Crash-class locks — two audit/review use-after-free crash classes are regression-locked; workspace-search no longer times out under concurrent load. (ANTS-2102 / ANTS-2144)
  • Code-navigation accuracy — file_outline stops mislabelling locals and case labels as functions and now finds old-C-style definitions; plus a sweep of related fixes. (ANTS-2159 et al.)
  • Security — block credential-bearing AI endpoint URLs and refuse cleartext-HTTP Bearer egress across the review family. (ANTS-2109 / ANTS-2108)

v0.7.97-rc1Pre-release

0.7.97 RC1 — Patron preview

Theme: Rolling Patron preview of the next release. Fixes and features land in this section as they ship during the freeze window; the section gets its public date when 0.7.97 is promoted to a stable release.

v0.7.96

0.7.96 — MCP token-frugality, teardown-crash sweep, security hardening

Theme: Ants-MCP token-frugality and roadmap / read-verb expansion, a sweep of teardown / use-after-free crash fixes, and indie-review security hardening.

Highlights

Added

  • Token-frugal MCP read verbs — read_region, apply_edits, project-wide codebase_index (ANTS-2021 / 2022 / 1637)
  • Lean-by-default MCP replies + opt-in compact:true (ANTS-2085 / 2091)
  • roadmap_log dry-run / custom stable IDs / near-duplicate advisory; changelog_log batch op; server build-identity in session_orient (ANTS-2077 / 2078 / 2043 / 2044 / 2073)

Fixed — crash & stability sweep

  • Teardown crashes/hangs on terminal-tab close, AI-review dialog close, and Lua plugin unload (ANTS-2110 / 2111 / 2117)
  • MCP/IPC idle-timer use-after-free under concurrent requests (ANTS-2026)
  • Crash when changing theme over a permission prompt (ANTS-2024)
  • Locale-dependent roadmap bundle-row sort (ANTS-2120)

Security — indie-review hardening

  • Hook-routing + MCP-wrap hardening (ANTS-1996); remote-control symlink/UID/DoS hardening (ANTS-1995); permission-prompt spoof gate (ANTS-1993); private cache dirs at 0700 (ANTS-1988); AI error-body secret scrub

See CHANGELOG.md for the full list (~85 entries). The x86_64 AppImage attaches automatically once the release workflow finishes building.

v0.7.95

0.7.95

Settings-dialog & menu usability fixes — readable fields at any window size, no surprise dialog closes, and menus that stay open while you toggle checkboxes (ANTS-1980/1981/1982).

v0.7.94

0.7.94

Theme

Auto-switcher refinement + MCP feedback/spec tooling — chooseable home tier, smarter safe-downgrade signals (ANTS-1974 + ANTS-1970..1973 + ANTS-1961/1962/1963).

Added

  • Settings: user-selectable default/home model tier — auto-switcher returns to it and only downgrades on clear evidence. (ANTS-1974) Let the user pick their home model (e.g. Opus) in Settings. The auto-switcher then treats that as the baseline — staying there for normal work and only stepping down to Sonnet/Haiku when the work is clearly mechanical/cheap.

  • Yield the composer_not_empty veto during a long-ToolUse foreground wait (the 67%-dominant near-miss blocker). (ANTS-1973) During a long build/test wait the text box often holds stale queued text, not active typing — yet that blocks the cheaper-model switch 67% of the time. When a foreground command has been running a while and the box hasn't changed, treat the text as abandoned and allow the downgrade.

  • Conditionally surface feedback_log / feedback_query in SessionStart orientation when a *_Ants_MCP_Feedback.md exists. (ANTS-1971) When a project has a cross-session feedback file, the startup hint should mention the two tools for reading/writing it — otherwise contributors fall back to hand-editing the file (which risks format drift).

  • Regret signal over-counts: any non-auto-authored /model is scored as a user-override regret, even when it's an independe… (ANTS-1957) The "regret rate" that decides whether to trust the auto-switcher counts every manual model switch you make after it acts as the switcher being wrong — even when you switched for your own reasons (testing, or picking a model for a specific task). So the feature looks worse than it is, especially during sessions where you're switching models a lot by hand.

  • spec_log MCP write verb — append cold-eyes loop entries / flip Status / append INVs on docs/specs/*.md. (ANTS-1963) Lets Claude update a spec file's status and review-log through Ants MCP instead of expensive hand-edits.

  • feedback_log MCP verb — write to a *_Ants_MCP_Feedback.md file (contributor append + maintainer tracking block). (ANTS-1962) Lets any CC session add feedback to the shared report files, and lets the maintainer session stamp a tracking block with roadmap IDs, without manually editing markdown.

  • feedback_query MCP verb — read the un-triaged tail of a *_Ants_MCP_Feedback.md file. (ANTS-1961) Lets the Ants maintainer session pull just the new feedback from a cross-session report file instead of re-reading the whole thing every week.

  • Auto-switcher task-shape signals round 2 — foreground-subprocess-wait (safe-downgrade) + post-clarification suppression … (ANTS-1959) Make the model-switcher smarter about WHEN it's safe to drop to a cheaper model: it's safe while waiting on a long build/test, but risky right after you answer a design question (hard work usually follows). Two cheap signals that catch the most common real sessions.

  • model_switch_stats surfacing/scoring polish — suppress calibration regret_rate, credit clean idle-end downgrades, do… (ANTS-1960) Three small honesty fixes to the model-switcher's stats: don't show a scary "50% regret" when it's based on one event; give credit when it correctly wanted a cheaper model right at a clean session end; and note that two of its counters only match when read at the same instant.

  • Auto-switcher task-shape signals round 2 — foreground-subprocess-wait (safe-downgrade) + post-clarification suppression … (ANTS-1959) Make the model-switcher smarter about WHEN it's safe to drop to a cheaper model: it's safe while waiting on a long build/test, but risky right after you answer a design question (hard work usually follows). Two cheap signals that catch the most common real sessions.

  • Near-miss telemetry needs the ANTS-1941 epoch boundary too — dominant_blocker stays contaminated by stale-binary records… (ANTS-1954) When you rebuild Ants, the auto-switcher's "why didn't it fire?" stats keep showing yesterday's reasons for about a day, because they don't reset at the rebuild boundary like the other stats do. That's exactly why we can't yet judge whether the switcher's remaining block is real. This makes those stats reset cleanly at each rebuild.

  • User-typed /model: inject a continuation prompt after auto-confirming the switch dialog. (ANTS-1953) When you type /model sonnet yourself, Ants now auto-clicks the confirmation dialog — but it still leaves you at a blank prompt instead of typing "please continue" to resume your work. This would add that last step.

  • Surface the MCP server's build identity (git SHA + build time) so callers can detect a ship-vs-live binary gap. (ANTS-1952) When a fix is committed but the running Ants build is older, the MCP tools report stale data and we waste time re-investigating. Expose the build's git SHA and build timestamp so a Claude session can instantly tell the server predates a fix.

  • Auto-confirm the 'Switch model?' prompt for user-typed /model commands too. (ANTS-1951) When you manually type /model to switch models, you still get a "are you sure?" popup — even though you just typed the command. This would make that popup automatic too.

  • find_definition could hint when a query matches a file stem but no symbol. (ANTS-1950) If you ask 'where is X defined' and X is actually a filename (not a function), the tool returns nothing — a one-line 'did you mean the file X.cpp?' hint would save a follow-up.

  • roadmap_log op:flip should accept common status synonyms ("done"→shipped, "wip"→in-progress, "todo"→planned). (ANTS-1932) When marking a roadmap item finished, the tool only accepts the word "shipped", not the more obvious "done" — so the first try fails and has to be redone. Teach it to accept the common everyday words too.

  • Trust-signal needs a fix-epoch boundary, not just an age window — pre-fix records poison regret_rate even when only days… (ANTS-1941) The auto-switcher scorecard still reads ~70% regret, but that is leftover noise from the buggy May-29 sessions, not the fixed behaviour. The 30-day window can't filter it out because the bad records are only a few days old. We need to mark a 'fixed from here' line so the scorecard only counts switches made by the current, fixed logic.

  • Auto-switcher calibration robustness — regret-driven conservatism + task-shape downgrade signal. (ANTS-1940) Now that the auto-switcher actually fires, the first real data shows it guesses wrong about half the time (on a tiny sample). Make it more cautious while it's still learning, and make it notice fiddly mechanical work (safe to use a cheaper model) versus deep reasoning (don't).

  • Extend the ANTS-1908 human-idle soft-veto to the focused_state_not_idle gate (now the dominant near-miss blocker). (ANTS-1939) The auto-switcher won't change models while a Claude session looks "busy" — but during a long hands-off task, "busy" means the agent is grinding away, which is exactly when a cheaper model would save the most. Let it switch when YOU haven't typed in a while, even if the agent is still working.

  • model_switch_stats firing/regret aggregation needs a recency window — stale pre-fix records poison the trust signal in… (ANTS-1936) The model-switch report mixes in old data from before the May-29 bug fixes, so it keeps showing a high "regret" number even though the one switch since the fix went fine. Let the report focus on recent switches so it reflects how the feature behaves NOW.

  • model_switch_stats should split upgrades/downgrades by trigger (auto vs manual) — current totals can't answer "is the … (ANTS-1934) The model-switch report mixes together the switches YOU make by hand and the ones the app makes automatically

  • Auto-switcher tracks pending-switch intent when blocked by composer_not_empty (ANTS-1919) When all guards pass but the composer has recent text, the target tier\nis stored in m_autoSwitchPendingTier so the switch fires on the next\n2 s tick once the composer clears. Status-bar visual annotation TBD.

  • session_orient now includes top-20 active roadmap bullets inline (ANTS-1922) Adds an active_bullets key (headline_only, status:active, limit:20)\nto the session_orient response envelope. Sessions can now identify the\nnext work bundle in one call without a follow-up roadmap_query.\nToken budget bumped to 17 000; does not affect allOk.

  • workspace_search adds max_match_bytes: + headline_only:true knobs — first-attempt tax on research sweeps with long… (ANTS-1904) When searching for threading keywords across a big roadmap, the response is dominated by 2-5 KB bullets each. Two new knobs (clip per-match bytes, or skip context entirely) would let the search return manageable results on the first try instead of forcing a regex-narrowing retry.

  • SessionStart orientation cheat-sheet: add roadmap_query + workspace_search + the verb-modes / ETag-and-fields footno… (ANTS-1910) The 9-tool cheat-sheet at session start is helpful but misses three things I reach for every session. Add roadmap_query (the read-side partner to roadmap_log), workspace_search (the cheap-grep), and a one-line footnote noting that most write verbs have multiple op: modes and most read verbs accept etag_match + fields=.

  • spec_query accepts project-configurable doc-path layouts (e.g. docs/phases/phase_<NN>_<topic>_design.md). (ANTS-1906) spec_query only finds specs at docs/specs/ANTS-NNNN.md — but Vestige's design docs live at docs/phases/phase_NN_topic_design.md. Either re-use the project_layout probe-set discovery, or accept path_prefix + id_pattern args.

  • roadmap_log op:append auto-detect stable-string-ID project shape — empty .roadmap-counter should default to `id_stra… (ANTS-1905) Some projects use stable string IDs (Ts20-FL1) instead of numeric ones (ANTS-1234). On those projects the append tool tries to allocate from an empty counter and produces wrong IDs. Auto-detect the project's existing style and default to the stable-prefix escape hatch.

  • roadmap_query per-section etag — let section=X short-circuit when only other sections changed. (ANTS-1907) Today reading the same section twice always re-parses if any byte of the file changed. A per-section ETag would let unchanged sections return 'not modified' independently — saves token cost across /cold-eyes and /test-audit loops that touch many sections.

  • Auto-switcher: enrich model_switch_stats headline with calibration progress + near-miss dominant-blocker. (ANTS-1909) When the auto-switcher hasn't fired yet, the headline says 'no switches yet' — which sounds broken. Rewrite the headline to also surface 'N near-misses blocked by composer_not_empty' so it reads as 'evaluating but blocked' instead of 'doing nothing'.

  • Auto-switcher: composer_not_empty soft-veto with stale-text detection — unblock long autonomous sessions where the dom… (ANTS-1908) The auto-switcher won't fire while there's any text in the input box — but in long autonomous sessions there's always leftover text. Make the rule smarter: if the text has been sitting untouched for 5+ minutes, treat it as stale and allow the switch.

Changed

  • Direct /model <tier> (explicit arg, no dialog) doesn't resume work — ANTS-1969 only covers the menu/dialog path. (ANTS-1975) When you type /model sonnet (with the model name) Ants doesn't resume the task — it goes idle. The fix from before only handles the pop-up menu version of /model, not the direct one.

  • Doc note: firings near_misses.total_24h excludes idle-end-suppressed; +idle_end_suppressed_24h reconciles to near_miss… (ANTS-1972) Two of the switcher's counters look like they disagree (18 vs 21) but don't — one excludes idle-end-suppressed near-misses. A one-line doc note stops the next session re-filing it as a bug.

  • User-typed /model does not continue work after auto-confirm (ANTS-1958 revisit). (ANTS-1969) When you manually type /model X and Ants auto-confirms the dialog, Claude Code goes idle rather than continuing work — you have to send a new message. When auto mode is on, a continuation prompt should fire just like after an auto-switch.

  • Pending-switch chip annotation (→ Tier) reads as a recommendation rather than an automatic queued action. (ANTS-1956) The model chip shows "Haiku (→ Sonnet)" when a switch is queued, but users interpret it as "Ants is suggesting you switch" rather than "Ants will switch automatically once you send your message." Rewording or a tooltip would make the intent clear.

  • model_switch_stats: document by_blocked_by gate-hit counting and /model ledger semantics (ANTS-1947 + ANTS-1949) Two clarifications in the MCP tool descriptor. (1) by_blocked_by\n values can sum to more than window_24h.total — a single near-miss\n blocked by several gates contributes one count per gate. (2)\n by_trigger.manual is structurally 0: only the autonomous switcher\n writes ledger records; user /model commands and chip-clicks do not.

  • Auto-switcher pending-switch visual indicator — model chip annotation when switch queued on composer_not_empty. (ANTS-1926) When the auto-switcher wants to change model but you have text in the input box, show a subtle 'pending → haiku' label on the model chip so you know a switch is waiting.

  • ModelRecommender::score() mtime cache — avoid re-parsing 512 KB transcript every 2 s tick. (ANTS-1927) The auto-switcher re-reads up to 512 KB of conversation history every 2 seconds even when nothing has changed

  • Colored cell backgrounds (diff green/red highlights) appear more opaque than the base when terminal opacity < 1. (ANTS-1864) When you make the terminal see-through, the green/red highlight bars (like Claude's diff view) stay nearly solid instead of going see-through to match the rest of the window. Make them respect the same transparency level.

  • Drop deprecated cwd field from session_memory schema in 0.7.93. (ANTS-1420) clean up the placeholder schema entry left behind

  • mcp-review-engines H1 — Ledger load per brief assembly is re-parsed N times for N lanes. (ANTS-1672)

  • mcp-review-engines M3 — extractCitedCodePaths slurps every cited doc body unbounded; ROADMAP.md (~600 KB today) is rea… (ANTS-1674)

  • --preset=fast is functionally identical to --preset=default — give it a real differentiator. (ANTS-1556) The "fast build" mode doesn't actually build faster than the normal mode — they have the same settings. Need to make it really faster, or drop it.

  • --preset=fast differentiator: bump link_pool from 1 → 2 for parallel test-bundle linking. (ANTS-1558) Let the "fast build" mode link two programs at the same time instead of one — should make full rebuilds a few minutes faster.

  • project_layout roadmap-format sniffer still returns unknown on Vestige despite the ANTS-1632 ship claim — re-investi… (ANTS-1903) Vestige reports that the format-detector for ROADMAP files is still returning 'unknown' on their project despite the previous fix being marked shipped. Either the fix isn't reaching their build, or it doesn't cover their specific roadmap shape. Needs a trace field + CI fixture.

Fixed

  • SessionStart orientation prelude exceeds its 1200-byte cap (test red). (ANTS-1970) The MCP cheat-sheet that prints when a Claude session starts grew past its size limit as new tools were added, so a test that guards the limit is failing. Either trim the cheat-sheet or raise the limit.

  • /model slash command double-posts in the transcript (command + confirmation echoed twice from one invocation). (ANTS-1958) When you type /model to switch models, the command and its "model set" confirmation sometimes show up twice from a single press. Harmless but messy — this de-duplicates it.

  • Auto-confirm user-typed /model dialog now catches it within ~120 ms instead of up to 2 s (ANTS-1955) The 2-s status tick was the sole trigger, so a dialog rendering just after a tick fire was missed for nearly 2 full seconds. Added a short polling burst (mirrors the auto-switch handshake path) that fires every 120 ms for up to 1.9 s after each tick, ensuring the confirm lands almost immediately after the dialog appears.

  • Near-miss telemetry no longer records idle steady-state as a blocked switch (ANTS-1946) When the auto-switcher's recommended model already matches the current\n model, it never wanted to switch — so that tick is no longer logged as\n a "blocked" event. Previously these idle records dominated the\n "what's blocking it?" counter, making composer-typing and cool-down\n look less significant than they are. The fix removes ~36% ledger noise\n and clears the path for accurate dominant-blocker reporting.

  • Auto-switcher thrash at context-compression boundary — fires /model to the model already set when the transcript's /mode… (ANTS-1944) After the conversation fills up and gets summarised, the auto-switcher loses track of which model was already set and keeps issuing the same switch command over and over (8 times in one observed session).

  • model_switch_stats MCP verb relies on StatsConfig struct-default for windowDays while the controller sets it explicitly … (ANTS-1942) Two parts of the auto-switcher read the same scorecard but configure the 'recent window' differently — they agree today only by luck. If the default ever changes, the human-facing number and the switcher's own caution dial would silently disagree.

  • Model-switch actuator: confirm by watching PTY output, not a blind 250 ms timer. (ANTS-1920) The auto-switcher sends "/model opus" then blindly waits a quarter-second and presses "1" to confirm. If the confirmation box is slow, or you have a message queued, the "1" lands in the wrong place and the switch gets stuck (observed by the user 2026-05-29 — model stayed on Sonnet, "/model opus" stranded in the composer). Watch for the actual "Switch model?" prompt before answering it.

  • model_switch_stats 24h near-miss count + dominant-blocker disagree between firings and near_misses modes. (ANTS-1938) The model-switcher's "what's blocking it today" number is different depending on which report you ask for — one says 2, the other says 22, and they even name different top blockers. Make them agree.

  • roadmap_log op:flip/flip_batch can't resolve composite IDs containing comma+space (read-path roadmap_query id= can… (ANTS-1937) When a roadmap item's ID has a comma in it, the "mark it done" tool can't find it — even though the search tool finds it fine. Make the two agree.

  • Inline graphics: back-to-back Sixel/Kitty/OSC sequences no longer lose a byte (ANTS-1777) (ANTS-1777) Terminals that stream pictures in back-to-back chunks (Sixel, Kitty graphics) dropped a byte between two chunks, rendering the second as garbage. The VT parser now re-arms a new string sequence when the byte after a string-terminating ESC is a string introducer (] P _ X ^), while still discarding the dangerous ESC-finals (c=RIS, etc.) and the CSI introducer — so the 0.7.53 hardening against hostile terminal-reset injection is fully preserved.

  • changelog_log op:add_from_roadmap drops the Layman line from the CHANGELOG body — regression, failing test. (ANTS-1933) The shortcut that copies a roadmap item into the changelog is currently leaving out the plain-English summary line — so changelog entries created that way are missing their friendly description

  • Auto-switcher stable-counter reset-hysteresis reduces false ticks_target_stable_insufficient blocks (ANTS-1925) A single score-boundary noise tick no longer wipes the stability counter.\nRequires kStableResetTicks (=2) consecutive target==current ticks before\nresetting, matching the same 4 s window as the forward stability gate.

  • ANTS-1924: model-switch PTY handshake — ESC + ENTER confirmation sequence + continuation prompt. (ANTS-1924) When Ants switches Claude models, it now sends the right keystrokes to confirm the switch dialog (ESC then Enter), then automatically sends "please continue" so Claude picks up where it left off — no manual typing needed.

  • Review Changes button doesn't appear when Claude only writes new files (no insertions/deletions on tracked files). (ANTS-1874) When Claude creates a brand-new file, the bottom-bar "Review Changes" button stays hidden — it only lights up if Claude edits an existing file. So new-file changes have no quick way to be reviewed.

  • Strip the "ccache + PCH" framing from --preset=fast docs. (ANTS-1557) Update the "fast build" mode's description to match what it actually does (just an isolated build folder).

  • /model actuator sent \n not \r — text landed in composer, never submitted (auto-switch dead loop). (ANTS-1912) When the auto-switcher fired /model sonnet, the text appeared in the Claude composer waiting for the user to press Enter — it never actually submitted. Single-char fix: send CR (0x0D, what the Enter key produces per terminalwidget.cpp:1930) instead of LF (0x0A).

  • Cross-tab Claude-state mix-up — focused tab's status bar shows ANOTHER tab's Claude: state. (ANTS-1911) When you have two Claude tabs open (e.g. Ants Terminal + Vestige), the status bar at the bottom sometimes shows the WRONG tab's state. Screenshots 2026-05-28: the focused Ants Terminal tab was actively running cold-eyes loops + 3 background agents but the status bar said 'Claude: idle' and the tab dot was idle; switching to the Vestige tab (which was actually idle) showed 'Claude: thinking' — the state of the OTHER tab leaked into this one's status surface.

v0.7.94-rc1Pre-release

0.7.94 RC1 — Patron preview

This release shipped without written notes.

v0.7.93Pre-release

0.7.93

Theme: Auto-switcher visibility — the silent /model injection finally announces itself + the operator-trust ledger (ANTS-1893 surfacing + ANTS-1894 near-miss telemetry + ANTS-1891 honest headline + ANTS-1897 MCP cheat-sheet).

v0.7.93-rc3Pre-release

0.7.93 RC3 — Patron preview

Theme: Auto-switcher visibility — the silent /model injection finally announces itself + the operator-trust ledger (ANTS-1893 surfacing + ANTS-1894 near-miss telemetry + ANTS-1891 honest headline + ANTS-1897 MCP cheat-sheet).

v0.7.93-rc2Pre-release

0.7.93 RC2 — Patron preview

Respin of v0.7.93-rc1 with cherry-picked fixes: c6cc905

v0.7.93-rc1Pre-release

0.7.93 RC1 — Patron preview

Theme: Auto-switcher visibility — the silent /model injection finally announces itself + the operator-trust ledger (ANTS-1893 surfacing + ANTS-1894 near-miss telemetry + ANTS-1891 honest headline + ANTS-1897 MCP cheat-sheet).

v0.7.92Pre-release

0.7.92

Theme: MCP token-saver depth + frozen-RC release pipeline. Wires the 0.7.92 milestone (pulls 34–42 MCP/test-audit/indie-review fold-ins) and bootstraps the weekly-Wednesday + Patron-RC cadence. Public ship target 2026-05-27; the inaugural v0.7.92-rc1 is cut from these bits.

v0.7.92-rc1Pre-release

0.7.92 RC1 — Patron preview

Theme: MCP token-saver depth + frozen-RC release pipeline. Wires the 0.7.92 milestone (pulls 34–42 MCP/test-audit/indie-review fold-ins) and bootstraps the weekly-Wednesday + Patron-RC cadence. Public ship target 2026-05-27; the inaugural v0.7.92-rc1 is cut from these bits.

v0.7.91

0.7.91 — indie-review fold-in (3 CRIT + 18 HIGH inline; 30 follow-ups roadmapped)

Theme: indie-review fold-in. The 2026-05-13 multi-agent /audit

  • /indie-review sweep across 14 subsystems surfaced 3 CRITICAL, 53 HIGH, 64 MEDIUM, ~75 LOW findings. This release lands the mechanical, security, and correctness items inline; 30 substantial follow-ups (refactors, multi-file API changes, planning-required items) are folded into ROADMAP as ANTS-1260…ANTS-1317. All 422 tests passing.

Security

  • vtparser — 8-bit C1 controls now routed. Raw 0x80-0x9F bytes in the PTY stream were being silently mis-decoded as malformed UTF-8 → U+FFFD, which made the ST (0x9C) terminator branches in OscString / DcsString / ApcString / IgnoreString dead code. ECMA-48 § 5.3 / Williams VT500 state machine treat C1 as first-class controls. Fixed at vtparser.cpp:feedByte — bytes in [0x80, 0x9F] route through processChar(byte) directly, plus a Ground-state dispatch table for 0x9B (CSI), 0x9D (OSC), 0x90 (DCS), 0x9F (APC), 0x98/0x9E (Ignore). Other C1 emit Execute.

  • lua-plugins — heap cap silently bypassed. Lua 5.4 manual: when ptr == NULL, the allocator's osize parameter encodes the object type (0..8 small int), not byte count. The custom allocator treated it as a byte count, drifting m_luaMemUsage downward unboundedly on every fresh allocation — a plugin could silently exceed the documented 10 MB cap. Fixed at luaengine.cpp:luaAlloc (zero osize when ptr == nullptr).

  • roadmapdialog — HTML injection (CWE-79). rec.kind was emitted unescaped into the rendered card HTML; rxKind admits <, >, &, ", so a hostile ROADMAP.md bullet like Kind: <img src=x onerror=…> rendered as raw HTML in QTextBrowser. Fixed at roadmapdialog.cpp:1337 — htmlEscape(rec.kind) before emission.

  • claudeintegration — cold-start hook gate tightened. During the 1–3 s window after a tab switch (while m_transcriptPath was empty), foreign-tab PreToolUse / PostToolUse / Stop events passed the focus gate and mutated singleton state — exactly the ANTS-1161 symptom returning transiently. Now only SessionStart

    • PermissionRequest land while the path is empty; other state-mutating hooks drop with a diagnostic log (DebugLog::Claude).
  • ptyhandler — child signal mask + dispositions reset before exec. Qt / glib / dbus install handlers for SIGCHLD / SIGPIPE / SIGUSR2 etc. POSIX § 2.4 keeps SIGPIPE=SIG_IGN across exec, so child shells inherited the parent's mask and pipelines silently hung. ~Pty also now sends SIGCONT alongside SIGTERM so a stopped child doesn't burn the full 500 ms escalation budget.

  • remotecontrol — cwd validation on cmdLaunch + cmdNewTab (CWE-22 parity). Other path-bearing verbs (workspace-search, file-outline, git_state) validated their path parameter against control-bytes / backslashes; launch and new-tab did not. Same-UID model holds today, but the verb is the seam Claude Code (and any future MCP) crosses with user-supplied input — validate at the boundary, not the kernel.

  • remotecontrol — getsockopt(SO_PEERCRED) len check. On getsockopt failure with a zero-initialised cred, the disconnect log read "peer UID 0" — a phantom "root tried to connect" alarm. Now checks len == sizeof(cred) before reading cred.uid.

  • antshelper — --repo-root traversal hardening. Rejects .. substrings + NUL bytes; canonicalises before script lookup so a planted packaging/check-version-drift.sh at an attacker- controlled path can't pose as a project root.

  • lua-plugins — init.lua symlink rejection. Plugin directory was already canonicalised; the init.lua file inside wasn't symlink-checked. A symlink to /etc/passwd or ~/.ssh/id_rsa would surface the file's first line in lua_tostring(err) when the parser failed. Rejected at pluginmanager.cpp:202 with a qWarning.

  • terminalgrid — Kitty chunk buffer flush on alt-screen + resize. An attacker m=1 chunked image transfer + alt-screen flip → stale bytes prepended to the next legitimate APC m=0 from a different sender → poisoned image attributed to new sender. Fixed at the alt-enter site + at the top of TerminalGrid::resize.

  • terminalgrid — OSC 8 id= length cap (256 bytes). URI was capped at MAX_OSC8_URI_BYTES = 2048; the id was uncapped. With the parser's 10 MiB per-OSC cap, a hostile id=<10MB> sequence scaled to ~800 MiB across 80 rows of scrollback.

  • terminalgrid — Sixel first-pass payload cap (4 MiB). The vt parser caps DCS bodies at 10 MiB, but Sixel's first-pass iterator walks every byte to compute width/height with no cycle budget. A 10 MiB payload of valid sixel bytes pinned the parse thread for ~10 M iterations.

Added

  • claudebgtasks::poll() — watch-loss recovery mirror of claudetasklist::poll(). Re-adds the QFileSystemWatcher when the transcript path (re-)appears + mtime-shortcircuits to skip reparse when nothing changed. Closes the documented CLAUDE.md claim that both trackers had poll() parity (only foreground did pre-0.7.91). Wired into ClaudeStatusBarController::refreshBgTasksButton.

  • claudebgtasks::parseTranscript — isSidechain + isCompactSummary filters. Foreground tracker already filtered these (ANTS-1158 sidechain rationale + ANTS-1224 compact-summary state reset); bg-tasks did not, so subagent-launched background tasks silently inflated the parent session's running count, and post-/compact resume left pre-compact bg entries lingering.

  • m_errorHideTimer owned member on ClaudeStatusBarController — replaces the prior QTimer::singleShot pattern in setError. Re-entry now cancels the prior auto-hide instead of inheriting it (rapid back-to-back errors no longer flash and disappear). Sticky errors via autoHideMs <= 0.

  • Accessibility — setAccessibleName on three previously-silent status-bar chips. m_bgTasksBtn, m_tasksBtn, m_errorLabel now expose Orca-readable labels per docs/standards/documentation.md § 7. The remaining a11y gaps (keyboard-nav to expand cards, aria-expanded on collapse anchors) are tracked as ANTS-1277.

  • vtparser — kSafeMinSigned / kSafeMaxSigned named constants for the SSE2 safe-ASCII scan boundary (-96 / -2 were magic numbers).

  • Q_ASSERT in handleAsciiPrintRun verifying the [0x20, 0x7E] precondition the SIMD scanner establishes.

Changed

  • CLAUDE.md — three doc-drift fixes. terminalwidget is QWidget (has been since 0.7.4), not QOpenGLWidget. featurecoverage lists 2 in-process lanes + 1 shell-based (test_health runs via QProcess, not inProcessRunner). claudetasklist / claudebgtasks bullet now reflects ANTS-1246 done/total chip semantics + the new bg-tasks poll().

  • SARIF partialFingerprints key bumped to primaryLocationLineHash/v1 (SARIF v2.1.0 § 3.27.13 versioning convention).

  • computeDedup chained single-arg .arg() — closes a %n-placeholder collision vector if a file path contains a literal "%3" / "%4" substring (legal on filesystems).

  • terminalwidget — dead #include <QSurfaceFormat> removed (no QSurfaceFormat reference left in the file).

  • terminalwidget — dead duplicate loop in invalidateSpanCaches removed (lines 3472-3478 re-erased the exact key set the previous loop had just erased; was a no-op).

  • terminalwidget — unused m_perfLastPaintUs member + the overlay line that read it dropped. The member was never written; the overlay always showed Paint: 0 us.

  • terminalwidget — isCellSearchMatch early-returns on empty m_searchMatches. Called twice per cell per frame; ~16,000 wasted comparator invocations/frame at 60 fps when no search active.

  • luaengine::lua_ants_on — per-event handler cap of 64. A plugin calling ants.on(event, fn) in a loop would otherwise grow the handler list until the heap cap fired.

  • CI — build-asan job timeout 20 → 30 min. Mirrors the earlier 421e32d build-test 15 → 25 bump shape; ASan is 2-3× slower than the release build, and the 0.7.91 sweep pushed it past the prior cap.

  • ROADMAP.md — 30 follow-up cards allocated ANTS-1260… ANTS-1317 across five themes: indie-review deferred items (1260…1277), MCP renderer bug (1278), MCP orchestration consolidation (1279…1288), skill displacement + context discipline + security hardening + namespacing (1289…1298), general Claude Code workflow MCPs (1299…1312), and visibility / Anthropic-discoverability (1313…1317). .roadmap-counter: 1259 → 1317.

Fixed

  • antshelper — silent false-clean on hung script. proc.waitForFinished(60000) return value was ignored; Qt's exitStatus() returns NormalExit + exitCode() returns 0 by default on a still-running process. A hung drift script was reported as {ok:true, clean:true} exit 0. Now branches on the timeout: kills the process and returns {ok:false, error:"drift script timed out after 60s", code:"script_timeout"} with exit 1.

  • antshelper — isatty(0) short-circuit on readStdin. Interactive runs no longer block until Ctrl-D when stdin is a TTY.

  • claudeintegration — m_planModeByPid PID-reuse leak. Cache was only pruned on explicit closeTab. Linux PID-reuse is fast; an abnormally-exiting shell left a stale entry that poisoned the next tab assigned the same PID. Now also pruned on the NotRunning transition in pollClaudeProcess.

  • claudetasklist + claudebgtasks — m_lastRescanMtimeMs reset on path change. A re-bind to the same path shortly after a clear could short-circuit poll() on a matched mtime, skipping the legitimate rescan.

  • claudestatuswidgets — m_promptActive now cleared in resetForTabSwitch (was the only render-state flag left out).

  • claudestatuswidgets — empty initial label on m_tasksBtn. The prior "☰ 0/0" literal contradicted the ANTS-1246 hide predicate (total <= 0 || done >= total). The chip is hidden until first refresh anyway, but the latent inconsistency is gone.

  • claudeintegration — qWarning on the 100 MiB transcript cap. Previously returned an empty QJsonArray silently; "transcript empty / status frozen" reports now have a logging breadcrumb.

  • tests — remote_control_launch test window 3000 → 3500 chars to admit the new cwd validation block in cmdLaunch.

v0.7.90

0.7.90 — ANTS-1113 v1: /debt-sweep fold (engine + 4 MCP tools)

Theme: ANTS-1113 v1 — fold /debt-sweep mechanical scan into Ants. The four canonical post-feature drift categories (code drift, test coverage, doc drift, packaging drift) lift out of the file-reading subagent into 4 new MCP tools backed by a DebtSweepEngine namespace; per-finding triage continues to live in optional Claude calls. Estimated saving per /debt-sweep run: ~15-40 K orchestrator tokens (subagent never opens 95% of the project files; findings come back as pre-classified JSON instead of free-form prose). Qt "Debt Sweep" tab deferred to ANTS-1259 v2.

Added

  • ANTS-1113 — DebtSweepEngine helper (src/debtsweepengine.{h,cpp}, Qt::Core only). Per-detector pure functions:

    • detectStaleTypeComments(projectPath, opt) — code drift (a): walks git diff <since>..HEAD --name-only for *.cpp/h/py/js/ts/tsx, extracts comments, flags leading-cap CamelCase tokens (≥4 chars, \b([A-Z][A-Za-z0-9_]{3,})\b) absent from the project source blob.
    • detectAddedTodos(projectPath, opt) — code drift (c): unified=0 diff parser flags TODO / FIXME / XXX / HACK markers added in scope.
    • detectOrphanQUnused(projectPath, opt) — code drift (d): per-file pass over git ls-files '*.cpp' '*.h' finds Q_UNUSED(x) / (void)x; markers wrapping a variable not declared anywhere in the same file. The only v1 detector that sets autoFixable=true.
    • detectMissingInvariantTests(projectPath, opt) — test coverage: parses \bINV-([0-9][0-9a-zA-Z]*)\b from each tests/features/*/spec.md (covers INV-7 and INV-8b); reports invariants not mentioned in any sibling test_*.{cpp,py,js,go,rs}.
    • detectRoadmapShippedWithoutCommit(projectPath, opt) — doc drift (a): reads ROADMAP, runs git log --all --format=%s once, flags ✅ items whose stable ID is unmentioned.
    • detectChangelogStaleBullets(projectPath, opt) — doc drift (b): parses CHANGELOG [Unreleased] block, flags bullets citing files not in git diff <since>..HEAD.
    • runPackagingDrift(projectPath, opt) — packaging drift: wraps the existing packaging/check-version-drift.sh; parses stdout into Finding structs.
    • scanAll(projectPath, opt) — convenience: runs every enabled detector in canonical category order.
    • applyMechanicalFix(projectPath, finding) — applies one mechanical edit; returns ApplyVerdict {applied, errorCode, errorMessage} so MCP handlers can disambiguate file_changed / not_fixable / io_error no-ops without inspecting QFile errno state.
    • templateDebtSweepFoldInBlock(deferred, ids, dateIso) — ### 🧹 Debt-sweep fold-in (DATE) block per roadmap-format.md § 3.8 + § 3.5.3. <dateIso> byte-identical between heading and per-bullet Source: line (locked by INV-10).
    • triagePrompt(llmShaped) — pure string templating of the LLM triage prompt for the judgment-required subset. Locked by tests/features/debt_sweep_engine/ (13 tests).
  • ANTS-1113 — 4 new MCP tools registered via the consolidated registerToolProvider registry from ANTS-1253:

    • debt_sweep_scan — runs the four-category scan; returns {findings, total_findings, by_category, since_resolved} (input: optional since, optional categories subset).
    • debt_sweep_apply_fix — applies one mechanical fix in-place; ok=true even on recognised no-ops (file_changed / not_fixable); ok=false only on io_error. Re-grep guard in applyMechanicalFix § 3.9 makes the operation idempotent against re-application (locked by INV-13a).
    • debt_sweep_defer — allocates IDs via RoadmapFoldIn::allocateIds, renders the fold-in block, and atomically inserts it into ROADMAP.md when findActiveReleaseHeading succeeds. Eager ID allocation (no rollback on written:false) — same trade-off as ANTS-1111 § 2.5; envelope returns the allocated IDs so the caller can splice manually.
    • debt_sweep_triage_prompt — emits the LLM triage prompt for a caller-filtered subset of findings. All 4 follow the existing UID-scoped 0700-perms IPC trust model; schemas use additionalProperties: false. Locked by tests/features/mcp_debt_sweep_tools/ (5 tests).
  • ANTS-1259 (new ROADMAP item) — ANTS-1113 v2: AuditDialog "Debt Sweep" tab + per-finding Fix / Defer / Allow buttons + Triage with AI button (aidialog dispatch). Adds README CLI flag drift detector (needs binary execution). Spec § 1.1 of docs/specs/ANTS-1113.md documents the v1/v2 split rationale.

Changed

  • featurecoverage.{h,cpp} — three internal helpers lifted to FeatureCoverage:: public surface so DebtSweepEngine can reuse them without copying the extension list, skip-dir set, or containment-check fallback chain:
    • buildProjectSourceBlob(projectPath) — concatenates every source/ config/doc file in the project tree (per the canonical extension list) into one UTF-8 blob; skips build dirs + spec.md files.
    • existsInSource(blob, token) — substring containment + :: and . tail-fallbacks (with the same identifier-shape guards runSpecDriftCheck had inline).
    • specStopwords() — public accessor for the existing internal kSpecStopwords set. runSpecDriftCheck is rewired to call them; behaviour unchanged.

Tests

  • tests/features/debt_sweep_engine/ (13 tests) — engine pure-fn invariants (INV-3, INV-4, INV-9, INV-10, INV-11, INV-13a, INV-13b
    • scanAll include-flag honouring).
  • tests/features/mcp_debt_sweep_tools/ (5 tests) — source-grep verification of the wiring layers (tools/list, registerToolProvider, remotecontrol.h declarations, remotecontrol.cpp definitions, schema additionalProperties:false).
  • tests/features/mcp_indie_review_tools/test_mcp_indie_review_tools.cpp — AllSchemasUseAdditionalPropertiesFalse region boundary tightened so it doesn't drift when new tool blocks land after it.

v0.7.89

0.7.89 — ANTS-1112 v1: /indie-review fold (engine + 5 MCP tools)

[0.7.89] — 2026-05-13

Theme: ANTS-1112 v1 — fold /indie-review orchestration into Ants. The mechanical halves of the multi-agent independent review (partition, brief assembly, cross-lane corroboration, synthesis-prompt templating, ROADMAP fold-in) lift out of orchestrator context as 5 new MCP tools backed by a IndieReviewEngine namespace; per-lane review judgment + dispatch continue to live in Claude subagent calls. Estimated saving per /indie-review run: ~20-50 K orchestrator tokens. Qt dialog deferred to ANTS-1258 v2.

Added

  • ANTS-1112 — IndieReviewEngine helper (src/indiereviewengine.{h,cpp}, Qt::Core only). Six pure functions:

    • derivePartition(projectPath) — reads CLAUDE.md ## Module map (src/) via the existing SubsystemMap::cachedLanes helper, walks src/ to compute per-lane source-file lists; honours <projectPath>/.indie-review/partition.json override when present.
    • assembleBrief(projectPath, lane) — verbatim brief text for one lane: header + source bodies + ROADMAP slice + standards links. Pure file IO bounded to projectPath.
    • extractFileLineCitations(projectPath, report) — regex pass over a single review report; rejects paths that escape projectPath (defense against fabricated cites).
    • corroboratedFindings(projectPath, reports, minLanes=2) — cross-lane corroboration; (file, -1) (file-level) and (file, 42) (line-level) are distinct keys (intentional).
    • synthesisPrompt(reports, threatModelExtras) — pure string templating of the optional cross-cutting synthesis prompt.
    • templateIndieReviewFoldInBlock(actionable, ids, dateIso) — ### 🔍 Indie-review fold-in (DATE) block per roadmap-format.md § 3.8 + § 3.5.3.
    • assembleThreatModelExtras(projectPath) — MCP-handler helper that concatenates CLAUDE.md / SECURITY.md / .semgrep.yml under === <header> === markers. Locked by tests/features/indie_review_engine/ (13 tests).
  • ANTS-1112 — 5 new MCP tools registered via the consolidated registerToolProvider registry from ANTS-1253:

    • indie_review_partition — returns lane list (no input).
    • indie_review_brief — returns the assembled brief for one lane (input: lane).
    • indie_review_corroborate — returns cross-lane corroborated findings (input: reports map, optional min_lanes).
    • indie_review_synthesis_prompt — returns the rendered synthesis prompt (input: reports, optional include_threat_model_extras).
    • indie_review_fold_in — allocates IDs via RoadmapFoldIn::allocateIds, renders the ### 🔍 Indie-review fold-in (DATE) block, and atomically inserts it into ROADMAP.md when findActiveReleaseHeading succeeds (input: actionable array, optional date_iso, optional release_block_heading). All 5 follow the existing UID-scoped 0700-perms IPC trust model; schemas use additionalProperties: false. Locked by tests/features/mcp_indie_review_tools/ (5 tests).
  • ANTS-1258 (new ROADMAP item) — ANTS-1112 v2: Qt IndieReviewDialog that wraps the v1 engine for users who want in-app indie review without Claude orchestration. Spec § 1.1 of docs/specs/ANTS-1112.md documents the v1/v2 split rationale.

v0.7.88

0.7.88 — ANTS-1111 v1: /audit triage fold (engine layer)

[0.7.88] — 2026-05-13

Theme: ANTS-1111 v1 — fold /audit triage into the Project Audit tool's engine layer. Five mechanical pieces lift out of the LLM round-trip: cross-tool corroboration severity-tier shift, framework auto-detect, ROADMAP fold-in helper, the // audit: drop[=rule] inline-suppress alias, and the RuleQualityTracker::noisyRuleIds accessor that feeds the shift. UI affordances (Fold-into-ROADMAP button, per-finding Allow button, Since-baseline pill) deferred to ANTS-1257 v2. Cumulative payload: ~750 LoC engine + ~33 new test cases, all green. Foundation ships ahead of ANTS-1112 / 1113 (which reuse RoadmapFoldIn).

Added

  • ANTS-1111 — RoadmapFoldIn helper (src/roadmapfoldin.{h,cpp}, Qt::Core only). Three operations: allocateIds(projectPath, n) reserves N consecutive IDs from .roadmap-counter under ::flock(LOCK_EX|LOCK_NB) (5 s budget; adopts the configbackup.h pattern); insertBlock(projectPath, heading, block) performs an atomic insert immediately after a named ## heading via QSaveFile, preserving original file permissions; findActiveReleaseHeading(projectPath) returns the first (target: …)-marked heading, falling back to the first shipped release block. Returns false on heading-not-found — caller is responsible for creating the heading first. Locked by tests/features/roadmap_fold_in/ (12 tests).

  • ANTS-1111 — AuditEngine::applyCorroborationShift (src/auditengine.cpp). Severity-tier promotion when ≥ 2 distinct CheckIds cite the same (file, line) (clamped to Blocker); demotion when a single-tool finding's checkId is in noisyRules (clamped to Info). Wired into AuditDialog::renderResults after the enrichment pass; the noisyRules set comes from the new RuleQualityTracker::noisyRuleIds(fpThreshold=50, minSamples=5) accessor (src/auditrulequality.{h,cpp}). Locked by tests/features/audit_corroboration_shift/ (8 tests).

  • ANTS-1111 — AuditEngine::templateRoadmapFoldInBlock (src/auditengine.cpp). Pure-string templating of a ### 🔍 Audit fold-in (DATE) subsection per roadmap-format.md § 3.8 (subsection shape) + § 3.5 (per-bullet fields). Caller pre-allocates IDs via RoadmapFoldIn::allocateIds. Empty input → empty string. Covered by tests/features/roadmap_fold_in/Inv5TemplateShape.

  • ANTS-1111 — AuditHygiene::detectProjectFrameworks (src/audithygiene.{h,cpp}). Probes the project root for 7 framework markers (flask, django, react, vue, qt6, rust, go) by reading requirements.txt / pyproject.toml / package.json / CMakeLists.txt / Cargo.toml / go.mod / manage.py. Companion semgrepRulePacks(QStringList) maps recognised frameworks to {"--config", "p/<fw>"} argv pairs. Pure-IO, bounded to projectPath. Wiring into the live runNextCheck semgrep invocation deferred to ANTS-1257. Locked by tests/features/audit_framework_detect/ (10 tests).

  • ANTS-1111 — // audit: drop[=rule] inline-suppress alias. Shorter ergonomic form of the existing // ants-audit: disable token. Same parser code (auditdialog.cpp:2055); same semantics (bare form suppresses every rule on the line; =rule suffix targets one). Both forms coexist indefinitely. The generic audit: prefix is confined by the verb constraint (\s*drop) plus the existing rule-list parser (auditdialog.cpp:2083-2107); collision-prevention covered in spec § 2.6 + INV-12. Locked by tests/features/audit_drop_alias/ (2 tests).

Changed

  • ANTS-1111 — Widened-allowlist documentation. The pre-existing .audit_allowlist.json filter (AuditDialog::allowlisted(), auditdialog.cpp:3991) was already cross-detector — the call site lives in the main per-finding loop and matches by exact checkId equality, so any detector's rule (clazy-X, cppcheck-Y, etc.) can be allowlisted. The doc-comment at auditdialog.h:101-110 now documents this widened scope (was previously labelled grep-rule only). No behaviour change.

  • ADR-0003 (docs/decisions/0003-cc-fold-relax-gate-and-draw-boundary.md) — relaxes the ADR-0002 dec 8 gate (ANTS-1120 measurement no longer pre-requisite for the CC-fold bullets), draws the ANTS-1108 ↔ ANTS-1111 / 1113 per-surface boundary, and enumerates pre-existing scaffolding so the subsequent ANTS-1112 / 1113 specs don't re-invent it.

  • ANTS-1257 (new ROADMAP item) — ANTS-1111 v2: UI affordances (Fold-into-ROADMAP button, per-finding Allow button, Since-baseline pill, semgrep wiring). Spec § 12 of docs/specs/ANTS-1111.md documents the v1/v2 split rationale.

v0.7.87

0.7.87 — MCP token-reduction pack

[0.7.87] — 2026-05-13

Theme: MCP token-reduction pack — five new MCP tools (workspace_search, file_outline, git_state, subsystem, last_audit_summary) collapse common Bash/Read patterns into structured envelopes, plus a token-saving hook pack and a provider-registry consolidation that makes the next tool a one-line add. Cumulative session saving on a typical /indie-review + /audit workflow: ~50-100 K tokens.

Changed

  • ANTS-1253 — Consolidate MCP-tool provider registry. Replaces the 12 per-tool setXProvider/m_xProvider setter+member pairs in ClaudeIntegration (each added by ANTS-1244 / 1247-1251) with a single registerToolProvider(QString name, ToolHandler handler) surface backed by std::map<QString, ToolHandler> m_toolProviders, where ToolHandler = std::function<QString(const QJsonObject&)>. The 92-line tools/call else if (toolName == "X" && m_XProvider) chain in claudeintegration.cpp collapses to one inline branch for get_session_info (the documented carve-out — it reads ClaudeIntegration's own state, not an external delegate) plus a single m_toolProviders.find(toolName) lookup. MainWindow::setupClaudeMcpProviders now makes 12 registerToolProvider("name", lambda) calls; each lambda absorbs the dispatcher-side argument extraction + result formatting that previously lived in the per-tool dispatch case. No behaviour change. Source delta: −157 LoC across claudeintegration.{h,cpp} + mainwindow.cpp. Spec: docs/specs/ANTS-1253.md (5-loop cold-eyes pass, ship-ready). New regression test: tests/features/mcp_provider_registry/ (10 invariants source-greped, pre-fix red verified — 9/10 fail against the unrefactored tree; INV-10 carve-out preservation passes both sides).

Added

  • ANTS-1254 — last_audit_summary MCP tool. New read-only tool that opens the latest .audit_cache/audit-*.sarif and returns a compact summary: counts (error/warning/note/suppressed) plus top_findings[] sorted by SARIF level desc → confidence desc → file asc → line asc. Default top_n=5, severity_floor="warning"; server-clamps top_n to [0, 50]. Saves ~5-15 K tokens per audit consultation vs reading the HTML report (which today's flow uses). Backed by the new AuditEngine::summariseSarif(path, topN, levelFloor) pure parser + RemoteControl::cmdLastAuditSummary with a single-entry mtime-keyed cache ((path, mtime, topN, floor) 4-tuple). Latest-SARIF discovery uses lex-max filename (audit-YYYYMMDD-HHmmss.sarif is sortable at second granularity); html_path derives via extension swap, falling back to lex-max audit-*.html within ±60 s of the SARIF timestamp (the SARIF and HTML export buttons each call QDateTime::currentDateTime() independently). Severity resolution: rule-index lookup (runs[0].tool.driver.rules[].properties.severity) with fallback for foreign SARIF (error→CRITICAL, warning→MAJOR, note→INFO). Per-tab gate inherits from resolveRootCanonical(MainWindow*). Lands on the post-1253 registry (one registerToolProvider call in setupClaudeMcpProviders, no per-tool setter). Spec: docs/specs/ANTS-1254.md (5-loop cold-eyes pass, ship-ready). New regression test: tests/features/mcp_last_audit_summary/ (10 invariants — 6 parser-side against committed fixture_min.sarif + fixture_empty.sarif; 4 wiring-side via source-grep). Pre-fix red verified — summariseSarif symbol absent from stashed engine fails build.

  • ANTS-1252 — Token-saving hook pack. Five bash hooks plus tools/install-hooks.sh that nudge Claude Code toward cheaper MCP tool calls. SessionStart preamble emits a ≤ 500 B branch/ahead/last-commit summary (cap enforced via head -c). PreToolUse(Bash) veto blocks grep -r src/, git status, cat ROADMAP.md | grep etc. with reasons capped at 200 bytes, redirecting to mcp__ants__workspace_search / mcp__ants__get_git_status / mcp__ants__roadmap_query. Per- command escape hatch: trailing # ants-bypass comment, stripped before pattern match (INV-12 — never appears in the emitted reason text). PreToolUse(Read) veto blocks full reads of ROADMAP.md > 50 KiB; bypassed cleanly via offset/limit args. Stop hook backgrounds an ants-helper drift-check under flock (INV-11) with a sane-toplevel marker write (INV-9). PreCompact hook walks the transcript JSONL and writes the most recent TodoWrite snapshot to ~/.cache/ants-terminal/precompact_<sessionId>.json only if sessionId matches ^[a-zA-Z0-9_-]{1,64}$ (INV-2). Per-project gate via committed .ants-project marker — non-ants sessions exit silently in sub-millisecond stat ascent (no git rev-parse fork). Install-hooks hardening: lstat symlink abort (INV-5), cp --no-dereference backup, sentinel-key fence ants_hooks_pack_v1 rather than text-fence comments (INV-6 — jq strips JSON comments), tmpfile + jq empty validate- before-rename (INV-8), idempotent re-install, --dry-run and --uninstall flags. Token saving: ~30-100 K/week depending on which siblings shipped first (calibrated in spec § 5). Conformance harness at tests/features/hook_pack/test_hooks.sh (shell-driven per audit_self_test.sh pattern, no C++ link) covers INV-1/2/3/4/7/9/10/12 + bash-veto behaviour + read- roadmap-veto behaviour + install round-trip + symlink abort — 36 assertions, all green; INV-6/8/11 deferred to manual smoke with documented rationale. Spec: docs/specs/ANTS-1252.md. Cold-eyes review on tests/features/hook_pack/spec.md returned 7 findings (1 HIGH on source-vs-runtime semantics for INV-12, 2 MEDIUM on doc-code drift, 4 LOW); all fixed inline.

  • ANTS-1251 — subsystem MCP tool (consolidated; map / files / recent_changes via op discriminator). Pre-parses the project's CLAUDE.md ## Module map (src/) H2 into a lanes[] array and serves per-lane chunks so /indie-review reviewers don't each re-read the file. Three ops: map returns [{name, summary}, …], files returns the lexicographically-sorted src/<lane>* glob, recent_changes returns merged-by-sha git log entries across every file in the lane. Cache is mtime-only on CLAUDE.md (no wall-clock TTL — concurrent reviewers share warm cache). Defensive parser drops bullets that don't match the `name` — summary shape and splits multi-name bullets like `a` / `b` into one Lane each. Hardening: lane membership check precedes any filesystem call (closes cold-eyes S1251-1 path-traversal vector); per-result canonical-startsWith re-check on resolved files (defence in depth against malicious symlinks inside src/). Composes cmdGitState({op:"log", path:<file>}) per lane file for the recent_changes op — no duplicated git plumbing. Distinct error codes bad_op (input enum) and unknown_lane (with lanes:[…] echoed in the response so the caller can recover). Single setter / member / provider-lambda triple on ClaudeIntegration + MainWindow. Locked by feature test mcp_subsystem/ (12 invariants — decl, INV anchors, IPC dispatch, MCP tools/list schema with op enum + op in required[], MCP tools/call dispatch, header surface, mainwindow lambda, op-switch literals, error-code surfacing, cmdGitState composition, parser surface, CMake wiring, and the ≥ 15-lane CLAUDE.md parser floor). Token math: ~24 K saved per /indie-review run (6 reviewers × ~3.5 K → ~250 each); permanent schema cost ~115 tokens. Side-effect: mcp_workspace_search test INV-3c now uses a word-boundary regex (\bsystem\() instead of substring match so cmdSubsystem( is not false-flagged as a shell escape.

  • ANTS-1250 — git_state MCP tool (consolidated; status / log / diff via op discriminator). Replaces multiple Bash calls to git status, git log, git diff with a single structured tool. Cold-eyes pass 2 collapsed three originally-proposed verbs into one to save ~240 permanent schema tokens per session start; per- call savings are ~14–300 tokens depending on op. Status returns {branch, upstream, ahead, behind, files:[{path,index,worktree}], untracked[]} parsed from --porcelain=v1 -b. Log returns {commits:[{sha,subject,date,body?}], truncated} parsed from unit-separator-framed --pretty=format:, with n+1 probe to detect truncation and per-body 1 KiB cap when body:true. Diff returns {files:[{path,added,removed}], totals} parsed from --numstat, with binary files surfaced as null added/removed. Hardening: shell-less QProcess::start("git", QStringList...) via the new gitwrap.{h,cpp} synchronous helper (5 s terminate

    • 200 ms grace → kill, 4 KiB stderr cap); strict regex on range excludes leading - from any rev-component (closes cold-eyes S1250-1 flag-injection); -- argv separator before every user-derived positional arg, with ./ prefix on --leading paths; canonical-startsWith path-escape guard for path mirroring ANTS-1248's lane check; distinct error codes bad_op, bad_range, bad_path, git_failed, git_missing, not_git_repo. Locked by feature test mcp_git_state/ (13 invariants spanning decl, INV anchors, shell-lessness in gitwrap.cpp, IPC route, tools/list schema with op enum + required:["op"], tools/call dispatch, header decl + member, provider lambda, op-dispatch chain, the stricter regex literal, the 2-tier kill constants, and CMake wiring). Spec: docs/specs/ANTS-1250.md.
  • ANTS-1249 — file_outline MCP tool. Returns a compact {header_doc, symbols:[{line, kind, name, signature}], total_lines, total_bytes} envelope for a single file instead of a full Read. ~13-39× compression on typical C++ source (e.g. auditdialog.cpp 67 K tokens → ~2 K). New src/fileoutline.{h,cpp} translation unit hosts a 6-regex scanner (C++ member / type / free-func / Qt-marker + Python + Markdown headings). Each regex is a static const QRegularExpression with .optimize() invoked at first use, so the JIT compiles once per process. Possessive quantifiers

    • per-line 1024-byte cap bound the worst case against catastrophic backtracking. Header-doc capped at 2 KiB. path argument is canonicalised + NFC-normalised + checked against the project root via startsWith (same security posture as ANTS-1248's lane). Locked by feature test mcp_file_outline/ (10 invariants: wiring + runtime floor of ≥ 8 symbols against the in-tree auditdialog.cpp to catch regex-set regressions, plus not-found path). Spec: docs/specs/ANTS-1249.md.
  • ANTS-1248 — workspace_search MCP tool (ripgrep wrapper). Replaces typical Bash grep -r ... src/ patterns with a structured {ok, matches:[{file,line,text}], truncated, elapsed_ms} envelope. Shell-less argv (QProcess::start("rg", QStringList...) — never bash -c, never a single-string overload). Server clamps max_results at 500 (default 50); lane and glob NFC-normalised and rejected if they contain control chars / backslash / .. segments / parent-traversal past the project root (canonical-startsWith check via QFileInfo::canonicalFilePath()); 4 KiB stderr cap surfaced only on ok:false; 2-tier hard kill (2 s terminate() then 200 ms grace → kill()) so a catastrophic regex can't outlive the wall budget. Estimated saving ~6–15 K tokens per typical bug-investigation session at ~150 permanent schema tokens. Locked by feature test mcp_workspace_search/ whose 10 invariants cover decl, INV anchors, shell-lessness, IPC route, tools/list schema (with required: ["pattern"]), tools/call dispatch, header decl + member, provider lambda, the literal ripgrep flags (--json, --no-heading, --line-number, --max-columns, --threads), and the 2-tier kill wiring. Spec: docs/specs/ANTS-1248.md.

v0.7.86

0.7.86 — MCP integration end-to-end + inputSchema compliance

Theme: MCP integration end-to-end — Claude Code sessions inside Ants tabs can now call the in-process MCP server. Three internal remote-control verbs (roadmap_query, tab_list, get_text) are promoted to MCP tools; a Python stdio bridge (tools/mcp-bridge.py) connects Claude Code's stdio MCP client to the Ants QLocalServer Unix socket; and a Zod-compliance fix repairs the silent zero-tools failure that the bridge surfaced on its first probe. Plus a Tasks chip semantics tidy-up and an optional status filter on roadmap_query that cuts typical "what's next" queries by ~7×.

Added

  • ANTS-1244 — roadmap_query / tab_list / get_text as MCP tools. Wires three existing RemoteControl IPC verbs as MCP tools so Claude Code sessions inside Ants tabs can query terminal state via tool-call rather than Bash / Read. The handlers (cmdRoadmapQuery, cmdTabList, cmdGetText) are promoted to public on RemoteControl; MainWindow lambdas in setupClaudeMcpProviders delegate directly, sharing the existing roadmap-query mtime cache. Token saving per session-with-roadmap-need: ~110 K tokens (a 120 K Read of the 482 KiB ROADMAP.md → ~13 K of structured JSON for 397 status-emoji bullets at ~133 B/bullet measured). Per-call saving on tab_list / get_text: 30–100 tokens of bash-glue Claude no longer composes. Locked by 7-invariant source-grep test mcp_extra_tools/. Spec converged across 7 cold-eyes loops.

  • ANTS-1247 — status filter on roadmap_query MCP tool / IPC verb. Optional status argument ("all" default, "active" = 📋+🚧, "shipped" = ✅; case-insensitive) on the roadmap_query MCP tool. On this repo at ship time: "active" returns 57 of 399 bullets (~1.75 K tokens vs ~12 K), a ~7× saving per "what's next" query. Provider lambda widened to thread the filter through; cache continues to hold the full unfiltered array (filter runs post-cache). Pack-mate with ANTS-1248..1252 (spec-only) and ANTS-1253/1254 (planned follow-ups). Cold-eyes pass 2 converged across 4 lanes (performance / token reduction / security / optimisation) over 4 loops — final pass clean.

  • ANTS-1255 — MCP stdio bridge (tools/mcp-bridge.py). Unblocks the entire MCP pack on the consumer side. The Ants in-process MCP server exposes JSON-RPC over a QLocalServer Unix socket; Claude Code's MCP client speaks stdio. Until now, every server-side tool registered since ANTS-1244 was unreachable from a Claude Code session. ~100-line Python 3 stdlib script reads line-delimited JSON-RPC from stdin, opens one AF_UNIX connection per request (server is one-shot by design), forwards the request, writes the reply to stdout. Notifications (no id) are forwarded without awaiting a response. Socket selection: $ANTS_MCP_SOCKET override or newest-mtime /tmp/ants-terminal-mcp-*. Register with claude mcp add ants -- /path/to/tools/mcp-bridge.py once per machine. End-to-end verified: initialize handshake → 9 tools listed → roadmap_query status="active" → bad_status error path → clean exit on stdin close.

Fixed

  • ANTS-1246 — Tasks chip progress semantics + Mode B batch reset. Two coordinated fixes for the same user-visible symptom (the bottom-right Tasks chip showing wrong or no info during an active Claude task list). (1) Chip now reads ☰ <completed>/<total> and stays visible iff 0 < done < total, closing the visibility hole left by ANTS-1221's pending-only predicate — visible end-to-end through every active run, hides cleanly at 100 %. (2) The TaskCreate path in ClaudeTaskListTracker::parseTranscript (Mode B) piled up completed tasks forever; now, when a new TaskCreate arrives and every task in out is completed, the prior batch is cleared before appending. Mode A (TodoWrite snapshot) was already correct. Partial batches (any pending/in_progress) are preserved. Locked by 4-INV tasks_chip_done_over_total/ and 3 new behavioural cases in claude_task_list/. Spec: docs/specs/ANTS-1246.md (2 cold-eyes loops, clean pass).

  • ANTS-1256 — MCP tools/list inputSchema compliance. The six zero-arg MCP tools (get_cwd, get_session_info, get_last_command, get_git_status, get_environment, tab_list) were emitted without an inputSchema field. Claude Code's MCP client validates tools/list with Zod and rejects the entire response when any entry omits the field — the connection reports "Connected" but registers zero tools. Surfaced 2026-05-12 on the first probe through the ANTS-1255 bridge: the log read tools[1].inputSchema expected object, received undefined for indices 2/3/4/5/7 as well. Fix: a shared QJsonObject emptySchema; emptySchema["type"] = "object"; sentinel, assigned to each zero-arg tool's inputSchema. Locked by feature test mcp_tools_list_schema/ whose third invariant pins tools.append() == ["inputSchema"] = count parity inside the tools/list block — so any future tool addition that omits the field breaks CI before it reaches a user.

v0.7.85

0.7.85 — Roadmap density toggle + Qt::Popup blocker fix

Theme: Roadmap dialog density toggle (compact / cozy / comfortable) — Material 3 / Linear / GitHub-style density selector that persists across sessions. Plus a regression fix that surfaces while building it: Qt::Popup widgets (combo dropdowns, menus, color pickers) opened from inside a dialog were being eaten by the ANTS-1051 pseudo-modal blocker. The fix is one short-circuit in dialogfocus::shouldSuppressEvent ForDialog and benefits every dialog that hosts a popup, not just the new density combo.

Added

  • ANTS-1238 — Roadmap dialog density toggle (compact / cozy / comfortable). Material 3 / Linear / GitHub all expose a density selector; the Roadmap dialog gains one to match. A new QComboBox lives at the trailing edge of the filterRow (after the existing addStretch(1)) with three options: compact (9/10/11/14 px text tier, proportional padding), cozy (the pre-1238 baseline — 10/11/12/13/16 px), and comfortable (12/13/14/15/18 px). Selection drives a CSS class on renderCardsHtml's root container; per-tier sizes/padding are defined in STYLE_COMPACT / STYLE_COZY / STYLE_COMFORTABLE. Persists via a new roadmap_density Config key, with graceful fallback to "cozy" on missing/invalid value and silent best-effort on persistence-write failure (matches the existing Config write convention). The default is cozy, byte-equal to the pre-1238 baseline render — INV-1 in the spec's test bundle locks that. Regression-locked by ANTS-1238-INV-1..9 in tests/features/roadmap_density/spec.md (tier-unique sentinels 9/16/18 px scoped to renderCardsHtml; combo present and wired; Config getter/setter round-trip; persistence-failure contract). No keyboard shortcut in v1 — the ?-cheatsheet (ANTS-1236) doesn't gain a row. Spec: docs/specs/ANTS-1238.md (cold-eyes-clean after 4 loops + ~50 verified findings fixed; full audit trail at ROADMAP.md ### 📝 Cold-eyes 2026-05-12 (ANTS-1238 spec)).

Fixed

  • ANTS-1051 — Qt::Popup events were swallowed by the pseudo-modal blocker. Surfaced by the ANTS-1238 density combo: clicking an option in the dropdown popup did nothing — the popup didn't even close. Root cause: a Qt::Popup window (QComboBox dropdown, QMenu context menu, color-picker popup, …) is its own top-level widget, and QWidget::isAncestorOf stops walking at window boundaries — so popup descendants opened FROM inside a dialog read as "outside the dialog's tree" and the pseudo-modal blocker (ANTS-1051) ate every mouse event on them. Fix is a one-block short-circuit at the top of dialogfocus::shouldSuppressEventForDialog: if QApplication::activePopupWidget() is non-null and target is either that popup or a descendant of it, return false immediately. Regression-locked by a new INV-2i in tests/features/dialog_pseudo_modal/spec.md — source-grep-asserted rather than behaviourally driven because the offscreen QPA platform doesn't promote Qt::Popup widgets to activePopupWidget(). Affects every dialog that hosts a combo / menu / color picker, not just the Roadmap density combo — Settings, AuditDialog, etc. all benefit transparently.

v0.7.84

0.7.84 — RoadmapDialog v2 finishing pass

Theme: RoadmapDialog v2 finishing pass — keyboard ergonomics (/-focus, Esc-clear, ?-cheatsheet), accessibility (status and theme glyph labels for screen readers), and a freshness signal ("Updated N days ago" on in-progress cards). Plus theme-aware frameless title bars + dark-theme link fixes across every dialog.

Added

  • ANTS-1237 — "Updated N days ago" line on 🚧 cards in the Roadmap dialog. Surfaces stall signal at-a-glance: every 🚧 in-progress card now shows when its bullet block was last touched, mirroring GitHub Projects / Linear / Jira's default "last activity" affordance. Derived from one git blame --line-porcelain call against ROADMAP.md (cached by mtime via the same pattern as parseShippedDates), then MAX(author-time) over each - 🚧 [ANTS-NNNN] bullet's line + its 2-space-indented continuation lines. Renders as <span class="rm-date">· Updated <X></span> inline with the ID chip, where <X> is one of: today / yesterday / Nd ago (2-13d) / Nw ago (2-8w) / Nmo ago (2-12mo) / Ny ago (≥1y). Reuses the existing .rm-date CSS class — no new style rule. Status-gated (✅ already shows shipped date; 📋/💭 don't need it). Graceful degradation on non-git checkouts: git blame failure → empty hash → no "Updated" line, mirroring the ✅-card path when shipped date is absent. ID mentions outside the bullet block (e.g. audit-trail prose in another card's body) do NOT contribute to MAX — the walker only enters block-mode at lines matching ^- 🚧 \[ANTS-NNNN\]. Cost: ~175 ms blame call on first dialog open per session against the current 8723-line ROADMAP, cached thereafter; ~4 MB transient peak for the porcelain output (verified by wc -c); ~1 KiB persistent per dialog at realistic 5-15 🚧 bullet counts. Regression- locked by ANTS-1237-INV-1..8 in tests/features/roadmap_inprogress_age/spec.md (renderer- layer + parser-layer test split; parser tests early-skip with GTEST_SKIP() when git is not on PATH). Spec: docs/specs/ANTS-1237.md (cold-eyes-clean after 4 loops + ~40 verified findings fixed; full audit trail at ROADMAP.md ### 📝 Cold-eyes 2026-05-11 (ANTS-1237 spec)).

  • ANTS-1234 — Roadmap dialog /-focus + Esc-clear + body-only auto-expand. Three keyboard-first ergonomics additions to the Roadmap dialog's existing substring search box. (1) Pressing / anywhere in the dialog focuses the search box and select-alls any existing predicate so the next keystroke replaces it (Linear / GitHub / Notion / VS Code / Slack convention). Layout-robust via event->text() == "/"; gated on !m_searchBox->hasFocus() so the user can still type / into the predicate (URLs, path tokens). (2) Pressing Esc while the search box has focus clears the predicate and removes focus — one press undoes a search, a second press closes the dialog as before. Implemented via installEventFilter(this) on the search box + a dialog-side eventFilter override that consumes the Escape KeyPress before it can reach QDialog::reject. For every non-Escape key, the filter falls through so the QLineEdit receives PgUp / F5 / typing characters normally. (3) Cards whose match lives only in the body continuation prose (not in ID / headline / Layman) auto-expand for that render so the matched substring is visible in context. Render-time only; m_expandedItems is never mutated, so clearing the search reverts every auto-expanded card to its prior user-driven state. The id:NNNN jump shortcut also auto-expands the matched card. The cheatsheet (ANTS-1236) gains a / row in lockstep — kRoadmapShortcuts[] bumps 9 → 10 rows and the static_assert + test assertions all move 9 → 10 in the same commit. Cost: +4 case-insensitive QString::contains calls per card per render (~0.3 ms on 200 cards, well below the 120 ms search debounce). Regression-locked by ANTS-1234-INV-1..9 in tests/features/roadmap_search_keybinds/spec.md. Spec: docs/specs/ANTS-1234.md (cold-eyes-clean after 7 loops + ~25 findings fixed).

  • ANTS-1236 — Keyboard-shortcut cheatsheet in the Roadmap dialog. Press ? inside the Roadmap dialog to open a sub-dialog listing every keyboard shortcut it ships today. Mechanism: a file-scope kRoadmapShortcuts[] data table in src/roadmapdialog.cpp is the single source of truth; roadmapShortcutRows() exports it to RoadmapShortcutsDialog (src/roadmapshortcutsdialog.{h,cpp}), which renders a two-column QTableWidget (Shortcut / Action). 10 shortcuts at ship after ANTS-1234 added /: ?, /, Esc, F5, Ctrl+C, Ctrl+A, ↑ ↓, PgUp PgDn, Home End, Tab Shift+Tab. The trigger uses event->text() == "?" (layout-robust — AltGr / dead-key paths on non-US layouts still hit it) and gates on !m_searchBox->hasFocus() so the user can still type ? into the substring filter. The overlay is lazy + reused (one instance via QPointer), inherits the active terminal theme through DialogChrome, and announces via setWindowTitle(tr("Roadmap Keyboard Shortcuts")). The static_assert(std::size(kRoadmapShortcuts) == 10, …) guard locks the row count to the test's exact-10 assertion so adding a future shortcut must bump the test in lockstep. Regression-locked by ANTS-1236-INV-1..8 in tests/features/roadmap_shortcuts_cheatsheet/spec.md. Spec: docs/specs/ANTS-1236.md (cold-eyes-clean after 7 loops + ~27 findings fixed).

  • ANTS-1235 — Accessible status / theme glyph labels in the Roadmap dialog. Screen readers (Orca / NVDA / VoiceOver) announce "✅" as "white heavy check mark" by default — useless as a scan cue on a several-hundred-bullet roadmap. The fix emits a short, lowercase text label inline alongside each status emoji on every card: <span class="rm-state">✅</span> <span class="rm-state-label">shipped</span> and friends. Section-header count chips gain trailing words too — ✅ 47 shipped · 🚧 2 in progress · 📋 3 planned · 💭 8 considered in place of the prior bare ✅ 47 🚧 2 …. Filter checkboxes get setAccessibleName() setters ("Show shipped items" etc.) so Orca speaks the verb-led form instead of the visible label. The visible ✅ Done checkbox label is renamed ✅ Shipped for vocabulary consistency with the rest of the roadmap-format standards (§3.3 accepts both terms). Theme glyphs (🎨/⚡/🔌/🖥/🔒/🧰/📚/📦/🐛/🔍/🧹) stay unlabelled because the heading text they prefix already labels them. Verified mechanism: HTML aria-label is dead on arrival in Qt 6 QTextBrowser (parser strips unknown attributes; QAccessibleTextInterface only reads toPlainText()), so any a11y solution must put the label in the rendered text. docs/standards/documentation.md gains a new Accessibility section documenting the Qt 6 a11y path so future contributors don't repeat the aria-label investigation. Regression-locked by INV-18..24 in tests/features/roadmap_dialog_cards/spec.md (302/302 tests pass). Spec: docs/specs/ANTS-1235.md. Cold- eyes audit trail: ROADMAP.md ### 📝 Cold-eyes 2026-05-11.

Fixed

  • ANTS-1242 — Theme-aware frameless title bar on every dialog. Fourth-pass user feedback after ANTS-1241 (2026-05-11): the Roadmap dialog's window-manager-drawn title bar still showed as the system grey, not the active terminal theme — because KWin (and GNOME / Wayland) paints server-side window decorations from the system colour scheme and ignores the application's QPalette. The main terminal window already worked around this by going frameless (Qt::FramelessWindowHint) and drawing its own TitleBar widget; dialogs did not. Fixed by introducing a shared DialogChrome helper (src/dialogchrome.{h,cpp}) that any dialog ctor can invoke with one line: auto chrome = DialogChrome::install(this, themeName); — it sets the frameless flag, prepends a themed TitleBar widget, wires close / minimize / maximize signals, and returns a content QWidget to use as the layout parent for the rest of the ctor. Applied to all ten QDialog subclasses: RoadmapDialog, AuditDialog, SettingsDialog, SshDialog, AiDialog, ClaudeAllowlistDialog, ClaudeTranscriptDialog, ClaudeProjectsDialog, ClaudeBgTasksDialog, and ClaudeTaskListDialog. MainWindow::applyTheme now also calls DialogChrome::setActiveTheme(name) so dialogs that don't receive a theme name still pick up the current one. Live-theme refresh is preserved on the dialogs that previously supported it (Roadmap, bg tasks, task list) — the helper's applyTheme(dlg, bar, name) re-styles both the dialog palette and the title bar.
  • ANTS-1241 — RoadmapDialog v2: inline #NNNN on summary row + larger ID font. Third-pass user feedback after ANTS-1240 (2026-05-11): the card's #NNNN (and shipped date) were emitted on a separate <div class="rm-meta"> row below the summary line, which (a) hit the same Qt nested-block QPalette::Base frame issue that ANTS-1240 fixed on the expanded body — painting a darker band under the ID on dark themes — and (b) was visually noisy. Fixed by emitting the hashed ID and shipped date as inline <span> children of the card div, on the summary row immediately before the rm-toggle anchor. CSS .rm-id font bumped from the old 10 px meta size to 12 px so the number is scannable at a glance. The .rm-meta CSS rule is gone. Regression-locked by spec INV-17 in tests/features/roadmap_dialog_cards/spec.md.
  • ANTS-1240 — RoadmapDialog v2: theme palette + expanded-body background frame. Second-pass user feedback after the ANTS-1239 link-colour fix (2026-05-11):
    • Dialog doesn't pick up the active theme. Only the QTextBrowser's QPalette::Link / LinkVisited were themed in ANTS-1239; the QDialog window, the QListWidget TOC sidebar, and the viewer's Base / Text roles all fell through to Qt's default dark palette. So the dialog looked greyer than the rest of the app (e.g. against the Tokyo Night / Dracula / One Dark navy backgrounds). Fixed by applying bgPrimary to QPalette::Window / Base on all three widgets, textPrimary to WindowText/Text, and accent to the TOC's Highlight so the selection indicator stays visible.
    • Expanded body has a different background colour from the card. Qt's text engine renders nested <div> block elements with their own QPalette::Base background frame — it does not visually inherit from the parent <div>'s background CSS. So the renderer's <div class="rm-body"> wrapper inside each <div class="rm-card"> painted bgPrimary over the card's bgSecondary background, creating a visible colour break at the divider. Fixed by emitting body <p> paragraphs directly as children of the card (no wrapping div) — <p> doesn't create a separate background frame, so each paragraph paints over the card's bgSecondary and the visual continuity is restored. First body paragraph carries class="rm-body-first" (dotted divider + extra padding-top); subsequent lines carry class="rm-body-line" (indent only). Regression-locked by spec INV-15 + INV-16 in tests/features/roadmap_dialog_cards/spec.md.
  • ANTS-1239 — RoadmapDialog v2: duplicate heading slugs + black <a> text on dark themes. Two bugs surfaced by user test-drive of 0.7.83's card renderer:
    • Slug collision. Three ### Performance h3s (under 0.7.0, 0.8.0, and Beyond 1.0) all slugged to performance, so expanding one expanded all three, and bySection["performance"] pooled bullets from every Performance section into a single bucket. Fixed by tracking a seen set across each walk and appending -2, -3, … to repeat slugs (uniqueSlug helper). Both parseBullets and renderCardsHtml walk the same sourceText in the same order, so their slug sequences agree and bySection[slug] keys match the URLs emitted into the click anchors. Same fix covers other duplicate h3s in the file (Platform, Cross-cutting themes, Security, Tier 2 — hardening sweep, …).
    • Black section titles on dark themes. Qt's text engine paints <a> foreground using the widget's QPalette::Link role, ignoring the inline <style> block's a{color:…} rule and not propagating through color:inherit. On Qt 6's default palette that role renders as near-black on most dark themes, so the chevron + heading text in each section header disappeared. Fixed by setting QPalette::Link / QPalette::LinkVisited on the QTextBrowser to the active theme's textPrimary, plus emitting explicit color:<textPrimary> on .rm-section-toggle / .rm-section-title (replacing the prior color:inherit) as belt-and-braces. Regression-locked by spec INV-13/14 in tests/features/roadmap_dialog_cards/spec.md.

v0.7.83

Theme: RoadmapDialog v2 ships — card-style rendering for glance-friendly scanning of what's done, in-progress, and outstanding. Live-iterated visual polish (status counts lead section titles, parent-h2 breadcrumb on h3 sections, Table-of-Contents h2 suppressed inside the content pane, section headings + chevron both clickable) plus a Wayland Copy-from-roadmap fix.

Added

  • ANTS-1154 — RoadmapDialog v2: card-style rendering. Layman: the Roadmap dialog now shows each item as a scannable card with a big state icon (✅/🚧/📋/💭), a type chip (⚙ implement, 🐛 fix, etc.), a one-sentence summary, and a meta row carrying the ID and the shipped date — instead of a wall of prose. Sections start collapsed with status-count chips leading the title (✅ 4 🚧 1 Performance) so a partially-sighted scan reads the counts before parsing the name. Click the chevron or the heading text itself to expand/collapse. Non-Full tabs (History / Current / Next / Far Future) now strip prose intros and empty sections so you only see cards relevant to that tab. h3 sub-sections show their parent h2 as a breadcrumb (Performance · 0.7.0 — shell integration) to disambiguate orphan headings that recur under multiple releases. The Table of Contents h2 is suppressed inside the content pane — the QListWidget sidebar already serves that role. Per-item + per-section expand state persist via four new Config::roadmap* keys. New optional Layman: line in any roadmap bullet body — when present, shown on the card face instead of the bold headline. Two new footer buttons: Refresh (F5) and Reset View (two-click confirm). Shipped scope is presentation-only; the original "tagged-text format v2" portion of the charter was deferred (see §1156 sub-(2) in ROADMAP for the spin-out). All existing roadmap_viewer* / roadmap_kind_facets / remote_control_roadmap_query tests stay green — renderHtml / parseBullets were extended additively, not replaced; the new card renderer is a sibling helper named renderCardsHtml. New feature test roadmap_dialog_cards wired into the existing test_dialogs bundle (no new add_executable per ANTS-1217). Source: user-2026-05-02 (original) + user-2026-05-11 (reframe). ([ANTS-1154])

Changed

  • docs/standards/roadmap-format.md §3.5 now documents the optional Layman: <one-sentence>. line. Additive on v1 — older parsers ignore it. No format-version pragma bump. Authors add Layman: lines opportunistically as they touch bullets; the bold headline remains the fallback when absent. ([ANTS-1154])

Fixed

  • RoadmapDialog Copy on Wayland. The QTextBrowser's automatic context menu was a no-op on Wayland: right-click → menu appears, Copy does nothing, outside-click can't dismiss it. Same QTBUG-79126 family that bit QDialogButtonBox::Close in 0.7.49 — the auto-popup attaches to a surface the compositor never grants input to. Switched to Qt::CustomContextMenu with a dialog-parented QMenu so the popup attaches to a stable xdg surface; Copy + Select All now work and outside-click dismisses. ([ANTS-1154])

v0.7.82

Theme: live-repro fix bundle from the 2026-05-10 session — two real bugs surfaced when the user /compact'd a Claude Code session, /exit'd, then "Continue previous coding session"'d back in. The Tasks chip kept showing five stale pre-compact tasks (ANTS-1224, a parser miss on the isCompactSummary checkpoint) and the bottom-bar Claude: status chip stayed hidden until a tab-switch (ANTS-1225, a PID-replacement gate too narrow to catch a Claude exit + respawn inside the 2-second poll window). Plus the session-isolation wiring lock-in (ANTS-1219) — source-grep contract that catches future drift in the resolver → tracker rebind path before the user does.

Fixed

  • ANTS-1224 — Task List parser ignored isCompactSummary checkpoint, so pre-compact tasks survived into post-claude --resume state. Layman: after you /compact'd a Claude session, exited, and chose "Continue previous coding session", the Tasks chip kept showing the todos from before the compact — a phantom list of work Claude already considered closed. Root cause: parseTranscript walked every JSONL event but never observed the isCompactSummary: true checkpoint, so any TaskCreate with no terminal TaskUpdate from the pre-compact phase counted forever. Fix: inside the per-line loop right after the sidechain filter, treat isCompactSummary:true as a state-reset — clear out, idxByToolUseId, and sawTodoWrite before the type-dispatch, so the checkpoint event itself contributes zero entries. Multiple checkpoints converge on "state after the final one" by induction. Zero new persistent state; all four new test cases wired into the existing test_claude bundle (no fresh exe, mindful of the ANTS-1217 build-OOM guardrails). Source: user-2026-05-10 live reproduction. ([ANTS-1224])
  • ANTS-1225 — Claude status indicator stayed hidden after /compact + /exit + "Continue previous coding session" until a tab-switch nudged it. Layman: the bottom-bar Claude: <state> chip disappeared whenever a new Claude process replaced the one that just exited inside the 2-second poll window, and only came back when you switched tabs and back. Root cause: pollClaudeProcess rebound the transcript only when m_claudePid == 0, which catches first-launch but not live PID replacement — between two polls a Claude could exit and a new one spawn, leaving m_claudePid holding the stale dead PID and the rebind branch never firing. Tab-switching worked by accident because setShellPid zeroes m_claudePid as a side-effect. Fix: generalise the gate to m_claudePid != foundPid, which catches both initial detection and live replacement uniformly. Diagnostic signature in the debug log (procStartMs=0 for every sessionPathForCwd/result: line on the stale PID) explains why the tasks chip kept working while the status indicator was broken — the freshness resolver degrades to recency-only mode and still finds the right JSONL, but m_claudePid correctness is what the status indicator depends on. Zero new persistent state. Source: user-2026-05-10 live reproduction, same session as ANTS-1224. ([ANTS-1225])
  • ANTS-1219 — Task List session-isolation wiring locked in via source-grep spec. Layman: the original "stale tasks after a Claude session change" report turned out to be three different bugs in a trenchcoat. ANTS-1224 and ANTS-1225 above fix the two surfaces that actually reproduced; this entry locks in the wiring contract for the underlying flow (resolver → tracker rebind → status emission) so any future drift in mainwindow.cpp / claudestatuswidgets.cpp surfaces as a red build rather than a user-visible regression. New tests/features/claude_task_list_session_isolation/ (source-grep, no GUI, wired into the existing test_claude bundle) anchors six // ANTS-1219-INV-N comments to load-bearing call sites — same shape as the ANTS-1161 per-tab gate test that locked in the session-id discriminant. Source: user-2026-05-10. ([ANTS-1219])

v0.7.81

0.7.81 — tasks-chip semantics polish + About-dialog build context

Theme: small follow-ups to the 0.7.80 user-feedback batch — a two-fix bundle that finishes the Tasks-chip semantics arc started in ANTS-1216, plus a long-requested About-dialog enhancement that surfaces enough build context to triage bug reports without a "what build are you on?" round-trip.

Added

  • ANTS-1222 — Help → About Ants Terminal… now shows a Build line. The About box used to list only the version and the Qt runtime; bug reports often needed a follow-up "what build are you on?" round-trip. The dialog now also displays a Build: line with the build date, build type (Release/Debug/RelWithDebInfo), short git commit (or unknown for tarball builds), and the compiler used (GCC or Clang with version). Implementation: a CMake configure_file() block at the top of CMakeLists.txt writes build/generated/build_info.h at configure time; the compiler ID is detected at compile time inside aboutdialogs.cpp so it reflects the actual toolchain (env-var overrides, distcc, distro alternative chains) rather than what CMake found. Reconfigure (cmake build/) to refresh the SHA between commits.

Changed

  • ANTS-1218 + ANTS-1221 — Tasks chip now shows progress count and hides at 100%. The bottom-right ☰ X/Y chip used to mean "X tasks left out of Y" (counted down) and stayed lit while a single in-flight Claude task remained. It now means "X done out of Y" (counts up, like every other progress display in the app) and disappears as soon as nothing is waiting on you. Two bundled changes: (1) ClaudeTaskListTracker::unfinishedCount() now counts only pending tasks — a task Claude is actively working on is Claude's problem, not yours, so it no longer keeps the chip visible. (2) The chip's numerator is now total - unfinished instead of unfinished, so X/Y reads like a GitHub-PR-checks progress bar. Pre-existing unfinished <= 0 hide branch (from ANTS-1216) still fires — combined effect: chip cleanly hides at 100%, monotonically increases as tasks complete, and never surfaces a single in-flight task as actionable.

CI is now building the AppImage and other release artifacts via release.yml; they'll attach to this release page once the workflow completes (typically a few minutes on the public-repo Linux runners).

v0.7.80

Theme: post-0.7.79 follow-ups + small high-signal user-feedback fixes plus a build-OOM cure. Three notification/UI false-positives that made the desktop feel noisy in normal use; a per-tab Claude: <state> widget bug that mis-attributed state across tabs; two refactors carried over from the 0.7.78 indie-review #2 sweep that finally land here (setupMenus per-menu carve-out and the Tier 4 Qt6 idiom polish); and ANTS-1217 — src/*.cpp refactored into STATIC libraries, build-time guardrails (ccache / CMAKE_OPTIMIZE_DEPENDENCIES / JOB_POOLS cap / Debug -gsplit-dwarf) so a cmake --build build -j$(nproc) cannot OOM a 32 GiB host any more. Full ANTS_TESTS=ON build now peaks at 526 MiB RSS (was >32 GiB OOM). 144/144 ctests green; 1/1 new claude_status_bar_per_tab feature test green.

Fixed

  • ANTS-1160 P2 — RoadMap status-bar button + GitHub repo-type badge stayed hidden on launch until the user manually switched tabs. Both widgets refreshed only via onTabChanged; the first call at startup saw shellPid() == 0 and shellCwd() == "" (the shell hadn't started yet), the widgets hid, and nothing re-fired until a tab switch. Same shape as the refreshBgTasksButton fix from 0.7.49. Fix: wire both refreshRoadmapButton and refreshRepoVisibility to the 2 s m_statusTimer alongside the existing review/bg-tasks/tasks refreshes (mainwindow.cpp:711-712), so a stale-hidden state recovers within one tick of shellCwd() resolving. New feature test tests/features/roadmap_status_bar_refresh/ locks both widgets in via source-grep on the timer connections. This is ANTS-1160 Phase 2 (status-bar bug fixes); the broader Roadmap dialog redesign remains in progress (see ROADMAP § Roadmap dialog redesign + format spec v2).
  • ANTS-1161 — bottom Claude: <state> status-bar widget showed another tab's state. Layman: with two Claude Code tabs open, the bottom-of-window status text could read "Claude: bash" on a tab whose Claude was actually thinking, because a sibling tab running a Bash tool spammed its hook events into the singleton's state. The per-tab dot indicator was already correct (it derives state from each shell's own transcript). Root cause: the hook server is one Unix socket shared across every Claude under any tab, and ClaudeIntegration::processHookEvent mutated m_state / m_currentTool and emitted stateChanged for every inbound event regardless of session_id. Fix: gate every state-mutating branch on the event's session_id matching the basename of the focused tab's transcript path (m_transcriptPath). PermissionRequest stays ungated — its slot routes per-tab via lastHookSessionId(). Pre-poll tolerance built into the gate so the bootstrap SessionStart (which fires before pollClaudeProcess resolves the transcript) still wires m_activeSessionId. New 4-invariant feature test claude_status_bar_per_tab (verified fails I1 against pre-fix code via the gate-neutralization repro).
  • ANTS-1214 — idle "Command Complete" notification fired on every long-running command, including successful watchdog ticks. User report 2026-05-08: notify-send repeating from a system-resource watchdog tab, breaking focus during normal work. Root cause: terminalwidget.cpp:checkIdleNotification() fired for any burst-then-idle pattern regardless of exit status. Fix: gate on m_grid->lastExitCode() != 0 (OSC 133 D exit-code). Successful commands and shells without OSC 133 integration stay silent; non-zero exits raise a dialog-warning-iconed "Command Failed" notification with the exit code in the body.
  • ANTS-1215 — Review Changes button stayed active after every push because of an untracked .directory (KDE Dolphin metadata). Root cause: mainwindow.cpp:refreshReviewButton() treated any non-header git status --porcelain=v1 -b line as "dirty," including untracked (??) lines. Untracked files don't appear in git diff, so clicking the button opened an empty diff viewer. Fix: skip ?? lines in the dirty count. Common false positives now silenced: .directory, IDE caches, swap files, build artefacts that aren't gitignored. Once the user git adds an untracked file it becomes A and counts again.
  • **ANTS-1216 — Tasks chip stayed visible after every task is done
    • miscount of "deleted" tasks as unfinished.** User report 2026-05-08: Task List dialog showed "27 done, 0 running, 0 outstanding" but the status-bar chip still read "☰ 1/28" and hadn't dimmed. Two stacked bugs: (1) ClaudeTaskListTracker::unfinishedCount() used status != "completed", so a deleted task counted as unfinished — the chip showed "1" when the dialog showed zero. Header-stated contract was "pending + in_progress"; implementation drifted. Fix: count only pending + in_progress. (2) refreshTasksButton only hid the chip when total <= 0, so a fully-done task list kept the chip visible at "☰ 0/N", reading as actionable chrome. Fix: also hide when unfinished <= 0.

Changed

  • ANTS-1181 — setupMenus 947-LoC monolith carved into seven per-menu helpers + About dialogs moved to their own TU. Phase A (in-place): setupMenus() is now an orchestrator of setupFileMenu, setupEditMenu, setupViewMenu, setupSplitMenu, setupToolsMenu, setupSettingsMenu, setupHelpMenu. Phase B (partial): About-Ants and About-Qt carved to src/aboutdialogs.{cpp,h} as namespaced free functions (AboutDialogs::showAboutAnts(parent) / showAboutQt(parent)) sharing an internal makeAboutDialog helper. The Wayland modal-exec workaround comment (QTBUG-79126 / QTBUG-90005) survives in the new TU verbatim. Deferred (re-evaluate when surrounding pressure increases): showSnippetsDialog and checkForUpdates stay on MainWindow — both have tight coupling that would force threading Config& + terminal-getter callbacks through free-function signatures for marginal win.
  • ANTS-1186 — Qt6 idiom polish, 4 of 5 sub-findings. (a) pluginmanager.cpp setRecentOutput / setCwd: m_engines.values() → std::as_const(m_engines) direct value-iteration; snapshot sites in unloadAll/fireEvent intentionally keep .values() per ANTS-1173 UAF defense. (b) mainwindow.cpp:4915,4955: currentDateTime().toMSecsSinceEpoch() → currentMSecsSinceEpoch(). (c) featurecoverage.cpp:392: unused QDir projectDir deleted. (d) titlebar.h:19: QColor("#e74856") → QColor::fromRgb(0xe74856). (e) Chained .arg(int).arg(int) left as-is — idiomatic for ints.
  • ANTS-1217 — build-OOM cure: STATIC-library refactor + build-time guardrails (Phase 0 + Phase 1). Pre-fix: tests/features/ had grown to 141 standalone add_executable(test_*) blocks, each carrying its own MOC autogen and its own copy of the src/*.cpp files it linked. src/mainwindow.cpp was compiled 42 times per build. Default cmake --build build -j$(nproc) peaked above RAM+swap on a 32 GiB host and tripped earlyoom — losing the linker mid-write. Phase 0: flipped ANTS_TESTS default to OFF, added Qt aggregate-header PCH (<QtCore/QtCore>, <QtGui/QtGui>, <QtWidgets/QtWidgets>) on the main exe. Phase 1: refactored src/*.cpp into 6 mandatory + 2 conditional STATIC libraries (ants_core_lib / ants_vt_lib / ants_chrome_lib / ants_claude_lib / ants_audit_lib / ants_dialogs_lib + ants_lua_lib if LUA_FOUND + ants_helper_lib if ANTS_ENABLE_HELPER_CLI); each src/*.cpp now compiles once for the whole build. Layered three belt-and-suspenders optimizations: (i) ccache as CMAKE_CXX_COMPILER_LAUNCHER (cache hits skip cc1plus entirely); (ii) CMAKE_OPTIMIZE_DEPENDENCIES=ON (Ninja skips relinking dependents whose lib interface didn't change); (iii) JOB_POOLS cap (compile_pool=nproc-2, link_pool=2) so a future regression cannot OOM the host even on a naked -j$(nproc). Plus -gsplit-dwarf on Debug builds — linker no longer relocates hundreds of MiB of debug strings. Phase 1 verified on the 32 GiB host: full ANTS_TESTS=ON build at 526 MiB peak RSS in 1m36s wall (was >32 GiB OOM); 144/144 ctests green. Spec at docs/specs/ANTS-1217.md; Phases 2–7 (test-bundle consolidation via GoogleTest + workstation preset + tools/safe-build.sh) pending.

v0.7.79

Theme: two bug-fix passes back-to-back. Pass 1 (morning, user-driven) chased a cluster of user-visible scrollback / output-pile / spinner-duplication / app-exit-crash reports — 8 ANTS items (1187..1194), one of which (ANTS-1194) turned out to be the structural root cause of three of the others. Pass 2 (afternoon, /indie-review #3) ran a scoped 4-lane review on the 8.2k-LoC terminalgrid.cpp + terminalwidget.cpp surface — the densest VT/render code in the repo — and shipped 17/17 calibrated findings (ANTS-1195..1213) in one session: 4 calibrated CRITICAL, 7 HIGH, 6 MEDIUM, INFO swarm. Headline patterns: doc-rot at scale (4 of 4 lanes flagged comments that survived a refactor and now lie about current code), and a recurring "missing soft-reset / partial parse" shape — ANTS-1194 (scroll region didn't grow on resize), ANTS-1196 (DECSTR soft- reset handler missing entirely), ANTS-1197 (OSC 9;4 short-payload misclassified as a notification with body "4"). 4 new feature tests added (decstr_soft_reset 8 INVs, osc9_progress_disambiguator 6 INVs, styled_font_kerning_off 3 INVs, wide_char_resize 2 INVs) plus scroll_region_growth_on_resize 7 INVs from Pass 1 and contract catch-up updates to debuglog_perms (post-ANTS-1190) and pty_dtor_off_main_thread (post-ANTS-1189). 141/141 ctests green.

Fixed

  • ANTS-1187 — output piled into the upper rows of a maximised tab; bottom rows stayed blank. Layman: open a terminal at the default 24 rows, maximise it to 60 rows, and new output would only fill the top 24 — the bottom 36 stayed empty until you typed something that scrolled. User report 2026-05-08 (Flask dev server). First diagnosed as a Flask DECSTBM bug, then a paint-pipeline bug (ANTS-1193), then byte-captured and exonerated both — Flask emitted no DECSTBM at all. Real fix shipped under ANTS-1194 (below). The 1187 ticket itself landed the Reset Scroll Region escape hatch (Tools menu) plus scrollRegionTop() / scrollRegionBottom() accessors so a user with a misbehaving TUI has a one-click recovery without needing to know about \e[r.
  • ANTS-1188 — Claude Code spinner duplicated into scrollback. Layman: while Claude Code was thinking, every spinner frame left a copy in the scrollback above instead of overwriting in place. Same root cause as ANTS-1187 — a too-small scroll region clipped the in-place CR/LF redraw to the top of the viewport. Closed by ANTS-1194.
  • ANTS-1189 — _int_free_chunk SIGABRT on every app exit. Layman: closing Ants Terminal would print free(): invalid pointer and dump core every single time, in front of three separate users with identical fingerprints today. Root cause: Pty::~Pty ran a detached std::thread for the SIGTERM → SIGKILL escalation. The 0.7.0 VtStream refactor moved ~Pty itself onto the parse-worker thread, making the 0.7.33 detach- for-GUI-responsiveness redundant — but the detached worker outlived the destructor and raced main-thread Qt teardown via glibc malloc arenas. Escalation now runs synchronously on the parse worker (1.5 s budget, within m_parseThread->wait(2000)). The post-SIGKILL reap is now also bounded — pre-fix waitpid(_, _, 0) could hang on uninterruptible-sleep children and trip Qt's "destroying a running QThread aborts" assertion, worse than the detach race. Test rewritten with 12 invariants on the synchronous-on-worker contract; verified fails 7/12 against pre-fix code.
  • ANTS-1190 — Tools → Debug Mode → Clear Log silenced subsequent writes. Layman: clearing the debug log from the menu would appear to work, but every category that wrote to the log afterward (PTY / Network / Lua) silently dropped. Pre-fix DebugLog::clear() truncated the file but didn't reopen the category file handles, so subsequent appends went to a deleted inode. Fix reopens the file post-clear. The debuglog_perms feature test had its I2 invariant updated to lock the new contract (post-fix slipped through yesterday's ship without the full features sweep — caught and fixed under this section).
  • ANTS-1192 — "Background tasks" status-bar button vanished for projects whose path contained underscores. Layman: open a project at /home/me/my_project and the BG-tasks chip in the status bar would silently disappear; switch to a project with no underscores in the path and it returned. Closes the ANTS-1052 regression. Root cause: encodeProjectPath (which derives the ~/.claude/projects/<encoded-cwd>/ lookup key) collapsed every other separator but left underscores intact, so the on-disk transcript directory and the in-memory key diverged. Now collapses underscores too, matching Claude Code's own canonical encoding.
  • ANTS-1193 — paint pipeline umbrella, mis-diagnosed. No user-visible change here — kept as a tombstone for traceability. ANTS-1193 was opened as a presumed paint-pipeline bug to explain the morning's #1 user report; byte capture and code walk exonerated the pipeline. Real fix is ANTS-1194.
  • ANTS-1194 — scroll region grows with the grid on resize. Layman: this is the actual fix for the morning's #1 bug (output-piles-in-upper-rows). TerminalGrid::resize() only clamped the scroll region; it never widened it on grow. Tab opens at 24 rows → maximise to 60 → m_scrollBottom stays at 23 (clamp(23, 0, 59) = 23) → new output piles into rows 0-23, bottom 36 stay empty. ANTS-1130's clamp was correct for shrink (preserves explicit DECSTBM from tmux/mc/less) but silently wrong for grow when no DECSTBM was ever set. Fix captures liveWasFullScreen / savedWasFullScreen before m_rows updates, then conditionally widens on full-screen and clamps on explicit partial — matches xterm semantics (default region tracks the screen, explicit DECSTBM is preserved). New scroll_region_growth_on_resize feature test, 7 invariants (default-grow, default-shrink, explicit-preserved-on-grow, explicit-clamped-on-shrink-below-bottom, alt-default-grows, alt-explicit-preserved, sequential grow/shrink); verified fails 5/7 against pre-fix. Closes ANTS-1187, ANTS-1193 (pipeline was innocent), and likely ANTS-1188.
  • ANTS-1196 — DECSTR (CSI ! p) soft-reset handler was missing. Layman: vim, neovim, mc, htop and other conformant TUIs emit DECSTR on startup as a defensive recovery from hostile prior shell state. Pre-fix Ants silently dropped the sequence — same shape as ANTS-1187 (a stale scroll region traps the user) but for the broader contract. Per xterm ctlseqs / VT220 reference, DECSTR is a subset of RIS: it resets scroll region (full screen), origin mode, autowrap, cursor visibility, SGR and saved-cursor — but does NOT wipe screen, scrollback, hyperlinks, OSC 133 prompts, integration callbacks, the OSC 133 HMAC key, the kitty keyboard stack, or alt-screen state. (ESC c / RIS keeps doing the hard reset.) New decstr_soft_reset feature test, 8 INVs on the spec- defined reset surface plus three "DECSTR is soft, NOT hard" preservation invariants; fails 6/8 against pre-fix. Added originMode() and autoWrap() accessors needed by the test.
  • ANTS-1197 — OSC 9;4 progress short-payload misclassified. Layman: a ConEmu / Windows-Terminal-style OSC 9;4 "remove progress" sequence (the minimal form, payload literally 9;4) used to pop a desktop notification with body "4" instead of clearing the progress indicator. Pre-fix disambiguator required payload.size() >= semi + 3 AND payload[semi+2] == ';', so the 3-character minimal form fell through to the notification path. New predicate accepts 9;4 (state=0, percent=0 defaults) AND keeps rejecting 9;42 / 9;4Hello. 6 INVs in osc9_progress_disambiguator; fails 3/6 pre-fix. Open question logged in spec: a notification body literally starting with 4; (e.g. 9;4;30 PM meeting) still misclassifies as progress — fundamental wire-format ambiguity, flagged for future stricter range-check.
  • ANTS-1198 — bold / italic / bold-italic font setters left kerning enabled. Layman: configure a custom bold or italic family in Settings and the matching style would slowly drift off the cell grid (kerning + monospace = column drift, per the existing CLAUDE.md note). setBoldFontFamily / setItalicFontFamily / setBoldItalicFontFamily rebuilt the per-style QFont but bypassed updateFontMetrics, inheriting Qt's default kerning-enabled state. Three-line fix (setKerning(false) on each); styled_font_kerning_off source-grep test, 3 INVs.
  • ANTS-1199 — synchronized output snapshot now re-captures immediately on resize. Layman: resize a terminal mid-frame while a TUI was using DEC mode 2026 sync output and the user could see the half-applied frame the BSU block was supposed to hide, until the next batch arrived (could be hundreds of ms during human-paced output). Pre-fix recalcGridSize cleared the snapshot but waited for the next onVtBatch to re-capture. Now re-captures immediately when m_syncOutputActive. Spec ANTS-1148 §H1 had named this exact gap.
  • ANTS-1200 — stuck-sync flicker loop on malformed BSU. Layman: a truncated DEC mode 2026 BSU sequence (no matching ESU within 500 ms) used to put the terminal into a permanent 500-ms-flicker loop instead of recovering on the safety timer. Pre-fix the safety timer dropped the local m_syncOutputActive flag and cleared the snapshot — but the grid's m_synchronizedOutput latch stayed set, so the next batch's pre-scan re-armed the flag and restarted the timer. New TerminalGrid::forceClearSynchronizedOutput() is now called from the safety timer after clearScreenSnapshot(); malformed BSU now escapes on first timer fire.
  • ANTS-1201 — OSC 52 selection field plumbed to QClipboard mode. Layman: xterm-style apps that copy to the X11 primary selection (middle-click paste) used to silently clobber the system clipboard instead. Pre-fix the OSC 52 selection prefix (c / p / s / q / 0-7 per xterm) was parsed and discarded; every clipboard write landed on the system clipboard. Now plumbed through the existing OSC52: sentinel envelope as OSC52:<selChar>:<bytes>. Selection collapse: c → system clipboard, p / s → primary; empty / unknown defaults to c (modern xterm.js / kitty behaviour — primary's middle-click paste is surprising when not explicitly requested).
  • ANTS-1202 — OSC 8 hyperlink hardening (3 sub-findings). Layman: three small but exploitable OSC 8 parsing gaps, all closed: (a) reopen-without-close used the legacy single-row span emission, silently re-introducing the 0.7.55 wrapped- hyperlink bug for any wrapped link followed by another OSC 8 open without a close in between; (b) id= parser used naive find("id=") which matched embedded id= inside a value (x=id=trick produced id="trick") — switched to a proper per-key scan; (c) URI scheme allowlist was applied on open only, defense-in-depth gap if any future path mutates m_hyperlinkUri outside the open validator. Refactor extracted pushHyperlinkSpansForActive() so close and reopen-close paths share the multi-row emit + scheme re-check.
  • ANTS-1203 — wide-char (CJK / fullwidth Latin / emoji) cells survive a resize. Layman: resize a terminal across a wide-char cell boundary and the character used to split — its lead glyph on one row, its continuation half on the next — visible corruption. Pre-fix rewrap chunk loop ignored Cell::isWideChar / isWideCont when splitting logical lines. Fix: if the chunk would split a wide-char pair AND the chunk doesn't end at total, decrement chunk by 1 to push the lead into the next iteration with its continuation. New wide_char_resize test, 2 INVs targeting the dangerous mid-pair boundary; fails 4/4 pre-fix.
  • ANTS-1209 — input-handler caps + IME cursor sync (3 sub-findings). Layman: three independent input-side hardening fixes. (a) REP cap shrunk from m_cols * m_rows to m_cols * 2 — a malicious payload \e[16000b\e[16000b... could pin the parser thread on a single byte for a 16,000× CPU multiplier; real REP use cases (tput rep, less border drawing) need at most m_cols, so 2 * m_cols is generous without giving an attacker a pin. (b) OSC 133 marker validated before HMAC verification: with m_osc133Key set, every received OSC 133;X;ahmac=<hex> used to fire a SHA-256 round even for unknown markers — only A/B/C/D are spec'd, drop early. (c) inputMethodQuery::ImCursorRectangle now uses effectiveCursorRow/Col instead of the live grid cursor, so during a sync-output BSU block the IME panel doesn't jump anywhere mid-redraw.
  • ANTS-1210 — ECH (CSI X) honours BCE and clears non-bg attrs. Layman: erase-character used to leave bold / italic / underline / inverse active on the resulting space, and could leave a stranded wide-char marker. Per xterm, ECH should clear all non-bg/fg attrs and set bg via the Background-Color Erase state — same shape as clearRow. Now zeros CellAttrs first, then assigns default fg + eraseBg(), and resets isWideChar / isWideCont.

Changed

  • ANTS-1191 — refreshTasksButton diagnostic logging. Added ANTS_LOG(Network, …) instrumentation around the Claude-tasks status-bar button refresh path so the next user-reported "the chip should be visible but isn't" report has a transcript to triage from. No user-visible behaviour change. Pairs with the post-mortem of ANTS-1192.
  • ANTS-1195 — zombie m_fallbackFont / m_nerdFallbackFont removed. Indie-review #3 Lane D Critical: the emoji/CJK and Nerd Font fallback fonts were probed at construction, resized on font-size change, but never consulted by any render path (zero non-declaration grep hits). Today's emoji/CJK/Powerline rendering relies entirely on Qt's implicit FontConfig substitution. Decision: delete (vs wire) — FontConfig substitution works on Linux/macOS and the flag pattern (m_hasFallbackFont / m_hasNerdFallback) was the wrong shape for any future explicit per-codepoint route. Net delta: −34 lines of code that never affected any render frame. Comment in the constructor names the deletion
    • future-wiring guidance for any change that wants explicit fallback.
  • ANTS-1204 — liveWasFullScreen / savedWasFullScreen rename + m_altScrollBottom default init. Two Lane A findings bundled. (a) The primaryWasFullScreen / altWasFullScreen names introduced by ANTS-1194 read backwards during alt-screen mode (the grid's m_scrollTop/Bottom are the LIVE values for whichever buffer is active; m_altScroll* are the SAVED values for the OTHER buffer). Behaviorally correct but misled the next reader — renamed to live / saved with explanatory comment. (b) m_altScrollBottom defaulted to 0 in the header until first alt-screen entry; symptom-free today but asymmetric with the constructor's primary init and a foot-gun for any future caller. Now initialised to m_rows - 1 in the ctor body alongside the primary slot.
  • ANTS-1205 — m_paintLayout.clearFormats() discipline. The mutable QTextLayout member is reused across runs and paints; setText() resets glyph state but not additional- format ranges installed via setFormats(). No caller sets formats today, so this is a no-op in practice — but any future addition that calls setFormats({...}) once would leak that formatting into every subsequent run for the lifetime of the widget. Bug shape is invisible to tests and brutal to track down; one-line defensive clearFormats() at the top of the runs loop with a comment naming the contract.
  • ANTS-1206 — doc-rot sweep on terminalwidget.cpp. Two stale comments cited the retired QOpenGLWidget+FBO architecture as the reason for current code behaviour (paintEvent background fill rationale at line 629; resize base-class call rationale at line 1934). Updated to describe the current QWidget backing-store + translucent-compositor reasoning. The history comments at lines 80 / 89 (which document the 0.7.4 refactor itself) stay — those are deliberate institutional memory, not stale rationale.
  • ANTS-1207 — per-frame allocator hygiene bundle. Three sub-findings: (a) cached overlay/badge QFonts as members — pre-fix paintEvent constructed 4 fresh fonts per frame (badge × 4pt-bold; cmd-mark labels at 2 sites; quick-select labels; perf overlay), every one triggering a FontConfig family lookup; now built once in updateFontMetrics() alongside the styled variants and reused (m_badgeFont, m_smallFont, m_quickSelectFont, m_perfFont, all inheriting setKerning(false)); (b) ASCII fast-path for QString::fromUcs4 in the hot run loop — >90% of TUI content is <0x80, where the UTF-32→UTF-16 transcode is pure overhead; pre-allocated QString + QChar copy is roughly 2-3× faster on ASCII runs, falls through to fromUcs4 only when a non-ASCII codepoint is detected; (c) m_freeCellBuffers.clear() in TerminalGrid::resize() now guarded by cols != oldCols — pre-fix cleared unconditionally, so a pure rows-resize lost cached cell allocations for no reason.
  • ANTS-1208 — paint re-entrancy audit (INFO close + defensive contract). Indie-review #3 Lane C H1 flagged a plausible re-entrant paint during the m_grid->processAction loop: if any installed callback synchronously pumped the Qt event loop, a queued repaint could fire mid-batch and the snapshot fallback (cellAtGlobal → live grid) would tear. Audit walked all 8 installed callbacks (response, bell, notify, progress, commandFinished, userVar, osc133Forgery, lineCompletion) — none synchronously pump. Closes as INFO with no code change beyond a defensive contract comment at the top of onVtBatch: future callbacks that want synchronous GUI dispatch (e.g. modal dialogs) MUST queue via QMetaObject::invokeMethod with Qt::QueuedConnection.
  • ANTS-1211 — setLineWidth fudge replaced + setFontFamily order hazard documented. (a) tline.setLineWidth in the run loop used runText.length() * m_cellWidth * 2. UTF-16 length conflated with glyph cells (CJK is 1 unit / 2 cells; emoji surrogate is 2 units / 1 cell), and the * 2 was a fudge to mask both directions. Replaced with qreal(INT_MAX) — the correct semantic for monospace cell-grid rendering ("do not break this run on width") regardless of run length. (b) setFontFamily clobbers per-style family overrides set via setBoldFontFamily / setItalicFontFamily / setBoldItalicFontFamily. Documented as an order hazard (callers that want both base
    • per-style customisation must call setFontFamily FIRST, then the per-style setters) rather than fixing the behaviour — the propagate-to-all default matches the "I picked one font for everything" user mental model.
  • ANTS-1213 — DECRQSS doc-fix. Comment at the bottom of handleCsi claimed "DECRQSS (DCS $q) is not implemented to prevent echoing attacker-controlled data" while handleDcs IS implemented and replies to DECSTBM r / SGR m / DECSCA " q / DECSCUSR SP q. Threat-model calibration confirmed the implementation is safe (replies are terminal- controlled — 4 fixed format strings, no attacker bytes echoed back, so the CVE-2003-0063 echo-back class doesn't apply); the comment was misleading, not the implementation. Comment updated to match reality. The CSI 20t/21t XTWINOPS portion of the original note stays — that one IS still absent and the CVE rationale still holds.

v0.7.78

Theme: independent-review sweep #2 — pre-0.8.0 audit + multi- agent code review, briefed cold against external standards (ECMA-48, xterm ctlseqs, OWASP LLM Top 10, POSIX forkpty(3), SARIF v2.1.0, Lua 5.4 sandbox, RFC 8259, freedesktop). The review surfaced 4 calibrated CRITICAL + 22 HIGH after threat- model calibration; this release ships 22 of 23 (ANTS-1181 — a 1500-LoC mainwindow.cpp carve-out — deferred to its own session). Headline pattern: ANTS-1163's just-fixed dialog- staleness bug recurred in 5+ more cached dialogs, exactly the "self-graded homework" trap that motivated the sweep — bundled into ANTS-1168 + a structural lint guard. Plus the rolled-over ANTS-1163 fix from 2026-05-07. 137/137 ctests, gitleaks 0, semgrep 0.

Security

  • ANTS-1164 — PTY-write debug log leaked keystrokes / paste payloads. Pty::write logged a 60-byte percent-encoded preview when ANTS_DEBUG=pty was active, with NO redaction. Short tokens (ghp_…, AKIA…, sub-60-char passwords pasted at sudo) landed verbatim in ~/.local/share/ants-terminal/ debug.log. Fix: route the slice through SecretRedact:: scrub() before percent-encoding. Pairs with ANTS-1170.
  • ANTS-1166 — Kitty a=d,d=a wiped Sixel + iTerm2 images cross-protocol. Kitty's "delete all images" cleared the shared m_inlineImages vector, erasing every Sixel and iTerm2 image the terminal had displayed. Exploitable from untrusted PTY output as cross-protocol visual-context redaction. New InlineImage::Origin enum; delete-all now removes only Kitty-tagged entries via std::remove_if + recomputeImageBudget.
  • ANTS-1169 — Boundary-cap audit (8 sites). Every untrusted-input crossing now has an explicit max-size + shape gate: tailFile canonicalizes against /tmp/claude-$UID/ + lstat S_ISREG (LLM01-reachable); 6 path setters reject >4096 chars or NUL-embedded; setBackgroundImage peeks via QImageReader::size() against MAX_IMAGE_DIM=4096 (was vulnerable to a 50000×50000 PNG → ~10 GB alloc); Kitty APC c/r clamped to MAX_IMAGE_DIM; OSC 8 trigger URI capped at 4096 chars; parseTranscriptTail falls back to last-complete-record on the 4 MiB cap instead of returning empty; runClient 1 MiB receive cap mirrors server; --remote send-text 1 MiB stdin cap. MAX_IMAGE_DIM promoted to a public static so non-grid intake paths share it.
  • ANTS-1170 — ANTS_DEBUG opt-in gate + log rotation. Honoring ANTS_DEBUG now prints a one-line stderr banner ("Ants: debug log active (ANTS_DEBUG=…) → /path") so a forgotten env var inherited from a CI image / .envrc is visible at startup. debug.log rotates to debug.log.1 when it exceeds 10 MiB, preventing unbounded secret-capture across multi-day forgotten sessions.
  • ANTS-1172 — Lua C-call wall-clock watchdog (un-defer ANTS-1143). LUA_MASKCOUNT-only doesn't fire inside pure-C Lua calls (string.gsub, table.sort); a plugin author feeding PTY output into data:gsub("(.-)+", …) could freeze the UI. New startPcallBudget() sets a 1500 ms wall-clock deadline; the hook now combines LUA_MASKLINE | LUA_MASKCOUNT @ 100K ops with a deadline check on every callback. Closes the only documented Lua sandbox-escape path.
  • ANTS-1178 — SettingsDialog regex validation + AI key Imh hints. Highlights/Triggers patterns now go through QRegularExpression::isValid() + AuditEngine:: isCatastrophicRegex() at save (catastrophic patterns silently dropped). AI key field gains Qt::ImhSensitiveData | ImhHiddenText | ImhNoAutoUppercase | ImhNoPredictiveText so virtual-keyboard / IME predictive caches don't capture the value.
  • ANTS-1184 — SecretRedact extended with Google shapes. Added rules for AIza… (Google API keys, 39 chars) and ya29.… (Google OAuth tokens). Closes a known gap in the AI-context redaction pass.

Fixed

  • ANTS-1163 — Task List dialog showed tasks from a previous Claude Code session after a fresh launch. User report 2026-05-07: "there is still this task list even though I rebooted and just started up a new Claude Code session." Root cause: ClaudeIntegration::sessionPathForCwd picked the newest .jsonl in ~/.claude/projects/<encoded-cwd>/ by mtime alone. After a reboot → claude sequence, the prior session's transcript outranked the new (empty) transcript until the new session appended its first event — surfacing the prior plan's TodoWrite tasks in the chip and dialog. /clear was a no-op because the bug lived in file selection, not in tracker state. Fix is two-layered: (a) process-anchored identity filter — when the focused tab's m_claudePid is known, candidates whose effective last-event ms predates the Claude Code process start by more than 5 s are dropped; (b) 24 h liveness floor — independent of (a), candidates whose effective last-event ms is older than 24 h are dropped. New static helpers processStartTimeMs(pid) + lastEventTimestampMs(path); new overload sessionPathForCwd(cwd, minLastEventMs, nowMs) with default args preserving legacy newest-by-mtime behaviour. New feature test claude_session_freshness (16 INVs).
  • ANTS-1165 — Ctrl+Shift+Up/Down configured bookmark shortcut silently dead. Default next_bookmark / prev_bookmark chord collided with the OSC 133 prompt- navigation chord that TerminalWidget::keyPressEvent intercepts before Qt dispatches QShortcuts; the bookmark shortcut never fired whenever the terminal had focus. Default changed to Ctrl+Alt+Up/Down.
  • ANTS-1167 — forkpty F_SETFL silent fall-through. If F_GETFL or F_SETFL failed, Pty::start swallowed the error and continued with a still-blocking master fd; a spurious QSocketNotifier wakeup would freeze the GUI thread inside read(). The failure path now logs and aborts start().
  • ANTS-1168 — Dialog-staleness sweep across 7 cached dialogs (ANTS-1163 family). Three independent reviewers identified the same shape recurring in: SshDialog form fields (added clearForm() called on setBookmarks); AiDialog last- error / in-flight reply (new resetTransient()); ClaudeAllowlistDialog rule input + validation hint; ClaudeProjectsDialog first-show-not-refreshed (now unconditional); ClaudeTranscriptDialog showing global newest-by-mtime regardless of focused tab (new setProjectFilter + recentSessionsForCwd); pollClaudeProcess second site of unscoped global newest- by-mtime (now scoped via /proc/<shellPid>/cwd → sessionPathForCwd); setShellPid not clearing m_changedFiles on PID change.
  • ANTS-1171 — Audit pipeline-order spec/code drift fixed in CLAUDE.md. Documented order updated to match the actual handleCheckOutput reality (filter-then-cap; capping first would surface junk findings while real ones get pushed out behind).
  • ANTS-1173 — PluginManager::unloadAll snapshot before iteration. Pre-fix code didn't snapshot m_engines.values(), unlike fireEvent. An unload handler that re-entered the event loop (status signal → palette repaint → keypress → fireEvent) could dereference an already-deleteLater'd engine — deterministic UAF window in dev/hot-reload mode.
  • ANTS-1174 — Mainwindow lifetime hygiene. Replaced 3 std::make_shared<QMetaObject::Connection> self-disconnect patterns with Qt::SingleShotConnection (Qt 6.0+). Added a transient m_paletteProxyHolder that gets replaced on every rebuildCommandPalette() so proxy QActions no longer accumulate as orphans on the MainWindow's child list across plugin reloads / config refreshes.
  • ANTS-1175 — PTY robustness: envp truncation log + new EOF signal. Parent environ exceeding kEnvpCap=512 now emits a qWarning (was the silent root of the user-reported "ants sometimes opens a shell with no PATH" symptom on 500+-entry desktop sessions). New childUnreapedAtEof signal disambiguates "child closed PTY but waitpid hasn't reaped" from "child exited with -1, reaped"; legacy finished(-1) continues to fire on the same code path.
  • ANTS-1177 — ANTS-1116 INV-6 spec/code reconciliation. Spec dropped the literal signal number from the "drift script killed by signal" error message; QProcess::exitStatus's exit code is unspecified on CrashExit per Qt 6 docs.
  • ANTS-1179 — Config robustness: setter validation + theme parse-failure rotation. setRoadmapActivePreset and setRoadmapKindFilters now reject unknowns at write (mirroring the getter's known-set filter), so future preset renames don't leave zombie values stranded on disk. Failed user-theme JSON files are rotated to <name>.json.corrupt. <epoch> so the user has a forensic copy on disk and the next launch doesn't keep silently skipping it.

Changed

  • ANTS-1176 — Remote-control observability. Per-verb structured ANTS_LOG(Network, "rc dispatch cmd=%s tab=%d text_bytes=%d", …) on every dispatch. Same-UID-attack post-mortem now has a record. Payload bodies deliberately excluded.
  • ANTS-1180 — paintEvent fillRect coalescing. Contiguous same-bg cells now produce one fillRect per run, mirroring the existing TextRun aggregation. Fully-styled vim status lines previously paid cols × per-frame fillRect calls; hot-path frames now scale with run count, not cell count.
  • ANTS-1182 — findChildren<TerminalWidget*>() walks collapsed. Replaced 12 sites in mainwindow.cpp with liveTerminals() over a maintained QList<QPointer< TerminalWidget>>. The hottest path was the broadcast-mode keystroke lambda — under 20 tabs × split panes that previously triggered a tree walk per keystroke.
  • ANTS-1183 — config.json schema versioning. New _schema: 1 stamp + migrate(int from, int to) placeholder so future on-disk renames have a canonical home. Pre-stamp configs (anything written by ≤ 0.7.77) read as v0 and migrate-up on first load.
  • ANTS-1185 — Tab a11y: per-tab Claude state exposed to AT-SPI / screen readers. Tab tooltips now read "Claude: <state>" (idle / thinking / tool use / bash / planning / auditing / compacting / awaiting input) so Orca / Windows Narrator can announce state alongside the tab title. The visual dot was previously invisible to assistive surfaces.

v0.7.76

Theme: ANTS-1150 Phase 1 — UI / chrome state persistence. User ask 2026-05-01: "Everything that makes sense to persist across sessions, please make that persist." Six persisted UI choices that previously reset on every launch — SettingsDialog last-active tab, RoadmapDialog active preset + Kind facet checkbox set + 5 status- emoji checkboxes, AuditDialog severity-filter pills + show-new-only toggle. Six new Config getter/setter pairs follow the existing storeIfChanged + atomic-write hygiene; round-trip identity tested via a sandboxed-XDG_CONFIG_HOME QTemporaryDir lane. 15 ANTS- 1150-INV-N invariants in tests/features/ui_state_persistence/. Cold-eyes spec review folded 2 CRITICAL + 5 HIGH + 6 MEDIUM + 4 LOW before code; indie-review of the implementation folded 1 MEDIUM + 2 LOW (Custom-preset silent rewrite to Full + dead-defense QSignalBlockers on tab/show-new restore — re-ordered connect- before-restore so blockers actually defend). 132/132 ctests pass.

Added

  • ANTS-1150 Phase 1 — six pieces of UI state now survive a restart. Phase-1 cohort, all driven by user ask 2026-05-01:

    • SettingsDialog last-active tab (settings_dialog_last_tab). Open Settings on whichever of the 9 tabs (General / Appearance / Terminal / AI / Highlights / Triggers / Keybindings / Profiles / Plugins) you closed it on, not always General. Index-keyed, clamped at the call site against the dialog's tab count.
    • RoadmapDialog active preset (roadmap_active_preset, string: "full" / "history" / "current" / "next" / "far_future" / "custom"). Re-opens to your last tab choice; invalid stored values fall back to "full".
    • RoadmapDialog Kind facet checkboxes (roadmap_kind_filters, sorted ASCII-codepoint array). Reverses the explicit ANTS-1106 "Kind filter resets on each open" decision per 2026-05-01 ask. Setter sorts on write so two saves of the same set produce byte-identical JSON (diff-friendly). Unknown Kind strings drop silently on read — defends a future KindEntry rename.
    • RoadmapDialog 5 status-emoji checkboxes (roadmap_status_filters, 5-key boolean object — done/planned/in_progress/considered/current). Restored only when the persisted preset is "custom" (named presets ride applyPreset's canonical mask anyway). Empty object on first read defaults each checkbox to true (the .toBool(true) Qt contract on missing keys).
    • AuditDialog severity-filter pills (audit_severity_filters, 5-key boolean object — blocker/critical/major/minor/info). The MIN+INF-off post-noise-sweep state survives a re-run.
    • AuditDialog "Show new since baseline" toggle (audit_show_new_only). Restored at dialog ctor only when m_hasBaseline is true (no point checking the box without a baseline to compare against). Lazy-invalidate stickiness: persisted bool stays through baseline-deletion gaps; next saveBaseline re-honours the preference.

    Implementation hygiene: two-write-site invariant for m_activePreset — applyPreset (named-preset path) and onCheckboxToggled (Custom-divergence path) both call the new RoadmapDialog::persistActivePreset helper, which uses a switch on Preset for compiler -Wswitch-enum coverage of any future enum addition. Lifted KindEntry table to file scope as kKinds[] so the build loop and the persisted-Kind restore iterate the same source-of-truth; new m_kindCheckboxes (QHash<QString, QCheckBox *>) hash for O(1) lookup during restore. AuditDialog ctor signature changed to require Config * as a third arg — no = nullptr default per the cold-eyes review (single call site at mainwindow.cpp:1351 always has &m_config available; a default would be dead scaffolding).

Changed

  • Config::setRoadmapKindFilters now sorts ASCII-codepoint-wise on write (not at the call site). Centralizes the canonicalization so future call sites can't accidentally write an unsorted list and break diffability.

v0.7.75

0.7.75 — DEC mode 2026 sync output atomicity fix (ANTS-1148)

[0.7.75] — 2026-05-02

Theme: ANTS-1148 — DEC private mode 2026 (Synchronized Output / BSU/ESU) atomicity fix. Pre-fix, onVtBatch suppressed only its own update() during BSU but left the live grid mutated by processAction; any other paint trigger (blinkCursor, focusIn/Out, hover, selection, visual-bell flash) would call paintEvent which read the live half-applied state and leak mid- sync content to screen. The fix unifies sync output onto the existing 0.6.33 frozen-screen snapshot machinery: pre-scan a VtBatch for CSI ?2026h, capture the snapshot before the processAction loop, route paintEvent's cursor reads through new effectiveCursorRow/Col() accessors, and clear on ESU or via the 500 ms safety timer. Single TU touched (terminalwidget.{cpp,h}), behavioural-correctness only — no LoC delta on mainwindow.cpp. 132/132 ctests pass.

Fixed

  • ANTS-1148 — Synchronized Output (DEC mode 2026) is now atomic against non-batch paint triggers. Pre-fix, BSU (CSI ?2026h) suppressed onVtBatch's trailing update() but did nothing about the live grid mutation underneath: any paintEvent driven by blinkCursor (550 ms cadence), focusInEvent / focusOutEvent, mouse-hover OSC 8 hyperlink rollover, selection drag, or visual-bell flash would re-render from the half-applied grid mid-BSU and leak partial state — exactly the tearing the protocol exists to prevent. Fix folds sync output onto the existing 0.6.33 frozen-screen snapshot path: a new file-scope helper batchEntersSyncOutput pre-scans the VtBatch for CSI ?2026h (CsiDispatch + intermediate ? + finalChar h + 2026 in params), and onVtBatch captures the snapshot under (m_syncOutputActive || batchEntersSyncOutput(*batch)) && m_frozenScreenRows.empty() before processAction runs. The left disjunct handles eviction-recovery — resize / RIS / alt- screen toggle clear the snapshot mid-sync, and we need to re-capture for the rest of the block. Cleanup runs at end-of- batch when !m_syncOutputActive && m_scrollOffset == 0 && !m_frozenScreenRows.empty() (drops the prior wasSync && gate that stranded same-batch BSU+ESU snapshots — cold-eyes C2); the 500 ms safety timer also clears on force-end. paintEvent's three cursor-render sites (cursor draw, under-cursor glyph, autocomplete ghost) route through new effectiveCursorRow/Col() inline accessors that read m_frozenCursorRow/Col when m_frozenScreenRows is non-empty and live otherwise; the eight typing / IME / semantic-output cursor reads (keyPressEvent, inputMethodQuery, clickToMoveCursor, findMatchingBracket, toggleFoldAtCursor, toggleBookmark, updateSuggestion, lastCommandOutput) intentionally keep direct m_grid->cursorRow/Col() reads — those paths want the real cursor, not the rendered one. Spec at docs/specs/ANTS-1148.md cold-eyes-reviewed before code (10 findings folded — 2 CRITICAL, 3 HIGH, 3 MEDIUM, 2 LOW); new feature test at tests/features/sync_output_snapshot/ locks 9 INVs by source-grep. Out-of-snapshot scope (intentional, matches xterm/foot): cursor visibility / shape / blink (DECSCUSR, DECTCEM) and inline images (Sixel / Kitty / iTerm2) read live mid-BSU.

  • ANTS-1148 — updateScrollBar's frozen-snapshot predicate extended to cover sync output. Indie-review HIGH on commit 9674e5a caught a missed call site: updateScrollBar at terminalwidget.cpp:2793 still used the pre-1148 predicate wantFrozen = (m_scrollOffset > 0). Closes spec transition row 7 — scrolled-back-during-sync, user scrolls back to bottom while BSU is still active: without the sync clause, the snapshot is evicted and the next paint reads the half-applied grid. Fix extends to (m_scrollOffset > 0) || m_syncOutputActive, matching the unified-predicate pattern already in use at the other three call sites (onVtBatch pre-scan, end-of-loop cleanup, safety-timer slot). Test gains INV-4b locking the disjunction at this third site (74395a6).

Changed

  • blinkCursor partial-update rect routes through effectiveCursorRow/Col(). Pre-fix the blink invalidation rect was computed from m_grid->cursorRow/Col() directly; under sync the live cursor position can have advanced past the frozen-rendered cell, so the partial-update rect wouldn't cover the cell paintEvent actually draws and the cursor would visually stall mid-BSU. Routing through the centralised accessor locks the invalidation rect to the rendered cell — a small consistency win that drops out of the snapshot unification rather than a separate fix.

0.7.77

Theme: two user-driven items landed back-to-back. ANTS-1158 — status-bar surface for the focused tab's Claude Code task list, replayed from the session JSONL transcript (per-tab, live-updated on QFileSystemWatcher + 2 s coalesce, hidden when there's no plan). ANTS-1159 — crash-safe session persistence: pre-fix code called saveAllSessions() only from closeEvent, so any abnormal termination (SIGSEGV / OOM-kill / power loss) discarded every write since the last graceful shutdown. New 30 s m_sessionSaveTimer + tab-create / tab-close / tab-reorder hooks into a cheap new saveTabOrderOnly() slot bound the loss window to ≤ event-loop latency for the tab list and ≤ 30 s for scrollback. Three follow-up regression fixes (Task List dialog refresh under atomic-rewrite; RoadMap button + GitHub repo-type badge cold-launch visibility) ride along under ANTS-1160 P2, plus a new docs/standards/status-bar.md contract to prevent recurrence. 13 INVs for ANTS-1158, 9 INVs for ANTS-1159, 5 INVs for ANTS-1160 P2; 136/136 ctests pass.

Added

  • ANTS-1158 — Claude Code task-list status-bar widget + dialog. When the focused tab's Claude Code session has an active plan (TodoWrite snapshot or TaskCreate/TaskUpdate stream in the session JSONL), a new status-bar chip shows <unfinished>/<total> (in_progress + pending out of all). Click opens ClaudeTaskListDialog with one row per task (☐ pending / ◐ in_progress / ✓ completed) plus a header "<N> tasks — <Y> running, <Z> outstanding, <X> done". Hidden when no plan exists for the focused tab. New claudetasklist.{cpp,h} (ClaudeTaskListTracker + static parseTranscript) and claudetasklistdialog.{cpp,h}; wired through claudestatuswidgets (button + tooltip + retarget on setTranscriptPath) and MainWindow::onTabChanged (per-tab retarget mirrors m_bgTasks). Replay handles two transcript shapes: Mode A (snapshot) — most-recent TodoWrite wins, found by 64 KiB EOF block-walk so we don't scan the whole file to grab a near-tail event; Mode B (incremental) — forward walk over TaskCreate (paired with tool_result for the allocated ID, mirroring claudebgtasks.cpp:222–243) + TaskUpdate flips, 16 MiB tail cap. Filters out isSidechain == true events (subagent tool calls inlined in the parent transcript) and Task / Agent tool_uses with subagent_type (sub-agent dispatch ≠ plan row). Dialog is non-modal with plain QPushButton close — no setModal(true), no QDialogButtonBox (per the debug_wayland_modal_dialog memory: QTBUG-79126 drops button clicks on KDE+KWin+Qt 6.11+frameless+translucent parent). Cold-eyes spec review folded before code. 13 ANTS-1158-INV-N invariants in tests/features/claude_task_list/.

Fixed

  • ANTS-1159 — session state now persists on a 30 s timer and on every tab create / close / reorder, not only on closeEvent. User reported 2026-05-02 that after a SIGSEGV, restart restored zero tabs. Pre-fix, MainWindow::saveAllSessions() was wired only to closeEvent (mainwindow.cpp:3798), so any abnormal termination — SEGV, OOM-kill, power loss, kernel panic — bypassed it and discarded every write since the last graceful shutdown. ANTS-1141 had already plugged a related leak (the destructive read on tab_order.txt); this closes the bigger one. Fix shape (option B, user-approved): new QTimer *m_sessionSaveTimer on MainWindow, 30 000 ms autoreloading, timeout → existing saveAllSessions(). Started in the ctor after restoreSessions() so the first save doesn't race the restore; stopped in closeEvent before the explicit save so it can't re-enter mid-shutdown. Three new tab hooks call a cheap new saveTabOrderOnly() slot (just walks m_tabWidget, builds the tabOrder QStringList + active index, calls SessionManager::saveTabOrder — no scrollback serialisation): from newTab() (post-addTab), from performTabClose() (post-removeTab), and from m_coloredTabBar's tabMoved signal. The same sessionPersistence()-off and 5-s uptime-floor guards as saveAllSessions apply (no on-disk overwrite during half-restored startup). currentChanged is intentionally not hooked — tab switches fire constantly and only update activeIndex; the periodic timer covers it. Worst-case loss window: ≤ event-loop latency for the tab list, ≤ 30 s for scrollback. 9 ANTS-1159-INV-N invariants in tests/features/crash_safe_session_persist/. Stale-binary triage for the originating SEGV (/proc/<pid>/exe reported (deleted) with binary mtime 29 min before crash; restart from the fresh binary did not reproduce) is documented in the spec body — not a code bug, but the persistence-loss it exposed is.

  • Claude Code Task List dialog now refreshes when the transcript file is rewritten (ANTS-1158 follow-up). QFileSystemWatcher silently drops its inotify watch when the watched file is replaced via atomic-rewrite — Claude Code's transcript update pattern — and never re-arms, so the dialog stayed pinned to whatever state was current when it opened. Added ClaudeTaskListTracker::poll() with mtime-gated rescan (QFileInfo::lastModified() compared against m_lastRescanMtimeMs), called from claudestatuswidgets::refreshTasksButton() which already ticks on the 2 s status-bar timer. Same fix shape as ClaudeBgTaskTracker::poll(). The existing 13 ANTS-1158-INV-N source-grep invariants still hold; no new test needed.

  • RoadMap status-bar button + GitHub repo-type badge now appear on cold launch instead of waiting for the first tab switch (ANTS-1160 P2). Both widgets read shellCwd(), which reads /proc/<shellPid()>/cwd; shellPid() is set by startShell() AFTER newTab()'s setCurrentIndex() triggers onTabChanged(0) for the first time. The previous onTabChanged-only wiring saw shellCwd() == "" on first fire, hid the widget, and nothing rescheduled. Fix is additive: both refreshRoadmapButton and refreshRepoVisibility are now also invoked from the 2 s m_statusTimer periodic refresh AND the startup QTimer::singleShot(0, ...) lambda. Same three-connection pattern that refreshBgTasksButton already follows correctly. Same regression class as the v0.6.29 review-button regression. New docs/standards/status-bar.md documents the State-category-widget refresh contract (shellCwd() readers must register on all three connection points) plus a checklist for adding new widgets, so this regression class doesn't recur. 5 ANTS-1160-INV-N invariants in tests/features/roadmap_status_bar_refresh/.

v0.7.74

0.7.74 — Bundle G Tier 3 closeout (mainwindow.cpp -785 LoC)

[0.7.74] — 2026-05-02

Theme: Bundle G Tier 3 carve-out — second and third of three L6 LoC decompositions of src/mainwindow.cpp. Ships the ClaudeStatusBarController extraction (ANTS-1146) and the themedstylesheet helper extraction (ANTS-1147), together cutting mainwindow.cpp from 6249 → 5464 LoC (-785, -12.6 %) — the largest single-bundle reduction in the file's recent history. Adds three roadmap items (ANTS-1155 / 1156 / 1157) covering the in-app self-update gap, a roadmap-system audit, and the Project Audit tool flesh-out. Closes with a post-bundle debt sweep (5 trivials + 3 behaviorals). 131/131 ctests pass.

Changed

  • ANTS-1146 — ClaudeStatusBarController extracted from mainwindow.cpp. L6 LoC2 carve-out. Three contiguous Claude-status sections — applyClaudeStatusLabel, updateClaudeThemeColors, setupClaudeIntegration (~680 LoC combined, including the ~120-LoC permission-button factory) — moved to a top-level ClaudeStatusBarController : QObject in new translation unit src/claudestatuswidgets.{cpp,h}. State booleans (m_claudePromptActive, m_claudePlanMode, m_claudeAuditing, m_claudeLastState, m_claudeLastDetail) and the six widgets they drive (m_claudeStatusLabel, m_claudeContextBar, m_claudeReviewBtn, m_claudeErrorLabel, m_claudeBgTasks, m_claudeBgTasksBtn) all migrate; service objects (m_claudeIntegration, m_claudeTabTracker) stay on MainWindow and are observed via attach(). Coupling shape: six signals out (reviewClicked, bgTasksClicked, allowlistRequested, reviewButtonShouldRefresh, statusMessageRequested, statusMessageCleared) plus four std::function providers in (current/focused/at-tab terminal

    • tab-indicator-enabled toggle). Three orphans that lived in setupClaudeIntegration for historical convenience but aren't Claude chrome (Roadmap button, update-available QAction, 5 s startup update-check singleShot) stay on MainWindow under the renamed setupStatusBarChrome host. mainwindow.cpp: 6249 → 5656 LoC (-593). Spec at docs/specs/ANTS-1146.md cold-eyes-reviewed before implementation; new feature test at tests/features/claude_statusbar_extraction/ locks 9 INVs; three pre-existing tests (claude_bg_tasks_button, claude_state_dot_palette, allowlist_add) re-pointed at claudestatuswidgets.cpp per the spec's INV-9 table.
  • ANTS-1147 — themedstylesheet helpers extracted from mainwindow.cpp + cache-and-compare branch chip. L6 LoC3 carve-out. applyTheme's ~178 LoC inline QSS body and four per-widget restyle templates moved to pure-function builders in new translation unit src/themedstylesheet.{cpp,h}: six public helpers (buildAppStylesheet, buildMenuBarStylesheet, buildStatusMessageStylesheet, buildStatusProcessStylesheet, buildGitSeparatorStylesheet, buildChipStylesheet). The chip QSS template that was inlined three times (applyTheme, updateStatusBar, refreshRepoVisibility) collapses onto one parameterised helper; margin asymmetry preserved byte-for-byte (unit-free 0 vs <N>px). mainwindow.cpp: 5656 → 5464 LoC (-192); themedstylesheet.cpp: 272 LoC. New feature test at tests/features/themedstylesheet_extraction/ locks 8 INVs; three pre-existing tests (menubar_hover_stylesheet, tab_close_button_visible, review_changes_clickable) re-pointed at themedstylesheet.cpp.

Performance

  • ANTS-1147 — branch-chip restyle is now cache-and-compare. updateStatusBar runs every 2 s on the status timer; pre-fix code rebuilt and re-applied the branch-chip QSS unconditionally on every tick even when nothing changed. Two new private members on MainWindow (m_lastBranchChipQss + m_lastBranchChipValid) cache the last-applied stylesheet; the tick now computes the new QSS via themedstylesheet::buildChipStylesheet and only calls setStyleSheet when it differs. applyTheme invalidates the cache on theme change so the next tick re-applies. User-visible only as a small CPU reduction in the steady-state status-tick path; the optimisation was the roadmap entry's stated motivation alongside the extraction.

Documentation

  • ANTS-1155 added to 0.8.0 — true in-app self-update (no AppImageUpdate dep). Today's "Update" click in handleUpdateClicked is in-place auto-update only when the user already has AppImageUpdate (GUI) or appimageupdatetool (CLI) on $PATH AND is running the AppImage build; every other path falls through to QDesktopServices::openUrl — a glorified browser link. User feedback 2026-05-02: "the terminal must download and apply the update directly, no link." Plan: download full AppImage via QNetworkAccessManager, SHA-256 verify against the *.sha256 artefact already published by release.yml, atomic rename(2) over $APPIMAGE, restart-with-session-preservation. Three-bucket distribution-channel contract (ANTS_BUILD_CHANNEL = appimage / distro / source): distro-packaged builds suppress the notifier structurally — Flatpak / RPM / .deb / Arch users update via their package manager, not via the app. Placed in 📦 Distribution readiness between H7 (website) and H13 (outreach launch) — auto-update lands before outreach so the wave of new users actually stays on latest.

  • ANTS-1156 added to 0.8.0 — Roadmap-system audit (split / tag / integrate / display / number / write). User ask 2026-05-02: "We need to iron out how the roadmap is going to work" — six concrete questions. Three already covered (defer to ANTS-1154 tagging, ANTS-1139 summary-table renderer, and docs/standards/roadmap-format.md § 3.5.1 numbering); three genuinely open (file-splitting strategy as ROADMAP.md approaches the § 3.9 archive-rotation threshold; Ants Terminal ↔ roadmap integration via Help-menu access + cross-roadmap navigation + two-pane reading mode; Claude Code ↔ roadmap integration via auto-load, fold-in of /audit and /debt-sweep findings, and a roadmap MCP capability). Six child deliverables (1156-A through 1156-F) enumerated to spin out once decisions land. Sequence dependency: ANTS-1154 (format v2 tagging) lands first since 1156's child items consume v2 tags. Placed at the lead of 🧰 Dev experience.

  • ANTS-1157 added to 0.8.0 — Project Audit tool flesh-out (cross-skill / cross-project history). User ask 2026-05-02: flesh out the Project Audit concept by analysing every /audit, /indie-review, and /debt-sweep run across all projects. Six capabilities sequenced smallest-first: (1) run- history persistence as dated SARIF v2.1.0 files in ~/.local/share/ants-terminal/audit-history/<slug>/; (2) per-finding fingerprint + recurrence detection via SARIF partialFingerprints (§ 3.36); (3) MTTR / open-finding-age tracking + a new History tab in AuditDialog; (4) cross-skill correlation lane for findings flagged by both static analysis and cold-eyes review; (5) Projects tab with per-project summary metrics modeled on Codacy/SonarQube; (6) roadmap- fold-in audit trail (raw → ANTS-N → status flip → CHANGELOG) integrating with ANTS-1117 IPC verbs and ANTS-1154 tagged-text. Online research synthesised from SARIF 2.1.0 (OASIS), DefectDojo v2.55.1, Microsoft sarif-tools, SARIF Visualizer, Codacy, and SonarQube. Placed at the lead of 🧰 Dev experience above ANTS-1156.

  • Post-bundle debt sweep — 5 trivials + 3 behaviorals. Trivials: stale comment in mainwindow.h:4 referencing the pre-1146 applyClaudeStatusLabel rewritten to point at the controller; refreshStatusBarForActiveTab doc-comment at mainwindow.h:404 updated similarly; unused <QApplication> and <QDir> includes dropped from claudestatuswidgets.cpp; tests/features/claude_bg_tasks_button/spec.md and tests/features/claude_state_dot_palette/spec.md INV prose updated to match the post-1146 controller-routed call shapes. Behaviorals (user sign-off 2026-05-02): deleted write-only m_lastBranchChipPrimary + m_lastBranchChipTheme cache members from ANTS-1147 (the QSS string itself encodes the theme × primary × margin triple, so per-flag fields were YAGNI redundancy); added "verified externally via git diff --stat; this in-process test asserts only the floor on the new TU" disclaimer to the two-sided LoC anchors in both docs/specs/ANTS-1146.md (INV-8) and docs/specs/ANTS-1147.md (INV-7) so spec text matches what the source-grep harnesses can actually enforce.

v0.7.73

Theme: Bundle G Tier 3 carve-out — first of three L6 LoC decompositions of src/mainwindow.cpp. Ships the DiffViewerDialog extraction (ANTS-1145) and a roadmap-hygiene pass that flips three stale 🚧 statuses (ANTS-1118 / 1133 / 1139) to ✅ where the work was already shipped but the emoji hadn't been updated. Adds ANTS-1153 to the 1.0 milestone: fresh-eyes audit of the feature-test corpus.

Changed

  • ANTS-1145 — DiffViewerDialog extracted from mainwindow.cpp. L6 LoC1 carve-out from the 2026-05-01 indie-review sweep. MainWindow::showDiffViewer (~430 LoC of dialog construction
    • five async git probes + QFileSystemWatcher + scroll-preservation + Copy-Diff payload) moved to a free function diffviewer::show(QWidget *parent, const QString &cwd, const QString &themeName) in new translation unit src/diffviewer.{cpp,h}. mainwindow.cpp shrank from 6636 → 6213 LoC; the post-extraction showDiffViewer slot is ~25 meaningful LoC (status message → focused-terminal lookup → button disable → call into diffviewer::show → connect dialog destroyed for re-enable + refreshReviewButton). Spec at docs/specs/ANTS-1145.md was cold-eyes-reviewed before the implementation: 1 CRITICAL + 2 HIGH + 3 MEDIUM findings folded in (per-test re-pointing table, exact- signature INV, user-visible-string preservation INV, surface-design justification, theme-capture-at-open + QProcess- parent-change semantics named explicitly). New feature test at tests/features/diffviewer_extraction/ locks 7 INVs; three of the four pre-existing review_changes_* tests re-pointed at diffviewer.cpp per the spec table; the fourth (clickable) was about button-gating policy and didn't need re-pointing. Full ctest 129/129 green.

Documentation

  • Roadmap hygiene — three stale 🚧 statuses flipped to ✅. Survey of Bundle G surfaced three items whose work shipped in earlier 0.7.x releases but whose emoji status was never updated: ANTS-1118 (smooth-scroll snapshot race — shipped 2026-05-01 in 0.7.65 per CHANGELOG); ANTS-1133 (VT parser CSI verbs — REP/CHT/CBT/HPA shipped 2026-05-01 in 0.7.69 + 0.7.70, edge cases explicitly deferred); ANTS-1139 (RoadmapDialog markdown subset — 3 of 5 sub-fixes shipped 2026-05-01 in 0.7.70, remaining 3 explicitly deferred). All three flipped with deferred-sub-items called out inline so future readers know what to re-open as separate IDs if the edge cases ever bite a user.

  • ANTS-1153 added to 1.0 milestone — fresh-eyes audit of feature-test corpus. ~190 feature tests live in tests/features/; many were written alongside their fix and source-grep the patch body rather than the behavioural contract. As the codebase matures, two failure modes leak in: tests that pin implementation details and break on cosmetic refactors without catching real regressions, and tests whose INVs no longer match their spec.md because the spec evolved but the test didn't. Plan: walk every tests/features/<dir>/ against its spec.md, mark each as keep / rewrite / retire, fold rewrites into a dedicated bundle. Pairs naturally with ANTS-1085 (perf-regression suite) as the "what does CI actually catch?" audit.

  • ANTS-1154 added to 0.8.0 — tagged-text roadmap format v2 + RoadmapDialog tab filtering on tags. User report 2026-05-02: the RoadmapDialog tabs (Full / History / Current / Next / Far Future / Custom) over-show because they infer scope from prose shape rather than reading explicit tags. v2 promotes the H-bundle summary-table format (| ID | Description | Status | Target release |) to a first-class section type and adds machine-readable tags on every line via HTML comments (<!-- header:section-intro -->, <!-- kind:fix -->, <!-- prose --> etc.) so per-tab filtering is exact instead of heuristic. RoadmapDialog parseBullets + renderHtml learn the new tags; v1 docs degrade gracefully. One-shot migration of ROADMAP.md tags every existing item in place.

v0.7.72

Theme: Bundle G perf optimisations from the post-revalidation review — closes ANTS-1140 (RoadmapDialog Kind extraction double-walk fold) and ANTS-1149 (paintEvent QTextLayout reuse across runs). Both were tagged "pure perf optimisation, not a bug" and originally deferred from the Tier 2 sweep; user explicit direction 2026-05-02 to ship them rather than defer.

Changed

  • ANTS-1140 — Kind extraction folded into single pre-walk + cache. RoadmapDialog::renderHtml previously did a per-bullet peek-ahead when kindFilter was non-empty: for each bullet, walk forward through continuation lines, append into a bodyFull QString, regex-match the Kind: line. With ~270 bullets and 8-10 renders/sec while the user types into the search box (debounced 120 ms) with a Kind filter active, this was the dominant render cost. Replaced with a single pre-walk at the top of renderHtml that builds a QHash<int, QString> kindByLine keyed on bullet line index, cached thread-locally across calls (mirrors the reverseTopLevelSections cache from 0.7.70). The bullet block now does an O(1) lookup. All 7 INVs of tests/features/roadmap_kind_facets/ pass against the refactor.

  • ANTS-1149 — paintEvent QTextLayout reuse. Pre-fix code constructed a fresh QTextLayout layout(runText, *drawFont) inside the inner paint loop on every text run. Claude Code's heavily-styled output produces dozens of runs per row × N visible rows × 60 fps, so the impl-alloc cost was a real hot spot. New mutable QTextLayout m_paintLayout member is reused across all runs in a paint via setText (and setFont only when the run's font role changes — bold / italic / both / regular). HarfBuzz shape pass still runs per unique text — load-bearing for ligature support. A future row-content-fingerprint cache could skip even the shape, but is left for later.

Performance

tests/perf/bench_vt_throughput baseline comparison (0.7.71 → 0.7.72): minor parser-side gains (ASCII print 29.96→30.10 MB/s, newline 6.04→6.30, ANSI SGR 18.02→18.32, UTF-8 CJK 8.34→8.80) consistent with compiler optimisations on the recent code. The QTextLayout reuse impact is in paintEvent, not vt_throughput; a bench_paint_throughput harness (per ANTS-1115) is still pending — without it, the win is qualitative rather than quantified.

v0.7.71

Theme: Bundle G post-revalidation closures. The 2026-05-01 revalidation pass (/audit re-run + scoped /indie-review on the changed surface) accepted all 13 Tier 1+2 closures shipped across 0.7.65–0.7.70 but surfaced 4 net-new findings worth closing before the codebase is called "audit-clean". 128/128 ctests pass.

Added

  • ANTS-1151 — Hook + MCP socket SO_PEERCRED + idle timeout. Symmetric extension of ANTS-1132's remote-control hardening to the two QLocalServers that ClaudeIntegration owns: onHookConnection + onMcpConnection. The L3 indie-review reviewer flagged that the file-side guarantees (0700 perms + UserAccessOption + safeToUnlinkLocalSocket) hold, but the peer-side trust-model reasoning that motivated SO_PEERCRED on remote-control applies symmetrically — a same-UID-but- different-process attacker (e.g. a malicious browser plugin) could otherwise inject hook events shaped like processHookEvent consumes, or call MCP verbs (filesystem reads, git status, environment). 5-second per-connection idle timeout closes the slow-loris foot-gun.

  • ANTS-1152 — GlobalShortcutsPortal permanent-failure latch. ANTS-1142 H-1's 0.7.67 fix (clear m_pending + m_sessionHandle on BindShortcuts rejection) was correct on first failure but didn't enforce terminality across re-binds — a future config-reload / "Reload Plugins" path that re-bound shortcuts would re-enter the same backend failure (the loop on GNOME the original finding flagged). Lane 6 reviewer caught this during revalidation. New m_permanentlyFailed member set in every sessionFailed-emit path; bindShortcut early-returns when latched. Today's only caller fires once at startup so the bug was dormant; latch makes the contract enforced rather than aspirational.

Fixed

  • ANTS-1136 (cancel path) — RuleQualityTracker flush on cancel. The 0.7.68 cycle-completion flush in runNextCheck closed the kill-9-loses-fires class on clean completion; this closes the symmetric hole on cancel. cancelAudit now also calls m_qualityTracker->save() before renderResults(). Lane 4 reviewer caught the asymmetry during revalidation.

  • ANTS-1134 (modifier list extension) — CapsLock, NumLock, Super, Hyper added to the modifier-only keypress guard. Lane 2 reviewer flagged the original list missed Qt::Key_CapsLock, Qt::Key_NumLock, Qt::Key_ScrollLock, Qt::Key_Super_L/R, Qt::Key_Hyper_L/R, Qt::Key_Mode_switch. Same reflexive- modifier class — pressing CapsLock while scrolled-up shouldn't kill the scroll position either.

  • Static-analysis closures. cppcheck identicalConditionAfterEarlyExit at auditdialog.cpp (m_cancelled check after early return — always false; redundant check removed). cppcheck passedByValue at globalshortcutsportal.cpp (Qt SLOT signature requires QString-by-value for old-style connect string-match; suppressed inline with rationale comment). shellcheck SC1087 ×2 in packaging/rotate-roadmap.sh (array-expansion brace hygiene).

v0.7.70

Theme: Bundle G Tier 2 closeout — heavy items. ANTS-1135 (post-fork setenv signal-safety, the headline correctness fix in this release), ANTS-1133 partial (3 more CSI verbs), 1140 partial (reverseSections cache), 1139 partial (3 of 5 markdown subset gaps closed). 128/128 tests pass.

Fixed

  • ANTS-1135 — post-fork setenv async-signal-safety. Pre-fix code called ::setenv() ×5 in the child after forkpty, but setenv is NOT on POSIX's async-signal-safe list (§ 2.4.3) — it can call realloc() to grow environ. CLAUDE.md and the flatpak path's pre-fork-allocation comment block both claimed "child only does execvp"; the non-flatpak path was silently breaking the discipline. Fix: pre-fork build of envp (parent thread, signal-safe context): walk environ, skip the 5 keys we override, append our 5 overrides as static-storage / stack-allocated entries. Child uses execle(shellCStr, argv0, nullptr, envp) instead of execlp + setenv loop. Matches the flatpak-path discipline. flatpak_host_shell test INV-5 updated to recognize the execle invocation.

  • ANTS-1133 (4/5) — CSI verbs. Three more on top of REP from 0.7.69: CSI Pn I (CHT, cursor horizontal forward tabulation), CSI Pn Z (CBT, cursor backward tabulation), CSI Pn ` (HPA, horizontal position absolute — same as CHA but spelled with backtick). All three are common in ncurses, less, and bash tab-completion. Combining-after- wrap edge case + wide-cont rewrap orphan remain deferred as discrete rare-edge items.

  • ANTS-1139 (3/5) — RoadmapDialog markdown subset. Two fixes: (a) **bold** recognized in body prose via a new pass in applyInline (pre-fix code rendered the literal ** characters in body text — every bullet's "bold headline" was visually wrong); (b) markdown tables render as <table> not <pre> — every |-row becomes <tr>, first row is <th>, separator row (|---|---|) auto-skipped, body rows are <td>. Per-cell applyInline so backticks + bold work inside cells. Sub-bullet rendering (heavy refactor) + Kind regex multi-value + bold regex anchor hardening remain deferred.

  • ANTS-1140 (1/2) — RoadmapDialog perf. reverseTopLevelSections gains a function-local thread-local cache keyed on the input markdown. History-mode renders + search keystrokes

    • filter toggles (every render path that uses DescendingChronological sort) now short-circuit on identical input. With archive markdown attached (ANTS-1125), the input can reach 64 MiB; pre-fix code re-walked the entire document on every render. Kind double-walk fold (the harder optimisation) remains deferred — perf only, not a bug.

v0.7.69

Theme: Bundle G Tier 2 final closeouts. ANTS-1134, 1138, 1142, 1144 all flip ✅ (sub-fixes from 0.7.67/0.7.68 closed out); ANTS-1133 partial — CSI REP shipped, other CSI-spec gaps remain deferred. 128/128 tests pass.

Fixed

  • ANTS-1134 (2/2) — span cache invalidation across scrollback push. New m_lastScrollbackPushed member tracks the monotonic counter; invalidateSpanCaches clears both URL + highlight span caches when the count changes since the last paint. Pre-fix code only invalidated entries for screen rows the grid flagged dirty — but scrollback push shifts every cached scrollback-line entry to point at a different historical line. Symptom: URL highlights occasionally painted at the wrong column ranges on scrolled-back lines after high-throughput output.

  • ANTS-1138 (2/2) — auto-profile-rules pattern cache invalidation. New Config::autoProfileRulesGeneration() counter bumped on every successful setAutoProfileRules write; MainWindow::checkAutoProfileRules compares to a function-local s_lastRulesGen and clears s_patternCache

    • s_warnedInvalid on change. Pre-fix code retired patterns lingered forever, and a fixed-then-rebroken pattern wouldn't re-warn after the third edit.
  • ANTS-1142 (4/4) — KDE-presence guard lifted into shared helper. kwinPresent() now lives as a free function in kwinpositiontracker.h (header-only, env-var check inlined). KWinPositionTracker::setPosition, MainWindow::moveViaKWin, and MainWindow::centerWindow all share the guard. Pre-fix code: moveViaKWin and centerWindow unconditionally fired the kwin-script + dbus-send chain on GNOME / Sway / Hyprland / etc., orphaning /tmp/kwin_*_ants_*.js files and triggering a dbus-send to a non-existent service every time. Existing kwin_position_tracker feature test updated to source-grep the header for the env-var literals + assert setPosition() calls kwinPresent() before any QTemporaryFile constructor.

  • ANTS-1144 (3/3) — Claude transcript render cap. ClaudeTranscriptDialog::loadTranscript now caps rendered entries to the last 2000 with a "showing last N of M" header. Pre-fix code built full HTML for transcripts up to the integration-layer 100 MiB cap and handed it to QTextEdit::setHtml, freezing the UI for tens of seconds on long Claude Code sessions. 2000 is empirically generous — even busy sessions stay well under that.

  • ANTS-1133 (1/4) — CSI Pn b (REP) handler. Repeats the preceding graphic character N times. Common in less, ncurses (tput rep), and zsh's reset-prompt path as a bandwidth optimisation for runs of ─ borders, spaces, etc. Pre-fix code dropped this silently — visible as missing border chars in less redraws + blanks in TUI separator lines on high-latency PTYs. Other CSI-spec gaps (Z CBT, I CHT, ` HPA, combining-after-wrap, wide-cont rewrap) remain deferred as discrete items.

v0.7.68

Theme: Bundle G Tier 2 closeouts — knocking off the deferred sub-fixes annotated in 0.7.67's ROADMAP entries. ANTS-1136, 1141, 1142, 1144 each move from 🚧 partial to substantively closed. 128/128 ctests pass.

Fixed

  • ANTS-1136 (5/5) — RuleQualityTracker durability. Final sub-fix: runNextCheck calls m_qualityTracker->save() when the audit cycle completes (m_currentCheck >= m_checks.size()). Pre-fix code only flushed at destructor (RAII), so a SIGSEGV / SIGKILL / power loss between recordFire calls lost up to 30 minutes of fire records.

  • ANTS-1141 (5/5) — parent-dir fsync after rename. New secureio::fsyncParentDir(path) helper (Postgres / SQLite / Git pattern). Called after every successful std::rename in Config::save and SessionManager::saveSession / saveTabOrder. ext4 with data=ordered could otherwise lose the rename's directory-entry update on a crash between rename(2) returning and the journal commit.

  • **ANTS-1142 (4/4) — MainWindow sessionFailed listener

    • status-bar surface.** MainWindow now connects to GlobalShortcutsPortal::sessionFailed with a qWarning
    • showStatusMessage fallback. Pre-fix code emitted the signal to no listener — on GNOME (where CreateSession succeeds but BindShortcuts fails) the user just saw "F12 doesn't work" with no diagnostic. The KDE-presence guard lift sub-fix (for moveViaKWin / centerWindow) deferred — small but distinct refactor.
  • ANTS-1144 (3/3) — BgTasks dialog ANSI strip. tailFile in claudebgtasksdialog.cpp now strips CSI/SGR escape sequences before HTML-escape via a regex pass. Build logs with ncurses-style progress bars (make -j$(nproc), claude-code's status bar, etc.) no longer show literal ^[[2J / ^[[31m etc. in the dialog. Per CLAUDE.md rule 3 (reuse-before-rewriting), the ideal would route through VtParser/TerminalGrid — but the regex pass covers ~99% of in-the-wild noise without pulling in the parser link footprint. Same regex also strips OSC sequences (window-title updates). Transcript chunked render still deferred (touches claudetranscript.cpp's setHtml flow more invasively).

v0.7.67

Theme: Bundle G Tier 2 progress sweep. 8 ANTS IDs touched (ANTS-1134, 1136, 1137, 1138, 1141, 1142, 1143, 1144) — 3 fully shipped (1137, 1143 doc-only, AI partial-stream fail-closed in 1144), 5 partial (sub-fixes shipped, remaining sub-fixes annotated in ROADMAP for 0.7.68). 128/128 ctests pass throughout.

Fixed

  • ANTS-1134 (partial) — modifier-only keypress guard. TerminalWidget::keyPressEvent no longer resets m_scrollOffset on bare modifier presses (Shift / Ctrl / Alt / Meta / AltGr / Key_unknown). Pre-fix code killed the scrolled-up workflow when the user reflexively pressed Ctrl in preparation for Ctrl+Shift+C. Span-cache invalidation on scrollback push deferred to 0.7.68.

  • ANTS-1136 (4/5) — audit pipeline doc + correctness. CLAUDE.md pipeline-order line corrected to match handleCheckOutput reality (.audit_suppress marks not drops; dedup runs after drop steps); Confidence formula description spelled out (floor +10, severity×15, cross-tool +20, AST +10, grep-and-short −5, test path −20, AI verdict caps ≤30 / ≥80). consolidateMypyStubHints dedup-key width 16→24 hex via AuditEngine::computeDedup. cancelAudit sets m_snapshotPersisted = true before renderResults() so cancelled-run partial picture doesn't pollute trend.json. static QString sourceForCheck trampoline dropped — one call site uses fully-qualified AuditEngine::sourceForCheck. refactor_set_permissions_pair rule removed from audit_rules.json (duplicate of hardcoded setPermissions_pair_no_helper). RuleQualityTracker durability deferred to 0.7.68.

  • ANTS-1137 — MainWindow chrome perf hotspots. refreshRoadmapButton replaces unbounded QDir::entryInfoList (which enumerated the entire CWD on every 2 s status tick) with three explicit QFileInfo::exists calls for the case-variant ROADMAP.md filenames (O(1) instead of O(N) per tick). New m_repoVisibilityProbeInFlight map mirrors m_reviewProbeInFlight to drop redundant gh repo view QProcesses on fast tab-switches.

  • ANTS-1138 (partial) — applyTheme early-return. MainWindow::applyTheme(name) early-returns when name == m_currentTheme && !m_currentTheme.isEmpty(). Closes the auto-profile-rules re-entrancy path (updateStatusBar tick → checkAutoProfileRules → applyTheme → setTheme → onConfigFileChanged → applyTheme) without depending on the m_inConfigReload latency. Pattern-cache invalidation deferred to 0.7.68.

  • ANTS-1141 (4/5) — Config + persistence hardening. Sessions directory tightened to 0700 after mkpath (matches the config dir which was already 0700 by accident of an earlier run; no longer leaks filenames + mtimes + sizes via directory listing). Config::setKeybinding / setPluginGrants / setPluginSetting / setRawData short-circuit when m_loadFailed is true (no more fictional in-memory state on a parse-failure recovery session). cleanupOldSessions extended to sweep orphan *.tmp files older than 1 day (catches rename-failure leftovers). SessionManager::loadTabOrder no longer destructively removes tab_order.txt on read (the atomic-write on save overwrites it; the destructive read lost the order on a 0-5 s crash-window before saveAllSessions could fire). Parent-dir fsync deferred to 0.7.68.

  • ANTS-1142 (2/4) — Wayland portal + debug log. GlobalShortcutsPortal::onBindShortcutsResponse drains m_pending and clears m_sessionHandle on BindShortcuts failure — sessionFailed is now terminal-per-process, no more failure-loop pathology on GNOME (where CreateSession succeeds but BindShortcuts fails). Debug-log file creation wraps s_file.open() in umask(0077) save/restore so the kernel applies 0600 at create time, eliminating the same-UID race window between open and the post-open setOwnerOnlyPerms call. Two sub-fixes deferred to 0.7.68: KDE-presence guard lift (for moveViaKWin / centerWindow), MainWindow listening to sessionFailed with qWarning fallback.

  • ANTS-1143 — PLUGINS.md spec drift. string.dump added to "libraries removed" list (the runtime nils it via LuaEngine::stripDangerousGlobals for CVE-2014-5461 defence; doc was missing it). os.date() example replaced with sandbox-safe ants._version so copy-paste doesn't crash. C-call timeout caveat documented (string regex / table.sort comparator chains bypass the instruction-count hook). Optional Lua wall-clock watchdog stays explicitly out-of-scope per the original spec.

  • ANTS-1144 (1/3) — AI dialog partial-stream Insert fail-closed. m_insertBtn->setEnabled(!hadError) — pre-fix code enabled Insert on truncated responses (e.g. network drop mid-fenced-block leaving the prefix intact), inviting the user to confirm a command missing its tail. Now Insert is only reachable on cleanly-completed responses. Two sub-fixes deferred to 0.7.68: transcript incremental render, BgTasks dialog ANSI strip via VtParser reuse.

v0.7.66

Theme: Bundle G Tier 1 remainder. Two HIGH fixes from the 2026-05-01 indie-review sweep — ANTS-1131 (PTY child-process lifecycle) and ANTS-1132 (IPC socket trust-model gaps). Tier 2

  • Tier 3 follow in subsequent bumps.

Fixed

  • ANTS-1131 — PTY child-process lifecycle. Two related bugs in src/ptyhandler.cpp + src/claudeintegration.{cpp,h}

    • src/mainwindow.cpp:
    1. Pty::onReadReady orphans m_childPid on EOF-before-reap (indie-review L3 HIGH-2). Pre-fix code unconditionally cleared m_childPid = -1 even when waitpid(WNOHANG) returned 0 (child alive, just not reaped) — defeating the destructor's SIGTERM/SIGKILL escalation block. Fix: gate the clear on w > 0. When waitpid returns 0, the destructor's escalation thread handles cleanup as designed.
    2. m_planModeByPid PID-reuse poisoning (indie-review L3 HIGH-3). The per-PID plan-mode cache grew monotonically over a session — never pruned on tab close. Linux PID reuse meant a freshly-launched shell could inherit a stale plan-mode flag from a closed Claude tab. Fix: new ClaudeIntegration::forgetShell(pid) called from MainWindow::closeTab alongside the existing m_claudeTabTracker->untrackShell(pid).
  • ANTS-1132 — IPC socket trust-model gaps. Three related hardenings (indie-review L5 HIGH-1 + L3 HIGH-4):

    1. SO_PEERCRED UID match on remote-control accept. Trust-model comment at remotecontrol.cpp:31-36 claimed UID-scope but the code only enforced file-side perms (0700). Now getsockopt(SO_PEERCRED) confirms the peer UID matches getuid() before any verb dispatches — defence-in-depth for the case where the socket path is ever moved (e.g. ANTS_REMOTE_SOCKET env override) and file ACLs stop being load-bearing.
    2. UserAccessOption + safeToUnlinkLocalSocket precheck on Claude hook + MCP servers. Pre-fix code called removeServer() against a /tmp/ants-claude- hooks-<pid> path with no symlink guard — a hostile same-UID process could pre-create a symlink there pointing at e.g. ~/.ssh/known_hosts and removeServer would unlink the target. Now both paths gate the unlink behind the lstat-checked S_ISSOCK + UID match guard (lifted into secureio.h from its file-scope home in remotecontrol.cpp so all three sockets share one helper).
    3. Per-connection idle timeout on remote-control. Slow-loris defence — QTimer::singleShot(5000, socket, &QLocalSocket::abort) per accepted connection. A peer that opens an idle connection without sending bytes can no longer hold the socket indefinitely.

v0.7.65

Theme: Bundle G Tier 1 fix-pass. Two CRITICAL fixes from the 2026-05-01 multi-agent indie-review sweep — ANTS-1118 (smooth-scroll snapshot race during streaming, root cause identified by the terminal-widget-paint reviewer) and ANTS-1130 (VT alt-screen + scroll-region invariants — DECSET 47/1047/1049 conflation, resize() destroying DECSTBM, alt scroll-region OOB on shrink-resize). Two more Tier 1 items (ANTS-1131 PTY child lifecycle + ANTS-1132 IPC socket trust gaps) defer to 0.7.66 to keep this release's blast radius small. Tier 2 + Tier 3 follow per the fold-in plan.

Fixed

  • ANTS-1118 — smooth-scroll snapshot race. User report 2026-04-30 + downgrade 2026-05-01 ("scrolling up during a Claude Code stream briefly shows overwritten text"). Root cause: wheelEvent set m_smoothScrollTarget and started the timer, but the first 16 ms tick had intStep = int(0.9) = 0, so updateScrollBar() didn't fire and the snapshot stayed uncaptured. During the 16–32 ms race window onVtBatch saw m_scrollOffset == 0 and let the grid mutate the rows the user was scrolling past — once the snapshot finally captured, it captured the post-mutation grid. Fix: trigger captureScreenSnapshot() + m_grid->setScrollbackInsertPaused(true) on scroll intent (positive m_smoothScrollTarget from offset 0 with no existing snapshot) rather than on committed offset transition. Plus smoothScrollStep timer-stop branch calls updateScrollBar() so any intent-captured-but-never- committed snapshot is dropped. Spec at docs/specs/ANTS-1118.md; feature test at tests/features/scroll_snapshot_intent/ drives 4 INVs (source-grep on the wheelEvent intent branch + smoothScrollStep cleanup + onVtBatch regression guard).

  • ANTS-1130 — VT alt-screen + scroll-region invariants. Three findings from indie-review L1 (VT parser + grid), all in terminalgrid.cpp, collapsed into one fix-pass:

    1. DECSET 47/1047/1049 split. Per xterm ctlseqs only mode 1049 carries DECSC semantics; 47 and 1047 don't save or restore the cursor / SGR / origin / wrap. Pre-fix code conflated all three. New m_altScreenMode member tracks which mode entered alt-screen; on exit only the 1049 path restores DECSC. Programs sometimes enter with 1049 and exit with a different code — xterm uses entry-mode for the decision regardless of exit code, and we now match.
    2. resize() preserves DECSTBM. Pre-fix code did m_scrollBottom = m_rows - 1; m_scrollTop = 0; — destroying any TUI's scroll region on every window resize (tmux split panes, less with status line, mc, all lost their regions). New behaviour: clamp the existing top/bottom to the new row range. If the user shrinks below the previous bottom, bottom is clamped; top is preserved.
    3. Alt scroll-region OOB on shrink-resize. Pre-fix code never clamped m_altScrollTop / m_altScrollBottom at all — after a shrink-resize while on alt screen with a non-default scroll region, regionSize = m_scrollBottom - m_scrollTop + 1 could exceed m_screenLines.size() and the next scrollUp's std::rotate would read past the end of m_screenLines. Latent UB → crash class. Same clamp as item 2, applied symmetrically to the alt scroll region.

v0.7.64

Theme: Companion partial closeouts pre-Bundle-G. ANTS-1106 ships the Roadmap viewer's Kind-faceted secondary filter — the last half of the "mandatory Kind: field + viewer faceted categorisation" item, with the field-required half having landed in 0.7.59 alongside the standards doc update. Three status hygiene flips: ANTS-1116 v1 (ants-helper drift-check) and ANTS-1117 v1 (roadmap-query + tab-list IPC verbs) both shipped in 0.7.59 but the status emoji never flipped — done now, with v2 of each explicitly noted as deferred per their specs. ANTS-1118 reframed from HIGH to MEDIUM after the user confirmed the bug downgraded post-0.7.63 (still a temporary visual artifact, but the scrollback buffer itself is intact).

Added

  • ANTS-1106 — Roadmap viewer Kind-faceted filter. A new Kind row in RoadmapDialog underneath the existing status- checkbox row, with one toggle per Kind value (12 total — the 10 spec-defined implement / fix / audit-fix / review-fix / doc / doc-fix / refactor / test / chore / release, plus the de-facto research and ux values that occur in the live ROADMAP). Each checkbox label uses an emoji prefix as a visual cue (✨ implement, 🐛 fix, 🔍 audit-fix, etc.). Empty filter set = no narrowing (current behaviour preserved); non-empty filter = OR-include semantics across the active Kinds; bullets without a Kind: line are excluded under non-empty filters (the user filtering by Kind doesn't want unclassified bullets sneaking through). Implementation: extended the static RoadmapDialog::renderHtml with a defaulted const QSet<QString> &kindFilter parameter; bullet walk peeks ahead for the Kind: line via the same regex parseBullets already uses; m_kindFilter set member + per-Kind QPointer<QCheckBox> widgets with stable roadmap-filter-kind-<value> objectNames for QSS targeting
    • test discovery. Spec at docs/specs/ANTS-1106.md; feature test at tests/features/roadmap_kind_facets/ drives the helper through 7 INVs (pure-helper drive across empty / single-Kind / multi-Kind / missing-Kind shapes, source-grep on the row + objectNames). Standards doc (docs/standards/roadmap-format.md) was already updated to mark Kind: as Required as of v1.1 in 0.7.59 — no doc change needed in this release.

Changed

  • ROADMAP status flips for ANTS-1116 v1 + ANTS-1117 v1. Both shipped in 0.7.59 but the status emoji never flipped. ANTS-1116 v1 = the ants-helper drift-check subcommand on the optional CLI binary (-DANTS_ENABLE_HELPER_CLI=ON, default OFF). ANTS-1117 v1 = the roadmap-query + tab-list IPC verbs on the existing remote-control Unix socket. Both have full feature-test coverage from when they shipped — the status flip is documentation hygiene only. v2 of each (MCP server wrapper + CLAUDE.md §8 bias rule for 1116; audit-run IPC verb for 1117) explicitly stays deferred per their respective specs — both are gated on ANTS-1120 measurement validation.

Fixed

  • ANTS-1118 — severity downgraded HIGH → MEDIUM after user confirmation. User report 2026-05-01: "the scrollback issue still happens but the severity has been downgraded — when I scroll back to the affected area, the text shows correctly. So it seems to be just a temporary visual artifact which we should still fix but it isn't as serious as it was in the past." Reframe: the scrollback buffer is intact (the 0.7.49+ pin/anchor work is doing its job); what's broken is the viewport repaint during a stream — visible cells momentarily reflect new-line content before the next paint cycle settles them back to the scrolled position. Folded into Bundle G (/audit + /indie-review sweep) as a candidate finding — multi-agent fresh-eyes pass over terminalwidget.cpp paint code is the next step.

v0.7.63

Theme: Outstanding-queue cleanup pre-Bundle-G. ANTS-1014 clipboard-write redaction lands as the one real code fix (centralises 15 raw setText(...) sites behind a single hygiene funnel — OSC 52 was the headline exfil vector flagged by the 7th-audit memory). Two regressions get explicit "awaiting repro" status flips so the upcoming /audit + /indie- review pass doesn't re-flag them as untreated bugs. Six big items get explicit deferral annotations with re-evaluation triggers so orphan 📋 bullets read as "deferred-by-design, not forgotten."

Added

  • ANTS-1014 — clipboard-write redaction funnel. New module src/clipboardguard.{h,cpp} exposes a pure sanitize(text, source) helper splitting on a Source enum (Trusted / UntrustedPty / UntrustedPlugin) plus a thin writeText(text, source, mode = Clipboard) wrapper. All sources strip embedded QChar(0) (NUL bytes that round-trip through clipboard managers in unpredictable ways). UntrustedPty (OSC 52 from the shell) and UntrustedPlugin (ants.clipboard.write from Lua plugins) cap at 1 MiB — defence-in-depth against accidental DoS via cat /dev/urandom | base64. Trusted (user-initiated UI copy actions) passes large inputs through unchanged so a 10 MiB context-menu copy of a file dump still works. All 15 raw QApplication::clipboard()->setText(...) sites (13 in terminalwidget.cpp, 2 in mainwindow.cpp) converted with appropriate source classifications. We deliberately do NOT apply pattern-based secret detection — false-positive redaction is worse than no redaction for this user surface. Spec at docs/specs/ANTS-1014.md; feature test at tests/features/clipboard_redaction/ drives the helper through 8 INVs (pure-helper drive across the three sources for NUL-strip + cap behaviour, source-grep that no raw setText( survives, source-grep that the two untrusted classifications appear).

Changed

  • ROADMAP outstanding-queue cleanup. Bundle G (full /audit
    • /indie-review) is up next; this pass tidies the queue so the audit surfaces only new signal:
    • ANTS-1052 + ANTS-1118 flipped 📋 → 🚧 with explicit "Awaiting user repro" headlines. Both have diagnostic logging in tree (commit 1abf768) waiting for the user to capture one repro session each. Status change so the /indie-review run knows the bugs are mid-investigation.
    • ANTS-1043 + ANTS-1044 annotated "Deferred to post-1.0" with structural-refactor rationale + the re-evaluation trigger ("when the file crosses 7000 LoC or when a feature touches three of the four extraction lanes at once").
    • ANTS-1003 / 1004 / 1005 / 1006 bundled under a single "Deferred to a dedicated 0.8.x test-infra release" preamble — these four mechanical CI-lane additions belong together.
    • ANTS-1107 + ANTS-1108 paired: 1107 (App-Build doc folder) waits for 1108 (Native App-Build runner) so the docs aren't created stale. 1108 itself deferred — needs an ADR + phased plan + the ANTS-1120 measurement (or its successor) confirming token-saving leverage on the heavy verbs before scoping.
    • ANTS-1115 — perf sweep deferred to 0.8.x with a baseline-first prerequisite — the ten-row table's "Expected win" column is unmeasured conjecture; landing bench_paint_throughput.cpp / bench_search_throughput.cpp scaffolding first means the sweep ships measurable wins rather than vibe-driven refactors.
    • ANTS-1053 — per-tab BgTasks annotated "Blocked on ANTS-1052 root cause" so the natural ordering is locked in: fix the regression first, then the per-tab refactor falls out as a clean follow-up rather than risking the same hidden bug.

v0.7.62

Theme: Bundle B of the pre-0.7.61 outstanding-item triage — two small UX wins that close ROADMAP carryover. The update-available indicator moves out of the status bar onto the menu bar so the one-shot "you have a new version" call-to-action reads as visually loud chrome instead of competing with the steady-state widgets, and the git-branch chip on the status bar gains a colour cue (green for main/master/trunk, amber for everything else) that pairs it visually with the existing "Public/Private" repo-visibility pill next to it.

Changed

  • ANTS-1124 — update-available indicator → menu bar. The GitHub release-check probe used to surface a clickable QLabel on the right edge of the status bar (m_updateAvailableLabel, added in 0.7.45). 0.7.62 promotes it to a top-level menu-bar QAction (m_updateAvailableAction), inserted to the right of &Help by call order. The one-shot CTA framing now reads louder, the status bar reclaims its rightmost slot for steady-state telemetry, and a future setCornerWidget move is unblocked. URL is stashed via QAction::setData; the triggered lambda replays it through the existing handleUpdateClicked slot — same in-place AppImageUpdate flow, same Update / Skip / Postpone dialog, no probe-logic changes. tests/features/update_available_menubar/ locks the migration via 8 source-grep INVs (no leftover m_updateAvailableLabel token, action wired via m_menuBar->addAction or insertAction, visibility-toggle on the action, whitespace-tolerant connect grep, action starts hidden). Spec at docs/specs/ANTS-1124.md carries the cold-eyes review fold-in (F1 / F2 / F3 / F4 / F5 / F7 / F15).

  • ANTS-1109 — git-branch chip colour cue. User screenshot 2026-04-30: the main branch chip read inconsistent with the framed Public repo-visibility pill next to it. The chip's shape (border-radius, padding, font) was already aligned; what was missing was a branch-derived colour. New pure helper branchchip::isPrimaryBranch (in src/branchchip.h) returns true for main/master/trunk, false for every other ref. Both branch-chip styling sites (MainWindow::applyTheme for theme switches + MainWindow::updateStatusBar for per-poll branch changes) consult the helper and pick theme.ansi[2] (green, same role as Public) or theme.ansi[3] (amber, same role as Private). Visually pairs the chip with the pill; functionally tells the user at a glance whether they're on the project's primary branch. tests/features/status_bar_branch_chip/ drives the helper over six branch-name inputs (positive + negative + empty + case-sensitive edge case) and source-greps every chip-setStyleSheet site for the helper consult and the palette-index wiring (cold-eyes F1 fold-in: pin every site, not just the first match). Margin asymmetry between the chip and the pill is preserved on purpose — chip is the leftmost status-bar widget, pill sits to its right.

  • ANTS-1100 + ANTS-1119 — ROADMAP status flips (Bundle A). Both shipped in 0.7.59 but the status emoji never flipped: ANTS-1100 (RoadmapDialog faceted-tabs redesign — five preset filters + search + persisted geometry) and ANTS-1119 (AuditEngine extraction — Qt6::Core-only module that unblocks non-GUI audit consumers like ants-helper v2 and the future MCP server). 📋 → ✅, no code touched. ANTS-1049 (audit-pipeline populateChecks-as-data-table) body trimmed to a "subsumed by ANTS-1044" note — same scope as the structural-tier auditdialog.cpp decomposition, kept under one ID per roadmap-format.md § 3.5.1.

Fixed

  • tests/features/github_status_bar/ — INV-1, INV-2, INV-11 reflect the ANTS-1124 migration. Previously locked the old QLabel *m_updateAvailableLabel shape (member declaration, addPermanentWidget, &QLabel::linkActivated, setOpenExternalLinks(false)). Now asserts the new QAction *m_updateAvailableAction shape (member, addAction, &QAction::triggered) plus a negative regression check that no m_updateAvailableLabel identifier survives anywhere in mainwindow.{h,cpp}. Stale <regex> include also removed.

v0.7.61

Theme: dialog-and-roadmap polish bundle — fixes three user-visible chrome bugs (theme propagation to dialogs, focus-return on dialog close, pseudo-modal click-blocking on the parent), splits the now- 260 KiB ROADMAP.md by minor version so tooling can keep up, retires two long-deprecated standards docs, and folds in a batch of indie-review Tier-3 closures (KWin position tracker rename + leak fix, post-fork stack-only argv).

Added

  • ANTS-1125 — per-version ROADMAP archive. The ROADMAP.md grew past 260 KiB and was hitting tooling caps (Read-tool 256 KiB, roadmap-query IPC cache, RoadmapDialog rebuild). Closed minors rotate to docs/roadmap/<MAJOR>.<MINOR>.md. The viewer pulls archives in only on demand (History preset OR non-empty search) via three new pure-static helpers (archiveDirFor, loadMarkdown, shouldLoadHistory) that the feature test drives without instantiating a dialog. Numeric (major, minor) descending sort defeats the lexical-sort trap at minor 10 (0.10.md before 0.9.md). Per-file 8 MiB cap + total 64 MiB cap with truncation sentinel. Thematic-break + HTML- comment sentinel separators between archives. Symlink-safe canonical-path resolution. Initial archives at docs/roadmap/0.5.md and 0.6.md; 0.7 stays in the live ROADMAP.md until 0.8.0 cuts. /bump-driven rotation via packaging/rotate-roadmap.sh (atomic via mktemp+mv, idempotent, no-clobber on existing archives, regex-escapes the closed-minor's ., handles EOF sections, walks closed-minor sub-headings together). docs/standards/roadmap-format.md § 3.9 documents the rotation convention. Spec at tests/features/roadmap_viewer_archive/spec.md — 18 INVs across three rounds of cold-eyes review (4+8+5+5 → 0+3+4+5 → 0+0+3 → clean).

Fixed

  • ANTS-1128 — theme stylesheet now reaches top-level dialogs. User reports 2026-04-30 (menubar dropdown background + Review Changes dialog footer) traced to MainWindow::applyTheme calling setStyleSheet(ss) on the MainWindow instance — Qt's stylesheet engine only propagates through a widget's render subtree, NOT through QObject parent-child relationships, so top-level QDialogs never inherited the theme. Switched to qApp->setStyleSheet(ss), which fans out to every widget including not-yet-constructed dialogs. All 9 dialog classes plus 7 ad-hoc new QDialog(this) instances (about, paste preview, snippet editor, Review Changes, settings-restore, etc.) now pick up the active theme.

  • ANTS-1050 — auto-return focus to terminal when any dialog closes. User request 2026-04-28: "once any dialog box is closed, automatically shift focus back to the terminal prompt." The existing qApp eventFilter gained a Close-event branch backed by a pure-logic helper dialogfocus::shouldRefocusOnDialogClose in src/dialogfocus.h. Helper returns true iff the event is a QEvent::Close, the watched object is a QDialog subclass, AND no other QDialog is still visible (stacked-dialog suppression). Refocus is deferred via QTimer::singleShot(0, ...) so the dialog teardown completes before MainWindow grabs focus. Null-guarded against early-startup dialogs closing before any terminal exists.

  • ANTS-1051 — pseudo-modal click-blocking on the parent. User request 2026-04-28: "when a dialog box is open, only the dialog box is interactive, anything behind the dialog box should not be interactive." 0.7.50 made all dialogs non-modal to dodge QTBUG-79126 (KDE+KWin+Qt 6.11+frameless+translucent: setModal(true) drops button clicks on Wayland), losing the click-blocking semantics. Restored manually via the qApp eventFilter and a new helper dialogfocus::shouldSuppressEventForDialog. Mouse/key/ wheel events landing outside any visible dialog's tree are swallowed; events on the dialog itself (or its descendants) pass through. Stacked dialogs both allowed; the strict-ancestor edge case (a widget is not its own ancestor in Qt) is handled explicitly so clicks on the dialog's own frame work. KeyRelease paired with KeyPress to prevent modifier-state desync.

  • ANTS-1058 — menubar dropdown flicker on mouse motion. User confirmation 2026-04-30 that the 0.7.5 NoAnimStyle plus the 0.7.5+1 HoverMove suppression were fully sufficient on the user's stack; the KWin-side experimentation hypothesis didn't materialise. Closed without further fix-pass.

  • ANTS-1054 — mystery flashing dialog in centre of terminal. User confirmation 2026-04-30 that another Claude Code session resolved the flashing-dialog popups by adding a YML configuration file (file local to the user's environment; Ants source has no related change). The ANTS_TRACE_DIALOGS=1 trace tooling introduced in 0.7.57/0.7.58 stays in place for future incidents.

  • ANTS-1045 — XcbPositionTracker rename + non-KWin bail + temp-file leak. Indie-review-2026-04-27 Tier-3. Class + files renamed to KWinPositionTracker (the class doesn't use XCB at all; it talks to KWin's scripting D-Bus interface). KWin-presence guard checks KDE_FULL_SESSION=true OR XDG_CURRENT_DESKTOP containing KDE and bails before any temp-file write — non-KWin desktops no longer accumulate kwin_pos_ants_*.js files at ~10 per Quake-mode toggle. QScopeGuard-style cleanup runs QFile::remove(scriptPath) on every synchronous failure path; the async dbus chain dismisses the guard and owns cleanup explicitly via the QProcess finished / errorOccurred lambdas. The 0.7.12 TOCTOU fix (QTemporaryFile + setAutoRemove(false)) is preserved.

  • ANTS-1046 — post-fork heap allocations in flatpak detect path retired. Indie-review-2026-04-27 Tier-3. The previous std::string + std::vector<const char *> argv build between forkpty and execvp("flatpak-spawn", ...) relied on the glibc malloc fork-handler — usually safe, not strictly POSIX async-signal-safe in a multithreaded program. Detection + argv string buffers now build pre-fork in the parent via snprintf (POSIX § 2.4.3 async-signal-safe) into stack buffers; the child only assembles a stack-allocated const char *argv[12] table and calls execvp. Zero std::string / std::vector references between forkpty and execvp. <string> and <vector> includes dropped from ptyhandler.cpp.

  • ANTS-1012 — RoadmapDialog::rebuild unbounded read. The remaining open gap from indie-review-2026-04-27's "unbounded reads" cross-cutting theme. f.readAll() now caps at 8 MiB per the loadMarkdown helper's per-file budget — defends against symlinked /dev/zero or accidental binary content.

  • ANTS-1013 — refreshReviewButton git-status QProcess dedup. The remaining open gap from indie-review-2026-04-27's "2 s status-timer redundant work" cross-cutting theme. New m_reviewProbeInFlight member skips the git status --porcelain=v1 -b spawn when a previous probe is still alive; cleared by both the finished and errorOccurred handlers.

Removed

  • ANTS-1105 — deprecated top-level STANDARDS.md and RULES.md retired (user authorised). Both predated the docs/standards/ bundle (2026-04-13/14) and duplicated content living canonically at docs/standards/coding.md and docs/standards/commits.md. Live references updated in CONTRIBUTING.md, PLUGINS.md, README.md, docs/RECOMMENDED_ROUTINES.md, and src/auditdialog.cpp (the "Review with Claude" header now prepends the four docs/standards/ files instead).

Documentation

  • ANTS-1121 — documentation drift fold-in reconciliation. The 9-theme finding list from doc-cold-eyes-2026-04-30 was largely addressed in flight during the 0.7.50–0.7.59 cycle; this release closes the bullet with a per-theme reconciliation pass. T1 (GPU references in retired docs) folds into ANTS-1105 above; T6 (44-bullet Source: field backfill) deferred to ANTS-1129 as a focused follow-up.

  • ANTS-1122 — audit fold-in fixes flipped to ✅; the four range-loop-detach / constVariablePointer / returnByReference fixes have been in code since the 0.7.59 / 0.7.60 cycle. This release just refreshes the roadmap narrative.

  • ANTS-1126 — three-pass cold-eyes review fold-in for the ANTS-1125 archive spec. 4 CRITICAL + 8 HIGH + 5 MEDIUM + 5 LOW on first pass; 0 CRITICAL + 3 HIGH on second pass; PASS on third pass with 3 LOW polish folded inline. Plus an INV-14 cluster review on the rotation script: 0 CRITICAL + 3 HIGH + 4 MEDIUM + 4 LOW, all folded.

  • ANTS-1007 → ANTS-1011 — indie-review cross-cutting themes closed. Atomic-write drift (closed by 1016+1017), frameless+translucent exec() (closed by 1015), argv -- separator + quote tokens (closed by 1024+1028+1030), Lua + ssh permission allow-lists (closed by 1022+1024), WCAG 1.4.1 colour-only state (closed by 1034+1035+1041) — each cross-cutting bullet's narrative refreshed with the constituent fix references.

v0.7.60

Theme: ANTS-1123 indie-review Tier 2 + Tier 3 fold-in — the remaining hardening + polish bullets from the 0.7.59 4-agent independent review of the Claude Code companion features. Cache contract on the roadmap-query IPC verb honours the spec "≤ 100 ms" budget literally; AntsHelper rejects malformed JSON input shapes; AuditEngine's parseFindings no longer emits bogus SARIF locators for version-string tokens; RoadmapDialog gracefully handles a corrupt persisted geometry blob and preserves user state on Custom-tab clicks. CLAUDE.md streamlined (glrenderer removal acknowledged, module map extended for the new modules that landed in 0.7.59).

Fixed

  • ANTS-1123 indie-review Tier 2 + Tier 3 fold-in. F6: ants-helper now rejects non-object JSON request bodies ([], 42, "foo") with a usage error envelope instead of silently treating them as {}. M1: parseFindings reJustFile regex tightened to require an alphabetic-led extension — bare version-string locators like cargo/1.75 no longer create bogus SARIF physicalLocation entries. F1: roadmap-query cache now honours the spec INV-10 "≤ 100 ms" wall-clock bound on top of the existing mtime check, so an in-place edit within the same mtime tick is still picked up. F9: ANTS-1117 spec § Error-response shape rewritten to the flat-envelope convention ({ok:true, bullets:[...]}) the codebase actually ships — earlier nested-on-data wording was the outlier. LOW-1: corrupt persisted Roadmap-dialog geometry blob is now cleared instead of masquerading as valid forever. LOW-3: clicking the "Custom" tab on the Roadmap dialog now preserves the user's checkbox tuning and sort order instead of resetting to document order. LOW-2: tab-order array gained a static_assert against the Preset enum size to catch future enum drift at compile time. F4 / F7 / F8: helper docstring usage-order corrected, stdin-open failure surfaces a usage error instead of degrading to empty input, and Linux-only platform note + TOCTOU note documented in antshelper.h. L1: short-circuit regex chain in parseFindings (was matching three regexes per line; now one). M2 / L2: comments added on the applyFilter empty-line drop and the highConfidence field's live cross-tool corroboration role.

v0.7.59

Theme: Claude Code companion features — faceted tabs + search on the Roadmap viewer (ANTS-1100), two read-only IPC verbs Claude can drive (tab-list, roadmap-query; ANTS-1117 v1), an optional standalone CLI helper (ants-helper drift-check; ANTS-1116 v1), an audit-engine extraction that unblocks future non-GUI audit consumers (ANTS-1119 v1), and a measurement gate that grounds further companion work in real token counts (ANTS-1120). Bundled with three rounds of fold-in fixes: documentation cold-eyes review (ANTS-1121, T1-T9 across PLUGINS / STANDARDS / RULES / SECURITY / archival relocation), audit findings (ANTS-1122), and indie-review findings (ANTS-1123, Tier 1 shipped in this cut). Strategic context in the new ADR-0002 (cold-eyes scope cleanup of the broader companion-feature plan).

Added

  • Roadmap dialog: faceted preset tabs + search + persisted geometry. Six tabs above the existing checkbox row — Full / History / Current / Next / Far Future / Custom — each maps to a (filter, sort) preset evaluated in pure C++ over the already-parsed bullet model (zero LLM round-trips). Search box accepts substring or id:NNNN shorthand to jump to a specific bullet. Default size is now 1200×800 (was 900×700) with the user's resize persisted via the new Config::roadmapDialogGeometry saveGeometry/restoreGeometry round-trip. New static helpers (RoadmapDialog::filterFor / sortFor / presetMatching) plus a non-history Document sort that preserves authored section order. Locked by tests/features/roadmap_viewer_tabs/ (13 INVs). (ANTS-1100)

  • Two new remote-control IPC verbs (read-only): roadmap-query + tab-list. Claude Code can now interrogate the running Ants Terminal for parsed ROADMAP bullets (id, status, headline, kind, lanes per top-level status-emoji bullet) and per-tab state (index, title, cwd, shell_pid, claude_running, color) without reading the underlying files or running ps. Both verbs inherit the existing RemoteControl transport's UID-scope trust (lstat + S_ISSOCK + UID match) and only run when remote_control_enabled is on. New helpers: RoadmapDialog::parseBullets (pure markdown → bullet records), MainWindow::tabsAsJson (rich per-tab snapshot alongside the existing slim tabListForRemote used by ls). Cache on the roadmap-query path keyed on file mtime so repeated calls don't re-parse the 4500-line ROADMAP.md. Locked by tests/features/remote_control_roadmap_query/ (9 INVs) + tests/features/remote_control_tab_list/ (7 INVs). (ANTS-1117 v1)

  • ants-helper CLI binary (optional, default OFF). New standalone binary built when CMake is invoked with -DANTS_ENABLE_HELPER_CLI=ON. v1 ships a single subcommand: ants-helper drift-check wraps packaging/check-version-drift.sh and emits a unified JSON envelope ({"ok": true, "data": {"clean": true|false, "violations":[…], "raw":"…", "exit_code": N}}) Claude can parse without re-running the script's stdout through interpretation. Exit codes per docs/specs/ANTS-1116.md § INV-8: 0=clean, 3=drift, 1=handler error, 2=usage error. Builds against Qt6::Core only. v2 (audit-run, id-allocate, test-runner) contingent on ANTS-1120 measurement validating per-call token savings. Locked by tests/features/local_subagent_framework/ (8 INVs). (ANTS-1116 v1)

  • AuditEngine GUI-free module. Extracted the data-shape types (CheckType, Severity, OutputFilter, Finding, CheckResult, AuditCheck) and the three pure parsing functions (applyFilter, parseFindings, capFindings) from auditdialog.cpp (~5800 lines, mixed engine + presentation) into src/auditengine.{h,cpp} depending on Qt6::Core only. The dialog re-exports the data types via a transitive include of auditengine.h; the dialog's static helpers isCatastrophicRegex / hardenUserRegex now forward to the engine (closes the divergence vector the indie-review C-cluster surfaced — pre-fix the engine's local copies had drifted from the dialog originals on LIMIT_MATCH value, double-prefix guard, and shape detector). sourceForCheck + computeDedup likewise promoted to the engine's public surface. Pure refactor; audit pipeline output is byte-identical pre/post. Unblocks ANTS-1116 v2 / ANTS-1117 v2 audit-run (no Qt6::Widgets in the linker path). Locked by tests/features/audit_engine_extraction/ (9 INVs). (ANTS-1119 v1)

  • Companion-instrumentation harness (ANTS-1120). New scripts/measure-companion-tokens.sh skeleton + journal placeholder at docs/journal/ANTS-1120-measurement.md. Per ADR-0002 § Decision 5, before any further companion bullet beyond ANTS-1117 v1 + ANTS-1116 v1 ships, this harness runs one Claude task in two configurations (baseline vs stubbed helpers) N≥3 times each, captures per-prompt token counts from the API usage field, and produces a per-bullet keep / iterate / drop / inconclusive verdict. v1 ships the framework; the actual measurement run is gated on stub capture + a representative scripted task. (ANTS-1120)

Changed

  • auditdialog.cpp slimmed by the engine extraction. The three moved functions (applyFilter / parseFindings / capFindings) now live in src/auditengine.cpp; the dialog's orchestration path (onCheckFinished) calls through AuditEngine:: directly. isCatastrophicRegex / hardenUserRegex / sourceForCheck are unified — single definition in the engine, dialog-side static helpers are thin forwarders. (ANTS-1119 v1, ANTS-1123 indie-review C-cluster + H1 fix-pass.)

  • ants-helper JSON envelope on usage errors. Both the unknown subcommand and invalid JSON paths now emit a {"ok": false, "error": "…", "code": "usage_error"} envelope on stdout in addition to the human-readable stderr line. Keeps the contract consistent — Claude consumers parsing stdout no longer need to special-case usage errors. (ANTS-1123 indie-review F1.)

  • ants-helper error-string match to spec INV-5. bash-unavailable case now returns {"error": "bash unavailable", "code": "missing_bash"} (was: "could not start bash"). (ANTS-1123 indie-review F2.)

  • Kind: field now required on every actionable ROADMAP.md bullet (previously optional, inferred from section context). Backfilled all 100 unannotated bullets; Source: added where non-default. Spec updated at docs/standards/roadmap-format.md § 3.5 and synced to the user-level /start-app template. Partial implementation of ANTS-1106 — viewer faceted categorisation by Kind remains 📋. (ANTS-1106)

  • Documentation tree refresh (ANTS-1121, full-app cold-eyes review):

    • GPU-renderer references retired from STANDARDS.md, RULES.md, PLUGINS.md — glrenderer.cpp was removed in 0.7.44; only the QPainter+QTextLayout path remains.
    • Version stamps refreshed: PLUGINS.md (v0.6.9 → v0.7.58+), SECURITY.md Supported Versions table (0.6.x → 0.7.x; last-updated stamp), packaging/README.md (recipe-version placeholder rather than a baked 0.6.20), README.md install-snippet comment, CONTRIBUTING.md example commit subject (<ID>: shape per docs/standards/commits.md).
    • Archival snapshots (DISCOVERY.md, AUDIT.md, FIXPLAN.md, project_audit_updates.md) relocated from project root to docs/journal/ with "historical snapshot, not current" headers and date prefixes; cross-refs in EXPERIMENTAL.md and the audit_2026_04_13_v2 agent-memory entry updated.
    • docs/AUTOMATED_AUDIT_REPORT.md (17-day-stale un-dated copy) retired; the dated archives at docs/AUTOMATED_AUDIT_REPORT_2026-04-{11,13}_*.md remain. docs/AUDIT_TRIAGE_2026-04-16.md gained a "historical snapshot" header so readers don't mistake its 4 verified findings for current state.
    • ROADMAP section-target drift fixed: ## 0.7.0 / 0.7.7 / 0.7.12 / 0.7.50 headings re-tensed from "(target: 2026-NN)" to "shipped 2026-MM-DD" (or the in-flight equivalent for 0.7.50–0.7.59).
    • ROADMAP legend wording softened: "🚧 In progress (branch or open PR)" → "(active commit work — usually direct-to-main on this project; rarely a branch / PR)" reflecting the actual workflow.
  • Audit-engine fold-in fixes (ANTS-1122, scoped /audit pass): clazy range-loop-detach fixed in antshelper.cpp drift-violation parser and in roadmapdialog.cpp::parseBullets (bind split() results to a const QStringList local before iteration); cppcheck constVariablePointer fixed in auditengine.cpp::applyFilter (the cached fileLines slot is read-through-pointer, written-through-reference); cppcheck returnByReference fixed on MainWindow::roadmapPathForRemote(). Strengthened tests/features/roadmap_viewer_tabs/ test (debt-sweep finding 2.x): added INV-9b non-history-sort negative case, parsed-numeric INV-12 dimension check (previously a static-string match), and INV-13 covering sortFor(Custom) == Document.

  • AscendingDocument sort enum value retired as YAGNI (debt-sweep finding 1.1) — it was an alias declared "reserved for future use" with zero call sites; removed rather than carried forward.

Fixed

  • docs/standards/roadmap-format.md section reference drift in newly-authored specs — internal cross-refs in docs/specs/ANTS-1117.md, docs/specs/ANTS-1120.md, and docs/decisions/0002-cold-eyes-companion-cleanup.md cited § 1.2 / § 3.1 / § 3.5 for facts that actually live in § 3.3 (status emojis) and § 3.5.1 (stable-ID immutability). Corrected during the cold-eyes review's full-doc cross-check pass.

Security

  • regex-DoS divergence closed. Pre-0.7.59 the engine extraction had left two independent definitions of isCatastrophicRegex / hardenUserRegex between auditengine.cpp and auditdialog.cpp: differing LIMIT_MATCH values (200000 vs 100000), no double-prefix guard on the engine's version, and shape detectors that caught different sets of catastrophic-backtracking patterns. A pattern that triggered the bound on one path could slip through on the other — the divergence-correctness vector was the actual bug. Unified to a single definition exported from the engine; both call sites delegate. (ANTS-1123 indie-review C1+C2+C3+H1.)

New documentation

  • docs/decisions/0002-cold-eyes-companion-cleanup.md — ADR for the scope reset of the Claude-Code companion bundle (reorder ANTS-1116/1117 priority, retire ANTS-1110 catalogue + ANTS-1056 + ANTS-1114 wishlist, drop ANTS-1113 memory-drift category, register ANTS-1119 + ANTS-1120).
  • docs/specs/ANTS-1116.md, docs/specs/ANTS-1117.md, docs/specs/ANTS-1119.md, docs/specs/ANTS-1120.md — per-bullet contracts (acceptance criteria, INVs, deliverables).

v0.7.58

Theme: runtime toggle for the ANTS-1054 dialog tracer — the 0.7.57 env-var-only path required restarting Ants Terminal to diagnose the user-reported mystery flashing dialogs. New menu action lets the user enable / disable the tracer mid-session without losing tab state.

Added

  • Tools → Debug Mode → "Trace dialog show events" — checkable menu action that toggles the same DialogShowTracer the ANTS_TRACE_DIALOGS=1 env var path installs. Output goes to stderr (always) and to the events debug-log category when enabled. Status-bar transient confirms the toggle. (ANTS-1054 follow-up)

Changed

  • DialogShowTracer extracted from src/main.cpp to its own src/dialogshowtracer.{h,cpp} translation unit so both the startup env-var path and the runtime menu toggle call one setActive(bool) entry point. Process-global instance is parented to qApp; idempotent install / uninstall.

v0.7.57

Theme: indie-review backlog cleanup — surgical Tier 3 fixes (shellQuote whitelist, claudeChildrenOf extraction), a diagnostic for the mystery flashing-dialog report, a cppcheck returnByReference perf sweep, and a ROADMAP audit that flipped ~30 prior indie-review items from 📋 to ✅ once verified shipped in 0.7.52–0.7.55.

Added

  • ANTS_TRACE_DIALOGS=1 env var → DialogShowTracer event filter installed on the QApplication. Logs every top-level QWidget / QDialog Show event with className, objectName, windowTitle, parent class+objectName, and geometry to stderr (and the events debug-log category when enabled). Diagnostic step (a) of ANTS-1054 — the user reported a small popup flashing periodically in the centre of the terminal; the trace attributes the next occurrence to either an Ants spawn site (logged) or a child process (silent trace). Zero cost when the env var is unset. (ANTS-1054 step a)

  • tests/features/shellutils_whitelist/ — feature test for the ANTS-1047 whitelist fix. Verifies the four invariants from spec.md: empty input maps to '', the whitelist [A-Za-z0-9_\-./:@%+,] passes through unchanged, anything outside it forces single-quote wrapping (specifically the glob/redirection/bracket characters the old denylist missed), and embedded single quotes round-trip via the POSIX '\'' form. Pure C++ harness, links Qt6::Core only.

Changed

  • ClaudeIntegration::findClaudeChildPid(pid_t shellPid) — extracted from the two near-identical /proc/<pid>/task/<pid>/ children walks in pollClaudeProcess and ClaudeTabTracker::detectClaudeChild. Both call sites now share one helper; detectClaudeChild also gains the /proc- scan fallback that previously only pollClaudeProcess had (resilience on kernels / containers that don't expose the task/<pid>/children file). Static method on ClaudeIntegration, no global state, safe to call from any thread. (ANTS-1048)

  • Cppcheck returnByReference perf sweep across seven hot getters: Config::loadFailureBackupPath, ClaudeBgTaskTracker::transcriptPath, GhostLineEdit::ghostSuffix, TerminalGrid::windowTitle, TerminalWidget::shellTitle, TerminalWidget::imagePasteDir, PluginManager::pluginDir. Each was returning a QString by value — now returns const QString &. Hot read on the 2 s status-timer path (transcriptPath) is the most consequential, but each saves a heap allocation per call. Same class as the 0.7.55 ClaudeBgTaskTracker::tasks fix.

Security

  • shellutils.h::shellQuote switched from a denylist regex [\s'"\\$!#&|;(){}]to a whitelist regex[A-Za-z0-9_-./:@%+,]+. The denylist missed glob wildcards (*, ?), redirection (<, >), and bracket-expansion specials ([, ]); a path containing any of those reached the shell unquoted and underwent globbing. Whitelist closes the class — anything outside the safe set gets single-quote wrapped. Affects every caller of shellQuote—mainwindow.cpp claude-launch builders (cd %1 && claude --continueetc.) andsshdialog.cpp` ssh-argv construction. (ANTS-1047)

Fixed

  • ROADMAP.md hygiene — flipped 28 [ANTS-1015..1042, 1047, 1048] bullets from 📋 to ✅ after verifying each item shipped in the 0.7.52–0.7.55 cycle (or in this 0.7.57 bundle). Prior to the audit, the roadmap presented these items as outstanding even though the source files carried the corresponding fix comments. Each ✅ bullet now cites the shipped version + the exact source-comment line so the trail is reviewable. ANTS-1054 flipped from 📋 to 🚧 with the diagnostic-shipped note. Tier 3 refactors ANTS-1043, 1044, 1045, 1046, 1049 remain 📋 (large- scope items deferred — file decompositions and Wayland- specific work need their own focused passes).

v0.7.56

0.7.56 — App-Build suite alignment

Theme: App-Build suite alignment — confirm-on-close for tabs running non-shell processes, four-doc shareable standards bundle synced to the user-level /start-app template, CI-blocking metainfo escape fix, and the strategic roadmap for incorporating the App-Build workflow natively into Ants Terminal so users can run as much of it as possible without spending Claude tokens.

Added

  • Confirm-on-close for tabs running non-shell processes (ANTS-1102). Closing a tab whose shell has any non-shell descendant (vim, top, claude, tail -f, ...) shows a Wayland-correct confirmation dialog naming the process. "Don't ask again" checkbox flips Config::confirmCloseWithProcesses to false for subsequent closes. Default on. Settings UI in the Terminal tab. Default safe-shell allowlist: bash, zsh, fish, sh, ksh, dash, ash, tcsh, csh, mksh, yash. Probe walks /proc/<pid>/task/<pid>/children transitively (cap 256 visited PIDs) — Linux-only, mirrors the existing ClaudeIntegration descendant probe. Locked by tests/features/confirm_close_with_processes/ (11 invariants).
  • Reopen-closed-tab via Ctrl+Shift+Z — already shipped pre-request, surfaced as ANTS-1101 ✅ during the 1102 implementation pass. Existing m_closedTabs deque (cap 10)
    • File → Undo Close Tab now dual-references the original closeTab refactor (push lives in performTabClose).
  • Four-document shareable standards bundle at docs/standards/ (coding · documentation · testing · commits) plus an index README, with the detailed ROADMAP and CHANGELOG format spec carried as a sub-spec at docs/standards/roadmap-format.md. Commits standard mandates <ID>: <description> subjects so every commit links back to a ROADMAP item; testing standard mandates TDD by default (ANTS-1055, ANTS-1104).
  • ADR scaffolding at docs/decisions/ with 0001-record-architecture-decisions.md (Michael Nygard's format) plus per-folder README.
  • docs/specs/ and docs/journal/ placeholder folders for per-feature spec drafts and per-phase outcomes.
  • New Project standards section in CLAUDE.md pointing at the five standards files and the /start-app, /app-workflow, /close-phase skills (ANTS-1104).
  • Roadmap for incorporating the App-Build workflow natively into Ants Terminal (ANTS-1108) so the mechanical 70 % of the per-phase 9-step loop (ID allocation, /audit, ctest, drift checks, /debt-sweep, /bump, /release, fold-in templating, atomic CHANGELOG/ROADMAP edits) runs in C++ with zero LLM round-trips. Three supporting roadmap items: ANTS-1106 (mandatory Kind: + viewer faceted categorisation), ANTS-1107 (adopt App-Build documentation folder structure: glossary.md, known-issues.md, audit-allowlist.md, ideas.md, design.md, .claude/workflow.md), ANTS-1109 (status-bar git-branch chip restyle to match the Public/Private repo pill).

Changed

  • ROADMAP masthead now points to docs/standards/roadmap-format.md for the format spec (was docs/ROADMAP_FORMAT.md originally; briefly lived inline in documentation.md § 3 before being extracted as a sub-spec for token efficiency under ANTS-1104).
  • docs/standards/ now byte-identical to the user-level /start-app template at ~/.claude/skills/app-workflow/templates/docs/standards/, with per-language idiom examples and push-policy details delegated to the global ~/.claude/CLAUDE.md rather than duplicated in the project standards (ANTS-1104).
  • README.md project-structure tree adds docs/standards/, docs/decisions/, docs/specs/, docs/journal/; flags STANDARDS.md / RULES.md as deprecated (retire under ANTS-1105 with explicit user confirmation).
  • ROADMAP dialog redesign refined per user feedback — tabs now Full / History / Current / Next / Far Future (rename of All/Completed/Outstanding), search field above the TOC, default size bumped to ~1200x800 with persisted geometry (ANTS-1100).

Fixed

  • CHANGELOG.md was missing a top-level [Unreleased] block that the Roadmap dialog reads for current-work signaling (roadmap-format.md § 4.1 mandates one always, even empty).
  • Unescaped & in the 0.7.55 metainfo <release> body broke appstreamcli validate and turned CI red on every commit since the release. Now &amp;. (ANTS-1099)

v0.7.55

0.7.55 — VT parser correctness + audit hardening

Theme: VT parser correctness + audit-dialog hardening from the 2026-04-27 indie-review. Six fixes — multi-row OSC 8 hyperlink span emission, ITU/ECMA-48 colon-RGB form parsing, audit comment-suppress hyphen handling, trend-snapshot dedup, bg-tasks liveness sweep split from full reparse, and a hot-path returnByReference fix.

Fixed

  • Multi-row OSC 8 hyperlink spans miscoded. Wrapped hyperlinks stored only a single mis-shaped span on the start row; now emits per-row spans on close. Pre-0.7.55 shape preserved for single-row spans + the resize-pushed-text-to-scrollback edge case.
  • ITU/ECMA-48 colon-RGB form 38:2::r:g:b dropped a channel. Parser now detects the 4-slot colon group (colorspace + R + G + B) and reads R/G/B from the right offsets. Legacy 38;2;r;g;b and 3-slot 38:2:r:g:b continue unchanged.
  • Audit comment-suppress regex mis-handled hyphenated rule IDs. // nosemgrep: bash-c-non-literal matched only bash because the terminator class included -. Now [)\]]|$.
  • Trend snapshot duplicated on every UI filter click. Was inside renderResults which fires on every severity-pill toggle. Now gated by m_snapshotPersisted, reset in runAudit().
  • bg-tasks: split liveness sweep from full transcript reparse. New sweepLiveness() walks m_tasks via stat()+mtime check; the 2 s status-timer drives it instead of the 16 MiB transcript reparse. File watcher continues to drive full rescan() on transcript-changed.
  • ClaudeBgTaskTracker::tasks() returned by value on hot path. cppcheck returnByReference. Now const &.

Tests

  • ctest 112/112 green.

v0.7.54

0.7.54 — A11y + UX bundle (indie-review fold-in)

Theme: A11y + UX bundle from the 2026-04-27 indie-review. Six fixes — accessibility plumbing on the status bar + ToggleSwitch, plan-mode persistence across tab switches, an SSE iteration cap to prevent UI freeze on misbehaving AI endpoints, and IPv6 support in the SSH Quick Connect parser.

Changed

  • A11y — status-bar QLabels gain setAccessibleName. Branch chip, repo visibility chip, foreground-process label, status-message slot, Claude session label, Claude context bar, Review Changes button. Screen readers (Orca / NVDA / VoiceOver) now announce each chrome widget with semantic text instead of raw tab content or Powerline glyph codepoints. The Claude session label additionally updates accessibleDescription on every state transition so the screen reader announces the current state.
  • A11y — ToggleSwitch accessibility plumbing. Default accessibleName + QAccessibleStateChangeEvent on every check-state flip (checkStateSet / nextCheckState). Keyboard users now get immediate AT-SPI / UIAutomation confirmation when their toggle lands.
  • Plan-mode survives tab switch. Caches per-pid plan-mode state in m_planModeByPid and restores it on tab switch back, so the indicator no longer flickers off→on when the transcript-tail window doesn't include the latched permission-mode event.
  • AI SSE parser caps per-tick iterations + re-arms via singleShot(0). Caps at 256 lines per tick (covers ~8 seconds of legitimate streaming output before yielding); pathological bursts yield to the event loop. Prevents UI freeze on misbehaving endpoints.
  • SSH Quick Connect parses IPv6 host literals. [2001:db8::1]:2222 no longer mis-parses as host=[2001 port=0. Matches RFC 3986 §3.2.2 / OpenSSH command-line convention.

Tests

  • ctest 112/112 green.

v0.7.53

0.7.53 — Tier-1 remainders + VT/paste/plugin hardening

Theme: Tier-1 remainders + VT/paste/plugin hardening from the 2026-04-27 indie-review. Six security-class fixes — three completing the Tier-1 ship-this-week list, three from Tier-2 paste/image hardening.

Fixed

  • HIGH — VT parser dispatched OSC's trailing ESC byte as a real ESC sequence. A crafted OSC ending in ESC c triggered RIS (full terminal reset); ESC D triggered IND; ESC 7 / ESC 8 triggered DECSC/DECRC. RCE-adjacent because OSC payloads can arrive from a hostile remote shell, trigger-rule expansion, or pasted content. Fixed at the C0 pre-handler — string-state ESC no longer transitions to Escape; new OscStringEsc / DcsStringEsc / ApcStringEsc / IgnoreStringEsc peek-states consume the trailing byte and return to Ground. Locked by new tests/features/vt_osc_esc_discard/ (5 invariants × 11 sub-cases).
  • HIGH — X10 mouse byte > 0xDF corrupted UTF-8 stream. Coordinates encoded as col + 32 produced bytes ≥ 0xE0 when col exceeded 223; apps reading the emit stream mis-framed subsequent click bytes as UTF-8 continuations. Both wheel-event + mousePressEvent SGR fallback now clamp col/row to 223 in X10 mode.
  • HIGH — Lua plugin permission allow-list + intersect missing. Manifest entries are now filtered against knownPermissions() (clipboard.write, settings); prompt-result is intersected with requested set before reaching the engine.
  • HIGH — Image-paste filename injection + path escape. m_imagePasteDir canonicalised + required under $HOME; UUID4 suffix on filename to prevent millisecond-collision clobbers.
  • HIGH — Paste preview split on LF only — bare \r terminator spoofed dialog. Preview now normalises \r\n and bare \r to \n for display; actual paste still writes original bytes verbatim.
  • HIGH — Bracketed-paste 8-bit C1 form \x9B[200~ not stripped. Now strips 7-bit (\x1B[), raw 8-bit (\x9B), and UTF-8-encoded 8-bit (\xC2\x9B) forms.

Tests

  • New tests/features/vt_osc_esc_discard/ — 5 invariants × 11 sub-cases.
  • ctest 112/112 green.

v0.7.52

0.7.52 — indie-review tier-1 sweep (data-loss + Wayland + secret-leak)

Theme: First Tier-1 sweep of the 2026-04-27 indie-review findings. Eight fixes covering CRITICAL data-loss / security regressions plus HIGH security-class hardening — SessionManager silent scrollback loss, the same Wayland modal-grab pattern from 0.7.50 lurking in the update-confirmation dialog, SARIF/HTML reports leaking secrets at 0644, and five smaller HIGH items. The user-reported GitHub repo-type chip regression is also resolved (verified visible).

Fixed

  • CRITICAL — SessionManager silent data loss. saveSession + saveTabOrder used QFile::rename, which on POSIX refuses to overwrite an existing destination. Every session save AFTER the first silently failed: the .dat file held the original snapshot, .dat.tmp accumulated each new write, user scrollback never updated past the first save. Switched both to std::rename (POSIX rename(2) — atomic replace), mirroring the 0.7.12 Config fix. Errno logged on failure; orphaned .tmp removed.
  • CRITICAL — Update-confirmation dialog same Wayland modal-grab regression as 0.7.50 About fix. Was QMessageBox box(this); box.exec() — exactly the QTBUG-79126 / QTBUG-90005 click-drop pattern. Converted to the proven non-modal QDialog + plain QPushButton + clicked()→close() shape.
  • CRITICAL — SARIF / HTML export not atomic + 0644 perms. The reports embed every finding the audit ran, including any leaked secret surfaced by gitleaks / secrets_scan rules. Switched to QSaveFile + commit() + setOwnerOnlyPerms (0600).
  • HIGH — new-tab / launch IPC commands bypassed the send-text C0 filter. Both now route through filterControlChars by default; raw: true opt-out preserved.
  • HIGH — OSC 8 file:// and ftp:// schemes removed from the hyperlink allowlist. xdg-open file:///foo.desktop autoexecutes via the desktop's .desktop handler — RCE-adjacent. Allowlist now covers only http, https, mailto.
  • HIGH — SSH extraArgs quote-bypass on the dangerous--o allowlist. Switched to QProcess::splitCommand for proper POSIX shell quoting.
  • HIGH — extractCwdFromTranscript unbounded readLine. Caps at 64 KiB.
  • HIGH — AI endpoint scheme allowlist (http/https only). Rejects file:// / gopher:// / bare-host schemes up-front.
  • HIGH — openFileAtPath argv-injection via attacker-controlled paths starting with -. Now prepends ./ for combined-path branches and inserts -- for separate-path branches.

Resolved (user reports, 2026-04-27)

  • GitHub Public/Private repo-type chip not showing. Confirmed visible per user screenshot 2026-04-28.

Tests

  • tests/features/persistence_post_rename_chmod/ INV-2 revised to accept either QFile::rename or std::rename + rc==0 gating.
  • tests/features/github_status_bar/ INV-17 revised to lock the Wayland-correct dialog shape (forbid QMessageBox box(this), require new QDialog(this) + &QPushButton::clicked + &QDialog::close).
  • tests/features/remote_control_new_tab/ INV-3c/3d added.
  • tests/features/remote_control_launch/ INV-4b/4c added.
  • ctest 111/111 green.

v0.7.51

0.7.51 — config-reload inotify-loop fix

Theme: Hot-reload doesn't loop any more. Same-day follow-up to 0.7.50 — three reported symptoms (status bar permanently sticking at "Config reloaded from disk", Help → Check for Updates appearing to do nothing, Settings → Preferences not opening) all traced to a single root cause: MainWindow::onConfigFileChanged re-entering itself in an infinite inotify loop. The 0.7.31 attempt at fixing the same loop (m_configWatcher->blockSignals(true/false) bracketing) never worked because Qt reads inotify events from the event loop after the slot returns — outliving the blockSignals window. Fix is at the source: make the setters idempotent so the slot has nothing to re-trigger.

Fixed

  • Config reload re-entered itself in an inotify loop. Config::setTheme unconditionally called save() even when the value matched, so any applyTheme(m_config.theme()) from inside onConfigFileChanged rewrote the watched file → kernel inotify event → next event-loop tick re-emits fileChanged → slot re-enters → loop. setTheme now early-returns when the value matches, breaking the loop at its source. onConfigFileChanged additionally skips the no-op applyTheme call when the theme didn't change, and carries an m_inConfigReload re-entrancy flag (cleared via QTimer::singleShot(0, ...) so a save inside the slot is dropped but a subsequent genuine external edit is honored). The failed 0.7.31 blockSignals calls are removed.

    User-visible cascade now resolves:

    • Status bar stops sticking at "Config reloaded from disk" — the 3 s dismissal timer can actually expire.
    • Help → Check for Updates produces a visible "Checking for updates…" then "Up to date — running v0.7.51 (latest)" toast (or the actual update notice when behind).
    • Settings → Preferences opens the dialog instead of having it deleted-on-reload faster than the user can click.

    Locked down by tests/features/config_reload_loop_safety/ — INV-1 idempotent setter (call shape + functional mtime check), INV-2 no-op skip at the call site, INV-3 re-entrancy guard, INV-4 failed blockSignals attempt removed.

v0.7.50

0.7.50 — Wayland-correct dialog dismissal

Theme: Round four on the same dialog-button bug — and the matching Roadmap-dialog Close button. Three prior fix attempts (0.7.22, 0.7.35, 0.7.49) each diagnosed a downstream symptom rather than the actual root cause; this release identifies it from upstream Qt bug reports and applies the fix that the bg-tasks dialog has been quietly using all along.

Fixed

  • Both Help → About OK buttons (still) didn't dismiss the dialog in 0.7.49, and the Roadmap dialog's Close button hadn't worked since 0.7.43 either. User report 2026-04-28: "OK buttons still do nothing on the 2 About dialogs… Close button does nothing on the Roadmap either."

    Real root cause: both upstream Qt bugs QTBUG-79126 ("Dialogs behavior on Wayland is wrong") and QTBUG-90005 ("global modality on Wayland") document that Wayland's xdg-shell protocol has no equivalent of Qt::ApplicationModal. Calling setModal(true) (which 0.7.49 added) is a no-op on Wayland except for an aggravating side effect: KWin/Wayland routes click events into the modal-grab handler which then drops them, instead of delivering them to the dialog's button. The role-based dispatch path inside QDialogButtonBox is a known second aggravator on the same bug.

    The Background Tasks dialog has been working all along on the same parent-window flags — by being non-modal, with a plain QPushButton whose clicked() is wired directly to QDialog::close. Both About dialogs and the Roadmap dialog now follow that proven shape: non-modal, plain QPushButton, direct clicked() → close(). No more setModal(true), no more QDialogButtonBox in these handlers. The OK / Close click reaches its slot and the dialog dismisses on the first try.

    Spec coverage extended: tests/features/help_about_menu/ INV-4 now asserts a plain QPushButton (negative grep on new QDialogButtonBox); INV-7f flips from "must call setModal(true)" to "must NOT call dlg->setModal(". tests/features/roadmap_viewer/ INV-14 mirrors the same. The regression history in help_about_menu/spec.md records the Wayland-modal root cause so the next contributor doesn't repeat the cycle.


🤖 AppImage builds via the existing release workflow. Wait for the workflow to attach the AppImage to this release.

v0.7.49

Theme: Three follow-up fixes to user reports against 0.7.48 — the About menu's OK buttons silently no-op'd again, the GitHub Public/Private badge wanted to live next to the git branch on the left, and the Background Tasks chip showed phantom counts of tasks that had long since exited.

Fixed

  • Both Help → About dialogs now dismiss on the OK button. The 0.7.35 attempt at this bug only swapped the dialog type (from QMessageBox to a custom QDialog); both dialogs still went through exec(). On KDE/KWin + Qt 6.11 with our frameless + WA_TranslucentBackground MainWindow, the nested QEventLoop exec() opens does not surface the dialog as the active input window — the OK button never receives focus, the click silently no-ops, and the user has to dismiss via the WM close button (the same symptom shape as the 2026-04-25 report).

    Both About handlers now use the heap + Qt::WA_DeleteOnClose + show() + raise() + activateWindow() pattern that the Background Tasks, Settings, and Claude Transcript dialogs already use successfully under the same parent-window flags. The OK button's clicked() signal is also wired directly to QDialog::accept in addition to the existing QDialogButtonBox::accepted connection — belt-and-braces in case any platform plumbing interferes with the button box's internal accepted-emission. The "About Qt" entry dropped QMessageBox::aboutQt (whose internal exec() is the same regression shape) for a custom QDialog with the same heap+show treatment, surfacing the Qt runtime + build versions and a link to qt.io/licensing. Locked by tests/features/help_about_menu/ spec INV-7 (heap+show pattern, with negative grep on QDialog dlg(this) + dlg.exec() and on QMessageBox::aboutQt).

  • Background Tasks chip now reflects only tasks that are genuinely running. The transcript-only completion detection used through 0.7.48 only marked tasks finished when Claude Code recorded a matching KillShell event or a BashOutput tool_result with status=completed/killed/failed. Background tasks that were spawned and never polled — the assistant moved on without checking their output — stayed finished == false indefinitely, producing a phantom running-count chip (12 tasks showing on a session with zero genuinely-running tasks per the user report).

    parseTranscript now performs a liveness sweep at the end: each unfinished task whose outputPath either no longer exists OR whose mtime is older than a 60 s staleness window is flipped to finished. The 60 s window is generous on purpose — a slow CMake configure or link step can have 30+ s of silence between progress prints. ClaudeBgTaskTracker::rescan was promoted from a private slot to public slots: and the 2 s status timer drives MainWindow::refreshBgTasksButton, which calls rescan() directly when the resolved transcript path is unchanged (setTranscriptPath short-circuits on same-path so it can't be the entry point for the periodic sweep). Result: the chip falls to 0 within ~60 s of the last task going idle. Locked by tests/features/claude_bg_tasks_button/ spec INV-12.

Changed

  • GitHub Public/Private badge moved to the left side of the status bar, next to the git-branch chip. Was on the right (addPermanentWidget, shipped 0.7.45) where it sat alongside the Claude Code chrome; the user asked 2026-04-27 to move it next to the branch where the two badges read as a "branch · visibility" pair. Restyled to match the branch chip — same rounded bgSecondary fill + border-1px frame — while keeping the green-for-public / red-for-private foreground colour from 0.7.45 for the at-a-glance visibility cue. Locked by tests/features/github_status_bar/ spec INV-2 (negative grep on addPermanentWidget(m_repoVisibilityLabel) — must be on the left via addWidget).

v0.7.48

Theme: Two tab-bar fixes the user spotted after 0.7.47 shipped. First, the per-tab Claude Code activity dot was only painting on one tab — the most recently focused one — even when multiple tabs each had their own running Claude Code session. Second, the dot sat too close to the tab text. The first one was a real correctness bug, the second is comfort.

Fixed

  • Per-tab Claude state dot now renders on every tab with a live Claude Code session, not just the most recently focused one. ClaudeTabTracker::detectClaudeChild was assigning each shell the system-wide newest *.jsonl as its transcript path on first detection — so all N tabs running Claude ended up watching the same file, and the inverse path→pid map (m_pathToShell) was last-write-wins, fanning the QFileSystemWatcher::fileChanged signal to a single shell. Result: only the last-tracked tab's dot ever updated; the others stayed on their initial-detection state.

    The 0.7.44 fix to ClaudeIntegration::activeSessionPath (which walks up projectCwd, encodes each ancestor, and probes ~/.claude/projects/<encoded>/ for the deepest match) already had the right shape — it just wasn't reused by the tracker. This release extracts that walk-up as a static helper ClaudeIntegration::sessionPathForCwd(cwd) and calls it from detectClaudeChild. The cwd comes from /proc/<claudePid>/cwd (the directory Claude itself encodes into its project dir on launch), with /proc/<shellPid>/cwd as a fallback. Two shells in two distinct project trees now end up with two distinct transcript paths and two independent state streams.

    Pinned by INV-8 in tests/features/claude_tab_status_indicator/spec.md: a source-grep half asserts the tracker calls sessionPathForCwd and reads /proc/<pid>/cwd; a round-trip half builds a synthetic ~/.claude/projects/ tree with two encoded subdirs and verifies each cwd resolves to its own subdir's .jsonl.

Changed

  • Tab leading-edge gutter widened from 16 px to 22 px (QTabBar::tab stylesheet → padding: 6px 16px 6px 22px). The Claude state dot's center moves from r.left() + 8 to r.left() + 11 so it stays gutter-centered, leaving ~7 px between the dot's right edge and the first character of the tab text instead of the previous 4 px. User feedback 2026-04-27: "add a little space between the tab label and the Claude Code status dot." Tabs without a Claude indicator pick up the same widened leading padding; the visual change is subtle and the bar still looks balanced.

v0.7.47

Theme: Smaller-footprint update checks + a pre-update warning dialog. Two pieces of user feedback after 0.7.46 shipped, both addressed in this release: (1) "An hourly check I think is a bit much. Let's do the check when the terminal is opened and when the user clicked on Help > Check for Updates." (2) "Before an update is processed, it should warn the user that it will be restarting the terminal. Any Claude Code sessions currently running will need to be reconnected." Cadence dropped to startup + manual; the update click now opens a confirmation dialog explaining the quit-and-relaunch consequence before kicking the updater.

Added

  • Help → Check for Updates menu action (objectName helpCheckForUpdatesAction). Click → 2-second Checking for updates… status-bar message + a userInitiated=true call to checkForUpdates. The result lands as a status-bar message: Up to date — running v0.7.47 (latest) if no newer release, Update check failed: <error> on network error, or the existing badge-show flow when a newer release exists.

  • Pre-update confirmation dialog in MainWindow::handleUpdateClicked. Before the QProcess:: startDetached(updater, ...) call, a QMessageBox asks "Download and install the new version now?" with informative text explaining (a) the new version is written alongside in the background, (b) the user must quit and re-launch to start using it, (c) active Claude Code sessions will be disconnected and need to be reconnected after the restart. The Cancel branch short-circuits the spawn and surfaces "Update cancelled." in the status bar so the click registers a visible acknowledgement.

Changed

  • MainWindow::checkForUpdates(bool userInitiated = false). New userInitiated parameter (default false). Background startup probe stays silent on negative results (userInitiated=false); manual triggers from the Help menu surface their result so the user sees something happen.

Removed

  • m_updateCheckTimer member + its 1-hour setInterval + start. The hourly background poll is gone; only startup + manual remain.

Tests

  • tests/features/github_status_bar/ — INV-9 revised + new INV-17 added (16 → 17 invariants):
    • INV-9 revised: m_updateCheckTimer must be absent from the header; the 5 s singleShot startup probe still required; helpCheckForUpdatesAction objectName must exist in the Help menu setup; action's connect must call checkForUpdates(/*userInitiated=*/true).
    • INV-17 (new): handleUpdateClicked must construct a QMessageBox before the QProcess::startDetached call (Cancel must short-circuit). The dialog text must mention "quit and re-launch" AND "Claude Code" AND "reconnected" so the warning is load-bearing rather than a generic confirm prompt. Cancel must surface a "Update cancelled." status message.

Notes

  • The startup probe stays at 5 s post-construction. If you don't want even that, the simplest workaround is to disconnect from the network — the call silently fails and nothing changes.
  • Privacy footprint: a single GET to api.github.com/.../releases/latest per session (instead of per session + once per hour).

v0.7.46

Theme: Phase B of the auto-update story — actual in-place binary updates via AppImageUpdate / zsync. The 0.7.45 status bar notifier knew about new releases but only opened a download page; this release closes the loop. Click "↗ Update vX.Y.Z available" and the AppImage updates itself. v0.7.46 is the first release whose binary can be updated in place — pre-0.7.46 binaries shipped without the embedded zsync metadata and continue to be manual-download only (no way to retroactively add the ELF note).

Added

  • UPDATE_INFORMATION ELF note + .zsync sidecar. .github/workflows/release.yml now passes UPDATE_INFORMATION="gh-releases-zsync|milnet01|ants-terminal|latest|Ants_Terminal-*-x86_64.AppImage.zsync" to the linuxdeploy invocation. linuxdeploy embeds the string into the AppImage's update-info ELF note AND produces a <output>.zsync sidecar file. The upload step uploads BOTH the AppImage and the .zsync sidecar to each release. The wildcard pattern resolves against latest so the embedded URL is version-stable — every shipped binary auto-updates to whatever's tagged latest at the time the user runs the updater.

  • MainWindow::handleUpdateClicked slot wired to m_updateAvailableLabel's linkActivated signal (replaces the prior unconditional setOpenExternalLinks(true)). Probe order:

    1. AppImageUpdate (GUI) — preferred when present, gives the user a progress window they can dismiss.
    2. appimageupdatetool (CLI) — silent fallback.
    3. QDesktopServices::openUrl — only when neither updater is installed or $APPIMAGE is unset (binary isn't running as an AppImage).

    When a tool is found AND $APPIMAGE is set, it's launched via QProcess::startDetached so the updater outlives the parent process. A status-bar message acknowledges the launch.

Changed

  • Update label setOpenExternalLinks(false). Was true in 0.7.45 — clicks now route through handleUpdateClicked instead of letting Qt auto-open the URL.

Tests

  • tests/features/github_status_bar/ extended from 12 → 16 invariants:
    • INV-11 (revised) — label connects linkActivated to MainWindow::handleUpdateClicked, no longer sets setOpenExternalLinks(true).
    • INV-13 — workflow embeds UPDATE_INFORMATION with the gh-releases-zsync|milnet01|ants-terminal|latest|... schema.
    • INV-14 — workflow uploads ${OUTPUT}.zsync sidecar.
    • INV-15 — handleUpdateClicked probes AppImageUpdate (GUI) and appimageupdatetool (CLI); reads $APPIMAGE env var; falls back to QDesktopServices::openUrl.
    • INV-16 — detached spawn via QProcess::startDetached.

Notes

  • AppImage updates write a fresh file alongside the original (with a .zs-old suffix on the previous version) — the running process keeps the old contents in memory until the user restarts. Quit + relaunch picks up the new version.
  • The first release that can use this feature is 0.7.46 itself — earlier AppImages were built without the metadata and AppImageUpdate refuses to act on them.
  • AppImageHub listing (P7 follow-up #3) and aarch64 builds (#5) are still open, untouched by this release.

v0.7.45

Theme: GitHub-aware status bar — two new badges that surface context the user previously had to alt-tab to a browser to see. A Public/Private repo visibility badge for the active tab's project, and an "↗ Update vX.Y.Z available" notifier that checks the GitHub releases API hourly and lets the user click through to the release page when a newer version exists.

Added

  • m_repoVisibilityLabel — Public/Private repo badge. Small QLabel (objectName "repoVisibilityLabel") added next to the Roadmap button on the status bar. Theme-derived foreground: th.ansi[2] green for public repos, th.ansi[3] amber for private. Tooltip <owner>/<repo> on GitHub. Per-tab via refreshStatusBarForActiveTab → new refreshRepoVisibility(): walks the active tab's shellCwd() up looking for a .git ancestor, parses [remote "origin"] url from .git/config (handles both https://github.com/owner/repo and git@github.com:owner/repo URL forms), then resolves visibility via gh repo view <owner>/<repo> --json visibility -q .visibility. Result is cached by repo root with a 10-minute TTL. Failure modes (no .git, non-GitHub origin, gh missing, unauthenticated, network error) all hide the label.
  • m_updateAvailableLabel — clickable update notifier. Cyan-coloured QLabel (objectName "updateAvailableLabel", setOpenExternalLinks(true), RichText) shown when a newer GitHub release exists. New checkForUpdates() slot hits https://api.github.com/repos/milnet01/ants-terminal/releases/latest via QNetworkAccessManager, parses tag_name, and compares against ANTS_VERSION via the new pure helper compareSemver. An hourly m_updateCheckTimer plus a 5-second singleShot on startup keep the badge fresh. Click opens the release page in the user's browser. Sets a non-empty User-Agent header (GitHub 403s requests without one).
  • Pure helpers in mainwindow.cpp (anonymous namespace): findGitRepoRoot(start) walks up looking for a .git; parseGithubOriginSlug(repoRoot) extracts owner/repo from the origin URL; compareSemver(a, b) compares two X.Y.Z triples component-wise as integers (so 0.10.0 correctly outranks 0.9.0).

Tests

  • New tests/features/github_status_bar/ — 12 invariants on the bundle:
    • INV-1/2 — both labels declared, constructed, named, addPermanentWidget-ed, and start hidden.
    • INV-3 — three pure helpers (findGitRepoRoot, parseGithubOriginSlug, compareSemver) exist.
    • INV-4 — parseGithubOriginSlug recognises both URL forms.
    • INV-5 — refreshStatusBarForActiveTab calls the new refreshRepoVisibility.
    • INV-6 — refreshRepoVisibility hides the label on every failure branch (≥ 4 hide() call sites).
    • INV-7 — 10-minute cache TTL constant present.
    • INV-8 — gh invoked with the minimal --json visibility -q .visibility slug.
    • INV-9 — hourly timer + 5 s singleShot first run.
    • INV-10 — checkForUpdates hits releases/latest with a User-Agent header.
    • INV-11 — update label has setOpenExternalLinks(true).
    • INV-12 — compareSemver splits on . and uses toInt so component compares are numeric (not lexicographic).

Notes

  • Out of scope: actual binary auto-update via AppImageUpdate / zsync. That's a follow-up release — needs the build workflow to publish a .zsync sidecar AND embed the gh-releases-zsync|... update-information string in the linuxdeploy command. Cannot retroactively update binaries that didn't ship with the metadata. This release only notifies; the user clicks through to download manually.
  • Update-available badge is global (one per running binary), not per-tab. Repo-visibility badge is per-tab.
  • GitHub API rate limit (60 req/hour for unauthenticated) is well within the hourly poll budget.

v0.7.44

Theme: Two themed cleanups in one bundle. Retiring the dormant GPU-accelerated glyph-atlas renderer (compiled-but-unreachable since 0.7.4) and its no-op user-facing chrome (Settings checkbox + View menu action) — ~900 LoC removed, retires ROADMAP § "Renderer subsystem decision". Plus scoping the Background Tasks dialog to the active tab's project tree so sessions running in another project's window don't leak into this window's button.

Changed

  • Background Tasks button now scopes to the active tab's project. Previously ClaudeIntegration::activeSessionPath() walked ~/.claude/projects/ system-wide and returned the most-recently- modified .jsonl, which meant a busy session in one project's window would surface its bg-tasks count on a sibling window pointed at a different project. The method now accepts a projectCwd argument; the active tab's shellCwd() is encoded via encodeProjectPath and the helper walks up the cwd (cdUp) probing each ancestor's ~/.claude/projects/<encoded>/ subdir, returning the deepest match's newest .jsonl. Empty projectCwd falls back to system-wide newest (kept for callers that genuinely want it; nothing in tree currently does).

Removed

  • src/glrenderer.{h,cpp} (~900 LoC). The optional GPU- accelerated glyph-atlas renderer with GLSL 3.3 shaders + instanced quads + a per-cell vertex buffer. Disabled in 0.7.4 when TerminalWidget switched from QOpenGLWidget to plain QWidget; sat compiled-but-unreachable for 39 releases. ROADMAP framed the choice as "revive via createWindowContainer or delete" — picked delete. The QPainter+QTextLayout path remains the only paint path and is fast enough for every shipped corpus.
  • gpu_rendering Config key + Settings checkbox + View menu. Config::gpuRendering() / setGpuRendering(), the Settings → Appearance "GPU rendering (glyph atlas + GLSL shaders)" checkbox, and the View → "GPU Rendering" menu action all wrote to the dormant bool with no observable effect since 0.7.4. All three are removed in this release. Existing config files with "gpu_rendering": true are silently ignored — Config doesn't validate unknown JSON keys.
  • TerminalWidget::setGpuRendering(bool) / gpuRendering() + the m_glRenderer / m_gpuRendering members. The setGpuRendering setter was a no-op stub; both are gone.
  • Disabled-OpenGL-path landmark comment + commented-out m_glRenderer cleanup in the TerminalWidget destructor and paintEvent chain.

Tests

  • tests/features/claude_bg_tasks_button/test_claude_bg_tasks_button.cpp extended from 10 → 11 invariants:

    • INV-11 — three-part check on the project-scoping fix: (a) header signature activeSessionPath(const QString &projectCwd), (b) implementation references encodeProjectPath AND cdUp (the walk-up logic — no shortcut to a single string match), (c) MainWindow::refreshBgTasksButton reads focusedTerminal()->shellCwd() and passes it through to activeSessionPath(cwd).

    Spec updated with the matching contract entry (linking the user-feedback quote 2026-04-27).

  • tests/features/settings_restore_defaults/spec.md updated to drop the no-longer-applicable gpuRendering off clause from Invariant 2's reset-default catalogue.

Notes

  • No user-visible behaviour change from the GL renderer removal — the path was never reachable. The user-visible improvements are the two pieces of chrome that no longer lie about their effect.
  • Out of scope: actually adding a real GPU acceleration path (e.g. via createWindowContainer + QOpenGLWindow). If that comes back, it'll be a fresh design with current Qt 6 idioms, not a resurrection of the 0.7.4-era code.

v0.7.43

Theme: Roadmap viewer polish — the dialog the previous release introduced gets a working Close button and a dynamic section-navigation sidebar that lists every heading in the active ROADMAP.md and lets the user click to jump. The TOC is built per project from whatever headings the file happens to have (no hardcoded section names) so the same dialog adapts to every repo that opens with a roadmap.

Added

  • TOC sidebar in the Roadmap viewer. QSplitter(Qt::Horizontal) inside RoadmapDialog puts a QListWidget (objectName "roadmap-toc") on the left and the rendered viewer on the right. The list is rebuilt from the markdown on every refresh — including live-update events on ROADMAP.md change — and entries are indented two spaces per level above 1 with level-1 headings shown bold. Click or activate an entry → the viewer scrolls to the matching heading.
  • Anchor-emitting renderer. RoadmapDialog::renderHtml now prepends <a name="roadmap-toc-N"></a> before each <h1>/<h2>/ <h3>/<h4> it emits. The N-th anchor matches the N-th entry returned by the new pure helper RoadmapDialog::extractToc, which returns a QVector<TocEntry> of {level, text, anchor} triples. Both helpers are pure and static so they're driven by the feature-conformance test without spinning a Qt widget tree.
  • QTextBrowser in place of QTextEdit. Required for scrollToAnchor support. Links are pinned non-navigable (setOpenLinks(false) / setOpenExternalLinks(false)) so a stray markdown link can't replace the document.

Fixed

  • Close button in the Roadmap viewer is now actually clickable. The dialog used the role-based QDialogButtonBox::rejected signal alone; under some Qt 6 builds the standard Close button does not trigger that signal, leaving the button looking active but doing nothing. The Close button is now wired both via rejected() (kept as belt-and-braces) and via a direct clicked connection on the underlying QPushButton retrieved with button(QDialogButtonBox::Close). The direct connect is the authoritative fix.

Tests

  • tests/features/roadmap_viewer/test_roadmap_viewer.cpp extended from 10 → 14 invariants:
    • INV-11 — extractToc returns a 5-entry list with matching levels, raw text, and roadmap-toc-N anchors against a known multi-level input.
    • INV-12 — renderHtml emits an <a name="roadmap-toc-0"> immediately before the first <h2> so scrollToAnchor can position the viewer at any TOC entry.
    • INV-13 — Source-grep on roadmapdialog.cpp confirms a QSplitter, the roadmap-toc objectName, and a scrollToAnchor invocation in the TOC handler.
    • INV-14 — Source-grep confirms the Close button is wired directly via QAbstractButton::clicked, guarding against the Qt 6 rejected()-only quirk.

Notes

  • The Roadmap viewer's TOC is intentionally flat (QListWidget) rather than tree-shaped (QTreeWidget) — flat-list with leading whitespace renders the hierarchy in O(1) widget cost without per-node expand/collapse state, which would invite sync bugs on live-update refreshes.
  • Out of scope for this release: cross-document jumps (e.g. from a ROADMAP heading to a CHANGELOG entry), heading-fold/collapse, fuzzy-search inside the TOC. The dialog is still a viewer, not an editor.

v0.7.42

0.7.42 — Command Palette ghost-completion (Tab to commit /slash-style suggestions)

Theme: Inline ghost-text completion in the Command Palette — Claude Code's /slash-command UX, ported to Ctrl+Shift+P. As you type, the unmatched suffix of the top fuzzy-match is rendered in a dimmed colour right after the cursor; Tab commits the suggestion into the input. Retires ROADMAP § "Command Palette ghost-completion (near-term, small scope)".

Added

  • GhostLineEdit subclass of QLineEdit. Lives in src/commandpalette.h. Carries a m_ghost string set via setGhostSuffix(); paintEvent calls QLineEdit::paintEvent(event) first, then opens a fresh QPainter and draws the ghost at cursorRect().right() + 1 using palette().color(QPalette::Text) with setAlphaF(0.45). No layout changes — the suffix overlays the existing line-edit content area, so palette geometry is unchanged.

  • CommandPalette::updateGhostCompletion(filter). Invoked from populateList after the result list is rebuilt. Looks at m_list->item(0), recovers the underlying QAction, strips & accelerators, and:

    • if the filter is empty, list is empty, or the action name does not start with the filter (case-insensitive) → setGhostSuffix("");
    • otherwise → setGhostSuffix(name.mid(filter.length())), which preserves the action name's original casing in the ghost.

    contains()-only matches (where the filter appears mid-string in the top match) get an empty ghost — the visual contract is that the suffix appears flush after the user's typed input, which only makes sense for prefix matches.

  • Tab-key commit handler. eventFilter adds Qt::Key_Tab → commitGhost(), which appends the ghost suffix to the input via setText. The follow-up textChanged → filterActions → populateList → updateGhostCompletion cycle clears the ghost in the same dispatch since the new filter equals the action name's tail exactly. The post-commit text equals the visible composition (user-typed prefix + ghost suffix), so user-typed casing is preserved — same as shell-completion semantics. Tab is always consumed by the palette (even when the ghost is empty) so focus cannot leak out of the input while the palette is open. Tab does not also execute the action — the user presses Enter to run, matching Claude Code's /slash-completion contract.

Tests

  • tests/features/command_palette_ghost_completion/ — ten invariants spanning the contract: I1 the input is a GhostLineEdit findable by objectName; I2 empty filter → empty ghost; I3 prefix "ind" → ghost "ex Review" (first item in setActions order is "Index Review"); I4 uppercase "INDEX" → ghost " Review" (case-insensitive prefix, casing preserved from the action name); I5 contains-only match → empty ghost; I6 no-match → empty ghost; I7 Tab commit → text = filter + ghost ("index Review") and ghost cleared; I8 Tab with empty ghost is consumed (text unchanged, focus retained on input); I9 source-grep on commandpalette.cpp (exactly one setAlphaF(0.45 literal, ≥1 cursorRect( reference); I10 Esc still hides the palette and emits closed() exactly once (regression guard on existing dismiss behaviour). Drives CommandPalette directly under QT_QPA_PLATFORM=offscreen — no MainWindow link, no PTY. Pre-fix verification: with commandpalette.{h,cpp} stashed, the test fails to even compile (missing GhostLineEdit symbol); restored, test passes.

Notes

  • Out of scope. The in-terminal shell ghost-suggestion (fish-style, OSC 133 prompt-detection + history scraping) is a separate ROADMAP item marked 💭 with deferral past 1.0. It would touch terminalgrid.cpp paint path and vtparser.cpp rather than commandpalette.cpp.
  • Frequency-ranked top match. The palette today picks the top match by setActions iteration order. A frequency-ranked source (most-used action first) is a noted future refinement; the contract above stays stable across that change because the test uses a fixed action ordering.

v0.7.41

0.7.41 — A11y for chrome (accessible names + AT-SPI introspection lane)

[0.7.41] — 2026-04-27

Theme: Accessibility — explicit setAccessibleName / setAccessibleDescription on every glyph-only chrome control so Orca, Speakup, and other AT-SPI screen readers announce them by purpose ("Close window") instead of by codepoint name ("eight-spoked asterisk", "multiplication sign x"). Two ROADMAP items retired in one bundle: § "Accessibility pass on chrome" (mainwindow / palette / title-bar control labels) and § "AT-SPI introspection lane" (an automated check that every user-visible chrome control carries a name).

Added

  • Accessible names on TitleBar window controls. centerBtn ("Center window" / "Center this window on the active screen"), minimizeBtn ("Minimize window"), maximizeBtn ("Maximize window" / "Maximize or restore this window"), and closeBtn ("Close window") each carry an explicit setAccessibleName and setAccessibleDescription set immediately after setText in titlebar.cpp:54-97. Each button also gains a stable objectName (the previously-set closeBtn is joined by the matching three siblings) so the introspection test can find them deterministically. Visual rendering is unchanged — the glyphs (✥ – □ ✕) still drive the on-screen pixels; only the AT-SPI tree changed.

  • Accessible names on the Command Palette. m_input (objectName commandPaletteInput) gains Command palette search / Type to filter actions; Tab to commit; Esc to dismiss. m_list (objectName commandPaletteList) gains Command palette results / Available actions matching the current filter. Placeholder text and visible behaviour are unchanged. The placeholder is a typing hint for sighted users; the accessible name is what screen readers announce on focus — separating the two unblocks the keyboard-only workflow.

Tests

  • tests/features/a11y_chrome_names/ — eight invariants spanning the contract: T1–T4 each TitleBar button has the right accessibleName + non-empty accessibleDescription; T5/T6 the CommandPalette input + list have theirs; T7 the AT-SPI introspection lane (every reachable QAbstractButton carries either a non-empty accessibleName() or a non-empty text(), every reachable QLineEdit has an explicit accessibleName), T8/T9 source-grep counts on titlebar.cpp and commandpalette.cpp so a future refactor that drops the setAccessibleName calls fails the build, not just AT-SPI usage at runtime. The test constructs the TitleBar and CommandPalette directly under QT_QPA_PLATFORM=offscreen so it runs in CI without a display.

Notes

  • tr() translation hooks for the new strings are deferred to the 0.9.0 H10 i18n bundle so the .qm files cover both UI text and accessibility strings in one pass — pulling them in now would mean re-touching every setAccessibleName(...) call later.
  • A custom QAccessibleInterface adapter for TerminalWidget itself (the text-changed event lane behind the H9 a11y bundle in 0.9.0) is a separate, larger design cycle and is out of scope for this bundle.

v0.7.40

0.7.40 — Perf bundle — scroll-region rotate + VtBatch zero-copy

Theme: Performance — scroll-region rotate + VtBatch zero-copy across the worker→GUI thread hop. Two Tier 3 perf items from ROADMAP.md § 0.7.x ⚡ / 🏗 Tier 3 — structural retired in one bundle. Both are observably equivalent at the API level — the existing tests/features/scroll_region_rotate/ and tests/features/threaded_parse_equivalence/ regressions pass unchanged — so the win is throughput / cost, not visible behaviour.

Changed

  • TerminalGrid::scrollUp / scrollDown use std::rotate. Replaced the per-iteration m_screenLines.erase + insert loop with a single rotate over [scrollTop, scrollBottom], gated by a pool-salvage / scrollback-push pre-pass and a fresh-blank post-pass. The CSI 2J doubling-guard window check now runs once per batch (elapsed time across a batch is microseconds, so the per-iteration window-extend was redundant). m_scrollback cap enforcement collapses from N pop_front passes to one. On CSI 100 S against an 80-row screen, the row-shift cost goes from O(count × rows) memmoves (8000) to O(rows) (80).

  • VtStream::batchReady carries VtBatchPtr instead of const VtBatch &. using VtBatchPtr = std::shared_ptr<const VtBatch>; is the new alias in vtstream.h. Qt's queued- connection plumbing must own the parameters it dispatches, so the prior const T & signal forced a deep copy of the entire batch (the actions vector + rawBytes QByteArray) on every worker→GUI hop, regardless of any move-from-pending shaping the emitter performed. The shared_ptr wrap reduces the cross-thread payload to a 16-byte atomic refcount bump; the underlying VtBatch lives on the heap and is not duplicated. Both emit sites (flushBatch, onPtyFinished) build via std::make_shared<VtBatch>(). TerminalWidget::onVtBatch signature changed to void onVtBatch(VtBatchPtr batch); field access is now via batch->… instead of batch.….

Tests

  • tests/features/vtbatch_zero_copy/ — 5 source-grep invariants locking the cross-thread signal shape so a future refactor can't silently revert to the deep-copy form. Behavioural equivalence on the action stream is already covered by tests/features/threaded_parse_equivalence/.

  • tests/features/scroll_region_rotate/ — pre-existing 8 invariants on rotation correctness (I1–I8 in spec.md) still pass. The spec was written algorithm-agnostic, anticipating this swap.

v0.7.39

0.7.39 — Claude state-dot palette + status-bar Roadmap viewer

Theme: Claude Code UX bundle + status-bar Roadmap viewer. Two user requests landed in quick succession.

User request 1 (Claude state-dot palette):

"Let's have a round dot on each tab that has a Claude Code session running (no icons or anything else other than the tab label). The dot will change colour with the various states that Claude Code is in. Each state has its own colour (grey for idle). Then extend those colours to the status bar Claude Code status too."

User request 2 (Roadmap viewer):

"Add a button on the status bar to view the roadmap. So, it brings up a dialog showing the roadmap. It should have filters as well to show what is outstanding and what is completed. If at all possible, it should also highlight what item is being done currently. The roadmap button should only show if there is roadmap documentation — let's simplify that to requiring a roadmap.md file only." Plus a clarification adding a fourth emoji toggle and elevating "Currently being tackled" to a peer filter.

Added

  • Unified Claude state-dot palette across tabs and status bar. New static helper ClaudeTabIndicator::color(Glyph) in coloredtabbar.h is the single source of truth for an eight-state palette: Idle grey #888888, Thinking blue #5BA0E5, ToolUse yellow #E5C24A, Bash green #6FCF50, Planning cyan #5DCFCF, Auditing magenta #C76DC7, Compacting violet #A87FE0, AwaitingInput orange #F08A4B. Red is intentionally absent — AwaitingInput is a normal interaction state, not an error. ColoredTabBar::paintEvent calls the helper for fill colour and uses a single kDotRadius = 4 for every state; the prior AwaitingInput "outline + radius 5" treatment is removed (per "no icons or anything else"). The bottom status-bar Claude Code label was rewired to map current state → Glyph → helper colour, replacing the prior Theme::ansi[N] mappings that drifted from the tab dot's colour and varied across themes.

  • Auditing now lights the per-tab dot. Previously surfaced only on the active-tab status bar (m_claudeAuditing). Plumbed into ClaudeTabTracker::ShellState::auditing (mirrored from the existing ClaudeTranscriptSnapshot.auditing field). Tab provider routes it to Glyph::Auditing magenta; hover tooltip reads "Claude: auditing". Precedence chain unchanged across both surfaces: AwaitingInput → Planning → Auditing → state-derived.

  • Status-bar Roadmap viewer button (roadmapdialog.{h,cpp}, new files). Visible iff the active tab's shellCwd() contains a ROADMAP.md (case-insensitive — accepts Roadmap.md, roadmap.md too). MainWindow:: refreshRoadmapButton is called from the central refreshStatusBarForActiveTab tick. Click opens RoadmapDialog: a non-modal QDialog with a top row of five peer category checkboxes (✅ Done · 📋 Planned · 🚧 In progress · 💭 Considered · Currently being tackled), all default-checked. A bullet renders iff any of its enabled category memberships matches; plain narration bullets (no status emoji) always render. The static RoadmapDialog::renderHtml helper is pure so tests can drive it without spinning a Qt widget.

  • "Currently being tackled" highlight. A bullet matches the current-work signal if its first-80-character payload (status- emoji-stripped, normalised — lowercase, hyphens-and-underscores as spaces, punctuation removed) contains a phrase from a signal set built from (a) the local CHANGELOG.md [Unreleased] block bullets, (b) the last 5 non-release/non-merge/non-revert git commit subjects. Matched bullets get a yellow left-border CSS highlight (border-left: 4px solid #E5C24A — the same ToolUse yellow from the dot palette; the consistency is intentional). When the signal set is empty, no highlight renders — the feature is silent when there's nothing to point at.

  • Live-tail dialog mechanics. QFileSystemWatcher on ROADMAP.md and CHANGELOG.md, 200 ms debounce timer, and the same scroll-preservation triple shipped with 0.7.37 / 0.7.38: m_lastHtml shared_ptr cache + skip-identical-HTML guard, capture vbar before setHtml, restore with qMin(saved, vbar->maximum()) clamp, was-at-bottom pin so live appends stay visible.

Changed

  • ClaudeTabIndicator::Glyph enum gains Auditing. Inserted between Planning and Compacting so the palette ordering reads Done-side → Action-side → InFlight-side → BlockingUser. ClaudeTabTracker::ShellState gains a bool auditing field and a new comparison branch in maybeEmit so changes to the audit latch trigger shellStateChanged like every other field.

  • MainWindow::applyClaudeStatusLabel no longer reads Theme::ansi[] for Claude-label colours. All state → colour mappings now route through the unified helper. The Theme &th = Themes::byName(...) local was removed (unused).

Removed

  • AwaitingInput-specific dot decoration (radius = 5 and white outline). Colour alone is the differentiator now, per the "no icons or anything else" user spec. The outline.alpha() branch was removed from paintEvent.

Tests

  • tests/features/claude_state_dot_palette/ — 8 source-grep invariants asserting helper signature, full palette, paintEvent helper-call, uniform geometry, mainwindow status-bar wiring, Auditing plumbing through ShellState, and double-use of the Auditing glyph (provider closure + status applier).

  • tests/features/roadmap_viewer/ — 10 hybrid invariants (source-grep + behavioral). Links the dialog source so the static renderHtml helper can be driven against synthetic markdown to verify five-bit filter semantics, the highlight CSS marker on signal match, the marker's absence on empty signal sets, the case-insensitive cwd probe, the wire-up shape (m_roadmapBtn construction + click connection), and the refreshRoadmapButton call from refreshStatusBarForActiveTab.

Documentation

  • ROADMAP user-request entries added for both 0.7.39 features with full citation of the user asks. Two stale 📋 entries in the cross-cutting themes section flipped to ✅ with shipped-release citations (the /tmp/*.js TOCTOU and the no-auth local-IPC chain — both shipped in the 0.7.12 Tier 1 batch but the umbrella narrative entries weren't updated at the time).

v0.7.35

0.7.35 — About dialog OK button fix — QDialog + connected QDialogButtonBox

Theme: UX bug fix. Single user-reported regression in the Help → About Ants Terminal dialog (the GUI version indicator added in 0.7.22).

Fixed

  • About Ants Terminal dialog OK button silently no-op'd (mainwindow.cpp). Was: Help → About Ants Terminal… opened the version dialog correctly, but clicking its OK button did nothing — the dialog could only be dismissed via the window-manager close (X) button. The 0.7.22 implementation used QMessageBox::Ok with Qt::TextBrowserInteraction (which pulls in Qt::TextSelectableByMouse); under the combination of our frameless MainWindow, Qt::WA_TranslucentBackground, KDE Plasma + KWin, and Qt 6.11 this caused the OK click to be silently dropped. Now: the About handler builds a custom QDialog with a QDialogButtonBox(QDialogButtonBox::Ok) whose accepted signal is explicitly connected to QDialog::accept, giving us a click path that's standard, testable, and independent of QMessageBox's internal standard-button dispatch.

  • About dialog GitHub link was a visual-only no-op (mainwindow.cpp). Was: the link in the About body was rendered as clickable (cursor changed on hover) because Qt::TextBrowserInteraction enables Qt::LinksAccessibleByMouse, but setOpenExternalLinks(true) was never called on the label — so clicking the link emitted linkActivated() into the void. Now: the body QLabel has setOpenExternalLinks(true), so clicking the GitHub URL opens it in the user's browser.

Changed

  • tests/features/help_about_menu/spec.md + test (8 invariants). Spec rewritten to lock the QDialog + QDialogButtonBox shape, document the 2026-04-25 regression in the History section, and assert via source-grep that the QMessageBox::Ok + Qt::TextBrowserInteraction pattern cannot reappear without the test failing. Pre-fix source fails 6 of the 8 new I4 invariants; post-fix all 8 pass.

v0.7.38

0.7.38 — Background-tasks status-bar button — Claude Code live tail

Theme: Feature. Background-tasks status-bar surface for Claude Code. User request 2026-04-25:

"a button on the status bar when there are background tasks being run. We then click the button to view what Claude Code shows for the background tasks. The button opens a dialog showing the live update info on the background tasks."

Claude Code can spawn long-running tasks via Bash or Task with run_in_background: true. The TUI shows a sidebar listing them with their tail output; users dropping into a different tab to keep working had no way to see those tasks from Ants Terminal — they had to flip back to the TUI and parse the sidebar by hand. The new status-bar button surfaces the live count and the dialog mirrors the 0.7.37 Review Changes update model (debounced live tail with scroll-preservation).

Added

  • Background-tasks tracker (new claudebgtasks.{h,cpp}). A pure parser walks the active session's transcript JSONL, recognizing tool_use blocks whose input.run_in_background == true (start) and tool_result blocks whose toolUseResult.backgroundTaskId is set (confirmation, with the on-disk output path extracted from the result body). Completion / kill state is derived from subsequent BashOutput results carrying status: "completed" | "killed" | "failed" and from KillShell tool calls. The ClaudeBgTaskTracker class wraps the parser with a QFileSystemWatcher on the transcript and emits tasksChanged() when the running-count or shape changes. Static parseTranscript(path) is testable without a watcher.

  • Status-bar button (mainwindow.{h,cpp}). Sibling to the Review Changes button; same fixed sizePolicy contract. Hidden by default; visible only while runningCount() > 0 for the active tab's session. Label re-renders as "Background Tasks (N)"; tooltip discloses running + total. Re-targeted on tab switch via refreshBgTasksButton() (called from refreshStatusBarForActiveTab), so each tab's session drives its own count independently.

  • Live-tail dialog (new claudebgtasksdialog.{h,cpp}). Mirrors the Review Changes live-update model that 0.7.37 stabilized. QFileSystemWatcher covers each task's .output file plus the transcript path, so new starts and completions appear without manual refresh. A 200 ms debounce timer collapses bursts of fileChanged signals (common when the backgrounded process is noisy) into one render. Skip-identical-HTML guard via a per- dialog m_lastHtml (std::shared_ptr<QString>) preserves selection and scroll position when the render is unchanged. Capture-vbar/hbar before setHtml, restore after with qMin(..., maximum()) clamp keeps absolute scroll position when content changes — same shape as the 0.7.37 Review Changes fix. Bonus "was at bottom" detection: when the user is tailing the bottom, pin them to the bottom across appends so live output stays visible without manual scrolling.

Changed

  • tests/features/claude_bg_tasks_button/spec.md + test (10 invariants). New regression test pinning the parser shape (run_in_background and backgroundTaskId keys), the tracker's QFileSystemWatcher + tasksChanged() signal, the button's hide-when-empty contract, the connect to showBgTasksDialog, the dialog's reuse of the 0.7.37 scroll-preservation pattern, the outputPath watch enumeration in rewatch(), the debounce timer interval (≤ 500 ms, single-shot), and the CMake source list. Source-grep harness, no Qt link.

v0.7.37

0.7.37 — Review Changes dialog — scroll preserved across live refreshes

Theme: UX bug fix. Live-update regression in the Review Changes dialog (the QFileSystemWatcher + 300ms debounce wiring added in 0.7.32). User report 2026-04-25:

"When using the Review Changes dialog, the constant resetting of the text means that if I scroll, it resets to the beginning every refresh. That means I can't scroll basically."

The 0.7.32 finalize lambda called viewerGuard->setHtml(html) unconditionally on every probe completion — every git change, every debounce tic, every Refresh click. QTextEdit::setHtml re-parses the document and snaps the vertical scroll bar back to the top, also discarding selection and cursor position. On a long diff with active live updates the dialog became unscrollable.

Fixed

  • Review Changes dialog: scroll position preserved across live refreshes (mainwindow.cpp). Two-layer guard around the setHtml call inside MainWindow::showDiffViewer's finalize lambda. (1) An auto lastHtml = std::make_shared<QString>() cache threaded through runProbes and finalize lets finalize early- return when the new render is byte-identical to the previous one — the common case during idle live-update tics, where branch metadata refreshes don't change anything visible. Selection, cursor, and scroll are byte-perfectly preserved. (2) When content does change, vertical and horizontal scroll-bar values are captured before setHtml and restored after, clamped to bar->maximum() so a shorter render after a commit doesn't over- scroll. First-render carve-out (isFirstRender = lastHtml->isEmpty()) keeps the initial paint at the top.

Changed

  • tests/features/review_changes_scroll_preserve/spec.md + test (6 invariants). New regression test pinning the lastHtml cache, the skip-identical guard, the capture-scroll-before-setHtml + restore-after pattern, and the first-render carve-out. Source-grep harness scoped to the MainWindow::showDiffViewer body so other QScrollBar / setHtml call-sites in mainwindow.cpp don't cause false positives.

v0.7.36

0.7.36 — Tab bar + status bar opaque under translucent parent

Theme: UX bug fix. Same translucent-parent failure mode that bit the menubar in 0.7.25/0.7.26 caught in two more places (user report, post-0.7.32):

"The tabs themselves are fine but the rest of the tab bar across the window is transparent as well as the background for the status bar."

The 0.7.32 close-button SVG drew the user's eye to the empty-area fill that had been mis-painted all along; the bug was not introduced by 0.7.32 but became visible because the new tab look re-balanced what the user noticed.

Fixed

  • Tab bar empty area paints opaque under WA_TranslucentBackground (coloredtabbar.{h,cpp}). Was: the strip to the right of the last tab rendered the desktop wallpaper through, with the QSS QTabBar { background-color: ... } rule silently dropped by Qt's stylesheet engine on KWin + Breeze + Qt 6 once WA_OpaquePaintEvent was set on the widget. Same root cause documented in opaquemenubar.h. Now: ColoredTabBar exposes setBackgroundFill(QColor) and prepends a CompositionMode_Source fillRect to its existing paintEvent, before the base class draws tabs and before the colour-group gradient overlay. applyTheme() pushes theme.bgSecondary into the override.

  • Status bar background paints opaque under WA_TranslucentBackground (new opaquestatusbar.h, mainwindow.{h,cpp}). Was: the entire status bar strip rendered the desktop wallpaper through — same root cause as the tab bar above. Now: a header-only OpaqueStatusBar mirrors OpaqueMenuBar (paintEvent fillRect → delegate). MainWindow constructs an OpaqueStatusBar, installs it via setStatusBar(...) before the first statusBar() call (Qt's lazy-creation would otherwise install a plain QStatusBar that paints transparent), and pushes theme.bgSecondary via setBackgroundFill from applyTheme().

Changed

  • tests/features/tabbar_statusbar_opaque/spec.md + test (5 invariants). New regression test pinning the setBackgroundFill
    • fillRect mechanism on both bars, the setStatusBar install order, the WA_OpaquePaintEvent attribute on the tab bar, and the paint order inside ColoredTabBar::paintEvent (fill before the base class draws tabs). Source-grep harness, no display required.

0.7.34

Theme: Terminal correctness. One ROADMAP item retiring the last known DECOM (origin mode) bug — tmux/screen save-restore round-trips were silently dropping the origin-mode flag and ignoring scroll-region-relative coordinates on absolute cursor moves.

Fixed

  • CUP / HVP / VPA translate to scroll-region origin under DECOM (terminalgrid.cpp). Was: with origin mode (DECOM, CSI ?6h) active and a scroll region set via DECSTBM (CSI top;bottom r), CSI 1;1H jumped to absolute (0, 0) and CSI 99;1H clamped to the bottom of the entire screen instead of the bottom of the scroll region. Programs that depend on origin-mode-relative positioning (vim split-window cursor restoration, screen's caption line, tmux status redraws) ended up writing to the wrong row. Now: the 'H', 'f', and 'd' cases inside processCSI add m_scrollTop to the requested row when m_originMode is set, then clamp to [m_scrollTop, m_scrollBottom]. Behaviour matches xterm.

  • DECSC saves DECOM + DECAWM, DECRC restores them (terminalgrid.cpp, terminalgrid.h). Was: saveCursor() stored only (row, col, attrs), so a TUI that flipped origin mode or auto-wrap, did CSI s / ESC 7, ran code that re-toggled the flag, then CSI u / ESC 8'd ended up with the flag in whatever state the inner code left it in — silent coordinate-space corruption. Now: m_savedOriginMode and m_savedAutoWrap members capture the flags on save and restore them on restore, matching the VT420 spec for DECSC / DECRC.

  • DECSTBM home position respects DECOM (terminalgrid.cpp). Was: setScrollRegion() always homed the cursor to absolute (0, 0) after setting the region. Now: when m_originMode is on at the time DECSTBM lands, the cursor moves to (m_scrollTop, 0) — the top-left of the origin-mode coordinate space — as xterm does.

Locked by tests/features/origin_mode_correctness/ — 12 behavioural invariants (CUP/HVP/VPA translation, scroll-region clamping, DECSTBM home, DECSC/DECRC save-restore in both CSI s/u and ESC 7/8 forms, DECAWM round-trip via the wrap-or-not at last-column probe) plus 4 source-grep checks anchoring the fix's shape (std::clamp(row, m_scrollTop, m_scrollBottom) near case 'H', m_savedOriginMode = m_originMode in saveCursor, the mirror line in restoreCursor, and the m_originMode ? m_scrollTop : 0 ternary in setScrollRegion).

0.7.33

Theme: Lifecycle / cleanup. Three ROADMAP items addressing GUI-thread blocking on shutdown, an xdg-desktop-portal session leak, and two latent issues (OOM surface + symlink escape) in the Lua plugin loader.

Changed

  • PTY destructor escalation runs off the main thread (Pty::~Pty). Was: SIGTERM-then-busy-wait-then-SIGKILL ran on the GUI thread; N split panes closing together blocked the window close N × 500 ms. KWin would throw a "window not responding" hint at four splits. Now: the destructor still does the cheap pre-escalation reap (SIGHUP + close master fd + waitpid(WNOHANG)) inline — most shells exit on SIGHUP in microseconds. If the cheap reap doesn't take, the SIGTERM/SIGKILL escalation moves to a detached std::thread capturing the pid by value (no this reference can outlive the destructor). The thread creation is wrapped in a try { ... }.detach() } catch (const std::system_error &) that falls back to the synchronous escalation when thread creation fails (rare — ulimit -u pressure at exit). Locked by tests/features/pty_dtor_off_main_thread/ — 11 invariants on <thread> include, lambda capture list (rejects [this] and [&]), .detach() call, fallback retention, and the pre-escalation cheap reap remaining inline.

  • GlobalShortcutsPortal closes its session on destruction (GlobalShortcutsPortal::~GlobalShortcutsPortal). Was: no destructor at all — the session handle returned by CreateSession leaked for the lifetime of the D-Bus client. xdg-desktop-portal accumulated one orphan session per Ants invocation that crashed or was SIGKILLed before the QObject parent-tree cleanup could implicitly close the bus connection; visible via busctl --user introspect org.freedesktop.portal.Desktop .... Now: the destructor issues an asynchronous org.freedesktop.portal.Session.Close call against m_sessionHandle when non-empty (early-returns on empty handle so X11 / GNOME / no-portal paths don't crash Qt's D-Bus marshaller). New kSessionIface constant in the anonymous namespace alongside the existing service/path/interface constants. Locked by tests/features/portal_session_close/ — 8 invariants on header dtor declaration with override, kSessionIface constant, empty-handle early return, and the asyncCall(createMethodCall(..., kSessionIface, "Close")) dispatch.

  • Plugin manifest cap + canonical plugin path (PluginManager::scanAndLoad). Two latent issues addressed together. Manifest cap: f.readAll() was unbounded — a multi-GB manifest.json (corrupt disk, malicious tarball) would allocate that much RAM before QJsonDocument::fromJson got a chance to reject it. Now: f.read(kMaxManifestBytes) with kMaxManifestBytes = 1024 * 1024 (1 MiB ≈ 250 plugins worth of permission/description text — real manifests are <10 KiB, so the cap never bites legitimate content). Files larger than the cap log a warning and skip without parsing. **Canonical plugin path:** the scan now anchors on QFileInfo(m_pluginDir).canonicalFilePath(), passes QDir::NoSymLinks to entryList (cheap first-pass filter), and per-entry verifies the resolved path is anchored inside the canonical root via startsWith(canonicalRoot + "/"). Closes the symlink-escape shape where a hostile plugin tarball containing evil -> /etc/cron.daily could trick the loader into attempting init.lua from outside the user's plugin tree. Locked by tests/features/plugin_manifest_safety/ — 12 invariants on the cap (named constant + value + bounded read

    • no readAll-feeding-fromJson regression), NoSymLinks flag, canonical anchor, per-entry containment check, and the reject-warning message text.

0.7.32

Theme: Dialog UX. Three bundle items from the Settings dialog ROADMAP list (dependency-UI gating, Cancel rollback for Profiles, Restore Defaults per-tab) plus one user feedback item from 2026-04-25 — the Review Changes dialog only surfaced current- branch state and missed work living on other branches.

Added

  • Restore Defaults button per primary Settings tab (SettingsDialog::setupGeneralTab, setupAppearanceTab, setupTerminalTab, setupAiTab). Was: only Keybindings had a defaults-reset button. A user who tweaked the dialog and wanted to start over had to either remember every default or delete ~/.config/ants-terminal/config.json (which lost unrelated settings: highlight rules, profiles, plugin grants). Now: each substantive tab has its own button with stable objectName (restoreDefaultsGeneral, restoreDefaultsAppearance, restoreDefaultsTerminal, restoreDefaultsAi). Reset slots mutate widget state only — applySettings still commits, Cancel still rolls back. Schema defaults match the second argument of each Config::xxx() getter so the dialog and config layer can't drift. Locked by tests/features/settings_restore_defaults/ — 22 invariants on objectNames, reset-value coverage per tab, and the "no m_config-> writes from within reset slots" rule.

  • Review Changes dialog: live updates via QFileSystemWatcher + manual Refresh button (MainWindow::showDiffViewer). User feedback 2026-04-25: dialog should show live or near-live updates. Was: probes ran once on dialog open and never again — the user had to close and re-open to see changes from a commit / fetch / branch operation done in the terminal underneath. Now: a QFileSystemWatcher watches cwd, .git, .git/HEAD, .git/index, .git/refs/heads, .git/refs/remotes, .git/logs/HEAD. fileChanged and directoryChanged signals feed a 300 ms single-shot QTimer (debounce — git pull / git fetch fire fileChanged O(refs) times in milliseconds; the debounce coalesces them into one re-probe). The probe-spawning logic was refactored into a runProbes lambda that constructs a fresh ProbeState per call, so an in-flight probe whose finalize outlives the next refresh can't decrement the new pending counter and render a half-populated mix. Atomic-rename safe: the fs-event handler re-adds paths that exist but are no longer watched (Qt loses the watch on rename(2), which git uses for HEAD/index/logs/HEAD updates). A live-status label (reviewLiveStatus objectName) shows "● refreshing…" / "● live — auto-refresh on git changes" so the user can confirm the watcher is wired. A manual Refresh button (reviewRefreshBtn objectName) bypasses the debounce for cases where state changed outside the watched paths (a build script in another shell, a different terminal). Locked by tests/features/review_changes_branches/ (extended) — now 33 invariants total covering ProbeState fields, runAsync targets, finalizer rendering, copy-handler payload, empty-state guard, runProbes lambda, watcher armament, debounce timing, atomic- rename re-watch, refresh-button bypass, and live-status label states.

  • Review Changes dialog: per-branch summary + cross-branch unpushed commits (MainWindow::showDiffViewer, MainWindow::ProbeState). Was: three async git probes (status, diff HEAD, log @{u}..HEAD) all scoped to the current branch's working tree and HEAD lineage. A user with five feature branches each holding unpushed work saw "no unpushed" if they happened to be on a clean branch. Branches without upstreams or with diverged ahead/behind state were invisible. User feedback 2026-04-25: "the Review Changes dialog doesn't consider changes in other branches of the project." Now: two additional probes drop in alongside the existing three — git for-each-ref refs/heads --format='%(refname:short) \t%(upstream:short)\t%(upstream:track)\t%(subject)' for the per-branch summary (with ahead/behind/gone/no-upstream colour cues), and git log --branches --not --remotes --oneline --decorate for the cross-branch unpushed log (every commit reachable from any local branch but not from any remote- tracking branch). Both are O(refs) and finish in milliseconds. Copy Diff includes both new sections. Locked by tests/features/review_changes_branches/ — 15 invariants on ProbeState fields, runAsync targets, finalizer rendering, copy-handler payload, and empty-state guards.

Changed

  • Tab close button (×) is always visible, not hover-only (MainWindow::applyTheme stylesheet — QTabBar::close-button). User feedback 2026-04-25: "the tabs still don't have a visible marker per tab that shows where to click to close the tab. The mouseover works but we need to also see it when onmouseout." The 0.6.27 fix removed image: none to let Qt fall back to the platform's standard close icon — that worked on Breeze/Adwaita but failed on Fusion / qt6ct / certain Plasma colour schemes where the platform style still rendered the × hover-only. Now: explicit data-URI SVG image: url("data:image/svg+xml;...") rules in both the default and :hover QTabBar::close-button variants. Glyph re-tints with the active theme via textSecondary (default) / textPrimary (hover); hover keeps the ansi-red background-color will-click cue. URL-encoded %23 is spliced into the arg list via QStringLiteral("%23") + theme.<color>.name().mid(1) rather than the format string — prevents Qt's CSS parser from truncating the data URI at the fragment delimiter and also avoids the QString::arg() placeholder-numbering collision. Locked by tests/features/tab_close_button_visible/ — 11 invariants on data-URI presence, two-line × shape, arg-side splice, and image-rule presence in BOTH state variants.

  • Dependency-UI enable gating (SettingsDialog::setupAppearanceTab, setupTerminalTab, setupAiTab). Was: master checkboxes (m_aiEnabled, m_autoColorScheme, m_quakeMode) gated logic but not UI — typing an API key into a feature-disabled AI tab, or selecting "Solarized" as the light-mode theme while auto-switch was off, both produced silent no-ops. Now: QCheckBox::toggled is wired to setEnabled on every dependent sibling, with a one-shot sync call at construction so the initial state matches the loaded config without relying on setChecked() always emitting toggled (it only emits when state actually changes). Disabled controls keep their current values, so toggling the master back on restores the user's prior selection rather than zeroing it out. Locked by tests/features/settings_dependency_gating/ — 16 invariants on the three sync lambdas, dependent setEnabled calls, toggled-connect wiring, and initial-sync call sites.

  • Profiles tab honors Cancel/OK semantics (SettingsDialog::setupProfilesTab, loadSettings, applySettings, m_pendingProfiles, m_pendingActiveProfile). Was: profile Save/Delete/Load buttons mutated m_config immediately via setProfiles() / setActiveProfile(). Cancel could not roll those edits back — they had already been persisted to config.json before the user's intent was known. Every other Settings tab follows the standard "stage in widgets, commit on applySettings, discard on reject" pattern; Profiles broke that contract. Now: the three buttons mutate a pending-state pair (m_pendingProfiles + m_pendingActiveProfile), loadSettings re-initializes the pair from m_config, and applySettings is the single commit point that calls setProfiles / setActiveProfile. Cancel skips applySettings, so dialog close leaves m_config unchanged. Locked by tests/features/settings_profile_cancel_rollback/ — 11 invariants including a global "exactly one m_config->setProfiles call site" check that catches a regression where a button callback starts writing to m_config directly again.

0.7.31

Theme: Persistence integrity (cross-file). Four items from the post-0.7.30 grouping plan, addressing the silent-data-loss / permission-drift / concurrent-writer surfaces that span Config, ClaudeAllowlist, SessionManager, and SettingsDialog. Splits the growing secureio.h into secureio.h (perms) + configbackup.h (rotation + cooperative write lock) before a third helper landed.

Added

  • ConfigWriteLock cooperative write lock for shared config files (src/configbackup.h). Was: two simultaneously-running Ants instances saving the same ~/.config/ants-terminal/config.json raced over <path>.tmp + rename(2) — both writers truncated the shared tmp, partial bytes interleaved on the same inode, and last-rename-wins silently dropped one process's keystrokes / settings / profile / AI key. Same shape for ~/.claude/settings.json where Allowlist + Install-hooks + git-context-installer all read-modify-write with no serialization. Now: an RAII guard wraps POSIX flock(2) on a sibling <path>.lock file with a 5-second poll deadline (100 × 50 ms), advisory so cooperating callers serialize while non-cooperating editors (vim, jq) bypass by design. Config::save, ClaudeAllowlistDialog::saveSettings, SettingsDialog::installClaudeHooks, and SettingsDialog::installClaudeGitContextHook now construct ConfigWriteLock writeLock(path) with an acquired() guard before the write block; failure to acquire logs and returns rather than risking the data race. Locked by tests/features/concurrent_writer_lock/ (5 runtime invariants via fork(2) so flock semantics are honestly tested across processes, plus 7 source-grep invariants on each save site). Pre-fix source fails 7; post-fix all 12 pass.

  • Belt-and-suspenders post-rename setOwnerOnlyPerms at every persistence site (Config::save, SessionManager::saveSession, SessionManager::saveTabOrder, SettingsDialog::installClaudeHooks, SettingsDialog::installClaudeGitContextHook). Was: each site set 0600 on the temp fd before write but relied on rename(2) preserving perms across the swap. ext4/xfs/btrfs honor that, but FAT/exFAT on removable media (no POSIX bits at all), some SMB/NFS servers (server-side rename applies server umask), and Qt's copy+unlink fallback (cross-device rename, exotic mounts — copy creates the destination with the process umask) do not. Files containing ai_api_key (config.json), Claude Code bearer tokens (settings.json), or paste-buffer scrollback content (session_*.dat) could land 0644 on those filesystems and leak to every UID on the host. Now: each site re-chmods the final inode after rename/commit() returns success — idempotent on POSIX filesystems, essential elsewhere. Locked by tests/features/persistence_post_rename_chmod/ (10 source-grep invariants spanning config.cpp / sessionmanager.cpp / settingsdialog.cpp). Pre-fix source fails 6; post-fix all 10 pass.

  • secureio.h / configbackup.h split. secureio.h is now perms-only — setOwnerOnlyPerms(QFileDevice&) and setOwnerOnlyPerms(const QString&), the original 0600-bitmask helpers that ~12 callers reach for. configbackup.h is the new home for rotateCorruptFileAside (silent-data-loss recovery, added 0.7.12) and the new ConfigWriteLock (concurrent-writer guard, added this release). The split preempts a third-helper cliff: the file was straddling perms + recovery + lock concerns and a fourth would have made the include cost-of-business across the codebase. Each downstream caller now picks the include it actually needs (config.cpp, claudeallowlist.cpp, settingsdialog.cpp pull both; sites that only set perms keep their secureio.h include). Locked by tests/features/secureio_configbackup_split/ (13 invariants: file-content boundaries, non-copyable lock, every caller's include set). Pre-fix source fails 4; post-fix all 13 pass.

Changed

  • Behavioural test coverage extended to the parse-failure mirror sites in ClaudeAllowlist + SettingsDialog. The 0.7.12 silent-data-loss-on-parse-failure refuse pattern shipped in three sites — Config::load, ClaudeAllowlistDialog::saveSettings, and SettingsDialog::install{ClaudeHooks,ClaudeGitContextHook} — but only Config had a regression test (tests/features/config_parse_failure_guard/). The other two were locked only by adjacent grep-style asserts inside other feature tests. Now they have their own dedicated test: tests/features/settings_parse_failure_mirror/ (8 invariants: rotation call site, return-false-after-rotation gating, open-failure branch distinct from parse-failure branch, comment anchors explaining the clobber-risk reasoning). Closes the ROADMAP "Allowlist + settings-dialog feature-test analogs" item. Pre-fix source already passes — these tests lock previously shipped behaviour against future regressions, not catch a new bug.

0.7.30

Theme: Session-file integrity. Three ROADMAP § 0.7.12 Tier 2 items shipped together from the post-0.7.27 grouping plan, all in sessionmanager.cpp: a SHA-256 envelope around the qCompress payload, a pre-flight on qCompress's 4-byte uncompressed-length prefix, and per-cell QDataStream::status() checks inside the decode loops.

Added

  • V4 SHA-256 envelope around session-file payload (SessionManager::serialize, SessionManager::restore, SessionManager::ENVELOPE_MAGIC, SessionManager::ENVELOPE_VERSION). Was: session files at $XDG_DATA_HOME/ants-terminal/sessions/session_<tabId>.dat were raw qCompress output with no payload integrity. Anyone with write access to that directory (the user's own UID, a compromised local process, a runaway pip install post-exec, an npm dependency) could plant a crafted session that fed arbitrary codepoints, fg/bg colors, and attribute flags into the grid on next restore — a render surface, not a sandbox. Now: serialize wraps the qCompress output in a V4 envelope [SHEC magic (0x53484543)][envelope version=1][SHA-256 of payload (32 bytes)][payload length (uint32)][compressed payload]; restore peeks the magic, verifies the hash, and refuses to restore on version mismatch, length disagreement, or hash mismatch. Inner QDataStream format unchanged (still V3) — the integrity layer is framing-only. Legacy V1-V3 files (no envelope) continue to load via the magic-peek fall-through; their next save writes them out as V4 organically. Regression test tests/features/session_sha256_checksum locks four invariants — serialize emits the envelope (INV-1), restore peeks the magic and verifies the hash with a return-false on mismatch (INV-2), ENVELOPE_MAGIC/ENVELOPE_VERSION declared on SessionManager with the agreed-upon magic value (INV-3), envelope version remains 1 at this milestone (INV-4). Pre-fix source fails eight invariant assertions; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

  • qUncompress length-prefix pre-flight (SessionManager::restore, SessionManager::MAX_UNCOMPRESSED). Was: a crafted file claiming 500 MB uncompressed in qCompress's 4-byte big-endian prefix triggered a 500 MB allocation that the post-hoc raw.size() > 500MB cap could only catch after the damage was done — and the on-disk payload could be tiny, so the allocation pressure showed up with no concomitant disk-space anomaly. Now: restore reads the first 4 bytes of the compressed payload, reconstructs the big-endian uint32, and rejects any claim above MAX_UNCOMPRESSED (500 MB) BEFORE qUncompress runs — constant-time, no allocator pressure. Short-payload guard (compressed.size() < 4) keeps the same path safe against truncated inputs that can't carry a length prefix at all. The post-decompression cap remains as a defense-in-depth backstop against payloads that under-claim and over-deliver. Regression test tests/features/session_qcompress_length_guard locks four invariants — pre-flight reconstruction precedes qUncompress (INV-1, INV-3), MAX_UNCOMPRESSED is the named constant (INV-2), short-payload guard exists (INV-4). Pre-fix source fails four invariant assertions; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

Changed

  • QDataStream::status() checks inside cell-decode loops (SessionManager::restore readCell / readCombining). Was: the readCell lambda was void; a stream truncated mid-cell still flowed default-constructed QRgb and uint8_t values through QColor::fromRgba etc. into the cell, silently writing uninitialized fg/bg/flags into the grid. The surrounding loops didn't check per-iteration status either, so the grid kept accepting cells from a stream already at ReadPastEnd. Now: readCell returns bool and short-circuits on in.status() != QDataStream::Ok; every call site (scrollback cells, screen cells in range, screen cells skipped on width shrink, screen cells skipped on height shrink) is guarded by if (!readCell(...)) return false. The combining-character helper checks status after each codepoint read, so a stream truncated mid-codepoint can't push default-constructed 0 into the combining map either. Pre- fix, a partial save (kernel crash mid-fsync, disk-full mid-write, hostile sender truncating the envelope payload) could materialize as garbage cells in the next restore — not a crash, but a corrupted scrollback. Regression test tests/features/session_cell_loop_stream_status locks three invariants — readCell returns bool with status check (INV-1), every call site uses if (!readCell(...)) and at least four sites exist (INV-2), readCombining inner codepoint loop checks status (INV-3). Pre-fix source fails three invariant assertions; post-fix all three pass. ROADMAP § 0.7.12 Tier 2 entry retired.

0.7.29

Theme: Audit pipeline II — output quality. Three ROADMAP § 0.7.12 items shipped together from the post-0.7.27 grouping plan, all in auditdialog.cpp: SARIF v2.1.0 result.suppressions[] array, a regex-DoS watchdog on user-supplied patterns, and a widened 96-bit dedup key.

Added

  • SARIF v2.1.0 result.suppressions[] array (AuditDialog::exportSarif, AuditDialog::loadSuppressions, AuditDialog::saveSuppression, m_suppressionReasons map, Finding::suppressed). Was: SARIF export silently dropped suppressed findings (those whose dedup key matched ~/.audit_suppress), producing a falsely-clean report that defeated the suppression-trend telemetry already computed for the in-app dashboard. Now: a parallel QHash<QString, QString> m_suppressionReasons map populated alongside m_suppressedKeys carries the user's free-text reason from the JSONL file into memory; the parse pipeline marks suppressed findings with Finding::suppressed = true instead of dropping them; render paths (UI, HTML, plain-text) continue to filter via isSuppressed; the SARIF export iterates ALL findings and attaches a suppressions[] block (kind: external, state: accepted, justification: the user's reason) per SARIF v2.1.0 §3.34. GitHub Code Scanning / SonarQube / VSCode SARIF Viewer now see the full audit picture and can compute fires-vs-suppressions ratios across export boundaries. Regression test tests/features/audit_sarif_suppressions locks five invariants — map declaration (INV-1), loadSuppressions populates + clears (INV-2), saveSuppression mirrors (INV-3), exportSarif emits the suppressions JSON property with external/accepted fields (INV-4), exportSarif no longer drops on isSuppressed (INV-5). Pre-fix source fails seven invariant assertions; post-fix all five pass. ROADMAP § 0.7.12 Tier 2 entry retired.

  • Regex-DoS watchdog on user-supplied audit patterns (AuditDialog::isCatastrophicRegex, AuditDialog::hardenUserRegex, applyFilter, loadAllowlist). Was: user patterns from audit_rules.json (OutputFilter::dropIfMatches) and .audit_allowlist.json (AllowlistEntry::lineRegex) flowed straight into QRegularExpression with no shape check and no match-time bound. A pathological pattern committed in either file could pin the GUI thread for seconds with classic catastrophic backtracking on adversarial scanner output — (.+)+, (\w*)*, (.*)+ against long lines. Now: a static isCatastrophicRegex heuristic rejects nested-quantifier shapes (a quantified group whose body itself contains a quantifier) at compile time with a qWarning naming the offending pattern; the rule continues to run without the filter rather than refusing to load. Patterns that pass the shape check are wrapped in PCRE2's (*LIMIT_MATCH=100000) inline option via hardenUserRegex so even catastrophic shapes that slip past the heuristic have a bounded match-step budget — PCRE2 returns "no match" on overrun (fail-safe). 100 k steps handles every sane pattern (typical match completes in < 1 k) and aborts adversarial patterns within milliseconds. Regression test tests/features/audit_regex_dos_watchdog locks four invariants — the helper exists and is invoked at both user-pattern entry points (INV-1), it recognizes nested-quantifier shapes (INV-2), the LIMIT_MATCH=N cap is in the [1k, 1M] sane range (INV-3), loadAllowlist emits a qWarning on rejection (INV-4). Pre-fix source fails four invariants; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

Changed

  • Widen computeDedup from 64 to 96 bits (computeDedup, AuditDialog::isSuppressed, Finding::dedupKey). Was: SHA-256 truncated to 16 hex chars (64 bits) — same key serves as the SARIF partialFingerprint, the suppression-JSONL key, the rule-quality bucket, and the in-app "Suppress" anchor URL. The 64-bit collision threshold (~2³² entries at 50 % collision probability) was comfortable but tight given the multi-role usage; a single false-collision cost a wrong-finding suppression. Now: .left(24) (96 bits) raises the birthday threshold to ~2⁴⁸ for 8 extra bytes per stored key — well past any plausible project's lifetime collection. A new bool AuditDialog::isSuppressed(const QString &dedupKey) const helper encapsulates a backward-compat lookup: match either the full 24-char key OR the leading 16-char prefix, so existing pre-0.7.29 user ~/.audit_suppress entries continue to suppress new 24-char findings without forcing a migration. Six render-pipeline call sites that previously read m_suppressedKeys.contains(f.dedupKey) now route through the helper. Regression test tests/features/audit_dedup_96bit locks four invariants — width ≥ 24 hex chars (INV-1), isSuppressed exists with .left(16) legacy path (INV-2), zero raw m_suppressedKeys.contains(f.dedupKey) sites remain (INV-3), saveSuppression mirrors into m_suppressionReasons (INV-4). Pre-fix source fails five invariant assertions; post-fix all four pass.

0.7.28

Theme: Audit pipeline I — process-side robustness. Three ROADMAP § 0.7.12 Tier 2 items shipped together as bundle 0.7.28 from the post-0.7.27 grouping plan: per-tool timeout overrides, incremental QProcess output drain with a 64 MiB cap, and a distinct tool-crash warning that no longer hides as "0 findings."

Changed

  • AuditCheck::timeoutMs field — per-check QProcess timeout (AuditCheck, AuditDialog::runNextCheck, ctor lambda). Was: m_timeout->start(30000) hard-coded inside runNextCheck, with the timeout-handler lambda printing a literal "Timed out (30s)" warning. Slow tools (cppcheck on a 500k-line tree, semgrep with rule-pack compile, osv-scanner rate-limited by OSV.dev, trufflehog over the full git history, clang-tidy / clazy on Qt-heavy code) routinely exceeded the 30 s cap and got demoted to tool-health warnings instead of producing findings. Now: a new int timeoutMs = 30000; trailing field on the AuditCheck aggregate (default preserves the pre-fix global so positional call sites stay correct), m_timeout->start(check.timeoutMs) at the use site, and the timeout-warning string formatted from the actual cap. populateChecks ends with a calibration loop that bumps known-slow tool IDs to 60 s (cppcheck, cppcheck_unused, clang_tidy, clazy), 90 s (semgrep), or 120 s (osv_scanner, trufflehog). Regression test tests/features/audit_per_tool_timeout locks four invariants — the field declaration (INV-1), the per-check use site with no hard-coded 30000 (INV-2), the parameterised warning message (INV-3), at least one calibration override above 30 s (INV-4). Pre-fix source fails all four; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

  • Incremental QProcess output drain with 64 MiB cap (AuditDialog::onCheckOutputReady, AuditDialog::onCheckErrorReady, AuditDialog::onCheckFinished). Was: m_process->readAllStandardOutput() called exactly once inside onCheckFinished. Until that moment, QProcess accumulated internal buffers without bound — a runaway semgrep against generated code or a buggy plugin emitting a tight printf loop could buffer hundreds of megabytes before the timeout fired, with the audit dialog showing a frozen progress bar the whole time. Now: the constructor wires readyReadStandardOutput and readyReadStandardError to new onCheckOutputReady / onCheckErrorReady slots that append incrementally to m_currentOutput / m_currentError; if the combined size exceeds MAX_TOOL_OUTPUT_BYTES = 64 * 1024 * 1024, the process is killed and m_outputOverflowed is flagged. On finished() the runner drains any tail data (Qt may buffer between the last readyRead and finished()), reads from the member buffers instead of the live process, and surfaces a distinct "Output exceeded N MiB cap" warning when overflow occurred. Buffers reset before each check so output never concatenates across checks. A small connectProcessSignals() helper centralises the three connections so the timeout-kill / cancel-kill / reconnect cycles never lose a drain slot. Regression test tests/features/audit_incremental_output_drain locks six invariants — both readyRead* connections (INV-1), buffer members declared (INV-2), MAX_TOOL_OUTPUT_BYTES cap in the 4 MiB ≤ cap < 1 GiB defensible range (INV-3), per-check reset (INV-4), onCheckFinished reads buffers not the live process (INV-5), overflow path kills the process (INV-6). Pre-fix source fails on the connect-grep alone; post-fix all six pass. ROADMAP § 0.7.12 Tier 2 entry retired.

Fixed

  • Tool-crash distinct from "no findings" (AuditDialog::onCheckFinished). Was: signature void AuditDialog::onCheckFinished(int /*exitCode*/, QProcess::ExitStatus /*status*/) — both parameters discarded via comments. A tool that segfaulted with empty stdout was indistinguishable from a clean run with zero findings, silently hiding both a real bug in the tool AND any findings the tool would have reported. Same went for non-zero-exit-with-stderr-only patterns (clang-tidy missing compile_commands.json, semgrep failing to parse a rule). Now: parameters named (exitCode, exitStatus); function branches on QProcess::CrashExit to emit a "Tool crashed (signal exit)" warning; on exitCode != 0 && stdout empty && stderr non-empty to emit a "Tool exited N with no findings on stdout" warning. Both warnings demoted to Severity::Info so they don't sort to the top of the report next to real findings. The four exit modes (timeout, overflow, crash, non-zero-with-stderr-only) all funnel through a single small file-scope makeToolHealthWarning() helper that centralises the row shape (Info severity, warning flag, distinct message prefix). Regression test tests/features/audit_tool_crash_distinct locks four invariants — named parameters (INV-1), CrashExit branch (INV-2), warning emission with crash/exit-message (INV-3), Severity::Info demotion (INV-4). Pre-fix source fails all four; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

0.7.27

Theme: PTY-handler hardening sweep. Two ROADMAP § 0.7.12 Tier 2 items shipped together — child-side FD closure no longer relies on a hard-coded fd<1024 cap, and master-side writes no longer drop data on EAGAIN.

Security

  • PTY child uses close_range(2) with RLIMIT_NOFILE-bounded fallback (Pty::start). Was: for (int fd = 3; fd < 1024; ++fd) ::close(fd) — silently leaked any inherited FD with index ≥ 1024 into the user's shell on systemd-service / container / hardened-server profiles where rlim_cur sits above the hard-coded ceiling. Qt's display socket, D-Bus session connection, plugin HTTP sockets, Lua VM eventfds, and the remote-control IPC socket are all valid leak candidates; a leaked AI HTTP socket is a credentials-exfiltration vector, a leaked D-Bus socket lets the shell impersonate the user's desktop session, a leaked remote-control socket is a UID-scope RCE-by-proxy vector. Now: post-fork child branch issues ::syscall(SYS_close_range, 3, ~0U, 0) first — single signal-safe syscall on Linux 5.9+, atomic over the whole range, ignores the soft cap. If the syscall returns non-zero (kernel < 5.9, missing build-time SYS_close_range), the fallback path consults getrlimit(RLIMIT_NOFILE) and iterates up to the runtime soft cap, capped at 65536 to bound the worst-case syscall storm on profiles where rlim_cur is in the hundreds of thousands. Two new headers (<sys/resource.h>, <sys/syscall.h>) added to ptyhandler.cpp. Regression test tests/features/pty_closefrom locks five invariants — SYS_close_range referenced inside the post-fork child branch (INV-1), the hard-coded fd<1024 loop is gone (INV-2), fallback consults RLIMIT_NOFILE (INV-3), required headers included (INV-4), fallback bound is capped to 65536 to avoid the unbounded-loop pathology (INV-5). Pre-fix source fails 4 of 5; post-fix all 5 pass. CWE-403 reference; ROADMAP § 0.7.12 Tier 2 entry retired.

Fixed

  • Pty::write queues on EAGAIN instead of dropping bytes (Pty::write, new Pty::onWriteReady). Was: the master FD is non-blocking, so a slow consumer on the slave side could fill the kernel PTY buffer; on EAGAIN the write loop's else-clause broke out with the comment // EAGAIN or fatal error, silently dropping the unwritten remainder. Behaviourally invisible during normal interactive use (the kernel buffer drains within microseconds) but provoked by realistic bursts — large pastes, AI-dialog command insertions, plugin-driven keystroke floods, or any write into a slave whose reader is suspended. Now: a new m_pendingWrite byte buffer and a QSocketNotifier( QSocketNotifier::Write) on m_masterFd (initially disabled because PTY masters are writable nearly continuously and a hot notifier would burn CPU). On EAGAIN the unwritten remainder is moved to the queue and the notifier is enabled; when the kernel signals writability, onWriteReady drains the queue and disables the notifier on completion. Fresh write() calls arriving while the queue is non-empty append rather than bypass, preserving FIFO ordering so newer keystrokes never race ahead of older ones. Queue capped at 4 MiB (MAX_PENDING_WRITE_BYTES) — large enough for realistic bursts, small enough that a permanently-stuck slave cannot OOM the GUI process. Regression test tests/features/pty_write_eagain_queue locks seven invariants — write-side notifier creation (INV-1), queue member declared (INV-2), notifier pointer member declared (INV-3), onWriteReady slot declared and connected (INV-4), EAGAIN handled distinctly inside Pty::write (INV-5), 4 MiB cap present (INV-6), direct-write path checks queue first for FIFO ordering (INV-7). Pre-fix source fails 5 of 7; post-fix all 7 pass. ROADMAP § 0.7.12 Tier 2 entry retired.

0.7.26

Theme: menubar opacity — the actual root-cause fix. 0.7.25's palette + widget-local-QSS belt-and-suspenders did not in fact suspend any belt: the user reported the desktop wallpaper still showing through the menubar strip on KWin + Breeze + Qt 6.

Fixed

  • Menubar background now actually paints opaquely under WA_TranslucentBackground (OpaqueMenuBar, MainWindow::applyTheme). User report 2026-04-25: "I can clearly see my desktop background behind it." Root cause: under WA_TranslucentBackground parent + WA_OpaquePaintEvent on the menubar, none of the conventional opaque-paint paths runs reliably on every WM/style stack. Specifically: autoFillBackground is suppressed by WA_OpaquePaintEvent (the contract is "the widget paints all pixels"), QPalette::Window is only consulted by autoFillBackground and inherits the suppression, and QSS QMenuBar { background-color: … } — which is supposed to draw via QStyleSheetStyle::drawControl(CE_MenuBarEmptyArea) — is silently skipped on KWin + Breeze + Qt 6 when WA_OpaquePaintEvent is set, because the QSS engine assumes the widget owns those pixels. Net effect: every safeguard 0.7.25 added was correctly installed and not painting anything; the menubar surface stayed cleared-to-transparent and the compositor showed the wallpaper through. New file src/opaquemenubar.h defines OpaqueMenuBar, a QMenuBar subclass whose paintEvent runs QPainter::fillRect(rect(), m_bg) with CompositionMode_Source before delegating to QMenuBar::paintEvent. That is the only path that actually keeps the WA_OpaquePaintEvent contract honest under translucent parents. m_menuBar is now an OpaqueMenuBar and applyTheme sets the fill colour via setBackgroundFill(theme.bgSecondary). The 0.7.25 palette + widget-local QSS calls are kept, no longer load- bearing for the strip's opacity but useful for child-widget theme propagation and for scoping the ::item :hover/:selected/:pressed rules on the menubar itself rather than relying on the top-level cascade. Regression test tests/features/menubar_hover_stylesheet extended with INV-8 — three new assertions that pin the OpaqueMenuBar construction site, the setBackgroundFill call in applyTheme, and the presence of paintEvent + fillRect inside src/opaquemenubar.h. The full spec is rewritten with the per-iteration history (0.6.42 → 0.7.26) and an explicit "manual verification" recipe (bright wallpaper + dark bgSecondary + ~0.85 opacity on KWin) so any future drift is reproducible by hand.

0.7.25

Theme: memory-DoS hardening on OSC 8 hyperlinks and Kitty APC graphics chunks, plus a menubar-opacity fix reported mid-session.

Security

  • OSC 8 URI cap + Kitty APC chunk-buffer cap (TerminalGrid). Two attacker-controlled accumulators used to be bounded only by the VT parser's per-envelope 10 MB ceiling, with no downstream ceiling of their own. A hostile program could emit one \x1b]8;;<10MB-URI>\x07 sequence per scrollback line and wedge tens of GB of URI bytes into per-row HyperlinkSpan copies + scrollback; or keep sending \x1b_G...,m=1,...\x07 frames without ever closing with m=0, growing m_kittyChunkBuffer without bound. src/terminalgrid.h now declares MAX_OSC8_URI_BYTES = 2048 (real URLs are < 2 KiB; abuse is not) and MAX_KITTY_CHUNK_BYTES = 32 MiB (larger than any realistic chunked image upload — Kitty itself transmits in ~4 KiB frames). The OSC 8 open branch rejects oversized URIs down the same drop path as the existing invalid-scheme rejection — following text prints unlinked. The APC chunk path clears and shrink_to_fits the staging buffer on cap breach, so a subsequent m=0 sees an empty buffer rather than attacker-prepended garbage. Regression test tests/features/osc8_apc_memory_caps locks five invariants — OSC 8 happy path, oversized-drop, at-cap-accepted, APC single frame, APC overflow-dropped. Pre-fix source fails INV-OSC8-B and INV-APC-B; post-fix all five pass.

Fixed

  • Menubar renders opaque under WA_TranslucentBackground (MainWindow::applyTheme). User report 2026-04-24: "the background of the menubar is transparent." Root cause: the QSS cascade from the QMainWindow-level stylesheet can race with the compositor damage rect under WA_TranslucentBackground — on frames where the compositor invalidates the chrome region before QSS polish has produced a paint, the menubar shows through to whatever is behind the window. applyTheme now installs a belt-and-suspenders opaque fill on m_menuBar: QPalette::Window = theme.bgSecondary (so autoFillBackground lays down an opaque fill before QSS paints) plus a widget-local setStyleSheet(QMenuBar ...) block (so the menubar's own style engine polishes the QSS independently of the top-level cascade). Mirrors the same pattern already used on the custom title bar and status bar. tests/features/menubar_hover_stylesheet extended with INV-7 locking both the palette and widget-local-QSS calls.

0.7.24

Theme: wide-char (CJK / wide emoji) overwrite no longer strands its mate. 0.7.12 Tier 2 hardening item.

Fixed

  • Wide-char overwrite now zeroes the stranded mate (TerminalGrid). A wide character occupies two adjacent cells — a first half (isWideChar=true, codepoint=CP) and a continuation (isWideCont=true, codepoint=0). Before this release, three write paths in src/terminalgrid.cpp left half-pairs in an inconsistent state when a new write landed on only one half: (a) handlePrint narrow write over a continuation left the mate at col-1 still claiming isWideChar=true with no neighbor (rendered with a gap); (b) handlePrint narrow write over a first half left the old continuation at col+1 claiming isWideCont=true with no mate (blocked selection / copy); (c) handlePrint wide write shifted by one cell from an existing wide pair left the old continuation at col+2 orphaned; (d) the SIMD-coalesced fast path handleAsciiPrintRun had the same left/right edge issues on its write span. Consolidated the policy into a single breakWidePairsAround(row, startCol, endCol) helper that runs before every write site: it blanks the stranded first half on the left edge and clears the stranded continuation on the right edge. Regression test tests/features/wide_char_overwrite_mate locks five subcases — pre-fix source fails four of them (INV-1, INV-2, INV-3, INV-4), post-fix all five pass.

0.7.23

Theme: xterm-compatible Background Color Erase (BCE) across every scroll and erase path. 0.7.12 Tier 2 hardening item.

Fixed

  • BCE on scroll and insert/delete paths (TerminalGrid). Apps that paint with a non-default SGR background (\e[44m etc.) and then scroll, insert lines, delete lines, delete chars, or insert blanks expect the newly-exposed cells to inherit the current bg — this is the xterm convention vim/less/tmux/mc/htop rely on for full-screen painted backgrounds. Previously takeBlankedCellsRow() hardcoded m_defaultBg, so CSI L / CSI M / CSI S / CSI T / LF-past-scroll-bottom all produced default-bg gaps through any painted region. deleteChars / insertBlanks used raw m_currentAttrs.bg without the .isValid() fallback that clearRow already had. Consolidated the policy into one TerminalGrid::eraseBg() helper; every erase/scroll callsite now reads from it. Regression test: tests/features/bce_scroll_erase/spec.md — 10 behavioral subcases (IL, DL, SU, SD, DCH, ICH, ED2, EL0, LF-scroll, SGR-reset-before-erase). Pre-fix source fails 5 subcases (IL/DL/SU/SD/LF-scroll); post-fix all 10 pass.

0.7.22

Theme: user-visible About menu (user ask 2026-04-24 — "How do I see what version of Ants Terminal I am running? Can you please add a GUI version?").

Added

  • Help → About Ants Terminal… menu. New rightmost menu on the menubar (matching Linux desktop HIG: Help is always last). Two actions:

    • About Ants Terminal… — shows a rich-text dialog with the running ANTS_VERSION (read directly from CMake's project-wide single source of truth, no hardcoded literal), the Qt runtime version (via qVersion()), the Lua engine version (when compiled with ANTS_LUA_PLUGINS), a one-line summary, and a clickable GitHub homepage link (TextBrowserInteraction enabled so URLs in the dialog open in the user's browser).
    • About Qt… — the stock QMessageBox::aboutQt dialog. Inherits future Qt-version bumps automatically.

    Pre-fix, ants-terminal --version on the command line was the only path to read the running version. Regression test: tests/features/help_about_menu/spec.md — 6 invariants (Help-menu-last, About-action present, ANTS_VERSION referenced not hardcoded, Qt::RichText + Qt::TextBrowserInteraction set, About-Qt action routed to QMessageBox::aboutQt, no "0.7." literal inside the handler body).

0.7.21

Theme: Lua sandbox hardening trio from the 0.7.12 /indie-review. Three small defense-in-depth fixes in LuaEngine, behavioral + source-grep regression test locked to all three.

Security

  • string.dump removed from the plugin sandbox. string.dump(f) returns the bytecode serialization of a Lua function. Lua 5.4 has no bytecode verifier — the loader parses any byte sequence beginning with \x1b as a binary chunk, and crafted bytecode can corrupt Lua's internal state and escape the sandbox. load/loadstring/loadfile are already nilled at init, so there is no supported round-trip from string.dump back to executing bytecode, but a future C API added to ants.* that wraps luaL_loadbuffer with plugin- supplied data would reopen the attack surface. Closing the primitive at the sandbox layer — lua_setfield(m_state, -2, "dump") scoped to the string table, not a blanket lua_setglobal — is cheaper than auditing every future C API for the same rule.
  • LuaEngine::loadScript forces "t" (text-only) load mode. The pre-fix path luaL_dofile → luaL_loadfile → luaL_loadfilex(L, path, nullptr) accepted both text and binary chunks at the loader level. The 0x1b-first-byte peek in loadScript was the first gate; the loader call is now the second gate. A future refactor that drops the peek still gets rejection at the Lua level.
  • Instruction-count hook cleared before lua_close in shutdown. lua_close runs every pending __gc metamethod in dependency order. Metamethods can execute arbitrary Lua code which the count hook observes. If the hook fires mid- close and walks back into registry data or the dying engine pointer via __ants_engine, we get a UAF window. Clearing the hook first with lua_sethook(m_state, nullptr, 0, 0) removes that window; the C-side cleanup proceeds without any Lua-level observer.

Regression test: tests/features/lua_sandbox_hardening/spec.md — 6 invariants, behavioral (string.dump is nil, valid-text loads, 0x1b-first-byte rejected) plus source-grep on the three fix tokens (luaL_loadfilex(..., "t"), lua_sethook(m_state, nullptr, 0, 0) before lua_close, lua_setfield(m_state, -2, "dump") scoped to the string table). Verified to fail against pre-fix source via git stash — 5 of 9 invariants flip red on the regression, all green after the fix lands.

0.7.20

Theme: Tier 2 hardening sweep — three open 📋 items from the 0.7.12 /indie-review landed as one release. Each has a behavioral regression test, source-grep on the fix's load-bearing tokens, and was verified to fail against pre-fix source via git stash before locking.

Security

  • debug.log lands 0600 regardless of umask. ~/.local/share/ants-terminal/debug.log was being created with the process umask (typically 0644 under 0022). The log can include PTY keystrokes (via the input / pty categories), AI endpoint request+response bodies (network), OSC 133 HMAC digest material (shell), and Claude transcript parse state (claude) — every one of those is material that must not be world-readable. DebugLog::setActive now calls setOwnerOnlyPerms twice after the file opens: once on the QFileDevice to cover the just-opened fd, and once on the path string to narrow any pre-existing 0644 file that append reused from a prior (pre-fix) run. Fix uses the project-standard secureio.h helper, not a raw QFile::setPermissions bitmask. Regression test: tests/features/debuglog_perms/spec.md (4 invariants: fresh-open perms, clear-then-reopen, pre-existing 0644 narrowed to 0600, source uses the helper).
  • Audit path-traversal guard on findings' file field. User- supplied audit rules, audit_rules.json in a cloned project, and external scanner regex outputs can all produce findings whose file field is e.g. ../../etc/passwd. Pre-fix, six call sites across AuditDialog naively concatenated m_projectPath + "/" + f.file and passed the result to readSnippet / lineIsCode / comment scans / AI-triage POST bodies — a textbook CWE-22 + OWASP LLM06 (sensitive-information disclosure via LLM) chain, since the AI-triage surface exfiltrates snippet contents to the configured /v1/chat/completions endpoint. New AuditDialog::resolveProjectPath helper canonicalizes the candidate path (resolves .. and dereferences symlinks in a single QFileInfo::canonicalFilePath call), requires the result to be anchored under the canonical project root (with a trailing-slash sentinel so /proj-foo can't escape from /proj), and returns QString() on any rejection. All six call sites migrated: dropFindingsInCommentsOrStrings, inlineSuppressed, the enrichment pass, single-finding AI-triage snippet fallback, batch AI-triage snippet fallback, and the dropIfContextContains regex-captured-relPath read. Regression test: tests/features/audit_path_traversal/spec.md — 5 invariants behavioral (via a byte-faithful reference reimpl — production helper is private on the heavy QDialog subclass) plus source-grep on the production code to confirm migration + helper structure (canonicalFilePath + anchored startsWith).

Fixed

  • Settings dialog discarded on external config.json reload. MainWindow caches the SettingsDialog across Preferences... opens; the dialog was constructed with &m_config and populated its widgets from the then-current values at construction. When QFileSystemWatcher fired onConfigFileChanged on an external edit, m_config = Config() reloaded from disk but the cached dialog still held pre-reload widget state. Next Preferences... open would show stale values, and clicking OK would replay them over the fresh Config — silently undoing the external edit. onConfigFileChanged now closes the dialog if visible, calls deleteLater(), and nulls the pointer, so the next open rebuilds from the freshly reloaded Config. Regression test: tests/features/settings_dialog_config_reload/spec.md — 4 source-grep invariants (cache nulled, visible-close gate, deleteLater-not-delete, invalidation scoped to onConfigFileChanged).

0.7.19

Theme: CI un-break + tab-rename persistence. CI had been red since 0.7.17 on the AppStream metainfo validation step — the 0.7.17 release description embedded git clone https://ghp_…@github.com/… as an example of what the new secret-redactor scrubs, and appstreamcli correctly rejects plaintext URLs in <description> bodies. Rewrote the example to use an inline non-URL form; local appstreamcli validate --explain now exits 0. Separately, user asked whether manual tab renames (right-click → "Rename Tab…") survive Ants restart. They didn't — the pin map lived only in MainWindow memory and SessionManager didn't serialize it. Fixed by bumping the session-file schema to V3 with a trailing pinnedTitle field; V2 files still load with the out-param defaulting to empty.

Added

  • Manual tab renames persist across Ants Terminal sessions. User ask 2026-04-24. The right-click "Rename Tab…" pin (m_tabTitlePins) is now written to and read from each per-tab session file, so a tab renamed to "Deploy" or "Prod DB" or "Claude #3" keeps that label after the app exits and relaunches. SessionManager schema bumped to V3: a trailing QString pinnedTitle field is appended after the V2 cwd. V2 files continue to load via the existing in.atEnd() gate — Ants 0.7.18 and earlier can't read V3 files, but since session files are a per-user cache (not an interchange format) that's by design. MainWindow::saveAllSessions threads m_tabTitlePins.value(w) (keyed by the outer tab widget, which may be a QSplitter for split tabs) into the save; restoreSessions pulls the pin back, populates the in-memory m_tabTitlePins map, and sets the tab label directly (pin takes precedence over the shell-derived window title from the saved grid's windowTitle()). Contract locked by tests/features/tab_rename_persist/spec.md — 20 invariants covering the V3 round-trip (three pin lengths including empty), V2 backward compat (hand-crafted V2 stream → restore leaves pinnedTitle out-param empty), and MainWindow source-grep that both save-side and restore-side wiring remain threaded. Verified to fail against pre-fix source before locking.

Fixed

  • AppStream metainfo validation passes. appstreamcli validate --explain failed on the 0.7.17 / 0.7.18 release descriptions because the 0.7.17 block contained git clone https://ghp_…@github.com/… as a scrubber example, and AppStream's description-has-plaintext-url rule rejects raw URLs in description bodies. CI's "Validate AppStream metainfo" step exited 3 on every push since 0.7.17. Rewrote the example as <code>git clone</code> with an embedded <code>ghp_</code> token in the URL. CI build-test step should be green again on this release.

Changed

  • SessionManager::restore initializes optional out-params before reading. Previously, callers that passed a pre-populated QString *cwd or QString *pinnedTitle would see their sentinel survive an older-format load (V2 files leaving pinnedTitle untouched, V1 files leaving cwd untouched). Fixed by clearing both out-params at function entry regardless of the stream's version. The version-gated read blocks still populate them only when the on-disk format actually has the field; the clear-first guarantees they read as empty rather than as whatever the caller happened to pre-fill. Locked by tab_rename_persist I3.

0.7.18

Theme: post-release documentation + tech-debt sweep. Three parallel audit agents (doc drift, source debt, spec-vs-code drift) inventoried every doc-vs-code mismatch, stale TODO, dead config key, and spec line number across the tree. The shell_command config key turned out to be the only actual functional bug: Settings → General's Shell picker persisted the user's choice to config.json but no code path ever read it — every tab opened $SHELL regardless of what the user picked. Two other deferred-but-flagged items from the 0.7.12 ROADMAP also landed: Qt::WA_OpaquePaintEvent on the menubar (closes the mouse-move-over-menubar dropdown flicker on KWin that the 0.7.4 QOpenGLWidget→QWidget refactor didn't fully eliminate), and background_alpha config key removed as redundant after user confirmed the opacity knob already does what they want.

Removed

  • **background_alpha config key + Config::backgroundAlpha() + Config::setBackgroundAlpha() + TerminalWidget::setBackgroundAlpha()
    • TerminalWidget::backgroundAlpha() + m_backgroundAlpha member.** The key had no paint-site consumer (the fillRect at terminalwidget.cpp:605 always used m_windowOpacity, which is driven by the opacity config key) and no UI widget. User confirmed (2026-04-24) they use opacity ~0.9-0.95 to make the terminal area translucent while the chrome stays solid — which is exactly what the existing paint path does. Removing the key collapses two overlapping knobs into the one that works. Backwards-compat note: stale "background_alpha" entries in existing config.json files are harmlessly ignored on load (Qt's JSON parser tolerates unknown keys). README config-defaults table and example JSON both pruned; CLAUDE.md "Per-pixel bg alpha is independent of window opacity" bullet rewritten to describe the actual single-knob behaviour. m_windowOpacity member kept (name is historical — it drives per-pixel terminal-area fillRect alpha, not Qt's whole-window setWindowOpacity); header + paint-site comments clarify the misnomer.

Fixed

  • Config::shellCommand() actually launches the user's shell. Settings → General's Shell picker (Default / bash / zsh / fish / Custom…) has persisted the user's choice to shell_command in config.json since the original implementation, and the Settings dialog loads/re-displays it correctly. But TerminalWidget::startShell took only a working directory and invoked VtStream::start with a hardcoded empty shell arg — Pty::start's empty-shell fallback always fired ($SHELL → pw_shell → /bin/bash), silently dropping the user's choice. Fix: startShell gains a shell parameter, the QMetaObject::invokeMethod call forwards it instead of QString(), and every production call site in MainWindow (newTab, newTabForRemote, splitCurrentPane, the restoredTabs loop in session restore) passes m_config.shellCommand(). Contract pinned by tests/features/shell_command_wiring/spec.md — 7 invariants covering the signature, the Q_ARG forward, the four wired call sites, a zero-orphan guard, and a Config round-trip. Verified to fail against pre-fix source (5 invariants fire on missing signature + hardcoded QString() + four orphan call sites) before locking.
  • Dropdown flicker on mouse-move-over-menubar fix (Qt::WA_OpaquePaintEvent on m_menuBar). The menubar's autoFillBackground + stylesheet fully cover every pixel on every paint, so marking it opaque-on-paint stops Qt from invalidating the translucent parent's compositor region under it when it repaints. Without this, each mouse-move over the menubar item owning an open dropdown damages the popup above the menubar on KWin. MainWindow ctor now calls m_menuBar->setAttribute(Qt::WA_OpaquePaintEvent, true); the QSS comment at the :hover rule always promised this was set at the construction site, but the call was missing. Flagged in 0.7.12 Tier 3 hardening sweep as "Missing per tests/features/menubar_hover_stylesheet/spec.md INV-3b". Locked by the reinstated INV-3b assertion in test_menubar_hover_stylesheet.cpp (was previously punted to terminal_partial_update_mode, but that test covers an orthogonal fix — both now assert their own attribute).

Changed

  • README.md config-defaults table corrected. session_persistence default shown as false in both the example JSON block and the key-description table; actual default in config.cpp:311 is true. font_size range shown as 8–32; actual range in config.cpp:170 and settingsdialog.cpp:206 is 4–48. Both corrected — the authoritative code defaults win.
  • PLUGINS.md Lua resource-limit docs. Previously claimed limits are enforced "per plugin per event invocation" and that each event gets a fresh 10 M-instruction budget. The implementation sets lua_sethook(LUA_MASKCOUNT, 10000000) once at engine init; the counter accumulates across all handlers in the same VM until the hook fires, at which point it resets. Doc rewritten to describe the real contract: "≥10 M instructions anywhere in the VM aborts the current pcall; plugin is not unloaded, next event starts from wherever the hook last fired."
  • tests/features/terminal_partial_update_mode/spec.md rewritten to match the live test. The .cpp test was updated in 0.7.4 to enforce the post-refactor invariant (TerminalWidget is a plain QWidget, no QOpenGLWidget:: calls, no makeCurrent()), but the adjacent spec.md still documented the pre-0.7.4 setUpdateBehavior(PartialUpdate) fix that didn't work. Spec now describes the real fix (base-class change), lists the 4 invariants the test actually enforces, and preserves the PartialUpdate detour in a History section for archaeology. Directory name kept for CMake stability — rename would touch add_executable / add_test / label wiring.
  • Spec line-number drift corrected. osc133_hmac_verification (1297 → 1722), session_persistence_default (config.cpp:218 → 310), review_changes_click (mainwindow.cpp:74 → 148, ~411 → ~595), ai_context_redaction (aidialog.cpp:114-136 → 115-137). Line- number drift is inherent to spec.md files; this sweep resets the ground state, no workflow change.

0.7.17

Theme: hot-path parser optimization + stale-TODO sweep. VtParser printable-ASCII runs now coalesce into a single VtAction carrying a pointer+length slice of the feed buffer; TerminalGrid grows a handleAsciiPrintRun fast path that batches per-row cell writes so markScreenDirty, the combining-char erase, and the cell() clamp all fire once per row instead of once per byte. Rebench on 8 MiB corpora: ascii_print 24.4 → 30.7 MB/s (+26%), ansi_sgr 27.5 → 31.4 MB/s (+14%). Two Tier 2 TODOs also closed: SIGPIPE was already shipped in 0.6.28 (bc97485) but never had a regression test — now grep-locked; RIS (ESC c) preserved only 2 of 8 integration callbacks, silently breaking notifications, progress, command-finished, user-vars, and the OSC 133 HMAC forgery alarm after any tput reset — now preserves all 8 plus the HMAC key.

Added

  • Claude Code UserPromptSubmit git-context hook (user ask 2026-04-24). New Settings-dialog button "Install git-context hook" writes ~/.config/ants-terminal/hooks/claude-git-context.sh and merges one entry into ~/.claude/settings.json under hooks.UserPromptSubmit. The script prints a compact <git-context> block — branch, upstream, ahead/behind counts, staged/unstaged/untracked file counts — on every user prompt, so Claude Code sees repo state up front without spending tokens on a Bash(git status) round-trip. Global scope (the user's explicit ask "can this be available to all projects?"): installed in ~/.claude/settings.json so it fires on every Claude Code session regardless of which project. No-op outside a git repo, no-op when git isn't on PATH, no-op when CLAUDE_PROJECT_DIR is unset and $PWD isn't a repo — silent exit-0 so non-repo sessions see zero behaviour change. Independent of the existing status-bar hook installer (different direction of data flow: this one is Claude→git, the status-bar one is Ants→Claude); users can opt into one and not the other. Installer is idempotent and preserves pre-existing user-added UserPromptSubmit entries (ripgrep cheatsheet injectors, TODO listers, etc.) via the same "append-only-if-not-already-present" loop used by the status-bar installer. Parse-error-refuse on a corrupt settings file matches the 0.7.12 /indie-review cross-cutting fix shape. Contract pinned by tests/features/claude_git_context_hook/spec.md + 10-invariant installer source-grep test + 5-scenario behavioral script test.
  • src/secretredact.h — OWASP LLM06 defense layer. Header-only module exposing SecretRedact::scrub(QString) → {text, redactedCount}. Regex-scrubs 14 well-known secret shapes out of any string before it leaves the process: AWS access keys (AKIA/ASIA), GitHub classic / OAuth / app / fine-grained PATs, Anthropic sk-ant-, OpenAI sk-proj- + legacy sk-, Slack xox[abpros]-, Stripe sk_live_ / rk_live_, JWTs, Bearer <token> headers, generic api_key=/token=/password=/secret= assignments, and multi-line PEM -----BEGIN … PRIVATE KEY----- blocks. Replacements are [REDACTED:<kind>] so the LLM still sees the surrounding command/variable shape. Priority-ordered rule set guarantees sk-ant-… is labelled anthropic, not mislabelled as legacy openai; overlap resolution drops lower-priority matches in the same range. Pure function, thread-safe, static-initialised once per process.
  • AiDialog::sendRequest now scrubs both outbound strings. The terminal scrollback context (m_terminalContext) and the user's own question (userMessage) both pass through SecretRedact::scrub before either is interpolated into the system prompt or the user message body. Pre-fix, a single cat .env, aws configure show, or git clone https://ghp_…@… line in recent scrollback exfiltrated the credential on the next AI request. When the combined redactedCount > 0, the dialog appends a System chat-history note — "Note: N secret(s) redacted from outbound request (OWASP LLM06)" — so the user knows the payload differs from what they saw/typed. The chat history's "You:" display intentionally shows pre-redaction text (redaction is a network-boundary concern, not a UX one).
  • Feature test: ai_context_redaction — 16 positive cases (one per secret shape) + 6 negative controls (UUID, commit SHA, plain URLs, prose mentioning "api token" without a value, empty string, ls -la). Asserts no raw secret substring survives, the expected [REDACTED:<kind>] label appears, surrounding text is preserved, cumulative redactedCount is accurate across multiple secrets, precedence orders sk-ant- above legacy sk-, and (source-grep) AiDialog::sendRequest wires SecretRedact::scrub to both inbound strings and gates the user-notice on totalRedacted > 0. Verified to fail against pre-fix source (5 grep invariants) before locking.
  • Feature test: tab_rename_pin — source-grep regression guard that pairs the right-click rename handler with the pin-map consumer side. Asserts the rename lambda calls setTabTitleForRemote(…), never calls m_tabWidget->setTabText directly, does not guard the call behind !newName.isEmpty() (empty-string clear is deliberate UX), and that the consumer- side m_tabTitlePins.contains(...) guards on both the titleChanged handler and updateTabTitles still exist — if either is deleted "to clean up," the fix silently regresses.
  • Feature test: vtparser_print_run_coalesce — locks grid-state equivalence between the bulk-feed (SIMD + coalesce) path and the byte-by-byte scalar path across 8 invariants (ASCII, CSI at every lane boundary, wrap at right edge, cursor mid-row, mixed UTF-8, empty input, combining-mark clearing).
  • Feature test: sigpipe_ignore — source-grep regression test that pins std::signal(SIGPIPE, SIG_IGN) in main.cpp before the QApplication constructor. Installing it after QApplication leaves any write path triggered by Qt resource loading exposed to termination.
  • Feature test: ris_preserves_callbacks — exercises all 8 TerminalGrid integration callbacks plus the m_osc133Key HMAC config before and after an ESC c reset and asserts every one survives. Pre-0.7.17 only responseCallback + bellCallback were preserved; the other 6 silently wiped.

Changed

  • VtAction carries an optional coalesced Print run. Two new fields: const char *printRun + int printRunLen. When set, they point into the caller's feed buffer (valid for the duration of the ActionCallback call only). TerminalWidget's direct-callback path uses this to skip wcwidth, the combining-char check, the wide-char branch, and std::clamp inside cell() on every printable byte.
  • VtStream::onPtyData callback expands runs into per-byte Prints. Its batch outlives the feed buffer, so it MUST materialize the run into per-byte Print actions before queuing for the GUI thread. Lifetime invariant documented in vtparser.h alongside the field.
  • tests/features/vtparser_simd_scan + threaded_parse_equivalence canonicalize runs before comparing. Both tests establish action- stream equivalence across feed strategies; the expand-runs-to-per- byte canonicalization preserves the contract after coalescing.

Security

  • Claude allowlist settings.local.json perms survive rename fallback (belt-and-suspenders). ClaudeAllowlistDialog::saveSettings previously set 0600 only on the QSaveFile temp fd, relying on rename to carry the perms across to the final path. Most local filesystems (ext4/xfs/btrfs) preserve fd perms across rename(2), but FAT/exFAT on removable media don't carry POSIX perm bits at all, some SMB/NFS servers apply umask to the rename destination, and Qt's QSaveFile::commit falls back to copy+unlink on cross-device renames — in all three cases the final file lands at the process umask (typically 0644), leaving it readable to every other UID on the host. Since settings.local.json can hold Claude Code bearer tokens in merged env/model/custom-hook keys, the widened perms are a credential-leak surface. Fix: after file.commit() returns true, call setOwnerOnlyPerms(m_settingsPath) on the final path too. The pre-commit fd-level chmod is retained (covers the open-to-commit window on filesystems that do preserve fd perms — neither call subsumes the other). Flagged by the 2026-04-23 /indie-review sweep. Locked by tests/features/allowlist_perms_postcommit/spec.md — runtime stat check under umask 0022 (the POSIX default) plus source-grep that both chmod calls remain and the post-commit call is sequenced after the if (!file.commit()) return false; gate so a failed commit can't chmod a path that may not exist.
  • AI context + user-message secret redaction (OWASP LLM06). The AI dialog previously shipped recent scrollback verbatim to whichever endpoint the user had configured — OpenAI, Anthropic, an in-house gateway, a self-hosted Ollama, any third party. Anything on the user's screen at dialog-open time exfiltrated: a single cat .env, env | grep, aws configure show, git clone https://ghp_…@github.com/…, or freshly-pasted SSH key landed in the provider's request logs or training-data pipeline. The user's own pasted questions were equally exposed. Fix: both outbound strings now pass through a 14-shape regex scrubber before reaching the JSON body. Complements the 0.7.12 LLM01/LLM02 fix (ai_insert_command_sanitize) — that handled untrusted input from the AI to the PTY; this handles untrusted output from the PTY to the AI. Contract pinned by tests/features/ai_context_redaction/spec.md (9 invariants).

Fixed

  • Right-click "Rename Tab…" pins the label. User report 2026-04-24: "I renamed a few tabs I had open (some with Claude Code running) and then Claude Code just renamed them again. Is this intentional?" It was not. The rename handler at mainwindow.cpp:4284 wrote directly to m_tabWidget->setTabText without populating m_tabTitlePins; the per-shell titleChanged signal handler and the 2 s updateTabTitles tick both consult the pin map to decide whether to relabel, so any tab whose shell writes OSC 0/2 (Claude Code writes one every prompt iteration, ~3–5 s) had its manual name wiped within seconds. Rename now routes through the existing setTabTitleForRemote (the rc_protocol set-title path) so non-empty names pin, and empty names clear the pin and restore the format-driven / shell-driven label — gives the user an in-UI "un-rename" path that didn't exist before. Locked by tests/features/tab_rename_pin (4 invariants).
  • RIS (ESC c) now preserves all 8 integration callbacks + m_osc133Key. Previously notifyCallback, progressCallback, lineCompletionCallback, commandFinishedCallback, userVarCallback, and osc133ForgeryCallback silently became empty functions after any tput reset / reset(1) / stty sane. Desktop notifications, progress bars, command_finished plugin events, user-var theming hooks (jj, starship), and the OSC 133 HMAC forgery alarm all died after a single RIS. Security impact for the forgery alarm: a well-timed tput reset silenced it permanently, enabling OSC 133 forgery to proceed without user notification. m_osc133Key (read from $ANTS_OSC133_KEY at process start) also now survives RIS.

Performance

  • VtParser printable-ASCII run coalescing. The SIMD fast path (scanSafeAsciiRun on SSE2/NEON) now emits one VtAction::Print per run instead of one per byte. For a 10 KB TUI-repaint PTY read, the allocation count drops from ~10 000 to 1 and the dispatch count drops by the same factor. Bench deltas on 8 MiB corpora: ascii_print 327 → 260 ms (−20%), ansi_sgr 291 → 255 ms (−12%). The newline_stream and utf8_cjk corpora are bound by newLine/scroll and UTF-8 multibyte decoding respectively — no safe-ASCII runs to coalesce — so their numbers are flat.
  • TerminalGrid::handleAsciiPrintRun batches per-row writes. Skips the per-byte wcwidth, combining-char branch, wide-char branch, and std::clamp inside cell(). Fires markScreenDirty once per row instead of once per byte.

0.7.16

Theme: per-tab Claude Code activity indicator — flagship response to the 2026-04-23 user request for at-a-glance multi-tab Claude visibility. Users running Claude Code in several tabs now see per-tab state glyphs (idle / thinking / tool-use / Bash / planning / compacting / awaiting-input) so they can tell which tab is blocked on them without cycling through tabs. Permission prompts route by session_id (not active-tab) so a prompt from any tab lights up that specific tab. Supporting refactor: ClaudeIntegration::parseTranscriptForState is now a thin wrapper over a new static parseTranscriptTail helper so the new per-tab tracker can share the exact parsing logic with zero drift.

Added

  • Per-tab Claude Code activity indicator. Each tab whose shell has a Claude Code child process now draws a small state-dependent dot at the leading edge of the tab chrome: muted grey for Idle / Thinking, blue for generic ToolUse, green for Bash (split out because it's the highest-signal, longest-running tool), cyan for Planning, violet for Compacting, and a bright orange dot with a white outline for AwaitingInput (permission prompt pending). Users running Claude across several tabs can see at a glance which one is waiting on them — and permission prompts route by the hook event's session_id (matched against the <session-uuid>.jsonl transcript filename) so a prompt emitted by Claude in tab 3 lights up tab 3's glyph, not the currently-focused tab. Hover tooltip on each tab shows "Claude: thinking…" / "Claude: Bash" / etc. so glyph colours can be disambiguated without switching tabs. Settings dialog → General adds a checkbox ("Show per-tab Claude activity dot …") that flips claude_tab_status_indicator (default on); the change takes effect on the next paint via the live config reload path, no restart needed. New src/claudetabtracker.{h,cpp} keys state by shell PID so tab reorder / close doesn't shift entries, and reuses the existing transcript parser via a new static ClaudeIntegration::parseTranscriptTail helper — no duplicated parsing logic. Contract + 11-invariant regression test at tests/features/claude_tab_status_indicator/. Roadmap item: 2026-04-23 user request.

Changed

  • ClaudeIntegration::parseTranscriptForState refactored to a thin wrapper around the new static parseTranscriptTail helper. Pure function, no side effects — safe to call from per-shell trackers. Behavior unchanged; claude_status_bar and claude_plan_mode_detection regression tests still green against the same transcripts.

  • Hook events stash session_id in ClaudeIntegration::lastHookSessionId(). Handlers of signals emitted by processHookEvent (e.g. permissionRequested) can now read the id to route the event per-session rather than treating every hook as "belongs to the active tab". Required for the per-tab activity indicator to flag the correct tab on PermissionRequest.

0.7.15

Theme: fold-in of the 2026-04-23 re-review follow-up list. Six of nine items had already shipped in 0.7.12 but were never crossed off the ROADMAP (code was present, bullet was not flipped); the remaining three are real new work — a /tmp/kwin_*.js orphan sweep, a behavioral replacement for the fragile source-grep gate test, and a signal-count assertion on the plan-mode tail-preservation path. Two new regression tests lock existing invariants.

Added

  • Startup sweep for orphan /tmp/kwin_*_ants_*.js script files. The position-tracker + move/center helpers write KWin scripts via QTemporaryFile(autoRemove=false) and rely on a chained dbus-send callback to remove them after KWin unloads. A crash, SIGKILL, or dbus hang between write and unload orphans the file — no functional harm (KWin already loaded its copy), but the files accumulate in /tmp. MainWindow ctor now runs a once-per-process sweep of kwin_{pos,move,center}_ants_*.js older than one hour. The one-hour floor avoids racing an in-flight script another Ants instance just wrote.

Changed

  • Remote-control opt-in gate test is now behavioral, not source-grep. The 0.7.12 test grepped for remoteControlEnabled() within 400 chars of m_remoteControl->start(). A refactor that renamed the getter or hoisted the check into a helper would have silently defeated the gate while still passing (or failing) the grep. Replaced with a Config API round-trip assertion: default is false, setter persists across instances, value is readable post-reload. A getter rename now fails at compile-time in the test. The structural "start() must live inside a conditional" check is retained as a plain-substring lookback to catch a refactor that removes the gate entirely — without the catastrophic-backtracking risk that killed the initial regex attempt.
  • Plan-mode regression test now asserts emission count, not just final value. runToggleFreeTailPreservesState confirms planModeChanged fires exactly once across both parse phases (seed toggles NotSet → plan = one emission; tail sees zero permission-mode events and must not re-fire the same state). Guards against a regression that re-emits on every parse.

Tests

  • config_parse_failure_guard adds retention-cap invariant (INV-5). Plants 8 stale .corrupt-* siblings with decreasing mtimes, trips a fresh parse-failure, asserts only the newest 5 (fresh + 4 old) remain. Verified to fail against a neutered prune loop (got 9, expected 5). Sets mtimes explicitly via QFile::setFileTime since QDir::Time ranks on mtime, not on the filename-embedded timestamp.
  • remote_control_opt_in adds behavioral Config round-trip section. Sandboxed via XDG_CONFIG_HOME (not setTestModeEnabled — that routes to a Qt-test-specific path that has collided with real user configs in practice). Four invariants: fresh-default false, in- memory round-trip, cross-instance persistence, setter-back-to- false round-trip.

Internal

  • ROADMAP re-review follow-up list reconciled with shipped code. Config backup retention cap, remote-control gate cache, kwin TOCTOU fix header comment accuracy, spec.md timestamp format, and plugin/theme parse warnings were all landed by 0.7.12 but still listed as 📋 on the roadmap. Flipped to ✅ with "Shipped 0.7.12" notes. No code changes for these items — just a book-keeping reconciliation so the next review doesn't re-flag them.

0.7.14

Theme: three Claude Code integration robustness fixes from the 0.7.12+1 re-review checkpoint. All three failed silently — no errors, no degraded-mode logs — so the symptoms were "a transcript- driven UI state that just stopped updating" or "a project that appeared under the wrong name in the picker". All three now have targeted fixture-based regression tests.

Fixed

  • Claude transcript tail window drops events larger than 32 KB. parseTranscriptForState read the final 32 KB of the JSONL transcript and skipped the first line after the seek as "likely truncated". When the final event was a user tool_result carrying inline file contents (routine for Read/Grep tool results on large files), the 32 KB window landed inside that one event, the firstLine-skip ate it, and the parser returned with zero events — state frozen at whatever it was before the turn. Replaced with a doubling-grow window (32 KB → 4 MiB cap) that only trims a potentially-partial prefix line when the buffer contains at least two newlines, guaranteeing real content remains for the parser.
  • Claude transcript dialog silently drops thinking content blocks. Real assistant events on extended-thinking-enabled models lead with one or more thinking blocks followed by the final text response. ClaudeTranscriptDialog::formatEntry only rendered text, tool_use, and tool_result block types; thinking fell into an implicit else that discarded the block. Now rendered as italicized, dimmed Thinking: paragraphs so readers can distinguish chain-of-thought from the visible reply.
  • decodeProjectPath mangles project names with embedded hyphens. Claude Code encodes absolute paths by replacing every / with - — lossy, since my-project (leaf name) and my/project (two segments) produce the same encoded string. The decoder replaced every - with / unconditionally, turning ~/my-project/sub-dir into ~/my/project/sub/dir. Rewrote as a greedy left-to-right filesystem-probing walker: at each hyphen, prefer the / form if that directory exists, fall back to - (folded into the segment name) if that exists instead, default to / otherwise. Legacy hyphen-free paths remain unchanged. Preferred source of truth is still extractCwdFromTranscript (used by discoverProjects); the probe-based decoder is the last-resort fallback.

Tests

  • claude_transcript_robustness (6 invariants) — tail window grows past a 40 KB trailing event (INV-1), tail-growth is bounded at the 4 MiB cap on pathological inputs (INV-2), leaf-name hyphens are preserved when the directory exists (INV-3), intermediate- segment hyphens are preserved when the full path exists (INV-4), missing filesystem hints fall back to the legacy all-slashes behavior (INV-5), and the canonical repo-style path still decodes correctly (INV-6). Verified to fail against the pre-fix code on INV-1/3/4, pass on INV-2/5/6 (which test the safety-cap and legacy paths).

Changed

  • No API or config changes. Pure bugfix release.

0.7.13

Theme: scope-tightening pass on the audit rule-pack trust boundary shipped in 0.7.12. The global audit_trust_command_rules bool over-granted after one opt-in — trusting Ants's own repo implicitly trusted every cloned repo the user later opened. 0.7.13 replaces the global flag with a per-project hash-bound trust store so trust is both narrow and self-invalidating.

Security

  • Per-project audit rule-pack trust store. Global audit_trust_command_rules bool is retired. Trust is now keyed on (canonical projectPath, sha256(audit_rules.json bytes)) via three new Config methods: isAuditRulePackTrusted, trustAuditRulePack, untrustAuditRulePack. Consequences:
    1. Trusting one project no longer extends to siblings.
    2. Any edit to the rule pack invalidates trust (re-prompt on next open) — a silent post-trust modification is detected.
    3. projectPath is canonicalized via QFileInfo::canonicalFilePath, so symlinks / trailing slashes resolve to a single record.
    4. ANTS_AUDIT_TRUST_UNSAFE=1 remains an escape hatch for CI that can't round-trip through the persisted store. Upgrade behavior: users who had the legacy bool set to true are re-prompted (fresh trust decisions per project). This is the safe default for a security feature.

Changed

  • Audit dialog: "Untrusted rules: N" badge. The skipped-rule count now appears on the Detected-types row alongside "User rules: N" and "Path rules: N", with a tooltip explaining the trust boundary and opt-in path. GUI users previously only saw the stderr qWarning, which was invisible outside a terminal-launched invocation. Stderr copy retained for headless / CI cases.
  • CLAUDE.md streamlined. Compressed from 325 → 147 lines (19.5 KB → 6.5 KB, ~67% reduction) by dropping the full project- structure tree (derivable from ls src/), collapsing the verbose audit pipeline prose, retiring the per-dep install-command list (each probes which <tool> and self-disables), and dropping the Config-keys table (derivable from config.cpp). Retained: non- obvious gotchas, rationale for design decisions, and the release- file triad rule.

Tests

  • tests/features/audit_command_rule_trust/. Seven-invariant regression test against the new trust API: default-untrusted, round-trip, hash-bound invalidation, cross-project isolation (with byte-identical packs to rule out hash coincidence), path canonicalization through symlinks + trailing slashes, idempotent untrust, and cross-Config-instance persistence through config.json. Spec-first: contract written before the code, every invariant mapped to an assertion with a diagnostic label.

0.7.12

Theme: independent-review sweep. Multi-agent /indie-review run (14 subsystems, 60 HIGH findings), followed by a re-review of the shipped Tier 1 batch that surfaced three more HIGH findings of the same shape. Every fix carries a regression test anchored to an external signal (published spec, CVE, real on-disk data) rather than author reasoning — closes the self-graded-homework loop. Final polish pass (this tag) handles the six mechanical follow-ups from the re-review.

Security

  • Remote-control (Kitty rc_protocol) opt-in default. New config key remote_control_enabled (default false, matches Kitty's allow_remote_control=no). send-text payloads pass through a C0 control-char filter stripping {0x00..0x08, 0x0B..0x1F, 0x7F} while preserving HT/LF/CR and UTF-8 multi-byte, unless the request carries "raw": true. Closes the UID-scope keystroke-injection chain — a same-UID attacker can no longer inject bracketed-paste-disable + newline payloads. Gate now snapshots once per process (static bool) so a second MainWindow reading the config after a runtime toggle doesn't try to bind a socket the first window isn't listening on — matches the "requires restart" documentation.
  • Config / Allowlist / Install-hooks silent-data-loss guard. The same load-without-else pattern (read → parse → on failure root stays empty → next save() writes defaults over the user's bytes) affected src/config.cpp, src/claudeallowlist.cpp, and src/settingsdialog.cpp (Install Claude hooks path, which shares the settings.json file with the Allowlist dialog). All three now rotate the corrupt file aside via the shared rotateCorruptFileAside() helper in src/secureio.h (ms-precision timestamp + collision-retry up to 10 suffixes), log the backup path, and refuse to write further until the user intervenes. Settings dialog shows the backup path in a QMessageBox so the user has an actionable recovery. rotateCorruptFileAside() now also prunes older <path>.corrupt-* siblings beyond the newest five — keeps recovery material available without letting repeated launches against a broken file accumulate unbounded.
  • std::rename return checked in Config::save. Failure logs errno via DebugLog::Config and removes the orphan tmp file instead of silently stranding it on disk.
  • SSH bookmark extraArgs sanitizer. SshBookmark::sanitizeExtraArgs now rejects -oProxyCommand=, -oLocalCommand=, -oPermitLocalCommand=yes, -oMatch=exec (OpenSSH Match exec runs /bin/sh -c on CLI-supplied commands), and -oKnownHostsCommand= (OpenSSH 8.5+, same RCE surface), in both single-token and space-separated forms, case-insensitive per OpenSSH's own option parser. Closes the CVE-2017-1000117-adjacent RCE-via-bookmark-field surface.
  • AI Insert Cmd sanitize + confirm. New AiDialog::extractAndSanitizeCommand extracts the fenced code block and strips C0 controls (minus HT/LF/CR), DEL, C1 controls (0x80..0x9F incl. NEL U+0085), line/paragraph separators (U+2028, U+2029), bidi overrides (U+202A..E, U+2066..9 — Trojan Source, CVE-2021-42574), and zero-width codepoints (U+200B..D, U+FEFF). Capped at 4 KiB. The click handler now requires explicit confirmation via QMessageBox::question showing the literal bytes plus a "N bytes filtered" footer when non-zero; when the 500-char preview doesn't cover the full command, the dialog surfaces a truncation warning so an attacker can't hide a payload past the preview window. OWASP LLM01 + LLM02 mitigation. Language-hint heuristic now also requires the first line be a single unbroken token (no space / tab) before treating it as a language identifier — foo bar\nls no longer has its first line eaten.
  • Audit rule-pack command fields gated. audit_rules.json entries that carry a command: field are now skipped unless Config::auditTrustCommandRules() is true (default false) or ANTS_AUDIT_TRUST_UNSAFE=1 is set. A cloned-but-untrusted repo can no longer run arbitrary shell via the Audit dialog. Skipped count surfaces via qWarning.
  • /tmp/kwin_*.js migrated to QTemporaryFile. The three callsites in xcbpositiontracker.cpp (window-position poll) and mainwindow.cpp (Quake-mode move + centre) used predictable filenames in /tmp, enabling a same-name symlink-swap TOCTOU and a collision when two Ants instances ran concurrently. All three now use QTemporaryFile with a XXXXXX template and setAutoRemove(false) so the async KWin finished callback can still read the script.

Fixed

  • Claude plan-mode detection. claudeintegration.cpp was reading value("mode") — a field that never appears in Claude Code v2.1.87 JSONL transcripts. Real field is permissionMode. Fixed, and the toggle-free tail scan now preserves the latched plan-mode state instead of resetting to false (otherwise a 32 KB window that scrolls past the last explicit toggle silently loses the flag).
  • Combining-char sidetable reflow on resize. Carried through in the cross-subsystem cleanup.
  • Plugin manifest + user theme parse warnings. Both loaders now emit a qWarning with the QJsonParseError offset when a file is malformed, so users can find their own typos instead of silently dropping the plugin or theme from the list.

Changed

  • rotateCorruptFileAside helper shared across three subsystems. Before the re-review, each site had its own ~30-line backup-rotation block; the three reviewers independently flagged the duplication. Now a single inline helper in src/secureio.h with retention cap and ms-collision retry.
  • Remote-control startup gate snapshots once per process. Caches Config::remoteControlEnabled() into a static local at first MainWindow construction and reuses across subsequent windows, so a runtime toggle of the config key during a session can't partially enable rc on a second window.
  • filterControlChars header comment corrected. Previously said "C0 / C1 control bytes"; only C0 is stripped at this layer — C1 codepoints are UTF-8 continuation bytes and can't be byte-filtered without mangling multi-byte characters. Comment now names aidialog.cpp as the layer that does strip C1 (operating on QChar, not raw bytes).

Tests

  • Five new feature-test lanes in tests/features/:
    • claude_plan_mode_detection/ — 8 scenarios against the JSONL transcript schema; each asserts behaviour against live on-disk bytes rather than a mock.
    • remote_control_opt_in/ — 17 assertions covering the gate, filterControlChars strip set, "raw": true bypass, and the stripped response field.
    • config_parse_failure_guard/ — 12 assertions covering the three load states (missing / valid / corrupt), backup rotation, save suppression, and std::rename failure path.
    • ssh_extra_args_sanitize/ — 22 assertions covering all five dangerous ssh_config directives in both -oKey=val and -o Key=val forms plus the toSshCommand() end-to-end invariant.
    • ai_insert_command_sanitize/ — 21 assertions covering the full strip set (C0 / DEL / C1 / NEL / line separator / bidi overrides / zero-width), UTF-8 preservation, 4 KiB length cap, and the language-hint heuristic regression (whitespace on first line is not a lang ID).

0.7.11

Theme: housekeeping pass — two long-deferred 💭 items from the 0.8+ queue and a ROADMAP correction. The perf item rewrites the combining-char map shift in deleteChars / insertBlanks to use node-handle extract() + key mutate + insert() instead of rebuilding the map. The security item is a defence-in-depth stat-guard around the remote-control socket's /tmp fallback cleanup. The correction promotes the 0.7.9-era text-run QString accumulator work from 💭 to ✅ (it was shipped, the ROADMAP entry just missed the update).

Changed

  • Combining-char map shift without rebuild (src/terminalgrid.cpp, deleteChars / insertBlanks). Old impl built a fresh std::unordered_map<int, std::vector<uint32_t>>, moved each surviving entry into it (re-hashing every key), then move-assigned it back. Rewrote to collect affected keys in two small local vectors (toErase / toShift), erase the deleted ones, then for each shift extract the node handle, mutate its integer key, and reinsert — zero bucket reallocation, zero re-hashing of the value vectors. deleteChars processes shifts in ascending order (leftward shift can't collide); insertBlanks in descending order (rightward shift can't collide). Empty-map fast-path skips the block entirely, so steady-state rows with no combining content (the vast majority) pay nothing. Tests combining_on_resize and osc8_insert_delete_lines still pass.

Security

  • IPC-socket /tmp fallback stat-guard (src/remotecontrol.cpp). RemoteControl::start() used to call QLocalServer::removeServer(path) unconditionally when listen() failed on a stale socket. The XDG_RUNTIME_DIR path (0700-owned) is safe, but the /tmp/ants-terminal -<uid>.sock fallback lives in shared-world-writable space where a symlink or a foreign file of the same name could trick us into unlinking something unrelated. Added a safeToUnlinkLocalSocket() helper that lstat()s the path and refuses removal unless it's an actual S_ISSOCK owned by getuid(). ENOENT passes through (nothing to remove anyway). On refusal the remote-control layer disables itself with a clear log message — no silent unlink of unknown files.

Fixed

  • ROADMAP accuracy — the 💭 entry for "Per-frame QString construction in the text-run accumulator" was actually shipped in 0.7.9 (both TerminalWidget::paintEvent and GlRenderer::render accumulate codepoints into a reusable std::vector<char32_t> on the Run struct and call QString::fromUcs4(data, size) once at run-push time). The stale entry is now promoted to ✅ with the same-style retrospective write-up the free-list entry got.

0.7.10

Theme: follow-through on the 0.7.9 null-result. The std::rotate scroll refactor was rejected because it paid for save/restore overhead without touching the real bottleneck — the per-scroll vector<Cell>(m_cols) heap allocation for the new bottom row. This release addresses that bottleneck directly with a small free-list of m_cols-sized cell buffers fed by any path that discards a row (scrollback eviction, scrollDown, insertLines, deleteLines) and drained by paths that need a fresh blank row. Steady-state scroll work on a full scrollback runs allocator-free.

Changed

  • TerminalGrid — cell-buffer free-list for scroll paths. New m_freeCellBuffers pool (cap 4 entries) + takeBlankedCellsRow() / returnCellsRow() helpers in src/terminalgrid.{h,cpp}. scrollUp, scrollDown, insertLines, deleteLines now recycle cell buffers instead of calling makeRow(m_cols, …) on every scroll. When scrollback hits m_maxScrollback and a pop_front evicts the oldest line, its cells vector is salvaged into the pool rather than freed; the next scroll's new bottom row pulls from the pool (blanked in place), skipping the allocator entirely. resize() clears the pool (stale-size entries would be wrong-width). Pool stays at 0–2 entries in normal use — memory footprint is bounded and negligible. All 49 feature tests still pass; scroll_region_rotate (shipped 0.7.9 to lock the contract) verified the invariants held across the refactor.

Performance

  • bench_vt_throughput newline_stream: 5.26 → 6.09 MB/s (+15.8 %). Median of ten-run pairs before / after. Other corpora are flat to mildly improved — ascii_print ~23 MB/s (baseline 23), ansi_sgr ~16.5 MB/s (baseline 16.5), utf8_cjk 8.9 MB/s (baseline 7.9, +12 %; the CJK payload hits scrollUp too). This is the first perf result to validate the 0.7.9 investigation's thesis that the container shuffle was a red herring and the allocator was the actual hot spot.

0.7.9

Theme: planned-work pass — three ROADMAP items from the 0.7.7 "deferred to 0.8+" queue investigated and shipped or rejected with findings on record. The investigations that didn't ship still left value behind (a feature-test that locks scroll-region semantics for any future refactor), and the one that did ship consolidates 13 path strings through one header and halves the per-frame QString::fromUcs4 call count in both render paths.

Added

  • scroll_region_rotate feature-test. Locks the observable behavior of TerminalGrid::scrollUp / scrollDown — row motion inside [scrollTop, scrollBottom], outside-region invariance, main-screen scrollback push, alt-screen no push, hyperlink tracking, and count > regionHeight clamp. Six scenarios, eight invariants. Pins the contract so the 0.8 ring-buffer-screen work can refactor safely. See tests/features/scroll_region_rotate/spec.md.
  • src/configpaths.h — single source of truth for home-relative paths. Inline namespace consolidating ~/.claude/projects, ~/.claude/sessions, ~/.claude/settings.json, ~/.claude/projects/<enc>/memory/MEMORY.md, and ~/.config/ants-terminal/hooks/claude-forward.sh. 13 duplicated path-concat sites across claudeintegration.cpp / settingsdialog.cpp collapsed to one call each. Future XDG-respect pass now has a single update surface.

Changed

  • Render path — per-run QString::fromUcs4. Both the QPainter (terminalwidget.cpp::paintEvent) and GL (glrenderer.cpp::render) code paths now accumulate codepoints into a std::vector<char32_t> per run and build the QString once at flush/draw time, replacing the previous runText += QString::fromUcs4(&cp, 1) pattern that allocated one small QString per non-space cell and repeatedly reallocated the run string on append. Saves N per-frame small-QString constructions in the common ASCII TUI case. Behaviour identical; ligature shaping via QTextLayout still receives the run text at the same point.

Investigated, not shipped (documented in ROADMAP)

  • std::rotate scroll refactor. Measured on bench_vt_throughput newline_stream: the rotate path was 12–15% slower (5.2 → 4.6 MB/s) than the erase/insert loop it replaces, because the newline-stream case is always count == 1 where rotate pays for a save/restore cycle that vector::erase + push_back avoids. Real bottleneck is the per-scroll makeRow(m_cols, ...) allocation + heap handoff to scrollback — not the container shuffle. Reverted. The scroll_region_rotate test stays so the next attempt can refactor safely. ROADMAP §0.7.7 updated with the scrollback-push cell-reuse approach as the real 0.8 candidate.
  • DialogBase. Only settingsdialog and claudeallowlist actually use QDialogButtonBox with Ok/Apply/Cancel; four other dialogs listed in the ROADMAP have bespoke button layouts. Would save ~10 lines across 2 call sites — premature abstraction.
  • ManagedProcess. auditdialog.cpp uses one shared QProcess member that runs checks serially (not six copies as the ROADMAP claimed). The timeout-and-cleanup dance already lives at one call site. No duplication to extract.

0.7.8

Theme: audit fold-in — every regression guard the 0.7.6 / 0.7.7 hardening pass left behind is now a repo-wide detector. Three new rules in the Project Audit dialog turn the one-off sweeps into persistent guards, so the next contributor that re-introduces the pattern gets caught at audit-time.

Added

  • ssh_argv_dash_host audit rule (Security, Major). Flags any << shellQuote(...host...) argv construction without a preceding args << "--" argv terminator — the CVE-2017-1000117 class. Runtime filter drops findings when the guard appears within ±5 lines (spans the if (!user.isEmpty()) / else split in sshdialog.cpp:67-71). tests/audit_fixtures/ssh_argv_dash_host/ provides the bad/good pair.
  • qimage_load_without_peek audit rule (Qt, Minor). Flags .loadFromData( calls that aren't either tagged // image-peek-ok or preceded by a QImageReader size-peek within ±5 lines — the image-bomb DoS vector that the 0.7.6 hardening sweep fixed. Both shipped sites in terminalgrid.cpp (OSC 1337 PNG + Kitty graphics) carry the explicit reviewer-sign-off tag and drop cleanly.
  • setPermissions_pair_no_helper audit rule (Qt, Info). Hygiene nudge toward setOwnerOnlyPerms() from src/secureio.h when a raw setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner) bitmask appears. Pattern deliberately excludes the 0755 hook-script case (extra | flags after WriteOwner) so the one legitimate literal-bitmask site in settingsdialog.cpp doesn't false-fire. The helper itself is suppressed via // ants-audit: disable-file in secureio.h — it is the definition, not a call site.

Changed

  • tests/audit_self_test.sh — three new run_rule lines, coverage cross-check now sees the new rule ids automatically. 55/55 pass (up from 52/52).

0.7.7

Theme: the 0.7.7 planned-work pass from the hardening four-dimensional review — every item marked 📋 in ROADMAP.md's 0.7.7 section is now shipped. Two de-duplication refactors, three perf hoists, four regression feature-tests locking the 0.7.6 → 0.7.7 fixes.

Added

  • Regression coverage for the four 0.7.7 hardening fixes — tests/features/osc8_insert_delete_lines/ (CSI L / CSI M sync invariant), tests/features/hyperlink_resize_clamp/ (shrink-resize clamp on the active OSC 8 start coordinates), tests/features/ssh_dash_host_rejected/ (the -- argv terminator against CVE-2017-1000117 class hosts), and tests/features/image_bomb_png_header_peek/ (static source-inspection guard ensuring every loadFromData is peeked first). Each test is designed to fail on pre-fix code and pass now. 49 feature/fast tests total.

Changed

  • setOwnerOnlyPerms helper (src/secureio.h) replaces the 11 copies of file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner) that were scattered across config.cpp, sessionmanager.cpp (×2), claudeallowlist.cpp, auditdialog.cpp (×4), claudeintegration.cpp (×2), remotecontrol.cpp, and auditrulequality.cpp. One typo (ReadOther, ReadGroup) away from silently widening access to files that may hold an API key. Two overloads: one for open QFileDevice& handles, one for existing path strings. The single 0755 case in settingsdialog.cpp (hooks shell script — needs executable bit) stays literal by design.
  • shellQuote helper (src/shellutils.h) replaces the previous static function in sshdialog.cpp and the re-defined lambda in mainwindow.cpp::openClaudeProjectsDialog. The two earlier impls differed on empty-string and no-special-chars handling; both call sites now share one inline implementation.
  • Paint-path hoists — QFontMetrics is no longer constructed per underlined cell; updateFontMetrics() caches underlinePos and lineWidth into members refreshed on font change. Selection bounds are now normalised once per paintEvent into local scalars with an inline cellInSelection lambda, rather than running std::swap + std::min/std::max per cell. Measurable on search-highlighted + selection-active frames.
  • isCellSearchMatch — std::lower_bound instead of linear scan. m_searchMatches is sorted by globalLine; the binary search jumps to the first match at or after the target line and iterates only the matches on that line. The previous implementation self-described as "binary search" but walked the whole vector from index 0 on every cell. Expected win: 5–20% of paint time on search-highlighted content in deep scrollback.

0.7.6

Theme: close the 0.7.x remote-control arc and close the audit-signal gap the 0.7.5 hygiene work opened. Seven rc_protocol commands now cover the "drive the terminal from a script" story end-to-end (ls, send-text, new-tab, select-window, set-title, get-text, launch), and the audit gains two feature-coverage lanes that catch shipped features without locking tests and spec prose that drifted past a rename.

Added

  • Audit — feature-coverage lanes (src/featurecoverage.{cpp,h}). Two new in-process audit checks that close the signal-loss frontier left by the 2026-04-21 RetroDB audit-hygiene work: scanner calibration was tightened, so the remaining noise was upstream of the scanners — features shipping without any locking test, spec prose referring to symbols that had since been renamed. The lanes are Info/Minor severity; the value is raising awareness, not gating releases.

    Lane 1 — spec↔code drift. For each tests/features/*/spec.md, extracts backtick-fenced identifier-shaped tokens (CamelCase, snake_case, kebab-case, scoped::names, dotted.ids) and reports the ones that no longer appear anywhere under src/. Scoped names fall back to their tail component (Class::method → method) so rename refactors that keep the leaf don't false-positive; a curated stopword list drops ubiquitous Qt / language keywords that always exist.

    Lane 2 — CHANGELOG↔feature-test coverage. Parses the top ## [x.y.z] section of CHANGELOG.md, walks the Added/Fixed bullets, and fuzz-matches each against the set of feature-spec titles. Backtick-token match wins; ≥2 significant-word overlap falls back (stopword-filtered, ≥4 chars). Bullets with neither surface as coverage gaps — release-note claims that never got a locking test.

    Both lanes use the AuditCheck::inProcessRunner hook so the pure parsers plug into the same parseFindings → suppress → render pipeline as the shelled-out scanners, sidestepping QProcess. Pinned by tests/features/feature_coverage/ (17 invariants across token extraction shapes, stopword filtering, top-section isolation, bullet-section tagging, and both fuzzy-match paths).

  • Remote-control — launch command. Seventh and final command in the initial rc_protocol surface. Convenience wrapper that spawns a command in a new tab — sugar for the idx=$(... new-tab) && ... send-text --remote-tab $idx --remote-text $cmd$'\n' pattern, collapsed into one round-trip.

    {"cmd":"launch","cwd":"<path optional>","command":"<string required>"}
    -> {"ok":true,"index":<int>}
    

    command is required (empty / missing yields a documented error pointing at new-tab for the bare-shell case). Auto-appends \n to the command if not already present — the convenience differentiator from new-tab, which stays byte-faithful for scripts that compose multi-line input or send control sequences.

    ants-terminal --remote launch --remote-command 'htop'
    ants-terminal --remote launch --remote-cwd /tmp --remote-command 'tail -f log'
    

    Pinned by tests/features/remote_control_launch/.

  • Remote-control — get-text command. Sixth rc_protocol command. Returns the trailing N lines of (scrollback + screen) joined with \n — so scripts can capture output, dispatch on visible state, or grab the last command's result.

    {"cmd":"get-text","tab":<int optional>,"lines":<int optional>}
    -> {"ok":true,"text":"<string>","lines":<int>,"bytes":<int>}
    

    Default 100 lines; capped server-side at 10 000 so a runaway --remote-lines 1000000 against a million-line scrollback can't blow up the JSON envelope. Reuses TerminalWidget::recentOutput(int) — already the AI dialog's context-capture accessor, so format stays consistent across both consumers. Client CLI adds --remote-lines <n> (validated via toInt(&ok); non-numeric or negative values reject before the socket call).

    ants-terminal --remote get-text                                   # default 100
    ants-terminal --remote get-text --remote-lines 24                 # screen-ish
    ants-terminal --remote get-text --remote-tab 0 --remote-lines 500 # specific tab
    

    Pinned by tests/features/remote_control_get_text/ — 6 invariants including the 10 000-line cap, the response field set (text/lines/bytes), and the client-side --remote-lines validation.

  • Remote-control — set-title command. Fifth rc_protocol command. Pins a tab label that survives both the per-shell titleChanged signal (OSC 0/2 from the inferior) and the 2 s updateTabTitles refresh — exactly the behaviour you want for "label this tab prod while I work on it" workflows.

    {"cmd":"set-title","tab":<int optional>,"title":"<string>"}
    -> {"ok":true,"index":<int>}
    

    Empty title clears the pin and the auto-title path resumes: under tabTitleFormat == "title" (default) the most recent shell-provided title is restored from TerminalWidget::shellTitle() immediately rather than waiting for the next OSC 0/2; under cwd / process formats updateTabTitles() rebuilds from current state. Pin storage is keyed by the tab's QWidget* and freed alongside m_tabSessionIds at tab-close time so the QHash<QWidget*, QString> doesn't accumulate dangling keys.

    Client CLI adds --remote-title <str> (isSet()-gated, so an empty string legitimately clears the pin):

    ants-terminal --remote set-title --remote-title '★ prod'
    ants-terminal --remote set-title --remote-tab 2 --remote-title 'logs'
    ants-terminal --remote set-title --remote-title ''   # clears pin
    

    Pinned by tests/features/remote_control_set_title/ — 8 invariants including the dual signal + timer guards, the tab-close cleanup, and the empty-title-restores-via-shellTitle behaviour.

  • Remote-control — select-window command. Fourth rc_protocol command. Switches the active tab to a given 0-based index.

    {"cmd":"select-window","tab":<int required>}
    -> {"ok":true,"index":<int>}
    

    tab is required — no implicit default-to-active, the caller always spells out which tab they mean. Validated via QJsonValue::isDouble() so a string-typed tab field doesn't silently become tab 0. After the switch, focus is restored to the new active terminal via QWidget::setFocus() so follow-up send-text calls without an explicit tab field land on the expected pane. Client CLI reuses the existing --remote-tab option:

    ants-terminal --remote select-window --remote-tab 2
    

    Pinned by tests/features/remote_control_select_window/ — 6 invariants including the required-tab contract, the isDouble check, and the setFocus-on-switch behaviour.

  • Remote-control — new-tab command. Third rc_protocol command. Opens a fresh tab and returns its 0-based index, so callers can chain into send-text --remote-tab <i> without guessing.

    {"cmd":"new-tab","cwd":"<path optional>","command":"<string optional>"}
    -> {"ok":true,"index":<int>}
    

    cwd omitted = inherit from focused/current terminal (same default as the menu-driven newTab() slot). command written to the new shell after a 200 ms settle via TerminalWidget::writeCommand (matches onSshConnect timing — shells drop keystrokes written before their init settles). Caller owns the trailing newline, matching send-text semantics for one consistent rule across every rc_protocol write-path. Client CLI adds --remote-cwd <path> and --remote-command <str>:

    ants-terminal --remote new-tab
    ants-terminal --remote new-tab --remote-cwd /tmp
    ants-terminal --remote new-tab --remote-cwd /tmp --remote-command 'pwd'
    

    Idiomatic chaining:

    idx=$(ants-terminal --remote new-tab --remote-cwd /tmp | jq .index)
    ants-terminal --remote send-text --remote-tab "$idx" \
        --remote-text $'ls -la\n'
    

    Pinned by tests/features/remote_control_new_tab/ — 7 invariants including the 200 ms settle, the QPointer<TerminalWidget> guard on the deferred lambda (tab-close between enqueue and fire), and the non-const int newTabForRemote(...) signature the dispatch layer depends on.

  • Remote-control — send-text command. Second rc_protocol command on top of the scaffold shipped in the previous Unreleased entry. Writes a UTF-8 string byte-for-byte to a tab's PTY master — control chars and escape sequences pass through unchanged, matching Kitty's rc_protocol send-text. Request shape: {"cmd":"send-text","tab":<int optional>,"text":"<string>"}; tab omitted targets the active tab. Response on success: {"ok":true,"bytes":<int>}. Client CLI adds --remote-tab <i> and --remote-text "<string>"; when --remote-text is absent, the client reads stdin until EOF — enables shell piping without inline-arg quoting pain:

    ants-terminal --remote send-text --remote-text 'echo hi\n'
    ants-terminal --remote send-text --remote-tab 0 --remote-text ''
    printf 'ls\n' | ants-terminal --remote send-text
    

    Does not auto-append a newline (matches Kitty; auto-newline would surprise binary-stream callers). tab is disambiguated via JSON isDouble() rather than toInt() == 0 so --remote-tab 0 stays meaningful. Locked by new source-grep test tests/features/remote_control_send_text/ (7 invariants including the no-auto-newline negative guard and the stdin-read ergonomic).

  • Remote-control protocol — first slice (src/remotecontrol.{h,cpp}). Kitty-style JSON-over-Unix-socket RPC. A QLocalServer listens on $ANTS_REMOTE_SOCKET or, by default, $XDG_RUNTIME_DIR/ants-terminal.sock (falling back to /tmp/ants-terminal-<uid>.sock when XDG runtime is unset). Each connection handles one request / one response, LF-terminated. The only command in this slice is ls, which returns {"ok": true, "tabs": [{"index", "title", "cwd", "active"}, ...]}. Unknown commands return {"ok": false, "error": ...} with client exit code 2. The same binary doubles as the client via --remote <cmd> (--remote-socket <path> overrides the socket). Clean-foundation release — the remaining commands (send-text, set-title, new-tab, select-window, get-text, launch) and the X25519 auth layer land in follow-up commits against this scaffold. Locked against future drift by tests/features/remote_control_ls/ (8 source-grep invariants including field-name stability and the --remote-before-MainWindow-construction ordering).

  • Main-thread stall detector (Debug Mode → Perf category). A 200 ms heartbeat QTimer in MainWindow measures the gap between its own firings; when the wall-clock gap exceeds the scheduled interval by more than 100 ms the event loop was blocked and the log records the drift, cumulative count, and worst-case observed so far. Gated by ANTS_DEBUG=perf (or all) so the detector is only armed when debugging — zero overhead when off, zero log output when no stalls are happening. Addresses the 2026-04-20 follow-up user report — "slow down experienced at various times, when tab has been clear or with lots of text, not one specific scenario." Intermittent slowdowns that span both empty and full tabs point at a periodic background handler rather than the PTY hot path, and this detector will fingerprint the exact blockage when the user next reproduces the slowdown.

  • VT throughput benchmark harness at tests/perf/bench_vt_throughput.cpp. Drives four fixed corpora (ascii_print, newline_stream, ansi_sgr, utf8_cjk) through VtParser → TerminalGrid::processAction at -O2, emits CSV per corpus (bytes, actions, wall-ms, MB/s, actions/s). First step of the 0.8.0 "Terminal throughput slowdowns" perf investigation (user report 2026-04-20). Registered under ctest label perf so it runs only on explicit ctest -L perf; default fast suite stays under two seconds. Baseline on dev laptop: ascii_print 23 MB/s, newline_stream 5 MB/s (4× slower — scroll/scrollback is the hotspot to profile next), ansi_sgr 17 MB/s, utf8_cjk 8 MB/s.

Changed

  • Dropdown-flicker — extended intra-action suppression to QEvent::HoverMove / HoverEnter / HoverLeave in MainWindow::eventFilter. The 0.7.5 NoAnimStyle fix only partially reduced the user-reported flicker (mouse-movement-triggered); Qt's style engine consults WA_Hover tracking — a separate channel from QMouseEvent — to re-evaluate :hover on every cursor tick. Without suppressing HoverMove the menubar was still repainting on every pixel of intra-item mouse motion. Cross-item motion (File → Edit) still passes through so menu switching works. Residual flicker is still visible (confirmed by the user after shipping this change) — pushed to the 0.8.0 roadmap's "Carried over from 0.7.x" section for a different-angle fix.

Fixed

  • new-tab honours its documented "caller owns newline" contract. The first ship of new-tab used TerminalWidget::writeCommand for the deferred command write, which always auto-appends \n — contradicting the documented send-text-style "caller owns the trailing newline" semantics. MainWindow::newTabForRemote now uses sendToPty (raw bytes) instead. The convenience case lives in the new launch command (which auto-appends explicitly), so callers who wanted "open + run" can switch with no behaviour change. Pinned by an INV-5 (neg) rule in tests/features/remote_control_launch/ against any future re-introduction of writeCommand in newTabForRemote.

0.7.5

Theme: same-day follow-up to 0.7.4 that resolves the dropdown-menu flicker the 0.7.4 "Known Issue" section called out. Root cause finally identified via the Debug Mode instrumentation that shipped in 0.7.4: Fusion's QWidgetAnimator was creating a 60 Hz QPropertyAnimation(target=QWidget, prop=geometry) cycle on the idle window (1439 animation completions in a 23 s idle log). Each cycle drove a full LayoutRequest → UpdateRequest → Paint cascade across every widget, which the user saw as dropdown flicker on mouse hover. Neither QApplication::setEffectEnabled(UI_AnimateMenu, false) nor QMainWindow::setAnimated(false) covers the style-hint-driven cycle — only overriding QStyle::SH_Widget_Animation_Duration does.

Fixed

  • Dropdown-menu flicker (root cause) — introduced a NoAnimStyle QProxyStyle in src/main.cpp that zeroes SH_Widget_Animation_Duration and SH_Widget_Animate. Wrapped around the Fusion style at application startup (app.setStyle(new NoAnimStyle(QStyleFactory::create("Fusion")))). Idle-log result: QPropertyAnimation events per second: 0 (was ≈62 Hz in 0.7.4). Total event volume dropped >99 % (29 096 → 62 log lines in 4 s idle). Pinned by the new no_anim_style feature test (tests/features/no_anim_style/).
  • Debug Mode — animation-creation hook switched from QEvent::ChildAdded to QEvent::ChildPolished. At ChildAdded time the child's vtable still points at QObject (the derived constructor hasn't run), so child->metaObject()->className() returns "QObject" and the QPropertyAnimation filter never fires. ChildPolished fires after full construction. The old hook was silently useless on the 0.7.4 investigation — this one actually logs creation sites.

0.7.4

Theme: a session's worth of user-facing UI fixes, the introduction of a comprehensive debug-logging system, and the debt-cleanup that came out of both. The centerpiece is the new Debug Mode (Tools → Debug Mode), which was what let us pin a ~54 Hz full-window repaint cascade that had been driving the menubar / dropdown / paint-dialog flicker everyone had been chasing for multiple releases. Fixes to the Command Palette, Paste dialog, TitleBar, QMainWindow animator, Qt UI effects, QMenuBar's hover rule, and several smaller items all landed here.

Added

  • Debug Mode (src/debuglog.{h,cpp} — ~200 lines). A single ANTS_LOG(category, ...) macro plus 14 independent category toggles (Paint, Events, Input, PTY, VT, Render, Plugins, Network, Config, Audit, Claude, Signals, Shell, Session). Categories can be enabled narrowly (one) or broadly (all) via either ANTS_DEBUG=<list> at launch (comma-separated names, or all, or 1 for legacy paint-only) or Tools → Debug Mode → [checkable submenu] at runtime. Output is timestamped and written to ~/.local/share/ants-terminal/debug.log (append mode, one header per process start with PID + active category mask); stderr mirror only when the env var was used, so 2>file.log redirection still works. Menu also offers Open Log File (xdg- opens the log in the system viewer) and Clear Log File. Hot-path cost when disabled is a single bit-test on a static quint32. Replaces the ad-hoc ANTS_PAINT_LOG diagnostic hack that was briefly landed mid-session.
  • OSC 133 prompt-on-new-line guard in TerminalGrid::processAction's OSC 133 A handler. When a shell emits ESC ] 133;A ST at the start of a new prompt, the terminal now nudges the cursor to column 0 of the next line first if the previous output didn't end with a newline (e.g. cat file.json whose last byte is }). Fixes the long-standing "prompt glued onto the end of the previous command's output" UX papercut. Equivalent to zsh's PROMPT_SP shell-side option, but works for bash / fish / any shell that emits OSC 133 A. Requires the shell-integration scripts at packaging/shell-integration/ants-osc133.{bash,zsh} to be sourced; without OSC 133, the terminal has no way to know a prompt is starting.

Fixed

  • Menubar File / Edit / View hover still flashed on mouseover despite the 0.6.42 focus-redirect guards and the 0.6.43 QMenuBar::item base stylesheet rule. Two remaining leaks: (a) the QMenuBar inherits Qt::WA_TranslucentBackground from the MainWindow, which disables auto-fill for the widget tree, so each hover repaint briefly rendered over cleared-to-transparent pixels; (b) Qt's QStyleSheetStyle treats QMenuBar::item:hover as a distinct pseudo-state from :selected, and the Breeze / Fusion paths can hover-flash for one frame before selection engages — on that frame, only :hover styling applied, and :hover was never defined. Fix sets autoFillBackground(true) + Qt::WA_StyledBackground on the QMenuBar so the stylesheet background-color paints reliably, mirrors the :selected highlight into :hover, and makes setNativeMenuBar(false) explicit so DE global-menu integrations never hide the menubar in our frameless window.
  • Paste-confirmation dialog: Paste button swallowed mouse clicks when focus wasn't already on it — only the &Paste Alt-mnemonic path worked. Root cause under a frameless + translucent main window on KWin: any dialog that goes through QDialog::exec() inherits Qt's Qt::ApplicationModal transition, which doesn't compose well with our QApplication::focusChanged redirect on the frameless parent — same incompatibility the Review-Changes dialog hit at 0.6.29. Rewrote pasteToTerminal() to use the Review-Changes shape exactly: heap-allocated QDialog with Qt::WA_DeleteOnClose, own QHBoxLayout of QPushButtons (Cancel default, Paste setAutoDefault(false)), clicked lambda calls performPaste() and closes. No blocking; caller returns immediately. Previous attempts via QMessageBox::exec(), custom QDialog::exec(), and show() + QEventLoop all hit the same modal-vs-frameless regression.
  • Command Palette QListWidget continuous-timer churn. The hidden QListWidget inside CommandPalette was running its internal QAbstractItemView machinery (layout scheduler, selection animation, view-update timer) from MainWindow construction onward, even though the palette itself was hidden. Debug-log measurement showed the timer firing at ~50 Hz, cascading LayoutRequest → UpdateRequest → full widget-tree paint. Fix: lazy-create the list in CommandPalette::show() so the view doesn't exist until the user first presses Ctrl+Shift+P. Zero QAbstractItemView timers at idle.
  • QMainWindow's built-in QWidgetAnimator was enabled by default (setAnimated(true)). It exists to animate dock-widget rearrangements; Ants has no dock widgets, but the animator still ran at idle. setAnimated(false) kills the continuous QPropertyAnimation(target=QWidget, prop=geometry) cycle that was cascading a LayoutRequest through the whole widget tree on every frame.
  • Qt UI effect animations (Qt::UI_AnimateMenu / UI_FadeMenu / UI_AnimateCombo / UI_AnimateTooltip / UI_FadeTooltip / UI_AnimateToolBox) disabled globally at startup via QApplication::setEffectEnabled(…, false). Menu show/hide, combobox dropdown, tooltip fade, and toolbox expand all went through QPropertyAnimation(geometry) at 60 Hz — each animation frame triggered a LayoutRequest cascade. We're a precision terminal, not a presentation app; these animations were pure cost with no user benefit.
  • TitleBar button tooltips (Center / Minimize / Maximize / Close) removed. A Qt / KWin / Wayland quirk re-fired the QTipLabel show/hide cycle on every frame while the cursor was near a titlebar button, animating the tooltip's geometry at ~60 Hz, cascading LayoutRequest through the whole widget tree. Diagnosed via ANTS_DEBUG=paint,events — the log pinpointed cls=QTipLabel name=qtooltip_label parent=QToolButton:closeBtn as the repaint source. The button glyphs (✥ / ✕ / ⬜ / ⟩) + the hover-colour feedback give enough affordance without tooltip text.
  • TerminalWidget switched from QOpenGLWidget to plain QWidget. The GL widget composites its FBO through the parent's backing store and the composition path always invalidates the top-level window — we chased this at length via PartialUpdate, WA_OpaquePaintEvent, swapInterval(0), setAlphaBufferSize opt-out, etc. None fixed it. Switching the base class eliminates the GL composition path entirely. The default QPainter path — already ligature-aware via QTextLayout / HarfBuzz — is unaffected. The optional GL glyph-atlas renderer (gpu_rendering config) is dormant in 0.7.4 and returns in a future refactor as an embedded QOpenGLWindow via createWindowContainer (the only shape that cleanly decouples GL composition from the parent's backing store).

Tests

  • tests/features/menubar_hover_stylesheet/ — pins the QMenuBar::item:hover rule, autoFillBackground(true), Qt::WA_StyledBackground, the QMenuBar::item base rule, and setNativeMenuBar(false) for the menubar hover no-flash contract.
  • tests/features/paste_dialog_custom/ — pins the async pasteToTerminal() pattern: new QDialog(this) on the heap, Qt::WA_DeleteOnClose, QDialogButtonBox::Ok+Cancel, Cancel setDefault(true), Paste setAutoDefault(false), the show() + raise() + activateWindow() activation dance, explicit setFocus on a button, QPointer<TerminalWidget> tab-close guard, performPaste() helper, no QEventLoop, no QDialog::exec(). Forbids the old synchronous bool confirmDangerousPaste(...) API from reappearing.
  • tests/features/terminal_partial_update_mode/ — pins TerminalWidget : public QWidget (not QOpenGLWidget) and the absence of QOpenGLWidget::* / makeCurrent calls in the .cpp.

Known issue

A residual dropdown-flicker cycle driven by a QPropertyAnimation(target=QWidget, prop=geometry) still runs in some idle states on KWin + Qt 6. The per-iteration fixes listed above resolved identifiable sources (CommandPalette QListWidget, TitleBar tooltip, QMainWindow animator, Qt UI effects); what remains is a Qt-internal animation we have not yet pinpointed. The new Debug Mode (Tools → Debug Mode) is the instrument to hunt it with — enable paint + events and grep the log for QPropertyAnimation CREATED lines.

0.7.3

Theme: H6.1 of the 📦 distribution-adoption plan — Lua plugins inside Flatpak. The 0.7.2 manifest shipped with plugin support disabled because org.kde.Sdk//6.7 does not carry lua54-devel and flathub/shared-modules has no Lua 5.4 entry today. 0.7.3 closes that gap with an in-manifest Lua archive module so a Flatpak build loads plugins the same way the openSUSE, Arch, and Debian builds do.

Added

  • Lua 5.4 archive module in the Flatpak manifest (packaging/flatpak/org.ants.Terminal.yml). A dedicated lua module appears before ants-terminal so the CMake configure step sees /app/include/lua.h + /app/lib/liblua.a and takes the LUA_FOUND=TRUE branch. Built from https://www.lua.org/ftp/lua-5.4.7.tar.gz with a pinned sha256 and the linux-noreadline target — the terminal only statically links liblua.a, so pulling readline into the sandbox would be pure bloat. MYCFLAGS="-fPIC" keeps the static library PIE-safe for linking into the ants-terminal executable. Installed to /app via make install INSTALL_TOP=/app, which is exactly where CMake's FindLua searches by default when CMAKE_INSTALL_PREFIX=/app.
  • Auto-refresh of the Lua tarball hash via flatpak-external-data-checker. The Lua module carries an x-checker-data: stanza pointing at https://www.lua.org/ftp/ with version-pattern: lua-(5\.4\.\d+)\.tar\.gz and url-template: https://www.lua.org/ftp/lua-$version.tar.gz. Flathub CI opens a PR against the Flathub repo with a refreshed url + sha256 on each Lua 5.4.x point release — no manual hash churn per bump, and 5.5.x majors are correctly excluded (they would break the in-source find_package(Lua 5.4) floor).
  • Feature test tests/features/flatpak_lua_module/. Source-grep regression against the manifest YAML pinning six invariants: Lua module precedes ants-terminal (order matters — flatpak-builder evaluates modules in sequence, and CMake's FindLua runs during ants-terminal configure), type: archive with a pinned sha256:, MYCFLAGS="-fPIC" in the build commands, make install INSTALL_TOP=/app, the linux-noreadline build target (guarding against the default make linux that aliases to linux-readline), and the x-checker-data stanza with the 5.4.x version-pattern. A regression that strips any of these fails at ctest time.

Changed

  • packaging/flatpak/README.md: the "Lua plugins — limitation" section is replaced with a "Lua plugins" section documenting the enabled path (archive-module build, linux-noreadline rationale, x-checker-data automation, success criterion of PluginManager loading a plugin inside the sandbox).
  • Manifest header comment updated to describe the in-manifest module and the automated hash-refresh path, superseding the "not wired up" note that shipped in 0.7.2.
  • ROADMAP.md H6.1 flipped from 📋 to ✅ (shipped in 0.7.3).

0.7.2

Theme: H6 of the 📦 distribution-adoption plan — Flatpak packaging. Ships the org.ants.Terminal.yml manifest and the PTY wiring Flatpak needs so a second tab's shell actually sees the host's $PATH and filesystem. One artifact that runs on every distro unblocks the "packaged everywhere" gating item without per-distro maintenance.

Added

  • Flatpak manifest (packaging/flatpak/org.ants.Terminal.yml). Targets org.kde.Platform//6.7 — the KDE SDK brings cmake, ninja, and every Qt6 component the build needs (Core, Gui, Widgets, Network, OpenGL, OpenGLWidgets, DBus) so the manifest's modules block is a single buildsystem: cmake-ninja entry. finish-args cover Wayland + fallback-X11 + DRI, --share=network (AI endpoint
    • SSH outgoing), portal access (org.freedesktop.portal.* covers global shortcuts + file dialogs), desktop notifications, and xdg-config/ants-terminal:create + xdg-data/ants-terminal:create so config and data land under standard XDG paths. The manifest is ready to re-point from type: dir / path: ../.. to type: git / url / tag for Flathub submission — local flatpak- builder --install --user builds end-to-end against the in-tree source today. See packaging/flatpak/README.md.
  • Flatpak host-shell wiring in ptyhandler.cpp. The forked child now probes FLATPAK_ID (set by the flatpak launcher) and /.flatpak-info (present in every sandbox regardless of launch path); either signal triggers a branch that exec's the user's shell via flatpak-spawn --host instead of direct execlp. The sandbox doesn't inherit env or cwd across the host boundary, so TERM, COLORTERM, TERM_PROGRAM, TERM_PROGRAM_VERSION (pulled from the ANTS_VERSION compile definition, not a literal), and COLORFGBG are forwarded explicitly via --env=KEY=VALUE; the requested working directory crosses via --directory=<workDir> gated on !workDir.isEmpty(). _exit(127) after execvp matches the direct-exec fallback's failure shape for a missing shell. This is the same PTY model Ghostty's Flathub build uses — the terminal emulator stays sandboxed (VT parser, renderer, plugin VM all inside the confined runtime); only the child shell escapes so the user's $PATH, $HOME, and tooling are reachable. Outside Flatpak the existing execlp(shellCStr, argv0, nullptr) path is hit byte-for-byte unchanged. Feature test tests/features/flatpak_host_shell/ locks the branch shape: detection probes both signals in an OR, --host and -- separators appear in argv, every TERM var is present as --env=..., workDir gating is correct, the direct-exec call site is preserved, _exit(127) is the post-exec fall-through.
  • packaging/flatpak/README.md. Local build instructions (flatpak-builder --install --user), host-shell rationale, Flathub submission workflow (the tag-based manifest body differs from the in-tree type: dir shape), and the documented Lua- plugins-disabled limitation for the initial manifest — plugin support returns via a shared-modules Lua entry once the tarball- sha256 refresh cadence is wired up.
  • packaging/README.md Flatpak section. Layout tree adds flatpak/; new "Flatpak — org.ants.Terminal.yml" section mirrors the openSUSE / Arch / Debian sections; shared-concerns header updated from "three recipes" to "four recipes".

Changed

  • ROADMAP.md H6 flipped from 📋 to ✅ (manifest shipped; Flathub submission is the residual 📋); distribution-adoption table entry updated to reflect the split shipped/remaining state.

0.7.1

Theme: first of the 0.8.0 🎨 multiplexing items — SSH ControlMaster auto-integration from the bookmark dialog. A second tab to the same host now piggybacks on the first tab's session, skipping the full auth handshake. Matches the precedent set by kitty's ssh kitten, Warp's SSH blocks, and iTerm2's SSH profiles.

Added

  • SSH ControlMaster auto-multiplexing. Connects opened from the SSH Manager dialog now carry -o ControlMaster=auto, -o ControlPath=$HOME/.ssh/cm-%r@%h:%p, and -o ControlPersist=10m when the new ssh_control_master config key is true (default). Opt-out via ~/.config/ants-terminal/config.json for sites that forbid lingering multiplex sockets by policy. $HOME is resolved in C++ via QDir::homePath() so the ControlPath works under dash / POSIX sh (neither does tilde expansion on command-arg foo=~/…); %r@%h:%p are OpenSSH ControlPath substitution tokens, not shell metacharacters, so the existing shellQuote() helper leaves them intact. The three -o options precede the [user@]host destination for parity with OpenSSH's own documentation examples. Feature test tests/features/ssh_control_master/ pins six invariants: default + explicit-false produce zero Control* tokens, explicit-true emits all three, $HOME resolution replaces any literal tilde, %r@%h:%p survives shell quoting, the flags precede the destination arg, and they coexist with legacy -p/-i/extraArgs without interference. See ROADMAP.md §0.8.0 — the item is carried forward from the 0.7.0 "SSH ControlMaster" roadmap bullet and lands ahead of the larger mux-server / remote-control work.

0.7.0

Theme: shell integration + triggers. Consolidates the 0.6.1 → 0.6.45 arc into the release the 0.7 ROADMAP plotted out: command blocks as first-class UI, .cast recording, semantic history, HMAC-verified OSC 133 markers, the full iTerm2-parity trigger set, SIMD VT parsing, a dedicated PTY read + VT parse worker thread, Wayland-native Quake mode (layer-shell + GlobalShortcuts portal), and the H1–H4 distribution slice (SECURITY.md, AppStream metainfo, man page, shell completions) that takes Ants from "side project" to "distro-ready." 0.7.0 is the minor-release rollup of 30 patch releases; the individual per-feature entries below 0.7.0 remain authoritative for the SHAs they shipped under.

Added

🎨 Shell integration
  • Command blocks as first-class UI (Warp parity, docs). OSC 133 prompt → command → output grouping; Ctrl+Shift+Up / Ctrl+Shift+Down jump to prev/next prompt; collapsible output with "… N lines hidden" summary; duration + timestamp in the prompt gutter; 2px pass/fail status stripe; per-block right-click menu (Copy Command, Copy Output, Re-run, Fold/Unfold, Share as .cast). Shipped across 0.6.x; consolidated in §0.6.10.
  • Asciinema .cast v2 recording (spec). Full-session via Ctrl+Shift+R; per-block export via the command-block context menu. §0.6.10.
  • Semantic history. Ctrl-click on a path:line:col capture in scrollback opens the file at the cited position. CWD resolution via /proc/<pid>/cwd, so relative paths Just Work without shell cooperation. Editor support broadened beyond VS Code + Kate to VS Code family (code-insiders, codium), vi-family (nvim, vim), nano, Sublime / Helix / Micro, and JetBrains IDEs. §0.6.12.
  • Shell-side HMAC verification for OSC 133 markers. When $ANTS_OSC133_KEY is set, every OSC 133 marker must carry an ahmac= param computed as HMAC-SHA256(key, <marker>|<promptId> [|<exitCode>]). Forged markers are dropped and a status-bar counter surfaces the count with a 5-second cooldown. Bash + zsh integration scripts under packaging/shell-integration/. §0.6.31.
🔌 Triggers + plugin events
  • Full iTerm2-parity trigger rule set with instant flag: bell, inject, run_script, notify, sound, command shipped in §0.6.9; highlight_line, highlight_text, and make_hyperlink shipped in §0.6.13 via a new TerminalGrid line-completion callback so matches map to exact column ranges on a real row before the row scrolls into scrollback.
  • OSC 1337 SetUserVar + user_var_changed event. §0.6.9. Disambiguated from inline images by the byte after 1337;. NAME ≤ 128 chars; decoded value capped at 4 KiB.
  • Command-palette plugin registration via ants.palette.register({title, action, hotkey}). Always-on (no permission gate); optional global QShortcut. §0.6.9.
  • New plugin events: command_finished (exit + duration), pane_focused, theme_changed, window_config_reloaded, user_var_changed, palette_action. §0.6.9.
⚡ Performance
  • SIMD VT-parser scan. Ground-state hot path now scans 16 bytes at a time via SSE2 (x86_64) / NEON (ARM64) for the next non-printable-ASCII byte, then bulk-emits Print actions for the safe run. A signed-compare trick (XOR 0x80 → two cmpgt_epi8 against pre-computed bounds) flags any interesting byte with a single movemask. Regression guard: tests/features/vtparser_simd_scan/ asserts byte-identical action streams across whole-buffer, byte-by-byte, and pseudo-random-chunk feeds over a 38-case corpus. §0.6.23.
  • Dedicated PTY read + VT parse worker thread. PTY read and parse run on a VtStream QThread; parsed VtAction batches cross to the GUI over Qt::QueuedConnection and apply on the main thread (paint stays where it was). Back-pressure: at most 8 batches (≈128 KB) in flight before the worker disables its QSocketNotifier and kernel flow control takes over; GUI re-enables on drain. Resize goes over Qt::BlockingQueuedConnection so winsize is current before the next paint. ANTS_SINGLE_THREADED=1 kill-switch was retired once the new path baked out. §0.6.34 / §0.6.37.
  • Incremental reflow on resize. Standalone lines that fit the new width get an in-place cells.resize() with default-attr padding or trailing-blank trim, skipping the allocation-heavy joinLogical / rewrap round-trip. Multi-line soft-wrap sequences still go through the full logic so correctness is preserved. §0.6.15.
🖥 Platform — Wayland native
  • Layer-shell Quake mode. find_package(LayerShellQt CONFIG QUIET) wires LayerShellQt::Interface when the layer-shell-qt6-devel package is installed; MainWindow::setupQuakeMode() promotes the window to a zwlr_layer_surface_v1 at LayerTop, anchored top/left/right with exclusive-zone 0. XCB path preserved for X11. §0.6.38.
  • Freedesktop GlobalShortcuts portal integration. GlobalShortcutsPortal client wraps the org.freedesktop.portal.GlobalShortcuts handshake (CreateSession → BindShortcuts → Activated) behind a single Qt signal. Wires the toggle-quake id on KDE Plasma 6 and xdg-desktop-portal-hyprland / -wlr; the in-app QShortcut fallback stays active on GNOME Shell. 500 ms debounce prevents focused double-fire. §0.6.39.
🔒 Security
  • Plugin capability audit UI. Settings → Plugins renders every declared permission as a checkbox per plugin; revocations persist to config.plugin_grants[<name>] and take effect at next plugin reload. §0.6.11.
  • Image-bomb defenses. New TerminalGrid::ImageBudget tracks total decoded image bytes across the inline-display vector + the Kitty cache; cap is 256 MB per terminal. Sixel rejects up front from declared raster size; Kitty PNG / iTerm2 OSC 1337 reject post-decode. Inline red error text surfaces the rejection. The per-image MAX_IMAGE_DIM = 4096 dimension cap remains in place. §0.6.11.
📦 Distribution readiness (H1–H4)
  • H1 — SECURITY.md + CODE_OF_CONDUCT.md. Coordinated-disclosure policy with supported-versions table, reporting channel (GitHub Security Advisory + encrypted email), disclosure timeline, severity rubric, in/out of scope lists. Contributor Covenant 2.1 verbatim with maintainer email + the private GitHub Security Advisory listed as conduct reporting channels. Clears the Debian / Fedora / Ubuntu security-team review gate. §0.6.16.
  • H2 — AppStream metainfo + polished desktop entry. packaging/linux/org.ants.Terminal.metainfo.xml (AppStream 1.0 with summary / description / releases / categories / keywords / OARS content rating / supports / provides / launchable) and packaging/linux/org.ants.Terminal.desktop (reverse-DNS id, tightened Keywords, StartupWMClass, Desktop Actions for NewWindow + QuakeMode). CMake install rules via GNUInstallDirs cover desktop / metainfo / six hicolor icons; CI runs appstreamcli validate --explain + desktop-file-validate on every push. §0.6.17.
  • H3 — Man page. packaging/linux/ants-terminal.1 in groff -man covering synopsis, every CLI flag, environment variables, files, exit status, bugs, authors, and see-also. CMake installs to ${CMAKE_INSTALL_MANDIR}/man1/; CI lints the source with groff -man -Tutf8 -wall. §0.6.18.
  • H4 — Shell completions (bash / zsh / fish). Each shell completion installed to the conventional vendor path (bash-completion/completions/, zsh/site-functions/, fish/vendor_completions.d/); all three are auto-discovered on system-wide installs. CI lints each file with the matching shell's parse-only flag. Closes the H1–H4 distribution slice; remaining packaging work (H5 distro recipes landed in §0.6.20; H6 Flatpak, H7 docs site, H13 distro outreach) lives in 0.8.0. §0.6.19.
🧰 Dev experience — Project Audit polish
  • Qt rule: unbounded callback payloads (unbounded_callback_payloads, §0.6.8).
  • Qt rule: QNetworkReply::connect without context object (qnetworkreply_no_abort, §0.6.8).
  • Observability rule: silent catch(...) (silent_catch, §0.6.7).
  • Self-consistency: fixture-per-addGrepCheck (audit_fixture_coverage, §0.6.6), CI-enforced via tests/audit_self_test.sh.
  • Build-flag recommender (missing_build_flags, §0.6.7).
  • No-CI check (no_ci, §0.6.7).
  • build-asan CI lane — ctest + binary smoke under ASan/UBSan on every push. §0.6.7.
  • CONTRIBUTING.md — derived from STANDARDS.md; covers build modes, test layout, adding an audit rule, version-bump checklist. §0.6.7.
  • actions/checkout v4.2.2 → v5.0.1. Runs on Node 24, pre-empting GitHub's 2026-06-02 Node 20 deprecation. Both CI pin sites SHA-pinned with # v5.0.1 humans-readable comment.

Changed

  • ROADMAP 0.7.0 theme — every 📋 item on the list moves to ✅. Deferred items (EGL swap-with-damage on GL, Domain abstraction, persistent workspaces, Kitty Unicode-placeholder graphics) are explicitly re-scoped to 0.8.0+ so the 0.7 minor bump reflects the work that actually shipped.

Notes

  • 0.7.0 carries no breaking API changes. Every plugin that loads against 0.6.45 loads against 0.7.0. The minor bump reflects the accumulated theme delta since 0.6.0 shipped five days ago — not a manifest or ants.* schema break.
  • Because 0.7.0 is a rollup, this entry deliberately duplicates individual 0.6.x bullets rather than linking through. The 0.6.x entries below remain authoritative for the SHAs + commits they landed in.

0.6.45

Theme: Deferred VT-standards items from the 10th-audit research report, plus atomic-write hardening for long-lived audit state. 11th audit (this release) returned 0/~103 actionable — baseline remains clean.

Added

  • OSC 10 / 11 / 12 colour queries. Apps (delta, neovim, bat, lazygit, fzf, jj, and most termenv-based Go TUIs) probe the terminal for default fg / bg / cursor colour with \e]10;?\e\\ etc. to auto-detect dark vs light themes without relying on the unreliable COLORFGBG env var. Response is the xterm 16-bit- per-channel form \e]<n>;rgb:RRRR/GGGG/BBBB\e\\. Only the query form (? payload) is honoured — the set form is deliberately silently dropped to keep default fg/bg theme-driven and prevent in-terminal theme injection.
  • DECRQSS (Request Status String). DCS $q requests for DECSTBM (scroll-region, r), SGR (current attributes, m), and DECSCUSR (cursor shape, q) now reply with DCS 1 $ r … / DCS 0 $ r … per the xterm spec. tmux, neovim, and kitty's kitten use these to save-and-restore state around their own output. Unknown settings reply "invalid" rather than dropping silently so callers fall back to defaults. Sixel DCS payloads continue to route to the image handler — the DECRQSS branch gates on the $q prefix before Sixel's q search.

Changed

  • Atomic JSON writes via QSaveFile for four long-lived files:
    • audit_rule_quality.json — per-fire/per-suppression history (written frequently; corruption loses every rule score).
    • .audit_cache/trend.json — cumulative per-run severity totals.
    • .audit_cache/baseline.json — baseline fingerprints anchoring the "new findings only" filter.
    • .audit_suppress v1 → v2 migration rewrite (the simple append path stays non-atomic on purpose — one-line writes; loader already skips malformed lines). Torn writes from crash / kill -9 now can't corrupt any of these. config.json and session snapshots already had a tighter fsync-then-rename pattern; those were left in place.

Tests

  • osc_color_query — locks OSC 10/11/12 response shape + the query-only gate (set form MUST NOT produce a response, preventing theme-injection via in-terminal programs).
  • decrqss — locks the DECSTBM / SGR / DECSCUSR replies plus the invalid-reply bytes and a Sixel-routing regression guard so the new DECRQSS branch can't eat image traffic.

0.6.44

Theme: Project Audit dialog — self-review improvements sourced from the 10th audit run (2026-04-19). That audit returned 0/55 actionable findings, and the nature of the 55 pointed at three bits of friction the dialog was creating for itself: the dialog was auditing its own output, every "is this noise?" verdict needed an individual click, and users had no summary-level readout for "how much of this run was signal?". All three are closed.

Changed

  • Signal ratio surfaced in the summary banner. renderResults() now computes an "X% actionable" figure — signal / (signal + noise) where noise = suppressions + AI-verdict FALSE_POSITIVE — and prints it below the severity pills with colour-coded context (green ≥60%, amber 20–59%, red <20%). A 30-day rolling average pulled from RuleQualityTracker sits next to it as a peer comparison point ("30d avg X% actionable, n=Y fires"). Makes the "should I bother scrutinising this run?" question answerable at a glance instead of requiring the user to count suppressions against the severity rows.
  • Auto-skip of prior audit artifacts in isGeneratedFile(). Three new built-in skip rules, no project configuration needed:
    • docs/AUTOMATED_AUDIT_REPORT_*.{json,md,html,txt} — the previous-run artifacts contain SHA-256 dedup keys that gitleaks consistently flagged as high-entropy secrets (19 of 23 findings in the last run).
    • .audit_cache/ and audit_rule_quality.json — the dialog's own baseline + self-learning ledger; auditing them is circular.
    • tests/audit_fixtures/** — scaffold data for the regex-rule self-test; bad.* files intentionally embed strings that third-party scanners misread as real findings (gitleaks picked up 4 of these last run).

Added

  • Batch AI triage. A 🧠 Triage visible (N) button sits next to the confidence-sort toggle in the filter bar. Click it and every visible, not-yet-triaged finding goes to the configured LLM in one JSON request (hard cap 20 per batch; above that it slices and dispatches sequentially). Each batch's response is an array of {key, verdict, confidence, reasoning} — verdicts are spliced back onto the right findings by dedup key so a reordering model can't misalign results. Prompt-injection hardening inherited from the single-finding path (4-backtick snippet fencing, literal ```` scrub). Confirms the exact count before the POST; hidden entirely when AI isn't configured. The button's label re-calculates on every render so it always shows the current visible-untriaged count.

Notes

  • No schema changes. audit_rule_quality.json and audit_rules.json are read-compatible with prior versions.
  • The 30-day actionable% uses fires as the denominator rather than run count, because run count isn't directly recorded — fires are a good enough proxy and the label makes the denominator explicit (n=<count> fires).

0.6.43

Theme: Two user-reported regressions closed, plus a quality-of-life default flip. The 0.6.42 focus-redirect guards didn't cure the menubar hover flash — the real culprit was a stylesheet gap, not focus churn. And users coming back to the app expected their tabs to be waiting for them; session persistence now ships default-on, matching what every modern terminal (iTerm2, Kitty, WezTerm, Konsole) does.

Fixed

  • Menubar hover still flashed after the 0.6.42 fix. The 0.6.42 pass narrowed focus-redirect firing but didn't address the real root cause. The stylesheet defined QMenuBar::item:selected without a matching base QMenuBar::item rule, so Qt fell back to the native style for the non-selected state and composited native item drawing under the :selected overlay. On hover transitions the native and stylesheet layers raced — visible as the flash. Added an explicit transparent-background QMenuBar::item base rule (with matching padding and border-radius) plus a :pressed rule so every menubar item state is owned entirely by the stylesheet. The 0.6.42 focus guards remain as defense-in-depth.
  • Tabs not remembered between sessions. Config::sessionPersistence() defaulted to false, so users who never toggled Settings → General → "Save and restore sessions" lost their tab set on every quit. Flipped the default to true; users with session_persistence: false explicit in config.json still get their opt-out honored (only the absent-key path reads the fallback). Matches iTerm2 / Kitty / WezTerm / Konsole default behavior. The 5-second uptime floor in saveAllSessions() still protects against crash-on-launch wiping real state.

Changed

  • Config::sessionPersistence() default changed from false to true. First-run users now get tab restore out of the box. Only affects users with no session_persistence key in their config; explicit values are respected unchanged.

Tests

  • tests/features/session_persistence_default/ — three-invariant feature test (spec.md + test_session_persistence_default.cpp) that locks in the truth table: missing key → true; explicit true → true; explicit false → false. Linked against src/config.cpp only (no GUI dependency). Verified against the pre-fix default by temporary revert — test fails red on (default false), passes green on (default true). Brings the feature-test count to 25, and the total ctest count to 26.

Internal

  • Cleaned up a -Wshadow warning at mainwindow.cpp:3921 — the Review-Changes finalizer lambda's const Theme &th shadowed the outer scope's th. Renamed the lambda-local to lth, silencing the build-time note without functional change.
  • Dropped an unused <cstdlib> include from the new session- persistence test (clangd lint flagged it).

0.6.42

Theme: Focus-redirect guard rails. The 0.6.26 auto-return-to-terminal behavior was firing through two narrow windows where it shouldn't — hover-across-menubar produced a visibly flashing highlight, and the paste-confirmation dialog's "Paste" button silently swallowed mouse clicks (only the &Paste keyboard shortcut worked). Both user-visible regressions traced back to the same QApplication::focusChanged handler. New guards short-circuit the redirect when a popup is open, when the cursor is over a menu/menubar, or when any top-level QDialog is visible — the last one covers the QMessageBox::exec() modality-handshake window where activeModalWidget() can briefly be null.

Fixed

  • Menubar File / Edit / View hover flash. Moving the mouse across the menubar caused the hover highlight to flicker on every motion tick. Root cause: Qt synthesizes brief focusChanged cycles during menubar hover; now could momentarily park on a chrome widget not covered by the parent-chain exempt list, which triggered the auto-return-to-terminal redirect and wiped the menubar highlight. The focus-redirect lambda now early-returns when QApplication::activePopupWidget() is non-null (covers QMenu / QComboBox popup / tooltip) and when QApplication::widgetAt(QCursor::pos()) has a QMenu or QMenuBar ancestor (covers the menubar, which is NOT a popup). Both guards are mirrored in the deferred QTimer::singleShot(0,…) fire-time block so a menu that opens between queue and fire is still honored.
  • Paste-confirmation dialog swallowed mouse clicks on "Paste". Multi-line paste (or paste containing sudo, | sh, control characters) triggered the "Confirm paste" QMessageBox, but clicking the Paste button did nothing — only pressing P (the &Paste keyboard mnemonic) worked. Root cause: QMessageBox::exec() enters modal state inside a brief show() handshake during which QApplication::activeModalWidget() can return null for a tick; if a focusChanged event fired in that window, the redirect stole the button's focus mid-click and cancelled clicked(). The redirect now walks QApplication::topLevelWidgets() at queue time and early-returns on any visible QDialog, mirroring the check that already existed at fire time.

Tests

  • tests/features/focus_redirect_menu_guard/ — new source-grep feature test pinning six invariants on the focusChanged lambda in src/mainwindow.cpp: the popup and menu-hover guards (queue + fire time), the visible-dialog walk at queue time, and the five existing guards that must not regress (activeModalWidget, QAbstractButton, mouseButtons, QDialog parent-chain, CommandPalette). Uses the balanced-brace scanner pattern (linear-time, constant stack) introduced in 0.6.41's command_mark_gutter test to avoid std::regex blowing the stack on a 4000+ line mainwindow.cpp.

0.6.41

Theme: Quake-mode + OSC 133 UX polish. Surfaces the out-of-focus portal binding state so users can tell at a glance whether their hotkey escapes focus, and adds a scrollbar-adjacent tick-mark gutter that shows OSC 133 command boundaries across the full scrollback — jump targets for Ctrl+Shift+Up/Down that were previously invisible.

Added

  • Command-mark gutter (show_command_marks, default on). A 4-px vertical strip just left of the scrollbar draws one tick per PromptRegion in the current scrollback. Each tick is color-coded by last exit status: green (success), red (non-zero exit), gray (in-progress, OSC 133 D not yet emitted). Ticks map linearly from PromptRegion.startLine / (scrollbackSize + rows) to y — so a 50,000-line scrollback compresses into the widget height with every boundary still visible. No-op when promptRegions().empty(), so users without shell integration see nothing change.

  • Settings → Terminal toggle. New "Show command markers in scrollbar gutter" checkbox (default on) under Scrollback, with a tooltip explaining the color states. Propagates to all live terminals via applyConfigToTerminal on apply — no restart needed.

  • Portal-binding status label in Settings → Quake groupbox. A one-line QLabel under the hotkey field reports whether out-of-focus hotkeys are available on this session:

    • ✓ Portal binding active (green) — hotkey works when Ants is unfocused, via Freedesktop Portal GlobalShortcuts. KDE Plasma 6, Hyprland, wlroots.
    • ⚠ Portal unavailable (amber) — hotkey works only while Ants is focused, with a hint pointing at the three xdg-desktop-portal-* backends that provide the service.

    Static check based on GlobalShortcutsPortal::isAvailable() — the D-Bus service registration is a per-session property that doesn't change while the app runs, so a snapshot at dialog construction is accurate.

Fixed (test infrastructure)

  • Feature-test regex stack overflow. The first draft of tests/features/command_mark_gutter/ used a [\s\S]*?^\} pattern with std::regex::multiline to extract paintEvent's body. On a 4500-line terminalwidget.cpp, libstdc++'s backtracking executor recursed 100,000+ frames into _M_dfs and segfaulted. Replaced with a linear-time balanced-brace scanner (extractFunctionBody(src, signature)) — same semantics, constant stack. Left as a guiding pattern for future source-grep feature tests against large files.

Invariants

  • Config::showCommandMarks() persists to show_command_marks with default true.
  • paintEvent gates gutter drawing on m_showCommandMarks AND non-empty promptRegions().
  • Gutter positions itself via m_scrollBar->width() subtraction so ticks never overlap the scrollbar thumb.
  • Three-state color branch must remain: success / failure / in-progress.
  • applyConfigToTerminal(t) propagates the toggle so settings-apply updates all live terminals.
  • Settings portal-status label branches on GlobalShortcutsPortal::isAvailable() with the exact strings "Portal binding active" and "Portal unavailable" so the test can lock them.

Locked by tests/features/command_mark_gutter (CTest labels features;fast, part of the release gate).

0.6.40

Theme: OSC 133 shell-integration polish. Adds two keyboard-driven "last completed command" actions that complement the existing block-at-cursor right-click menu (Copy Command / Copy Output / Re-run Command / Fold / Share as .cast) with no-selection-needed top-level equivalents. The iTerm2 ⇧⌘O / WezTerm CopyLastOutput convention, surfaced through the View menu, the command palette, and configurable keybindings.

Added

  • TerminalWidget::copyLastCommandOutput() slot. Walks promptRegions() backwards for the most recent region with commandEndMs > 0 && hasOutput, delegates to outputTextAt(idx) (unbounded), and places the result on the clipboard. Returns the number of characters copied, or -1 when no completed command exists. The backwards walk is load-bearing: if the tail region is still running (user typed a command but it hasn't emitted OSC 133 D yet), the method skips it and uses the previous completed region — so Ctrl+Alt+O during sleep 30 copies the previous command's output, never the partial in-progress one.
  • TerminalWidget::rerunLastCommand() slot. Same backwards-walk skip-in-progress rule, then delegates to rerunCommandAt(idx) which writes the command text + \r to the PTY. Returns the re-run region index or -1. Bailing on in-progress tails prevents splicing text into whatever the user is typing.
  • View menu entries + configurable keybindings. "Copy Last Command Output" (default Ctrl+Alt+O, config key copy_last_output) and "Re-run Last Command" (default Ctrl+Alt+R, config key rerun_last_command) live under the View menu next to the existing Previous/Next Prompt entries. Both surface in the command palette automatically (it collects all menu actions). The Ctrl+Alt+* modifier combo avoids the already-taken Ctrl+Shift+O (split pane) and Ctrl+Shift+R (record session).
  • Status-bar toasts for discoverability. Success path shows "Copied N chars of last command output" for 3 s; failure path shows "No completed command to copy (enable shell integration)" — the "(enable shell integration)" hint is load-bearing UX guidance when the user doesn't know OSC 133 requires bash/zsh/fish hooks.

Why not reuse lastCommandOutput()

The existing lastCommandOutput() helper caps output at 100 lines because it feeds commandFailed triggers where huge stderr noise is harmful. A user-initiated copy expects the full output or a clear "nothing to copy" signal, not a silently truncated string. The new action uses outputTextAt(idx) (unbounded) instead, and a feature test enforces this choice.

Invariants

  • Both slots must walk promptRegions() backwards and gate on commandEndMs > 0.
  • copyLastCommandOutput must call outputTextAt, never lastCommandOutput (100-line cap).
  • MainWindow wires both actions through config.keybinding(...) so users can rebind them via config.json.
  • Defaults don't collide: Ctrl+Alt+O / Ctrl+Alt+R must appear exactly once in mainwindow.cpp.
  • Both action handlers emit at least one showStatusMessage(...) toast so the operation isn't silent.

Locked by tests/features/osc133_last_command (CTest labels features;fast, part of the release gate).

0.6.39

Theme: Wayland-native Quake-mode, part 2 of 2. Completes the 0.7.0 ROADMAP item: out-of-focus global hotkey registration via the Freedesktop Portal org.freedesktop.portal.GlobalShortcuts D-Bus API. Replaces the originally-planned KGlobalAccel-on-KDE + GNOME-dbus branching with one compositor-agnostic portal API that upstream has converged on since 2023 (KDE Plasma 6, xdg-desktop-portal-hyprland, xdg-desktop-portal-wlr).

Added

  • GlobalShortcutsPortal client class. New src/globalshortcutsportal.{h,cpp} wraps the three-step handshake (CreateSession → Response with session_handle → BindShortcuts → Activated) behind a simple activated(QString) Qt signal. The class handles the portal's request-path prediction convention (subscribing to the Response signal before dispatching the method call so the portal's reply can't land in a window where no match rule is installed), Qt D-Bus custom meta-type registration for the a(sa{sv}) shortcuts array, and session lifecycle.
  • Portal-aware MainWindow Quake setup. When GlobalShortcutsPortal::isAvailable() returns true, MainWindow instantiates the portal, binds toggle-quake with config.quake_hotkey as the preferred trigger, and routes Activated signals through toggleQuakeVisibility(). The in-app QShortcut from 0.6.38 stays wired unconditionally as a defence-in-depth fallback — for GNOME Shell (portal service present but no GlobalShortcuts backend yet), for the window between CreateSession and BindShortcuts response, and for any future revocation / re-bind path.
  • Focused double-fire debounce. Both the in-app QShortcut lambda and the portal activated lambda stamp m_lastQuakeToggleMs with QDateTime::currentMSecsSinceEpoch() and reject any toggle if the previous stamp is less than 500 ms old. Without the debounce, a focused system where both paths deliver the same key press would hide-then-show the window in one frame (visible flicker).
  • Qt → portal trigger translation. qtKeySequenceToPortalTrigger() converts "Ctrl+Shift+\"/"Meta+F12" into the freedesktop shortcut-syntax equivalents ("CTRL+SHIFT+grave", "LOGO+F12"). Deliberately minimal — modifiers plus a handful of common punctuation → xkb-keysyms; full keysym coverage is xkbcommon's job. Unrecognised keys pass through unchanged, and the user adjusts in System Settings if needed (the portal prompt treats preferred_trigger` as advisory anyway).

Invariants

  • New source-grep regression test tests/features/global_shortcuts_portal/ pins: (1) isAvailable() gate on portal construction, (2) canonical org.freedesktop.portal.Desktop / /org/freedesktop/portal/desktop / org.freedesktop.portal.GlobalShortcuts / .Request literals in the impl, (3) bound id "toggle-quake" matches the Activated handler filter, (4) in-app QShortcut fallback stays wired, (5) both paths debounce via m_lastQuakeToggleMs (regex match count ≥ 2), (6) CMake lists the source + keeps Qt6::DBus linked, (7) header surfaces the expected public API (isAvailable, bindShortcut, activated).

0.6.38

Theme: Wayland-native Quake-mode, part 1 of 2. The 0.7.0 ROADMAP item is split into (a) layer-shell anchoring + wiring the long-dead quake_hotkey config key — shipped here — and (b) Freedesktop Portal GlobalShortcuts for out-of-focus hotkey registration, shipping in 0.6.39. After this release, running Ants Terminal in Quake mode on Wayland no longer depends on the compositor's goodwill for positioning/stacking.

Added

  • LayerShellQt integration for Wayland Quake-mode. When the layer-shell-qt6-devel package is available at build time, find_package(LayerShellQt CONFIG QUIET) wires LayerShellQt::Interface in and defines ANTS_WAYLAND_LAYER_SHELL. At runtime on Wayland, MainWindow::setupQuakeMode() promotes the QWindow to a zwlr_layer_surface_v1 at LayerTop, anchored top/left/right with exclusive-zone 0 and KeyboardInteractivityOnDemand. This is the Wayland equivalent of X11's _NET_WM_STATE_ABOVE + client-side move() — without it the compositor could place the dropdown anywhere and stack it below other surfaces.
  • Build-time + runtime fallbacks. When the devel package is missing at build time, CMake logs LayerShellQt not found — Quake-mode on Wayland falls back to the Qt toplevel path and the binary still runs. When the binary is run on X11, the XCB path (pre-0.6.38 behaviour) is taken — no regression.

Fixed

  • quake_hotkey config key was saved but never read. The QLineEdit in Settings → "Dropdown/Quake Mode" persisted the user's chosen sequence to config.json, but nothing consumed it — the value was inert. MainWindow now wires a QShortcut with Qt::ApplicationShortcut context to toggleQuakeVisibility() when Quake mode is active. The shortcut fires only while Ants has focus; true out-of-focus global hotkey registration goes through the Freedesktop Portal GlobalShortcuts API coming in 0.6.39.
  • Slide animation snap on Wayland. The QPropertyAnimation(this, "pos") path in toggleQuakeVisibility() is a no-op on Wayland (the compositor owns position) and the animation's end-value move() would visibly snap. The Wayland branch now takes a plain show()/hide() toggle; the XCB slide animation is unchanged.

Packaging

  • openSUSE spec: added BuildRequires: cmake(LayerShellQt) >= 6.0.
  • Arch PKGBUILD: added layer-shell-qt to makedepends and listed it under optdepends for runtime discoverability.
  • Debian control: added liblayershellqt6-dev to Build-Depends. Older Debian releases packaged the devel headers as liblayershellqtinterface-dev; the CMake QUIET flag lets the build proceed either way.

Invariants

  • tests/features/wayland_quake_mode/ — source-grep regression test pinning the 0.6.38 contract: (1) the <LayerShellQt/Window> include is guarded by ANTS_WAYLAND_LAYER_SHELL; (2) setupQuakeMode() does a runtime QGuiApplication::platformName() dispatch and preserves the X11 flags; (3) toggleQuakeVisibility() returns before the QPropertyAnimation setup on Wayland; (4) the constructor wires quake_hotkey to a QShortcut connected to toggleQuakeVisibility; (5) CMakeLists.txt uses find_package(LayerShellQt CONFIG QUIET) and links LayerShellQt::Interface when found.

0.6.37

Theme: Phase 3 of the 0.7.0 threading refactor — retire the ANTS_SINGLE_THREADED=1 kill-switch and delete the legacy single-threaded read/parse code paths from TerminalWidget. Bake period (0.6.34 → 0.6.37) proved the worker path; the 0.6.35 hotfix proved the kill-switch worked. Shipping on the worker is now the only path.

Removed

  • ANTS_SINGLE_THREADED=1 kill-switch. The env-var fork in TerminalWidget::startShell() and the legacy branch it guarded are gone. Every launch now goes through VtStream on m_parseThread.
  • Legacy m_pty / m_parser members and onPtyData slot. The widget no longer owns a Pty or a VtParser directly; both live on the worker inside VtStream. hasPty() and ptyChildPid() simplified to worker-only checks; no behaviour change at the call sites.
  • #include "vtparser.h" and #include "ptyhandler.h" from terminalwidget.h. Transitively pulled in via vtstream.h for callers that still need VtAction / Pty types.

Changed

  • ROADMAP.md §0.7.0 threading entry updated to note the 0.6.37 kill-switch retirement and the four-test invariant set (parse equivalence, response ordering, resize synchronicity, ptyWrite gating).

0.6.36

Theme: Tenth periodic audit (10th in the series, post-0.6.35). Two actionable findings across ~110 raw — a 2% signal ratio consistent with the codebase's audit-calibration anchor. Both fixed in a single pass.

Fixed

  • RuleQualityTracker::prune() dead code. Declared in src/auditrulequality.h:133 and defined in .cpp:250; never called. The retention-cutoff logic was inlined into save() (const-method) when that method needed to filter records without mutating. Two copies of the same logic would drift over time. Removed the dead function — save() is the single source of truth for the 90-day retention window. Flagged by cppcheck's unusedPrivateFunction rule.
  • pipeRun regex recompiled on every paste. src/terminalwidget.cpp:2129 constructed a fresh QRegularExpression with the CaseInsensitiveOption on every call to confirmDangerousPaste() — a hot path that fires on every user paste. Hoisted to a function-local static const so the pattern compiles once per process and is reused thereafter. Flagged by clazy's use-static-qregularexpression check. Safe because QRegularExpression::match is reentrant and const-safe since Qt 5.4.

0.6.35

Theme: Hotfix for the 0.6.34 threaded-parse refactor. Keystrokes typed into the terminal were silently dropped — the echo never appeared on screen. Shipped 0.6.34, caught in minutes of dogfood, fix is small and the regression test makes it impossible to come back quietly.

Fixed

  • 0.6.34 regression: keystrokes not echoing. Twelve PTY-write call sites in TerminalWidget (keystrokes, Ctrl modifiers, arrow keys, clipboard paste, focus reporting, mouse middle-click paste, input method commit, scratchpad, context-menu paste and clear, writeCommand, rerunCommandAt) still carried pre-0.6.34 if (m_pty) ptyWrite(...) or if (cond && m_pty) { ptyWrite(...) } guards. Under the threaded parse path m_pty is null (the Pty lives on the worker thread inside VtStream), so the guard silently swallowed every write. Replaced each guard with either hasPty() (path-agnostic boolean — true for either the worker or the legacy path) or an unconditional ptyWrite(...) (the helper is already null-safe). The legacy path's m_pty lifecycle code in startShell / recalcGridSize / ~TerminalWidget is unchanged.
  • shellPid() / shellCwd() / foregroundProcess() were also gated on the bare m_pty pointer, returning empty on the worker path. Replaced with a new ptyChildPid() helper that reads from whichever path is active (VtStream::childPid() on the worker; Pty::childPid() on the legacy path). The PID is written once by forkpty() during startShell's blocking-queued invocation, so the cross-thread read is synchronised and mutex-free.

Added

  • tests/features/threaded_ptywrite_gating/ regression test. Source-grep inspection: no if (m_pty) / && m_pty) / !m_pty gate may appear outside the four allowlisted functions (ptyWrite, ptyChildPid, startShell, recalcGridSize, ~TerminalWidget). Ground-truth verified — re-injecting the regression makes the test fail at the offending line; restoring the fix makes it pass.

0.6.34

Theme: Decouple PTY read + VT parse from the GUI thread. Ships the first planned 0.7.0 performance item early since the architectural change landed clean with three feature-conformance tests locking the invariants.

Previously, every byte of PTY output — from a fast prompt echo to a find / firehose — traversed the Qt GUI thread through QSocketNotifier → Pty::dataReceived → VtParser::feed → TerminalGrid::processAction and then on to paint. Heavy streams blocked scroll and repaint. As of 0.6.34, read + parse run on a dedicated worker QThread (VtStream); only batched VtAction streams cross back to GUI for grid application and paint.

Added

  • Threaded PTY read + VT parse (VtStream). New worker-thread wrapper around Pty + VtParser (src/vtstream.{h,cpp}). The worker reads the PTY master via its own QSocketNotifier, feeds bytes through VtParser, accumulates emitted VtActions into a VtBatch, and ships the batch to the GUI over Qt::QueuedConnection. GUI applies the batch to TerminalGrid and repaints — the grid and paint path do not move.
  • Back-pressure. Worker buffers up to 8 batches (≈128 KB of unprocessed PTY bytes) before disabling its read notifier so the kernel applies flow control to the child process. GUI re-enables reads on drain via VtStream::drainAck(). Replaces "let RAM grow unbounded while GUI catches up" with kernel-natural back-pressure.
  • Pty::setReadEnabled(bool) — hook to pause/resume the read notifier without tearing it down. Used by the back-pressure path. Pty::write and Pty::resize moved into public slots: so they can be invoked cross-thread via QMetaObject::invokeMethod.
  • Three feature-conformance tests under tests/features/:
    • threaded_parse_equivalence — 11 input fixtures (plain ASCII, mixed ANSI, DCS Sixel, APC Kitty, OSC 52 long payload, UTF-8 multibyte across chunk boundaries, DEC 2026 sync, OSC 133 markers, CSI with 32 params, UTF-8 bulk, 50 KB Print run) × 6 chunking strategies (whole, byte-by-byte, 16 KB worker flush, 7-byte, 3-byte, two pseudo-random seeds). Asserts action-stream identity across strategies.
    • threaded_response_ordering — DA1 / CPR / DA2 / mode-996 / DSR-OK sequence asserted to fire in parse order across three chunking strategies. End-to-end write order across the worker boundary follows from this plus Qt's per-receiver FIFO.
    • threaded_resize_synchronous — source-level contract that every invokeMethod(m_vtStream, "resize", ...) uses Qt::BlockingQueuedConnection, and that VtStream::resize / ::write are declared in a public slots: block.

Changed

  • TerminalWidget PTY-write path unified through ptyWrite(). ~30 call sites (keystrokes, bracketed paste, Kitty kbd protocol, response callback, snippets, scratchpad) now route through a single helper that branches on m_vtStream ? invokeMethod(QueuedConnection) : m_pty->write(). One-line diff per call site; correctness is centralised.
  • Resize path uses Qt::BlockingQueuedConnection. TerminalWidget::recalcGridSize waits for the worker to finish Pty::resize before returning, so the next paint always sees consistent ws_row/ws_col vs. m_grid->rows()/m_grid->cols(). No 1-frame flicker at the new size with old PTY dims.
  • Session logging and asciicast recording timestamps are now sampled on the worker at batch-flush time, which is more accurate than resampling on GUI (GUI paint latency previously skewed the recorded elapsed field). Log write granularity is now per-batch (coarser than per-PTY-read); users grepping the file minutes later see no difference.

Kill switch: set ANTS_SINGLE_THREADED=1 in the environment to force the legacy single-threaded path while the new code proves itself. It will be removed in a follow-up release once no regressions surface.

0.6.33

Theme: Nine long-standing user-reported bugs around the status bar, scrollback integrity, session restore, and the Review Changes dialog — fixed in one pass. The quiet centerpiece is the root-cause diagnosis of the long tail of "Review Changes button does nothing" reports: a 0.6.26-era focus-redirect lambda was firing a singleShot(0) refocus between a button's mousePress and mouseRelease, ripping focus off the button mid-click and silently cancelling the clicked() signal that QAbstractButton only emits when focus is continuous through release. Also retires the ElidedLabel + stylesheet chip padding class of status-bar bug (branch chip truncating to "…") by converting every fixed-width status-bar chip to plain QLabel / QPushButton with QSizePolicy::Fixed.

Added

  • One-click Claude Code hook installer in Settings > General. Writes ~/.config/ants-terminal/hooks/claude-forward.sh and merges PreToolUse / PostToolUse / Stop / PreCompact / SessionStart entries into ~/.claude/settings.json. Idempotent, preserves the user's existing hooks, safe to re-run.
  • Frozen-view scrollback snapshot. When the user is scrolled up, cellAtGlobal / combiningAt now read from a snapshot of the screen taken at scroll-up time rather than the live grid. The viewport is completely immune to live screen writes, not just to deep-scroll rows. Locked by new tests/features/scrollback_frozen_view/.
  • Claude status vocabulary. Per-tab status now distinguishes idle / thinking / bash / reading a file / editing / searching / browsing / planning / auditing / prompting / compacting, each with a distinct theme colour. Plan-mode and /audit are detected in the transcript parser; new ClaudeIntegration::planModeChanged and auditingChanged signals.
  • refreshStatusBarForActiveTab() — single per-tab refresh entry point covering branch, notification, process, Claude status, Review Changes, and Add-to-allowlist. Replaces the scatter of direct calls in onTabChanged where one miss meant a stale chip for 2 s.
  • notification_timeout_ms config key (default 5000) with a seconds spinner in Settings > General.
  • tab_color_sequence config key — ordered colour fallback independent of session persistence. UUID-keyed tab_groups is retained for within-session drag-reorder stability.

Changed

  • Branch chip colour moved from green (ansi[2]) to cyan (ansi[6]) so it is visually distinct from Claude-state chips and transient notification text.
  • Status-bar layout is now Fixed-sized. Branch, process, Claude status, and all transient buttons use QSizePolicy::Fixed with plain QLabel / QPushButton. Only the transient notification slot is elastic and elides with "…". The ElidedLabel + stylesheet chip padding miscalculation class of bug is retired.
  • Review Changes button is now tri-state. Hidden when not a git repo, disabled when clean and in-sync, enabled otherwise. The 0.6.29 "hide on clean" contract was too aggressive — an unpushed commit is reviewable work too. Probe switched to git status --porcelain=v1 -b so ahead-of-upstream counts. The global QPushButton:hover rule is now gated with :enabled so disabled buttons no longer light up on hover. The dialog renders Status + Unpushed + Diff sections; spec and feature test updated to match.
  • Review Changes dialog is now fully async. Opens instantly with a loading placeholder; git output streams in. The status-bar button disables while the dialog is open and re-enables on close. WindowModal was tried and reverted — non-modal with button-disable is the pattern that actually works.
  • Add to Allowlist gains a 3-scan debounce against TUI-repaint flicker, a shared toolFinished / sessionStopped retraction path across scroll-scan and hook detections, and duplicate-rule feedback on prefill.

Fixed

  • Focus-redirect race silently cancelled button clicks. Root cause of the long tail of "Review Changes does nothing" reports. The 0.6.26 focusChanged lambda queued a singleShot(0) to refocus the terminal the moment focus touched status-bar chrome. That zero-delay timer could fire between mousePress and mouseRelease on a QPushButton, ripping focus away mid-click; QAbstractButton requires continuous focus through release to emit clicked(), so the click was silently dropped. Fix: the lambda now exempts QAbstractButton focus targets and defers while any mouse button is held down.
  • Branch label eliding to "…" with ample room. ElidedLabel's sizeHint fudge could not account for the chip stylesheet's padding + margin. Converted to a plain QLabel with Fixed sizePolicy.
  • Claude status label truncating and occasionally stale. The new vocabulary fits the fixed chip width, and the consolidated refreshStatusBarForActiveTab() wiring ensures state never bleeds from a previous tab on onTabChanged.
  • Scrollback blank-line bloat on inactive tabs after session restart. Two root causes compounded: (a) recalcGridSize ran on inactive tabs before their geometry was laid out, shrinking grids to 3×10 — fixed with a pre-layout guard; (b) TerminalGrid::resize pushed every reflowed row into scrollback indiscriminately — now skips entirely-blank rows.
  • Tab colours not persisting across restarts. UUID-keyed tab_groups only worked when session persistence was enabled, because only the session-persist path re-used saved UUIDs. Added an ordered tab_color_sequence fallback applied at startup once tabs are constructed.

0.6.32

Theme: Audit self-learning layer + contributor-facing docs + a one-line CI hotfix for 0.6.31's OSC 133 verifier. The headline is a per-rule fire/suppression tracker with an inline LCS-based tightening suggester — the lightweight always-on counterpart to the heavier weekly cloud routine documented in RECOMMENDED_ROUTINES.md. Without instrumentation the 2026-04-16 one-shot FP-rate triage would silently re-accrue as the codebase evolves; now each suppression is recorded and the noisiest rules surface on demand in the audit dialog.

Added — audit-tool self-learning layer

  • RuleQualityTracker (src/auditrulequality.{h,cpp}) records every per-rule fire and user-suppression event from the project Audit dialog into <project>/audit_rule_quality.json. The JSON is bounded to a 90-day rolling window, capped at 50 000 records per category. Tracker writes back on dialog destruction (RAII finalisation), and force-flushes immediately on each suppression so user actions are durable even on a hard exit.
  • Rule Quality dialog — new "📊 Rule Quality" button in the audit dialog opens a modal table of per-rule stats: 30-day fires, 30-day suppressions, FP rate (capped 0-100 %), all-time fires, and a proposed dropIfContains tightening when the heuristic suggester has enough samples. Sorted by FP rate desc so the noisiest rules surface first; rows with FP ≥ 50 % are highlighted.
  • LCS-based tightening suggester — when the user suppresses a finding, the tracker computes the longest common substring across the last 5 suppressed line texts for that rule. If the LCS is at least 6 chars AND contains a structural boundary character (space, paren, brace, bracket, semicolon, quote, angle bracket), the suggestion is surfaced inline in the status bar (💡 <substring> looks like a common FP shape — consider adding it to <rule>'s dropIfContains in audit_rules.json). Pure-identifier substrings are rejected so the suggester proposes rule-shape filters, not project-noun filters.
  • Test: tests/features/audit_rule_quality/ covers fire + suppression aggregation, the LCS suggester's positive and negative paths (too-few-samples, pure-identifier rejection), and JSON persistence round-trip. Headless model-only test, no Qt GUI.

Added — contributor docs

  • docs/RECOMMENDED_ROUTINES.md captures the four Claude Code routines that earn a slot on the shared account quota for this repo: nightly audit triage (diff-against-baseline, PR only NEW findings), PR security review on main, weekly audit-rule self-triage (cross-rule LLM analysis of audit_rule_quality.json, opens tightening PRs — the deeper counterpart to the in-process tracker above), and a monthly ROADMAP-item scoper. Each entry includes the trigger config, environment setup, and the exact prompt text. Total budget ~3-4 runs/day, leaving room for ad-hoc work and the Vestige repo's own routines.

Fixed

  • CI build on Ubuntu runner. QMessageAuthenticationCode::addData in older Qt 6.x does not accept QByteArrayView; the 0.6.31 OSC 133 verifier used the view-taking overload and broke the Ubuntu job. Switched to the (const char*, qsizetype) overload present in every Qt 6.x version. Behaviour unchanged; the HMAC feature test still passes. Purely a build-compat fix — users on 0.6.31 are unaffected if they're already building locally.

0.6.31

Theme: Security + audit signal-to-noise + two regressions. The headline is OSC 133 HMAC-signed shell integration (a 0.7.0 ROADMAP item shipped early): protects the command-block UI from forged shell- integration markers emitted by untrusted in-terminal processes. Also cuts the audit-tool false-positive rate from ~97 % to under 10 % via a two-pass triage of the Ants and Vestige audit runs, fixes the user- reported Review Changes click-does-nothing bug, and locks the 0.6.26 Shift+Enter "tab freezes" regression with a feature test.

Added — Security

  • OSC 133 HMAC-signed shell integration. When $ANTS_OSC133_KEY is set in the terminal's environment, every OSC 133 marker (A/B/C/D) MUST carry a matching ahmac=<hex-sha256-hmac> parameter computed as HMAC-SHA256(key, "<marker>|<promptId>[|<exitCode>]"). Markers without a valid HMAC are dropped (no UI side-effects) and a forgery counter increments, surfaced in the status bar via a throttled warning ("⚠ OSC 133 forgery detected") with a 5-second cooldown. The verifier closes the spoofing surface where any process running inside the terminal — a malicious TUI, cat malicious.txt, anything that writes to stdout — could otherwise mint OSC 133 markers and pollute prompt regions, exit codes, and the command_finished plugin event.

    When $ANTS_OSC133_KEY is not set, the verifier is silent and OSC 133 behaves as in 0.6.30 (legacy permissive). No upgrade penalty for users who don't opt in.

    Shell hooks ship under packaging/shell-integration/ants-osc133.{bash,zsh} and install to ${datarootdir}/ants-terminal/shell-integration/. The README in the same directory walks through key generation (openssl rand -hex 32), ~/.bashrc/~/.zshrc setup, threat model, and verification steps.

    Headless feature test (tests/features/osc133_hmac_verification/) covers verifier OFF back-compatibility, verifier ON accepting valid HMACs (including uppercase-hex), and rejection of missing/wrong/promptId-mismatched/ exit-code-mismatched HMACs. Implements ROADMAP §0.7.0 → 🔒 Security → "Shell-side HMAC verification for OSC 133 markers."

Fixed

  • Review Changes button silently swallowed clicks on a clean repo. refreshReviewButton's clean-repo branch left the button setEnabled(false); show() "as a hint that Claude edited something." QAbstractButton::mousePressEvent drops clicks on disabled buttons, so clicked() was never emitted and the 0.6.29 silent-return-with-flash guards inside showDiffViewer never fired. The global QPushButton:hover rule isn't :enabled-gated either, so the disabled button still highlighted on hover — advertising itself as actionable while doing nothing. Net symptom (user report 2026-04-17): "the Review Changes button is showing and is active as it has an onmouseover event that highlights the button. When I click the button though, nothing happens." Fix: hide() on clean repo. The button reappears via the next 2-second refresh / fileChanged tick when a real diff appears. New tests/features/review_changes_clickable/ locks the regression with a source-grep guard against re-introducing the setEnabled(false); ... show(); shape and a tripwire on the global hover stylesheet remaining un-:enabled-gated.

  • Shift+Enter wedged the terminal tab in bracketed-paste mode (0.6.26 regression). QByteArray("\x1B[200~\n\x1B[201~", 8) truncated the 13-byte literal to 8 bytes, dropping the closing [201~ end-paste marker and leaving an orphan ESC that kept the shell in bracketed-paste mode and ate the next keystroke. Switched to QByteArrayLiteral(...) so the size is derived from the literal at compile time — closes the entire class of "length argument drifts away from literal" bugs. New tests/features/shift_enter_bracketed_paste/ locks the byte sequence AND source-grep-guards against the QByteArray(literal, <num>) shape recurring.

  • Status-bar allowlist button dedup missed the hook-path container. The scroll-scan-path cleanup used findChildren<QPushButton*>("claudeAllowBtn"), but the hook-path permission handler creates the button as a QWidget container (with child controls) that has the same objectName. A QPushButton-typed findChildren skipped the container, letting both buttons stack when a scroll-scan detection fired while a hook-path container was already visible. Switched to QWidget* for parity with the onTabChanged and hook-path dedup lookups.

Fixed — audit signal-to-noise overhaul (~97 % → <10 % FP rate)

Two-pass triage of the Ants audit (docs/AUDIT_TRIAGE_2026-04-16.md) followed by the Vestige 3D engine triage. 137 of 141 findings in the Ants run were FPs; 100 % of four addFindCheck rules in the Vestige run were FPs. Targeted rule tightenings:

  • memory_patterns regex inverted — only flags new X() / new X(nullptr) / new X(NULL) (i.e. NOT parented). Any identifier inside the parens is treated as a Qt parent and suppressed. Kills 30 FPs from new QWidget(parent) / new QAction(this) etc. that the previous substring blacklist could only suppress when the parent expression matched a hardcoded name.
  • clazy --checks= drops qt-keywords. The project documents bare signals: / slots: / emit as house style; flagging them was ~48 FPs per run.
  • clang-tidy auto-disables when no compile_commands.json exists; collapses 'QString' file not found storms into a single banner line. Kills 34 near-identical driver-error FPs.
  • cppcheck excludes every build-* variant by name (build-asan, build-debug, etc.). cppcheck was parsing moc_*.cpp files in build-asan/ and tripping on their #error "This file was generated using moc from 6.11.0" banners — likely the source of the 30-second timeout on Compiler Warnings.
  • Context-window suppression added to qt_openurl_unchecked (±5 lines for startsWith("http" / QUrl("https" / // ants-audit: scheme-validated), insecure_http (±5 lines for startsWith scheme gates), unbounded_callback_payloads (±10 lines for .truncate( / .left( / .size() <= / constexpr int kMax), cmd_injection (suppress on , nullptr) / , NULL) exec-family terminators), bash_c_non_literal (±5 lines for m_config. / Config::instance Config-trust-boundary references), debug_leftovers (±8 lines for if (m_debug / if (on) / #ifdef DEBUG debug-flag conditionals).
  • secrets_scan RHS literal constraint — requires the right- hand side to be a "…" string of ≥16 chars (or '…', or a ≥16-char unquoted token). Rejects variable-name LHSes with constructor / variable RHSes (m_aiApiKey = new QLineEdit(tab), m_apiKey = apiKey).
  • Severity demotion: tool timeouts → Info (was inheriting the check's severity, polluting the Major / Critical tiers with "(warning) Timed out (30s)"); long_files → Info; missing_ compiler_flags → Info. The 2026-04-16 triage flagged these as "severity leak" — advisories that aren't bugs sharing a tier with real bugs.
  • Find/grep exclude lists pick up __pycache__/, .claude/, target/, .venv/, venv/, .tox/, .pytest_cache/, .mypy_cache/. The previous static lists missed common ecosystem scratch dirs.
  • // ants-audit: scheme-validated inline marker recognized in the context-window for qt_openurl_unchecked — closes the last MAJOR-tier FP from the Ants triage where the OSC 8 openHyperlink openUrl was 35 lines below the OSC 8 ingestion scheme allowlist (different file; ±5 line context can't span).

Four small real findings from the prior Ants triage:

  • claudeallowlist.cpp:409 — std::as_const(segments) on the value-captured QStringList (clazy range-loop-detach).
  • auditdialog.cpp:3115 — hoisted QStringList parts out of the finding-render loop (clazy container-inside-loop).
  • elidedlabel.h:33, sshdialog.h:32 — getters return const & not by value (cppcheck performance:returnByReference).

Net measured effect on a clean re-scan: the same audit drops from ~141 to ≲10 findings, with no loss of real coverage.

Fixed — audit rule noise follow-up (triage of the Vestige 2026-04-16 run)

Four addFindCheck rules produced 100/100 false positives when ants-audit scanned the Vestige engine on 2026-04-16. Each is a rule-shape bug rather than a find-site bug; all four are tightened so the next scan of the same tree drops straight to zero noise for these categories.

  • file_perms — World-Writable Files. Dropped the -perm -020 (group-writable) branch. Mode 664 is the default umask result on most Linux distros, so the check was flagging every normal file as a security finding. The rule now matches only true world-writable (-perm -002, CWE-732). src/auditdialog.cpp:708-720.
  • header_guards — Missing Header Guards. Scan window widened from head -5 to head -30 (copyright blocks at the top of a header commonly pushed #pragma once past line 5, so valid guards were read as missing). The _H-suffix requirement on traditional #ifndef guards was also dropped — naming conventions vary (FOO_HPP, FOO_GUARD, __FOO__) and the suffix was over-fitted to a single house style. src/auditdialog.cpp:1006-1026.
  • binary_in_repo — Binary Files in Source. Prefer git ls-files over find . when the project is a git checkout. find was matching gitignored paths (__pycache__/*.pyc, .claude/worktrees/…, target/…) because kFindExcl is a static allowlist and can't keep up with every project's .gitignore. The scan now falls through to the legacy find command only when git rev-parse fails. src/auditdialog.cpp:582-597.
  • dup_files — Duplicate File Detection. Same fix as binary_in_repo — prefer git ls-files, fall back to find. md5sum runs only on git-tracked files ≥100 bytes, sidestepping the Claude-managed-worktree and build-artefact duplication that made the report useless. src/auditdialog.cpp:561-579.

Measured against the same Vestige tree:

Rule Before After
file_perms 30 FPs 0
header_guards 20 FPs 0
binary_in_repo 30 FPs 0
dup_files 30 FPs 0

The timeout-as-tool-health and security-rule self-exclusion fixes cited in the same Vestige triage report landed earlier — see the 0.6.30 Fixed section (auditdialog.cpp:162-165 for timeouts, kGrepFileExclSec for self-exclusion).

0.6.30

Theme: Contributor-facing workflow infrastructure — project-level Claude Code hooks + recipe-driven version bumping. No runtime or behavior changes for end users. The ants-terminal binary is bit- identical to 0.6.29 modulo the ANTS_VERSION macro string. The new files are entirely tooling for contributors who clone the repo and work on it through Claude Code.

Added

  • .claude/settings.json wires a PostToolUse hook on Edit|Write that delegates to packaging/hook-on-cmakelists-edit.sh. The script short-circuits unless the touched path ends in CMakeLists.txt, then runs packaging/check-version-drift.sh and surfaces drift via a systemMessage. Contributor benefit: the version-drift gap that let 0.6.23 ship with stale packaging files can no longer survive even an in-session edit — the agent learns about drift the moment it edits CMakeLists, not at commit time.
  • .claude/bump.json — per-project recipe consumed by a user-level /bump skill (lives in ~/.claude/skills/bump/, not in this repo). Lists the 6 version-bearing files with templated {OLD} / {NEW} / {TODAY} find/replace patterns, plus 4 todos for content the recipe can't synthesise (CHANGELOG body, AppStream <release> HTML, debian changelog block, build/test verification). /bump 0.6.30 walks the recipe and runs check-version-drift.sh as its post-check.
  • packaging/hook-on-cmakelists-edit.sh — small bash dispatcher the project hook calls. Stdin is the PostToolUse JSON payload; stdout is empty on a clean state or {systemMessage:"…"} on drift. Always exits 0 — never blocks an edit, only informs.

Changed

  • .gitignore narrowed .claude/ → .claude/* with explicit !.claude/settings.json / !.claude/bump.json exceptions so the team-shared workflow files commit while developer-local .claude/settings.local.json and .claude/worktrees/ stay ignored. Same pattern adopted in the Vestige 3D Engine repo.

0.6.29

Theme: Status-bar reliability pass — nail down every user-reported status-bar symptom in one go: git branch chip elided to "…" when the statusbar had plenty of space, Review Changes button not appearing until a tab-switch, Add-to-Allowlist button silently no-op on save failure, Claude status label 2 s stale after tab-switch, Review Changes click "does nothing" with no feedback.

Added

  • Feature-conformance test: status_bar_elision (tests/features/status_bar_elision/) — spec + test covering the ElidedLabel elision policy. Three assertions: short text never elides, over-cap text elides with a non-empty tooltip, and the statusbar layout's QBoxLayout squeeze never drops short chips to "…" (the user-reported regression vector). Fails against pre-fix src/elidedlabel.h, passes on fix.

Changed

  • ElidedLabel::minimumSizeHint + sizeHint now compute against m_fullText, not QLabel::sizeHint() which reports the displayed (possibly already elided) text. This is the root-cause fix for the user-reported "git branch shows … instead of main" regression: the previous 3-char minimum let QStatusBar squeeze the chip below what "main" required, fontMetrics().elidedText() returned "…" for the squeezed width, the shorter displayed text fed back into sizeHint, and the widget was stuck on a fixed point it couldn't escape. minimumSizeHint now returns full-text-width + padding capped at maximumWidth, so short text is guaranteed to fit and only genuinely over-cap text elides.
  • refreshReviewButton() now fires on every 2 s status-bar tick via the shared m_statusTimer. Previously only onTabChanged and the Claude Code fileChanged hook triggered it, so a dirty repo at startup (no tab-switch, no hook fired) left the Review Changes button hidden. Also called once via QTimer::singleShot(0) at end of the MainWindow constructor so the button appears immediately on boot rather than 2 s later.
  • ClaudeIntegration::setShellPid calls pollClaudeProcess() immediately after arming the timer. Previously the Claude status label was NotRunning for up to 2 s after every tab-switch to a tab where Claude was actually running, until the next poll tick caught up — user-visible as "status doesn't update right away."
  • Add-to-Allowlist button unified across both creation paths. The hook-server button-group QWidget (Allow/Deny/Add-to-allowlist) now carries objectName "claudeAllowBtn" matching the scroll-scan path's bare QPushButton; onTabChanged now searches for QWidget (not just QPushButton) with that objectName so both paths are cleaned on tab-switch. Hook-server path also listens on every terminal for claudePermissionCleared instead of only currentTerminal(), so a brief visit to another tab doesn't orphan the button.
  • Both permission paths now set/clear m_claudePromptActive so the Claude status label consistently reads "Claude: prompting" while a prompt is live, and onTabChanged resets the flag so the next tab doesn't inherit the previous tab's prompt state.
  • showDiffViewer no longer has silent return branches. The user-reported "Review Changes shows but click does nothing" symptom was a combination of missing feedback when focusedTerminal returned null, empty shellCwd, or the underlying git diff returned non-zero. Each case now emits an explicit showStatusMessage. Also combined worktree + index diff into a single git diff HEAD (matches what refreshReviewButton uses for its enablement probe) instead of two sequential calls where the second was only reached on empty-worktree cases.

Fixed

  • ClaudeAllowlistDialog::saveSettings now returns bool and all three callers (prompt-prefill auto-save, QDialogButtonBox::accepted, Apply-button onApply) surface failure to the user: the prefill path shows a status message with the target path; OK/Apply surface an error in the dialog's validation label and refuse to accept() so the user can retry. Previously every QSaveFile open / write / commit failure was swallowed — the user saw the "Rule added to allowlist" toast even when the write had failed, then Claude Code re-prompted and the user reported "Add to allowlist does nothing."

0.6.28

Theme: End-to-end feature review — same-class overflow sweep across chrome widgets, quake-mode toggle fix, combining-character preservation on resize, disk-write durability hardening, and auto-profile regex caching.

Added

  • ElidedLabel widget (src/elidedlabel.h) — QLabel that truncates its full text with "…" to fit the current widget width, re-eliding on every resize and exposing the un-elided string via tooltip. Header-only; drop-in replacement wherever dynamic strings could outgrow a bounded slot.
  • Feature-conformance test: combining_on_resize (tests/features/combining_on_resize/) — exercises the three invariants covering the new resize fix (I1 main-screen preservation, I2 alt-screen preservation, I3 shrink-range eviction). Confirmed to fail against pre-fix src/terminalgrid.cpp and pass against the fix.

Changed

  • Status-bar text slots elide on overflow. The three text widgets — git branch (m_statusGitBranch, cap 220 px, elide right), transient message (m_statusMessage, stretch, elide middle), and foreground process (m_statusProcess, cap 160 px, elide right) — now cap out and show "…" instead of growing unbounded. User report: when the transient-message slot showed "Claude permission: Bash(git log …)" it pushed the git-branch chip and process indicator off the right edge. Middle-elide keeps both the leading label and the trailing detail visible in the message slot.
  • Same treatment applied to the other overflow-risk labels:
    • TitleBar::m_titleLabel — window title from OSC 0/2 is user-controlled and can push the minimize/maximize/close buttons off a frameless window. Middle-elide keeps "cmd" prefix and "cwd" suffix both visible.
    • MainWindow::m_claudeStatusLabel — "Claude: <tool-name>" in the ToolUse state accepts an arbitrary tool name from the transcript (MCP tools, custom tools); right-elide caps at 220 px.
    • AuditDialog::m_statusLabel — "Running: <check-name>…", "SARIF saved: <path>", and friends were unbounded; right-elide keeps the dialog footer stable.
  • Auto-profile rule regexes cached across poll ticks. checkAutoProfileRules previously compiled a fresh QRegularExpression per rule on every 2 s tick — 10 rules × 30 ticks/min = 300 wasteful JIT compiles/min. Cached keyed on pattern string in a function-local static QHash. Invalid patterns (from mistyped user config) are now detected via QRegularExpression::isValid() and surfaced via a one-shot status message instead of silently never matching.

Fixed

  • Quake-mode window no longer hides itself 200 ms after a show toggle. The m_quakeAnim QPropertyAnimation is reused across hide/show toggles. The hide branch connected finished→hide() with Qt::UniqueConnection, but Qt can't dedupe lambda-wrapped slots, and the stale connection from the previous hide fired at the end of the show slide-down — the window flashed in then vanished. Fix: QObject::disconnect(m_quakeAnim, &finished, this, nullptr) before every start(), and re-attach the hide() slot only on the hide branch. Show branch has no finished listener (the animation simply lands at its on-screen end value).
  • Terminal resize preserves combining characters on both main and alt screen. The simple-copy path in TerminalGrid::resize() (taken when cols is unchanged or when the alt-screen buffer is being resized alongside main) walked TermLine::cells only and default-constructed the combining side table — stripping accents, ZWJ sequences, and variation selectors off every on-screen cell whenever the user dragged the window edge. Symptom: filenames like résumé.pdf or naïve.cpp in ls output mutated to re?sume?.pdf / nai?ve.cpp on every resize. Fix: copy TermLine::combining and softWrapped alongside cells; filter out combining entries whose column exceeds the new width (shrink case). Alt-screen TUIs (vim, less, htop) that render accented filenames see the same bug and the same fix.
  • Config + session disk writes fsync() before atomic rename. Config::save, SessionManager::saveSession, and SessionManager::saveTabOrder each follow the write-to-.tmp
    • rename pattern for atomicity, but QFile::close flushes only userspace buffers. On ext4 data=ordered (the common default), a kernel crash or power loss between close() and rename() can leave a zero-sized file replacing the previous good one — silently losing config or a whole session's scrollback. Matches the write-rename-fsync pattern used by SQLite/Git.

Dev experience

  • Packaging files caught up with the CMakeLists version. Pre-0.6.28 CI had been red since 0.6.27 landed without the five packaging files (.spec, PKGBUILD, debian/changelog, man page, metainfo.xml) being bumped in lockstep — packaging/check-version-drift.sh correctly flagged the drift and blocked the merge. All six files now agree at 0.6.28 and CI passes.

0.6.27

Theme: clear-command scrollback fix, scroll-offset clamp, tab-close visibility, Claude "prompting" status, and allowlist-button lifecycle tightening.

Fixed

  • clear now properly wipes scrollback. On TERM=xterm-256color (set by PtyHandler), ncurses clear emits \E[H\E[2J\E[3J (verified via clear | od -c). The 0.6.26 Ink-overflow-repaint heuristic — "mode 3 within 50 ms of mode 2 = Ink frame-reset, preserve scrollback" — false-matched this byte-burst because the 2J+3J pair is identical to Ink's clearTerminal. User-reported symptom: after clear, the scrollbar remained scrollable showing nothing, because m_scrollback still held the pre-clear lines.

    Disambiguator: cursor position at the moment 3J arrives. clear emits H FIRST so cursor sits at (0,0); Ink emits H LAST so cursor is still wherever the overflowing output left it (bottom of the screen in practice). Treating cursor-at-origin as "user-initiated clear" wipes scrollback on clear while still preserving it for Ink — because Ink's overflow only triggers when output has filled past the viewport, by construction the cursor is never at (0,0) when Ink's 3J fires. New user-clear scenario in tests/features/scrollback_redraw/test_redraw.cpp pins the behaviour; the existing ink-overflow-repaint scenario continues to pass.

  • Scroll-to-bottom button no longer lingers after scrollback shrinks. TerminalWidget::m_scrollOffset is a widget-side value that wasn't re-clamped when TerminalGrid::m_scrollback.clear() ran (via \E[3J). The scrollbar's setValue clamp hid the symptom for the scrollbar itself, but m_scrollOffset > 0 was still the show-button condition and the viewport-rendering base (viewStart = scrollbackSize - m_scrollOffset went negative, painting phantom blank rows). Clamping m_scrollOffset to [0, scrollbackSize] in updateScrollBar() — the single choke-point called after every output batch — keeps the widget state consistent with the grid.

  • Tab close (×) button is now always visible. The theme stylesheet had QTabBar::close-button { image: none; }, which hid the × glyph entirely and left only an invisible hover hit-target. New users didn't discover the close affordance unless they happened to mouse over it. Removing the image rule lets Qt fall back to the platform close icon (QStyle::SP_TitleBarCloseButton), which adapts to the active palette. Hover still applies an ansi-red background (%7) for "will-click" feedback.

  • "Add to allowlist" button now also clears the accompanying status message on dismiss. Clicking the × button cleared the message, but the claudePermissionCleared path (prompt disappears on approve/decline) just deleted the button and left the "Claude Code permission: …" text stuck in the status bar. Both paths now call clearStatusMessage() symmetrically.

Added

  • Claude status bar says "Claude: prompting" while a permission prompt is waiting. Previously the label kept showing "Claude: idle" because ClaudeIntegration's transcript-driven state machine is unaware of the on-screen permission UI. For a user scrolled up in history — who can't see the prompt directly — "idle" was misleading. The prompt state is now tracked per-window as a boolean overlay on top of the ClaudeState: any detected prompt flips the label to yellow "Claude: prompting" regardless of the underlying state, and the prompt-cleared signal reverts it to the base state. Implementation consolidates the label-rendering switch into a single MainWindow::applyClaudeStatusLabel() method so prompt and state changes share one code path.

0.6.26

Theme: UX polish — status bar consistency, focus handling, Ink overflow-repaint fix, tab-colour gradient.

Fixed

  • Ink overflow-repaint no longer wipes scrollback or duplicates the replay. Claude Code's Ink/React TUI emits CSI 2J + CSI 3J + CSI H + fullStaticOutput + output whenever its rendered frame overflows the viewport (see ink/build/ink.js:705, ansi-escapes/base.js:124 clearTerminal). Previously the CSI 3J part of that burst wiped the user's conversation history and the subsequent replay duplicated into scrollback. Now CSI 3J arriving within 50 ms of CSI 2J on the main screen is classified as Ink's frame-reset marker: scrollback is preserved, and the 0.6.22 suppression window is armed for the replay so the scroll-off lines don't duplicate. Standalone CSI 3J (user-initiated clear -x or similar) still wipes scrollback as the user requested. Two new conformance scenarios — ink-overflow-repaint and standalone-3J — in tests/features/scrollback_redraw/test_redraw.cpp pin both behaviours.
  • Claude Code context progress bar no longer paints persistent "0%". ClaudeIntegration::setShellPid emits stateChanged(NotRunning) + contextUpdated(0) on every tab switch. The stateChanged slot hid the bar; the contextUpdated(0) slot unconditionally re-showed it. Fresh tabs, or tabs where Claude was never started, now stay hidden until a real non-zero percent is emitted.
  • Review Changes button no longer renders in small-caps monospace. The 0.6.26-in-progress "bold at 11 px to clear Gruvbox contrast" attempt combined with Qt's app-wide monospace font (main.cpp:150-153) and Fusion style produced squished lowercase letterforms that read as uppercase. Button now inherits the global QPushButton stylesheet so it matches the sibling "Add to allowlist" button. Disabled state gets its own distinctive visual (dashed border + italic + muted fg) so a clean-repo state reads clearly as non-actionable.
  • Status bar is now height-consistent. QStatusBar's size hint tracks its tallest child, so the bar jumped when the transient "Add to allowlist" button appeared and shrank when it went away. A 32 px setMinimumHeight floor covering the default QPushButton size keeps the bar stable regardless of transient widget presence.

Added

  • Git-branch chip ↔ transient-status-slot divider. A 1-px vertical QFrame::VLine painted in textSecondary (not border, because border is nearly invisible against bgPrimary on low-contrast themes like Gruvbox-dark and Nord). Gives a deterministic visual boundary every theme renders correctly.
  • Focus auto-return to active terminal. Connected to QApplication::focusChanged. When focus lands on chrome widgets (QStatusBar, QTabBar, bare QMainWindow) with no active modal, keyboard focus is deferred-set back to the terminal one tick later. Dialogs, menus, command palette, QLineEdit/QTextEdit input widgets, and TerminalWidget descendants all retain focus legitimately — the whitelist is specifically tuned to avoid hijacking user-meant focus.
  • Tab-colour gradient. Per-tab colour badge changed from a 3 px bottom strip to a vertical gradient — transparent at the top, 140/255 alpha of the chosen colour at the bottom. Tab text stays readable across every theme while the colour reads as an intentional wash. The active-tab 2 px accent underline (QTabBar::tab:selected { border-bottom }) is preserved by excluding the bottom 2 px from the gradient area.

0.6.25

Theme: a user-reported scrollback regression from 0.6.24's CSI-clear window extension, plus the two packaging-audit follow-ups from the ninth periodic audit (audit_2026_04_15).

Fixed

  • Viewport no longer shifts when streaming content arrives while the user is scrolled up in history. User report (2026-04-15): with ~500 lines in the terminal, scrolled up a few lines, with Claude Code actively streaming — the content at the viewport kept getting overwritten ("line 251 becomes line 250"). Root cause: the 0.6.21 scrolled-up pause and the 0.6.22 (extended 0.6.24) post-full-clear suppression window both skipped scrollback pushes to protect against TUI redraw pollution. Skipping the push kept TerminalGrid::scrollbackPushed() frozen, which kept TerminalWidget::onOutputReceived's scroll anchor from advancing m_scrollOffset. The screen still scrolled on every LF, so the viewport rows overlapping the screen got overwritten in place, and — worse — the anchor never re-pinned to the user's reading position in scrollback, so every subsequent push (once the suppression let up) moved the viewport further from where the user wanted to be. The 0.6.24 broadening to CSI 0J / CSI 1J made the window arm during the far more common "home + erase to end" repaint idiom, so the regression surfaced as near-continuous viewport drift during Claude Code activity. Fix: when the user is scrolled up (m_scrollbackInsertPaused == true), always push to scrollback — even inside the CSI-clear window. The anchor then advances m_scrollOffset by the push count, keeping viewStart pinned to the user's reading position. The doubling-protection window keeps its teeth for the at-bottom case (the only case where the doubling symptom is observable). New feature test tests/features/scrollback_redraw/test_viewport_stable.cpp asserts content at viewport rows is invariant across streaming for both the pause-only path and the CSI-clear path, across scroll offsets from barely-scrolled-up to deep-in-history. spec.md gains a §Viewport-stable companion contract documenting the invariant and its scope (must-hold when viewport is entirely in scrollback; partial guarantee when viewport overlaps the screen, since screen writes are inherently visible there).

Changed

  • packaging-version-drift rule now scans AppStream metainfo. The ninth audit noted that packaging/linux/org.ants.Terminal.metainfo.xml couples to every release — its <release version="X.Y.Z"> list has to advance on each bump — but the drift rule was not scanning it. Added to the rule's coverage list; the grep extracts the newest declared release (metainfo lists releases newest-first, so head -1 captures the latest) and diffs it against CMakeLists.txt's project(VERSION). Verified end-to-end: a contrived CMake bump to 0.9.99 now emits six findings (five packaging recipes + the metainfo entry) instead of five.
  • Drift-check logic extracted into packaging/check-version-drift.sh. The check used to live as a ~20-line bash string embedded in src/auditdialog.cpp. That meant CI couldn't reuse it without duplicating the logic and risking a new axis of drift. The script is now the single source of truth; auditdialog.cpp invokes it with a minimal [ -x … ] && bash … || exit 0 wrapper (graceful no-op when the tree is missing the script), and CI invokes it directly so a non-zero exit fails the job. Audit dialog's parser continues to ignore exit codes — same as before — so the CI-friendly non-zero-on-drift exit semantics don't break the in-app view.
  • CI gate: Check packaging version drift step in .github/workflows/ci.yml. Runs the script after every push and pull request. A PR that bumps CMakeLists.txt without updating every coupled file — spec, PKGBUILD, debian/changelog, man page, AppStream metainfo, README — now fails CI before merge. Closes the honor-system gap that let 0.6.23 ship with four stale packaging files (and would have let this very release ship with drifted metainfo if the new metainfo coverage had not been added in the same commit).

0.6.24

Fixed

  • Status-bar "Add to allowlist" button now persists for the life of the permission prompt. The previous lifecycle retracted the button on the next TerminalWidget::outputReceived signal after a 1 s grace — but Claude Code v2.1+ repaints its TUI continuously (cursor blink, spinner animation) even while the prompt is still visible, so the button vanished within seconds of appearing. Introduced a stricter claudePermissionCleared signal that fires only when the grid scan detects the prompt footer is no longer on screen, and rewired both the grid-scan and hook-stream button paths to retract against that signal instead. The button now stays visible for as long as the prompt is on screen.
  • "Review Changes" button legible on every theme, including those with deliberately-muted textSecondary. The 5e2ac58 fix gave the disabled state an explicit textSecondary color — which works for themes where textSecondary is ~70% luminance, but still fails WCAG AA on themes like One Dark (#5C6370 on bgSecondary #21252B ≈ 3:1 contrast). Switched to textPrimary + italic + dissolved border: guarantees legibility across all 11 themes while the italic still communicates "nothing to review right now."
  • Scrollback-doubling suppression window now covers all full-clear shapes, not just CSI 2J. The 0.6.22 fix armed the 250 ms suppression window only on eraseInDisplay mode 2; a ninth-audit probe found that CSI H; CSI 0J and CSI N;M H; CSI 1J (with (N,M) at the bottom-right corner) produce the same post-state — every visible cell cleared — and therefore exhibit the same doubling bug on subsequent redraw. The window is now armed for all three equivalent-effect shapes. Added two regression scenarios to tests/features/scrollback_redraw/ (identical-repaint-0J and identical-repaint-1J) that reproduce the bug pre-fix and pass post-fix. Spec §Contract updated to document the broader invariant.

Changed

  • Packaging files caught up to 0.6.24. The 0.6.23 release tag shipped with CMakeLists.txt at 0.6.23 but the openSUSE spec, Arch PKGBUILD, Debian changelog, and man page still reading 0.6.22 — the exact drift the packaging-version-drift audit rule (0.6.22) was written to catch. Rule wasn't run on the 0.6.23 release commit. Also added org.ants.Terminal.metainfo.xml release entries for 0.6.23 and 0.6.24 (AppStream metadata is not currently checked by the audit rule — follow-up item).

0.6.23

Theme: status-bar event-driven contract + three user-reported UX fixes (allowlist subsumption false-positive, tab-colour picker had no visible effect, Shift+Enter paste-image regression) and the first 0.7.0 performance item — SIMD VT-parser scan.

Fixed

  • Allowlist "already covered" check no longer blocks legitimate compound rules. User-reported via screenshot: clicking Add to allowlist for Bash(make * | tail * && ctest * | tail *) opened the dialog with the right compound rule, but a pink warning said *"Already covered by existing rule: Bash(make )" and refused to add it — so the next compound invocation re-prompted. Root cause: ClaudeAllowlistDialog::ruleSubsumes did a flat narrowInner.startsWith(broadPrefix + " ") check, which was true for any compound command whose first segment matched. Fix: split the narrow rule on shell splitters (&&, ||, ;, |) and require every segment to start with the broad prefix before declaring subsumption. Matches the per-segment semantics generalizeRule has used since 0.6.22. A compound broad rule deliberately returns false rather than risk a false positive — a duplicate rule is benign, a false-positive warning is not. ruleSubsumes promoted from private to public so the feature test can exercise the contract directly. Feature test in tests/features/allowlist_add/ adds 16 cases including the exact reproduction + simple-prefix regression guards + all four splitters. See tests/features/allowlist_add/spec.md §C.
  • Right-click tab → choose colour now renders visibly. The context-menu's "Red / Green / Blue / …" picker stored the choice in m_tabColors[idx] and called setTabTextColor, but the window-level stylesheet rule QTabBar::tab { color: %6; } beat setTabTextColor on QSS specificity, so the colour was stored and never rendered. Fix: new ColoredTabBar subclass (storage via QTabBar::tabData — survives drag-reorder and auto-drops on close, unlike an index-keyed map) + a trivial ColoredTabWidget wrapper that installs it at construction (QTabWidget::setTabBar is protected). paintEvent overlays a 3-px bottom strip in the chosen colour after the base class paints, outside stylesheet influence. MainWindow's context-menu handler now calls m_coloredTabBar->setTabColor(idx, color) and the stale m_tabColors map is removed. New feature test tests/features/tab_color/ asserts round-trip through setTabColor/tabColor, colour-follows-tab across moveTab (drag reorder), and cleanup on removeTab with no index leak into a newly-inserted tab. See tests/features/tab_color/spec.md.
  • Status bar now clears event-scoped transients on tab switch. User-stated contract: the status bar is event-driven — state / location widgets (git branch, foreground process, Claude state, context %, Review Changes button) are always visible and reflect the active tab; transient notifications carry a timeout; widgets tied to a specific event are visible only while the event is live on its originating tab. MainWindow::onTabChanged now calls clearStatusMessage(), hides m_claudeErrorLabel, and deleteLater's every QPushButton on the status bar with objectName == "claudeAllowBtn" at the top of the handler — before state-bar refresh (updateStatusBar, refreshReviewButton, setShellPid) paints the new tab's state. Without the cleanup, switching from "tab A asked permission" to tab B left the Allow button visible inviting approval of the wrong prompt, and theme-change notifications from tab A lingered for five seconds on tab B. Contract codified in tests/features/claude_status_bar/spec.md §D (new section).
  • Shift+Enter no longer pastes the clipboard image. User-reported: with a picture on the clipboard, Shift+Enter inserted the image (saved to disk, filepath pasted) instead of the literal newline. The keypress was being routed through the Kitty keyboard-protocol encoder when kittyKeyFlags() > 0, which emits CSI 13;2u for Shift+Enter; some TUIs (Claude Code v2.1+ among them) treat that sequence as "paste clipboard" when an image is present. Fix: move the Shift+Enter literal-newline block to fire before the Kitty encoder so the sequence Ants puts on the wire is always \x16\n (readline quoted-insert + LF) regardless of the negotiated Kitty flags. Ctrl+Shift+Enter's scratchpad continues to take precedence; the new guard !(mods & Qt::ControlModifier) makes that explicit. src/terminalwidget.cpp keyPressEvent reorder.

Added

  • SIMD VT-parser scan (0.7.0 Performance ROADMAP item). The Ground state now fast-paths runs of printable-ASCII bytes (0x20 ..0x7E) via a 16-byte-wide SIMD lane, avoiding per-byte state-machine work. Implementation in src/vtparser.cpp uses SSE2 on x86_64 and NEON on ARM64 with a scalar tail / fallback; a signed-compare trick (XOR 0x80 → two cmpgt_epi8s against pre-computed bounds) flags any byte outside the safe range in one movemask + ctz. TUI repaints (the workload that drove the 0.6.22 scrollback-doubling fix) are the biggest winner — thousands of printable bytes at a time now go through a tight SIMD loop instead of four function calls per byte. New feature test tests/features/vtparser_simd_scan/ asserts the emitted action stream is byte-identical across whole-buffer, byte-by-byte, and pseudo-random-chunk feed strategies over a 38-case corpus including every offset-mod-16 alignment for an interesting byte (regression guard against scan-boundary off-by-ones). See tests/features/vtparser_simd_scan/spec.md.

0.6.22

Theme: root-cause fix for the main-screen TUI "scrollback doubling" regression, plus packaging catch-up (four recipes bumped to 0.6.22, three missing AppStream <release> entries, openSUSE %post/%postun scriptlets, Debian postinst/postrm). The scrollback fix is the headline — Claude Code v2.1+ repaints its entire TUI on every state update (not just /compact), and each repaint was pushing a full-screen worth of duplicate content into scrollback. 0.6.21's scrollback-insert pause only triggered while the user was scrolled up; this release adds a sliding time window anchored to main-screen CSI 2J, so the suppression works at the bottom too.

Fixed

  • "Review Changes" button is now state-reactive. User-reported: clicking the button sometimes produced a "No changes detected" toast and did nothing else, because the button was shown unconditionally on every fileChanged signal without checking whether a diff actually existed by the time the user clicked. Three-state design: git diff --quiet HEAD (async, off the UI thread) determines (a) hidden — no active terminal, no cwd, or cwd is not a git repo; (b) visible + disabled — clean git repo (Claude edited something but diff is gone); (c) visible + enabled — real diff present. Re-checked on fileChanged, on tab switch, and after the diff viewer finds empty output.
  • Add-to-Allowlist rule generalisation now covers every shell splitter. generalizeRule() previously only handled cd X && cmd (and even that dropped the cd prefix, producing rules that Claude Code's allowlist matcher rejected on the next compound invocation). User feedback: the "Add to allowlist" button would add a rule that re-prompted on the very next turn. Rewritten to split on &&, ||, ;, | while preserving compound structure: Bash(cd /x && make y) now generalises to Bash(cd * && make *) (matching the convention already in the user's settings.local.json), Bash(cat f | grep x) to Bash(cat * | grep *), etc. Safety denylist retained and extended to ALL segments (any rm/bare-sudo/SUDO_* anywhere in the chain blocks generalisation). Feature test in tests/features/allowlist_add/ exercises 23 cases covering every splitter variant and the denylist.
  • Claude Code status bar no longer shows stale state after tab switch. User-reported: "Claude status indicator doesn't work half the time." Root cause: ClaudeIntegration::setShellPid() re-pointed the watcher to the new tab's shell PID but left the cached m_state / m_currentTool / m_contextPercent from the previous tab intact until the next poll tick (~1 s later). Tab A's "Claude: thinking..." bled into tab B's status until polling caught up. Fix: when setShellPid() receives a PID different from the current one, immediately clear the cached state, drop the transcript watch, and emit stateChanged(NotRunning, "") + contextUpdated(0) so the UI reflects the switch within the current event-loop iteration. Paired with a new feature test (tests/features/claude_status_bar/spec.md) that verified the fix both ways — fails against the un-fixed setShellPid, passes with the one-line state-clear.
  • Visual artifacts from leaked SGR decorations. User-reported: TUI apps (Claude Code v2.1+ is the motivating case) leave strikethrough / plain-single underline active across rows, producing horizontal dashed lines on otherwise-blank rows and leaked decorations on pending task-list items. Two-layer fix: (a) render-side filter skips strikethrough and plain single-color underline draws on empty-glyph cells (space / null codepoint) — the attribute is still recorded, just not painted where it has no glyph to decorate, so "underlined text with a space in the middle" still draws correctly per-glyph; (b) SGR 8 / 28 (conceal / reveal) and SGR 53 / 55 (overline / no-overline) now have explicit handlers instead of falling through to the default branch silently. Hovered-URL underline hint always draws regardless of cell content. See tests/features/sgr_attribute_reset/spec.md for the invariant asserted by CTest.
  • Scrollback no longer accumulates duplicates during main-screen TUI repaints. On main-screen full-display erase (CSI 2J, eraseInDisplay(2)), open a 250 ms sliding window during which scrollUp() drops the push-to-scrollback step. Each scrollUp during the window extends it, so the window survives the entire repaint burst regardless of length, but closes promptly when the app goes quiet. Alt-screen bypasses scrollback already and is unaffected; mode-3 erase (which clears scrollback by request) is also unaffected. Defensively cleared on alt-screen entry. The 0.6.21 `m_scrollOffset

    0pause (for users actively reading history) is retained — the two triggers compose (either one suppresses). File:terminalgrid.cpp eraseInDisplay()+scrollUp(); header additions in terminalgrid.h (m_csiClearRedrawActive, m_csiClearRedrawTimer, kCsiClearRedrawWindowMs`).

Added — Audit tooling

  • Trivy filesystem-scan lane in Project Audit. New "Trivy Filesystem Scan" check runs trivy fs with the vuln, secret, and misconfig scanners in one invocation, severity-floor MEDIUM. Output is piped through jq into the standard path:line: severity: scanner/rule-id: title shape so parseFindings() consumes it directly. Self-disables when either trivy or jq is missing (the description text explains how to install). Auto-selected when both are present.
  • Three new audit rules. cmake_no_version_floor (find_package REQUIRED with no version constraint), bash_c_non_literal (bash -c <ident> injection-sink heuristic, restricted to lines containing bash / sh so it doesn't flag grep -c), and packaging_version_drift (cross-file consistency: extracts CMakeLists.txt project(VERSION X.Y.Z) and diffs it against every packaging recipe). Both regex-based rules ship with bad/good fixture pairs under tests/audit_fixtures/; the drift check is exercised by the audit dialog itself.

Added — Test infrastructure

  • Feature-conformance test harness (tests/features/). New test lane alongside the existing audit_rule_fixtures. Each feature subdirectory ships a spec.md (human contract — reviewed like code) and a C++ test executable that exercises the feature through its public API and asserts the contract holds. GUI-free, fast, labelled features so CI and local ctest -L features pick them up. First test landed: scrollback_redraw — the regression guard for this release's headline fix. Verified that it FAILS against 0.6.21 code (growth=77 vs. threshold=34, reproducing the doubling) and PASSES against 0.6.22 — i.e. it would have caught the incomplete 0.6.21 fix at commit time. See tests/features/README.md for the authoring guide and rationale. CLAUDE.md updated.

Changed — Packaging

  • openSUSE spec, Arch PKGBUILD, Debian changelog, AppStream metainfo all bumped to 0.6.22. Prior releases had drifted: spec and PKGBUILD still showed 0.6.20, metainfo's most recent <release> was 0.6.17 (so 0.6.18/.19/.20/.21 were invisible to GNOME Software / KDE Discover users reading the metainfo catalogue).
  • openSUSE spec gets %post/%postun scriptlets (update-desktop- database, gtk-update-icon-cache). Without these, minimal Tumbleweed images won't see the launcher in the application menu until next session restart. Matches Fedora/openSUSE packaging guidelines for any package shipping a .desktop entry + hicolor icons.

0.6.21

Theme: audit-tool noise reduction, dialog theming, and a long- standing main-screen-TUI scrollback corruption fix. Self-audit triage turned up a GNU grep 3.12 argument-ordering quirk that silently disabled the --include file-type filter, producing the bulk of the CRITICAL/MAJOR false positives. Same release extends the QSS cascade so every popup dialog inherits the terminal's active theme, and adds a scrollback-insert pause that stops TUI redraws from interleaving intermediate frames into the history the user is trying to read.

Changed

  • Scroll-lock on scrollback read (main-screen TUI fix). When a TUI program runs on the main screen (Claude Code v2.1+ is the flagship example — it renders its approval prompts and context bar via cursor movement + overwrite, not alt-screen) and the user has scrolled up to read history, every cursor-up-and-rewrite cycle that extends past the scroll region used to push the overwritten top line into scrollback. With a redraw-heavy TUI that fires many frames per second (spinner, context growth, expanding tool blocks), this interleaved duplicate frames into the scrollback the user was reading — the user's perceived position stayed stable (viewport anchor shifts with scrollbackPushed), but the history below their read point filled with duplicated content and the "scrolled" view drifted. Fix: TerminalGrid::scrollUp() now honours a new scrollbackInsertPaused flag, set by TerminalWidget::onPtyData() while m_scrollOffset > 0. Lines that would have been pushed into scrollback during a paused window are simply dropped (they were about to be overwritten in-place anyway). Flag auto-clears on the next PTY packet once the user returns to the bottom.
  • Dialog theming cascade. MainWindow::applyTheme() now ships a comprehensive QSS covering QDialog, QLabel, QPushButton, QLineEdit, QTextEdit / QPlainTextEdit / QTextBrowser, QCheckBox, QRadioButton, QComboBox, QSpinBox / QDoubleSpinBox, QGroupBox, QListWidget / QTreeWidget / QTableWidget, QHeaderView, QScrollBar (v+h), QToolTip, QProgressBar, and QDialogButtonBox. Every pop-up created with the main window as parent (Settings, Audit, AI, SSH, Claude Projects/Transcript/Allowlist, homograph-link warning, permission prompts, QMessageBox, QInputDialog, QFileDialog, QColorDialog, …) now paints with the terminal's active theme colors — no more light-gray system defaults leaking through. Cached dialogs are re-polished on theme switch so live widgets pick up new colors without re-instantiation.

Fixed

  • Audit: grep argument-ordering bug (major noise source). GNU grep 3.12 silently drops the --include=<glob> filter whenever a file-level --exclude=<name> appears earlier on the command line — every file under the search root is scanned regardless of extension. Our addGrepCheck() builder put kGrepExclSec (which carried --exclude=auditdialog.cpp --exclude=auditdialog.h) before kGrepIncludeSource, so security scans that should have been scoped to source files were also matching .md, .xml, CMakeLists.txt, and the audit tool's own pattern-definition strings. Split the exclude-dir and file-exclude portions, append file-excludes AFTER all --include flags (including caller-supplied extras). Cuts the CRITICAL cmd_injection false-positive count from 8 → 4 (remaining hits are either real — execlp in the forkpty child, by design — or intentional test fixtures).
  • Audit: lineIsCode() over-counted string-only lines as code. The comment/string-aware filter flagged continuation lines like "system(), popen()…", "Security", as real code because the string-delimiter character itself was treated as a code token. Tightened to require an identifier- or operator-class character outside strings/comments before a line counts as code. Drops the remaining self-referential matches in auditdialog.cpp's own pattern-definition arguments without affecting legitimate mixed code+string lines like int n = f("foo");.
  • Audit: cppcheck invalidSuppression noise. cppcheck's own --inline-suppr parser misreads documentation comments that mention the literal cppcheck-suppress token (e.g. the passthrough-marker docs in auditdialog.cpp) as actual suppression directives and emits invalidSuppression errors. Added --suppress=invalidSuppression to both cppcheck invocations — the category catches only meta-parsing failures, never real code bugs, so losing it has no downside. Also restructured the offending docs (auditdialog.{h,cpp}) so the token no longer appears as the first word of a comment body.

0.6.20

Theme: ROADMAP §H5 (Distribution-readiness bundle 5) — ready-to-submit packaging recipes for the three mainstream non-Flatpak distros. Each recipe drives the existing CMake install rules (no build-system forks, no per-distro patches) and runs the audit-rule regression suite in its %check / check() / dh_auto_test step. Fully additive, no runtime code changes.

Added

  • packaging/opensuse/ants-terminal.spec — openSUSE RPM spec targeting Tumbleweed. Uses the core openSUSE CMake macros (%cmake, %cmake_build, %cmake_install, %ctest, %autosetup) so the file is close to portable to Fedora. BuildRequires declared via cmake(Qt6*) pkgconfig-style entries so OBS resolves them against whichever Qt6 stack the target project carries. %files enumerates all fifteen install paths explicitly so a missing or relocated artefact fails the OBS build instead of producing a silently-incomplete package.
  • packaging/archlinux/PKGBUILD — AUR recipe on the release track (package name ants-terminal). check() runs ctest. sha256sums is SKIP in the in-tree recipe with a comment pointing packagers at updpkgsums since the upstream tarball doesn't exist until the tag is pushed. A rolling -git variant is documented in packaging/README.md (three-line diff: pkgname, source, pkgver()).
  • packaging/debian/ — Debian source tree: control, rules, changelog, copyright, source/format. debhelper-compat 13 drives dh $@ --buildsystem=cmake with Ninja as the backend; DEP-5 copyright carries the full MIT license text. DEB_BUILD_MAINT_OPTIONS = hardening=+all stacks dpkg-buildflags' hardening wrappers on top of our CMake hardening flags. Suitable for debuild -uc -us, a Launchpad PPA, or an eventual Debian ITP.
  • packaging/README.md — one-page build / submission guide for all three recipes: local-build recipe, OBS / AUR / PPA submission flow, dch / osc vc / updpkgsums version-bump recipes, and the -git variant diff for Arch.

Changed

  • CMakeLists.txt project(... VERSION 0.6.19) → 0.6.20. The single-source-of-truth macro ANTS_VERSION propagates to setApplicationVersion, the SARIF driver, the dialog title badge, and the HTML-export metadata on rebuild.
  • packaging/linux/ants-terminal.1 .TH line now reads ants-terminal 0.6.20 (was stuck at 0.6.18 — missed the 0.6.19 bump). groff ships the version string in its header/footer, so man ants-terminal now matches ants-terminal --version again.
  • README current-version line bumped to 0.6.20. The Install footprint table didn't need to change — H5 is pure packaging metadata, not new installed files.
  • ROADMAP §H5 status 📋 → ✅ in both the distribution-adoption overview table and the §0.8.0 📦 narrative section, with a link back to this CHANGELOG entry. H1–H5 distribution slice now five bundles deep; remaining 0.8.0 packaging work is H6 (Flatpak) and H7 (docs site).

0.6.19

Theme: ROADMAP §H4 (Distribution-readiness bundle 4 of 4) — shell completions for the current QCommandLineParser surface, installed to the conventional vendor locations for bash, zsh, and fish so distro packages don't have to relocate them. Closes the H1–H4 distribution slice; remaining packaging work (H5 spec/PKGBUILD/debian, H6 Flatpak, H7 docs site) lives in 0.8.0+. Fully additive, no runtime code changes.

Added

  • packaging/completions/ants-terminal.bash — bash-completion function _ants_terminal registered via complete -F. Suggests the full long/short option set; suppresses suggestions after --new-plugin (the next token is a freeform plugin name with no useful enumeration).
  • packaging/completions/_ants-terminal — zsh #compdef function using _arguments, with mutually-exclusive groups so --quake and --dropdown only offer the unspecified alias, and -h / -v short-circuit further completion ((- *) exclusion).
  • packaging/completions/ants-terminal.fish — fish complete declarations, one per flag, with descriptions that mirror the manpage one-liners. --new-plugin declared -r -x so fish knows it consumes the next token but doesn't try to file-complete it.
  • CMake install rules for all three files at the GNU vendor locations: ${datarootdir}/bash-completion/completions/ants-terminal, ${datarootdir}/zsh/site-functions/_ants-terminal, and ${datarootdir}/fish/vendor_completions.d/ants-terminal.fish. All three are auto-discovered without the user sourcing anything by hand.
  • CI lint job — .github/workflows/ci.yml runs bash -n on the bash file, zsh -n on the zsh file, and fish --no-execute on the fish file so syntax regressions fail the build the same way the H2 AppStream / desktop-entry validators and H3 groff lint do.

Changed

  • README Install System-wide table gains three rows for the completion files so the install footprint stays self-documenting.
  • ROADMAP §H4 status 📋 → ✅ in both the distribution-adoption overview table and the §0.7.0 📦 narrative section, with links back to this CHANGELOG entry. Closes out the H1–H4 distribution slice.

0.6.18

Theme: ROADMAP §H3 (Distribution-readiness bundle 3 of 4) — a spec-compliant man 1 ants-terminal page so distro packagers and users alike get the standard Unix discovery experience (man ants-terminal, apropos terminal, whatis ants-terminal). Fully additive, no runtime code changes.

Added

  • packaging/linux/ants-terminal.1 — groff -man source covering NAME / SYNOPSIS / DESCRIPTION / OPTIONS (-h/--help, -v/--version, --quake/--dropdown, and --new-plugin <name> with its full validation contract) / ENVIRONMENT (SHELL, HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, ANTS_PLUGIN_DEV, QT_QPA_PLATFORM) / FILES (config / themes / plugins / sessions / recordings / logs / audit_rules.json / .audit_suppress) / EXIT STATUS (the four --new-plugin codes) / BUGS / AUTHORS / SEE ALSO (cross-refs to xterm, konsole, gnome-terminal, tmux, ssh, forkpty(3), appstreamcli, desktop-file-validate). Renders cleanly under both groff -man and man -l.
  • CMake install rule for the man page — install(FILES … DESTINATION ${CMAKE_INSTALL_MANDIR}/man1). DESTDIR-staged install verified to drop the page at …/share/man/man1/ants-terminal.1, which man-db picks up automatically once the prefix is on MANPATH (true for /usr and /usr/local out of the box).
  • CI lint job — .github/workflows/ci.yml installs groff and runs `groff -man -Tutf8 -wall packaging/linux/ants-terminal.1

    /dev/nullso syntax regressions in the man source fail the build the same wayappstreamcli/desktop-file-validate` do for the desktop entry and metainfo XML.

Changed

  • README Install System-wide table gains a row for the man page so the install footprint stays self-documenting alongside the binary, desktop entry, metainfo, and icons.
  • ROADMAP §H3 status 📋 → ✅ in both the distribution-adoption overview table and the §0.7.0 📦 narrative section, with links back to this CHANGELOG entry.

0.6.17

Theme: ROADMAP §H2 (Distribution-readiness bundle 2 of 4) — AppStream metainfo and a spec-compliant desktop entry so Ants appears in GNOME Software / KDE Discover catalogues once a distro packages it. Fully additive, no runtime code changes.

Added

  • packaging/linux/org.ants.Terminal.metainfo.xml — AppStream 1.0 descriptor: id / name / summary / multi-paragraph description / categories / keywords / url (homepage, bugtracker, vcs, help, contribute) / provides/binary / supports/control / content_rating (OARS 1.1, all-none) / launchable tying to the .desktop id / releases with 0.6.14– 0.6.17 changelog summaries. metadata_license CC0-1.0, project_license MIT. Validated by appstreamcli validate (pedantic clean aside from the reverse-DNS uppercase hint, which matches the convention used by org.gnome.Terminal and org.kde.Konsole).
  • packaging/linux/org.ants.Terminal.desktop — reverse-DNS app id so it round-trips with the metainfo launchable. Fields: Type=Application, Terminal=false, Categories=Qt;System;Terminal- Emulator; (exactly one main category — avoids the multi-listing menu hint), Keywords= tightened, StartupWMClass=ants-terminal, TryExec=ants-terminal. Two Desktop Action entries — NewWindow and QuakeMode (wires --quake) — for right-click launcher integration in most DEs. Validated by desktop-file-validate clean.
  • CMake install rules (include(GNUInstallDirs)) for the desktop file (share/applications/), metainfo XML (share/metainfo/), and the six hicolor icons renamed to ants-terminal.png under share/icons/hicolor/<size>x<size>/apps/. DESTDIR=… staging works out of the box for distro build roots.
  • CI validation job — .github/workflows/ci.yml installs appstream + desktop-file-utils and runs appstreamcli validate --explain + desktop-file-validate on every push so schema drift in the packaging files fails the build instead of landing in a release tarball.

Changed

  • README Install System-wide section replaces the bare make install with cmake --install build, adds a table of every path the install rule lays down (bin/ desktop/ metainfo/ hicolor icons), and notes DESTDIR=… support for packagers.
  • README Desktop Entry section split into two paths: the dev workflow (ants-terminal.desktop.in + launch.sh for running uninstalled from the build tree) and the distro workflow (the new packaging/linux/ files installed via CMake). Dev path unchanged; packaged path is new.
  • README Project Structure map mentions packaging/linux/.
  • ROADMAP §H2 status 📋 → ✅ in both the distribution-adoption overview table and the §0.7.0 📦 narrative section, with links to this CHANGELOG entry.

0.6.16

Theme: ROADMAP §H1 (Distribution-readiness bundle 1 of 4) — community and security policy docs that distro packaging teams look for before accepting a package. Fully additive, docs-only.

Added

  • CODE_OF_CONDUCT.md — Contributor Covenant 2.1 verbatim with two reporting channels: a dedicated maintainer email and the private GitHub Security Advisory (tagged [conduct] for triage) for reporters who prefer GitHub's private-report flow. Email is listed first per CoC convention — no GitHub account required. Ships ROADMAP §H1.
  • SECURITY.md — coordinated-disclosure policy: supported-versions table, reporting channels (GitHub Security Advisory preferred; signed encrypted email for reporters who need it), 48h / 7d / 30d / 90d disclosure timeline, severity rubric (Critical / High / Medium / Low), in-scope / out-of-scope lists, and an acknowledgement of the hardening already in the tree (image-bomb budgets, URI scheme allowlist, plugin sandbox, OSC 52 quotas, multi-line paste confirmation, compile-time hardening flags, ASan + UBSan CI). Completes ROADMAP §H1. (The file itself landed in commit 4599813; this release changelogs it alongside the CoC.)
  • Distribution-adoption plan in ROADMAP.md — new 📦 theme plus a rollup section covering bundles H1–H16 (security/CoC, AppStream + desktop, man page, shell completions, distro packaging for openSUSE / Arch / Debian, Flatpak, docs site, macOS, accessibility, i18n, reproducible builds, SBOM, distro submissions, deb/rpm signing). Nothing ships from this plan yet except H1; it exists so later releases have a single source of truth for packaging work.

Changed

  • ROADMAP transition: §H1 status 📋 → ✅ in both the distribution-adoption overview table and the §0.7.0 📦 narrative section, with links to this CHANGELOG entry.
  • README's Contributing section now links CONTRIBUTING.md, CODE_OF_CONDUCT.md, and SECURITY.md before the step-by-step fork/PR instructions, so first-time contributors see the policy docs before they see the mechanics.
  • CONTRIBUTING.md intro references the CoC (participation agreement) and SECURITY.md (private channel for sensitive reports) so the rules aren't buried.

0.6.15

Theme: the incremental reflow optimization from ROADMAP §0.7.0 Performance. Resizing a terminal with a long scrollback used to re-join every line into a logical-line buffer and re-wrap every logical line back into TermLines, allocating scratch vectors and copying cells cell-by-cell — O(scrollback) allocation churn on every width change. This release adds a fast path for the common case: standalone lines (not part of a soft-wrap sequence) whose content fits the new width get resized in place, skipping join/rewrap entirely.

Changed

  • Incremental scrollback reflow in TerminalGrid::resize(). The new algorithm walks scrollback once:
    • Fast path (standalone line, softWrapped == false, not mid- sequence, content width ≤ new cols): tl.cells.resize(cols) with default-attr padding when growing, or trim trailing blanks + drop combining-char entries at now-removed columns when shrinking. No allocation of scratch TermLines. No cell-by-cell copy.
    • Slow path (soft-wrap sequence OR standalone that doesn't fit): the existing joinLogical + rewrap round-trip, unchanged. This preserves correctness for trailing-space trim, combining-char position merging, and wide-character boundary handling. Typical scrollback is dominated by standalone lines ≤ the new width, so the fast path handles most rows. Multi-line soft-wrap sequences (long commands that wrapped at the prompt's edge) still round-trip through the full logic so nothing regresses there.
  • ROADMAP transition: §0.7.0 → ⚡ Performance → "Incremental reflow on resize" 💭→✅ with a link to this CHANGELOG section. See ROADMAP.md.

Notes

  • No behavior change on correctness. The fast path only applies when the line's logical-line representation would be a single TermLine identical (up to trailing blanks) to the one we produce by direct resize. For every case the slow path is still reachable via the existing code.
  • Screen-line reflow is unchanged. Screen rows are already small-N (typically ≤ 50) so the O(N) savings aren't worth the added branch complexity; the joinLogical/rewrap loop runs on m_screenLines as before.
  • Alt-screen and scrollback-hyperlink reflow paths unchanged. Alt-screen is explicitly simple copy today; scrollback hyperlink spans are not re-wrapped at all (pre-existing behavior) — both outside the scope of this optimization.
  • No plugin API change, no config schema change, no UI change. PLUGINS.md does not need a bump.

0.6.14

Theme: small security follow-up to 0.6.13 — catch a URI-scheme injection vector in the new make_hyperlink trigger action that self-review after shipping turned up. No ROADMAP transition; this is a hardening patch on an already-shipped feature.

Security

  • URI scheme allowlist in TerminalGrid::addRowHyperlink(). The make_hyperlink trigger action expands $0..$9 backrefs from the matched PTY output into the URL template — and PTY output is attacker-controllable (SSH sessions, remote shells, any inner-tty process). Without a scheme check, an innocent-looking template like https://example.com/$1 could be weaponized when the pattern matches an attacker-supplied string that expands to javascript:alert(1) or data:text/html,.... The new check mirrors the existing OSC 8 allowlist (http, https, ftp, file, mailto); hyperlinks with any other scheme are silently dropped, matching OSC 8's drop-on-bad-scheme semantics — text still prints, it just isn't clickable.

Notes

  • Same allowlist used by OSC 8 remote hyperlink parsing in handleOsc(); kept in lockstep so both sources share the same threat model.
  • No config schema change, no plugin API change, no UI change.
  • QSet added to terminalgrid.cpp includes for the allowlist storage.

0.6.13

Theme: close out the last deferred bullet from the 0.6.9 trigger-system bundle — the iTerm2 HighlightLine, HighlightText, and MakeHyperlink actions, which needed grid-cell mutation surgery that chunk-level matching (checkTriggers) couldn't provide. This release adds a new line-completion callback on TerminalGrid that fires from newLine() with the row the cursor is leaving, and TerminalWidget runs the grid-mutation subset of trigger rules against the finalized line text there — so matches map to exact col ranges on a real row, and mutations land before the row scrolls into scrollback.

Added

  • highlight_line trigger action — when the pattern matches in finalized output, recolors the entire line the match landed on. Action Value format: "#fg" (fg only), "#fg/#bg" (both), or "/#bg" (bg only). Empty-string slots mean "leave unchanged" — so partial overrides compose cleanly with the line's existing SGR attributes.
  • highlight_text trigger action — same color-string format as highlight_line, but recolors only the matched substring using the regex's capture-0 span as the col range.
  • make_hyperlink trigger action — turns the matched substring into a clickable OSC 8-equivalent hyperlink. Action Value is a URL template with $0..$9 backrefs expanded against regex capture groups ($0 = whole match, $1..$9 = groups). Example: pattern #(\d+) + template https://github.com/milnet01/ants-terminal/issues/$1 → a clickable issue link on every #123 reference in output. Matches iTerm2's MakeHyperlink contract.
  • TerminalGrid::setLineCompletionCallback(…) — new public API. Fired from newLine() before any cursor/scroll motion with the screen row the cursor was leaving. Used by TerminalWidget to run grid-mutation trigger rules; any other consumer can hook in for per-line post-processing (e.g., future trigger-system actions that need line-level context).
  • TerminalGrid::applyRowAttrs(row, startCol, endCol, fg, bg) — overrides per-cell fg/bg on an inclusive col range of a screen row. Invalid QColor() leaves that channel untouched so callers can write fg-only, bg-only, or both. Marks the row dirty for re-paint.
  • TerminalGrid::addRowHyperlink(row, startCol, endCol, uri) — pushes a hyperlink span onto the screen row's per-line m_screenHyperlinks vector with the same shape the OSC 8 parser uses. Auto-grows the vector if the row index is past the current end. Rows scrolling into scrollback preserve the span via the existing scrollUp move-semantics path.
  • TerminalWidget::onGridLineCompleted(int screenRow) — dispatch handler installed as the grid's line-completion callback. Iterates rules, filters to the three new grid-mutation types, runs globalMatch() on the finalized line text, and invokes the appropriate applyRowAttrs / addRowHyperlink for each match (so multiple hits on one line all apply).

Changed

  • Settings → Triggers dropdown now exposes all action types. The hardcoded {"notify", "sound", "command"} list was hiding the bell / inject / run_script types shipped in 0.6.9 from the UI; 0.6.13 adds those plus the three new grid-mutation types so every trigger rule supported by the backend is reachable from Settings. The trigger-tab description was also expanded to document the Action Value format for each type.
  • TerminalWidget::checkTriggers() now skips the three new grid-mutation types — those dispatch through onGridLineCompleted instead. Routing them through both paths would double-fire and match on raw PTY byte chunks (which don't map to grid cells) instead of finalized line text.
  • ROADMAP transition: §0.7.0 → 🔌 Plugins trigger system — the "HighlightLine / HighlightText / MakeHyperlink actions from the iTerm2 reference are deferred" caveat on the 0.6.9 bullet is now resolved; the bullet is marked fully complete.

Notes

  • Grid-mutation triggers fire on line completion only — i.e., when newLine() is called after a \n. The instant flag is not honored for these three types (it wouldn't make sense — the mid- line text isn't final, and half-matched mutations would flash then get overwritten). Dispatch types (notify/sound/etc.) still honor instant via the existing chunk-level matcher.
  • No config schema change. Same {pattern, action_type, action_value, instant, enabled} rule shape from 0.6.9; the new types just teach both checkTriggers and onGridLineCompleted how to interpret action_type and action_value.
  • Multi-match support — highlight_text and make_hyperlink both use globalMatch() so every occurrence on a line is decorated (not just the first).
  • No plugin API surface change. PLUGINS.md does not need a bump; trigger rules live in config, not in the ants.* API.

0.6.12

Theme: close out the semantic-history ROADMAP item (which was already implemented but never promoted on the roadmap) and round out its editor coverage so the line/column jump works for editors beyond just VS Code and Kate.

Added

  • Editor jump support for nvim, vim, sublime, JetBrains IDEs, helix, and micro in TerminalWidget::openFileAtPath(). Ctrl-click on a path:line:col capture in scrollback (compiler / linter / stack-trace output) now opens the file at the cited location for:
    • VS Code family: code, code-insiders, codium, vscodium via --goto <path>:<line>:<col>
    • Kate: -l <line> + -c <col> (col was previously ignored)
    • Sublime / Helix / Micro: <path>:<line>:<col> argv shape
    • Vi family: nvim, vim, vi, ex via +<line> <path>
    • Nano: +<line>,<col> <path>
    • JetBrains IDEs: idea, pycharm, clion, goland, webstorm, rider, rubymine, phpstorm, datagrip via --line <line> + --column <col>
    • Anything else: best-effort (open the path, no jump). Users can wrap their editor in a small shell script if it isn't on the list yet. Editor name match uses QFileInfo(editor).fileName() so absolute paths (/usr/local/bin/code) and bare names (code) both work.

Changed

  • ROADMAP transition: §0.7.0 → 🎨 Features → "Semantic history" 💭→✅. The OSC 7-less implementation was already shipped (uses /proc/<pid>/cwd to resolve relative paths); this release just documents the existing behavior and broadens the editor list. See CHANGELOG.md §0.6.12.

Notes

  • No new config keys, no new permissions, no plugin API change. PLUGINS.md does not need a bump.
  • The path-detection regex is unchanged from 0.6.x — the existing (?:^|[\s("'])((\.{0,2}/)?[a-zA-Z0-9_\-./]+\.[a-zA-Z0-9_]+(?::(\d+)(?::(\d+))?)?) pattern in detectUrls() already captures path:line:col and was the basis for the original semantic-history routing.
  • Line jump is suppressed when the captured line is 0 (unparseable) so editors don't see a bogus +0 / --line 0 arg.

0.6.11

Theme: ship the 0.7.0 security pair — a UI for auditing and revoking plugin capabilities, plus image-bomb defenses for the inline graphics decoders. Both items were the smallest, lowest-risk slice of the remaining 0.7.0 work; bundling them keeps the security theme closing out before the riskier threading/platform work begins.

Added

  • Plugin capability audit UI. Settings → Plugins lists every discovered plugin with its name, version, author, description, and the full set of permissions declared in its manifest.json. Each permission renders as a checkbox whose initial state reflects the current grant in config.plugin_grants[<name>]. Unchecking + Apply persists the revocation; takes effect at the next plugin reload (matches the existing first-load permission prompt's grant semantics). When no plugins are loaded — either Lua is not compiled in or the plugin directory is empty — the tab shows a guidance message pointing at PLUGINS.md. Permission descriptions match the first-load dialog wording so users see the same language across both surfaces. Closes ROADMAP.md §0.7.0 → "Plugin capability audit UI".
  • Image-bomb defenses. New ImageBudget class (private to TerminalGrid) tracks total decoded image bytes across every inline-display entry (Sixel + Kitty T/p + iTerm2 OSC 1337) and the Kitty graphics cache. Cap = 256 MB per terminal — when a decode would push the total past the cap, the decoder rejects the payload up front (Sixel) or post-decode (Kitty PNG / iTerm2 OSC 1337 — those don't carry pre-decode dimensions in the params block) and surfaces an inline error in the warning color ([ants: <kind> WxH (NN MB) exceeds 256 MB image budget]). Every eviction site (inline-image FIFO drain, Kitty cache eviction, Kitty delete-by-id, Kitty delete-all) releases bytes back to the budget. Alt-screen swap calls recomputeImageBudget() so the counter stays accurate when the active and saved containers cross paths. Existing per-image dimension cap (MAX_IMAGE_DIM = 4096) remains in force — ROADMAP specified 16384 as the upper bound but our existing 4096 is stricter. Closes ROADMAP.md §0.7.0 → "Image-bomb defenses".
  • SettingsDialog::PluginDisplay — lightweight POD struct decoupling the Settings dialog from pluginmanager.h (and from the Lua-gated build chain). MainWindow translates each PluginInfo into this form before handing the list to the dialog via setPlugins(), so the Plugins tab compiles and runs even without Lua support.
  • TerminalGrid::writeInlineError(QString) — emits a short red ASCII diagnostic line at the cursor (CR+LF after) for surfacing image-bomb rejections without the disruption of a desktop notification. Color matches the command-block status stripe red.
  • TerminalGrid::recomputeImageBudget() — bulk-recompute helper for the alt-screen swap path where individual add/release tracking would be brittle. O(N) over the inline + cache vectors.

Changed

  • SettingsDialog constructor now wires a "Plugins" tab between "Profiles" and the OK/Cancel button row. applySettings() collects checked permissions per plugin and calls Config::setPluginGrants(name, granted) for every plugin in the audit list — including the "all unchecked" case so revocation persists as an empty grant array.
  • MainWindow::settingsAction lambda now gathers the current plugin snapshot from m_pluginManager->plugins() (or an empty list when ANTS_LUA_PLUGINS is undefined) and calls setPlugins() on the dialog every time it opens — so hot-reloads / new installs are reflected without recreating the dialog.
  • TerminalGrid Sixel / Kitty / iTerm2 decode paths and every image-eviction site now consult / update the per-terminal ImageBudget. RIS (ESC c) replaces the entire TerminalGrid via *this = TerminalGrid(...), which naturally resets the budget alongside the containers; no special-case code needed.
  • ROADMAP transitions: "Plugin capability audit UI" 📋→✅ and "Image-bomb defenses" 📋→✅ in §0.7.0 → 🔒 Security, both linked back to this CHANGELOG section.

Notes

  • Plugin permissions storage on disk is unchanged — same config.plugin_grants[<name>] = [...] shape from manifest v2 (0.6.0). The new UI is additive: it reads what the first-load permission prompt already wrote and exposes it for editing.
  • Image budget is conservative. Same QImage stored in both m_inlineImages and m_kittyImages is counted twice even though Qt's COW means the underlying bitmap is allocated once. We may reject earlier than strictly necessary; we never reject too late.
  • No plugin API surface changed — PLUGINS.md does not need a bump. The audit UI sits entirely outside the ants.* API.
  • No new permissions were introduced; the descriptions for clipboard.write, settings, and net mirror the first-load dialog text in mainwindow.cpp.

0.6.10

Theme: ship the command-block UI bundle from 0.7.0 §Features (shell integration). OSC 133 prompt regions already carried the data; this release wires the UI that makes blocks first-class citizens — per-block context menu, pass/fail status stripe, asciicast export of a single block. Also formalizes asciinema recording (the recorder itself has shipped — it's now marked ✅ on the roadmap).

Added

  • Command-block context menu. Right-click inside an OSC 133 prompt region surfaces block-scoped actions without requiring a selection: Copy Command (extracts just the command text via the OSC 133 B cursor column so PS1 is stripped), Copy Output (extracts the range between the C marker and the next region), Re-run Command (writes the captured command + \r to the PTY — bypasses paste confirmation since it's your own history, not external input), Fold / Unfold Output (toggles the existing fold triangle for this specific region rather than the cursor region), and Share Block as .cast… (exports one block to a standalone asciicast v2 file). The menu header shows Command Block (exit N) when the block has finished so users can see pass/fail before acting. Actions are gated on block state — Re-run requires commandEndMs > 0, Copy Output requires hasOutput, and so on.
  • Per-block exit-code status stripe. A 2px vertical bar painted on the far-left gutter of every finished prompt line — green for exit_code == 0, red otherwise. Unlike the fold triangle it shows on the most recent finished block too (no successor region required). Gives at-a-glance scannability of long scrollback for pass/fail.
  • exportBlockAsCast(int, QString) — writes a standalone asciicast v2 file with a single command block's output. Header = current grid dimensions + commandStartMs timestamp. Event stream = two records: the command echo at t=0.0, and the captured output at t=duration (synthesized; we don't have per-byte timing for historical blocks). Players replay as a prompt followed by the output dump — honest about what we recorded. Shared via the Share Block as .cast… context menu action; file extension .cast per the asciicast v2 spec.
  • Block-text extraction helpers on TerminalWidget — promptRegionIndexAtLine(int), commandTextAt(int), outputTextAt(int), rerunCommandAt(int), toggleFoldAt(int). Public so plugin code and future UI (marketplace, block sharing service) can consume the same semantics.

Changed

  • PromptRegion now carries three new fields — exitCode (stored in the OSC 133 D handler alongside m_lastExitCode; lets block UI paint per-region pass/fail instead of relying on the grid-global most-recent value), commandStartCol (cursor column at OSC 133 B fire — lets commandTextAt strip PS1 from the extracted command), and outputStartLine (global line where OSC 133 C fired, so output extraction doesn't guess based on endLine + 1). All additive; existing consumers unaffected.
  • ROADMAP 0.7.0 §Features. "Command blocks as first-class UI" and "Asciinema recording (.cast v2)" move from 📋 (planned) to ✅ (shipped in 0.6.10). Four bullets consolidated under the command-block item — navigation (Ctrl+Shift+Up/Down) was already wired in an earlier release, metadata display (duration
    • timestamp + exit stripe) completes here, per-block menu ships here, asciinema recorder was already implemented (now marked shipped). The "suppress output noise" sub-bullet from the original 0.7.0 design was deferred — it's ill-defined without a noise heuristic and not worth blocking the bundle on. Semantic history (Cmd-click on path:line:col) and OSC 133 HMAC remain as future work.

Notes

  • No plugin API changes in this release. PLUGINS.md not bumped.
  • No new permission gates. The context menu is client-side UI; rerunCommandAt uses the same m_pty->write path the user's own keystrokes use.
  • Multi-line wrapped commands are captured correctly — commandTextAt follows the region from endLine through outputStartLine - 1 (or the next region's startLine - 1 when no output was captured), joining lines with \n. Trailing-space padding is stripped per line.

0.6.9

Theme: ship the trigger system bundle from 0.7.0 §Plugins — four items that share LuaEngine / PluginManager / OSC-dispatch plumbing and are cleaner to land together than to drip-feed across four releases. Sets up shell-aware plugin workflows for 0.7.

Added

  • Trigger system extensions. TriggerRule JSON schema gains an instant boolean (iTerm2 convention — true fires on every PTY chunk for in-progress matches like password prompts; false defaults to firing only on newline-terminated chunks so settled output doesn't double-fire mid-line) and three new action_type values: bell (alias for sound — explicit per iTerm2's vocabulary), inject (writes action_value directly to the focused PTY — useful for "yes\n" auto-confirm rules), and run_script (broadcasts to plugins as a palette_action event with payload "<action_id>\t<matched_substring>"). Existing notify / sound / command actions unchanged.
  • OSC 1337 SetUserVar channel. Parses ESC ] 1337 ; SetUserVar=NAME=<base64-value> ST (iTerm2 / WezTerm convention) and fires a user_var_changed plugin event with payload "NAME=value". Disambiguated from inline images by the byte after 1337; (S for SetUserVar, F for File). NAME capped at 128 chars, decoded value capped at 4 KiB — this is a status channel, not a transport. Lets a shell hook pipe state (git branch, k8s context, virtualenv) into plugins without prompt-text scraping.
  • ants.palette.register({title, action, hotkey}). Lua API for plugins to inject Ctrl+Shift+P palette entries. title and action required; hotkey optional (when set, registers a global QShortcut that fires the same action). Entries appear as "<plugin>: <title>" to keep the palette scannable across plugins. Triggering an entry fires a palette_action event with action as payload, scoped to the registering plugin only — broadcast events use a different payload format ("<id>\t<match>" from run_script triggers).
  • Five new plugin events:
    • command_finished — fires on OSC 133 D; payload "exit_code=N&duration_ms=N" (URL-form so plugins can split on & without escaping).
    • pane_focused — fires on tab switch; payload = tab title. Today fires on tab changes; will cover within-tab pane focus when split- pane focus tracking lands.
    • theme_changed — fires after applyTheme() completes; payload = theme name. Lets plugins swap palette/icon assets to match.
    • window_config_reloaded — fires after Settings → Apply; payload empty (plugins re-read settings via ants.settings.get on demand).
    • user_var_changed — fires on OSC 1337 SetUserVar (see above).
    • palette_action — fires when a registered palette entry is triggered, or when a run_script trigger matches.
  • TerminalGrid callbacks. New setCommandFinishedCallback, setUserVarCallback. Wired by TerminalWidget to corresponding Qt signals (commandFinished, userVarChanged, triggerRunScript) which MainWindow forwards into PluginManager::fireEvent.

Changed

  • Command palette is now dynamic. MainWindow::rebuildCommandPalette() collects menu actions and plugin-registered entries on every change (plugin load/reload/unload, individual register calls). On pluginsReloaded signal, all plugin entries are torn down so a removed plugin's entries don't survive across reloads — init.lua re-runs on reload and re-registers anything that should still appear.
  • PLUGINS.md — documents the six new events, the palette API, the instant flag, the new trigger action types. The 0.7 roadmap section in PLUGINS.md is updated to reflect what's now shipped vs what remains.
  • ROADMAP.md — moves all four trigger-system items from 📋 to ✅ in 0.7.0 §Plugins. Remaining 0.7.0 work is shell-integration UI (command blocks, asciinema), performance (SIMD VT-parser scan, decoupled threads), platform (Wayland Quake mode), and security (plugin capability audit UI, image-bomb defenses).

Notes

  • The trigger system's HighlightLine / HighlightText / MakeHyperlink actions from the original ROADMAP item are deferred — they require grid-cell mutation (per-cell color overrides + OSC 8 hyperlink injection from outside the parser) which is a bigger surgery than the dispatch-level actions shipped here. The actions shipped (notify, sound/bell, command, inject, run_script, PostNotification via notify) cover the iTerm2 trigger doc's most-used cases. Grid-mutation actions are tracked as a follow-up.
  • pane_focused fires on tab switches today; once split-pane focus tracking lands, the same event covers within-tab pane changes — plugin handlers don't need to change.

0.6.8

Theme: close the two remaining Qt-specific audit rules from 0.7.0 §Dev experience. With these in place, the entire 0.7.0 "Project Audit tool" lane is shipped — every motivating case surfaced by the 0.6.5 audit pass now has automated coverage.

Added

  • Audit rule unbounded_callback_payloads. Same-line regex flags PTY / OSC / IPC byte buffers forwarded straight into a user-supplied *Callback(…) without a length cap (.left() / .truncate() / .mid() / .chopped() / .chop()). Motivating case: pre-0.6.5 OSC 9 / OSC 777 notification body shovelled the entire escape payload (potentially MB) into the desktop notifier, which then crashed the notification daemon and/or amplified a malformed-OSC DoS. Fix shipped in 0.6.5; the rule prevents the regression. Severity Major. terminalgrid.cpp is the canonical safe call site (filtered at runtime by .left(kMaxNotifyBody)); rule produces zero findings on our tree.
  • Audit rule qnetworkreply_no_abort. Detects the dangerous shape from the pre-0.6.5 AiDialog incident: a 3-arg connect() to a QNetworkReply signal whose third argument is a bare lambda. With no context object, Qt cannot auto-disconnect when the lambda's captured this is destroyed — owner closed mid-flight → reply fires later → use-after-free. Pattern enforces the 4-arg form (sender, signal, context, slot), which Qt protects via auto-disconnect on context destruction. Severity Major. Covers readyRead, finished, errorOccurred, sslErrors. Single-line only — multi-line connect formatting is a known false-negative (rare in practice). Ants's own AiDialog and AuditDialog use the safe 4-arg shape and are not flagged.
  • Fixtures for both new rules. bad.cpp with three @expect markers each, good.cpp with the corresponding safe shapes that must not match the regex. Wired through audit_self_test.sh so CI catches regressions on every push (now 14 rules, 46 total checks pass).

Changed

  • ROADMAP — moved the two remaining 0.7.0 §Dev experience audit-rule items (unbounded_callback_payloads, qnetworkreply_no_abort) from 📋 to ✅. The Project Audit tool lane in 0.7.0 is now fully shipped; remaining 0.7.0 work is features (command blocks, asciinema), the trigger system, and the performance / platform / security items.

0.6.7

Theme: close the remaining grep-shaped items from 0.7.0 §Dev experience in one sweep, plus fix the CI workflow file which — it turns out — had never parsed (unquoted colon in a step name rejected the entire YAML).

Added

  • Audit rule silent_catch. Flags empty same-line catch (...) {} handlers that swallow exceptions without logging or rethrow. Conservative first cut (same-line empty body only); extending to multi-line / single- statement trivial bodies needs grep -Pzo plumbing and is deferred.
  • Audit rule missing_build_flags. Nudges projects toward better compile-time coverage by flagging absence of -Wformat=2, -Wshadow, -Wnull-dereference, and -Wconversion in the top-level CMakeLists.txt. Strips comment-only lines before matching so a CMakeLists that discusses a flag in a comment (e.g. why it's disabled) doesn't cause a false negative. Severity Minor — this is a nudge, not a bug. Correctly reports one finding on our own tree: -Wconversion, which is intentionally off (noisy in combination with Qt), documented in CMakeLists.txt.
  • Audit rule no_ci. Detects projects missing CI configuration (.github/workflows/, .gitlab-ci.yml, .circleci/, .travis.yml, Jenkinsfile). Severity Major — regressions ship silently without a CI safety net.
  • Sanitizer CI job (build-asan). New parallel GitHub Actions job builds with -DANTS_SANITIZERS=ON (ASan + UBSan), runs ctest under sanitizers, and smoke-tests the binary (--version, --help) with QT_QPA_PLATFORM=offscreen so initialization-path bugs surface on every push. detect_leaks=0 for now — Qt global singletons on fast-exit paths look like leaks to LSan; re-enable once we have a real unit-test suite exercising full app lifecycle.
  • CONTRIBUTING.md. Short actionable guide derived from STANDARDS.md: build modes, where tests live, step-by-step for adding an audit rule, version-bump checklist, commit conventions, what-not-to-send.
  • Fixtures for silent_catch — bad.cpp with three @expect markers, good.cpp with logging/rethrow/return-with-value catch bodies that must not match.

Fixed

  • CI workflow was never parsing. name: Run cppcheck (audit rule: Qt-aware static analysis) had an unquoted : inside the scalar, which GitHub Actions' YAML parser rejected as "mapping values are not allowed here" — the workflow file was dropped entirely and no jobs ran for 0.6.5 or 0.6.6. Fixed by double-quoting the step name. The parse error was silent on GitHub's side (workflow-file runs showed 0 jobs with a one-line "workflow file issue" notice); caught here by running PyYAML's strict parser over the file locally.

Changed

  • ROADMAP — moved four items from 0.7.0 §Dev experience to 0.6.7 shipped (silent-catch rule, build-flag recommender, no-CI check, sanitizer-in-ctest hookup, CONTRIBUTING.md).

0.6.6

Theme: close the first Dev-experience item queued in 0.7 after the 0.6.5 audit. One new audit rule + matching CI enforcement, backfill the two fixtures it immediately surfaced.

Added

  • Audit self-check: fixture-per-addGrepCheck. New audit_fixture_coverage rule in the Project Audit dialog enumerates every addGrepCheck("id", …) call in src/auditdialog.cpp, dedups by id, and reports any id without a matching tests/audit_fixtures/<id>/ directory. Catches the exact gap we hit in 0.6.5 — four rules shipped a cycle without regression fixtures. Silent no-op on projects that don't follow this convention (grep yields no ids). Output uses the parseFindings-friendly file:line: message shape so findings link directly to the registration site.
  • CI-enforced fixture-coverage cross-check in tests/audit_self_test.sh. Belt-and-suspenders: the runtime check surfaces findings to the dev on next audit run; the test-harness assertion blocks merges in CI when a new rule id lacks either a fixture dir OR a run_rule line in the script. Mirrors the runtime check exactly. Added to ctest output as PASS/FAIL: fixture-coverage <id>.
  • Fixture coverage for memory_patterns and qt_openurl_unchecked — the two real gaps the new rule just surfaced. Each gets bad.cpp with @expect markers and a good.cpp that avoids matching tokens (including in comments — the pattern is case-sensitive and line-based, so comment wording had to use UPPER-CASE or non-literal phrasing).

Changed

  • ROADMAP — moved the "Self-consistency: fixture-per-addGrepCheck" item from 0.7.0 §Dev experience to 0.6.6 shipped.

0.6.5

Theme: second audit of the day. Ran the 5-phase prompt (/mnt/Storage/Scripts/audit_prompt.md) against the post-0.6.4 tree. Five subagent reports converged on three real High findings, three Medium, and a batch of noise. Five subagent claims were rejected on Phase 4 verification (documented in the commit body) — keeping the signal honest matters more than the count.

Security

  • Cap OSC 9 / OSC 777 desktop-notification payloads at 1024 bytes (title at 256). The VT parser caps an OSC payload at 10 MB so inline-image escapes round-trip cleanly; handleOsc() was forwarding that whole payload to the notification callback as a QString. A program inside the terminal could spam the freedesktop notification daemon with multi-MB titles. src/terminalgrid.cpp:761-790 now .left(N)-clamps before the callback, matching the OSC 52 clipboard-cap pattern.
  • Cap accumulated SSE response buffer in AiDialog at 10 MB. m_sseLineBuffer already had this cap on the pipe side (src/aidialog.cpp:179); m_streamBuffer — which holds parsed content — did not. A misbehaving or hostile endpoint could stream past max_tokens indefinitely. Once capped, we append a single [response truncated] marker and drop subsequent chunks.
  • AiDialog now has a destructor that aborts any in-flight reply. Qt auto-disconnects signals from a destroyed receiver, but there's a narrow window during member destruction where a reply can still fire readyRead / finished on a partially-destructed AiDialog. Explicit abort() + deleteLater() closes the window.

Changed

  • Stricter compile flags. Added -Wformat=2, -Wshadow=local, and -Wnull-dereference to the default warning set. -Wshadow=local was picked over the full -Wshadow deliberately: the project uses Qt-idiomatic void setData(QByteArray data) patterns where parameter names legitimately match member accessors; flagging those would drown the real int x = …; if (cond) { int x = …; } shadowing that the flag exists to catch. Surfaced two legitimate local-vs-local shadows (mainwindow.cpp:1170 and auditdialog.cpp:3062), both renamed.
  • Silent catch (…) blocks in OSC handlers now route to the existing DBGLOG channel. Three sites in src/terminalgrid.cpp (OSC 133 D, OSC 9;4, OSC 1337 param parse) write a diagnostic line when their std::stoi throws, guarded by m_debugLog so there's zero cost when debug isn't enabled. The two Kitty-graphics safeStoi/safeStoul helpers stay intentionally silent — documented with a comment — because they fire per-chunk in the APC parameter loop and logging would flood.

Added

  • .github/workflows/ci.yml. Runs ctest plus the cppcheck Qt-aware pass on every push to main and every PR. actions/checkout pinned by 40-char SHA (not tag) per STANDARDS.md §Supply chain. Workflow token scoped to contents: read.
  • Four new audit-rule regression fixtures under tests/audit_fixtures/: todo_scan, format_string, hardcoded_ips, weak_crypto. These four rules ship in auditdialog.cpp but had no bad/good fixture pair, so regex drift would have shipped silently. tests/audit_self_test.sh now covers 9 of the 9 unconditional addGrepCheck() rules; ctest exercises 27 assertions (up from 23).
  • launch.sh now starts with set -eu. Three-line wrapper, but free hardening against silent failures if a typo lands in a future edit.
  • -Wl cert-err33-c fix. src/ptyhandler.cpp:112's argv0 ::snprintf return value was being ignored; now (void)-cast with a comment explaining the truncation is a known-acceptable loss (the written buffer is a login-marker, not a lookup key).

0.6.4

Theme: Project Audit signal-to-noise. Addresses a reviewer-provided brief after a 2026-04-14 audit run surfaced 26 BLOCKER findings that were all false positives from ASCII section underlines in a vendored single-header library, and 30 MAJOR world-writable-file findings that were a FUSE-mount artefact.

Fixed

  • Selection auto-scroll direction was inverted. Dragging a selection past the bottom edge of the viewport scrolled up into older scrollback instead of revealing newer lines below (and symmetrically at the top edge). The auto-scroll timer in TerminalWidget applied m_scrollOffset - delta with a signed direction, which inverted the intent; flipped to + delta so the direction convention now matches wheelEvent / smoothScrollStep — +offset reveals older content, -offset reveals newer.

  • Conflict-marker regex no longer matches ASCII underlines. The conflict_markers rule required exactly 7 sigil chars at start-of-line but had no trailing anchor, so =========== (and longer banners in vendored headers) still matched. Tightened to ^(<{7}|\|{7}|={7}|>{7})(\s|$). Also added |{7} for the diff3-style merge-base marker that the old pattern missed entirely. A conflict_markers self-test fixture now carries eight ASCII-underline canaries in good.cpp so regressions are caught by the existing CTest driver.

  • World-writable check on non-POSIX mounts. On filesystems that don't enforce POSIX permission bits (FUSE, NTFS, vfat/exfat, CIFS, 9p) every file reports as world-writable because the mount maps all Unix perms to 0777. AuditDialog now probes stat -f -c %T <project> at construction; when the result is in a known non-POSIX list the file_perms check is replaced with an INFO note explaining the skip, instead of producing ~every file in the tree as a finding.

Changed

  • Default exclude list broadened. kFindExcl and kGrepExcl now exclude external/ and third_party/ in addition to the existing vendor/, build/, node_modules/, .cache/, dist/, .git/, .audit_cache/. Vendored code is not project-maintained, so findings there aren't actionable and used to drown out real signal.

  • Category headers surface multi-tool corroboration. The HTML and plain-text renders now append (N corroborated) to each category header when one or more of its findings are flagged by ≥2 distinct tools on the same file:line — the same signal that drives the ★ badge and boosts the confidence score. Lets the downstream consumer prioritise triage at a glance instead of having to scan every finding's inline tags.

  • Confidence legend in the plain-text header. The conf N and ★ tags were opaque without documentation. A four-line legend now sits in the handoff header explaining what the score means and how corroboration feeds into it.

Added

  • 5-phase workflow scaffold in the Claude Review handoff. The /tmp/ants-audit-*.txt export used by the "Review with Claude" button now carries a BASELINE → VERIFY → CITATIONS → APPROVAL → IMPLEMENT+TEST prompt between the project-docs block and the findings list. Stops the downstream session from plunging straight into fixes without verifying findings are real, cross-checking CVE version ranges, or committing a regression test per fix. Aligns with the project's existing no-workarounds rule (documented in CLAUDE.md).

0.6.3

Theme: Claude Code status responsiveness. Replaces the 2-second transcript poll with an inotify-driven event pipeline; state transitions now surface in ~50ms instead of up to 2s, with lower steady-state CPU.

Changed

  • Transcript state updates are event-driven. ClaudeIntegration already had a QFileSystemWatcher on the active transcript but bypassed it with an unconditional per-poll parse ("QFileSystemWatcher can miss rapid changes on Linux"). That was over-defensive — QFSW's only real miss modes are atomic rename-over (already handled by the re-add in parseTranscriptForState) and full file replacement (now handled by a slow backstop). The watcher's fileChanged signal now drives a 50ms single-shot debounce timer that coalesces streaming-output bursts (during an assistant turn Claude writes dozens of JSONL lines per second) into at most ~20 parses/sec. Net effect: "Claude: thinking…/compacting…/idle" transitions flip in ~50ms instead of up to 2000ms, and the CPU no longer re-parses 32KB of JSON every 2 seconds when nothing has changed.

  • Process-presence poll kept at 2s, with a 20s transcript backstop. Polling /proc for claude-code starting/stopping under our shell has no clean event equivalent (we're not the direct parent, and netlink proc-connector needs elevated caps), but it's cheap — ~40 bytes read per cycle. The poll now also re-parses the transcript once every 10 cycles (~20s) as a defensive net for the rare file-replaced case where QFSW loses its watch; the re-parse also re-arms the watch via the existing addPath-if-missing check.

0.6.2

Theme: Claude Code status readability. Adds a dedicated "compacting" state so the status bar doesn't flatten a multi-second /compact into generic "thinking…".

Added

  • "Claude: compacting…" status. New ClaudeState::Compacting surfaces when a /compact is in flight, rendered in magenta so it's visually distinct from idle (green), thinking (blue), and tool use (yellow). Detection is transcript-driven: the parser walks the existing 32KB tail window looking for a user event whose string content contains <command-name>/compact</command-name>, and only fires while no subsequent isCompactSummary:true user event (the condensed-history marker Claude Code writes when compaction finishes) has appeared. The PreCompact hook path now routes through the same state, so terminals that have the hook server wired up get the same indicator without the 2-second poll latency.

0.6.1

Theme: scroll-anchor correctness. One-liner fix for a long-standing drift bug that only surfaced with a full scrollback buffer.

Fixed

  • Scroll anchor drifts when scrollback is at its cap. While scrolled up reading history, bursts of new output (e.g. Claude Code redrawing its prompt / spinner) appeared to "redraw text at the current history position" — actually the pinned viewport was sliding by one content-line per pushed line. Root cause: the anchor math in onPtyData diffed scrollbackSize() before/after parsing, but once the buffer hits scrollback_lines (default 50k) each push also pops a stale line from the front, so the size delta is always zero and the anchor thinks nothing happened. TerminalGrid now exposes a monotonic scrollbackPushed() counter that keeps incrementing through the cap; TerminalWidget diffs that instead. Below-cap behavior is unchanged.

0.6.0

Theme: make the two features that already make Ants distinctive (plugins + audit) production-grade. Ships the full context polish + plugin v2 arc from the 0.6 roadmap — scrollback regex search, OSC 9;4 progress, multi-line paste confirmation, OSC 8 homograph warning, LRU glyph-atlas eviction, cell-level dirty tracking, per-plugin Lua VMs, manifest v2 with declarative permissions + capability-gated APIs, plugin keybindings, and hot reload.

Added

🎨 Features
  • Scrollback regex search. Ctrl+Shift+F search bar gains three toggle buttons: .* (regex mode via QRegularExpression), Aa (match case), and Ab (whole word, wraps the pattern in \b…\b). Alt+R / Alt+C / Alt+W flip toggles without leaving the input. Invalid regex patterns render the input with a red border and show the error in the match- counter tooltip (!/!). Zero-width matches (\b, ^) are detected and skipped to avoid infinite loops. Matches Kitty / iTerm2 / WezTerm behavior; closes the Ghostty 1.3 gap.
  • OSC 9;4 progress reporting. Parses ConEmu / Microsoft Terminal ESC]9;4;state;percent ST sequences. State 0 clears, 1 shows a normal blue progress bar, 2 shows a red error bar, 3 shows an indeterminate (full-width) lavender bar, 4 shows a yellow warning bar. Renders as a 3-pixel strip along the bottom edge of the terminal (above the scrollbar) and as a small colored dot next to the tab title. Coexists with the existing OSC 9 desktop-notification handler via payload disambiguation (9;4;… vs. 9;<body>). Spec: MS Terminal progress sequences.
  • Click-to-move-cursor on shell prompts (previously in code, now documented). When the cursor is in an OSC 133 prompt region with no output yet, clicking on the same line sends the appropriate run of ESC [ C / ESC [ D arrow sequences to reposition the cursor. Capped at 200 arrows to guard against runaway sends.
⚡ Performance
  • LRU glyph-atlas eviction. Replaces the old "clear everything on overflow" behavior with a warm-half retention policy: entries touched within the last 300 frames are kept, colder ones are re-rasterized into a fresh atlas. Median-based fallback handles cold-boot overflow. Eliminates the paint stall on long ligature-heavy sessions. See glrenderer.cpp:compactAtlas.
  • Per-line dirty tracking. TermLine gains a dirty flag set by every grid mutation (print, erase, scroll, alt-screen swap, resize). TerminalWidget::invalidateSpanCaches() now does targeted eviction — only URL / highlight caches for dirty lines get dropped, not the whole map. Big win on high-throughput output that only touches a few lines. (Full cell-level render-path partial-update is deferred; the dirty primitive is in place for that work.)
🔒 Security
  • Multi-line paste confirmation. Dialog appears when the pasted payload contains a newline, the string sudo , a curl/wget/fetch piped into sh/bash/python/etc., or any non-TAB/LF/CR control character. Policy is independent of bracketed paste — iTerm2 / Microsoft Terminal got bitten by conflating the two (MS Terminal #13014). Config key: confirm_multiline_paste (default true). UI: Settings → Behavior → "Confirm multi-line / dangerous pastes".
  • Per-terminal OSC 52 write quota. 32 writes/min + 1 MB/min per grid, independent of the existing 1 MB per-string cap. Protects against drip-feed exfiltration.
  • OSC 8 homograph warning. When an OSC 8 hyperlink's visible label encodes a hostname-looking token (github.com) that doesn't match the actual URL host (github.evil.example.com), a confirm dialog shows both and requires explicit opt-in. Strips leading www. for fair comparison.
🔌 Plugins — manifest v2 + capability model
  • Per-plugin Lua VMs. Each plugin gets its own lua_State, heap budget (10 MB), and instruction hook (10 M ops). One VM stalling or leaking globals can no longer affect others. PluginManager owns a map of (name → LuaEngine *) and fans out events to each in turn.
  • Declarative permissions in manifest.json. Plugins declare a "permissions": ["clipboard.write", "settings"] array. On first load, a permission dialog lists each requested capability and lets the user accept / un-check individual grants. Grants persist in config.json under plugin_grants; subsequent loads don't re-prompt unless the manifest requests a new permission. Un-granted permissions surface as missing API functions (not nil stubs) so plugins can feature-detect with if ants.clipboard then … end.
  • ants.clipboard.write(text) — gated by "clipboard.write" permission. Writes to the system clipboard via QApplication.
  • ants.settings.get(key) / ants.settings.set(key, value) — gated by "settings" permission. Per-plugin key/value store, persisted under plugin_settings.<plugin_name>.<key> in config.json. Manifest can declare a "settings_schema" JSON-Schema subset (plumbing in place; schema-driven Settings UI is a follow-up item).
  • Plugin keybinding registration via manifest "keybindings" block: {"my-action": "Ctrl+Shift+K"}. Firing the shortcut dispatches a keybinding event to the owning plugin with the action id as the payload. ants.on("keybinding", function(id) ... end) listens. The shortcut lives on the MainWindow; invalid sequences log a warning.
  • Plugin hot reload via QFileSystemWatcher. Watches init.lua, manifest.json, and the plugin directory itself; on change, debounced 150 ms, the plugin VM is torn down (fires unload event) and re-initialized (fires load event). Enabled only when ANTS_PLUGIN_DEV=1 — idle cost is zero otherwise.
  • ants._version exposes ANTS_VERSION string so plugins can feature-detect without hardcoding.
  • ants._plugin_name exposes the plugin's declared manifest name.
  • New events: load, unload (lifecycle), keybinding (manifest shortcuts). Added to eventFromString() in luaengine.cpp.
🧰 Dev experience
  • ants-terminal --new-plugin <name> CLI. Creates a plugin skeleton at ~/.config/ants-terminal/plugins/<name>/ with init.lua, manifest.json (empty permissions array), and README.md templates. Validates the name against [A-Za-z][A-Za-z0-9_-]{0,63}.
  • ANTS_PLUGIN_DEV=1 env var enables verbose plugin logging on scan/load, plus hot reload. Cached per-process via a static.
  • --help / --version flags wired through QCommandLineParser (previously --version set via setApplicationVersion but no CLI handler existed).

Changed

  • README.md — navbar now links to PLUGINS.md, ROADMAP.md, and CHANGELOG.md. Plugins section points to PLUGINS.md as the source of truth; keeps a quick-start summary table inline.
  • CLAUDE.md, STANDARDS.md, RULES.md — cross-linked to the new docs; plugin-system sections defer to PLUGINS.md for the author contract and document internal invariants separately.
  • PluginManager no longer exposes a single engine() accessor; callers use engineFor(name) to get the per-plugin VM. Event delivery fans out over all loaded engines.
  • Paste path now runs through TerminalWidget::confirmDangerousPaste before the bracketed-paste wrap — confirmation policy is independent of PTY-side behavior.

0.5.0

Added

Project Audit — context-aware upgrade (this release's headline feature). The audit pipeline now collects and applies context at every stage, closing the gap with commercial tools (CodeQL, Semgrep, SonarQube, DeepSource).

  • Inline suppression directives. Findings can be silenced directly in source via // ants-audit: disable[=rule-id] (same line), disable-next-line, and disable-file (first 20 lines). Foreign markers are also honored for cross-tool compatibility: clang-tidy NOLINT / NOLINTNEXTLINE, cppcheck cppcheck-suppress, flake8 noqa, bandit nosec, semgrep nosemgrep, gitleaks #gitleaks:allow, eslint eslint-disable-*, pylint pylint: disable. Rule-list parsing and glob matching (google-*) are supported.
  • Generated-file auto-skip. Findings in moc_*, ui_*, qrc_*, *.pb.cc/*.pb.h, *.grpc.pb.*, flex/bison outputs, /generated/ paths, and *_generated.* files are dropped before rendering.
  • Path-based rules in audit_rules.json. A new path_rules[] block takes entries of {glob, skip, severity_shift, skip_rules[]} to configure per-directory severity shifts and skips (e.g. tests/** shifts everything down one severity band; third_party/** skips entirely). Glob syntax supports **, *, ?.
  • Code snippets (±3 lines) attached to every file:line finding, cached per-file, rendered with the finding line highlighted. Exposed via SARIF physicalLocation.contextRegion, embedded in the HTML report's expandable [details] panel, and included in the Claude review handoff.
  • Git blame enrichment. Author, author date, and short SHA shown next to every finding. Cached per (file, line) with a 2 s timeout; auto-disabled for non-git projects. Exported via SARIF properties.blame bag (sarif-tools de-facto convention).
  • Confidence score 0-100. Replaces the binary highConfidence flag with a weighted sum: severity × 15 + 20 cross-tool agreement + 10 external AST tool − 20 test-path, clamped, adjusted by explicit AI verdicts. Displayed as a coloured pip in the UI; exported in SARIF (properties.confidence) and HTML.
  • Severity pill filter + live text filter in the dialog, matching the HTML export's UX. Filters across file name, message, rule id, and author (via blame). Re-renders instantly.
  • Sort by confidence toggle reorders every check's findings by confidence descending — the "which should I look at first" view.
  • Collapsible [details] per finding. Click to reveal snippet context and an inline "🧠 Triage with AI" action. Expanded state persists across re-renders.
  • Semgrep integration (optional). When semgrep is available, a Security-category check runs p/security-audit plus language packs (p/c, p/cpp, p/python, p/javascript, p/typescript). Picks up project-local .semgrep.yml automatically. Complements clazy's Qt-aware AST lane.
  • AI triage per finding. Click "🧠 Triage with AI" inside an expanded finding to POST rule + message + snippet + blame to the project's configured OpenAI-compatible endpoint. Response parsed as {verdict, confidence, reasoning}; verdict badge rendered inline, confidence score adjusted accordingly. Opt-in per click, respects Config::aiEnabled.
  • Version stamp on all audit outputs. Dialog title, dialog types label, SARIF driver.version, HTML report meta line, and plain-text Claude handoff now all carry ants-audit v<PROJECT_VERSION> pulled from a single CMake add_compile_definitions(ANTS_VERSION=…) source of truth.
  • Single-source-of-truth versioning infrastructure. CMakeLists.txt project(... VERSION 0.5.0) propagates via add_compile_definitions(ANTS_VERSION=…) to every caller. Four previously-hardcoded "0.4.0" literals retired: main.cpp (setApplicationVersion), auditdialog.cpp (SARIF driver), ptyhandler.cpp (TERM_PROGRAM_VERSION env), and claudeintegration.cpp (MCP serverInfo.version).

Changed

  • audit_rules.json schema extended with path_rules[]. Existing rules[] entries load unchanged; loader documents both blocks.
  • SARIF export now emits physicalLocation.contextRegion with the full ±3 snippet for every finding that has a file:line location, plus properties.blame and properties.confidence. properties also carries aiTriage when the user triaged that finding.
  • HTML report template upgraded with confidence pips, blame tags, verdict badges, and per-finding expandable snippet panels. Added CSS classes (.conf-pip, .blame, .verdict, .snippet, .hit). DATA.version now propagated through the inline payload.
  • Plain-text Claude handoff body lines carry [conf N · blame · AI] tags and a 3-line indented snippet per finding. Header gains a Generator: line.
  • dropFindingsInCommentsOrStrings still opt-in per check via the existing kSourceScannedChecks set, but is now augmented by the inline-suppression scan (applied to every finding with a file:line). Pipeline order documented in STANDARDS.md.

Security

  • Inline suppression scan reads files through a bounded per-file cache (4 MB cap) — runaway check on a huge file cannot stall the dialog.
  • Git blame shells out with a 2 s timeout and is auto-disabled when not in a git repository.
  • AI triage respects the project's existing ai_enabled config and refuses non-http/https endpoints.

Tests / Tooling

  • tests/audit_self_test.sh extended with 18 suppression-token assertions covering every honored marker (ants-native, NOLINT/NOLINTNEXTLINE, cppcheck-suppress, noqa, nosec, nosemgrep, gitleaks-allow, eslint-disable-*, pylint: disable) plus negative cases. Total: 23 rule tests passing.
  • ANTS_VERSION compile definition wired via CMake add_compile_definitions. main.cpp and auditdialog.cpp no longer carry hardcoded version strings.

Docs

  • README.md — expanded "Project Audit Dialog" section with examples of inline suppression directives and audit_rules.json schema including path_rules; semgrep added to optional dependencies.
  • CLAUDE.md — pipeline order, confidence formula, generated-file patterns, new design decisions around context-awareness.
  • STANDARDS.md — context-awareness pipeline order invariants, blame cache rules, AI triage config sourcing.
  • RULES.md — three new audit rules covering inline-suppression preference, path_rules vs. hardcoded paths, and "confidence is display-only, not a gate".

0.4.0

Added

Project Audit dialog — full feature arc. Over the course of eight incremental commits, the audit panel evolved from a regex-heavy scanner into a structured, AST-aware, cross-validated tool.

  • SonarQube-style taxonomy — every finding carries a type (Info / CodeSmell / Bug / Hotspot / Vulnerability) and a 5-level severity (Info / Minor / Major / Critical / Blocker).
  • Per-finding dedup keys (SHA-256 of file:line:checkId:title).
  • Baseline diff — save current findings to .audit_cache/baseline.json; later runs highlight only new findings.
  • Trend tracking — severity counts persisted at .audit_cache/trend.json (last 50 runs) and rendered as a delta banner against the previous run.
  • Recent-changes scope — restrict findings to files touched in the last N commits, or (stricter) to exact diff hunk line ranges via git diff --unified=0 HEAD~N.
  • Multi-tool correlation — ★ badge on findings flagged at the same file:line by ≥2 distinct tools.
  • clazy integration — Qt-aware AST checks (connect-3arg-lambda, container-inside-loop, old-style-connect, qt-keywords, etc.) via clazy-standalone, reading the project's compile_commands.json. Retires three FP-prone regex checks.
  • Comment/string-aware filtering — per-check opt-in state-machine scan that drops matches inside //, /* */, or "strings".
  • SARIF v2.1.0 export — OASIS-standard JSON consumed by GitHub Code Scanning, VSCode SARIF Viewer, SonarQube, CodeQL. Includes per-rule catalogue, partialFingerprints, and per-finding properties bag.
  • Single-file HTML report — no external assets, embedded JSON payload + vanilla JS. Severity pills, text filter, collapsible check cards.
  • Interactive suppression — click any dedup hash to prompt for a reason and append to .audit_suppress (JSONL v2: {key, rule, reason, timestamp} per line).
  • User-defined rules via <project>/audit_rules.json. Flat schema mirrors the internal AuditCheck struct. User rules run through the full filter / parse / dedup / suppress pipeline.
  • CTest harness — tests/audit_self_test.sh with per-rule bad.*/good.* fixtures using // @expect <rule-id> markers; count-based assertion.
  • Review with Claude handoff — emits a plain-text report with CLAUDE.md, STANDARDS.md, RULES.md, and CONTRIBUTING.md prepended so Claude can weigh findings against documented rules.

Changed

  • .audit_suppress upgraded from v1 plain-key-per-line to v2 JSONL; v1 still loads, first write converts in place with a migration marker.
  • Audit rule pack format: JSON (audit_rules.json), not YAML — QJsonDocument is built-in; flat schema's readability gap with YAML is small enough that a parser dependency isn't worth it.

Security

  • audit_rules.json is a trust boundary — its command field runs through /bin/bash unconditionally. Documented analogous to .git/hooks: your repo, your commands, no sandbox.

0.3.0

Added

  • Claude Code deep integration: live status bar, project/session browser (Ctrl+Shift+J), permission allowlist editor (Ctrl+Shift+L), transcript viewer, slash-command shortcuts.
  • 12 professional UX features: hot-reload config, column selection, sticky headers, tab renaming/coloring, snippets, auto-profile switching, badge watermarks, dark/light auto-switching, Nerd Font fallback, scrollbar, Ctrl+arrow word movement, scroll anchoring.
  • Session persistence: save/restore scrollback via QDataStream + qCompress binary serialization.
  • AI assistant dialog: OpenAI-compatible chat completions with SSE streaming, configurable endpoint/model.
  • SSH manager: bookmark editor, PTY-based ssh connection.
  • Lua 5.4 plugin system: sandboxed ants API, instruction-count timeout, event-driven handlers.
  • GPU rendering path: QOpenGLWidget with glyph atlas, GLSL 3.3 shaders.

Fixed

  • Six rounds of comprehensive security + correctness audits covering PTY FD leaks, bracketed-paste injection, SIGPIPE, Lua coroutine escape, hardcoded colours, atomic config writes, and more.

0.2.0

Added

  • Full C++ terminal emulator rewrite: custom VT100/xterm state-machine parser, TerminalGrid with scrollback + alt screen, PTY via forkpty, QPainter rendering with QTextLayout ligature shaping.
  • Mouse reporting (SGR format), focus reporting (mode 1004), synchronized output (mode 2026).
  • OSC 8 hyperlinks, OSC 52 clipboard write, OSC 133 shell-integration markers, OSC 9 / 777 desktop notifications.
  • Sixel graphics (DCS), Kitty graphics protocol (APC), iTerm2 images (OSC 1337).
  • Kitty keyboard protocol with progressive enhancement (push / pop / query / set).
  • Combining characters via per-line side table.
  • Command palette (Ctrl+Shift+P), tab management with custom frameless titlebar, split panes via nested QSplitters, 11 built-in themes.

0.1.0

Added

  • Initial release: basic terminal emulator prototype.