Skip to content

Project history

A timeline of releases and notable events. The full record lives in git log and the GitHub release page.

2026

July

Date Event
2026-07-03 v1.8.0: Elixir support (.ex / .exs) via tree-sitter-elixir. defmodule → nested namespaces; def / defp → function / method; defmacro / defguard / defdelegate carry [macro] / [guard] / [delegate] markers; defprotocol → interface, defimpl Name, for: Type → class; defstruct / defexception → one field per key (enforced :atoms and key: default alike); @type / @typep / @opaque → fields, @callback → method; @doc / @moduledoc absorbed as docs (no leak onto the first member), @spec not surfaced; use / import / alias / require → imports (alias MyApp.{A, B} expands per name); ExUnit / Phoenix DSL blocks (describe, test, scope) → named containers. Function clauses of one (keyword, name, arity) collapse to one entry — distinct arities and def-kinds stay separate. Adapter contributed by @atavistock (#7), then integrated, hardened, and corpus-validated on 1,649 files from six major OSS projects (Elixir stdlib, Phoenix, Ecto, Plug, Credo, Phoenix LiveView) — 35,686 declarations, zero crashes. Suite: 2339 tests.
2026-07-02 v1.7.3: Decorated-definition [def] tagging completed for GDScript — the last adapter affected by the start_line-vs-name-line divergence. When annotations (@tool, @export, @rpc(...)) sit on their own line above a declaration, start_line is pulled up so show prints them, which had left grep <Symbol> --kind def and dir-mode show <dir> <Symbol> missing the declaration. The hand-written GDScript parser now records name_line on the real declaration line. Closes an exhaustive sweep of every adapter.
2026-07-02 v1.7.2: Decorated-definition [def] tagging extended to Kotlin, PHP, Swift, Scala, and C++. The same start_line-vs-name-line divergence fixed for Python / C# / Java / TypeScript in 1.7.1 affected every adapter whose grammar folds a leading annotation / attribute / modifier preamble into the declaration node (@Serializable class, #[Attribute] class, @objc class, @deprecated def, [[deprecated]]). Each now records name_line by skipping its language's preamble node(s).
2026-07-02 v1.7.1: Decorated / attributed / annotated definitions are now tagged [def]. The grep def-classifier compared a match's line against start_line, which decorated declarations extend up over their decorator lines — so a decorated class / function matched on its own name classified as ref / call, and grep <Symbol> --kind def / dir-mode show <dir> <Symbol> returned "not found". Declarations now carry a name_line recording the signature line independently; the classifier compares against it. Covers Python, C#, Java, TypeScript (Rust #[derive] was already correct).

June

Date Event
2026-06-30 v1.7.0: Vue Single-File Component support (.vue) — the first composite adapter. Rather than a dedicated grammar, it parses the .vue file once with tree-sitter-html to find the top-level <template> / <script> / <style> sections, then delegates each to a grammar the project already ships: <template> → HTML (CSS-selector tokens, heading previews, bare-container drop/lift, ERROR-node recovery for Vue directives like v-for / @click), <script> / <script setup> → TypeScript (both Composition and Options API; multiple script blocks each parsed), <style> → CSS (rule sets, at-rules, @import → imports). So it adds no new dependency. Declarations from all three sections merge into one flat outline, with byte offsets and line numbers rewritten to the original file so show and grep resolve against .vue line numbers with no remapping. Deliberate exclusions: <script lang="tsx"> uses the plain TS grammar (a safe superset); <style lang="scss"> uses the CSS grammar; custom blocks (<i18n>, <docs>) are ignored. Original adapter contributed by @Xayan. Suite: 2268 tests.
2026-06-11 v1.6.0: The usage-history release — all four changes come from analyzing 960 real Claude Code agent sessions (8,900+ ast-outline invocations plus the surrounding tool calls) and fixing where agents stumbled, fled to raw grep, or invented workarounds. (1) Shebang detection for extensionless files: an explicit file argument with no extension is resolved by its #! line (env/-S unwrapping, VAR=value and flag-argument skipping, version normalization python3.13python; interpreters declared per adapter — python/pypy/uv, node/deno/bun/ts-node/tsx, ruby, lua/luajit, php, swift). Agents had invented a symlink-to-/tmp/x.py protocol for these (177 setups, 537 calls through it). Directory walks still filter by extension — no sniffing cost. (2) show same-directory rescue: the dominant show failure (237 misses) was a right-class-wrong-file guess; a file-target miss now scans the file's own directory (one level) and points — # hint: defined in the same directory: ThingIdGenerator.cs:27-58 (class) — re-run show against it — never printing a body from a file the agent didn't ask for. Candidate pointers everywhere now carry path:start-end ranges. (3) grep shows string literals by default — only comments stay noise-filtered. Strings are program data (translation keys, asset paths, reflection targets, YAML run: \| shell, inline <script>, markdown fences, docstrings); hiding a hit there produced silent false "not used" — the string-hint converted 0 of 14 times and agents ran 417 raw greps over locale files after learning the tool "can't see strings". Hits are tagged [string]. (4) Flag forgiveness: a mistaken invocation with exactly one sensible reading runs that reading with a # note: (outline --format=namesdigest; outline --signature → ignored; symbol-less showoutline; grep -r/-n → no-ops). One repair attempt only; JSON mode keeps the strict envelope. Suite: 2219 tests.
2026-06-11 v1.5.0: GDScript support (.gd) — the first adapter with a hand-written parser instead of tree-sitter (no maintained tree-sitter-gdscript wheel exists on PyPI). Grammar ground truth: the Godot 4 tokenizer/parser sources, with Godot 3 shapes (export var / onready var / setget / rpc keywords) cross-checked against the tree-sitter-gdscript grammar. The scanner keeps each logical line in two aligned copies — source text and a "shadow" with string contents blanked — so declarations inside string literals can never leak into the outline. class_name + extends merge into the script's implicit class; signal → event; properties via get/set blocks, get =/set = references and legacy setget; @abstract bodyless functions; ## doc comments; preload → imports; engine callbacks (_ready, _process, …) stay public so digest doesn't hide them. Stress-tested on ~1.9k .gd files from seven open-source Godot projects (Pixelorama, godot-demo-projects, Dialogic, Material Maker, Beehave, Phantom Camera, Tanks of Freedom): zero crashes, IR invariants hold, and the corpus caught two real bugs pre-release (Godot allows raw newlines in ANY string literal; annotation arguments may have whitespace before the paren). A differential run against tree-sitter-gdscript agrees on 1891 of 1895 comparable files — the four disagreements are a tree-sitter indentation quirk around comment-only lines, where this parser matches Godot's actual tokenizer. Suite: 2164 tests.
2026-06-10 v1.4.0: Bug-audit release — 16 fixes across grep, the core renderers, and 9 language adapters, each reproduced as incorrect behavior first and pinned by a regression test (suite grew 2085 → 2112). Highlights: grep -w / -i now work for non-ASCII identifiers (the matcher ran on raw bytes where \b / IGNORECASE are ASCII-only); Rust lifetimes ('a) no longer flip the in-string heuristic and swallow every match after them as [string]; a doc comment at byte 0 of a file is now included in show across all adapters; C# multi-declarator fields (int x, y, z;) emit every name; TypeScript abstract members and generator functions are no longer dropped; Go interface embeddings (io.Reader) and union constraints are complete; SQL no longer extracts phantom functions from block comments; CSS/SCSS compound selectors (#header.nav, &.active) are fully findable; HTML heading previews read in source order; Scala pattern bindings name the bound identifier, not the RHS. Internally, per-language knowledge (comment/import prefixes, lexical quirks, digest rendering family) moved from central tables in core.py / grep.py onto the adapters as declared attributes — output is byte-identical, and a new language adapter never needs a core edit.
2026-06-06 v1.3.8: No more UnicodeEncodeError crash on Windows consoles using a legacy code page (cp1251, cp866, …). ast-outline <file> on such a console died the moment any non-ASCII character reached stdout (UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f501') — either one of the tool's own → — … notes/legends, or arbitrary Unicode echoed from the user's own source (identifiers, string defaults, an emoji in a literal). That content is intrinsic to the output and can't be sanitized away, so the CLI now reconfigures stdout/stderr to UTF-8 at startup — only when they aren't already UTF-8 — formalizing the assumption --json already made via ensure_ascii=False. The previous PYTHONIOENCODING=utf-8 workaround is no longer needed.
2026-06-05 v1.3.7: show --json candidate-list note aligned to absolute paths, matching the structured file. A follow-up consistency fix to the v1.3.5 relative-path work. In show <dir\|glob> <symbol> --json for an ambiguous symbol, the re-run guidance echoed in notes (re-run with one of: a.py:1 …) had become cwd-relative in v1.3.5 (it shares the text-mode helper), while each match's structured file stayed absolute (/abs/…/a.py) — so within one JSON response the prose note and the structured field disagreed, tripping consumers that parse the note to drive a follow-up call. The note is now absolute, matching file. This formalizes the path convention: JSON paths are absolute and self-contained; text-mode paths are cwd-relative (compact for inline agent reading). Structured fields unchanged (show's file was already absolute; outline / grep / digest file path stays root-relative against the envelope root). A full cross-command harmonization to one form was rejected as overengineering — it would break existing root-relative consumers of grep / outline with no user-visible benefit, and trip the SCHEMA_VERSION stable-contract guard. No schema bump.
2026-06-05 v1.3.6: The same token-economy audit, applied to two more advisory lines. Following v1.3.5, the remaining # note: / footer lines were scored against real transcripts by the same lens (does it print, and do agents act on it). (1) The grep "N matches in comments/strings hidden — pass --include-noise" footer now prints only when a file has zero visible matches (header (0 matches)) — there it's the guard that stops an agent concluding a symbol is absent. It fired on every file with any filtered match, but agents acted under 1% of the time and ~75% of firings were on files that already had visible code matches (pure noise); the count still rides the JSON filtered_count field. (2) The "ignored N dirs … pass --no-ignore" note for a successful outline / digest batch moved to the --json notes array only (agents acted under 6% of the time; pruned dirs are almost always junk). It is kept in text on the empty-result path — every candidate directory filtered — where it's the "your folder was filtered, the path isn't empty" guard. Kept untouched: the truncation footer (only fires under an explicit -m) and all empty-result hints (did you mean, --kind mismatch, no-files-glob) — cheap and high-value.
2026-06-05 v1.3.5: grep stops narrating its auto-recognition, and grep / show / outline print paths relative to the working directory. Two token-economy cuts from transcript analysis of real agent sessions. (1) The grep advisory # note: lines — regex auto-promotion (which fired on nearly every \| alternation search) and leading definition-keyword stripping — are dropped from text output: agents reliably ignored them and kept typing \|, so the "pass --regex to silence" advice never changed behavior while every call still paid ~25 tokens for the note. The underlying behavior is untouched — auto-promotion, the \|| BRE→ERE conversion, keyword-strip with auto-narrow-to-def, and the genuine # note: invalid regex error all stay — and the advisories are preserved verbatim in the --json notes array for machine consumers. (2) grep, show, and outline headers (plus grep -l / -c lines and show's candidate lists) now print file paths relative to the current directory instead of repeating the absolute /Users/…/project/src/… prefix on every file; paths outside the working directory stay absolute so they remain resolvable when fed straight back into show / Read. digest was already directory-grouped and is unaffected.
2026-06-03 v1.3.4: help now lists every supported language — the table no longer drifts. The "SUPPORTED LANGUAGES" block in ast-outline help and the "SUPPORTED" line in ast-outline help outline were hand-maintained and had fallen behind: 7 of 19 adapters (C++, Rust, PHP, Ruby, CSS, SCSS, SQL) shipped without ever appearing in the help, and the two sections had also diverged from each other. Both are now generated from the adapter registry at import, so a newly added adapter shows up automatically and the listing can't go stale again. Each adapter carries a display_name (e.g. C#, TypeScript/JavaScript) — a per-language fact that lives on the adapter, consistent with how keywords and extensions already do. A new test asserts every adapter appears in both help sections by name and extension. Help text only — no change to parsing or output.

May

Date Event
2026-05-31 v1.3.3: prompt snippet now notes that output is already compact and that grep \| head hides counted matches. One cross-cutting paragraph added to the canonical ast-outline prompt snippet: the renderers emit a signatures-only skeleton, so output is usually small and worth narrowing with the tool's own flags before piping to head; a grep \| head cut in particular drops matches the header still counts in (N matches) — so results look complete but aren't — with -m N as the per-file cap that doesn't lie. Motivated by transcript analysis (a third of real ast-outline calls were wrapped in \| head -N while the built-in limiters went nearly unused, and most grep \| head cuts silently truncated counted matches). Text only — no behavior change.
2026-05-31 v1.3.2: show <dir\|glob> <symbol> at N>1 lists candidates instead of dumping all bodies. When a symbol resolves to several definitions (across files, or several declarations), show no longer prints every body — it prints a single pointer note naming the candidate locations and asks the agent to re-run against one file: # note: 2 definitions of 'MailSpec' — re-run with one of: a/MailSpec.cs:12 (class), b/MailSpec.cs:5 (class). This makes show single-shape: when it prints content that content is always source code; when it can't (an ambiguous symbol) it prints a # note: and no code — never a mix, so a parser of show's output no longer has to branch on "is this code or a list?". It also matches how agents disambiguate in practice — pick one definition (or a named subset), don't read all N at once. This supersedes the v1.3.0/v1.3.1 behavior where a multi-definition show dumped all bodies under an … all shown below note. The N=1 case (body + found … in <file> note), the N=0 case (not found + did-you-mean hint), and per-file show <file> <symbol> mode are all unchanged; no --all flag was added (it would optimize for a usage pattern that does not occur). In --json the ambiguous result is flagged ambiguous: true and its matches become body-less candidate locators (file / kind / qualified_name / start_line / end_line, no source), with the re-run guidance echoed in notes; the N=1 JSON (full match with source) is unchanged, with ambiguous: false now present on every result.
2026-05-31 v1.3.1: show accepts a glob target, not just a file or directory. A quoted glob — show "src/**/*.cs" MailSpec — is expanded by show itself (quote it so the shell leaves ** literal) and the matched files are searched exactly like the v1.3.0 directory target: one definition prints its body with a found … in <file> note, several print all bodies with a count note, none yields symbol not found (naming the glob as the scope) plus a did-you-mean hint. Reuses the same grep pre-filter + find_symbols resolver — a glob just hands grep the matched file list instead of a directory. A glob that matches no files returns # note: no files match glob: … (exit 0); a plain non-glob path that doesn't exist still gets the precise # note: file not found (the glob branch only triggers on * / ? / [). A directory search honors .gitignore / .ignore; a glob is expanded literally with no ignore-filtering (you already narrowed via the pattern). In --json the multi-file show envelope now carries two always-present locator fields — directory and glob, exactly one non-empty (glob is additive; the v1.3.0 directory JSON is otherwise unchanged).
2026-05-31 v1.3.0: show <dir> <symbol> finds the definition itself — one call instead of two. Pointed at a directory rather than a file, show now locates the symbol's definition(s) under it and prints the body directly, collapsing the agent's most common show failure loop — show <dir> <symbol># note: not a filegrep <symbol> <dir> --kind defshow <file> <symbol> — into a single call. The directory walk and def classification reuse the existing grep machinery (the same collection that honors .gitignore / .ignore), and the authoritative find_symbols resolver runs only on the candidate files, so a substring near-miss like MailSpecHelper is collected cheaply then dropped precisely. One definition → its body, preceded by # note: found 'MailSpec' (class) in <path> naming where it lives. Several definitions (same name across files) → all bodies printed (a directory show is still a show), preceded by # note: N definitions of 'MailSpec' across M files — all shown below, each body header carrying its own path. No match# note: symbol not found, plus a did-you-mean hint (reusing grep's edit-distance suggester) when a close name exists. All show flags (--signature, --no-doc, --view) and --json apply to the located file(s); --no-ignore / --exclude (new on show, directory-only) steer the search. In --json the envelope uses a directory locator and tags every match with its own file. File-target show <file> <symbol> is unchanged.
2026-05-31 v1.2.0: grep recovers from empty results instead of leaving an agent to guess. An empty grep is the most expensive miss for an LLM agent — the usual next move is a blind retry or abandoning the tool for raw rg / file reads. Two heuristics make the first call land, or hand back enough to make the second call correct. Leading definition-keyword stripping — a literal single pattern of the shape <keyword> Identifier (enum ItemSoundFamily, class MailSpec, def handler, fn parse, type Config, …) has the keyword stripped, the identifier searched, and — when no explicit --kind was given — the search auto-narrowed to def. Agents habitually paste the source keyword in front of a symbol; as a literal substring the match landed on the keyword (not the name), classified as a ref, and a --kind def narrow then dropped it — a silent "no matches". ast-outline grep "enum Foo" now behaves like ast-outline grep Foo --kind def, with a # note: documenting the strip. The recognised keywords are owned per-language by each adapter (a new definition_keywords attribute) and unioned at search time, so a new language adapter extends the behaviour for free. Did-you-mean by edit distance — on a true no-match for a plain identifier, grep gathers the declaration names in scope and surfaces the closest real symbol(s) via the standard library's difflib (no new dependency), catching plural/singular slips and typos (MissSortPilesMissSortPile). Names sharing no structure with any real symbol produce no suggestion (no false leads), and the lookup is bounded so a no-match never turns into a stall. Both ride the established exit-0 / # note: / # hint: convention, are disabled under explicit --regex, and compose with the existing --kind-mismatch hint (one follow-up line per empty result keeps the output scannable).
2026-05-25 v1.1.0: grep no longer tracebacks on auto-promoted regex with unbalanced parens. The BRE→Python regex auto-promote converts \| (alternation) but leaves ( untouched — so a pattern like foo\|bar\.method( became the Python regex foo|bar\.method(, which has an unterminated group, and re.compile raised. That violated the CLI exit-0 invariant: agents driving the tool in parallel batches saw a Python traceback and a non-zero exit instead of the documented # note: line. The CLI now pre-compiles patterns after auto-promote and surfaces a pointed # note: invalid regex … + # hint: … (suggesting \( / \) or --regex), returning 0.
2026-05-23 v1.0.0: First stable release — HTML adapter (.html, .htm) added. HTML pages (landing pages, templates, documentation sites, form-heavy admin UIs) get the same outline / digest / show / grep surface as code. Elements render as CSS-selector tokenssection#hero, header.site-nav, input[name=email type=email required], [import] link[rel=stylesheet href=/css/main.css] — so the outline line and the show argument share a vocabulary, and one selector grammar covers HTML, CSS, and SCSS. Headings (<h1><h6>) carry a 60-char text preview (h1: Pull exactly the context you need). Bare wrappers <div> / <span> / <p> / <li> / <tr> / etc. without id/class/significant attributes are dropped but their meaningful descendants float up to the parent's depth — real-world templates have 5-10 wrapping containers per visible block. Inline text-styling tags (<em>, <strong>, <code>, …) are skipped entirely. <svg> / <math> render the root only — inline icons would otherwise dominate with 30-50 unaddressable paths. Three or more consecutive sibling <details> collapse to one synthetic details ×N line so FAQ pages don't crowd the outline. Imports<link rel=stylesheet|preload|prefetch|modulepreload|icon|manifest> and <script src=…> surface three ways: signature gets an [import] prefix, --imports lists them, grep classifies inner matches as [import] via import_regions. Inline <script> / <style> bodies and <!-- comments --> ride in noise_regions so grep filters them by default. ERROR-node recovery for templated HTML — when Jinja {% if %} at root, Vue/Svelte raw templates, Handlebars, or PHP <?php cause tree-sitter to wrap the document in a single ERROR, a one-pass recovery walks the ERROR subtree to surface any well-formed elements inside; templated files get a partial outline instead of a blank one. Hardening protections present from day one: class de-duplication (class="btn btn primary"tag.btn.primary); duplicate same-name attribute last-wins (DOM semantics); attribute-value quoting for values containing whitespace, ], or " so the bracket grammar stays unambiguous; srcset / sizes whitelisted on <img> and <source> for responsive images; UTF-8 BOM, CRLF, Cyrillic / Chinese / emoji content survive intact; data-URI / base64 attribute values truncate at 40 chars; empty / doctype-only / comment-only files don't crash; long XHTML doctypes parse cleanly. A new core._split_query short-circuit recognises HTML compound selectors (section#hero.primary[disabled]) as a single token — same family as the CSS-sigil short-circuit. A new elements counter in the file-header summary mirrors the existing types/methods/fields (code), headings/code blocks (markdown), and per-doc count (YAML). Out of scope (deliberate, may revisit): <base href> not classified as import (sets base URL, not a resource pull); inline <script> and <script type="module"> are content, not imports; data-* / aria-* / role not promoted (would over-inflate the bracketed selector); SVG / Math children not recursed. The release is also a stability marker — 1976 regression tests pass, the IR + CLI contract is stable enough that consumers can pin a major version.
2026-05-20 v0.9.6: --json reworked into a pure encoding switch. The JSON mode added in v0.9.3 shipped as "lossless" — it ignored every content-filtering flag and always emitted the complete IR, so --json behaved differently from the same command without it. That is the minority design; rg --json, kubectl -o json, and eslint --format json all treat --json as an encoding switch that changes only how output is serialized, not what it contains. v0.9.6 aligns with that convention. Content-filtering flags now apply to JSON exactly as they apply to the text output: outline --no-private / --no-fields / --no-docs / --no-attrs prune the declarations tree; digest --include-private / --include-fields prune it; show --view / --no-doc trim each match's source. The JSON schema is unchanged — these filters thin arrays and empty sub-lists, they never alter the shape, so JSON-Schema validation still passes. Behavior change: because digest itself defaults to a public-API map (private members and fields hidden), digest --json now does too — pass --include-private / --include-fields or --format=wide for the complete tree (in v0.9.3 digest --json was always complete). Layout / output-mode flags stay text-only — they have no JSON equivalent because JSON has no layout: --no-lines and --imports (outline), the --format preset's layout dimension and the --max-members readability cap (digest), and -l / -c (grep). A --format preset's content settings still apply, so --format=wide --json differs from --format=default --json, while --format=names/compact/default — sharing the same content — produce identical JSON. Internally a new pure core.filter_declarations IR→IR transform gives the JSON serializer and the text renderers one shared definition of the content filter, so the two cannot drift.
2026-05-20 v0.9.5: .swift added to the LLM-agent prompt. Patch release — v0.9.4 shipped the Swift adapter and the CLI help with Swift, but the AGENT_PROMPT snippet (ast-outline prompt, the text wired into AGENTS.md / CLAUDE.md) was missed, so an agent running ast-outline prompt on a Swift project never learned the tool applies to .swift. The snippet now lists .swift alongside the other supported extensions. The regression test meant to guard exactly this drift was itself the reason it slipped: it checked a hardcoded extension list rather than the registered adapters, so it kept passing while .swift was absent. It now derives from ADAPTERS — adding a future adapter without naming one of its extensions in the prompt fails the suite.
2026-05-20 v0.9.4: Swift adapter (.swift) — parses Swift through tree-sitter-swift; merged from PR #5. Full outline / digest / grep / show coverage of classes, structs, enums (raw-valued + CaseIterable), protocols, extension blocks, actors, methods, top-level functions, initialisers / deinitialisers, subscripts, stored and computed properties, enum cases, typealiases, associated types. init / deinit classify as constructor / destructor and subscript as an indexer; an extension surfaces as a type group so its members stay attached to the type they augment, and an actor is its own type kind. Visibility follows the language — default internal, private / fileprivate honoured, protocol members implicitly public. Attributes (@MainActor, @available, @Published, @objc) ride along in the signature, generics and protocol-conformance clauses are preserved, and /// doc comments attach to the following declaration. import directives register as static imports — Swift has no in-body imports, so conditional_imports_count stays 0. Structural grep classifies Swift // / /// comment lines as noise and tags import lines as [import], consistent with every other //-comment language.
2026-05-20 v0.9.3: --json machine-readable output mode — the four structural commands (outline, digest, grep, show) gain an opt-in --json flag that swaps the text format for a single JSON document. The text digest stays the default for humans and LLM agents — it is deliberately token-dense and free to change every release; --json exists for programmatic consumers — editor plugins, CI gates, scripts — that need a stable, parseable contract instead of regexing the evolving text format. Every document is wrapped in a fixed envelope ({tool, schema_version, command, …}) carrying a schema_version integer so a consumer can detect a breaking change; every field is always present (empty lists as [], empty strings as "") so no defensive .get() is needed. Lossless by design — in --json mode every content-filtering flag is ignored (--no-private / --no-fields / --no-docs / --no-attrs / --no-lines for outline; --format / --include-private / --include-fields / --max-members / --oneline for digest; --no-doc / --view for show; -l / -c for grep); the complete IR is always emitted and the consumer filters itself. Flags that select which files to process (--no-ignore, --exclude, --glob) still apply — they shape input, not output. Errors stay valid JSON — a user-facing failure (path not found, bad argument, unsupported extension) is emitted as an {"error": {"notes": […], "hint": …}} object on stdout rather than a # note: text line, so the output is always parseable; the exit-0 contract is preserved. A zero-result search (grep with no matches, a file with no declarations) is a valid empty document, not an error. Unicode identifiers are emitted unescaped (ensure_ascii=False). The LLM-agent prompt (ast-outline prompt) is intentionally unchanged — agents keep using the token-dense text digest. New json_output module holds the IR-to-JSON serialization, kept separate from the text renderers so the JSON schema evolves on its own cadence as a contract.
2026-05-20 v0.9.2: Ruby callback-DSL blocks — extends the KIND_BLOCK recognition added for TypeScript/JavaScript in v0.9.1 to the Ruby adapter. RSpec suites (describe / context / it / feature / scenario / shared_examples), Rake tasks (task :build do), route maps (namespace :admin do / resources :users do), ActiveSupport::Concern's concerning, Sinatra routes, and any in-house DSL of the same shape were invisible to the outline — an RSpec spec file digested as "no declarations". They are now recognised as block declarations and descended into. The rule is structural — no hard-coded list of framework names: a block-bearing call (do...end or {...}) becomes a block when (1) its callee is a plain identifier and (2) its first argument is a string or symbol label. A constant first argument is intentionally NOT a label — assert_raises(ArgumentError) { ... } is structurally identical to describe User do, so promoting on a constant produces false positives; a constant-named bare container still has its body surfaced by transparent descent, it simply isn't given its own heading. Transparent descent is the Ruby-specific addition: a block-bearing call that is not itself a named container — a member call such as RSpec.describe User do (the modern RSpec entry point) or Rails.application.routes.draw do — still has its body walked, so the plain-identifier blocks nested inside it surface. Without it a modern RSpec.describe spec file would still outline as empty. Works across all four commands (outline / digest / show / grep) exactly as the TypeScript blocks do. Validated on ~9,000 real-world Ruby files (Rails, RuboCop, Mastodon, Jekyll, Devise, Sidekiq, RSpec, Rake, Sinatra): no crashes, no signature body-leaks.
2026-05-20 v0.9.1: TypeScript / JavaScript callback-DSL blocks — closes issue #3. The TS adapter was blind to structure expressed through callback arguments: a test file whose suites and cases live inside describe(...) / it(...) outlined as empty, and a Pinia setup-store (const s = defineStore('id', () => {...})) dumped its entire callback body onto one signature line. Both are now recognised as a new KIND_BLOCK container kind and descended into. The recognition rule is structural — no hard-coded list of framework names: a call_expression becomes a block when (1) its callee is a plain identifier, (2) its last argument is a function literal, and (3) its first argument is a string-literal label. Clause 3 is the discriminator between a named container (describe('suite', fn), defineStore('id', fn) — the label is its reason to exist) and a bare function wrapper (action(fn), memoize(fn)) or a property-definition wrapper (defineGetter(obj, 'name', fn) — string not first); the rule therefore covers vitest / jest / mocha / jasmine / ava / tape / node:test, Pinia defineStore, and any in-house DSL of the same name('label', callback) shape, and rejects setTimeout(fn, 1000) / useEffect(fn, deps) / el.on('x', fn). Blocks nest (describeit → inner declarations) and work across all four commands: outline (indented tree), digest (a block <label> header with member tokens for the nested cases, like a type), show (extract by label — including labels with spaces, colons, or a literal dot, via a whole-string fallback in find_symbols when the dotted-path walk finds nothing), grep (blocks join the scope chain, and a test description matches as [def] rather than vanishing as string-literal noise). Same release: function bodies no longer leak into field signatures — a field whose value embeds a function/method body (const inc = action((d) => { ...stmts... })) renders with the body elided to {…}, so an outline shows structure, never implementation code; and commented-out code no longer renders as documentation — a leading run of // comments that is disabled code (braces, arrow bodies, declaration keywords, by majority vote) is dropped instead of being shown as doc lines on the next declaration, while genuine prose and /** ... */ JSDoc blocks are kept. Known blind spots (a pure shape rule cannot avoid them): a member-expression callee (QUnit.test(...), Playwright test.describe(...)) is excluded the same way arr.map / el.on are — bare-global frameworks are unaffected; a bare addEventListener('x', fn) is structurally identical to it('x', fn) and becomes a block. Validated on 326 real-world files (Pinia, Express, Lodash) with zero crashes; ~40 new regression tests.
2026-05-17 v0.9.0: Lua adapter (.lua, .wlua) — first new language since the Ruby drop. Vanilla Lua 5.1–5.4 via tree-sitter-lua; full outline / digest / grep coverage. Maps Lua's flat-file convention-based shape onto the existing IR without inventing synthetic kinds. Decisions worth flagging: function M.foo() (dot) is KIND_FUNCTION; function M:bar() (colon) is KIND_METHOD — the colon is Lua's source-true marker for implicit self, the cheapest and most honest signal for "instance method", avoiding the brittle "detect a class from setmetatable" path. Metamethods (__add, __sub, __mul, __div, __mod, __pow, __unm, __idiv, __band, __bor, __bxor, __bnot, __shl, __shr, __eq, __lt, __le, __concat, __len, __call, __index, __newindex, __tostring, __metatable, __pairs, __name, __close, __gc, __mode) → KIND_OPERATOR regardless of shape (method-style function C:__tostring(), assignment-style C.__add = function() end, non-function value C.__index = C); --kind operator isolates every protocol declaration in one query. Visibility: local scope → private (Lua's actual private), metamethods → public protocol (underscore prefix is part of the language contract, NOT a private convention — Python-dunder analogue), M._helper → private (underscore convention universal in Neovim / LÖVE / Roblox-Lua). Lua 5.4 attributes (<const>, <close>) ride in Declaration.attrs. require imports: bare (require "x"), parenthesised (require("x")), AND local Y = require("x") all register as static imports; the local X = require(...) shape pushes the WHOLE statement byte range into import_regions so grep promotes it to [import] despite the line prefix being local (too broad to whitelist). require calls inside function bodies / branches / loops bump conditional_imports_count and never appear in the static list. Long-bracket comments (--[[ ]], --[==[ ]==] at any = level) and long-bracket strings ([[ ]], [=[ ]=]) ride in noise_regions so grep filters matches inside them. Direct-return-table module shape (return { foo = function() end, V = 1 }) walks the returned table fields and surfaces each as a top-level decl — the second canonical Lua module pattern besides local M = {} ... return M. Grep classifier — Lua chain skipping and sugar calls (opt-in via new language kwarg to _next_call_paren_after): obj:method(x) matched on obj classifies as KIND_CALL (chain skipping over :method); same for deep dot chains (a.b.c.d(x)). Sugar-call shapes f"x" / f'x' / f{...} / f[[...]] classify as KIND_CALL (Lua treats them as syntactic sugar). Bare [ is intentionally NOT a call marker (f[key] is a subscript). Strictly opt-in — in TypeScript / Rust the same shapes still classify as KIND_REF. Out of scope (v1): Luau (.luau) — vanilla tree-sitter-lua produces ERROR nodes on Luau type annotations; symbol semantics are the same so the v0.9.1+ path is a suffix-dispatch branch on tree-sitter-luau. setmetatable-based inheritance — no syntactic anchor in vanilla Lua, only convention; v1 leaves bases = [] and agents read the __index assignment (which lands as KIND_OPERATOR) themselves. Module-shape detection — each decl emitted at the file's top level with the qualifier baked into name (M.foo, M:bar), flat like the Python adapter; wrapping under a synthetic KIND_NAMESPACE would need a heuristic that fails on the many real files holding several module-shaped tables. 42 regression tests, 10 fixtures (module_pattern, mt_class, direct_return, neovim_plugin, love_callbacks, requires_and_attrs, nested_names, strings_and_comments, broken_syntax, empty). 1493 tests green.
2026-05-17 v0.8.13: ast-outline grep — match classification on Unicode source lines. The classifier resolved a match's line column as a 1-based byte offset, then indexed line_content (a Python str) with it. For ASCII-only lines the two coincide, so the bug was silent in the 99% case; for any line containing a multi-byte UTF-8 character — Cyrillic (2 bytes/codepoint), CJK (3 bytes), emoji / supplementary plane (4 bytes), accented Latin / math symbols — the byte offset over-shoots the codepoint index for everything after the first multi-byte char. Three downstream classifiers consumed the wrong column: _next_call_paren_after (the call/ref walker) started past the intended position and either landed on a later character or ran off the end of the line; _column_inside_string counted unescaped quotes against a string-length cap that wasn't its bound, returning even/odd parity for the wrong span; _classify_match's inline-comment check compared # index against column - 1 so a comment match could be mis-claimed as code on Cyrillic-prefixed lines. Net effect: locale-sensitive false negatives across --kind filtering, leaked string/comment content into the default code view, and degraded behavior for any project with non-ASCII identifiers, comments, or string literals — Russian / Ukrainian / Chinese / Japanese / Korean projects, math-heavy code with / α / β, emoji in test names. Fix: the loop that converts per-match byte spans to columns decodes the line-prefix bytes once per side and uses the codepoint length, so every downstream check receives a codepoint-correct cursor regardless of multi-byte content ahead of the match. No-op for ASCII-only lines (len(decoded_prefix) == len(prefix_bytes)), so no performance regression in the common path. Companion fix in _next_call_paren_after's rest-of-identifier skip (introduced in v0.8.12 for ASCII-only [A-Za-z0-9_]): now uses str.isalpha() / str.isdecimal() / _, covering Unicode letter and decimal-digit categories — matches what Python / Rust / TypeScript / Swift accept as identifier chars beyond the first. Deliberately narrower than isalnum(), which would also accept Unicode No-category numerics like ² / ¼ that aren't identifier chars in any supported language. The other walker skip cases (< / > / [ / ] / ?. / :: / !) stay ASCII: they're language-syntax markers, not identifier content. Both fixes are necessary together — boundary conversion lands the cursor at the right codepoint, Unicode-aware skip walks the rest of the identifier to the (. Behavior change: GrepMatch.column is now documented as a 1-based codepoint offset (was "1-based byte offset on its line, +1"). On ASCII-only lines the values are identical; on multi-byte lines the reported column is now the codepoint index — what an editor / language server / IDE reports. 7 new regression tests covering Cyrillic / CJK / accented-Latin walker-unit cases, Cyrillic and CJK call classification end-to-end, ASCII call after a Unicode prefix, in-string detection on a Cyrillic-prefixed line, inline-comment detection past a Cyrillic-prefixed comment marker, and the codepoint-offset contract. 1451 tests green.
2026-05-17 v0.8.12: ast-outline grep — match ending mid-identifier no longer demotes calls to [ref]. Repro that surfaced the gap: agent runs ast-outline grep "TryAssembleFragments\|TryAssembleFragmentsNear" Assets/Scripts/App --kind def,call against a C# Unity codebase to find every definition + caller of a method; the three [def] matches surface correctly, but the caller at ThingDragNDropController.cs:1208controller.TryAssembleFragmentsNear(thingData, currentPosition); — is missing. Plain grep -rn confirms the call exists. Cause: Python re resolves alternation by picking the leftmost alternative that matches at a given position (NOT the longest), so TryAssembleFragments\|TryAssembleFragmentsNear matches the shorter TryAssembleFragments against TryAssembleFragmentsNear(...) and the match ends on N mid-identifier. The call/ref classifier in _next_call_paren_after (grep.py) walks the line right of the match-end looking for (, already skipping whitespace, generic blocks <…> / […], bare closers > / ] (v0.8.8), turbofish ::, TS ?., TS !, but never the trailing bytes of an identifierN was treated as "other significant char", returned False, classified as [ref], filtered out by --kind def,call. Bug was language-agnostic — the walker is shared across all adapters, so any language with name(args) call syntax was affected. Verified on Python, TypeScript, Go, Rust, and C#; same regression on all. Fix is a one-shot rest-of-identifier skip at the entry of the walker: if start lands on [A-Za-z0-9_], advance past the remaining word bytes, then run the existing skip loop. Applied only at entry (not mid-walk) since legitimate intermediate identifiers in real-language line shapes don't arise — after the existing generic-block balance / :: / ?. skips, an identifier char mid-walk indicates a different identifier whose call shape shouldn't be attributed to the original match. Pinned by test_next_call_paren_after_identifier_skip_one_shot_not_recursive: foo<T>bar() matched at foo returns False, not True. Symmetric to the bare-closer skip from v0.8.8 — both cover the same root cause (match end position doesn't align with a token boundary), just on different sides of the syntactic boundary. Behavior change worth flagging: the fix also affects literal (non-regex) substring searches by the same path — foo against fooBar(x) now classifies as [call] (was [ref]). Consistent with the walker's documented "bias toward call" — agents searching for a substring inside a called identifier almost always want the call site surfaced. Non-call sites (foo against fooBar = 1) still classify as [ref] because the trailing ( check fails. Identifier-skip uses ASCII [A-Za-z0-9_] only — non-ASCII identifiers (Cyrillic / CJK) are bypassed by a separate pre-existing byte-vs-char index mismatch higher in the pipeline; flagged as a follow-up. Workaround for users on older versions: write the longer alternative first (TryAssembleFragmentsNear\|TryAssembleFragments) or anchor the shorter one (TryAssembleFragments\b\|TryAssembleFragmentsNear). 10 new regression tests across 5 languages and the walker unit layer. 1444 tests green.
2026-05-16 v0.8.11: ast-outline outline / digest / grep gain --exclude <glob> — repeatable, gitwildmatch (.gitignore syntax), so agents can narrow the walk inline without editing .gitignore or .ignore files. Repro that surfaced the gap: agent runs ast-outline digest src/ against a Unity-shaped repo where Tests/ and *.gen.cs swamp the public-API map; today the only escape hatches are (a) edit .gitignore (touches version control for a one-shot exploration filter), (b) edit .ignore (still a file write, agent has to know the convention), (c) pre-filter paths via shell ls \| grep -v then pass globs (loses directory recursion + the project-root anchor of .gitignore). rg --glob '!pattern', fd --exclude, tree -I, tar --exclude all offer the same shape inline; ast-outline was the only structural-search tool without it. Flag is repeatable, anchored at the project root (same anchor as the root .gitignore frame — --exclude src/generated/ resolves identically regardless of cwd), supports !pattern negation. Layered ABOVE the auto-filter frame in collect_files_with_stats, so a bare --exclude '!node_modules/' re-includes a default-filtered dir on a single line — the three-line git escape idiom (!node_modules/ + node_modules/* + !node_modules/our-fork/) required inside .gitignore isn't needed because the --exclude frame sits one level higher and its negation wins. Survives --no-ignore: that flag silences the AUTO-filter (.gitignore + hardcoded defaults), --exclude is the user's explicit voice and keeps applying; two semantic axes, both useful together (--no-ignore --exclude secret/ = walk every dir EXCEPT the named one). Explicit single-file inputs continue to bypass filtering — same rule as the .gitignore precedent. When the flag contributes to ignored-dir pruning, the existing # note: ignored N dirs (…) via … line widens its source list from .gitignore/.ignore + defaults to .gitignore/.ignore + defaults + --exclude, so an agent debugging "where did my folder go" sees its own flag named — visibility over magic. Malformed patterns (! alone, trailing backslash — GitWildMatchPatternError) surface as # note: invalid --exclude pattern: … with rc=0, honoring the CLI batch-friendly invariant. Implemented as one extra GitIgnoreSpec frame in collect_files_with_stats, reusing the existing pathspec>=0.12 dependency; no new deps. The agent prompt snippet (ast-outline prompt) gains ONE universal sentence under the 4-step menu so any LLM picks the flag up on first read without studying --help. What's NOT added (intentional): --exclude-from=FILE — YAGNI, .gitignore / .ignore already serve that role for any pattern stable enough to be worth a file; [tool.ast-outline] exclude = […] in pyproject.toml — the tool is principally stateless / zero-config and reading project config would break that invariant; per-command divergence in flag shape — all three subcommands take the same --exclude GLOB, keeping the agent-facing surface symmetric.
2026-05-16 v0.8.10: ast-outline show resolves markdown headings whose query drops inline-markdown decoration. outline prints headings verbatim, so an H2 with inline code surfaces as ## `useState` — when to reach for it; agents (and humans) copying the title routinely strip the backticks as if they were rendering hints, and show then returned # note: symbol not found because the substring matcher compared raw heading text — backticks broke continuity. Same shape with *emphasis*, _emphasis_, ~~strike~~. The matcher now strips `, *, _, ~ from BOTH the heading title and the query before the case-insensitive in test; symmetric, so passing the decorated form verbatim still resolves the same heading. Scoped to the substring=True branch of _trail_matches, which only fires for KIND_HEADING (markdown-only) — code-symbol matching keeps strict equality, so a Python _foo or a Rust *const T is never silently broadened. Composes with the existing numbered-prefix short-circuit (2. \Bar` setupresolves fromBar setup). Structural markup — linkstext, autolinks`, raw HTML — is intentionally NOT stripped: char-wise removal would corrupt the visible label rather than peel decoration. Fixes #4.
2026-05-13 v0.8.9: ast-outline digest gains a --format= preset selector with four levels (names, compact, default, wide) plus the --oneline alias. Repeated agent traces showed that digest's default per-file detail (line ranges, per-class counters, blank-line paragraph breaks) is exactly right when the next step is Read --offset N, and exactly wrong when the next step is "pick a file to drill into" — multiple skills reached for head -N digest or piped output through awk to throw away detail they didn't need, truncating the tail instead of compressing per-file. Preset solves the underlying problem: let the caller name the level of detail that matches the task. names for repo orientation = one line per file (name.py [large]: ClassA, ClassB, func_c), top-level symbols only, no methods / no () / no : Base / no L<a>-<b> / no legend; files with no public top-level symbols hidden. compact for module-structure questions = full hierarchy minus per-file , X types, Y methods, Z fields counters, L<a>-<b> line ranges, blank paragraph breaks between types-with-members, and # no declarations markers (declaration-less files hidden entirely); inheritance, decorators, modifiers, and size labels all survive — they carry semantic weight. default unchanged (byte-identical back-compat — every existing skill parsing v0.8.x output keeps working). wide for one-file deep-dives = CLI-side preset that cranks --include-private + --include-fields + lifts --max-members to 10**9; no rendering branch, the existing default-format renderer already shows everything when those toggles are on. Names format extends to non-code languages — top-level H1 headings for markdown, top-level keys for single-doc YAML / doc-separator captions for multi-doc YAML (--- doc 1 of 3 — ConfigMap …), flat selector list for CSS/SCSS. [huge] files (>100k tokens) collapse to a header-only line with no symbol list in names; [broken] parse-error marker preserved across all formats. Preset overrides follow kubectl-style silent override: explicit --include-private / --include-fields / --max-members win over the format's defaults (--format=wide --max-members 5 = wide's private+fields with a small cap; --oneline --include-private = names with private symbols added). --imports composes with every format including names, adding an indented imports: … line per file — an earlier draft silently dropped imports under --oneline, which would have misled agents into thinking "this file has no imports" and broken the prompt's stated invariant. Design boundaries — a fifth grep-shape flat format (path:line:qualname per symbol, pipe-ready) was considered and dropped: it would have collided with the existing ast-outline grep subcommand's output shape, and no concrete agent task was unmet by the four-level set. No new --no-X flags either (point YAGNI — in-between cases are reachable by picking the adjacent preset plus existing override flags). : Base stays the inheritance notation: a Python-native Class(Base) alternative would actively mislead on C# 12 (primary constructor) and isn't language-native for the OOP-heavy languages digest most cares about. The canonical agent prompt (ast-outline prompt) gets a single 2-line addition under the digest entry — obvious-by-convention preset names (kubectl -o name/-o wide, jq -c, git --oneline) chosen so an LLM reads the flag without --help.
2026-05-12 v0.8.8: Generic-call invocations now classify as [call] across all languages. ast-outline grep "Bind.*SaveSystem" --regex --kind call against a C# Unity codebase previously returned 0 hits even though c.Bind<SaveSystem>() is clearly an invocation — the call-vs-ref walker (_next_call_paren_after) only knew how to skip an opener (< / [) preceding the cursor, balancing the block to the matching closer before resuming the ( search. It didn't know how to skip a bare closer (> / ]) left over from a greedy regex match (Bind.*SaveSystem ending on >) or from a literal-with-type pattern agents type to disambiguate generic overloads (parse::<i32, Map[String, Int). With no closer-skip the walker hit the closer, fell through return ch == "(" → False, and classified the whole invocation as [ref] — making --kind call return 0 across C# / Java / Kotlin / Scala / TypeScript / Rust-turbofish-with-explicit-type / Go (1.18+ Foo[int]()) / C++ uniformly. The walker now skips bare leading > / ] the same way it already skipped whitespace, ?., !, and :: — symmetric to the existing opener skip, consistent with the walker's existing bias-toward-call policy (when the line shape is ambiguous, classify as call; ref false positives in code-search are more painful). 8-language test matrix pins the classification. Foo.*Bar patterns now surface the --regex hint on zero results. Companion gap to the above: even before the agent thought to add --regex, the warn-on-no-match hint that would have suggested it didn't fire on .* patterns — the ambiguous-regex fingerprint required either an escaped metachar (\., \(), a letter-or-)/] followed by a quantifier (d*, )*), or an edge anchor (^, $). The .<quantifier> shape — where . is the char before * / + / ? — slipped through both the strict auto-promote fingerprint (intentionally excluded — . and * individually appear in literal code as qualified names and array types) AND the warn-on-no-match fingerprint. The ambiguous-regex fingerprint now treats .[*+?] as unambiguous regex intent: bare . in qualified names (User.save) is still skipped, but the pair .* / .+ / .? has no literal-code interpretation worth protecting. Extends the same hint-coverage principle as v0.8.4 to the regex-mode blind spot.
2026-05-12 v0.8.7: HTML block comments (<!-- ... -->) in markdown classify as [comment] in grep. A multi-line <!-- TODO: revisit useState patterns --> block in README.md previously surfaced under ast-outline grep useState as a regular [ref] match alongside real prose mentions — agents reading the result couldn't tell signal from author-private annotation. The markdown adapter now appends (start, end, "comment") noise regions for every block-level html_block whose first four bytes are <!--, which the existing noise filter handles identically to source-language comments. Surfaces with [comment] under --include-noise so the agent can opt back in. Inline <!-- --> inside a paragraph is NOT covered — tree-sitter-markdown fragments those into single-character punctuation nodes with no clean byte range; block-level is the 95% case for hidden TODO / NOTE / draft annotations. Other html_block content (raw <div>, <table>) intentionally stays visible — embedded HTML carries searchable signal (component names, data attrs) that an agent may legitimately grep for.
2026-05-12 v0.8.6: YAML block scalars (|, >) noise-filtered in grep. ast-outline grep npm .github/workflows/ previously returned every npm install, npm run lint, npm run build line lifted from run: | step bodies — the structural mentions (job names, step names) drowned in the shell-line noise. The YAML adapter now populates ParseResult.noise_regions with the byte ranges of every block_scalar node (kind string, so the existing --noise-filter / --include-noise path handles it without a new flag). Plain scalars stay visible — image: registry.example.com/api, replicas: 3, single-line run: npm publish are exactly what agents grep YAML for; only the multi-line block forms (\| literal, > folded) — which YAML authors reach for specifically to embed opaque scripts / templates — get masked. Mirrors the v0.8.5 fenced-code-block treatment in markdown.
2026-05-12 v0.8.5: Fenced code block bodies noise-filtered in markdown grep. ast-outline grep useState docs/ against a tutorial site previously surfaced every example-code occurrence of useState alongside the prose mentions, making the result useless on docs-heavy repositories. The markdown adapter now populates ParseResult.noise_regions with the byte ranges of each fenced block's code_fence_content (kind string, so the existing --noise-filter / --include-noise path handles it without a new flag). Fence delimiters and the info string itself (```python) stay searchable, so language-by-fence queries still work. Indented (4-space) code blocks are intentionally not masked yet — rare in modern markdown and almost always paired with a fenced equivalent. Repro that surfaced the gap: ast-outline grep useState README.md returning identical hits from prose and from the JSX example below it.
2026-05-11 v0.8.4: Kind-filter hint on empty grep results. When grep --kind X returns zero matches but the pattern would have matched under other kinds, the CLI now appends a # hint: line under the bare # note: no matches with a count breakdown and a retry suggestion: # hint: --kind call excluded 4 matches (4 ref) — retry with --kind call,ref or drop --kind. Motivation: agents reading EditorPrefs.GetString(...) see a call, pass --kind call, and get nothing — because the match lands on the type name EditorPrefs (a ref, since . follows it), not on the called method GetString. The bare "no matches" hid the fact that the symbol was present in a different role; one extra line collapses what was previously a binary-search through --kind values into a single retry. Universal across all six kinds (def / call / ref / import / comment / string) and works with multi-kind filters (--kind def,import); covers the noise-filter blind spot too — a pattern living only in a comment / string under a non-noise --kind narrow now counts toward the breakdown, and the suggested retry (--kind def,comment) works because --kind comment auto-enables --include-noise in the CLI. Suppressed when the regex-syntax hint fires for the same empty result — one hint per call keeps the output scannable.
2026-05-11 v0.8.3: show "3. Foo" / show "4.2 Foo" now resolve. Markdown headings like ## 3. Numbered Heading are stored with their numeric prefix intact and outline prints them that way, but the find_symbols query tokenizer was reading the dot in 3. as a path separator — splitting the query into ["3", " Foo"] and demanding a two-segment trail that never exists (markdown headings are single declarations whose name carries the prefix). Result: a silent symbol not found for the exact text outline had just printed, breaking the round-trip workflow. _split_query now short-circuits queries shaped like a numbered-heading prefix (\d+(\.\d+)*\.?\s+<text>) into a single opaque token, letting the existing substring-matching path resolve them. Bare dotted-numeric queries without trailing text ("1.2", "1.foo") keep the previous split behaviour so non-markdown lookups are unaffected. Fixes #2.
2026-05-10 v0.8.2: Agent-prompt --signature scope disambiguated. The canonical agent prompt previously read "Add --signature to any of the above" inside the show section — literal models (Opus 4.7, GPT-5.5) read the menu globally and reached for --signature on outline and digest, where the flag does not exist. Reworded to "Add --signature to show (only there)" — preserves the workflow hint, adds an explicit anchor. Cross-command flag hint on unrecognized arguments. When an unknown flag passed to one subcommand is recognized by another, the # note: unrecognized arguments: --flag line now appends (hint: \--flag` is a flag of ``, not ``)`. Multi-owner flags list every owning command. Truly unknown flags get no hint. Agents self-correct in one retry instead of guessing or asking the user.
2026-05-08 v0.8.1: grep -e PAT PATHS... now works without a positional pattern. Previously ast-outline grep -e foo src/ failed with the following arguments are required: paths because argparse couldn't disambiguate the trailing string as path-vs-pattern. Patch fix: a pre-argparse rewrite promotes the first -e PATTERN value into the positional slot when no positional pattern appears before the first -e. Aligns with POSIX grep -e and rg -e muscle memory. Long form --expression PAT and equals form --expression=PAT covered too. Existing call shapes (grep PAT PATH, grep PAT -e PAT2 PATH) unchanged.
2026-05-08 v0.8.0: ast-outline grep — AST-aware structural code search. New subcommand returning matches annotated with their enclosing class/function and a kind classification ([def] for definitions, [import] for import statements; calls and refs render untagged because identifier-followed-by-( makes them obvious). Comments and string literals are filtered by default; --include-noise opts in. The intended consumer is an LLM agent that today does grep symbol → 20 hits → read 5 files to understand which class / function contains each match; this collapses that into one call by placing scope and kind inline in the output, eliminating the cascade of follow-up Read calls. Built for code-symbol queries — not a replacement for rg on TODO comments / log strings / free-text. POSIX-style flags: -e/--expression PATTERN (repeatable, multi-pattern via alternation — one walk for N symbols), -w/--word (whole-word \b...\b), -l/--files-with-matches (paths only), -c/--count (path:N per file), -m/--max-count NUM (per-file cap with explicit # truncated — N more... footer so partial results are never silent), -i/--case-insensitive, --regex. Regex auto-detect — patterns with unambiguous regex syntax (\|, \d, \w, (?:, bare |) auto-promote with a # note:; \| normalizes to | (BRE→ERE). Ambiguous metachars (., *, [, ^) never auto-promote but emit # hint: on zero matches suggesting --regex. --kind def\|call\|ref\|import\|comment\|string filter eliminates the most common post-filter step ("show me only definitions of X" / "only call sites"); accepts repeated (--kind def --kind call) or comma-separated (--kind def,call) forms. Multi-line / block-form imports classify correctly via tree-sitter: Go import (...), Python from X import (...), TypeScript multi-line import {...} from, Rust use foo::{...}, PHP use App\{...}, Scala import foo.{...} — inner symbols get [import], not [ref] or [string]. Python lazy imports inside function/class bodies also covered (e.g. def foo(): from x import (\n a,\n)). For C++, AST node distinction means using namespace std; and using std::vector; classify as [import] while using my_int = int; correctly stays as a type alias declaration. C# 10+ global using recognized too. Implementation uses a new ParseResult.import_regions field populated via piggyback on existing tree walks — zero measurable overhead vs not collecting them. --kind filter, regex auto-detect, and multi-line import handling are all part of the public surface from the startgrep is stable, no longer experimental. Bumped to a minor version (0.7.x → 0.8.0) to reflect the new command.
2026-05-06 v0.7.7: Setup-prompt — Claude-Code-only Explore-shadow sub-step. Claude Code's built-in Explore subagent runs in an isolated context — it does not inherit CLAUDE.md / AGENTS.md, so the snippet written in Step 2 of setup-prompt does not reach Explore invocations on its own. Setup-prompt now detects when the user has Claude Code (~/.claude/ exists) AND no .claude/agents/Explore.md shadow file exists yet, and asks once whether to create the shadow. If approved, writes a ready-to-go Explore definition that embeds the full fresh canonical from Step 2.1 verbatim (not a short pointer — the shadow is a brand-new file and embedding avoids forcing every Explore invocation to re-run ast-outline prompt), wrapped in the standard markers; offers project-local vs global scope (.claude/agents/Explore.md / ~/.claude/agents/Explore.md). Codex and Gemini subagents are user-defined files only — they fall under the existing diff-aware patch logic, no shadow concept needed. Skipped in headless mode and when a shadow already exists. Closes the gap where ast-outline integration via AGENTS.md silently failed to reach the most-used Claude Code subagent. Step 2 also detects user-written ast-outline content outside markers — previously the "markers absent" branch silently appended a fresh marker block at the end of an existing AGENTS.md / CLAUDE.md / GEMINI.md, leaving two competing references when the user had hand-written content (e.g. from an old ast-outline prompt >> AGENTS.md run that they later edited). Setup-prompt now scans for ast-outline mentions in the target file before any write; if found outside markers, shows the offending lines and asks the user which path to take — (1) wrap the existing content in markers verbatim (preserves their version, future re-runs go diff-aware), (2) replace with the fresh canonical, (3) append anyway with duplication, or (4) skip Step 2 entirely. Default is ask — silent append on top of user content is no longer reachable. Site homepage now also surfaces an Install with AI call-to-action linking to a dedicated section that walks through the setup-prompt flow before the manual install paths.
2026-05-06 v0.7.6: setup-prompt subcommandast-outline setup-prompt prints an install-time checklist meant for one-shot consumption by a coding agent (Claude Code, Codex CLI, Gemini CLI, Cursor). Tell the agent "run ast-outline setup-prompt and follow its instructions" and it walks the user through three idempotent steps: (1) verify the CLI (offer uv tool install / pipx / pip if missing, run on user's behalf with explicit consent — never silently); best-effort PyPI version check, surface the matching upgrade command (uv tool upgrade / pipx upgrade / pip install -U) without auto-upgrading; (2) write the canonical ast-outline prompt snippet to the right persistent-context file — ./AGENTS.md cross-tool default (covers Codex CLI, Claude Code via @AGENTS.md import, Gemini CLI with settings.json config, and Cursor) or the native single-vendor file (./CLAUDE.md / ./GEMINI.md); offers project-local vs global scope (~/.claude/CLAUDE.md / ~/.codex/AGENTS.md / ~/.gemini/GEMINI.md); checks for Codex AGENTS.override.md so writes are not silently shadowed; wraps the snippet in <!-- ast-outline:start --> ... <!-- ast-outline:end --> markers and runs diff-aware on re-run — if existing block content differs from the fresh canonical, the agent shows the diff and offers replace / keep / show-full-diff, never overwriting customizations silently; (3) optionally patch existing exploration-oriented subagent files in .claude/agents/ / .codex/agents/ / .gemini/agents/ with per-agent permission. Cross-vendor universal — outcome-first markdown structure, no persona, no chain-of-thought, no aggressive emphasis; calibrated for Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5, GPT-5.x, and Gemini 3.x. Cross-OS — agent translates which / $VIRTUAL_ENV / curl to PowerShell / cmd.exe equivalents on Windows. Adapts to whichever human language the surrounding conversation uses for spoken replies and any free-form prose written into a freshly-created AGENTS.md / CLAUDE.md, while keeping the marker block content and subagent files in English. Headless harnesses (codex exec, claude -p, Gemini non-interactive, scheduled CI) restrict execution to read-only checks plus AGENTS.md write at project-local scope. The CLI itself is print(SETUP_PROMPT, end="") — file IO is delegated to the agent's native edit tools, so encoding / permission / merge-conflict edge cases are handled in agent context, not in Python.
2026-05-06 v0.7.5: SQL adapter (.sql) — DerekStride tree-sitter-sql plus a regex fallback for grammar-unsupported constructs. Tables surface as KIND_TABLE with each column as a KIND_FIELD child carrying the full source-true column line as signature (email TEXT NOT NULL UNIQUE); views, materialized views, composite types, enums, functions, triggers, indexes, sequences, schemas, extensions all parse natively. The regex fallback recovers six constructs the upstream grammar can't parse: CREATE [OR REPLACE] PROCEDURE, CREATE DOMAIN, CREATE TABLE … PARTITION OF parent (modern PG declarative partitioning — child tables vanish without this), CREATE FUNCTION … SECURITY DEFINER and similar exotic modifier orderings, LOAD 'lib', and IMPORT FOREIGN SCHEMA … FROM SERVER … INTO …. Fallback is line-anchored, gated by AST-derived skip ranges (comment / marginalia / literal / block subtrees) so red herrings inside comments / string literals / PL/pgSQL bodies don't surface as spurious declarations, short-circuited by a bytes-level fingerprint check so the typical schema-dump file with no fallback constructs pays nothing, and uses a byte-range guard against double-extracting AST-parsed functions. PL/pgSQL bodies inside AS $$ … $$ parse as opaque dollar-quoted strings — header (name, parameters, return type) extracts cleanly, and parse errors inside function bodies are excluded from error_count so files using := or IF…THEN…END IF don't get mis-reported as broken. PostgreSQL is the primary target; MySQL and SQLite extract tables / columns / indexes / views cleanly with some error_count > 0 noise on dialect-specific syntax (ENGINE=InnoDB, AUTOINCREMENT, inline KEY constraints). MSSQL, T-SQL, Oracle PL/SQL, and BigQuery have partial coverage — bracketed identifiers, GO separators, VARCHAR2, and STRUCT<> / backtick names degrade. Same release adds TypeScript / JavaScript conditional dynamic importsimport('...') calls inside a function / method / control-flow scope contribute to conditional_imports_count, rendered as [+ N conditional includes] next to the imports line, mirroring Python / Ruby / PHP. Top-level dynamic imports are NOT counted (they execute unconditionally on module load).
2026-05-06 v0.7.4: CSS adapter (.css) and SCSS adapter (.scss). Rules carry simple-selector tokens in match_namesfind_symbols(".btn-primary") returns every cascade-relevant rule (top-level, inside @media, themed, descendant in .modal) with the wrapping at-rule visible in the breadcrumb. Pseudo-classes and attribute filters are stripped for matching, so .btn-primary finds the rule whether it carries :hover, [disabled], or sits in .modal .btn-primary. :is(.a, .b) / :where(.a, .b) recurse (additive); :not(...) / :has(...) don't. Native CSS nesting and SCSS & resolve against each parent simple selector — .card { &__header { } } is findable as .card__header; multi-selector parents propagate (a, .link { &:hover { } } is findable as both a and .link). SCSS additionally surfaces @mixin name($args) (callable, gets () in digest), @function, top-level $variable: value, and %placeholder; Sass privacy convention applied — names with leading _ / - marked private. New optional Declaration.match_names field for declarations reachable under several identifiers. [huge] size label ships in the same release — fourth bucket alongside [tiny] / [medium] / [large] with a behavioral twist: files at or above 100 000 estimated tokens collapse to header-only in digest mode, leaving the agg counters intact so the agent can still size the file up. A directory of 50 huge generated/vendored mega-files goes from 1 602 lines of digest output to 52 — measured 31× reduction. outline and show are unaffected. The legend gets one extra clause whenever a huge file appears in the batch: [huge]=body omitted (use \ast-outline outline `). Default ignore list also expanded with.min.js/.min.mjs/.min.cjs/.min.css/.min.html/.map`.
2026-05-06 v0.7.3: outline header now carries the [tiny] / [medium] / [large] size label — the same categorical bucket digest stamps next to each filename. An agent calling outline directly (skipping digest) gets the at-a-glance size signal in plain English alongside the precise ~N tokens count, instead of having to map the raw token number onto a bucket from memory. Header reads # /abs/path.py [medium] (95 lines, ~1,200 tokens, 5 types, 12 methods). Digest output is unchanged.
2026-05-06 v0.7.2: Ruby language adapter (.rb, .rake, .gemspec, .ru, plus Rakefile / Gemfile resolved by exact basename — first adapter to ship basename-matching) covering modules with qualified-form (module Foo::Bar) + old-style nested-module collapse to A::B::C (HIGH-fix from C++ applied: comments don't count as structural children when deciding whether to collapse), classes with < Super superclass + include / extend / prepend mixins surfaced as : Base, include Mod, extend Mod2 on the digest type header (signature stays Ruby-true class Foo < Bar, mixins live only in the digest's MRO clause), methods, def self.foo singleton methods + entire class << self blocks (both render [static]), full operator coverage as KIND_OPERATOR (+ / - / * / / / % / **, == / != / < / > / <= / >= / <=> / ===, & / | / ^ / ~ / << / >>, [] / []=, unary -@ / +@ / !), attr_accessor / attr_reader / attr_writer (one KIND_FIELD per symbol with [accessor] / [reader] / [writer] marker — multi-symbol calls split so each name stays grep-able), alias / alias_method (with [alias] marker and new → old signature), constants (MAX_NAME_LENGTH = 64) as fields, visibility tracked as a state machine across the class body — bare private / public / protected flips subsequent decls; targeted private :foo, :bar and private_class_method :baz retroactively mark named methods (forward + back references both supported); private() with explicit empty parens parses as a call node and still flips state. require / require_relative / load / autoload collected as imports; lazy loads inside method / block / lambda bodies counted into [+ N conditional includes]. Rails associations recognised by defaulthas_many, has_one, belongs_to, has_and_belongs_to_many surface as KIND_FIELD with [has_many] / [has_one] / [belongs_to] / [habtm] markers, by direct analogy to how the C++ adapter recognises Unreal Engine UPROPERTY macros. Other Rails DSL (validates, scope, before_action) intentionally NOT recognised — line drawn at relations because they describe model-to-model edges, not behaviour.
2026-05-05 v0.7.1: agent-prompt snippet (printed by ast-outline prompt, copied verbatim into the EN/RU/ZH READMEs and docs/agents.md) now lists C++ extensions (.cpp/.cc/.cxx/.h/.hpp/.hh) alongside the rest of the supported set. v0.7.0 shipped the C++ adapter but the snippet still claimed the tool only handled the pre-C++ language list, so an LLM agent reading the snippet for the first time on a UE or general C++ project wouldn't learn ast-outline applies. RU and ZH READMEs also got the C++ row in their supported-languages table that was previously only in the English copy.
2026-05-05 v0.7.0: C++ language adapter (.cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++, .ipp, .tpp, .inl, .cppm, .ixx) covering classes / structs / unions / enums (classic + enum class), namespaces with single-chain collapse to a::b::c (anonymous + inline preserved), templates with full / partial specialisation / variadic / template template params, operator overloads incl. conversion operators and the C++20 spaceship <=>, ctors / dtors with classification, public: / protected: / private: access blocks with C++-correct defaults (class → private, struct/union → public), base-class clauses with access + virtual markers, out-of-class member definitions (Widget::draw), #include directives as imports, C++20 concepts. Unreal Engine reflection macros recognised by default: UCLASS() / USTRUCT() / UENUM() / UINTERFACE() attach to the next type, UPROPERTY() / UFUNCTION() to the next member; GENERATED_BODY() family is stripped from the source pre-parse (length-preserving — line numbers stay aligned) so UHT's missing-semicolon convention no longer breaks tree-sitter on UE headers. Synthetic MISSING-; parse errors that tree-sitter emits after every UE macro are subtracted from the reported error count, so valid UE files no longer surface as [broken] in the digest.
2026-05-05 v0.6.8: directory walks now respect .gitignore and .ignore (root + nested, with deepest-wins override semantics including ! negation) and prune a hardcoded list of universally non-source dirs out of the box (.git, node_modules, __pycache__, .venv, .tox, .next, .gradle, .idea, .vscode, .cursor, .zed, .fleet, .vs, *.egg-info/, …). Conflict-prone names (build/, bin/, dist/, target/, vendor/, out/, obj/) are intentionally NOT in the hardcoded list — those go through .gitignore per-repo. When the walker filters anything, output starts with # note: ignored N dirs (basename1, basename2, …) via .gitignore/.ignore + defaults — pass --no-ignore to disable so the agent sees what got skipped and learns the escape hatch. New --no-ignore flag disables the entire filter pipeline (one switch — no per-rule combinatorics like ripgrep's six). New pathspec>=0.12 dependency (MPL-2.0, Apache-2.0 compatible).
2026-05-04 v0.6.7: ast-outline digest legend is now dynamic — only entries whose token shape actually appears in the rendered body are listed. YAML- and markdown-only digests (whose body contains no callables, kinds, markers, or inheritance) emit no legend at all; code batches keep a legend pruned to the subset of tokens that actually surface. The omission rule also drops the legend in the rare case where only L<a>-<b> would fire (e.g. a code batch of pure marker classes with no members) — a one-entry legend documenting line ranges is more overhead than insight when the suffix shape is already obvious from the body. Drops ~200 bytes of noise from yaml/md digests and saves prompt budget when digest output is piped into LLM context.
2026-05-04 v0.6.6: ast-outline show gains a depth dial — --signature (and the long form --view signature) returns header only (docs + attributes + the signature line, no method body), with the mutex-grouped --full / --view full aliases preserving the existing body-extraction behavior as the default. Closes the gap between digest (just symbol names) and show (full body) for the post-digest "I have the name, I want the contract, not the implementation" workflow — and removes the temptation for agents to pipe show through head -80 to peek at signatures of large methods. Doc placement matches outline: /// / JSDoc / Rust before the signature; Python docstrings after with +1 indent. Composes with --no-doc for a bare contract line. New public helper core.render_signature_view(match) and SymbolMatch.decl back-reference. Agent-prompt snippet (English canonical + RU/ZH README copies + docs-site agents.md) gets a closing line on step 3 documenting the flag as a modifier across every show form.
2026-05-03 v0.6.5: ast-outline prompt snippet (and its README copies) reworded to be explicitly cross-vendor — Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5 and OpenAI GPT-5.x (5.3-codex / 5.4 / 5.5). The broad-to-narrow command list lead-in changed from "Stop at the step that answers the question" to a menu/decision-tree framing, avoiding GPT-5.5's process-prescription antipattern while keeping the same scope guard for Claude's literal models. Module docstring of _prompt.py now codifies the cross-vendor invariants future edits must preserve.
2026-05-03 v0.6.4: outline and digest no longer produce empty stdout when every file in a batch fails to parse. Per-file # note: parse error in <path>: <err> lines now go to stdout (matching the existing convention for user-facing failures), so an LLM agent reading stdout sees what happened instead of (no output). Detailed # WARN lines still go to stderr for humans; partial-failure batches keep their existing behavior.
2026-05-03 v0.6.3: conditional_imports_count extended to Python, Rust, Scala — same [+ N conditional includes] marker now flags lazy import inside fn / class bodies (Python), use inside fn / closures (Rust), and method-scoped import (Scala).
2026-05-03 v0.6.2: PHP language adapter (.php, .phtml, .phps, .php8) targeting modern PHP 8.x and the 7.4 LTS line — namespaces, classes (abstract / final / readonly), interfaces, traits, PHP 8.1 enums, magic ctor / dtor, PHP 8.0 ctor property promotion, multi-variable properties, PHP 8.3 typed class constants, PHP 8.0 #[Attr] attributes, use (incl. grouped) and top-level include / require. Verified on real WordPress core (no parse errors on files up to 291 KB). New common-IR field ParseResult.conditional_imports_count — renderers append [+ N conditional includes] to the imports line when the file has dependencies that aren't statically listed.
2026-05-03 v0.6.1: PyPI metadata refresh after the GitHub Organization transfer (no code changes).
2026-05-03 Repository transferred from dim-s/ast-outline to the ast-outline GitHub Organization. Old dim-s/ast-outline URLs continue to redirect. Copyright remains with Dmitrii Zaitsev (dim-s); the org is hosting infrastructure, not a new copyright holder.
2026-05-03 v0.6.0: relicense from MIT to Apache License 2.0. Documentation separately licensed under CC BY 4.0. The previous MIT text is retained in LICENSE-MIT for compatibility with downstream forks of the 0.5.x tree.
2026-05-02 First publish to PyPI as ast-outline. v0.4.2 / v0.4.3 / v0.5.0 (code-outline CLI alias dropped) / v0.5.1 (implements command dropped — outline/digest already render : Base) / v0.5.2 (--imports flag) / v0.5.3 (--version flag).
2026-05-01 v0.4.0: digest method markers ([async] / [unsafe] / [const] / [suspend] / [static] / [abstract] / [override] / [classmethod] / [property]); type modifiers, attrs, and [deprecated] tag.

April

Date Event
2026-04-30 YAML adapter; per-file size labels + token estimate in digest headers; Rust adapter.
2026-04-28 # note: … LLM-friendly error contract on stdout with rc=0; substring matching for Markdown headings.
2026-04-25 Go adapter.
2026-04-24 Scala adapter. Renamed code-outlineast-outline (v0.3.0). GitHub repo renamed to dim-s/ast-outline.
2026-04-23 Kotlin adapter; prompt subcommand.
2026-04-22 Repository created on GitHub as dim-s/code-outline. First public commit, v0.2.0b0. Russian and Chinese READMEs added; TypeScript / JavaScript adapter shipped same day.