Skip to content

Release history

What’s new

What shipped in the six flagship trusty-tools crates, generated from the crates’ own CHANGELOG.md files.

trusty-memory

Memory palace storage

11 releases. crates/trusty-memory/CHANGELOG.md on GitHub is the source this section is generated from.

0.22.0 2026-07-27

MINOR, not the patch 0.21.3 this was originally staged as (#4177). This crate publicly re-exports trusty-common items — src/palace_id_derive.rs:19:

pub use trusty_common::palace_id::{
    derive_palace_id, owner_repo_from_git_remote, palace_override_from_env,
    parent_dir_slug, PALACE_OVERRIDE_ENV,
};

The module is declared pub mod palace_id_derive; at lib.rs:140 with no cfg gate and no feature guard, so the whole shim is unconditional public API. Raising the trusty-common requirement from ^0.26.2 to ^0.27 changes the identity of publicly re-exported items, which at patch level would let ^0.21.2 re-resolve already-published consumers onto the new identity — the same shape that forced the trusty-analyze 0.7.3 yank. ^0.21 excludes 0.22.0, so published consumers keep resolving to 0.21.2 and stay installable.

Consumer pin updated in the same change: trusty-agents required trusty-memory = "0.21.1", i.e. ^0.21.1 = >=0.21.1, <0.22.0, which 0.22.0 does not satisfy — left alone it would have traded one red for another. It is now "0.22". trusty-agents is unpublished (0.38.6), so this is a requirement edit only and its own version is untouched.

Changed

  • trusty-common requirement raised to ^0.27 (was ^0.26.2): 0.27.0 makes ChatEvent #[non_exhaustive], which a ^0.26 requirement cannot express. Because the re-exports above are public, this requirement change is itself the reason for the MINOR level.

Fixed

  • Post-publish source drift — the SSE chat handler did not compile against the ChatEvent::Usage variant. src/chat/handler.rs gained its ChatEvent::Usage(_) arm in #4112, after 0.21.2 was published, so the published 0.21.2 artifact cannot build against any trusty-common carrying that variant. This release ships the arm. The match also gained a wildcard arm now that ChatEvent is #[non_exhaustive], so the next variant addition is no longer breaking here.

0.21.2 2026-07-26

Fixed

  • slim build (--no-default-features) now compiles: tools::dream_ops reached the user-config loader through the axum-server-gated crate::web:: re-export, breaking any dependent that opts out of axum-server (e.g. trusty-agents, which uses default-features = false). Now routes through the axum-free crate::service::load_user_config like chat_provider() already does (closes #2049).
  • decouple recall/remember from embedder warm-up (closes #1970) (#1972) (bb322d4)
  • palace-level alias resolution for claude-mpm parity (owner-repo -> bare palace) (#1945) (af7f904)
  • stop console_metrics force-opening every palace on poll (closes #1924) (#1926) (74e9e54)

0.21.1 2026-07-24

Fixed

  • trusty-memory service install wrote the LaunchAgent plist but never loaded (bootstrapped) it (#3832, demo-critical): every sibling daemon's service install (trusty-search/trusty-analyze/trusty-review) both writes AND loads the agent in one step, and trusty-installer's post-install bootstrap step depends on that uniform contract — it shells out to <binary> service install for every launchd-managed member and treats a clean exit as "installed and bootstrapped". trusty-memory alone split "install" (write-only) from "start" (write+load), so a fresh-machine tctl install reported success while ~/Library/LaunchAgents/com.trusty.memory.plist sat on disk unbootstrapped and absent from launchctl list — and the installer's later launchctl kickstart -k recovery retry then failed outright (kickstart cannot force-start a label that was never bootstrapped), surfacing as a bare, undiagnosed down with no (kickstarted) qualifier. service install now calls LaunchdConfig::bootstrap() after writing the plist, exactly like its siblings; a bootstrap failure now propagates as an Err (never swallowed) instead of being silently skipped. service start is kept as an idempotent alias of service install for backward compatibility with existing scripts/docs. Verified manually (cargo run -p trusty-memory -- service install + launchctl list); there is no trait seam in trusty_common::launchd::LaunchdConfig yet for unit-testing install-then-bootstrap sequencing in isolation — tracked as a follow-up in trusty-installer's CHANGELOG. trusty-installer 0.4.8 adds an independent installer-side defensive fallback (verifies launchctl list itself and force-bootstraps if needed) so this fix is not the only thing standing between a demo and #3832 recurring.
  • serve --stdio --palace <default> never reached real MCP tool calls: inject_default_palace (commands::serve_stdio_bridge) only wrote the default into top-level params.palace, but a real MCP client (Claude Code) sends the standard tools/call envelope (method: "tools/call", params: {name, arguments}) and tool handlers read arguments.palace — so every real tools/call request reached the handler with no palace at all, surfacing as -32603: memory_recall: missing 'palace' (no --palace default configured) even with --palace supplied on the CLI. inject_default_palace now mirrors the sibling inject_caller_context's dispatch-shape branching: it injects into params.arguments for tools/call requests, and keeps the pre-existing top-level params.palace injection for legacy direct method-per-tool requests. Caller-supplied palace values are never clobbered either way.
8 earlier releases
  • 0.21.0 2026-07-23
  • 0.19.2 2026-07-09
  • 0.20.0 2026-07-21
  • 0.17.0 2026-06-25
  • 0.15.5 2026-06-16
  • 0.15.2 2026-06-09
  • 0.15.1 2026-06-05
  • 0.15.0 2026-06-03

What each of these changed is in crates/trusty-memory/CHANGELOG.md.

trusty-mpm

Multi-agent orchestration

31 releases. crates/trusty-mpm/CHANGELOG.md on GitHub is the source this section is generated from.

1.3.5 2026-08-10

Added

  • BASE-AGENT.md (synced to trusty-code) adds a "Never Directly Monitor a Declarative Process" rule: run gates in their quiet/filtered form (--quiet, tail/grep for the summary line, or tm compress --tool "<tool-name>"), re-run only the failing case with full output, and never gh pr checks --watch. Prompted by two agents burning 415k and 546k tokens streaming per-test and per-check-poll output directly. States explicitly that filtered output is still raw output — the evidence rule is unchanged

  • BASE-AGENT.md (synced to trusty-code) now states a "No Subagent Fan-Out" rule: a delegated agent does its own work or reports back to its dispatcher — it never spawns its own subagents. The Agent/Task tool is reserved for the top-level PM/orchestrator. Prompted by a rust-engineer spawning an untyped child agent that bypassed the roster entirely for routine documentation work

  • BASE-AGENT.md (synced to trusty-code) adds "Agent-Authored Prose", extending the PM's "Write Plainly" register (core.md, #4757) to the agent side: review verdicts, reports back to the PM, ticket/PR body text, and generated documentation — lead with the concrete referent, state cause then effect, show before-and-after, cut hedges and process narration, end options as a bare enumeration. Governs how, never whether: the evidence rule is unchanged

  • BASE-AGENT.md (synced to trusty-code) Agent-Authored Prose adds "never announce the register you're writing in" — no heading or preamble that labels the writing as plain, honest, direct, candid, blunt, or unvarnished (e.g. "What remains unknown, stated plainly:"); state the fact in place instead. Same family as the banned word "honest" — the label is forbidden, not the disclosure

  • core::claude_md_writer — the single owner of CLAUDE.md section-override writes, the write-side counterpart to the reader-only core::claude_md_sections (refs #4754)

    • write_section_override is idempotent by construction: a repeat write replaces its block in place and collapses any pre-existing duplicate, so a section can never accumulate a stacked second copy
    • core is refused because the shipped package declares it fixed — the writer asks CustomizationTier::permits, it does not carry its own list
    • every refusal (protected section, empty body, a marker line inside the body, an unpaired marker in the host, an unreadable host) leaves the file byte-identical and the bundled section in force
    • written blocks adopt the host's dominant line ending, so splicing into a CRLF CLAUDE.md does not leave a mixed-ending file
    • ensure_compiled_pointer records where the composed PM prompt lands, using delimiters deliberately outside the TRUSTY-MPM: grammar so the reader sees no override and raises no diagnostic; it collapses duplicate pointer blocks the same way section writes do
  • bundled framework-manifest.toml — the framework tier of the existing manifest.toml format — now declares which agents deploy, replacing the computed "everything not in LANGUAGE_ENGINEERS" rule (closes #4760)

    • four deployment categories: universal (no detection), language, framework, and platform (marker-gated), plus deprecated
    • a missing, malformed, or non-exhaustive framework manifest fails loudly rather than falling back to deploying everything or nothing
    • tm generate capabilities now sources the agent reference's deployment category from the manifest
  • new elixir-engineer agent, gated on mix.exs, covering general Elixir and OTP work — supervision trees, GenServer processes, Ecto, ExUnit

  • New bundled tm-slack skill folding tm-slack-canvas-delivery into general Slack delivery guidance — routes through the native mcp__slack-mcp__* connector per ADR-0014, enumerates the verified 22-tool surface, and generalizes the "creation is not delivery" completion rule to every delivery shape (message, canvas, scheduled) (closes #4761)

    • registration into bundle_all.rs's ALL table is a separate, sequenced follow-up (that file was held by a concurrent PR); the skill does not ship until that line lands
  • tm hook --pm-guard now denies Task/Agent dispatch when the calling session is itself a subagent (closes #4784)

    • the PM keeps dispatching; only fan-out from within a subagent is blocked
    • SendMessage is never denied, so a blocked agent can always report back
    • fails OPEN — an indeterminate caller context allows the dispatch rather than risking a false deny against the PM
  • Per-subagent context-cost guard at the PreToolUse hook (#4837) — a subagent's accumulated context is measured from its own transcript and classified against the new [agent_cost] section of ~/.trusty-mpm/config.toml (enabled, warn_tokens, max_tokens)

    • ships WARN-ONLY: warns at 250k, and the hard stop is opt-in (max_tokens = 0 by default). Sized against the measured population rather than one sample — across the 600 most recent subagent transcripts on a working machine over 14 days the distribution is p50 136k, p90 268k, p95 323k, with 18/600 (3.0%) at or above 400k, so a shipped-on 400k ceiling would deny roughly one dispatch in 33
    • the warning reaches the agent, once, as hookSpecificOutput.additionalContext — no permissionDecision is emitted, so the tool call still goes through the normal permission flow. "Once" is per agent, keyed on agent_id or, when the payload carries only the subagent's transcript path, on that path — siblings of one parent no longer share a key, where previously the first to warn silenced the rest; a key that cannot be derived fails open and keeps informing the agent
    • when an operator opts into the stop, SendMessage and git add/commit/push/status/diff stay permitted so a stopped agent can always save and report its work; every segment of a composed Bash command must be one of those, so work cannot be smuggled behind an allowed verb
    • that allowlist judges the flags, not just the subcommand name, and is default-deny on anything it does not recognise: git global options other than -C/--git-dir/--work-tree/--no-pager/--no-optional-locks/--literal-pathspecs are rejected (so -c, --config-env and --exec-path cannot inject diff.external, core.gitProxy, credential.helper, protocol.ext.allow or an alias), the program must be git itself with no env-assignment/sudo/env prefix, any unquoted $/`/(/)/</> disqualifies the whole command (closing process substitution and redirection in both directions), any :: token is rejected as a remote-helper transport (ext::sh -c …), and the --exec/--*-pack/--ext-diff/--textconv/--output families are rejected wherever they appear
    • measured from a bounded tail read (64 KiB, retried once at 1 MiB when the smaller window holds no complete usage record), so the check costs the same on a 1 MB transcript as on a 500 MB one
    • fails open at every step: the PM is never evaluated, and an unreadable transcript, a missing usage record, or max_tokens = 0 all allow the call
  • tm-delegation-patterns now carries two concurrent-dispatch rules: every brief must name the files owned by other in-flight branches ("we stack, we do not race"), and a running agent's scope is fixed — new work is a new agent, because cost tracks accumulated context over the agent's lifetime (#4837)

  • BASE-AGENT.md bans ending a gate chain in a pipe — cargo test … | tail and cargo test … | tm compress both exit 0 on a failing suite — and gives the canonical redirect-then-echo form

  • tm memory import <DIR> --palace <SLUG> — bulk-load a directory of memory .md files into a trusty-memory palace with zero LLM inference (refs #4837, unblocks #4834)

    • maps YAML frontmatter onto drawer fields: the description leads the stored text, and name + metadata.type + every [[wikilink]] target become tags
    • --dry-run reports what would be written and issues no writes; --json prints a per-file report carrying each drawer id
    • re-running never writes a file twice: the file's own drawer is found by its slug tag with linking drawers excluded structurally (their wikilink targets are re-derived), so a file whose text has drifted since it was imported is still recognised and skipped rather than duplicated. Never memory_recall (#4836)
    • fails closed instead of risking a duplicate or a truncated fact: an ambiguous candidate set, a slug tag shared by more drawers than one memory_list page returns, a frontmatter line with no key: separator, and YAML's plain multi-line scalar form are all reported as per-file failures rather than imported
  • Two prose rules in the PM's "Prose Style — Write Plainly", mirrored into BASE-AGENT.md: no praise for the user (acknowledge with "OK" or disagree and say why), and delete the framing opener and lead with the fact. Both state the ban as a category or template with their examples explicitly marked non-exhaustive, because a literal example list had already failed to generalize

  • tm reinstall — redeploys the bundled agents and skills to EVERY deploy destination, not just the one the command you happened to run writes. It refreshes the compiled-in bundle onto disk first (hop 1), then deploys into the tm-managed claude-config, the operator's ~/.claude/skills, the current project's .claude/skills, and the standalone driver's ~/.trusty-mpm/claude-config — the last of which no command reached before (closes #4849). Reports deployed / repaired / preserved / unchanged / failed per destination.

    • Ownership is the deployers' existing model: a framework-owned file that drifted or was corrupted is repaired, a file you own or edited is preserved. --force overwrites, backing every file up first.
    • The binary is never touched unless --binary is passed, and the route is decided by install provenance — a crates.io install upgrades through the shared update path, a cargo install --path install rebuilds from its recorded source directory, and anything unclassifiable refuses instead of guessing.
    • A skill a higher-precedence tier owns the name of is counted and named in the report. The tier planner drops it before the deployer runs, so it reaches none of the deployed/preserved/unchanged/failed counts — reporting only those printed zeros for a bundled skill that was never deployed. A bundled-named skill the destination does not track (#4605) additionally names tm reinstall --force, which adopts exactly that set.
    • The project tier is refreshed only when <cwd>/.claude/skills already exists, so running tm reinstall from any directory never creates a .claude/ there, and a failure at that destination is reported without failing the command.
    • A skill several tiers carry is counted once, not once per shadow record.
  • tm-capabilities gains a generated references/framework.md — the install layout, agent tier precedence, skill deploy tiers, per-session state, and an existence-checked index of the authoritative docs (separating what ships in the published crate from what is repo-only). Rendered from the path constants and tier resolvers the runtime uses, so moving a directory or reordering a tier drifts the committed skill and fails scripts/check_capabilities.sh (closes #4946)

    • the compiled prompt's agent-precedence block no longer names ~/.trusty-mpm/agents/, a tier no code reads; it now states the real order including $CLAUDE_CONFIG_DIR/agents/
  • tm doctor --fix repairs the findings tm can prove it owns, instead of only reporting them (closes #4948)

    • dry run by default: it prints every change per item, with paths, and writes nothing until --yes
    • covers skill_staleness (redeploy from the bundled asset), hooks_contamination (strip tm's own hook entries from this project's .claude/settings*.json, backing the file up first), and push_guard (retrofit the cross-branch pre-push guard)
    • legacy_sources findings under ~/.claude are reported as REFUSED and never deleted — a copy there may be hand-edited, and a directory name cannot prove otherwise
    • a hand-edited (FROZEN) skill and a foreign pre-push hook are refused in both modes; --include-frozen now works with either --fix or --fix-skills
    • --fix-skills gained a preview through --fix, and its output now names the exact file it repaired at each tier
  • Fail-Open Check added to the bundled PM workflow section — a five-question review gate for any failure branch that advances a cursor, watermark, index or "done" marker past the thing that failed

  • tm shell-init <zsh|bash|fish> prints a tm wrapper function so your shell is left in the alias's repo after tm run <alias> exits (#4986)

    • install it yourself: eval "$(tm shell-init zsh)" in ~/.zshrc, or tm shell-init fish | source in config.fish. The command is print-only — nothing in trusty-mpm writes to a shell rc file
    • a process cannot change its parent shell's cwd, so before this a session that ran in another repo left the shell behind and a later bare tm re-detected the wrong project
    • the wrapper resolves through the existing tm path <alias>, which derives the path from (alias, root) and never persists it; there is no state file or env-var channel
    • it passes the real exit status through unchanged, cds only for tm run, and stays silent and stationary when tm path fails, prints nothing, or names a directory that is gone
  • tm run <owner>/<repo> cold-starts a daemon-managed session for a repo that is not on disk yet

    • clones to the managed checkout at ~/trusty-mpm-projects/<owner>/<repo>, then hands off to the existing tm launch path — so the result is a real SessionRecord with a tmux pane, visible in tm ls and tm sessions, not a blocking foreground claude
    • the positional is classified by the same predicate tm register uses, so tm run bobmatnyc/trusty-tools and tm register bobmatnyc/trusty-tools cannot disagree about what a string means; a registered alias keeps the unchanged DOC-24 standalone behaviour
    • reusing an existing managed checkout FAILS LOUD when its origin names a different repository — no auto-repoint, and the refusal lands before anything writes to the directory
    • a dirty existing checkout is not an error: the fast-forward is skipped and said so in normal output, while the fetch still runs. The session's branch is cut from a freshly-fetched origin/<default-branch>, so it never inherits the checkout's local HEAD
    • tm run ./some/dir now reports that a relative path is not a repository instead of "alias not found"
  • tm f [pattern] — find a session by name, filtering as you type

    • the list stays on screen and narrows per keystroke against the session NAME only; [pattern] seeds the filter box. Up/Down select, Enter opens the highlighted session through the numbered picker's own resume path, Backspace edits, Ctrl-U clears, Esc cancels
    • tm ls <term> is unchanged: still the one-shot, pipeable listing, still matching every visible column (id, name, project, state, task). tm f is the narrow lookup — a task description mentioning "api" no longer pads the answer to "which sessions are CALLED api-something"
    • piped output, --json, --all, or TERM=dumb never enter raw mode; they print the same static filtered table tm ls would. The gate is checked before raw mode is requested, so a script that pipes tm f cannot hang on a keyboard nobody is at
  • tm ls colors the ID column, dimmed — it was the widest cell on the row and the only uncolored one, so the table read as a wall of undifferentiated UUID. NUM, ID, and NAME now carry three distinct hues, all suppressed on a pipe and under NO_COLOR as before

  • [hooks] prompt_context = false in ~/.trusty-mpm/config.toml turns off the per-prompt trusty-memory prompt-context injection (closes #5034)

    • the hook costs a measured ~1,211 tokens on every prompt (median 1,252, range 693–1,438 across 1,114 firings over five days) — roughly 24,000 tokens across a 20-turn session, recurring — and #4904 measured 0 clean matches out of 17 curated facts on that same corpus. There was no way to stop paying it: the hook block is a hardcoded const written unconditionally at every session launch, so a hand edit to .claude/settings.json was overwritten on the next launch
    • default is true. An absent [hooks] section, and a present-but-empty one, both leave the write byte-identical to before
    • only the UserPromptSubmit entry is suppressed. SessionStarttrusty-memory inbox-check, the PreToolUse PM guard, and the six-event lifecycle triad are written either way
    • the strip that removes trusty-mpm's own prior entries now covers every event trusty-mpm owns rather than only the ones the current config writes. Without that, the key would have done nothing on any project already launched once — the stale UserPromptSubmit entry would have stayed in .claude/settings.json and kept firing
    • config file only, by design: no CLI flag and no environment variable
    • config.rs's inline test module moved to a sibling config_tests.rs (no test changed) — the new section pushed the production file to 533 SLOC, over the 500 cap
  • tm doctor gains a search_index_pin check that resolves the trusty-search index a session is actually pinned to (#5045)

    • session launch writes trusty-search serve --index <id> into the project's .mcp.json and registers that index best-effort. Every step of the registration swallows its failures at warn!, so the pin advances even when index creation never happened. The existing search check asks the daemon whether it is healthy and whether the derived id appears in /indexes, so it kept reporting fine: 4 of 75 live worktrees had an index, while a bare search in the rest returned 404 unknown index
    • the new check reads the pinned id out of .mcp.json and resolves it with GET /indexes/{id}/status. A 404 is Fail and names the id; an index that resolves with 0 chunks is Warn (registered, never populated); an unanswered daemon is Unknown, never Ok
    • read-only — it never creates or reindexes anything
  • a worktree is now registered with trusty-search at the moment it is created, instead of only when a session launches in it (#5060)

    • covers both worktree-minting paths: the in-project managed-session path (daemon::managed_routes::inproject::create_session_worktree) and the clone-provisioned path (provisioner::workspace's RealGitBackend::worktree_add)
    • the index is BM25 + KG only — no embeddings. Exact text and the symbol graph are branch-specific and must be worktree-accurate; conceptual similarity is not, so the expensive lane stays on the base checkout
    • fire-and-forget on a detached thread: worktree creation and session launch never wait on indexing at any repo size. A cold BM25+KG build of this workspace (5,865 files, 75,282 chunks) measured 35.0 s
    • idempotent — a repeat call against an already-indexed worktree is two short HTTP round trips and no walk
    • only a real git worktree is indexed; a plain clone is refused, so worktree creation never makes an operator's opt-in decision for them
    • teardown is unchanged: session_manager::search_gc already derives the same index id and deletes it at decommission, with an orphan sweep behind it
  • tmux.alternate_screen in ~/.trusty-tools/trusty-mpm/config.yaml, alongside history_limit and mouse (#5151). It defaults to true, tmux's factory value and today's behaviour; setting it to false stops panes using the terminal's alternate screen buffer, which is what makes a full-screen TUI's output land in scrollback instead of being discarded. This is why a tm session can verify a 100,000-line history-limit and still have nothing to scroll back through — the history was never written to the pane.

    Turning it off is server-wide in effect: every trusty-* managed session shares one tmux server, so vim, less, htop and man also stop restoring the screen they covered, each leaving its final frame smeared into the scrollback on exit. It is not retroactive either — anything already written while the alternate screen was up stays unrecoverable.

    The option rides the existing verified apply path (start-serverset-optionshow-options readback, retried, before new-session), and its readback queries the same -wg scope the set writes, so a wrong-scope set cannot verify green.

  • TmuxConfig and ResolvedTmuxOptions are now #[non_exhaustive], so this field addition — and every future one to a section designed to grow — is not a SemVer break. Construction from outside the crate goes through Default plus field assignment or resolve_tmux_options; field reads are unaffected. Same treatment trusty-common's IndexOptions got in #5065.

  • The session-worktree base directory is configurable via worktrees_dirname in ~/.trusty-tools/trusty-mpm/config.yaml or TRUSTY_MPM_WORKTREES_DIRNAME, defaulting to .worktrees (#5204). Creation uses the configured name; detection also keeps matching .worktrees, so retargeting never orphans worktrees already on disk.

  • trusty-mpm now reads a committed, project-level config file (closes #5207)

    • <project>/.trusty-mpm.toml is tracked in git, so a project's conventions travel with the clone, show up in PR diffs, and stop being something every operator has to re-declare on every machine. It is a ROOT-LEVEL dotfile rather than a member of <project>/.trusty-mpm/, because that directory holds machine-local session state and projects gitignore it wholesale — this repository's own .trusty-mpm/* rule already makes the #4832 framework/manifest.toml layer untrackable, and trusty-mpm cannot carve a re-include out of a consumer's .gitignore
    • worktree is the first setting to use it. Precedence, highest first: the project's .trusty-mpm.toml, then the machine-global projects.json registry, then the built-in true. The project layer wins in both directions — a repo can force isolation back ON over a local opt-out, not only off
    • the clone-based spawn branch, which asks the question before any project directory exists, is unchanged and still resolves from the registry alone
    • a key the schema does not define is REJECTED (serde(deny_unknown_fields)), so worktre = false is an error instead of silently reading as "worktrees stay on". At spawn time a rejected file contributes nothing and is logged at error level, and resolution falls through to the registry — a committed file is shared, so one bad push must not brick every operator's launches
    • default_model is the second setting: it tops the same chain resolve_agent_model already ends in. Explicit --model, per-agent overrides, and agent frontmatter are more specific than a default and still win over it
  • tm launch --worktree, and the matching worktree field on POST /api/v1/sessions/managed: the launch-time request that puts one specific session in its own protected clone plus worktree, overriding the main-checkout default.

Fixed

  • code-review-standards and the code-critic agent (both synced to trusty-code) no longer let Promote act as a filing engine. The default is now stated explicitly: a review finding is fixed in the surfacing PR, or dropped. Promote is reserved for defects that are genuinely separable, schedulable work — not every LOW/MEDIUM finding — and the critic never files an issue itself or instructs anyone to; it only recommends, and the PM or user decides. Prompted by nine issues filed in a single review leg against one merged PR, several forwarded straight from an unqualified Promote disposition
  • agent-delegation.md no longer claims code-critic is a "universal qa-tier agent" for "any engineer dispatch" or design critique — both were owner rulings against the actual standard. Added a "code-critic Dispatch Standard" keyed to the project's test-ladder rung (never on rungs 1–3, conditional on rung 4, required on rungs 5–6), plus explicit escalations and non-reasons to dispatch
  • code-production-process (and its stage-architect.md/stage-critic.md references, synced to trusty-code) no longer describes an "optional Phase 2 design critic pass" at the Architect stage — that contradicted the standard; early interface review routes to code-analyzer instead
  • A test in pm_guard_bash set TMPDIR to a machine-specific path with std::env::set_var, reddening unrelated tests across the whole repo at random. cargo test runs tests as threads in one process, so the variable was visible to every concurrent sibling; tempfile honors it, so pm_guard_budget's tempdir calls tried to create directories under a macOS-only path and panicked NotFound on the Linux CI runner. It failed PR #4914's Test job (run 31023632348) on code that PR never touched. The expansion rules are now asserted against an injected PathEnv instead of process env — no global mutation remains anywhere in the workspace — and pm_guard_budget/pm_guard_cost take their temp dirs from a new hermetic test_support::hermetic_temp_dir for the tm bin target, which roots under the hardcoded system temp path and ignores $TMPDIR entirely.
  • tm catalog apply --prune now reads an agent's ledger ownership tier, not just its checksum (completes the agent half of #391)
    • the agent manifest records an Origin the skill manifest lacks, and agents::deployer already honours it — a non-Bundled entry is the seed-once tier and is preserved on a checksum mismatch. Prune read the checksum and not the tier, so a pristine user- or registry-owned agent could still be deleted by a bundled exclude rule: the hole the skill side closed by deriving the user tier from its source directory
    • latent today, because nothing writes a non-Bundled origin; pinned by a regression test that constructs the ledger entry directly
  • tm catalog apply --prune no longer deletes user content (closes #391)
    • it removed any managed skill directory the current include/exclude rejected, with no checksum comparison, no frozen check and no backup — the only skill-mutating path with none of the guards deploy_one_file and skill_repair already have
    • a hand-edited (frozen) agent or skill is now left in place, as is a skill directory holding any file trusty-mpm did not deploy — remove_dir_all would have taken that file too
    • a skill the user-custom tier supplies is never pruned, whatever the bundled include/exclude says: deploy_all_skill_tiers deploys that tier in full and exempt from those rules, but its ledger entry is indistinguishable from a bundled one, so a checksum gate alone waved a pristine user skill through. The tier is derived from the live ~/.trusty-mpm/skills/ source rather than a new manifest field, so no existing ledger entry has to be guessed at
    • everything actually deleted is copied to ~/.trusty-mpm/backup-catalog-prune-<timestamp>/ first, and the command now prints what it kept and why
    • prune judges skill STEMS, not raw ledger keys: an include rule matching a stem but not its carried <stem>/references/*.md key used to drop that file's ledger entry while leaving the file on disk, after which the deployer read it as user-owned and never updated it again
  • Updated two doc comments that still named open-mpm to say trusty-agents (renamed in #831).
  • managed sessions no longer dirty a git-tracked .mcp.json, and this repo stops tracking one (refs #4181, closes #5058)
    • every entry the MCP injectors write is machine- or session-specific: trusty-search's --index pin names the ephemeral worktree, and the tm mcp add bridge can carry an absolute command path valid on one machine. While the file was tracked, each managed session started dirty and a routine git add -A could commit an operator-specific path into the repository
    • prepare_session now adds .mcp.json to .git/info/exclude before any injector writes to it, reusing the helper the .env.local secret route already used (ensure_env_local_git_excluded is now the filename-parameterised ensure_git_excluded). Non-fatal and deliberately not fail-closed — skipping injection would drop the force-overwrite that #3934/#3950 require before a builtin name may enter enabledMcpjsonServers
    • runtime::claude_code::prepare_managed_config — the second site that runs the same four injectors — excludes the file too. Two of its three callers (spawn_resume, build_inplace_resume_command) reach it with no prepare_session anywhere in their chain, so without this every resume re-dirtied the file
    • a tracked .mcp.json now produces an operator warning naming git rm --cached. The exclude write silently succeeds on such a repo, since info/exclude never applies to a path already in the index, so the failure arm alone left that case with no signal at all
    • info/exclude cannot untrack a path already in the index, so this repo's own .mcp.json is removed from it. .gitignore had declared the file machine-local since before this change; it was tracked only because git rm --cached was never run. That also removes the committed tickets-mcp registration: no install path builds trusty-common's bin targets, so the binary was never on PATH and every session reported it as ✘ ENOENT
    • the shared injector write path now ends .mcp.json with a newline, so a repository that does track it no longer sees \ No newline at end of file on every rewrite
  • register_project_index_never_bypasses_sensitive_path_denylist opts into real daemon writes with TRUSTY_ALLOW_PRODUCTION_STATE=1, so it keeps receiving the POST /indexes body it asserts on now that test processes are barred from writing to a live trusty-search daemon (refs #4255)
    • the override is scoped to the test and points at its own loopback socket, never the operator's daemon
  • a stale project-tier agent file that shadows a bundled agent is now quarantined on session launch and on tm sessions sync-assets, instead of silently winning the resolution race forever (closes #4448)
    • retract_framework_agents (#4409) can only delete what its ownership ledger names, so a copy written before that ledger existed survived it, outranked the canonical user tier, and was never refreshed again — #4408 made permanent
    • the sweep runs AFTER retraction, against the workspace's OWN .claude/agents — never fw.claude_agents_dir(), which is the operator's real ~/.claude/agents on the non-git tm session start and TUI /connect paths
    • a file moves only on a POSITIVE answer from every gate: it resolves to a bundled name, the ownership ledger does not record it as the operator's, git gives a definitive answer that nothing claims it — either a readable work tree that does not track it, or no repository at all — and its frontmatter and body are trusty-mpm's own composer output. Anything less — a tracked file, an unreadable or ceiling-blocked repository, a bare repo, a stale worktree pointer, git absent, a claude-mpm artifact, a hand-authored file — is left alone and recorded in the receipt with the reason
    • it never deletes: each moved file keeps a verified byte-identical backup under .trusty-mpm/agent-quarantine/ plus an inert .md.disabled sibling, and a receipt records how to restore it
    • the bundled-name roster moved to core::bundled_roster, so tm doctor's asset_tier probe and the quarantine resolve it from one place and cannot drift on what counts as canonical
  • research's deployed skills: list now matches its source asset — tm-capabilities was inherited from BASE-AGENT and silently appeared only in the deployed copy (closes #4643)
    • DOC-42 now documents that skills: unions base-first across the extends: chain, so a foundation template's declaration is paid for by every descendant
  • tm ls auto-prune no longer misses stopped sessions or non-TTY invocations, so dead records stop accumulating (closes #4702)
    • every listing surface prunes now — piped, scripted, --json, tm session ls, and bare tm — not just the interactive TTY picker
    • a stopped/errored record whose workspace the CLI independently verifies is gone is cleared, closing the gap where a display-reconciled zombie (persisted active, tmux pane gone) never got the daemon's unresumable probe
    • a stopped record whose workspace still exists on disk is never cleared, and decommissioned records, attached records, and any record naming a live tmux session are untouched
    • a workspace is only "gone" when its parent directory still exists, so an unmounted volume no longer reads as every session on it being dead
    • confirmation requires 10 minutes of real elapsed age since the FIRST sighting; an intervening listing no longer resets that clock, which would otherwise leave auto-prune inert under any tm ls cadence tighter than the window
    • when tmux cannot be enumerated at all, nothing is pruned — without a liveness signal an errored record with a live pane could be tombstoned into a terminal state
    • the prune clears registry records only: never a git worktree, a branch, a file on disk, a live runtime, or a search index
  • the delegation roster no longer admits prompt fragments as delegatable agents (closes #4711)
    • is_foundation_file now matches a bare base.md, not only base-*.md
    • a file with no name: frontmatter is excluded outright — Claude Code dispatches by name:, so the old file-stem fallback advertised targets the harness could never resolve
  • Worktree teardown no longer deletes worktrees git refused to delete (closes #4732)
    • remove_session_worktree fell through to std::fs::remove_dir_all on ANY non-zero git worktree remove --force exit. Git exits 128 for every fatal condition, so git worktree lock — the operator's only "do not remove this" — was exactly what caused deletion. A stale worktree pointer, an unreadable .git, and a repository git merely declined to read all produced the same outcome, with uncommitted work in the worktree.
    • Every git failure is now classified into three states — git holds state here / git positively holds nothing here / git could not be asked — and only the middle one permits a raw removal. Locked, stale-pointer, broken-.git, and unrecognized-message cases are refused. Reachable from tm session decommission, tm sessions prune --state stopped, the age-based reaper, prune_orphaned_worktrees, and the --merged-prs reclaim pass.
    • The refusal reason is now returned to the caller instead of buried in a log line: tm session prune-worktrees --merged-prs reports removal_failed entries as "<path>: <reason>".
  • cargo test can no longer destroy a real trusty-search index (closes #4743)
    • session_manager::search_gc formatted DELETE /indexes/{id}?delete_data=true at two sites and sent it to whatever daemon resolve_daemon_base_url discovered — under a test run, the operator's live one on port 7878. ?delete_data=true destroys the index's on-disk data directory
    • fixture workspaces derive their index id from a bare file_name(), so decommission_full_still_terminates_the_runtime asked that daemon to destroy an index named full; sess, live and proj appear the same way elsewhere in the suite
    • both sites now go through one DestructiveIndexDelete capability that holds the only copy of the ?delete_data=true literal and exposes no constructor taking a base URL, so a caller cannot build the request without acquiring it — and a cargo test process never can
    • the orphan sweep acquires the capability before listing anything, so a test process makes no request at all instead of enumerating real indexes and stopping at the delete
    • TRUSTY_ALLOW_PRODUCTION_STATE=1 remains the explicit opt-in for a test that deliberately drives a real daemon
  • the compiled PM prompt is now refreshed at the spawn seam, so resume, guided-resume and crash-recovery launches no longer run a prompt that never reached INSTRUCTIONS-COMPILED.md (refs #4752)
    • the compiled write is the last step of session preparation, so a failure can no longer skip the MCP content-pinning injectors that defend against the #3918/#3950 name-squatting class
  • compiled PM system prompt now writes to its own INSTRUCTIONS-COMPILED.md instead of colliding with the bundled instructions/INSTRUCTIONS.md stub that the pipeline reads as input (closes #4752)
    • the compiled prompt is refreshed at every session launch, before claude is spawned; a failed write now aborts the launch rather than leaving a stale prompt on disk
    • tm install removes a stale pre-#4752 framework/instructions/INSTRUCTIONS.md, which no writer could refresh but the pipeline still read
  • PM instruction section non-overridable-rules.md no longer describes section overrides as "override files" — the live mechanism is marked blocks in the project's CLAUDE.md, and the section now points at the spec of record (refs #4753)
  • a session whose instructions cannot be written no longer starts (refs #4752)
    • build_instructions returned a non-fatal error, so every launch path logged it and started the session anyway — with no instructions established and no .mcp.json written. Reachable whenever the project's CLAUDE.md or the framework instructions path cannot be read, for example when something has left a directory at CLAUDE.md
    • it is now the same fatal condition as the compiled-prompt write, reported as one error rather than two classes
  • bare tm in a managed pane now refreshes the project's compiled prompt before relaunching, and refuses the relaunch if that write fails (refs #4752)
    • this path calls neither session preparation nor the daemon resume route, so it was the one way to start a session on a stale or missing compiled prompt where a fresh launch would have refused
    • the two resume-shaped paths now share one refresh_compiled_prompt entry point; session preparation keeps its own composition because it must apply the operator's resolved output style
  • an unwritable .trusty-mpm/ no longer causes a session to start without its instructions recorded (refs #4752)
    • the last-instructions.md stash write degraded the whole preparation to an early non-fatal error, so callers launched the session having skipped the fatal write below it
    • the stash is an inspection copy; it now logs and continues instead of short-circuiting
  • the CLAUDE.md compiled-instructions pointer no longer names the retired global path (refs #4752)
    • it pointed at ~/.trusty-mpm/framework/INSTRUCTIONS-COMPILED.md, which nothing writes since the compiled prompt became project-local; it is now the project-relative path, pinned against the pipeline that writes the file
  • The TUI health panel's palace detail showed "Wings: N" from a field that has always carried a ROOM count (closes #4811, ADR-0027 C3.4). It now reads the daemon's truthful room_count and renders "Rooms: N", falling back to the legacy wing_count when talking to a daemon that predates the change — where that field held exactly this number.
  • tm session start from a directory outside any git repository is now refused with an actionable error instead of scattering .trusty-mpm/, a CLAUDE.md stub and a .claude/ tier into whatever directory the shell happened to be in (refs #4832)
  • session preparation no longer reads the retired <framework_root>/instructions/INSTRUCTIONS.md. Its content had no consumer, but any read error other than NotFound — a permissions fault, a directory at that path, invalid UTF-8 — raised the one fatal PrepError variant and killed the launch
  • the .gitignore block tm writes into a target project now covers .trusty-mpm/sessions/, .trusty-mpm/logs/ and .trusty-mpm/last-instructions.md. framework/ and config.toml are deliberately still trackable — they are operator-authored config
  • Agent frontmatter using a YAML block scalar (description: > or |) is now folded instead of taken literally. Five bundled writing agents — copyeditor, pangram-editor, proofreader, writer, writing-critic — rendered into the PM prompt as a bare >; the same defect would have silently truncated any block-scalar role: or model:
  • Bundled agent assets now reach a daemon-managed session without a manual tm install (closes #4840)
    • managed session provisioning re-materializes ~/.trusty-mpm/framework/agents/ from the compiled-in bundle and deploys it, gated on a sha256 stamp so an unchanged bundle costs one file read
    • the standalone path (tm run / tm load / tm login) still routes through global_config::ensure_global_config_dir, which does not refresh — tracked as #4849
    • a bundled-origin file whose checksum drifted is still overwritten (corruption, not user ownership); an untracked-and-differing or user-edited file is still preserved — but now surfaces in a bounded count-plus-preview warning carrying the tm install --reset-agents <name> pointer, instead of being skipped silently
    • agents that fail to compose (bad frontmatter, strict-YAML rejection) are reported in the same summary — those do not land at all, which is worse than stale
    • an uninitialized agents/agents submodule directory no longer wins the source resolution and deploys nothing; it falls back to the compiled-in bundle
    • the whole step fails open: a refresh or deploy that cannot run degrades to a warning and never blocks the session
  • tm ls auto-prune now clears dead session records whose git worktree was fully removed (closes #4872)
    • git worktree remove deletes leaf and parent together, so the old immediate-parent probe read the removal as an unmounted volume and the record could never advance — it was never even marked in auto-prune-seen.json
    • absence is now corroborated against the nearest surviving worktree root (.base/.worktrees, .claude/worktrees); when that root is gone too, the record is still kept, so an unplugged external volume cannot mass-tombstone the sessions on it
  • a skill the every-run deploy declines to refresh is now reported instead of skipped in silence: ensure_managed_config_dir emits one bounded warning naming the withheld files and pointing at tm doctor --fix-skills --include-frozen (closes #4873)
    • the skip itself is unchanged and still correct — a hand-edited (checksum-frozen) skill and an unmanaged project-custom skill are both preserved
    • corrects the resume_managed and run_inplace_relaunch comments that claimed agent/skill redeploy and MCP injection were not re-run on those paths; all three run paths reach ensure_managed_config_dir through prepare_managed_config, so they always were
  • The project skill tier (<workspace>/.claude/skills) now redeploys on every run path when the binary version or the manifest's skill selection changes (closes #4880). It was written only by prepare_session and tm sessions sync-assets, neither of which runs on resume or in-place relaunch — and because project outranks user, a skill an older binary left there silently shadowed the copy managed_config refreshes every run, the #4408 shape one tier down.
    • A version bump re-runs deployment; the manifest never needs to see skill content. The manifest's [skills] selection is the second trigger because the operator's <harness-root>/.trusty-mpm/framework/manifest.toml can move between releases. Both are recorded in <workspace>/.claude/skills/.trusty-mpm-project-tier-stamp; a matching stamp writes nothing at all.
    • Custom skills survive the redeploy: a project-custom skill is dropped by the tier planner before any file I/O, and a checksum-frozen hand edit is skipped by the deployer.
    • A skill source that exists but holds no skill files — an unfetched agents/skills submodule looks exactly like this — is a refusal that leaves the stamp unwritten and warns, rather than recording an empty deploy as current.
  • take the skill ledger lock in the two trusty-mpm writers that also read-modify-write it — tm doctor --fix-skills repair and tm catalog apply --prune (closes #4881)
    • a tier directory that does not exist is skipped before the lock, so a repair never creates an empty skills/ directory in a project it was only inspecting
    • skill-drift auditing no longer counts the lock sidecar as deployed content when deciding whether a manifest-less tier is populated
  • User-tier skills authored as a directory (~/.trusty-mpm/skills/<name>/SKILL.md) now deploy into every project (closes #4949)
    • they were silently rejected by the source scan, so duetto-design-system and cto-kb-ingest were hidden from every project's skill manifest
    • a skill directory with no SKILL.md is now named in the deploy warning instead of vanishing
  • session worktrees are now branched from a freshly-fetched origin/<default-branch> instead of the base checkout's local HEAD, which left every new session as stale as the operator's last git pull (closes #4957)
    • the default branch is resolved from the repo, never hardcoded to main
    • a failed fetch falls back to the last-known remote-tracking ref (or HEAD) and logs a warning naming the stale tip, rather than reporting a clean success
    • a repo with no remote still branches from HEAD, with no spurious warning
  • startup hygiene no longer destroys gitignored working-tree content in the managed checkout (closes #4961)
    • the update step is now a non-destructive git merge --ff-only, never git reset --hard, and it refuses when any path the update would write already exists untracked on disk — git status --porcelain never reported gitignored paths, so a gitignored file holding real content read as clean and was silently overwritten
    • the update also refuses off the default branch, where the ahead-count validated origin/<checked-out> while the operation moved the branch to origin/<default>
    • a .trusty-mpm-no-hygiene marker file in a base clone now opts that single checkout out of the sweep; previously the only control was the process-wide TRUSTY_MPM_INPROJECT_HYGIENE env var
  • tm ls no longer labels a session with two or more attached tmux clients (active) instead of (attached). #{session_attached} is a client count, not a boolean, and the row parser compared it to the literal "1" — so the session the operator was actually sitting in (two terminals on it) read as detached while every single-client session read correctly.
  • tm ls no longer shows dead session records in its default view (closes #4994)
    • a record the listing-time sweep confirms dead — no tmux pane, no workspace directory left on disk — was rendered [dead] but still listed, because is_live_session_state consulted lifecycle state alone
    • visibility keys off that sweep's own verdict, never the raw unresumable wire flag: the flag is computed from a record's persisted state and can ship alongside state: "active" for a session whose pane is running, which the sweep then refuses to reap. Hidden now implies reapable
    • the records are untouched: tm ls --all still lists every one, and the remedy is unchanged (tm sessions delete <id> --force)
    • the default-view filter runs AFTER the sweep, so it still sees the records it reaps
    • the "N more dead records pending confirmation" line says where those rows went, and only where they were actually hidden — not on --json or --all, which print them
    • that line now also counts records whose decommission was attempted and failed. They stay hidden either way, so leaving them out meant a sweep where every attempt failed printed nothing at all and the rows vanished with no explanation
  • tm sessions rename (and the tm ls picker's r<N> <new-name>) now surfaces the daemon's own failure message instead of a bare HTTP status server error (500 …) (closes #5001)
    • the client called error_for_status(), which discards the response body — the daemon had always sent an actionable reason there, and it was thrown away before the operator ever saw it
    • the daemon also now logs a warn! when a rename fails with an unmapped error, so a 500 leaves a diagnosable trace
  • tm ls no longer staggers NAME/TASK/CREATED on rows whose state carries an annotation
    • the STATE column was a hardcoded 14 characters wide, but its rendered value is the state plus any annotation — attached [stale-assets] is 23 — so every annotated row pushed the three columns after it nine places right
    • the width is now measured across the rows actually being printed, floored at the historical 14 so an unannotated listing keeps its shape
  • Worktree creation no longer logs worktree index registered when the trusty-search daemon never confirmed the registration. index_new_worktree now reports RegistrationUnconfirmed with the reason (unreachable, non-2xx, or suppressed under the test harness) and warns instead of claiming success — #5045 measured the daemon-absent case at ~94% of worktrees, so this was the common outcome (refs #5060)
  • Session launch now decides skip_vector from the git-root the index is keyed to, so launching a session in a worktree can no longer re-register that worktree's index with the vector lane on. BM25+KG-only is now an invariant rather than a property of whichever call reached POST /indexes first (refs #5060)
  • The trusty-search orphan sweep no longer deletes a brand-new worktree's index out from under the walk populating it. is_orphan_index gives a .worktrees root a 300 s grace window before a chunk_count == 0 reading counts as abandoned; a root that is gone from disk is still reclaimed at any age (refs #5060)
  • core::worktree_index's cost model now states the disk budget beside the 35 s wall-clock figure: colocated stores put ~1–1.5 GiB per worktree on disk, and skip_vector removes only the HNSW file, not the corpus (refs #5060)
  • session_context_catchup returns the paused sessions that actually postdate the watermark (#5072)
    • the tool returned exactly one session — a hand-written snapshot with every field empty or null — while resolved_snapshot pointed at a different, well-formed file. A resume driven off sessions[] restored the wrong snapshot; one driven off resolved_snapshot restored a file that had never been parsed
    • the response gains undatable_sessions_dropped. An empty sessions array now only means "nothing paused since your last catch-up" when that count is 0; non-zero means sessions exist but could not be dated and were withheld, and the caller should re-call with full
    • resolved_snapshot and sessions[] still disagree under a recent watermark, by design — they answer "what should I resume from" and "what paused since your last catch-up". What is fixed is the inversion, not the disagreement
    • recent_commits / recent_memory coming back empty is unchanged and correct — a non-full call reports only what is newer than the stored watermark
  • pm_guard no longer always-allows a bare MEMORY.md write, matching the PM prompt's write prohibition (#5086)
    • is_pm_orchestration_path matched any path whose basename was literally MEMORY.md, so a top-level MEMORY.md landed in the HARD always-allow set that wins over the source-code rule. core.md (merged in #5079) says never to write, update, or cite MEMORY.md, so the enforcement layer contradicted the prompt
    • the two legitimate targets — the palace index under ~/.trusty-tools/…/memory/MEMORY.md and the retired .trusty-mpm/MEMORY.md override — already match on the .trusty-tools / .trusty-mpm path-component check, so dropping the basename match removes no real coverage. TASK.md is unchanged
  • Corrected the PM prompt's claim that the enforcement tables cannot be removed by customization. Since #4838 an ENFORCEMENT marker in a project's CLAUDE.md does replace them; CORE is the only structurally protected section. framework-guaranteed-conventions.md and non-overridable-rules.md now state what "Non-Overridable" governs — the rules are not the PM's to relax, which is separate from whether the section can be replaced — and assert_authority_intact's failure message no longer asserts the false claim.
  • ~/.trusty-tools/trusty-mpm/config.yaml's default_model now actually affects launched sessions (#5207)
    • the trusty-console Config tab and the config_write MCP tool both wrote the field — under a placeholder reading "(unset — uses ~/.trusty-mpm/config.toml)" — and nothing ever read it back, so setting it did nothing. MpmConfig::load_effective folds it into [models] default, above the TOML value as that placeholder advertises and below the project file
  • a misspelled key in ~/.trusty-mpm/config.toml or ~/.trusty-tools/trusty-mpm/config.yaml is now reported instead of silently dropped (#5207)
    • both loaders answer a parse failure by returning Default, so putting deny_unknown_fields on MpmConfig or TrustyToolsConfig would upgrade "one key is ignored" into "the whole file is ignored" — a worse failure than the one it fixes. They keep the lenient parse and gain a warning naming every dropped key path (models.defualt), derived from a serde round-trip so it needs no hand-maintained key list and cannot drift as sections are added
    • Project (projects.json) is deliberately left lenient too: it is machine-written, and denying unknown fields on a persisted record would make an older tm refuse to read a registry a newer one wrote
  • the console's auto-resume toggle now changes what the supervisor does (closes #5208)
    • auto_resume_set wrote ~/.trusty-mpm/auto_resume and reported success, but the only reader was the console's own status display. TRUSTY_MPM_AUTO_RESUME, read once at supervisor startup, was the sole thing that gated a resume, so toggling auto-resume in the console changed nothing in the running fleet and raised no error
    • Supervisor::tick re-reads the file every sweep. Precedence is: the persisted file when present > the boot-time TRUSTY_MPM_AUTO_RESUME / --auto-resume flag > off. An operator's change takes effect within one poll interval, with no process restart
    • the read uses a new tri-state auto_resume::read_override_at, which tells "file absent" (leave the boot flag alone) apart from "file says false" (turn a running supervisor off). The existing read_desired_at flattens both to false, which would have let a never-touched toggle disable an env-enabled supervisor
    • an unreadable desired-state file holds the last known value and logs at error instead of failing open to off
    • a failed auto-resume now marks the session Errored rather than leaving it Stopped. It was previously a warn! line and a counter on an endpoint the console does not read, while the session stayed dead and the next sweep silently retried it forever. Errored shows up in FleetMetrics.errored, which drives the console's Degraded health, and resume still accepts Errored so a manual retry works
    • supervisor_status / console_metrics gained an effective field (what the next sweep will do) and now report pending_restart: false — the "restart pending" hint described the old behavior and would be a false claim now
  • retiring a bundled skill now removes its deployed copy instead of orphaning it (#5224)
    • the skill deployer only ever writes, so a skill the binary stopped shipping kept its deployed directory and its ledger entry forever. Claude Code went on loading text that was deliberately removed, and the orphaned ledger key made tm doctor's skill_staleness check report Unknown — a check reporting Unknown has stopped protecting anything
    • tm install and every session launch now sweep all three deploy tiers. A skill is retired only when no live source has it: not the compiled-in bundle, not the resolved bundled source, not ~/.trusty-mpm/skills/, not the synced catalog, and not the project-custom stems already on disk
    • a user-tier or hand-placed project-tier skill is never removed, and a retired copy the operator edited — or one sharing its directory with a file trusty-mpm never deployed — keeps every file; only the ledger claim is released
    • deselecting a skill via a harness manifest still leaves its deployed copy alone; that remains tm catalog apply --prune's business
  • a session in a worktree cut before CLAUDE.md was tracked no longer runs on fabricated instructions (closes #5228)
    • instruction_pipeline::load_or_create_claude_md read "file absent" as "new project" and wrote CLAUDE_MD_STUB. On a branch cut before #4660/#4661 tracked the real CLAUDE.md, that replaced every project instruction — and every CLAUDE.md named-section override — with boilerplate the session had no way to detect
    • the damage also persisted: once the stub was on disk, every later run found a file present and took the read path, so the fabrication was indistinguishable from an authored file
    • it now asks git whether a remote-tracking ref already carries the file — the branch's own @{upstream} and origin/HEAD, falling back to origin/main/origin/master only when neither of those resolves, so an abandoned origin/main cannot override a develop default branch. When one carries it, no stub is written and the launch is refused — prepare_session maps the error onto PrepError::Instructions, the one condition #4752 rules must stop a session
    • the refusal names where the tree sits, the ref that has the file, and the recovery: git merge --ff-only <ref> or git checkout <ref> -- CLAUDE.md on a branch, and only the second on a detached HEAD, where a fast-forward would move HEAD off the checked-out commit
    • detection is local-only (no fetch) and fails open: no git, not a work tree, or no candidate remote-tracking ref carrying the file all still seed the stub, so a genuinely new project is unaffected. A missing @{upstream} alone does not fall open — origin/HEAD is still consulted
    • instructions_failure_message no longer says "could not write" and no longer points only at permissions and free space — nothing is written on this path, and the refusal carries its own remedy
  • session_context_catchup no longer hands a session another session's pause snapshot (#5272)
    • resolution used to fall through to "newest pause overall", then LATEST-SESSION.txt, then an mtime scan — all three session-blind. A session with no snapshot of its own got whichever one existed, with nothing in the response saying whose it was. Correct under the one-session-per-checkout model that introduced the chain (#2731); wrong now that the PM runs on the project's main checkout and several sessions share one .trusty-mpm/sessions/ store
    • resolved_snapshot is now resolved strictly for the session_id you pass. No id, or an id that owns no snapshot, returns null
    • reading another session's state still works — pass that session's id. That explicit request is the only way a cross-session read happens
    • session_context_pause writes snapshots to .trusty-mpm/sessions/<session-id>/. Existing flat session-YYYYMMDD-HHMMSS.md files at the store root are untouched and still resolve, through the sessions-log.jsonl line that attributes them; a flat file with no log line resolves for nobody rather than for whoever asks
    • the catch-up digest scans per-session directories as well as the store root, so snapshots written under the new layout still appear in sessions[]
    • a sessions-log.jsonl entry naming a path outside the store is refused instead of read

Performance

  • bundled agents no longer carry boilerplate skills: frontmatter, cutting the skill bodies the harness renders into every dispatch by 78% (2,602,763 → 560,654 bytes across the 37-agent roster) (closes #4642)
    • worst case qa 84,343 → 21,170 bytes (~21,085 → ~5,292 tokens); research 55,894 → 0
    • an omitted skill is unchanged on disk and still invokable on demand via the Skill tool
    • a new regression gate caps any bundled agent's resident skill bodies at 24,000 bytes (34,000 for the rust family, which keeps rust-build-performance per the standing 2026-07-17 directive)

Changed

  • new sessions provisioned from a repo URL now get their worktree at <project-root>/.worktrees/<name>, following the git convention, instead of nesting it inside a .base bare clone (closes #4270)
    • the base checkout is now the project directory's own clone, the same one the in-project spawn path establishes, so both paths share one base clone per project
    • existing .base stores are left exactly as they are — still discoverable via git worktree list, still working — and provisioning refuses loudly rather than moving or deleting one; retiring .base stays a manual operator step
    • a project directory holding git or trusty-mpm state (.git, .base, or .worktrees) is never offered for mv-aside recovery and never renamed by the old-layout migrator; the presence probe treats an unreadable entry as present, so a permissions blip cannot read as "no git dir"
    • the base clone's .worktrees/ is excluded in .git/info/exclude, as the in-project path already does — without it git clean -ffd in the base deletes every session worktree including uncommitted work (single-force clean skips them; -ff does not)
  • the composed PM prompt drops 6,611 bytes (56,783 → 50,172, −11.6%) by relocating content the PM cannot act on and deduplicating the voice rules (#4574)
    • Skill Deployment / Agent Deployment / Skills System collapse to one dispatch-time block; the tier tables were already generated and drift-gated in tm-capabilities (references/framework.md), and the deployment lifecycle they also carried moves to that skill's hand-authored references/workflows.md
    • the gh label create / gh issue create shell block leaves the prompt — tm-ticketing and tm-pr-workflow already carry it verbatim, and P6/P7 forbid the PM running it
    • the Fail-Open Check's five review steps move to the code-review-standards skill, which code-critic already loaded and code-analyzer now declares; the BLOCKING rule and its error-arm-test requirement stay in the prompt
    • Prose Style — Write Plainly becomes a pointer: the voice rules are stated once, in the output style, which was carrying a live mirror of the same text. Both copies were session-resident, so the project paid for them twice
    • dead rules dropped: /mpm-configure --preview and /mpm-init name no command that exists
    • Delegation Mechanics said bundled agents deploy to ~/.claude/agents/; they deploy to $CLAUDE_CONFIG_DIR/agents/, which tm never writes into the operator's own install
  • the PM voice rules gain two entries, in the output styles and in BASE-AGENT.md: don't justify the restraint ("I don't know yet" is the whole answer), and no trailing emphatic negation ("— not before" restates the sentence by negating its opposite)
  • the composed PM prompt drops 18,947 bytes (52,921 → 33,974, −35.8%; ~20.4k → ~13.1k tokens at 2.6 chars/token) by applying the owner's per-prompt rule — an instruction not needed on EVERY prompt lives in a skill and keeps a one-line trigger here (#4595)
    • moved to tm-delegation-patterns: the retry protocol, task-complexity sizing, the batching anti-pattern table, per-agent model overrides and the cost model, the code-critic dispatch standard, isolation: "worktree", cross-workstream claim drawers, the structural delegation brief, and the architecture-suggestion cap
    • moved to tm-workflow: the per-phase dispatch-brief templates, the override commands, and the close-and-fold / branch-vs-worktree half of the sprint-then-harden doctrine
    • moved to tm-pr-workflow: the pre-push credential scan, which the PM delegates rather than runs
    • moved to tm-circuit-breaker: the Quick Violation Detection list, which restated the two tables above it
    • reduced to pointers, with the imperative left resident: the customization mechanics, the Trusty tool-priority per-tool tables, the direct-action budget's pm_guard detail, and the Fail-Open Check's five steps
    • the framework floor keeps every imperative — the Prohibitions table, the Circuit Breakers table, the direct-action budget with both its halves, and the Framework-Guaranteed Conventions
  • the PM asks the user based on observable conditions (ambiguous requirements, a missing credential, an irreversible architecture choice, an unrequested destructive step) rather than an uncalibrated "<90% success probability" estimate
  • the four-part report template applies to task-completion reports only, not to every PM response
  • Moved the customization doctrine into core.md, the only section a project's CLAUDE.md cannot override. One surface per artifact type: prompt/instruction sections are customized in the project's root CLAUDE.md via named-section markers and nowhere else; skills through the skill tiers (project .claude/skills/ > user ~/.trusty-mpm/skills/ > bundled, a hand-edited deployed skill frozen against redeploy by design). Ad-hoc channels — the retired .trusty-mpm/ files and anything shaped like them — stay banned as a third channel duplicating the first. Second half: what earns a place in CLAUDE.md is what is needed on EVERY prompt, since it is resident every turn; content needed only sometimes belongs in a skill, a doc under docs/, or memory. Stated in the framework-floor section instead, as it was, a project could override away the rule telling it not to override elsewhere (the hole #4722 describes). Customizing PM Behavior keeps the mechanics — marker syntax, the section-token table, tm sessions instructions verification — and now points at CORE for the rule
  • the compiled PM prompt is now written per project, to <project>/.trusty-mpm/framework/INSTRUCTIONS-COMPILED.md, and a failure to write it refuses the launch (refs #4752)
    • removes the cross-project collision by construction: concurrent sessions no longer share one global file
    • the resume path writes it too, so resumed sessions can no longer run against a prompt that was never recorded
    • tm install no longer writes a compiled prompt — install has no project, so it has nothing to compile
    • a refused launch names the path and the remedy instead of surfacing a bare I/O error
  • the prose rules in sections/core.md ("Write Plainly" and clickable references) now govern every artifact the PM authors — dispatch briefs and ticket/PR body text, not only its own responses — and add the six concrete-referent/cause-effect/before-after rules the owner ratified for framework prose (closes #4757)
  • framework engineers now gate on framework evidence instead of language evidence (#4760)
    • react-engineer, nextjs-engineer, and svelte-engineer no longer deploy to a JavaScript project that does not declare the framework as a dependency; phoenix-engineer no longer deploys to a non-Phoenix Elixir project, which elixir-engineer now covers instead
    • gcp-ops and vercel-ops are platform-gated and no longer deploy to projects with no GCP or Vercel marker
    • project marker matching gained a bounded content-probe form (<path>::<needle>) for React and Phoenix, which ship no config file that distinguishes them from plain JavaScript and plain Elixir
    • markers are now evaluated at the project root AND at every workspace member the root manifest declares (npm/yarn workspaces, pnpm-workspace.yaml, Elixir umbrella apps_path:), so a monorepo that keeps its framework in a member package keeps its framework engineers; the walk is bounded to one glob level, 256 members, and 16 MiB per detection call, and fails closed
  • framework-manifest.toml is now the single authority for agent AND skill bundling (#4765)
    • each gated agent declares its own markers in the manifest; the LANGUAGE_ENGINEERS and PLATFORM_AGENTS Rust tables are gone, so an entry's category and the condition that deploys it are one declaration
    • a new [skill_categories] section declares the bundled skill roster; a bundled skill nobody declares is a hard error, matching the agent side
    • a gated entry declaring no markers is rejected rather than silently becoming undeployable
    • stack and platform detection now share one probe budget and one workspace-member resolution per call instead of one of each
    • references/agents.md gains a Deploys When column rendered from the manifest, and references/skills.md renders the declared skill roster
    • the bundled tm, tm-delegation-patterns, and agent-delegation documents no longer restate the roster or its gates — they point at the manifest and at the generated reference
  • Retired the blocking-CI-wait doctrine across every bundled instruction copy: agents now push, take a ONE-SHOT gh pr view / gh pr checks status read, report, and end their turn; the PM owns re-engagement when CI settles (#4792)
    • gh pr checks --watch is now forbidden — it streams check output into the agent's context (546k tokens over 54 minutes on one PR). The retirement is about context cost, not runnability
    • BASE-AGENT.md "Foreground Execution — NEVER End Your Turn To Wait" is replaced by "Finishing Work — Push, Report, Stop"; own-gate commands still block in the foreground
    • version-control.md, local-ops.md, BASE-ENGINEER.md, tm-delegation-patterns.md, and the trusty-code asset mirror updated to match
    • PM instructions section renamed "Parked-Subagent Re-Engagement": a hand-back with CI pending is correct behavior, not a park, and must not be nudged back into a blocking wait
    • idle_nudge::DEFAULT_NUDGE_MESSAGE no longer tells a stalled pane to run gh pr checks --watch
    • The one-shot read now documents two traps: bucket can report a false DONE under GitHub API eventual-consistency lag (cross-check state), and repeated gh pr update-branch is a treadmill that mints a new untested head each time (BEHIND is not a correctness gate)
  • .trusty-mpm/ now belongs to a project's MAIN CHECKOUT, never to a worktree (closes #4832)
    • the compiled PM prompt moved to <project>/.trusty-mpm/sessions/<session-id>/INSTRUCTIONS-COMPILED.md; two concurrent MANAGED sessions in one project can no longer overwrite each other's. Launches with no session identity (in-place start, standalone load, tm connect) share a sessions/local/ bucket and can still collide there — narrower than the single per-project file it replaces, not collision-free
    • the project manifest.toml override moved to <project>/.trusty-mpm/framework/manifest.toml
    • the last-instructions.md stash and tm project init's scaffolding resolve against the same root, so a session launched from a worktree writes no .trusty-mpm/ into it
    • the root is resolved from git: a linked worktree maps to its main checkout and trusty-mpm's own bare <project>/.base provisioning clone maps to the project, while a submodule, a --separate-git-dir checkout and an ordinary repository merely named .base each own their own state
  • Removed feature (owner ruling, not a bug fix): the user-level ~/.trusty-mpm/manifest.toml layer is gone. Instructions and their manifest are per-project and always deployed locally, so there is no user-level manifest surface. Move any home-level overrides into <project>/.trusty-mpm/framework/manifest.toml.
  • migration is automatic and best-effort: every launch deletes a pre-existing <dir>/.trusty-mpm/framework/INSTRUCTIONS-COMPILED.md (in the worktree and at the harness root) and removes the framework/ directory only when it actually migrated a file and the directory is left empty, so neither an operator's manifest.toml nor a tm project init scaffold is disturbed
  • a pre-existing <project>/.trusty-mpm/manifest.toml is still deliberately not moved for you, but a launch that finds one with no manifest at the new path now warns and names both paths, so an override cannot go dark silently
  • the PM instruction package now carries a binding rule, in the non-overridable core section, that MEMORY.md (or any static memory-index file) is never a write target or a source — durable facts go to the palace instead, and CLAUDE.md is the only non-dynamic instruction source (closes #4834)
    • the PM Allowlist's "write a single non-source file" row no longer lists a bare memory file as an unbudgeted write
    • sections/memory.md carries a one-line pointer back to the new rule
  • BASE-AGENT.md "Never Directly Monitor a Declarative Process" no longer recommends <command> 2>&1 | tm compress, which took its exit code from tm compress and masked a failing gate. The trim now reads from the captured file (tm compress --tool "cargo test" < /tmp/gate.txt) and only when the verdict is non-zero
  • Collapsed six agent-routing surfaces in the compiled PM prompt into one Routing Table plus the generated roster. Agent Routing (core), When to Delegate to Each Agent, Ops Agent Routing, Make / Mise Command Routing and Common User Request Routing all answered "which agent handles what" for the same ~11 agents; every routing fact that existed only in a deleted table — make/mise run delegation, version/publish → local-ops, the browser-tool ban, the "just do it" pipeline — was folded into the survivor and is pinned by the_surviving_routing_table_covers_every_folded_mapping
  • Dropped the Handles: line from generated Delegation Authority roster entries. Its text was the agent's frontmatter description, byte-identical to the description the harness already publishes in its own Agent-type catalog; Role: and Model:, which the harness does not supply, are kept
  • Stated the direct-action budget once, in enforcement.md's canonical "The direct-action budget (P1 and P5 only)". core.md, identity.md and non-overridable-rules.md now point at it by title instead of restating it — the floor stays self-sufficient because enforcement.md is itself part of the floor
  • Rewrote BASE-AGENT.md in a compressed, instruction-dense register for its agent reader: imperative mood, tables over prose, rules stated once, examples kept only where the rule alone is ambiguous. All 139 rule markers verified present before and after
  • Compiled PM prompt is 12.2% smaller — 66,628 → 58,564 bytes, roughly 2,180 fewer tokens on every PM session launch
  • The daemon and supervisor launchd labels used by the autostart gate, the launchd probe, the unsupervised-daemon refusal, and the MCP bridge's no-spawn hint now come from trusty_common::launchd_labels instead of four separate literals. Values are unchanged. The hints matter most: a remedy string naming a plist the host does not have is exactly #2827, and #1900 was itself a label-lookup miss (#4868)
  • tm register now takes the repo first with an optional alias, and owner/repo is the primary form (closes #4912)
    • tm register bobmatnyc/trusty-tools registers https://github.com/bobmatnyc/trusty-tools under alias bobmatnyc-trusty-tools. GitHub is assumed, matching the is_github_remote gate tm launch already applies
    • a full URL is the alternative form and any host works there — https://…, git@host:owner/repo.git, .git suffix, trailing slash, ports. Non-GitHub shorthand is deferred, not refused on the merits
    • with no alias, one is derived as hyphen-joined owner-repo; the hyphen is deliberate, since an alias becomes a path segment wherever it is consumed
    • the legacy tm register <alias> <url> order still works: whichever positional names a repo is taken as the repo
    • a URL with no owner segment falls back to the bare repo slug; a derived alias already bound to a different URL refuses without touching the registry
    • behaviour change: arguments that name no repository are now refused instead of registered. A host with no path (https://example.com), browser paths into a repo (…/tree/main, …/pull/123), relative paths (./repo, ../repo, and dot-prefixed directories like .hidden/repo), and bare words are all errors naming the accepted forms. Each previously registered silently under a wrong alias. A ?query or #fragment is stripped before the URL is stored
    • behaviour change: a GitHub URL with any path past owner/repo is refused, so single-word tab URLs (…/owner/repo/issues, /pulls, /releases, /wiki) are caught too. They used to register repo-issues → …/owner/repo/issues at exit 0 — unclonable, and the wrong alias never collided with the canonical owner-repo. Hosts that nest legitimately are unaffected: gitlab.com/group/sub/blob still registers, as does a repo actually named tree
    • behaviour change: a URL is now stored in a form git clone accepts. A scheme-less host gains https:// (github.com/owner/repohttps://github.com/owner/repo), and a leading ~/ is expanded (~/src/repo.git/home/you/src/repo.git). Both used to be stored verbatim and failed at tm load time with repository … does not exist
  • tm-pr-workflow's frontmatter description now names the per-PR changelog fragment requirement, so a relevance match can load the skill instead of only an explicit pointer reaching it (#4968)
    • the description previously covered only branch protection, the trusty-review gate, squash-merge, and worktree discipline, so an agent searching for the changelog.d fragment format never matched the one skill that documents it
    • tm-capabilities' generated skill catalog is regenerated to match
  • PM prompt: moved instructions not needed on every prompt into the skills that already held them, shrinking the compiled prompt 60,243 → 56,783 bytes (−5.7%) (#4969)
    • the QA evidence table, QA-target routing table and forbidden-phrase list are stated once, in tm-verification-protocols; core keeps the gate trigger and workflow's third copy is gone
    • PM identity is stated once in core; the identity section keeps only its own tm-session context
    • the attribution footer is stated once in Framework-Guaranteed Conventions; workflow's inline copy is gone
    • parked-subagent re-engagement MECHANICS moved to tm-delegation-patterns' new "PM Re-Engagement" section, whose frontmatter description now names the CI-wait / parked-agent trigger so it can fire on its own; core keeps the trigger
    • every skill pointer now reads as an instruction naming the Skill(...) call. Bare [SKILL: name] notation is documentation style, not a tool call, which is how a pointer and a re-inlined copy of its target came to sit side by side; a test forbids the notation from returning
  • bundled output styles now set keep-coding-instructions: true and carry the PM prose rules directly
    • the field defaults to false, so all three styles were silently stripping Claude Code's built-in scoping, comment, and verification instructions
    • the Prose Style — Write Plainly rules are mirrored from assets/instructions/sections/core.md into each style, on the same #2647 rationale as the PRIMARY DIRECTIVE — the output style is the only channel that survives a manual claude launch with no tm-appended system prompt
    • the older, lighter ## Communication block is folded into the mirrored rules rather than left beside them
    • BASE-AGENT.md gains a graduated-verbosity rule: sparse on a clean pass, detailed on failures — the evidence rule (raw output for failures, flakes, and performance claims) is unchanged
  • tm ls now evicts session records that have sat in a terminal state (decommissioned/deleted) for more than 7 days, and releases the NUM slot each evicted record held. Numbers stopped tracking the visible fleet because nothing ever left the store — 76 of 107 slots on one machine were held by rows the default view hides, which is why NUM 107 appeared at the bottom of a 31-row listing. Retention runs on the daemon's existing GC tick. A record whose workspace directory still exists on disk is never evicted. Records written before this change carry no terminal timestamp; they are backfilled from their last evidence of life rather than from the current time, so an already-stale backlog is cleared on the next sweep instead of seven days after upgrading.
  • tm ls colors the NUM and NAME columns in two distinct hues (magenta and cyan). Deliberately not one hue: a name's trailing serial is per-project and reuses gaps, while NUM is a global slot, so any match between them is coincidence. Color is gated on stdout being a TTY and honours NO_COLOR; piped and redirected output is byte-identical to before.
  • ticketing agent must search open AND closed issues and reopen a closed ticket for a recurrence before creating a new one, and must report which outcome it took
    • ticket bodies are now a rule, not advice: defect, evidence, resolution — no structured headings, no restating linked material, file and symbol instead of line numbers
    • tm-ticketing's issue schema is relabelled six facts a reader must be able to tell, not six headings to fill in
  • ticketing agent must label every issue at creation with three families — one type (bug/enhancement/refactor/chore/documentation/epic), one or more component/crate labels, and P0P3 only when the issue text itself asserts severity — on top of the existing --assignee @me / trusty-mpm / ws/<session> defaults
    • milestones are release slots, not a field every issue receives: left unset unless the issue is deliberately scheduled into a release confirmed open, and never used to hold a ws/ workstream value
    • the agent's report now names the labels applied and the milestone state for anything it created
    • tm-ticketing states that the two harness defaults are not the whole label set and points at the agent asset for the rest
  • PM instruction package trimmed from 31,940 bytes / 4,764 words to 27,921 bytes / 4,179 words (-12.6% bytes, -12.3% words). The per-agent routing table and default-model column moved to the tm-delegation-patterns skill, which loads on trigger; workflow.md's duplicate 5-phase table and non-overridable-rules.md's duplicate marker-block mechanics were replaced by pointers; the Prohibitions and Circuit Breakers tables were compressed. No prohibition, circuit-breaker code, routing target, or the direct-action budget was removed. The #4595 word target was revised 2,000-3,000 -> ~4,000 on #5087: ~4,000 is the measured floor that keeps every rule.
  • Workflow, ticketing, and version-control policy now has one owner per surface, and tm-pr-workflow is retired into tm-workflow (#5202). tm-workflow is the sole workflow skill: it carries the delivery chain, one-outcome-one-PR, the phase dispatch briefs, worktree and branch discipline, the pre-push credential scan, branch protection, the changelog gate, the seven-field PR body, the baseline-failure protocol, the trusty-review gate, squash-merge, and the CLAUDE.md customization mechanism. tm-ticketing is the sole issue authority: promotion, deduplication, ticket style, labels, milestones, lifecycle, and issue attribution. The composed core instructions now route to exactly those two and state no workflow policy of their own.
  • Routing between ticketing and version-control is now by ARTIFACT, not by a bookkeeping/mechanics distinction. Every Issue operation is ticketing's; every Pull Request operation — including the PR title and body, first draft and every later edit — plus every git operation is version control's. Previously core, tm-ticketing, and the ticketing agent called PR-body authorship "ticketing bookkeeping" while P7, tm-pr-workflow, and the version-control agent assigned the PR to version control, which split a single gh pr edit across two owners. P6 and P7 now describe the whole artifact rather than a partial verb list.
  • The ticketing→workflow→version-control→ticketing handoff is documented once, in tm-workflow, and referenced from core. Neither specialist delegates to the other; the PM carries the canonical issue ID, outcome, and closure conditions into the version-control brief, and hands the merged PR and squash SHA back to ticketing for closure.
  • Deduplication now produces one of four named dispositions — COMMENT, REOPEN, NEW REGRESSION, or NO TICKET — each with its own criterion, replacing the previous unconditional "reopen on any recurrence". A verified fix followed by a different failure mode is new work, not a reopen.
  • Ticket bodies now require a type-aware title and one to four observable closure conditions on top of the existing sparse-body rule, with bounded length exceptions for epics, security issues, and research/audit issues only.
  • Labels are stated as four separable families (type, owning component, optional triaged priority, optional provenance) and no instruction may invent a label the repository does not carry. tm-bug-reporting previously required agent-error, skill-error, and high-priority, none of which exist in this repository; it now defers to tm-ticketing's families and sparse-body form instead of its own five-heading template.
  • Milestones stay unset unless the issue is deliberately scheduled into a confirmed-open release, is child work a release-gating parent carries, or is a blocker for a release already in flight.
  • Conflicting ticket prose is reduced to a pointer in all three output styles, BASE-AGENT.md, tm-bug-reporting, and SM_WORKFLOW.md. The output styles previously required file+line citations while the ticketing agent forbade line numbers; the styles now govern voice only and defer the binding schema to tm-ticketing and tm-workflow.
  • Ticketing model guidance in tm-delegation-patterns now says sonnet, matching the embedded agent's frontmatter and the generated roster; it previously said haiku.
  • tm doctor's skill_staleness conventions-bearing allowlist now names tm-workflow in place of the retired tm-pr-workflow, so the attribution footer, the shipped label/assignee defaults, and squash-merge-only still escalate drift to Fail.
  • The PM's resident ## Session Management section now names the session-lifecycle verbs directly — tm session ls | rename | pause | resume | stop, with rename's one-arg in-session form — instead of pointing only at tm-session-management, which covers context-limit pause/resume snapshots and never mentioned the CLI verbs. A PM asked to rename, stop, or list a session answers from resident instructions rather than dispatching an agent to discover the command (#5211). Paid for by cutting redundant resident text: two back-pointer stubs carrying no rule (identity.md's "stated once in CORE", memory.md's "see Core's Memory & Instruction Sources"), the second copy of the retired .trusty-mpm/ override-file list, and the third restatement of what "Non-Overridable" means. Package: 4,268 -> 4,231 words.
  • tm-cli-operations documents tm session rename in both forms; it was the one lifecycle verb the skill's session listing omitted, present only in the generated tm-capabilities CLI reference.
  • A session now starts in the project's main checkout instead of provisioning its own git worktree. The project's worktree setting no longer decides this — it decides whether the AGENTS a session dispatches get isolated, and a project registered worktree: true still isolates them exactly as before.

Removed

  • the deprecated ops agent is deleted from the bundle; it was superseded by local-ops and still reached every roster (#4760)
    • an ops.md already deployed to a machine is NOT retracted — orphan retraction is #391 and has not shipped
    • trusty-code's embedded mirror of the agent catalog drops ops.md and gains elixir-engineer.md, keeping scripts/check_agent_assets.sh green
    • the bundled tm skill and agent-delegation.md no longer name ops in their rosters, and now say which agents are marker-gated rather than implying every bundled agent reaches every project
  • PipelineOutput::merged and PipelineOutput::instructions_loaded, and PipelineInput::framework_instructions_path. All three existed only to serve the retired framework-instructions read and had no production consumer

Security

  • auto-prune can no longer destroy anything outside the session registry: decommission_record_only is now a dedicated single-effect function rather than a flag on the destructive teardown (closes #4728)
    • as a flag it left two destructive effects ungated — a runtime SIGTERM plus kill_session against any live pane sharing the record's name, and a cross-daemon DELETE /indexes/{id} whose target resolved to the PARENT PROJECT's search index once the workspace was gone
    • both were reachable from tm ls in 1.3.3 and 1.3.4; neither required a name collision
    • decommission_with_root keeps every effect, no longer takes a record_only parameter, and now documents its side effects as an explicit table
  • allowlisted untracked files (default .env*) are no longer copied into a session worktree until git confirms they will be ignored there (closes #4733)
    • the info/exclude registration ran AFTER the copy and a failure was only a warn!, so a worktree whose git rev-parse merely failed — a stale gitlink, detected dubious ownership, an unreadable .git — was left holding the operator's .env unregistered, where a later git add -A && git commit stages it into history
    • the order is inverted: register first, then re-verify each path with git check-ignore (git's own authority, mirroring native_mcp::is_env_local_actually_ignored) and copy only what it confirms
    • this also stops a secret overwriting a path the repo already TRACKS — git reports tracked paths as not-ignored no matter what info/exclude says, and git add -A would stage the change
    • a destination with a corroborated absence of any repository has no history to leak into and is still copied to freely, so non-git destinations are unaffected
  • agent-cost persistence hatch: close the git option-abbreviation bypass (#4850)
    • git's parse-options resolves any unambiguous prefix of a long option, so --exe is --exec and --rece is --receive-pack. The exec-option deny list matched names exactly, so git push --exe=<program> … and --rece=<program> classified as "persistence only" past the context ceiling and ran the named program. The post-subcommand long-option surface is now default-deny against a safe allowlist, so an abbreviation of a dangerous option — and any construct nobody has thought of — is rejected without being enumerated.
    • the program check compared the basename, so ./git, /tmp/evil/git, and ../../tmp/evil/git all passed as "git". It now requires the literal token git; a path-qualified git is rejected.
    • the deny text an agent sees at the ceiling now names the shell-metacharacter rule and the single-quote fix, so git commit -m "fix $ISSUE" no longer denies without a hint. It also names the :: rule and that quoting does not exempt it, so git commit -m 'use std::process::Command' — routine content in a Rust workspace — denies with a reason instead of silently. Narrowing that rule to spare quoted -m values is tracked in #4869.
    • corrected the module's residual note: -C / --git-dir are allowed globals, so the repository whose config supplies diff.external is attacker-choosable in the same command, not confined to the agent's own repo. The note now also names git hooks (a planted .git/hooks/pre-commit fires on git commit -am hooked while the classifier allows the command) and core.fsmonitor, which fires on a bare git -C <path> status.

Documentation

  • corrected trusty-mpm's stated skill precedence, which had Claude Code's runtime order backwards and conflated it with tm's deploy-time order
    • mpm-skills-manager and mpm-agent-manager now carry the verified runtime resolution for their own artifact type and state the asymmetry: for skills personal ~/.claude/skills/ beats project, for agents project .claude/agents/ beats user. Each says where to write a file so it actually wins the collision
    • the skills manager's existing tier section is relabelled deploy-time source precedence, so tm's project-custom > user-custom > bundled rule is no longer read as Claude Code's runtime order
    • core::project_skill_tier and core::managed_config asserted that <project>/.claude/skills outranks $CLAUDE_CONFIG_DIR/skills. That is backwards for skills, and it was load-bearing for the #4880 redeploy rationale. Both now state the real order and name which axis they mean; the redeploy still earns its place because the project tier is what loads for every name the managed roster does not carry
  • the bundled tm-workflow skill now carries the full generic worktree discipline, so a project's CLAUDE.md no longer has to restate it (#5269)
    • moved in from this repo's CLAUDE.md: branch off origin/main never local main, "experiments stay session-local", the stash-first escape hatch for a one-off main-checkout command, git push origin --delete as the manual cleanup case, QA agents getting their own worktree, and the note that worktree cleanup never touches the main checkout
    • tm-cli-operations' prune-worktrees line keeps its .worktrees/ path — that is tm's own session provisioning, which still writes there — and now says so, so it is not mistaken for the canonical .claude/worktrees/ home

1.3.4 2026-08-03

Added

  • New bundled ## Source Citations section in sections/workflow.md: source citations in docs and reports link to a GitHub blob permalink pinned to a commit SHA (never blob/main), with path:line as the link text and the line number verified before linking. This is framework doctrine, not project-specific, so it ships in the bundled instructions rather than a project-level .trusty-mpm/ override (see #4578).

Fixed

  • A changelog fragment carrying a second category is now rejected instead of silently mis-rendered. Only line 1 is a category and everything after it is copied through verbatim, so bare Changed/Added/Fixed lines became body text and every bullet landed under the line-1 heading — the 1.3.3 4286-retire-trusty-mpm-override-files.md fragment put all four of its categories under ### Removed, caught only by a human diffing the --stdout preview. Both the bare-word and the markdown-heading form (## Changed) are caught, and scripts/assemble-changelog.sh names the file and the exact line of each. Fenced code blocks are exempt, so a fragment may document the fragment format or show example output without tripping the check. The CI gate inherits all of it, because scripts/check_changelog_fragment.sh asks the assembler rather than re-implementing validation. Guarded by scripts/assemble_changelog_selftest.sh, which replays the real fragment.

1.3.3 2026-08-03

Added

  • tm doctor check legacy_overrides: FAILS when a project still carries any retired override file, naming every file found and the migration. The prompt resolver logs the same signal on every session launch, so a leftover file can never drop a project's rules silently (#4286).
  • crates/trusty-mpm/src/assets/instructions/sections/README.md documenting how framework instructions compose, the customization tiers, why there is no floor, and what must not be reintroduced (#4286).

Fixed

  • The seeded CLAUDE.md stub could declare a live override. A worked marker example in the stub was parsed as a real WORKFLOW block — marker recognition is whole-line and knows nothing about code fences — so every newly seeded project silently lost its entire bundled workflow section and received the placeholder prose instead. Found by running a real tm instance during acceptance; guarded by seeded_claude_md_declares_no_overrides (#4286).
  • Swept shipped asset text that still advertised the five retired .trusty-mpm/ per-file PM instruction overrides (#4286): the tm-workflow skill described them as the live customization mechanism and told the PM to write to them; three output styles (trusty-mpm, trusty-mpm-research, trusty-mpm-teacher) described the compiled prompt as four monolithic files (PM_INSTRUCTIONS.md + WORKFLOW.md + AGENT_DELEGATION.md + BASE_PM.md) that haven't existed since #4183; tm-delegation-patterns, tm-circuit-breaker, and tm-pr-workflow pointed at AGENT_DELEGATION.md / .trusty-mpm/INSTRUCTIONS.md as if still reachable. All now describe the current model: framework instructions compose from assets/instructions/sections/*.md, and the sole project-customization channel is a named-section marker in the project's root CLAUDE.mdcore is the only section such a marker cannot replace.
  • assets/instructions/sections/workflow.md pointed the project test-ladder lookup at the retired .trusty-mpm/INSTRUCTIONS.md; it now points at CLAUDE.md. This is a compiled-prompt change, so the pm-prompt-bundled- fallback.md and pm-prompt-roster-absent.md golden fixtures were regenerated (UPDATE_GOLDEN=1 cargo test -p trusty-mpm golden) to match.
  • Verified no scaffolding path (tm-init, tm project init) creates any of the five retired override files — the only writers found were the legacy_overrides doctor check's own fixtures.

Changed

  • ticketing agent's default model tier is now sonnet, up from haiku. Duplicate-detection and scope-boundary judgement (is this issue already filed, is this work in scope for a milestone) are judgement calls, not clerical ones — observed 2026-08-03: a haiku-tier ticketing agent filed a duplicate issue after being told not to, and cleared milestones on a shipped release when asked only to report them.
  • core is now the ONLY section a named-section override cannot replace. identity, enforcement, non-overridable-rules and framework-guaranteed-conventions become tier project and are overridable like every other section; no content moved between sections. validate enforces the tier assignment as an iff, so both retiering core away from fixed and marking a second section fixed are hard errors (#4286).
  • The seeded project CLAUDE.md stub now documents the marker grammar, lists the accepted tokens, and points at .trusty-mpm/last-instructions.md as the record of what the session actually received. It no longer refers to BASE_PM.md, which has not existed since #4183 (#4286).
  • Credential resolution now imports from trusty_common::credentials instead of trusty_common::inference::credentials, which was deprecated in the same change (see #4564). Import-path churn only — no behaviour, precedence, or credential surface changes in this crate.
  • The bundled tm-ticketing skill now carries a Ticket-Promotion Gate: a finding is not automatically a ticket. Filing requires a duplicate search first (by test name, error text, affected symbol, and package) and then at least one of five promotion criteria — a reproduced user-visible defect, accepted feature work, a different owner/release/dependency/security disposition, work that cannot fit the current PR without changing its outcome or risk, or an explicit user request to track it. Everything else stays a session task, a PR review comment, or a parent-issue checklist item, and "follow-up" is named as a category that does NOT bypass the gate. Four overlapping recommendations are merged into one five-step protocol rather than stacked as near-duplicates: the gate also fixes issue granularity to outcomes instead of findings (one canonical issue per recurring flaky test, occurrences appended; never a separate issue for the tests, docs, changelog, or review cleanup of one outcome) and supplies a six-field issue schema. It cross-references the already-shipped Opportunistic Fixes rule instead of restating it — an easy fix found while working a file never enters the gate at all (#4630).
  • Findings filed as issues now state a confidence level — Observed, Reproduced, Inferred, or Speculative — with a default disposition for each. Inferred and Speculative belong on the parent issue or PR unless severity justifies escalation. The skill says explicitly that nothing reads this label mechanically: it is a drafting rule whose only check is whether a reader of the filed issue can tell which state was claimed (#4630).
  • The bundled tm-pr-workflow skill now states the one-outcome-one-PR invariant, which the skill previously lacked entirely. A PR carries one primary outcome plus everything needed to ship it safely — implementation, regression tests, refactoring, docs, the changelog fragment, and in-scope review fixes — and those are never split across PRs merely because different agents produced them. One PR may close several tickets when one coherent change satisfies them, which is preferred over coupled PRs with an artificial merge order. A seven-field minimal PR body accompanies it (#4630).
  • tm-pr-workflow gains a six-step baseline-failure protocol for a red gate the branch did not cause: establish causation against the base branch, fix branch-caused red in the PR, append run/SHA/command/failure-signature to the canonical issue when the failure is already tracked, and open exactly one canonical issue when it is not — after reproduction or sufficient CI evidence, never from a single unrelated red run. Step 6 mandates a literal report string, change-specific gates pass; <gate name> blocked by canonical issue #N, so the rule is checkable; "all tests pass" is never reportable while a gate is red. Step 1 cross-references the shipped "never turn red green by deleting coverage" line rather than duplicating it (#4630).
  • The tm-pr-workflow delivery chain now reads accepted outcome -> optional issue -> worktree branch -> one cohesive PR -> applicable gates -> trusty-review gate -> squash-merge -> worktree cleanup. The issue step is optional for docs/CI/chore work and a small explicitly-requested fix that completes in one PR, and required for features, reproduced defects, security work, cross-release dependencies, and work that must survive the current session (#4630).
  • rust-engineer agent now scopes its Quality Bar to the crate under change (cargo test -p <crate>, not a bare unscoped cargo test), matching the crate-scoped-gate guidance the PM already ships in core.md. A crate-scoped run finishes well under the 10-minute tool timeout; a workspace run does not.
    • Adds a change-class table for widening scope deliberately, and an explicit "scope is for speed, never for hiding a failure" rule: narrowing the scope you run is fine, shrinking the coverage that exists#[ignore], cfg-gating, --exclude, dropping to --lib — is never allowed.
  • The bundled code-review-standards skill now gives every review finding a three-way disposition, extending the shipped "fix it in the surfacing PR or drop it" rule with the third exit it left implicit. A finding ends as Fix here (corrected in the surfacing PR — the default for correctness, security, acceptance criteria, and regression coverage), Parent (kept with the work in flight as a PR comment or parent-issue checklist item, creating no durable artifact), or Promote (recommended for a standalone issue). Promote is a recommendation only: the reviewer never files, the Ticket-Promotion Gate in tm-ticketing decides, and the PM or user makes the prioritization call. An APPROVE verdict no longer implies tickets for its non-blocking MEDIUM/LOW observations (#4633).
  • The critic verdict template carries the disposition explicitly, so the rule is checkable rather than decorative: the Findings table gains a Disposition column, every row must carry exactly one token, and a blank or missing cell is named an incomplete review — a reader of the posted verdict can tell per finding which of the three was chosen. A zero-finding APPROVE has no rows and so no dispositions to state (#4633).
  • tm-ticketing gains the reciprocal pointer: a review or QA finding reaches the Ticket-Promotion Gate by exactly one route, the Promote disposition, and Fix here / Parent findings never reach it. The gate's five criteria stay stated once, in tm-ticketing (#4633).
  • Risk is now the second input to phase entry in the PM instruction package. Three labels — Low (docs, comments, mechanical metadata), Normal (a localized behaviour change inside one package), and High (security, destructive or irreversible paths, persisted state, release/SemVer, or a contract another package depends on) — fold into the EXISTING skip conditions rather than forming a competing gate matrix. Where a skip condition is a size or simplicity heuristic, High risk means it does not hold: a 30-line change to a credential path is small and still earns its review. Phase 2's skip condition gains "not High risk" in both the canonical CORE phase table and its WORKFLOW restatement, so the two cannot drift. The labels carry no testing standard — the project's test ladder answers how much testing a change needs (#4633).

Removed

  • The bundled crates/trusty-mpm/src/assets/instructions/INSTRUCTIONS.md stub and the FRAMEWORK_INSTRUCTIONS constant/bundle-table entry that embedded it. This is the framework-owned launch-artifact stub, distinct from (and unrelated to) the project-level .trusty-mpm/INSTRUCTIONS.md override file retired by #4665. Its content never reached a live session: tm install/tm launch always overwrite the same on-disk path (instructions/INSTRUCTIONS.md) with the fully assembled system prompt in the same call, and the one legacy reader that treats it as optional (build_instructions) already discards its result for anything but a side-effect (#4286 split A).
  • The five .trusty-mpm/ PM instruction override files (PM_INSTRUCTIONS_DEPLOYED.md, AGENT_DELEGATION.md, WORKFLOW.md, MEMORY.md, INSTRUCTIONS.md) are retired and no longer read. Project customization is named sections in the project's root CLAUDE.md. .trusty-mpm/INSTRUCTIONS.md also stops being a marker host; CLAUDE.md is the only one (#4286).
  • The non-overridable framework floor, in full: SectionId::is_floor(), the FloorNotFixed and OverridableAfterFloor validation rules, validate_floor_is_last, scripts/check_instruction_floor.sh, scripts/instruction_floor.sha256, and .github/workflows/instruction-floor-guard.yml (plus its duplicated step in ci.yml). A project owns its own CLAUDE.md, so the floor was the appearance of a control rather than a control (#4286).
28 earlier releases
  • 1.3.2 2026-08-03
  • 1.3.1 2026-07-31
  • 1.2.3 2026-07-28
  • 1.2.0 2026-07-27
  • 1.0.2 2026-07-25
  • 1.0.1 2026-07-24
  • 1.0.0 2026-07-24
  • 0.21.0 2026-07-23
  • 0.20.0 2026-07-21
  • 0.19.29 2026-07-21
  • 0.19.28 2026-07-20
  • 0.19.27 2026-07-19
  • 0.19.26 2026-07-19
  • 0.19.25 2026-07-18
  • 0.19.24 2026-07-17
  • 0.19.23 2026-07-17
  • 0.19.22 2026-07-17
  • 0.19.4 2026-07-09
  • 0.14.0 2026-07-01
  • 0.13.0 2026-06-30
  • 0.12.0 2026-06-27
  • 0.11.0 2026-06-24
  • 0.10.0 2026-06-17
  • 0.9.0 2026-06-16
  • 0.8.2 2026-06-16
  • 0.5.0 2026-05-28
  • consolidation 2026-05-26
  • 0.4.0 and prior

What each of these changed is in crates/trusty-mpm/CHANGELOG.md.

trusty-analyze

Code analysis sidecar

19 releases. crates/trusty-analyze/CHANGELOG.md on GitHub is the source this section is generated from.

0.9.0 2026-08-10

Breaking

  • POST /webhooks/github is retired and now returns 404 (#5181, ADR-0034). GitHub deliveries reach trusty-analyze only through trusty-console's POST /api/webhooks/{source}, which verifies the HMAC once, spools the payload durably, and relays over UDS to trusty-analyze webhook-listen. The route is deleted rather than stubbed, so a delivery still aimed at it fails visibly at GitHub instead of being acknowledged and dropped. Anyone with a GitHub webhook pointed at trusty-analyze directly must repoint it at the console. The analysis pipeline is unchanged — the route's handler already delegated to webhook_drain, which the UDS path uses.
  • Removed public API: service::handlers::review::github_webhook_handler, core::verify_webhook_signature (and core::github::verify_webhook_signature), and AnalyzerAppState::{webhook_secret, with_webhook_secret}. This crate no longer verifies a webhook signature at all; that is trusty-console's single implementation (ADR-0034 §3), so the hmac, sha2 and hex dependencies are dropped.
  • webhook_listener::run now takes a TrustySearchClient, which it needs to run the analysis pipeline. Callers must pass the client they already build from --search-url (#5192).

Added

  • trusty-analyze webhook-listen binds trusty-analyze-webhook.sock, the socket trusty-console has been relaying verified GitHub deliveries to since #5089 step 3 with nothing on the other end. Each delivery is fsync'd to a durable inbox under the crate's data directory before the acknowledgement is written; an acknowledgement is what lets console delete its own copy, so nothing is acked that is not already held. The listener exits on SIGTERM, so the socket exists without the service running resident. Both the socket and the inbox root resolve from trusty_common::webhook_relay rather than being spelled here, so the directory this service writes to is by construction the one trusty-console meters for an undrained backlog. The legacy POST /webhooks/github route is unchanged.
  • GET /indexes/{id}/complexity_distribution and the matching complexity_distribution MCP tool return the full A-F cyclomatic-complexity histogram over an index, with the counted total, in a payload bounded at five rows regardless of corpus size (#5320).

Fixed

  • service install evicts com.trusty.trusty-analyze, the label an older installer registered. The registry recorded it as a legacy alias and nothing acted on it, so the record meant nothing on a host that needed it (#4868)
  • SCIP graph overlays now survive a daemon restart (closes #5049). POST /indexes/{id}/scip wrote into an in-process HashMap<String, KgGraph> and answered HTTP 200; a restart discarded the ingest, and GET /indexes/{id}/graph then served a tree-sitter-only graph indistinguishable from one where the overlay had been applied. A SCIP index is uploaded by the operator and cannot be re-derived from the corpus, so the overlay is now written to a redb store (scip_overlays.redb, a sibling of the facts store — no new CLI flag).
  • A caller can now tell "no SCIP data" from "empty SCIP graph". GET /indexes/{id}/scip is new: 404 when nothing has ever been ingested for that index, 200 with {index_id, nodes, edges, ingested_at} when an overlay exists — including a legitimately symbol-free one, which reports nodes: 0. GET /indexes/{id}/graph carries the same fact as an x-scip-overlay: present|absent response header; its JSON body is still a bare KgGraph, so existing consumers are unaffected. A failure to read the overlay store is a 500 rather than a silent fall-through to the tree-sitter-only graph.
  • POST /webhooks/github now fails closed when no webhook secret is configured (closes #5173). With GITHUB_WEBHOOK_SECRET unset the handler logged no webhook secret configured — skipping webhook signature verification and processed the payload, so any local process that could reach the loopback port could inject arbitrary PR coordinates into the analyze pipeline and make the daemon fetch a diff and post a comment under the daemon's GITHUB_TOKEN. An unset or empty secret now returns 401 webhook secret not configured before the payload is parsed, matching trusty-review's handle_github_webhook. Deployments that relied on the unauthenticated path must set GITHUB_WEBHOOK_SECRET; every other endpoint is unaffected and the daemon still starts without it.
  • Scope: this closes the webhook route only. POST /review/github-pr still accepts arbitrary owner/repo/pr coordinates with no authentication and drives the same GITHUB_TOKEN; it is unchanged here.
  • trusty-analyze webhook-listen now drains its webhook inbox into the analysis pipeline instead of holding acknowledged deliveries forever. The PR-event filter and the fetch/analyse/comment pipeline moved to webhook_drain, so the legacy POST /webhooks/github route and the UDS drain run one implementation.
  • A delivery is never analysed twice. The shared drain's processed-delivery ledger closes the crash window that would otherwise post a duplicate PR comment (#5192).
  • GET /indexes/{id}/refactor-suggestions no longer suggests refactors for files with no mapped language. Documents, FAQs, and CI workflow YAML were scored by the keyword text heuristic, graded F, and returned as critical "extract method" suggestions (#5317).

Changed

  • LAUNCHD_LABEL is read from trusty_common::launchd_labels::ANALYZE rather than restated beside the installer's separate copy of it. The value is unchanged — the point is that the installer's copy can no longer drift away from the daemon's, which is what broke trusty-search (#4868)
  • One shared open-with-quarantine policy for both redb stores, in the new core::redb_open module (part of #5049). FactStore already renamed a format-obsolete facts.redb aside as *.v2-incompatible and booted with a loud ERROR (#702); the new SCIP overlay store now does the same, quarantining as *.quarantined. Both classify the redb error first: an obsolete on-disk format is moved aside, while a transient failure to open — permissions, disk, a held lock — stays fatal, because recreating on top of a file that is merely unavailable would destroy data that is still good. Neither store deletes anything. This replaces a duplicated classifier, so the two stores cannot drift into giving opposite answers to the same byte-level cause.
  • Breaking (library API), part of #5049: AnalyzerAppState::scip_overlays changed type from Arc<RwLock<HashMap<String, KgGraph>>> to the new core::ScipOverlayStore, and AnalyzerAppState::new / AnalyzerAppState::with_registry take it as a required argument. It is a constructor parameter rather than a with_* override so no caller can end up with a non-durable overlay store by omission — that omission was the bug.
  • The MCP tool section of README.md and CLAUDE.md is now generated from mcp::tool_descriptors() plus mcp::descriptors::review_tool_descriptors() by tests/generated_docs.rs. The feature-dependent surface is stated as derived numbers — 19 tools with default features, 22 with --features review — with a per-row Available column, replacing prose that told the reader to go read tool_descriptors() because no fixed number was safe. Regenerate with UPDATE_DOCS=1 cargo test -p trusty-analyze --test generated_docs (#5205)
  • review_tool_descriptors() moved from the #[cfg(feature = "review")] mcp::review module to mcp::descriptors, so the three tr_review_* descriptors compile in every build. Dispatch stays feature-gated and tools/list is unchanged in both configurations; the move is what lets a default build — the only one CI runs — verify the documented review rows (#5205)
  • README.md keeps its HTTP-equivalents table hand-written, because the route a tool forwards to is not in the descriptors. It now sits outside the generated markers and every tool name in it is asserted to be real by http_equivalents_name_only_real_tools (#5205)

Removed

  • BREAKING — the next release of this crate must be 0.9.0, not 0.8.x. Removed the fastembed/ONNX neural clustering embedder and, with it, public API: EmbedderKind::Neural, embedder::NeuralEmbedder, the bundled-ort / load-dynamic / cuda Cargo features (default is now ["http-server"]), and ClusterQueryParams::method's type (now Option<String>, validated in the handler). CI cannot detect a SemVer break (#4088 — the gap that got 0.7.3 yanked), so this line is the record a releaser has to act on. Nothing selected method=neuraltrusty-console, the cluster_concepts MCP tool and the embedded UI all used the bow default — yet the daemon constructed the model at every boot, and the untimed Hugging Face request that construction made blocked the listener for as long as the request took (31m46s in one production boot; reproduced at 60.17s and 120.13s against a stub HF endpoint with matching injected delays, versus 0.20s after the fix). bow is now the sole embedder, --fastembed-cache is an accepted no-op so existing launchd plists keep starting, and ?method=neural returns 400 instead of BOW vectors labelled neural (#5067)

0.8.0 2026-07-27

MINOR, not the patch 0.7.5 this was originally staged as (#4177). This crate publicly re-exports trusty-common types — src/types/entity.rs:14-15:

pub use trusty_common::symgraph::contracts::EdgeKind;
pub use trusty_common::symgraph::{fact_hash_str, EntityType, RawEntity};

surfaced unconditionally as trusty_analyze::types::{EntityType, RawEntity, EdgeKind, fact_hash_str} (lib.rs:82 pub mod typestypes/mod.rs:14 pub mod entitytypes/mod.rs:21 pub use entity::{…}; no cfg, and the trusty-common/symgraph feature is enabled unconditionally). Raising the trusty-common requirement from ^0.26 to ^0.27 therefore changes the identity of publicly re-exported types.

At patch level that is a break shipped silently: ^0.7.4 re-resolves already-published consumers onto the new type identity, and a consumer that also depends on trusty-common 0.26 directly gets two semver-incompatible copies linked at once and mismatched-type errors against trusty_analyze::types::EntityType. That is bit-for-bit the defect that forced the 0.7.3 yank on this very crate. ^0.7 excludes 0.8.0, so published consumers keep resolving to 0.7.4 and stay installable.

Changed

  • trusty-common requirement raised to ^0.27 (was ^0.26, inherited from [workspace.dependencies]): 0.27.0 makes ChatEvent #[non_exhaustive], which a ^0.26 requirement cannot express. Because the re-exports above are public, this requirement change is itself the reason for the MINOR level.

Fixed

  • Post-publish source drift — explain did not compile against the ChatEvent::Usage variant. src/core/explain.rs gained its ChatEvent::Usage(_) arm in #4112, after 0.7.4 was published, so the published 0.7.4 artifact cannot build against any trusty-common carrying that variant. This is the same failure shape as #4079 below — a downstream exhaustive match broken by an upstream variant addition — and this release ships the arm before it can bite a cargo install. The match also gained a wildcard arm now that ChatEvent is #[non_exhaustive], so the next variant addition is no longer breaking here.

0.7.4 2026-07-27

Fixed

  • cargo install trusty-analyze failed to compile with error[E0063]: missing field no_spawn_hintin initializer ofDaemonBridgeConfig`` (#4079): a fresh install of the previously-published 0.7.3 could not be built at all. 0.7.3 was published 2026-07-07 and declared trusty-common = "0.22.0" — a caret range [0.22.0, 0.23.0). Five days later, trusty-common 0.22.5 added the public field no_spawn_hint to DaemonBridgeConfig (a SemVer-breaking public-field addition shipped in a patch bump; the struct carries neither #[non_exhaustive] nor a Default impl, so a struct literal missing a field is a hard compile error). cargo install re-resolves without a lockfile, so it picked the newest in-range 0.22.x and paired 0.7.3's pre-field source with a post-field dependency. This release carries the corrected call site (crates/trusty-analyze/src/commands/daemon_guard.rs now sets no_spawn_hint: None) and declares trusty-common ^0.26.0, verified against the live registry with cargo publish --dry-run. Workspace CI never caught this because every member consumes trusty-common through a path dependency plus a root [patch.crates-io] override, so caller and definition are permanently in lockstep and no version resolution ever happens locally. Users on 0.7.3 could work around it with cargo install trusty-analyze --locked.

  • Smell-count false positives made the codebase-wide quality metric untrustworthy (#3522): the whole-codebase quality report and PR-review path (core/quality.rs, core/review/mod.rs) always scored chunks with the language-agnostic text heuristic, which over-counts DeepNesting on ordinary, idiomatically formatted Rust (e.g. for (i, x) in v.iter().enumerate()) and flags MissingDocstring on every undocumented function regardless of visibility. Both paths now dispatch through the existing language-aware compute_complexity_for (tree-sitter-backed for Rust/TypeScript), and MissingDocstring now only fires for public API surface (pub items in Rust; exported functions / non-private class methods in TS/JS). Measured on this repo's own crates/ tree: smell count dropped from 21,407 to 8,906 across ~47.8K chunks (0.448 → 0.186 smells/chunk).

Changed

  • UI tokens now CI-enforced against the canonical Foundry source (refs #3486): flipped from the scripts/check_token_drift.mjs allowlist to ENFORCED. The token-drift CI job now compares ui/src/lib/styles/tokens.css's plain-CSS --trusty-*: #hex values directly to docs/design/UI/design-system/tokens.css on every push/PR (light [data-theme="light"], dark :root, [data-theme="dark"]; the crate-local alias layer is ignored), so a hand-edit that drifts this crate's palette from canonical fails the build.
  • UI design tokens migrated to Foundry v2 (#3490, epic #3486): the dashboard's tokens.css now sources its light/dark palette from the canonical Foundry v2 ("rust-on-paper") design tokens instead of Catppuccin Mocha/Latte. The crate's existing component-facing alias names (--bg, --border, --text, --grade-*, etc.) and light/dark activation mechanism ([data-theme] on <html>) are unchanged — only the underlying color values moved.

Security

  • Router-wide same-origin (CSRF) write guard (#3304): destructive write routes (POST /indexes/{id}/scip, POST /review, POST /analyze/deep, POST /facts, DELETE /facts/{id}, the GitHub webhook) are now guarded against cross-origin browser requests via the shared trusty_common::server::with_guarded_middleware. Method-gated (GET reads and /sse unaffected) and fail-open on a missing Origin (the console proxy, curl, and GitHub's server-side webhook POST keep working).
16 earlier releases
  • 0.7.3 2026-07-09
  • 0.7.2 2026-06-16
  • 0.7.0 2026-06-09
  • 0.6.0 2026-06-09
  • 0.5.1 2026-06-07
  • 0.5.0 2026-06-03
  • 0.4.2 2026-06-02
  • 0.4.1 2026-06-01
  • 0.3.0 2026-06-01
  • 0.2.1 2026-05-31
  • 0.2.0 2026-05-29
  • 0.1.10 2026-05-22
  • 0.1.6 2026-05-20
  • 0.1.5 2026-05-20
  • 0.1.2 2026-05-11
  • 0.1.0 full Phase 1 + Phase 2 static analysis engine

What each of these changed is in crates/trusty-analyze/CHANGELOG.md.

trusty-review

LLM code review

30 releases. crates/trusty-review/CHANGELOG.md on GitHub is the source this section is generated from.

0.15.0 2026-08-12

Breaking

  • report now always synthesizes, and a synthesis that produces no verified prose fails the run instead of writing a deterministic-only report (#5454).
  • --synthesize is accepted but ignored, and prints a deprecation line — the flag stays parseable so scripts and older tga invocations keep working.
  • report checks OPENROUTER_API_KEY before reading the manifest, so a missing credential costs nothing but the error.
  • Synthesizer::synthesize returns Result<Synthesis, SynthesisError>; SynthesisStatus, Synthesis::unavailable, and Synthesis::is_available are removed, so the type can no longer represent a failed pass.
  • Reporter::write rejects a model carrying no synthesis with the new ReportError::SynthesisRequired.
  • The deterministic Executive Summary composition from #5374 is kept: it now fills §2 when the numeric guardrail rejects the model's summary.

0.14.1 2026-08-11

Fixed

  • Inline PR comments now lead with a verification caveat when the finding's own verified outcome owes one, so a refuted finding no longer reads exactly like a surviving one (#5312). A clean refuted says it was disproved and is not a merge blocker; error_refuted / truncation_refuted say the verifier was never reached and the claim is unverified, not disproved.

Security

  • DD-report findings are now scrubbed of this process's credentials before they reach the report (#5323). trusty-analyze's daemon output, a manifest-declared metrics JSON, and the investigation's verified findings previously travelled verbatim into the rendered finding bands, the executive summary, the synthesis digest sent to the LLM provider, and the JSON twin — tga's report path has had this guarantee since #5239 and trusty-review's had none.
  • An investigation finding reaches the page by two independent routes and both now scrub. apply_investigation covers the metrics route and the investigation record stored on the model; merge_investigation_prose covers the synthesis route, which FindingRow::merge_prose overwrites the metrics prose with. The verbatim evidence_quote has no metrics route at all and is only ever scrubbed on the second.
  • The scrub runs where findings enter each sink, ahead of every downstream truncation, and covers every producer-supplied string — including AnalyzeMetrics.schema_version, which a declared metrics JSON authors freely. Needles come from trusty_common::credentials' registry walk, so no secret is passed across a process boundary.

0.14.0 2026-08-10

Fixed

  • The technical-DD report's §2 Executive Summary no longer renders _No data available — see Gaps & Caveats._ on a run without --synthesize (issue #5318). It was filled only from LLM synthesis prose, so every tga audit report collapsed the first section a diligence reader opens while listing real RED/AMBER findings in §5. §2 and its Top Risks table now roll up from the report's own data — applications, size, language mix, severity counts by dimension, and the application risk concentrates in — with the provenance of the figures used. Verified synthesis prose still wins when --synthesize runs.
    • When nothing measurable was supplied, §2 now names the specific missing inputs (no metrics file, no --analyze fetch, no scannable checkout) instead of collapsing to the generic Gaps & Caveats pointer.
    • The Top Risks table caps at five rows and now says so in the table itself ("Top 5 of 7 — 2 further RED/AMBER finding(s) are not listed here…"), so a reader who skims the table without the paragraph above it cannot mistake five rows for the whole risk picture.
  • The report's Data gaps: line now states how many gaps it lists and then lists exactly that many. It used to comma-join the labels straight after the colon, so a label carrying its template section number rendered as Data gaps: 2. Executive Summary, … — read as a count of two ahead of sixteen names. The count is now the length of the same slice the line joins, and items are separated with ; so a label's own comma or leading section number cannot be read as a count or a list boundary (#5319).
27 earlier releases
  • 0.13.0 2026-08-10
  • 0.12.0 2026-08-10
  • 0.11.0 2026-07-27
  • 0.10.1 2026-07-23
  • 0.10.0 2026-07-21
  • 0.9.2 2026-07-17
  • 0.9.1 2026-07-16
  • 0.9.0 2026-07-11
  • 0.8.1 2026-07-11
  • 0.8.0 2026-07-10
  • 0.6.1 2026-06-25
  • 0.6.0 2026-06-24
  • 0.5.0 2026-06-24
  • 0.4.0 2026-06-18
  • 0.3.16 2026-06-18
  • 0.3.15 2026-06-18
  • 0.3.10 2026-06-16
  • 0.3.8 2026-06-09
  • 0.3.6 2026-06-07
  • 0.3.5 2026-06-03
  • 0.3.4 2026-06-03
  • 0.3.3 2026-06-03
  • 0.3.2 2026-06-03
  • 0.3.1 2026-06-03
  • 0.3.0 2026-06-03
  • 0.2.0 2026-06-03
  • 0.1.0 2026-05-28

What each of these changed is in crates/trusty-review/CHANGELOG.md.

trusty-git-analytics

Developer analytics from git

37 releases. crates/trusty-git-analytics/CHANGELOG.md on GitHub is the source this section is generated from.

2.17.0 2026-08-12

Changed

  • audit now passes --synthesize to trusty-review report, so the due-diligence report carries a written executive summary, top-risk rationale, and RED/AMBER finding prose. Until now it passed only --analyze, and every audit report ever produced was fully deterministic (#5454).
  • audit requires OPENROUTER_API_KEY and checks it before stage 1. An unset or blank key fails immediately, naming the variable and how to set it, rather than after a multi-minute sweep.
  • A failed render now prints the exact command to re-run just the render; manifest.toml is written before the renderer is called, so nothing collected is lost.
  • audit requires trusty-review 0.15.0 or newer, and checks the installed version before stage 1. The two are installed separately through PATH, and an older renderer accepts --synthesize, degrades to a narrative-free report whenever the model call fails, and still exits 0 — so the audit used to report a clean pass over a report with no written analysis.
  • audit now checks the report trusty-review wrote, not just the child's exit status. A report carrying no written analysis fails the run and names the renderer upgrade that fixes it.

2.16.0 2026-08-11

Added

  • Agentic detection reads operator-supplied markers from a YAML file, so a target org's house footer is detectable without a code change or a tga release (#5414). The path comes from TGA_AI_MARKERS, defaulting to ~/.config/tga/ai-markers.yaml; each entry is a tool label, a mode (full_agentic / ide_assisted), a scope (trailer / message / email), and a regex pattern.
    • New public module collect::ai_marker_config (MarkerConfig, MarkerSpec, MarkerMode, MarkerScope, MarkerConfigError, marker_file_path). Deliberately a separate type rather than a field on Config, which would have forced a major bump.
    • The shipped markers are scanned to completion first, and operator markers are consulted only for a commit they left unmarked. An operator marker can therefore classify a commit the shipped set missed, and can do nothing else to one it already catches — including a full_agentic operator entry against an ide_assisted builtin verdict, which appending alone did not prevent.
    • A marker file that cannot be read, parsed, or compiled — or that declares more than 256 markers — is rejected whole, logged at warn!, and named in detection_disclosure(); the run continues on the builtin markers.
    • detection_disclosure() now states how many operator markers were active and where they came from, so an agentic_pct figure can be read against the run that produced it.

Fixed

  • The openhands trailer marker no longer classifies a human at the vendor as an agent. \bopenhands\b matched Co-authored-by: Simon Rosenberg <simon@openhands.dev>; it now keys on openhands@all-hands.dev, openhands-release-bot, or OpenHands Bot. Found by running the marker against a real All-Hands-AI/OpenHands clone rather than fixtures (#5414). Of the 51 commits the narrower pattern stops matching, 50 carry only a human contributor's @openhands.dev address; one (06cc1ef2) carries Co-authored-by: OH <openhands@example.com>, which is ambiguous and is left uncaught rather than anchored on a reserved placeholder domain.
  • The trusty-mpm and Claude Code footer markers now match the markdown-link form (Generated with [trusty-mpm](...)), which 14 commits in this repo's own history use and the #5249 patterns missed. Catch rate on trusty-tools' history rises from 91.03% to 91.35%.

2.15.0 2026-08-11

Fixed

  • Agentic-commit detection no longer undercounts. The seven hardcoded patterns all keyed on the literal "Claude", so a commit carrying only a house footer counted as human work — on trusty-tools' own 2434 commits the detector caught 47.7% against a 91.0% agentic share. Detection now runs a marker set with entries for the trusty-mpm footer, Devin, OpenHands and Aider alongside Claude Code, Copilot and Cursor, and also matches the author and committer email, which tga collect extracted and discarded. Measured on the same history the shipped set now catches 91.0% (#5249).
  • commits.ai_tool is now populated from body footers and bot identities as well as Co-Authored-By: trailers, from a single scan shared with agentic_mode so the two columns cannot disagree. Existing databases keep their stored values until tga backfill ai-detection-commits is run; that repair pass reads the stored author email and classifies exactly as the forward walk does.
  • tga collect and tga backfill ai-detection-commits log the active marker set and its limits once per run. Detection is marker-based, so a repository whose trailers were stripped, squashed, or rewritten reports a low share for a reason that is not "no AI assistance" — the log says so rather than leaving a reader to infer provenance from silence.
34 earlier releases
  • 2.14.0 2026-08-10
  • 2.13.0 2026-08-10
  • 2.12.0 2026-08-10
  • 2.10.0 2026-07-27
  • 2.9.4 2026-07-21
  • 2.9.2 2026-07-09
  • 2.9.0 2026-07-07
  • 2.8.1 2026-06-16
  • 2.7.1 2026-06-07
  • 2.6.1 2026-06-04
  • 2.2.1 2026-05-29
  • 2.2.0 2026-05-29
  • 2.1.1 2026-05-28
  • 2.1.0 2026-05-28
  • 2.0.0 2026-05-28
  • 1.5.4 2026-05-28
  • 1.5.3 2026-05-27
  • 1.5.2 2026-05-27
  • 1.0.12 2026-05-19
  • 1.0.11 2026-05-19
  • 1.0.10 2026-05-18
  • 1.0.9 2026-05-15
  • 1.0.8 2026-05-15
  • 1.0.7 2026-05-15
  • 1.0.6 2026-05-14
  • 1.0.5 2026-05-12
  • 1.0.4 2026-05-12
  • 1.0.3 2026-05-12
  • 1.0.2 2026-05-12
  • 1.0.1 2026-05-12
  • 1.0.0 2026-05-12
  • 0.3.0 2026-05-12
  • 0.2.0 2026-05-11
  • 2026-05-11

What each of these changed is in crates/trusty-git-analytics/CHANGELOG.md.

The rest of the workspace

Only the six flagship crates are published here. Every other crate keeps its changelog alongside its source — see crates/ in the repository. Nothing on this page is hand-written: it is generated at build time from the same files, and the repository remains the source of truth.

trusty-tools

One Cargo workspace for the trusty-* tooling ecosystem. MIT licensed.

© 2026 · MIT · github.com/bobmatnyc/trusty-tools