chameleon 2.50.0
Real-usage QA pass over the whole plugin, driven through its actual entry points
(hook executables fed real payloads, the MCP server over real stdio JSON-RPC, and
the statusline/merge/daemon surfaces) against real bootstrapped repositories in
TypeScript, Ruby, and Python. Five parallel adversarial QA passes confirmed the
newest surfaces are solid — the v2.49.0 TypeScript DI call edges and the v2.47.0
WP-C5 cross-workspace advisory both survived hostile re-testing with every
fail-open holding, every v2.46–v2.49 fix claim held under original-repro plus
adjacent attack, and the blocking/enforcement surface passed with zero P1s. The
gaps found were honesty gaps on older comprehension and diagnostic surfaces: a
profile built before a schema bump would silently degrade while the tools that
report health said everything was fine. This release closes them.
Added
-
Class and module names are searchable (
search_codebase). The symbol
index held callables only, so "find classFoo" (or a Rubymodule) returned
nothing — a class only surfaced if it happened to be a called constructor. The
index now records class and module definitions with their file and line
across all three languages: TypeScript and Python from each extractor's
class_shapes(with a newstart_line), Ruby from its class and module nodes
(a new emission).search_codebasereturns them asclass Name(Base) — file:line, and a class that is also instantiated keeps its real definition
line rather than the caller-graph fallback's line-less row. The class section
is additive to the symbol-signatures artifact — an index built before it
simply has no classes until the next refresh, and describe/nearby-signature
consumers that count or render callables are unaffected. -
Python module-attribute call edges (
module_attributegrade): a
mod.func()call wheremodis a submodule bound byfrom pkg import mod—
the dominant Django/FastAPI layering idiom (from app import crud; crud.create_user(...)) — was invisible to the calls index, soget_callers,
get_callees,get_blast_radius, and the turn-end correctness judge's caller
grounding came up empty on it. This is the Python analog of the TypeScript
typed_property(v2.49) and Rubyconstant_receiveredges. The index now
resolves the receiver to the submodule it names and records the edge on the
method defined there. Deterministic and false-positive-free: the receiver must
be a from-imported name whosepkg.modspecifier resolves to a real in-repo
module file (a name that is a callable or class from the package's__init__,
not a submodule, resolves to no file and yields nothing), and the target must
be a callable defined in that module. Validated on two real Python repos —
47 edges on the FastAPI template and 67 on a Django app, with zero false edges.
Additive to the calls-index schema (a new grade value only), so an old index
loads unchanged and simply gains these edges on the next refresh.
Fixed
-
doctornow catches a stale-schema index, not just a missing or corrupt
one. Theprofile_artifactscheck parsed each generated artifact as JSON but
never applied the loader's schema gate, so a pre-v2.41calls_index.json(or
exports_index/reverse_index) — valid JSON at the oldschema_version—
read as "present and parseable" while every calls and cross-file tool silently
returned nothing.doctornow calls each version-gated artifact's real loader
and warns "unreadable by this engine (stale schema or oversize); run
/chameleon-refresh" when it rejects the file. Each loader honors its own
readable-version set, so a still-accepted version is never false-flagged. -
The caller-graph tools distinguish a stale index from a never-built one.
get_callers,get_callees, andget_blast_radiusreturned a flat
no-calls-indexfor both "the profile predates the calls index" and "the index
is present but on an older schema" — indistinguishable, and only the latter is
fixed by a refresh. They now returncalls-index-stalewhen the artifact is on
disk but the loader rejects it, so the result says whether /chameleon-refresh
will help. -
describe_codebasemarks its answer degraded when the symbol or calls index
is absent, not only when it is corrupt. A profile built before those
artifacts existed reportedfile_count: 0, symbol_count: 0, god_symbols: []
next to a populated archetype list with no degraded flag, reading as a verified
empty repo. Absent now setsdegradedthe same way corrupt already did. It
also flagstruncated+degradedwhen the file count sits at the signatures
artifact cap, so a capped count on a large monorepo is not reported as the
repo's true total. -
get_callers/get_calleescarry the "absence is not dead code" caveat on
an empty result, the same noteget_blast_radiusalready returned, so a
consumer echoing the payload never reads an empty edge set as proof a symbol is
unused. -
doctorcollapses stale registry rows for one path. A repo re-bootstrapped
under a new identity (a uuid or id-scheme change) left older rows in the
registry, soknown_reposlisted the same path several times with
contradictory trust states. It now shows each path once — the most recent,
active profile — with astale_registry_entriescount and a note when the
older rows disagree on trust. -
The per-edit "inbound callers" section ranks cross-file dependents first.
With a small export cap, a cluster of same-file callers (a class's private
methods calling each other) could crowd the section and push out the one
export whose caller lives in another file — exactly the cross-file break the
section exists to warn about, and the one a same-file edit can't see for
itself. Exports are now ordered by their cross-file caller count before the
cap applies, and cross-file call sites lead within each export's own list. -
CHAMELEON_ENFORCE=0is documented accurately. Its env-var reference said
"advisory-only regardless of enforcement.mode," but the Stop backstop returns
nothing under it (a deliberate, test-encoded contract) — advisory-only holds
at the per-edit hooks, while Stop is fully silent. The doc now states that and
points atenforcement.mode=shadowfor turn-end advisories without the block. -
The
session-startandposttool-recorderhooks no longer leak a traceback
when the process working directory was deleted (a git-worktree removal or
repo move mid-session). Both fell through toPath.cwd()/os.getcwd(),
which re-raise the sameFileNotFoundErrorthe fallback was meant to catch;
the traceback landed in the error log that/chameleon-doctorreads for
degraded health, so a benign deleted directory looked like a broken install.
Both now route through a guarded cwd helper and fail open cleanly. -
get_statuslabels its degraded-delivery counters as user-global. The
no-interpreter and spawn-failed counts come from per-user logs (a
no-interpreter failure happens before any repo resolves), so three different
repos returned byte-identical degraded blocks. The block now carries
scope: "user-global"so it is not read as this repo's alone. -
explain_editno longer mislabels a fired credential rule as a coverage
gap. On a no-archetype (fallback/none quality) file, the classification put
the coverage-gap check ahead of the "a rule fired" check, so a leaked
credential the scanner flagged — and a human waved through with
chameleon-ignore— replayed ascoverage-gap, whose remediation ("refresh
so an archetype resolves") is the wrong route. Fallback/none quality drops the
archetype-shape rules, so a violation raised there is necessarily an
archetype-independent rule (a secret, an eval) that did fire; it now classifies
asadvised. A true coverage gap (no archetype and nothing fired) is unchanged. -
The refresh/bootstrap response no longer balloons on a large monorepo.
Each workspace's WP-C5cross_candidateslist (an internal cross-package
JOIN input the coordinator consumes before the response is built) was serialized
in full into the envelope — the one uncapped per-workspace aggregate — so a
20-workspace repo where packages import many siblings pushed the refresh
response past ~1.9MB and blew MCP transport limits. The envelope now emits a
cross_candidates_countinstead of the full list; the in-memory JOIN (and the
cross-workspace existence index it builds) is unchanged. -
The
teach_profileMCP tool now forwards itsarchetypeargument. The
MCP wrapper exposed only(repo, feedback), so an archetype-scoped freeform
teach silently wrote an untagged idiom — the underlying function implemented
the scoping, but no shipping interface delivered it. The wrapper now passes
archetypethrough. -
teach_profileis idempotent on an identical idiom. Re-teaching the exact
same feedback (a user repeat or a skill retry) appended a duplicate — the slug
guard only avoided a duplicate header, not a duplicate body — so the same idiom
rendered two or three times in every injected block. An identical body already
in the active set is now a no-op (already_present: true); a body sitting under
## deprecatedstill re-activates. -
doctor's judge-spawn-health message counts distinct sessions, not
attestation records, and therepo_uuiddocstring no longer claims a moved
checkout keeps its trust grant (the uuid stabilizes the repo identity; a
whole-repo trust grant is root-path scoped, so a moved checkout can still need
one/chameleon-trust).