Releases: luizgmassa/massa-ai
Release list
v1.15.0
Added
skills/duplication and reachability now ship as a committed regression gate.
scripts/skills-duplication-metric.tsmeasures shingled line duplication across every
skills/Markdown file (duplicatedLinesis the total footprint,excessLinesis the
removable figure —length × (copies - 1)— and the two are now reported and tested
separately after an earlier draft conflated them) andscripts/skills-reference-graph.ts
resolves everyskills/file against the rest of the repository to flag orphaned or
weakly-referenced files. Both ship with unit tests reached bybun run test:scripts, and
excessLinescarries a ceiling assertion so future duplication growth fails the gate
instead of accumulating silently; a snapshot-equality assertion was rejected because it
would go red on any unrelated edit.
Fixed
skills/AGENTS.md's Agent Table hand-named a model per specialist in a second location,
already stale for two roles and unresolvable or profile-ambiguous for two more —
skills/model-profiles.jsonis the only place that should name a model or effort level
for any agent on any host. The "Model hint" column is removed in favor of a pointer to the
charter's ownmetadata.model_tier, and a scripted, mutation-demonstrated scan now bans
any model display name or model ID from that file.- Three citations across two shipped Maestro reference files pointed at one developer's
home directory (references/maestro.md,references/maestro/fact-ledger.md, one
instance named as a tier of the fact ledger's evidence taxonomy), unreachable for any
other developer, CI runner, or session, and copied verbatim into all four
apps/*-plugin/skills/bundles. The coverage-checklist rule is rewritten to a reachable
location instead of being dropped, and a repo-wide scan now fails on any/Users/or
/home/path underskills/. references/agent-orchestration.md's Roles table omittedjudgeandmeta-judge
for a full release cycle, invisible to the existing guard because it validated only the
charter paths already mentioned there. Both charters are now documented, and a new
coverage test asserts every directory underskills/agents/has a Roles table entry.- The sub-agent roster guard could not match the string it existed to ban. It rejected
the literal patterns16 subagent,16 [Ss]pecialist, and16 reusable sub-agent, but
the hyphenated16 sub-agent specialistsindocs/ONBOARDING.mddefeated all three while
CLAUDE.mdseparately still read "15" — both passed the guard. It now rejects any count
other than 17 in that shape, with an explicit historical allowlist for legitimate
past-tense narration, and all four stale counts found by scanning
(CLAUDE.md,docs/ONBOARDING.md,FEATURES.md,.claude-plugin/marketplace.json) are
corrected.
v1.14.0
Changed
-
Model and effort for the 17 subagent specialists now resolve from one registry, and
three separate defects closed with it.skills/model-profiles.jsonis the only
hand-authored place that names a model or an effort level for any agent on any host;
each charter declares ametadata.model_tier(replacingmetadata.model_hint) and
scripts/lib/model-profiles.tsresolvestier + host + profile → {model, effort}at
generate time. Before this, 184 of 304 model facts were hand-copied across six surfaces,
and the four per-host tables ingenerate-subagent-artifacts.ts,
subagent-parity.test.tsandFEATURES.mdcould — and did — disagree.Three shipping defects, not one cleanup:
- Cross-host tier drift on three roles, changing five shipped pins.
navigator,
requirements-analystandplannereach carried contradictory tiers across hosts.
The sharpest proof was insideFEATURES.mditself:navigator's Claude and Codex
rationale cells held the same sentence — "no frontier reasoning needed" — beside a
standard-tier model on one host and a light-tier one on the other. Normalized to one
tier per role, sonavigatordrops to the light tier on Claude,requirements-analyst
rises to standard on OpenCode, andplannerrises to deep on OpenCode. - The Cursor emitter had never produced a valid agent file. It emitted
toolsand
reasoningEffort— neither is a Cursor frontmatter key — plus a human-readable
display name inmodel:where Cursor requires a model ID, and noreadonly, which
is Cursor's only documented permission mechanism. All 17 Cursor agents now emit only
Cursor's documented fields, withreadonly: trueon the 14 read-only charters.
model:isinheriton every tier: Cursor publishes no display-name→ID mapping and
its catalog lists no entry for two of the three models previously pinned there, so
inheritis the documented default rather than a guessed ID.cursor-agent modelsis
the discovery path, and restoring differentiation is a registry-only change. - OpenCode was sending
nameandmetadatato the model provider as bogus model
options on every subagent invocation. OpenCode's documented rule forwards
unrecognized frontmatter keys to the provider as model options, so these were not
inert — this was live behaviour, not hygiene.name:is gone (OpenCode takes the agent
name from the filename, which already yieldsmassa-ai-<n>), and the
massa-ai-owned: trueownership marker moves from frontmattermetadata:to a
<!-- massa-ai-owned: true -->body comment. It moves rather than being deleted
becausemassa-ai-config agents uninstallscopes by that literal substring: removing
it would have matched zero files, printedremoved 0, and orphaned 17 installed
agents. The substring is unchanged, so uninstall also still matches agent files an
older version installed in the frontmatter form.
Profiles are open data: adding, renaming or removing one is a registry edit with no
TypeScript type, enum, or doc table to update. Selection is--profile=<name>>
MASSA_AI_MODEL_PROFILE> registryhostDefaults[host], with no fourth rank — an
unknown name at any rank is a named error, never a silent fallback to a default model —
andvalidateRegistryreports every violation in one throw instead of one per run.
Resolution is build-time because no host resolves a per-agent model from an env var;
switching profiles means regenerating. One consequence is documented rather than worked
around:CLAUDE_CODE_SUBAGENT_MODELset to a real model silently defeats every registry
pin on Claude, because it overrides frontmatter.FEATURES.mdloses its four 17-row
per-host tables and all four rationale columns in favour of one role→tier table, guarded
by a doc-drift test;bun run verify:model-idsprobes the installed harness CLIs for
unresolvable IDs and skips an absent CLI with a named reason instead of passing vacuously.Merge note:
maingained thejudgeandmeta-judgecharters (2 more specialists,
17 total) from a concurrently-merged PR while this branch was in review. Both still
declaredmetadata.model_hintunder the retired convention; merging this branch onto
that state migrates them tometadata.model_tier: deep— matching their original
Claude/Codex pins (opus/gpt-5.6-sol, both this registry'sdeeptier) — and their
Cursor/OpenCode output now goes through the same emitter fixes as the other 15 (Cursor
inheritinstead of the raw charter hint; OpenCode dropsname/metadata).That normalization moves two more shipped model pins, which is a behaviour change and not
bookkeeping: on OpenCodejudgegoesopencode-go/deepseek-v4-pro→opencode-go/minimax-m3
andmeta-judgegoesopencode-go/kimi-k3→opencode-go/minimax-m3. Both had the same
cross-host drift this release removes —judgeshipped deep on Claude and Codex but light on
OpenCode. Note the side effect:judge-with-debatewants per-slot model diversity, and one
tier resolves to one model per host, so the two charter defaults now coincide on OpenCode.
That affects the fallback only — the workflow requests per-slot models at dispatch time
where the host supports it, and names the degraded caseDIVERSITY DEGRADEDper its own
contract. - Cross-host tier drift on three roles, changing five shipped pins.
v1.13.0
Added
judge-with-debateworkflow +meta-judgeandjudgesub-agents (17 specialists). A new
standalone workflow evaluates a user-supplied artifact through multi-agent debate: a meta-judge
authors a tailored evaluation specification once (two-stage syntactic/semantic validation with a
single named retry), three independent judges score it with quoted evidence, and the panel
debates disagreements over up to 3 rounds until consensus (overall gap ≤ 0.5, every criterion
gap ≤ 1.0, explicit accept) or reports an honest no-consensus — never a forced verdict. Judges
communicate through the filesystem only; the orchestrator computes consensus from structured
reply blocks and never opens judge report files. Reports persist under the new
audits/judge/family inaudit-report-io.md(per-judge + consensus contracts, fidelity
checklists). Per-slot model diversity (metakimi-k3, J1deepseek-v4-pro, J2minimax-m3,
J3GLM-5.2) is requested per dispatch; hosts without dispatch-time model selection run charter
defaults and the verdict is markedDIVERSITY DEGRADED, with a per-invocation capability probe
so diversity activates automatically when a host gains the capability. Both charters are
registered across all four plugin bundles (Claudeopus, Codexgpt-5.6-sol, Cursor charter
hint, OpenCodeopencode-go/kimi-k3+opencode-go/deepseek-v4-pro), enforced by a new
workflow prose-contract test and the existing integrity/parity/validate-repository gates.
v1.12.1
Security
- CodeQL code-scanning close-out (SEC-1..SEC-6). All GitHub workflows now declare
least-privilegeGITHUB_TOKENpermissions (contents: readbaseline; publish jobs keep
their elevated per-job grants). Synapse session IDs are generated by a single shared
newSynapseSessionId()helper backed bycrypto.randomUUID()instead of the duplicated,
guessableMath.random()template. The code chunker's inline block-comment stripping is
a linear single-pass scan, removing a quadratic-regex indexing stall on minified or
adversarial single-line files.sanitizeFilePathremoves../traversal tokens to a
fixpoint so overlapping tokens like....//can no longer smuggle a live traversal
segment through a single pass. The unusedisValidEmailexport (quadratic regex, no
production callers) was deleted. Compaction snapshot retrieval calls now interpolate
values viaJSON.stringify, closing a backslash-quote escape gap.
v1.12.0
Added
- Plugin bundles now auto-detect hosts and upgrade only on version change. The
harness plugin phase (the rootinstall.shk)harness menu,
scripts/setup-local-first.sh,scripts/install-harness.sh --plugins) detects each agent host — its config dir
exists or its binary is onPATH— and installs only detected hosts; absent hosts
produce one skip log line and no filesystem writes. Every successful plugin install
records the bundle version and an ISO-8601 timestamp in
~/.config/massa-ai/install-state.json(a v2-compatible extension that
install-skills.shround-trips but never writes): re-runs at the same version are
no-ops, older recorded versions upgrade automatically, newer ones are never
downgraded, and a host whose installer fails records nothing and never aborts the
remaining hosts.--dry-runreports the per-host decision (install / upgrade /
skip-current / skip-absent) without writing anything, and--uninstallremoves the
version record while keeping its existing semantics (whole-record delete only for
plugin-owned platforms). Directapps/<host>-plugin/install.sh --userinstalls
behave exactly as before, plus the same version recording.
v1.11.0
Changed
-
The search subsystem's first capability module is extracted, and the split is now measured rather than asserted.
rlm-fusion.tsbecomesresult-fusion.ts:fuseResultsandgenerateScoreExplanationtake no
dependency record at all. The only facade member either ever read wasRRF_K, the literal60,
now a module constant — so the module has never heard ofContextualSearchRLM, which is the
propertyscripts/search-hub-metric.tsmeasures. Foreign modules reading the facade drop from
6 to 5. Deepest foreign reach stays at 14 and the gate still fails, both expected: the
maximum is set byrlm-search.tsand cannot move until that file is split. Three of the four
frozen needle anchors live in this file and travelled byte-identical — resolution is by content,
so moving an anchor is safe and reflowing one is a hard gate failure. -
Before-baselines are frozen to committed fixtures instead of measured against the working tree.
The previous sensors recorded their before-state as unit tests that scan the live directory, which
holds only until the refactor those baselines exist to police begins — one extraction reddened five
of them, and at the target state the scanned set is empty by design. Updating the pins per task
would have turned a before-record into an after-record tracking whatever the change produced,
leaving the final comparison with no referent.scripts/capture-facade-baseline.tswrites the
matrix, fan-in/fan-out and anchor records once and refuses to run when the measured subject has
moved; the suites assert that attestation, so a silent re-capture fails loudly instead of
relocating the reference. Scoped to the nine figures that actually change — assertions about
untouched directories, and those written as floors rather than pins, still measure the live tree. -
The frozen-anchor check now pins anchor text, not anchor paths. It asserted that four needles
resolved to two named files, which is the opposite of the constraint it enforces: anchors are
resolved by content, so moving one between files is explicitly legal and only reformatting is
not. The path pin therefore failed on the permitted operation and caught nothing the uniqueness
check above it did not already catch.
v1.10.0
Changed
-
The shipped skills now carry their implementation obligations in the references every workflow already loads, instead of re-stating them per workflow. Six updates (SWU-01..06) consolidate onto the two load-line references, so the rules cannot drift out of sync with the workflows that inherit them.
references/code-annotation.md§3 now excludes data/domain models — ORM and persistence entities, schema-mapped classes, repository entities, and behaviorless value objects — from unit testing, because their behavior lives at the repository/service seam, not in the fields; the exclusion is named by kind and language so it cannot be read as "skip tests on anything called a model" and does not weaken test-every-changed-path for code that has behavior.references/implementation-delivery.mdmakes worktree isolation mandatory for every implementation task with no size exemption. The commit, spec-driven, and ticket workflows were tightened so a Jira Phase/Wave maps to one Task with per-Task sub-tasks, branch names follow the Phase/Wave, and each Task is one[KEY]-prefixed commit, with the three prefix sites (branch, commit, PR) cross-linked rather than duplicated. -
spec-driven now enforces the target repository's own rules and requires dependency injection per task and per phase. A new
references/repo-rules-discovery.mddefines how the workflow discovers and loads a target repo's AI-harness rule files (.claude/,.cursor/,.cursorrules, and the repo's module/unit-test/testing-area conventions) before implementation, enforces conformance, and recordsrepo-rules: none presentrather than fabricating rules when none exist. Independently (DI-01),references/spec-driven/tasks.mdandreferences/spec-driven/execute.mdnow require dependency injection at both the per-task and per-phase granularity. Both are inherited through the existing load lines; no workflow body was duplicated, and the four host skill bundles (claude, codex, cursor, opencode) were regenerated from the single source.
v1.9.2
Fixed
-
A structural refactor of the search subsystem is now measurable, not asserted.
contextual-search-rlm.ts
was split once before, in M14, and the split moved code without moving responsibility: the host
file went from 1668 to 463 lines while every extracted delegate took the class straight back as
its first parameter. Lines-per-file — the only metric anyone watched — reported that as a
success.scripts/search-hub-metric.tsmeasures what it missed. For everyclass,interface
andtypedeclared in a directory it countsmaxForeignReach: the most distinct members any
single other module reads off a binding of that type. Run across M14, the numbers invert the
verdict — deepest foreign reach went 1 → 14 while the member count stayed flat (26 → 24).
M14 did not widen the type; it multiplied who reaches into it.The script takes no type name, deliberately. An earlier version audited one hardcoded name and
was evaded two ways: renaming the class produced a vacuous0/0pass, and moving the same state
onto a differently-named aggregate record made the identical hub invisible. Enumerating every
declaration closes both, and each has a regression test. One evasion is documented as surviving —
passing collaborators as N individual parameters drives reach to 0 with coupling unchanged — so
the gate is stated as necessary, not sufficient.It ships with 13 unit tests, one per defect found while building it. That is not ceremony: the
measurement method was wrong four separate times, and two of the defects pushed in opposite
directions, so the reported figure was stable across runs and commits and still incorrect. The
worst of them stripped string literals, which pairs quotes across unrelated apostrophes and
deleted real code — under-reporting by 42% on one commit and 0% on the others, which is exactly
the shape that survives cross-commit comparison. Stability is evidence of determinism, not of
correctness. -
Four more sensors, so the refactor's before/after can be taken at all.
scripts/search-facade-matrix.tsrebuilds the facade's member→consumer matrix from source;
scripts/search-facade-metrics.tsmeasures fan-in and fan-out overgit ls-files;
scripts/check-frozen-anchors.tsverifies every needle anchor still resolves to exactly one
location, in under a second, instead of only at the Ollama-backed retrieval gate; and
scripts/check-characterization.tspins the three behaviors that have exactly one real test
each, by assertion count rather than by the presence of adescribename a hollowed block
keeps.benchmarks/needles/run.tsnow records each needle's rank and its resolved target into the
report. Both were computed and then dropped on write, which made a report uncomparable across
exactly the kind of change it exists to measure: recomputing an old report's rank against a
renamed tree resolves the anchor to its new home, matches nothing in the old hit list, and
reads as a miss on every needle — a total collapse manufactured by the measurement rather than
observed by it.The recurring lesson, hit three times while building these: a reading can be an artifact of the
state it was taken in. One suite was green at 17 pass / 0 fail while its own files were
untracked, and it enumeratesgit ls-files— so it was blind to itself, and tracking it moved
the number it reported.
v1.9.1
Fixed
-
Indexing no longer aborts when one file declares the same name twice with different
kinds.let totalin one block andconst totalin another — orclass Xalongside
interface X, which is ordinary TypeScript declaration merging — made the whole index
fail withfqn_identity_collision, discarding every file in the run. Measured on this
repository: the abort came after all 1219 files had been discovered and parsed, roughly
four seconds in, so the visible symptom was a project that simply never became
searchable. The uniqueness check that decides whether a symbol gets the simple
file#nameidentity or a disambiguated one was counting names per(file, name, kind)
while the identity it protects is keyed on(file, name)alone; two declarations
differing only in kind therefore each believed themselves unique and both claimed the
same identity. Same-name/same-kind declarations were already disambiguated correctly and
are unaffected. Symbols that do not share a name keep the identity they had. -
An allow-list with exactly one file extension no longer matches nothing. The three
file scanners built a single combined glob,**/*{.ts,.js,…}. A brace expansion with one
alternative is not an alternation — it is matched literally — so a one-extension allow-list
found zero files and indexing reported success over an empty corpus. Measured: a bounded
index completed in 181 ms over 0 files. This was unreachable while the extension list was
always the 33 built-in defaults, and became reachable with thesecurity.allowedExtensions
fix below; a single-language project is exactly the case where someone sets one extension. -
capturePolicyinconfig.jsonnow actually reaches indexing. The block was parsed,
bounds-checked anddenyUnknownFields-validated at config load, and then never consulted:
applyCapturePolicyhad no caller anywhere in the product, so a configured policy narrowed
nothing. Discovery now applies it after the.gitignoremerge, which is the composition
ignore-patterns.tshas documented since the policy was introduced. With no policy
configured the built-inDEFAULT_POLICYapplies and itsDropset mirrors the default
ignores, so a default install discovers exactly the same files as before.Keepcannot
resurrect a path.gitignoreexcludes — the two layers compose with AND, and the ignore
layer runs first. OnlyDropexcludes;MetadataOnlyfiles are still discovered. -
security.allowedExtensionsinconfig.jsonnow actually narrows what gets indexed.
Setting the key had no effect whatsoever: the assembled config hardcoded the built-in
default list and never read the value, and the user-facing config type did not declare
the field at all — so the value was parsed, then discarded. The indexer, the search index
scanner and the MCP upload collector all read the assembled value, so every consumer saw
the defaults no matter what the file said. Omitting the key still yields the same 33
default extensions, so installs that never set it are unaffected. An empty array is now
rejected at config load rather than honoured: it would match no files, and indexing
would report success over an empty corpus. Entries must be dot-prefixed (.ts, notts). -
search_memorieshonoursincludePersistent. The option has been advertised in the
published MCP tool schema — and forwarded from the tool to the controller — while
nothing ever read it, so a caller passingfalsesilently received persistent memories
anyway. It now excludes L0 (MemoryLevel.PERSISTENT) memories: the level assigned to
orchestrator decisions and criticals, and the one the bootstrap seed writes. The
published schema is unchanged; this makes the existing advertisement true rather than
altering it. Callers passingincludePersistent: falsetoday will see a smaller
result set. The default remainstrue, so callers that omit it are unaffected.
v1.9.0
Changed
-
BREAKING — the ten
RLM_LLM_*environment variables are renamed toMASSA_AI_LLM_*.
The old names no longer do anything. There is no dual-read and no deprecation window:
an unrenamed variable is silently ignored, and the setting falls back to its default —
which forRLM_LLM_ENABLEDmeans every LLM-driven feature degrades to its rule-based
path.RLM_was the last surviving prefix from the pre-rename project identity, kept as
a compatibility boundary by two earlier renames; that boundary is now retired
(AD-010). Update any.env, shell profile, CI secret, compose file, or systemd unit:Retired Replacement RLM_LLM_ENABLEDMASSA_AI_LLM_ENABLEDRLM_LLM_BASE_URLMASSA_AI_LLM_BASE_URLRLM_LLM_API_KEYMASSA_AI_LLM_API_KEYRLM_LLM_MODELMASSA_AI_LLM_MODELRLM_LLM_CODE_MODELMASSA_AI_LLM_CODE_MODELRLM_LLM_TEMPERATUREMASSA_AI_LLM_TEMPERATURERLM_LLM_MAX_OUTPUT_TOKENSMASSA_AI_LLM_MAX_OUTPUT_TOKENSRLM_LLM_TIMEOUT_MSMASSA_AI_LLM_TIMEOUT_MSRLM_LLM_DISABLE_THINKMASSA_AI_LLM_DISABLE_THINKRLM_LLM_PROMPTMASSA_AI_LLM_PROMPTconfig.jsonis unaffected — its keys were already prefix-free, so there is no
config migration and no file to edit. Only environment variables change.The rename also fixed a bug it would otherwise have carried under a new name: only four
of the ten were listed inturbo.json'spassThroughEnv, so the other six arrived
undefinedin any test run underbun run testwhile working fine whenbun testwas
invoked directly. All ten are listed now. -
Coverage is no longer computed on every
bun test.bunfig.tomlset
coverage = true, so every invocation — including a single-file run in a debug loop —
paid to instrument and report coverage, and nothing ever failed on the result: the cost
of a gate without the gate. Coverage is now opt-in (--coverage) and enforced by the
new explicit gate below. -
The three divergent copies of
run-tests-isolated.tsare one shared module.
packages/core,apps/tools-apiandapps/mcp-clienteach carried their own copy
(236 / 124 / 141 lines) which had drifted apart; they now share
scripts/lib/run-tests-isolated.tsand supply only their own isolation predicate.
Observable behaviour is deliberately unchanged, including each package's distinct log
wording and its exit code for unknown arguments.
Added
-
bun run lintis a real gate. It was declared inturbo.jsonbut implemented by no
package, so it printed "No tasks were executed" and passed unconditionally. It now runs
oxlint (pinned exactly to1.76.0) with thecorrectnesscategory on and every other
category off. Adoption surfaced 337 violations; all 337 were fixed rather than
downgraded, so every correctness rule ships aterrorrather thanwarn. It runs over
the whole repository rather than per package, because turbo dispatches only to workspace
packages andscripts/andbenchmarks/— which held 21 of the 337 — are neither. CI
runs it in thebuildjob. No formatter was added and nothing was reformatted. -
bun run test:coverageenforces a coverage floor. 90% line coverage per file, with
nine documented exclusions, each carrying the justification that earned it. Both the
floor and the exclusions live inscripts/check-coverage.tsas executable data rather
than as prose in a scratch file that gets rewritten. The gate refuses to run unless the
dedicated test database is configured, because 50 core suites are gated behind
describe.skipIf(!DEDICATED_DB)and would otherwise report near-zero coverage for
subjects whose own tests are sitting skipped beside them. It also runs the suites against
a scratchXDG_CONFIG_HOME, so the numbers are a property of the tree rather than of
whatever is in the developer's~/.config/massa-ai/config.json.
Fixed
- Unit tests no longer make live LLM calls against the developer's own configuration.
CodeCompressorresolvesllm.enabledfrom~/.config/massa-ai/config.json, so on a
machine with a local Ollama configured, tests that construct it without pinning the
test seam issued real network requests — measured at 42 s on a cold model load against
a 5 s per-test budget, and 0.7 s once warm. That is why they looked flaky rather than
broken, and why CI, which has no configuration file, never saw it. The affected tests
now inject the seam their subject already exposes.