Skip to content

Release history

What’s new

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

trusty-memory

Memory palace storage

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

0.26.2 2026-09-23

Fixed

  • serve --stdio answers initialize and tools/list from its own process, so a daemon that is unreachable during the MCP handshake no longer costs a client session its memory tools. The client marks a server that fails its handshake dead and never re-spawns it, which is how ten seconds of daemon downtime ended memory for a whole session (#8351).
  • serve --stdio no longer exits when the daemon cannot be started. The failure is reported on stderr and the bridge keeps serving: tool calls answer with an error naming the socket while the daemon is down, and succeed on the next call once it returns, with no restart. This deliberately replaces the exit-on-unreachable-daemon behaviour of #1152, whose no-spawn half is unchanged — the bridge still never starts an unmanaged daemon (#8351).
  • serve --stdio re-resolves the daemon socket for every forwarded request instead of trusting the path resolved at startup, so a bridge that resolved a stale path heals on the next call (#8351).

0.26.1 2026-09-18

Added

  • memory.palaces_list takes counts, so a caller that wants NAMES no longer pays for the opens. Counting is the whole cost of this method: it opens every palace on disk, which on a 93-palace install took 8.5-11 s and repeatedly exceeded the console bridge's 30 s budget, leaving /tools/memory's Palaces view on "Loading palaces…" and its KG palace selector empty. counts: false answers from PalaceRegistry::peek instead — ids, names, created_at and the cached flag, with no cold opens. Those rows carry cached: false and zeroed counts, which #4637 defines as UNKNOWN rather than empty; memory.palace_get still opens one named palace for its real numbers (#6155)
    • counts defaults to true, and {} and null both still deserialise, so trusty-common's monitor and trusty-mpm's health TUI — which send {} and want measurements — are unchanged
    • rpc_palaces_list_without_counts_does_not_open_a_cold_palace proves it is a different code path rather than the same sweep with the numbers dropped: the fixture's palace cannot be opened at all, so under counts: false it must report no error and leave the registry empty, while the default still opens it and reports why the open failed
  • The daemon now detects what machine it is on. AppState gained a public machine: trusty_common::machine_tier::MachineBudget — the cgroup-clamped RAM reading, the tier band, and the two proportional soft limits, resolved through the same shared module trusty-search reads. Before this trusty-memory detected nothing: max_open_palaces stayed at DEFAULT_MAX_OPEN_PALACES (64) whether the host had 12 GB or 128 GB, and nothing in the log said which machine the daemon had sized itself for (#6820)
    • serve --foreground logs the tier and both limits at startup, and warns once when the host is below the documented 16 GB minimum
    • No cap is retuned here. #6820 is scoped to detect, expose, and document; #6823 is what makes TRUSTY_MEMORY_MAX_OPEN_PALACES RAM-proportional by reading this field
    • Adding a public field to a struct with public fields and no #[non_exhaustive] breaks a downstream struct literal, which is why this is a MINOR bump
  • console_metrics reports this daemon's own usage at schema version 5: ram_bytes (physical footprint), disk_bytes (the palace store's allocated size, matching du -s), data_root, an optional ram_heap_bytes / ram_file_backed_bytes / ram_compressed_bytes split where the OS supplies one, and a per-palace disk_bytes. Additive — a console built against schema 4 reads unchanged.
  • Store validated chat image assets and attachment references in trusty-memory, scoped to the owning palace and session.
  • MCP results fold to a 48 KiB serialized byte ceiling on the seven result-returning tools — memory_recall, memory_recall_deep, memory_recall_all, memory_list, kg_query, chat_session_recall, list_prompt_facts (#7493)
    • whole entries are dropped from the tail, never one cut mid-object, and the response carries truncated, returned, withheld, and a truncation_notice naming the knobs that fetch the rest
    • a recall's L0 identity and L1 essential drawers are never dropped; L2 hits go first
    • max_bytes overrides the ceiling up to 512 KiB, with the clamp reported in the response; full: true disables it. A non-integer max_bytes or a non-boolean full is rejected, not coerced
    • a response whose smallest foldable form still exceeds the ceiling ships anyway, under a notice that says so rather than one claiming it fits

Fixed

  • min_score rejects a present non-numeric value instead of failing open, and a floored recall fetches a wider candidate set (owner ruling 2026-09-14, review follow-up). A min_score that arrived as a string or bool used to resolve to "no floor" — indistinguishable from omitting it, and reported as dropped_below_floor: 0, which reads as "nothing was below the floor". A caller could not tell a floor that matched everything from one that never ran. It is now an invalid-argument error naming the tool, with no string coercion: "0.4" is rejected, not parsed. An absent key or an explicit null still means no floor
    • The retrieval lanes truncate to the count they are handed, so applying the floor to exactly top_k candidates dropped hits with nothing to backfill them — top_k: 10 with four below-floor hits returned six. With a floor set, the vector, deep and lexical lanes are now asked for top_k * 4 candidates (capped at 200) and the floor truncates that set to top_k. Fewer than top_k still comes back when the widened window holds fewer qualifying drawers; the descriptor and docstring say so rather than promising a full page
  • memory.health now sees a write stalled on a palace write lock. On 2026-09-13 a memory_remember held the trusty-tools palace lock and never finished. Later writers timed out after 60 s while health reported ok with 0 in flight, so both tm doctor and trusty-memory doctor read HEALTHY. memory_remember, memory_note and task_add now count in worker.in_flight from the start of their lock wait until they release the lock, so a stalled holder turns status to wedged once it passes the wedge threshold (#4001)
  • memory.health now sees a palace handle lock held by a dream cycle, forget, or import. On 2026-09-13 a dream cycle held the palace's inner write lock, and no holder of that lock is tracked. Writers queued behind it gave up below the wedge threshold, so health stayed ok. The daemon now sweeps each open palace's write and commit locks every quarter of the threshold (at most 30 s) and probes a held lock until it frees. A lock unavailable past the threshold sets status to wedged, and worker.stalled_lock names the palace, the lock, and its age. trusty-memory doctor fails and names the lock. If the sweep ticker stops or its state is poisoned, status reads degraded (#4001)
  • The wedge threshold is now twice the larger of open_queue_timeout and write_lock_timeout, so raising TRUSTY_WRITE_LOCK_TIMEOUT_SECS cannot make a writer queued inside its bound read as wedged (#4001)
  • trusty-memory doctor no longer ends a run green with exit 0 when a check is undetermined. A health probe that times out against a daemon that accepts the connection but never answers now makes the run exit 1. The rule covers every check, not only the health probe: a Tier S check against a daemon predating #4890, and an MCP-registration check on a machine where $HOME will not resolve, now exit 1 on an otherwise clean run (#4001)
  • worker.stall_tracking_ok in the /health payload reports whether the palace-lock stall detector is running at all. A stopped sweep ticker or a poisoned stamp table used to reach doctor only as status: degraded, which read as a warning and left the run green — the detector's own failure hiding the wedge it was added to catch. Doctor now reports it as UNKNOWN and exits 1. An absent field means a daemon with no stall detector at all — the 2026-09-13 build, which reports worker.wedged and so was never caught by the pre-#4001 arm — and is equally UNKNOWN (#4001)
  • worker.wedged_reason names which observation crossed the threshold, lock or pool. worker.stalled_lock is reported below the threshold too, so doctor used to describe any worker-pool wedge as a wedged palace, naming the wrong palace and a harmless 3 s age. Doctor's wedged-palace message now reads "for at least Ns" and prints the palace and lock unquoted (#4001)
  • A stall stamp is keyed to the handle mutex it was taken against, so a palace that is idle-evicted and reopened no longer inherits the superseded handle's stall — a probe queued on a lock nobody can reach again used to keep a healthy palace reading as stalled (#4001)
  • The two full-estate palace sweeps no longer stall the async executor. MemoryService::list_palaces_with_counts (memory.palaces_list) and palace_embed_sweep both open every palace on disk, and both ran those opens inline on a tokio worker thread. Opening a palace is cold disk I/O, so on a many-palace install the sweep parked the executor for its whole duration and every other request the daemon was serving stopped until the last palace was open. Each per-palace open now runs on tokio::task::spawn_blocking, which also gives the executor a yield point between palaces rather than one uninterruptible block (#6836)
    • Per-palace error handling is unchanged: a palace that will not open still gets its own row carrying the failure — an Err entry from list_palaces_with_counts, an error row plus the same embed sweep could not open palace warning from the sweep. A spawn_blocking join failure is reported the same way, so a palace can still never vanish silently
    • list_palaces_with_counts_opens_palaces_off_the_executor and embed_sweep_opens_palaces_off_the_executor pin it: on a single-threaded runtime a concurrent task samples the registry's open-handle count and must observe the sweep part-way through, which is impossible while the opens run on the executor thread
  • A serve --foreground daemon spawned by a test no longer survives a SIGKILL of the test process, whether it is the test's direct child or a detached daemon a stdio bridge auto-started on its behalf. The daemon arms trusty_common::parent_death when the spawner stamped TRUSTY_EXIT_WITH_PARENT and shuts down within a second of that process disappearing; 102 orphaned debug-build daemons holding 12.6 GB had accumulated because DaemonGuard's Drop never runs when the parent is killed outright. Launchd-supervised and hand-run daemons are untouched — nothing arms unless the env var is set, and arming announces itself on stderr (#7085).
  • Startup work now holds at most four palaces open at once across every job that opens one (#7106, epic #6802). Hydration and both BM25 sweeps each walk the whole estate, and each open hydrates a drawer table, an HNSW graph and a KG adjacency (~90 MB) plus three redb page caches; three jobs each bounding themselves separately multiply, so the bound has to be a property of the process. All three now draw on one startup_budget::StartupOpenGate carried on AppState, sized by TRUSTY_MEMORY_STARTUP_OPEN_LIMIT (default 4), and the gate reports its high-water mark so the bound is observable rather than merely intended. An unparsable override keeps the bounded default and logs a warning naming it. The gate is a ceiling, never added concurrency: hydration still opens one palace at a time, because hydrating four at a time raised phys_footprint_peak over a 94-palace copy of the reporting host's store from 336 MB to 409 MB.
  • Both BM25 sweeps hand each palace back to the LRU when they are done with it, rather than leaving every palace they touched resident for the life of the process (#7106). This matters most for the periodic repair sweep: its queue is fed by the write path's drop-on-full arm, so it runs under sustained writes rather than only at boot, and every palace it opened used to stay hydrated until a restart. It releases only what it brought in itself: a palace already resident is left alone, a palace whose persisted last_used stamp is inside fifteen minutes stays warm per the residency ruling in #7087, and a handle anything still references is never dropped.
  • community_count no longer runs on an async worker. palace_info enrichment, kg_graph and kg_graph_seed read the graph counts under spawn_blocking, so the first Louvain partition after a write cannot park the executor for its duration (#7106).
  • spawn_dream_scheduler staggers each palace's first dream tick instead of starting every loop's clock at the same instant (#7106). Palace k of n now waits idle_secs * k / n past its first interval, so the 61 loops on the reference host no longer wake in the same second — 61 dream_stats.json writes landed inside one 31-second window, and a 43 GB transient came with them. The startup log line records each loop's offset and the process-wide cycle cap.
  • A recall-all no longer holds every palace on disk open at once (#7125, epic #6802). MemoryService::recall_all, the memory_recall_all MCP tool and the chat dispatcher's execute_recall_all all walked the whole estate into one Vec<Arc<PalaceHandle>> and held it for the duration of the query, so peak residency scaled with the palace count — on a 94-palace estate roughly 13x the on-disk bytes — and when that Vec dropped the 64-slot LRU was left holding its most recent victims, a multi-GB floor until the idle sweep ran. The 64-slot cap bounded the cache, never the peak and never the residue. All three now share one bounded walk that opens eight palaces at a time, searches them, and hands back every handle the query itself brought in, so the daemon's open-handle count after a recall-all matches its count before one. Every palace is still opened and still searched — answering from cache-resident palaces only would silently drop most of the corpus (#4637).
  • A recall-all leaves the operator's working set alone (#7125). Palaces the registry already held when the query arrived are never released, and a handle another task still references is never dropped, so the fan-out cannot trade a residency leak for a cold reopen on the next request. Release runs before a failing search's error propagates, so an erroring batch cannot leak either.
  • Intra-doc links in startup_budget (StartupOpenGate, release_after_sweep, DEFAULT_KEEP_RECENT_SECS) now resolve under cargo doc. The module's own inner //! doc referenced its items bare, which resolves against the declaring module's scope rather than the module's own — fixed with [name]: crate::path::to::name link-reference definitions (#7157).
  • The three chat_asset_* tools now appear in the scope map, the tool count and the generated README table. #7370 registered chat_asset_capabilities, chat_asset_put and chat_asset_get in tools::definitions but changed nothing that enumerates the tool surface, so rpc.discover emitted them with an empty x-scopes array — an orchestrator enforcing least privilege had no rule to apply — and seven tests failed on main from the day it merged. scopes_for_tool now classifies capability discovery and an owned asset read as memory.read and the asset store as memory.write; the count assertions read 52; the roster contract in tools::tests lists all three (#7654)
    • The README's generated mcp-tools region was refreshed from the code, which also picks up the attachments? argument #7370 added to chat_session_add_turn and chat_turn_append
    • read_write_classification pins the split so a later change cannot quietly make the asset write a read
  • The messaging slug tests now take the crate-wide commands::env_test_lock alongside #[serial], so TRUSTY_MEMORY_PALACE is guarded by one lock instead of two disjoint ones and cwd_palace_slug_at_env_override_wins can no longer lose the override to a concurrent remove_var (#7995).

Changed

  • memory_recall hides creator:* attribution tags and takes an optional min_score relevance floor (owner ruling 2026-09-14, token savings). Every drawer this daemon writes carries about four creator:* tags — client, version, source, cwd — and recall returned all of them on every hit, so most of a response's tags bytes were provenance no caller reads. They are now omitted by default via the same attribution::is_creator_tag predicate the prompt-context renderer already filters with; include_creator_tags: true returns them. Nothing is deleted: the tags stay in redb and stay queryable through memory_list's tag filter
    • min_score drops query-scored hits (L2, L3, and the lexical lane) below the floor BEFORE top_k is applied, so a filtered-out hit never consumes a slot. L0/L1 identity and essential drawers are never filtered — L0 scores a flat 1.0 and L1 scores by importance, neither comparable to a similarity. The default is no floor, so existing callers see no change; the descriptor recommends 0.4 for PM-context recall, where the L2 lane returns loosely related drawers in the 0.38-0.46 band
    • The response gains dropped_below_floor, always present, so a short result list is no longer ambiguous between "the corpus had no more" and "the floor removed them"
    • memory_recall_deep takes both arguments on the same terms (ADR-0027 D4.1: two spellings of one option is how these two drift). memory_recall_all takes include_creator_tags but no floor — one threshold across heterogeneous per-palace corpora would mean a different thing in each
    • serialize_recall moved from tools::bm25 to the new tools::recall_projection; the lexical lane was never its subject
  • serve --stdio forwards through the shared trusty_mcp::daemon_bridge_json_rpc. commands::serve_stdio_bridge kept its own framed client, jsonrpc normaliser, streaming-method refusal, notification check and reply mapper; all five were a second copy of what trusty-analyze also had, and #6286 showed how quietly they drift — a streaming method added to the daemon and not to the bridge's list left an MCP client waiting for a frame that was never coming. The module now builds a UdsBridgeConfig and supplies only what is trusty-memory's: the --palace default and the DOC-53 §4.3 caller-identity stamp, both through with_request_rewriter (#6316)
    • Behaviour is unchanged for a client. The daemon readiness guard still runs here before the loop starts — run_stdio deliberately probes and starts nothing — so serve --stdio still takes the same StartLock on the same lock file trusty-memory start uses (#5267)
    • The transport-failure message now reads the trusty-memory daemon at <socket> could not be reached: <cause> rather than trusty-memory daemon unreachable: …. It still names the socket and still carries the failing request's own id, which is what keeps an unreachable daemon from reading as a hang (#6309)
  • A palace-scoped READ tool called with no palace now returns an index instead of an error. memory_recall, memory_recall_deep, memory_list, room_list, wing_list, kg_query, kg_gaps, kg_list_subjects, task_list, palace_info, chat_session_get, chat_session_list and chat_session_recall answer a call with no palace argument and no --palace default with a successful, structured index — every palace on the host, ordered most-recently-used first, with drawer / room / wing counts, a row per room, and a hint naming the retry. Before this each returned Err("<tool>: missing 'palace' (no --palace default configured)"), which told a caller who did not know a palace nothing about which ones exist (owner ruling 2026-08-27, #6318)
    • Writes are unchanged and still refuse. memory_remember, memory_note, memory_forget, room_create, kg_assert, task_add, chat_session_create and every other mutating tool keep the error: an index is a truthful answer to "what can I read", never a target for a write
    • A palace that was NAMED but does not exist still errors, and a session with a resolvable --palace default is unaffected — the index is the last resort, not a new precedence step
    • The affected tools' inputSchema no longer lists palace in required, so a compliant MCP client can actually omit it; their descriptions document the success path
    • The index describes at most 24 palaces in full (each full row opens that palace's redb file); every palace past that is still listed, marked detail: "omitted"
  • palace_verify_embedded called with no palace now returns the same palace index the other read tools return. It was the one palace-scoped READ tool left out of the first wave, because embed_audit.rs was being changed concurrently; it now calls the same resolve_palace_or_index helper and answers a call with no palace argument and no --palace default with a successful, structured index — every palace on the host, ordered most-recently-used first, with drawer / room / wing counts, a row per room, and a hint naming the retry. Before this it returned Err("palace_verify_embedded: missing 'palace' (no --palace default configured)") (owner ruling 2026-08-27, #6318)
    • The index is decided before drawer_ids is validated, so a caller who does not yet know which palace to name is told which palaces exist rather than that its ids are missing
    • palace is gone from the tool's inputSchema required list on both schema branches, so a compliant MCP client can omit it; drawer_ids stays required
    • Behaviour with an explicit or a defaulted palace is unchanged, and palace_embed_sweep — which never took a palace — is untouched. A verified: true gate cannot be passed by an index, so tm memory import --refresh still fails closed
  • memory.palaces_list keeps its counts: true default, and the doc now says why. It is a published UDS/MCP method, so an external caller sending {} asked for the #6286 contract and still gets it. What changed in #7125 is who sends {}: the two periodic pollers named in that doc — trusty-common's monitor client and trusty-mpm's health TUI — send counts: false now, so a poll no longer opens every palace on disk to count it (#7125)
    • palaces_list_poll_residency.rs pins both halves against a real daemon: monitor_client_poll_leaves_closed_palaces_closed drives the shared monitor client and asserts the registry's open-handle count is unchanged, and palaces_list_without_counts_opens_nothing_on_the_daemon names the daemon side directly so a future regression is attributable to one side or the other

Removed

  • The embedded admin UI is gone from this crate: src/ui_assets.rs (the rust_embed bundle and its asset lookup), build.rs and its Vite step, the rust-embed and mime_guess dependencies, and the committed ui/dist/ bundle. The Svelte source moved to crates/trusty-console/ui-memory, and the console serves the dashboard at /tools/memory/ over the daemon's existing memory.* socket methods. Nothing had served these assets since #6286 deleted this crate's HTTP listener — the removal takes the public ui_assets::WebAssets and ui_assets::asset items with it, which is a breaking change and owes a MINOR bump at release under Cargo's 0.x rule (#6155).

0.25.6 2026-09-02

Added

  • trusty-memory palace stats <name> (#6652) — read-only report of a palace's kg.redb: file size, per-table row counts and byte usage, the active-vs-history triple split, superseded-drawer count, and a reclaimable estimate. Safe against a live palace with the daemon running.
  • trusty-memory palace compact <name> [--dry-run] — prune stale history rows and rewrite kg.redb to reclaim disk.
  • palace_dream accepts compact: true (and dry_run: true), returning a compaction object with before/after byte counts and pruned-row counts.
  • trusty-memory doctor reports the largest kg.redb on disk: warns at 100 MB, fails at 500 MB.
  • [dream] config keys compact, prune_history_after_days, compact_min_bytes, compact_keep_backup in ~/.trusty-memory/config.toml.

Fixed

  • transport::uds::serve_with_shutdown awaits the BM25 exit flush under trusty_common::shutdown::CLEANUP_RESERVE (#6601 review). The reserve is the time serve_until's drain holds back so the work after it can run, and bm25_lane::shutdown claimed there was "no window in which a SIGKILL can land mid-flush" — but flush_all takes the residency mutex and flushes every resident palace with no deadline. A slow flush spent the whole reserve, the socket unlink after it never ran, and the SIGKILL left behind the stale socket file bind_singleton_hardened exists to work around.
  • An abandoned flush warns, naming the budget, and costs nothing a SIGKILL would not have: BM25Index::flush renames a temp file into place, so every palace keeps the snapshot its last coalescing tick published.
  • The stdio bridge no longer spawns an unsupervised daemon onto the production socket while launchd is restarting the unit. ensure_daemon_running's single-flight flock (#5267/#6286) coordinates bridges with each other and cannot see launchd, so a bridge that probed during a bootout/bootstrap window read the transiently unserved socket as "nothing is running" and started its own daemon — without the plist's FASTEMBED_CACHE_DIR / FASTEMBED_CACHE_PATH — on the path launchd's own instance wanted. launchd's process then found the socket held and exited 0 ("another instance is already running"), reporting success while a misconfigured orphan owned the socket (#6619). The guard now asks whether a launchd unit owns the path and waits for it, bounded by the termination grace, erroring with the unit's label instead of spawning.
  • A daemon startup refuses the production socket outright when a launchd unit is registered for it and launchd positively reports it does not run this process. This is the callee-side half, and it holds for a daemon started by anything — by hand, by a script, by an older bridge. It refuses only on a positive NotSupervised: Unknown means launchd could not be asked, and refusing on that would take the daemon down on every host with an unreadable launchctl.
  • Both guards apply only to the canonical production socket. A daemon under a TRUSTY_DATA_DIR_OVERRIDE sandbox, or on a host that never installed the service, keeps the on-demand spawn unchanged.

Changed

  • palace_compact's description now states outright that it is vector-index-only and does not touch kg.redb (#6652).
13 earlier releases
  • 0.25.5 2026-08-31
  • 0.23.1 2026-08-15
  • 0.22.0 2026-07-27
  • 0.21.2 2026-07-26
  • 0.21.1 2026-07-24
  • 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

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

1.7.1 2026-09-23

Added

  • tm doctor has a launchd_process_type row. It fails when the com.trusty.mpm or com.trusty.mpm.supervisor LaunchAgent declares ProcessType Background, which clamps tmux and every session it hosts to background QoS, and warns on any other value short of Interactive. The message names the plutil and launchctl commands for the plist on disk; the deploy supervisor template now declares Interactive (Refs #8415)
  • tm doctor has a tmux_priority row. It fails when the running tmux server's Darwin priority is below 20 and warns from 20 to 30 (launchd Standard throttling), naming the server PID, the observed priority and the remedy: fix the plist, then restart the tmux server. It runs on macOS only and reports not applicable elsewhere. A server keeps its class until it exits, so a plist fix alone does not lift it. It reports Unknown, never Ok, when tmux or ps cannot be read (Refs #8415)
  • TRUSTY_MPM_LAUNCH_AGENTS_DIR points the launchd_process_type row at another LaunchAgents directory; unset, it reads ~/Library/LaunchAgents (Refs #8415)

Fixed

  • --user <login> is now accepted everywhere --account <login> is, including after the repository in the bare form (tm <url> --user bob-duetto), which clap previously swallowed into the external subcommand's argv and refused as an extra argument (#5850).
  • The gh pr list calls in the worktree-reclaim survey (which tm session prune-worktrees runs) and in the ADR-0057 worktree-removal guard now resolve the project's pinned gh identity from the ProjectRegistry — what tm --user/tm projects register --gh-account actually write — before the static trusty-tools config, so a private repository only the pinned account can see is no longer probed as the machine's global account. Other daemon gh calls, such as the supervisor's PR-cleanup sweep, still use the ambient identity (#5850).
  • When several registry records name one repository, the pinned record is used even if an unpinned one sorts first, for both the worktree-reclaim lookup and the session-spawn gh environment. Records that pin different identities block the reclaim lookup and leave a spawned session unpinned, with a warning, instead of picking one by position. A registry document without a projects key now blocks too, and a record that sets only github.host no longer counts as a pin (#5850).
  • Two registry records that pin the same login now agree even when only one carries a config_dir, or when the login differs only in case, so a project registered once from config and again with tm <url> --user <login> uses the scoped config dir instead of being refused or spawned under the global account. Records conflict only when their logins differ or they set different config_dir or token_env values (#5850).
  • A registry record that pins a config_dir without naming a login (what seed_from_config and tm projects register --gh-config-dir write) no longer conflicts with a record naming a login on the same dir. The chosen pin carries both the dir and that login, instead of the reclaim lookup refusing with "different gh accounts" and the session spawning under the global account (#5850).
  • A registry a daemon gh spawn cannot interrogate — unreadable, unparsable, or pinning an account with no usable credential, including a github.token_env naming an unset variable — now blocks with a lookup failure naming that account instead of silently falling back to the global identity. Only "no pin recorded" falls through (#5850, #5851).
  • A trailing account flag with a blank value (tm <url> --user=, --user "") is now refused with "needs a gh login" instead of being read as no account and cloning as the machine's global identity (#5850).
  • The same blank value given before the repository (tm --user= <url>, tm --account " " <url>) is now refused at parse time with "needs a gh login" instead of falling back to the machine's global gh account (#5850).
  • A clean worktree whose work already landed can be reclaimed even when GitHub has no MERGED pull request for its branch (#7889) — the donor-branch shape, where the work landed through a sibling -r2 branch's squash and no pull request will ever carry the parked branch's own name
    • two routes admit: landed-content, when merging HEAD into the freshly fetched landing base would change no file (gitlink bumps included, whatever the submodule-ignore config says), and merged-pr-ancestry, when HEAD is the head commit of a MERGED pull request or one of its ancestors
    • the ADR-0057 git worktree remove guard and tm session prune-worktrees --merged-prs share one implementation of the check, but the guard does not ask it once its own or a sibling's pull request has matched, or for a detached HEAD, so it is stricter than the sweep there; where they disagree, one of them refuses
    • every git worktree remove guard grant, including the merged-PR one, now refuses a tree holding a nested repository with unsaved work or a high-value gitignored file, naming the first nested path, because --force deletes ignored content; a scan that cannot run refuses too
    • fail-closed throughout: a failed or expired origin refresh, an unresolvable landing base, a git merge-tree error or conflict, a residual path, a commit search that did not answer, an ancestry check that could not run, an open pull request, a dirty tree and a live owner all still refuse; a pull request whose head is behind HEAD is never a match, and ancestry against a squash commit is never used
    • a refusal names each route that failed and the first path the merge would still change
    • the git worktree remove guard now finishes its daemon owner query and every re-check within 3.5 s of the tm process starting and denies when time runs out, naming the check still running; before, a guard slower than the hook's 5 s limit was killed and returned no decision, which let the removal through. The deny is printed and flushed before its audit, the audit must end 4.5 s after process start, and the admission reuses the guard's own origin fetch instead of fetching twice
    • on the sweep, a donor branch's commits that reach no origin ref no longer refuse on their own, whether gate 5 found no pull request or found the sibling's through its commit search; an uncommitted file, a dirty nested repository, or a commit on session/<leaf> that HEAD cannot reach still refuses (naming the branch and the first such commit), a grant is re-checked for new dirt and a moved HEAD before it is returned, and the pre-delete re-check asks the admission again
  • The tm-epic manual-procedure.md skill reference no longer regenerates the tracker's phases: block with awk -v tbl="$(cat ...)", which threw newline in string and silently produced empty stdout on stock macOS/BSD awk, wiping the epic's body once gh issue edit --body-file accepted the empty file. The recipe now uses a perl -e whole-file substitution, adds an explicit non-empty-and-both-markers guard before pushing, and saves the pre-edit body to body.orig first (#8376).
  • Config tmux.alternate_screen now decides Claude Code's renderer on these launch paths: true starts claude with CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=0, false with =1. The paths are daemon spawn, restart and attach; the tm launch, tm connect, tm session start (in place) and client /connect launch lines; the control-plane tmux backend; tm run; and the bare-tm in-place relaunch. On these paths the value no longer comes from whatever environment the tmux server or the launching shell inherited. A settings-file env entry still overrides it. The GUI New Session path and the in-session restart still type relaunch_command and keep the inherited value. A config file that cannot be read never blocks a launch: tm logs a warning naming the file and the error, and falls back to the same value the tmux alternate-screen option falls back to (#8405).
  • tm sessions resume X with X gone no longer reaches a live session whose name starts with X. The has-session probe, the resume runtime probe (list-panes -s), the daemon's kill-session and display-message pane lookups, attach-session, switch-client and list-clients now all use exact tmux targets. Before, tm sessions resume tm-cto killed tm-cto-reports (#8443).
  • The resume runtime probe now reads a can't find session or no server running reply as "no runtime is live" instead of failing open, because an exact target makes that reply certain (#8443).
  • POST /claude-config/restart now answers 400 for an empty tmux_session instead of typing C-c and claude into whichever session tmux resolved =: to, and every tmux spawn refuses an empty session name before it runs (#8443).
  • tm only acts on $TMUX_PANE when it is an immutable %N pane id (#8443).
  • The tm-session-resume skill now realigns to the recorded window by its immutable @N id instead of a bare session:index target (#8443).
  • The attach command the daemon returns (attach_cmd) and the hints tm prints are now tmux attach-session -t '=<name>', quoted so zsh does not read the leading = as a command-path expansion (#8443).
  • tm doctor's launchd_process_type row description no longer restates the com.trusty.mpm / com.trusty.mpm.supervisor launchd labels as literals; it now points at trusty_common::launchd_labels::MPM and MPM_SUPERVISOR, the registry that owns them (#4919, #8415).
  • The ADR-0057 git worktree remove re-check bounds its daemon owner query by the removal deadline (#8082). A late-starting guard facing a daemon that accepts and never answers now denies, naming sole-owner, inside the budget instead of after its own 2.5 s client timeouts. A regression test also pins the removal deadlines inside the guard hook's registered timeout.
  • The #7266 secret-read guard no longer refuses terraform apply or terraform plan that names a state file only as a -state, -state-out or -backup value (#8249). A scratchpad copy of a Terraform root can now apply against the main checkout's state without copying module files into the main checkout. Every such value must be a *.tfstate or *.tfstate.backup path (-backup=- too), so a state flag cannot launder another secret or copy state out to a readable file. terraform show, terraform state, a secret named outside a state flag (a -var-file), a state file read by any other program, and a state flag built from a command substitution are still refused.
  • The ADR-0037 main-checkout guard no longer refuses git checkout <sha> -- <paths> and the other whole-tree git verbs inside a disposable clone under the session scratchpad (#8339). It applies the same canonicalized proof #7778 gave the write boundary, so a symlink from the scratchpad into a real checkout, and a -C $VAR the guard cannot expand, are still refused.
  • The ADR-0057 git worktree remove merged-PR check takes the repository's identity from the origin remote URL (#8403). A branch pushed to a fork remote searches origin first and the fork second, instead of the fork (<account>/<repo>) alone, so a cross-repository PR merged in origin is found. An unreadable origin URL refuses even when the push remote is valid, and a repository that cannot be asked still denies unless another repository reported the merge.
  • The ADR-0057 git worktree remove scope check recognises the harness's <repo>-worktrees/<tree> sibling layout (#8413). A version-control removal of a linked worktree there now reaches the clean/pushed/merged/owner re-checks instead of being refused at worktree-scope. A main checkout that merely sits under a *-worktrees directory is still refused.

Changed

  • model_inject::build_claude_command, build_claude_command_with, build_inplace_session_command, build_client_session_command, build_agent_command, standalone::run::build_launch_command and alt_screen::managed_shell_assignments keep their 1.7.0 signatures and now read the renderer from the operator's config. They are superseded by the *_configured variants and configured_shell_assignments, which take the renderer as an argument (#8405).

Documentation

  • The tm-tool-usage-guide bundled skill and the resident PM's Trusty Tool Priority section now name the Atlassian twg CLI as the preferred way to read or write Jira and other Atlassian content, ahead of MCP connectors or WebFetch (owner ruling 2026-09-23).
  • The launchd doctor row's module doc now links check_launchd_process_type_in instead of the test-only check_launchd_process_type, fixing the broken intra-doc link the Rustdoc intra-doc links CI job reported after #8415 renamed the production function (#8415)
  • The resident PM instructions (sections/core.md, the fixed-tier core section) now name a project-root TICKETING.md as the ticketing agent's override of the tm-ticketing defaults, so a PM no longer treats a ticketing-generated TICKETING.md as an unrequested file (#8437)
  • The tm-workflow skill states the sanctioned pattern for a brief that needs a ticket-named branch (#8337): the isolated agent creates that branch inside its assigned worktree instead of adding a second worktree, whose every later operation the Claude Code isolation pin refuses. When the directory itself must carry the ticket name, the PM creates it before dispatch.

1.7.0 2026-09-23

This release lowers the tmux scrollback limit from 100,000 lines to 10,000. The lower limit removes the keystroke lag in tm sessions (#8404).

Breaking

  • This 1.x release breaks the trusty-mpm library API against 1.6.3 and ships under an owner-approved override of the semver gate (#8372). The tm binary's behaviour is unaffected. The list below is every break scripts/check_semver.sh --crate trusty-mpm reports, grouped by the issue that introduced it.
  • #8233: ManagedError gained the ResumeInFlight and AutoResumeRecorded variants, and ResumeManagedError gained AlreadyResuming.
  • #8233: runtime::build_adapter takes a fourth parameter, framework_root: &Path, the framework root the Claude Code adapter writes its managed config under.
  • #8233: runtime::ClaudeCodeAdapter::new takes a third parameter, framework_root: &Path, for the same reason.
  • #8261: core::builders::BuildersConfig gained the public fields load_factor, free_memory_floor_mb and slot_pool_root, so a struct literal outside trusty-mpm no longer compiles.
  • #8261: core::builders::BuildersConfig no longer derives Eq, because load_factor is an f64. It still derives PartialEq.
  • #8261: daemon::builder_slot_routes::BuilderSlotResponse gained the public fields ceiling, capacity_reason, fail_closed_surface, slot_path, slot_seed, slot_notice and slot_refused, so a struct literal outside trusty-mpm no longer compiles.
  • #8261: core::agent::Delegation is now #[non_exhaustive], so it can no longer be built with a struct literal outside trusty-mpm.
  • #8361: core::instruction_package::SectionId gained the AutonomousExecution variant, inserted after Core. Every later variant (Memory through FrameworkGuaranteedConventions) moved one position, which changes its implicit discriminant and its derived PartialOrd/Ord order.
  • #8372: ManagedError and ResumeManagedError are now #[non_exhaustive]. A match on them outside trusty-mpm needs a wildcard arm.
  • #8372: runtime::launch_spec::LaunchSpecError, core::builder_slot_pool::SlotPoolError, core::builders::BuildersConfigError and core::memory_verbs::MemoryVerbError are new since 1.6.3 and ship #[non_exhaustive] from their first release. This is not a break; it means a later variant is not one either.

Added

  • A pane COMMAND line longer than 960 bytes is now refused rather than typed, so a launch builder that still composes an unbounded line fails loudly instead of being silently truncated by the tty's canonical-mode input buffer. The guard is on the command-line senders only; task injection into Claude Code's raw-mode TUI, where the limit does not apply, is untouched (#8233).
  • An admitted builder is now granted a private CARGO_TARGET_DIR from the slot pool and told to prefix it inline on every cargo command, so concurrent builders stop contending on one cargo build-directory lock. The daemon resolves the pool root and repo identity, seeds the slot once by copy-on-write clone from the repo's shared target directory, and carries the path back in the builder-slot answer as slot_path/slot_seed; and every admitted builder is told it, whichever exit its dispatch takes — merged into the worktree rewrite the grant arms already print, or carried on its own additionalContext object at the plain-dispatch exit, which prints nothing otherwise. A PreToolUse hook's stdout carries exactly one object either way. The claim itself only RESERVES: it stats the slot's seed marker and creates the slot's parent, and the clone runs on a background task after the answer has been sent, because cloning a target directory measured at 207 GB cannot finish inside the dispatch guard's 2-second claim budget. A slot whose seed has not run yet is admitted without a private directory and told so; a slot the pool cannot reserve at all is refused, naming the path and the errno, so a failed reservation neither becomes an unthrottled builder pointed at the shared directory nor reads as a full machine. At most one seed per slot index is ever in flight: the reservation registers the index under the claim mutex and the background task releases it on either exit, so a dispatch that ends mid-clone cannot hand its index to a second seed that would delete the first one's staging tree and leave a directory assembled from two interleaved runs marked warm; the release runs from a drop guard, so a panicking seed leaves the index seedable rather than stranded. Across a daemon restart, where an in-memory registry cannot reach a cp child that outlived its parent, each seed run stages into its own .slot-N.seeding.<pid>.<nanos> directory, sweeps only staging trees whose process is confirmed dead, refuses to replace a slot another run has already marked, and writes the marker with create_new so exactly one run ever claims to have seeded a slot (#8261).
  • tm memory recall|remember|note reach the trusty-memory palace over the daemon's Unix socket, so a session whose mcp__trusty-memory__* connection is dead keeps memory access. They honour TRUSTY_MEMORY_PALACE and the committed pin with a --palace override, mirror the MCP tools' arguments (--top-k, --room, --wing, --min-score, --tag), print a stable envelope under --json, and exit non-zero naming the socket when nothing answers.
  • tm-ticketing (2.1.0) states its taxonomy and behaviour as explicit defaults, names the resolution order a project-root TICKETING.md sits at the top of, and carries the canonical skeleton the ticketing agent copies when generating one (#8376)
    • the component label is now defined by the project's own stack unit — a Cargo crate, an npm/pnpm workspace package, a Python distribution, a Go module — with Cargo as one example and never the definition; the no-component-label: prefix is unchanged because tm issue audit parses it
    • epic defaults follow the committed tracker + phase-issue pattern at docs/reference/tracker-phases-pattern.md: [EPIC <epic#>] <outcome> trackers (created [EPIC], renamed once the number is known), [EPIC_<epic#> PHASE_<n>] phase issues as native sub-issues, a wholesale-regenerated phases block and an amended deferred block, and four update triggers. This supersedes the [EPIC N · Phase M] naming proposed earlier the same day
    • research output belongs in a committed doc under research_docs_path (default docs/research/<effort>/) that the tracker links to; no issue body carries findings
    • a per-phase follow-up budget with a severity floor and a due-by window, and a staleness policy whose human decisions are requested per epic as a digest
    • Refs #N and trusty-mpm as a component label stay fixed at every tier
    • tm-issues-prune points at the same policy for the thresholds its Prune phase applies
  • New bundled tm-epic skill (/tm-epic): authoring a GitHub epic as one tracker issue plus native phase sub-issues — the gate test, the four rules, acceptance-criteria writing, the four tracker-update triggers, and four reference files carrying the tracker template with its phases/deferred/followups marker blocks, the five-heading phase template, the manual gh procedure verified against gh 2.96, and the anti-pattern table. The /tm-epic create|sync|defer|close verbs are documented as the intended shape and are not yet implemented. tm-ticketing gains a bounded-exception row for the phase body and an "Epics and phases" pointer; its two gh-version claims now match the installed 2.96 (#8376).

Fixed

  • The two resume_managed tests that drive the real launch path now plant a stub claude on PATH, so they exercise the pane-typing and post-send arms on a machine with no Claude Code install instead of stopping at the adapter's binary lookup and failing on every CI runner (#7862).
  • The managed launch no longer resolves its framework root, managed CLAUDE_CONFIG_DIR, session-MCP file or launch-spec directory from the process home. ClaudeCodeAdapter takes the root DaemonState already holds, so a test driving the real spawn or resume route can no longer redeploy the bundled agent and skill catalog into the operator's live ~/.trusty-mpm/framework/. (#8233)
  • tm meta run names that root at the command entry point and hands it to the launch ritual, so the launch module resolves no framework layout of its own. (#8233, #4203)
  • Confirm the pane's shell is executing what it is typed before a managed launch is sent, and recognise a shell continuation prompt (quote>, cursh> and kin) as its own state, flushed with an interrupt rather than a closing quote or a bare Enter (#8233).
  • Key the launch-started sentinel on the launch instead of the session, so a marker left by an earlier launch can no longer satisfy a later one (#8233).
  • Poll for the runtime before consulting the sentinel, so a slow-starting session is never interrupted on a 2 s budget; report "not delivered" apart from "ran and failed", at ERROR, so the failure reaches errors.jsonl (#8233).
  • Clear a record's accumulated [error: ...] notes once its runtime is verified running (#8233).
  • Relaunch the runtime on the automatic resume paths through the same adapter and the same verification an interactive resume uses; a record becomes Active only behind a runtime that was seen (#8233).
  • Resolve a fresh spawn's own pane id, so the pre-launch interrupt no longer lands in the operator's active pane (#8233).
  • Return an error from resume_managed after it marks a record errored, instead of reporting success (#8233).
  • Route the control backend's tmux launch through the pane-command length guard (#8233).
  • Sweep abandoned launch specs on daemon boot and on every supervisor sweep, not only when a later launch happens (#8233).
  • Abandoned launch specs — left by a killed pane, a daemon restart or a launch the shell never ran — are reaped after ten minutes, so the GH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN they carry no longer sit on disk indefinitely (#8233).
  • The tcode adapter routes its run-task line through the pane-command length guard; a task long enough to overflow the tty's canonical buffer is now refused instead of silently truncated into a different task (#8233).
  • A bare-tm in-place relaunch resolves and applies the project's pinned gh identity itself, restoring the GH_TOKEN/GH_CONFIG_DIR binding the pane shell stopped carrying once the launch moved into a spec file (#6668, #8233).
  • The launch-on-main spawn path verifies its launch like the other two spawn sites, so a shim failure errors the record in seconds instead of leaving it Active until the reaper (#8233).
  • Managed sessions no longer fail to start from a truncated launch line. The daemon's spawn, resume and attach paths now type a fixed-shape invocation into the pane — tm internal-spawn-disclaimed --launch-spec <file> — and the claude cwd, argv and environment travel in that file instead of in the typed text. The old line grew with the workspace path, TMPDIR, every env assignment and every flag, and at 1054 bytes it exceeded the tty's canonical-mode MAX_CANON limit (1024 on macOS), so the kernel dropped the tail and the session died mid-command (#8233).
  • GH_TOKEN, GH_USER and CLAUDE_CODE_OAUTH_TOKEN now ride in the launch spec — a file created with mode 0600 inside a mode-0700 directory and deleted as it is read — instead of a second sourced temp file. No secret appears in typed pane text.
  • A managed spawn whose runtime never comes up is marked errored within ~5 s instead of staying Active until the ~60 s reaper noticed. The post-send launch check that #6766 added for resume now runs on the spawn path too.
  • Hold the in-flight resume claim across the WHOLE operator resume — the self-heal, the prompt refresh, the pane handshake, the spawn and the post-send check — so the runtime reaper can no longer stop a session mid-resume and hand the next supervisor tick a second launch; a second resume arriving meanwhile is refused as "already resuming" (409), not reported as a launch failure (#8233).
  • Count a failed auto-resume once: the supervisor no longer appends a second [error: ...] note to a record resume_auto already marked errored (#8233).
  • Treat a pane whose tmux session cannot be observed as unknown rather than unresponsive, so a launch is no longer refused after the full blocking budget on the documented tmux-absent fallback (#8233).
  • Stop parking a Tokio worker for up to 3 s during the pre-launch pane handshake, on the same runtime that serves the daemon's HTTP routes (#8233).
  • Log, rather than discard, a store failure while recording a launch that never came up (#8233).
  • Strip an [error: ...] note whose message contains its own ] completely, instead of leaving its tail on the task forever (#8233).
  • The managed-session reaper now skips a session whose resume is in flight. A sweep whose live-session snapshot predated the resume's tmux recreate stopped the record, killed the pane the resume had just made, and stamped it Deliberate, which no automatic path revives (#8233).
  • The pre-launch pane handshake reads the tri-state tmux existence probe, so a transient probe failure is logged and reported as unobservable instead of being silently read as "no session" and skipping the wedged-pane check (#8233).
  • The resume route's deployment repair reads the framework root the daemon runs on instead of $HOME, so a test driving resume_managed no longer deploys the agent and skill manifests into the operator's own ~/.trusty-tools/trusty-mpm/claude-config/ (#8233).
  • Two resume attempts for one managed session can no longer run at once. The manual and supervisor resume paths share a per-session in-flight claim; a second attempt returns a typed ResumeInFlight error and does nothing — no second launch into the pane, no state change, no flap-breaker stamp. The supervisor skips such a session for the tick instead of appending an error to a record another path is resuming (#8233 acceptance item 4). The claim is released on every exit path, including a failed resume and a cancelled poller future, so a session can never be stranded unresumable.

Changed

  • Align workflow instructions with project test ladders, reusable verification evidence, bounded deterministic helpers, and task-owned builds and cleanup.
  • builders.max_concurrent now means the hard CEILING rather than a fixed count. The effective builder-slot count is derived per admission decision from the measured 1-minute load average and free memory: the ceiling when load is at or under logical_cores * builders.load_factor AND free memory is at or over builders.free_memory_floor_mb, otherwise the current holder count (never below one, and never revoking a granted lease). An unreadable load or memory reading fails CLOSED to the fixed ceiling, logged at warn with its errno and named as builder-cap-load-read-failure / builder-cap-memory-read-failure. N drops at once and rises only after one full 60-second quiet window. New [builders] keys load_factor (default 2.0), free_memory_floor_mb (default 8192) and slot_pool_root (default ~/.trusty-tools/cargo-target-pool); an out-of-range value in the section is refused with the key named, and an unknown one is reported by the existing unknown-key surface rather than failing the whole config parse. The measured count excludes the asking dispatch's own in-flight delegation record, so the throttle counts the builders actually running. A builder slot is now a persistent per-slot CARGO_TARGET_DIR under the pool root, grown lazily and seeded once by APFS clone from the repo's shared target directory, so concurrent builders stop serialising on one cargo build-directory lock (#8261).
  • the Disk survey reads its deadline through an injected clock (DiskProbes::now, SYSTEM_CLOCK in production) so its tests cross a deadline as a step instead of racing the host's load (#8277)
  • The PM's ## Autonomous Execution rule is its own tier-project instruction section with its own AUTONOMOUS-EXECUTION marker token, carved out of the tier-fixed core section. The rule now reads "run without stopping while the direction is clear" and names the ambiguity that stops it; tm-session-resume and tm-session-management no longer instruct a confirmation on resume, so the two can no longer contradict each other. A project sets its own comfort level with an AUTONOMOUS-EXECUTION block in its root CLAUDE.md (refs #8361)
  • PM workflow instructions and the tm-adr skill now state that a project-layout ADR or scaffold is only the default for a project with no defined layout, is framework-specific, and never justifies restructuring an existing layout to match it (Refs #8382).
  • A managed tmux session with no tmux.history_limit in ~/.trusty-tools/trusty-mpm/config.yaml now gets 10,000 lines of scrollback, down from 100,000, which lagged every tmux pane on the host. Set history_limit to keep a larger value; values below 1,000 still clamp to 1,000 (Refs #8404).

Security

  • tm doctor gains a launchd_secrets row: it fails when a com.trusty.* LaunchAgent plist holds a plaintext credential, naming the file and the KEY and never the value, and reports UNKNOWN rather than OK when a plist cannot be read or parsed (#8236).
  • tm doctor --fix --yes migrates each registered credential into the credential store, confirms a byte-equal read-back, and only then removes that key from the plist, leaving every other key untouched. A failed import can never destroy the only copy of a working credential. It takes no backup on purpose — a backup would be a second readable copy of the credential — and each step says the credential still has to be rotated. An unparseable or unwritable plist is reported as a failed step, never skipped.
  • tm doctor now FAILS on a binary (bplist00) LaunchAgent plist rather than reporting it unknown, and both the row and --fix's refusal name the remedy — convert it with plutil -convert xml1 <path> and re-run. A binary plist can be neither judged nor repaired here, and an unknown on a file that once held a credential gets ignored.
  • A SYMLINKED LaunchAgent plist is reported as unjudged and refused by --fix. Reading through the link would judge a file outside ~/Library/LaunchAgents, and the atomic rewrite would replace the link with a plain file and leave the operator's real plist stale.
  • The daemon's three credential readers — the LLM overseer, the Telegram channel, and session-manager provider resolution — go through one secret_source::resolve_secret instead of each walking .env.local, .env, and std::env::var by hand. Every failure returns None after an ERROR log naming the variable and the error kind, and leaves the dependent feature disabled; no arm falls back to a default.
  • tm doctor gains a credential_reach row: for each credential the daemon consumes it reports present, absent, the store's error kind, or timed out — never the value. A timeout is reported as a waiting Keychain approval dialog, which is the expected state after a cargo install rebuild, so it is no longer mistaken for "not configured". The probe runs on the blocking pool, so a dialog cannot stall the runtime thread serving GET /api/v1/doctor.

Documentation

  • rust-build-performance skill states the crate-edge rule: never add a workspace crate as a [dev-dependencies]/[build-dependencies] entry absent from the consumer's normal dependency tree, with the cargo tree -e dev check.

1.6.3 2026-09-18

Added

  • tm doctor rust_build_env row: on a project whose detected stack includes Rust, reports the shared CARGO_TARGET_DIR (exists, writable, size), the resolved CARGO_BUILD_JOBS, and whether sccache is on PATH and wired as build.rustc-wrapper, closing with the CARGO_TARGET_DIR=… CARGO_BUILD_JOBS=… SKIP_UI_BUILD=1 line a PM pastes into an engineer brief. A non-Rust project reports the row as not applicable.
  • build: section in ~/.trusty-tools/trusty-mpm/config.yamlcargo_target_dir (default ~/.trusty-tools/cargo-target/<owner>/<repo>, derived from the origin remote), build_jobs (default: half the host's cores, minimum 2), and sccache (default false). An absent section is the defaults and never a finding.
  • tm doctor --fix creates the shared target directory with its parents and seeds the build: defaults when no build key is present, preserving every existing key. It never writes ~/.cargo/config.toml, which is machine-global for every Rust project on the host.
  • rust-delivery-workflow bundled skill — the Rust DELIVERY-PROCESS rules, split out from rust-build-performance's build-speed scope so neither skill carries the other's concern. Eight sections: scope and precedence against rust-build-performance/cargo-commands/tm-workflow; commit-and-push before the gate chain so a crash mid-gate cannot destroy uncommitted work; CI-equivalent clippy (the CI workflow file is the pin's source of truth, and a crate-scoped local exit 0 is not CI evidence — PR #5488); build concurrency (a Rust build is CPU and RAM bound, so two on one host can OOM regardless of worktree isolation, with the cap read off tm (#8193) and the doctor check (#6868) rather than hardcoded, and CARGO_BUILD_JOBS prefixed inline per command because agent shell env does not persist); a pointer to BASE-AGENT's gate-output economy (#4790) rather than a copy; an sccache-posture pointer to rust-build-performance section 6; batching merges behind one cargo install and one live-probe pass for rung 4-6 closes; and a test-scope-by-stage pointer to tm-workflow. Wired into framework-manifest.toml's universal list, the ALL bundle table, and the rust-build skill-override family, so it deploys everywhere rust-build-performance does. tm-delegation-patterns's engineer-brief template gains the Rust-conditional line instructing a dispatched agent to prefix RUSTC_WRAPPER=sccache CARGO_BUILD_JOBS=<n> inline on every cargo command (Refs #8192).
  • verification-before-completion skill now carries the background-command wait protocol, the gate-chain pipe recipe (corrected: pipefail reports the LAST non-zero status, and ${PIPESTATUS[0]} is unset under this harness's zsh, where ${pipestatus[1]} is the per-stage code), and the stack-specific gate traps, all moved out of the resident BASE-AGENT body; tm-workflow gains a copy of the changelog-fragment placement and one-category rules, which stay resident in BASE-AGENT too (#8274).

Changed

  • PM prohibition P10 gains one narrow exception: a read-only tmux capture-pane of the PM's own session pane, filtered at source to agent status lines, so the PM can observe its dispatched agents' elapsed time and token burn. Every other tmux verb, every other pane, and every other non-git Bash command stay forbidden. The PM Allowlist carries the matching entry.
  • Fixed the documented filter: -S -80 now reaches scrollback, where agent status rows actually sit, and the pattern anchors on the row's own leading shape so it stops self-matching the echoed command or PM prose that quotes the filter. tm-delegation-patterns's "PM Allowlist, in Full" table now carries the same carve-out as the two instruction sections.

Documentation

  • tm-delegation-patterns's mandatory engineer-delegation closing instruction now points at BASE-AGENT's "Gate Output: Quote Results, Summarize Progress" rule instead of the bare "Show raw test output" phrase that led one engineer to tail a full build log for 280k tokens
  • tm-delegation-patterns's dispatch brief template adds a mandatory Time Box / Token Box line, with defaults (45 min/150k tokens single-crate, 20 min/60k research or ticketing) and a rule that the PM checks the box rather than pinging on a clock, sending one corrective message on overrun and re-dispatching narrower on a second
  • tm-delegation-patterns adds a row to the acceptance-criteria table: a review of a count-gated resource flipping 0 to 1 must read the first plan's computed attribute values, names in particular, not only the resource count (Refs #8130)
  • code-review-standards adds a Liveness Criteria Require a Sampled Check section: an acceptance criterion asserting a certificate or endpoint is live must cite a sample with a stated count, never a single request (Refs #8131)
  • code-review-standards adds a Lifecycle-Guard Escape-Path Verification check: an acceptance record for a prevent_destroy or other lifecycle-guard change must state how the documented escape path was actually exercised, never MET on prose alone (Refs #8132)
  • code-review-standards adds a Check Block Red-Path Coverage check: a Terraform check block on a scoped data source needs evidence for both the assertion-failure and read-failure paths, not the assertion path alone (Refs #8143)
  • code-review-standards adds a Check Block Transient-State Coverage check: a Terraform check block reviewed only against the final steady-state plan misses failures that appear only during resource replacement (Refs #8144)
  • tm-delegation-patterns documents the version-control isolation carve-out: a dispatch that commits source declares isolation: "worktree" even though version-control otherwise runs without isolation (#8156)
45 earlier releases
  • 1.6.0 2026-09-16
  • 1.5.36 2026-09-13
  • 1.5.27 2026-09-10
  • 1.5.26 2026-09-10
  • 1.5.25 2026-09-10
  • 1.5.17 2026-09-06
  • 1.5.16 2026-09-02
  • 1.5.15 2026-09-01
  • 1.5.14 2026-08-31
  • 1.5.13 2026-08-31
  • 1.5.12 2026-08-31
  • 1.4.1 2026-08-14
  • 1.4.0 2026-08-14
  • 1.3.6 2026-08-12
  • 1.3.5 2026-08-10
  • 1.3.4 2026-08-03
  • 1.3.3 2026-08-03
  • 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

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

0.12.6 2026-09-03

Changed

  • The MCP dispatcher dials the daemon through the shared trusty_mcp::daemon_bridge_json_rpc. mcp::rpc_client::call built its own JSON-RPC frame, called trusty_common::uds::send_framed_request_capped and unpacked an RpcResponse by hand — the same transport trusty-memory's stdio bridge carried a second copy of. Both now run one implementation (#6316)
    • The 32 MiB response budget and core::mcp_client_timeout() are unchanged; they are passed to the bridge rather than to the client. No request rewriter: the dispatcher has already built the exact params each analyze.* method expects
    • mcp::stdio::run is untouched. This crate's MCP surface is a tool translator with its own tools/list and its own #917 response-size guard, not an envelope forwarder, so the stdio loop stays where it is
    • A transport failure and a daemon-side JSON-RPC error still both surface as DispatchError::Transport naming the failing method. The message now carries the daemon's error code as well: <method> over <socket>: <message> (<code>)

0.12.5 2026-09-02

Breaking

  • RefactorSuggestion gains a public region_kind field, so an exhaustive struct literal built outside this crate no longer compiles.
  • core::refactor::analyze (re-exported as core::analyze_refactor) takes an eighth parameter to carry the region kind through.
  • Both are what move the version to 0.11.0 rather than 0.10.1; for a 0.x crate the breaking position is MINOR.
  • The daemon no longer binds 127.0.0.1:7879. It serves JSON-RPC 2.0 over <data dir>/trusty-analyze/trusty-analyze.sock, which every consumer derives through trusty_common::daemon_socket_path rather than reading a written-down address (#6287, ADR-0032). The http_addr discovery file is gone with it. serve --port and serve --mcp-port are accepted, hidden, and ignored with a warning rather than removed: the launchd plist on every machine that installed before this change still passes --port 7879, a cargo install does not rewrite it, and clap exiting 2 under KeepAlive::Always is a permanent crash loop with nothing in the logs but a usage message. serve --socket overrides the derived path.
  • trusty-analyze port is replaced by trusty-analyze socket. The path resolves whether or not a daemon is running, so the new command reports liveness too and exits non-zero when nothing answers — preserving the property that $(trusty-analyze socket) fails rather than handing a caller a path to a dead socket.
  • service::routes, service::ui and the axum router are replaced by service::rpc. service::events::DEFAULT_PORT is removed, and ApiErrorKind replaces the axum::http::StatusCode the handlers reported through.
  • --mcp-port and the /sse broadcast are DELETED, not ported. /sse's only subscriber was this daemon's own SPA; --mcp-port had no in-repo consumer at all and was a second ADR-0032-forbidden HTTP surface.
  • The embedded UI is not served by this daemon any more. ui/dist stays tracked; the console-hosted mount is follow-up work.
  • commands::daemon::handle_start no longer takes a socket parameter. It probed the socket it was handed but always spawned a child that derived its own, so a non-default path would have been probed and reported while a different one was served. It resolves the single path itself now.

Added

  • New scip_status MCP tool wraps analyze.scip_status — an MCP caller can now distinguish an index with no SCIP overlay ingested from one whose overlay carried zero symbols, the same distinction GET /indexes/{id}/scip gave HTTP callers in #5054. extract_graph's scip_overlay flag already carried this through the JSON-RPC body since #6287; this tool adds the dedicated node/edge/ingested_at lookup MCP had no way to reach (#5056)
  • Refactor suggestions carry a region_kind for Python, distinguishing a class_body from a function. Every other language emits no key at all, so their payloads are unchanged.
  • service::events::CODE_DEADLINE_EXCEEDED (-32005) so a handler that exhausted its own deadline stays distinguishable from one that broke — trusty-review reads the code to print "ran out of time" rather than "could not be reached". CODE_NOT_FOUND (-32004) preserves #5049's ingested-but-empty distinction across the transport change.
  • service::rpc::METHODS, the list the four crates that dial these names by literal are checked against, and tests/uds_consumer_contract.rs, which stands the daemon up on a temp socket and asks each of them what it sees.
  • trusty-analyze doctor warns when a retired LaunchAgent plist is still on disk (~/Library/LaunchAgents/com.trusty.analyze.plist from a pre-#6350 install), naming trusty-analyze service uninstall as the way to clear it. The check only reports; it never deletes (#6621).
  • trusty-analyze version subcommand, so tctl doctor trusty-analyze --self-check can spawn version --json instead of failing on a clap usage error. --json emits the DOC-1 capability-discovery envelope (contract_version, tool_version, verbs); without it, a one-line trusty-analyze v<version> (#6631).
  • trusty-analyze report --manifest <path> [--template cast] [--code-only] and the matching tr_report MCP tool generate a technical due-diligence report over the embedded trusty-review pipeline, under the existing review feature. Both call trusty_review::report::run_report rather than reimplementing manifest loading, template precedence, or the credential preflight. (#6669)

Fixed

  • trusty-analyze health, daemon status and the setup readiness poll no longer report a healthy daemon as DOWN when HTTP_PROXY is exported. All three build through trusty_common::http_client, which applies .no_proxy() (#4392).

  • The dashboard misrouted every API call on a reload of a hash-routed URL (/ui/#/). computeBase() in ui/src/lib/base.js ran its $-anchored index.html / ui/ strips against the raw document.baseURI, which includes the URL fragment, so the ui/ mount segment survived and API paths resolved under /ui/ — onto the SPA catch-all, which answers 200 text/html (closes #4980)

    • trusty-analyze mounts its SPA at /ui/ (src/service/routes.rs) with the JSON API as siblings at the daemon root, so it had the identical defect to trusty-search rather than a merely theoretical one
    • the strips now run against new URL(document.baseURI).pathname, which carries neither fragment nor query, re-joined to origin. The window.__ANALYZER_BASE__ override branch and the non-browser guard are unchanged
    • same fix as trusty-search, per the KEEP IN SYNC contract on this file; the committed ui/dist/ bundle is regenerated, since CI and release set SKIP_UI_BUILD=1 and ship whatever is committed
  • --features review pulled in trusty-review's entire default feature set, including a contributor-profile pipeline this crate never calls. The trusty-review dependency carried no default-features = false, so enabling review transitively compiled tga, rusqlite, and a vendored libgit2 with no source-code trigger anywhere in trusty-analyze. It now takes default-features = false and gets only the mcp feature the review gate already names (#5466)

    • this had to land with the removal itself: trusty-review 0.16.0 deletes the profile feature, which would otherwise have broken --features review in a crate whose source nobody touched
  • GET /indexes/{id}/diagnostics ran unbounded and clients got zero bytes instead of a response. The handler awaited one spawn_blocking that looped every unique file and spawned one subprocess per file-scoped tool with no per-request deadline; on the 4097-file trusty-tools index that ran past ten minutes and every client abandoned the connection at the transport layer (#6018)

    • the dispatch now takes a wall-clock deadline and checks it between subprocess spawns, so it stops mid-corpus and returns what it has. The response carries timed_out plus a cutoff object naming the files never reached and the tools never invoked — a truncated list can no longer read as a clean corpus
    • the deadline defaults to 180 s and is tunable with TRUSTY_DIAGNOSTICS_DEADLINE_SECS. Past that budget plus a 30 s grace the handler answers HTTP 504 with a JSON body saying which request was abandoned, rather than holding the connection
    • service/routes.rs layers a blanket tower_http::timeout::TimeoutLayer as a last-resort net under every non-streaming route. /sse is merged in after the layer so the event stream is not cut off
    • the four timeouts between a client and a cargo clippy subprocess now derive from one place, core::deadlines, instead of being independent hardcoded constants. Each rung is computed from the configured deadline, so raising TRUSTY_DIAGNOSTICS_DEADLINE_SECS cannot invert the ordering — a fixed 300 s router timeout used to lose to the handler's own budget at any deadline above 270 s, which handed the client the layer's empty-bodied 504 instead of the handler's structured JSON on the exact remediation path the error message recommends
  • The MCP run_diagnostics tool still timed out with no body in the default configuration. AnalyzerMcpServer's HTTP client used a flat 150 s request timeout, below the 180 s diagnostics deadline, so any run between the two produced a transport error rather than the daemon's answer — the original symptom, one layer further out (#6018)

    • the client timeout is now the outermost rung of the same ladder, with a floor that keeps deep_analysis above the 120 s OpenRouter ceiling regardless of how low the diagnostics deadline is set
  • A project-scoped build could outlive the request that asked for it. The deadline gated only whether run_project STARTED; inside, each cargo clippy or dotnet build ran under a flat build_tool_timeout() (300 s default, and two spawns per project for Roslyn). Because spawn_blocking cannot be cancelled, one cold project — or several in series — kept building for multiples of that after the client had its 504, and a client retry stacked another build behind it on the same toolchain lock (#6018)

    • StaticTool::run_project now takes the request deadline. Clippy and Roslyn cap each subprocess at min(remaining budget, build_tool_timeout()) and recheck between project roots, so the existing kill-on-timeout path terminates the child when the request ends instead of 300 s later. The default run_project checks the deadline between files
  • cargo clippy was invoked once per Rust file in a directory with no Cargo.toml, so it could never produce a diagnostic. Every invocation errored with "could not find Cargo.toml" and returned Ok(vec![]) while still costing ~0.155 s, which made a structurally useless tool the endpoint's main cost driver (#6018)

    • ClippyTool is now project-scoped, like the existing Roslyn tool: the dispatcher hands it real on-disk paths and calls run_project once per request instead of run once per file
    • run_project groups the files by their enclosing cargo root — the workspace root when one exists, so a 21-crate workspace is one build and not 21 — runs cargo clippy --workspace there under the build-class timeout, parses that output once, and keeps the diagnostics belonging to the requested files
  • The chunk export now walks trusty-search's cursor pagination (?after=) rather than offset pagination, and refuses an export that falls short of the total the daemon reports. Offset mode reads trusty-search's in-memory chunk cache — a map that is evicted after 300s idle, rehydrated on a detached task the request does not wait for, and capped by TRUSTY_MAX_CHUNKS — so a cold or unreadable corpus answered HTTP 200 with an empty page and every analyze endpoint then asserted a confident zero. Refs #6043, #5917.

  • build.rs keeps the committed ui/dist/ bundle instead of rebuilding it on every cold build. It used to run the package manager's install and a full vite build unconditionally, and both write files git tracks: vite build empties ui/dist/, deleting the tracked ui-source-hash.txt the publish-time freshness gate reads, and the pnpm-absent npm install fallback rewrote ui/package-lock.json with the host platform's optional-dependency set. Freshness is decided by scripts/check-ui-bundle-freshness.sh, the same check preflight-publish.sh runs, and an unreadable answer keeps the committed bundle rather than rebuilding it. FORCE_UI_BUILD=1 rebuilds unconditionally and re-stamps the bundle afterwards, which is what a UI change now needs. Backported from trusty-memory (#6060, #5078)

  • ui/package-lock.json is untracked and ignored. Nothing read it — CI and every build.rs install run pnpm against pnpm-lock.yaml — and its only writer was the npm fallback above (#5936)

  • trusty-analyze service uninstall reports a unit it could not clear and exits non-zero. It used to fold "no plist" and "removal failed" into one false, so a surviving file rendered as evicted or absent and the command exited 0 while launchd still reloaded the unit at next login. It now delegates the eviction to LaunchdConfig::evict_legacy_detailed — the workspace's one implementation, which also verifies launchd actually let go rather than trusting bootout's exit code — and reports its EvictionOutcome per label (#6350).

  • --help no longer advertises service install, service status and service logs; all three were removed from the CLI and each exited 2. The retired---port warning points at service uninstall rather than the service install that no longer exists (#6350).

  • A server that ended could make its own successor fail to start. serve_with_idle unlinked the socket while the router — and through it every AnalyzerAppState clone, so both redb handles — was still alive, so the facts.redb and scip_overlays.redb locks outlived the path a client keys off. A client that saw the unlink spawned a successor, whose FactStore::open hit Database already open. Cannot acquire lock.; the successor died before binding, Supervisor::ensure_running never noticed, and the caller waited out the full 20s spawn probe for a SpawnTimeout. The router is now released before the unlink, so the locks are free by the time anything can observe the server as gone (#6595)

    • measured at 54-560 ms of exposure on an idle machine, with lsof naming the exiting server as the only holder in 15 rounds out of 15; 0 out of 15 after the change
    • the signalled exit had the same window and no IdleGuard to close it: serve_until_idle returns Shutdown the instant the signal resolves, leaving a connection task holding a router clone. That case now waits out SHUTDOWN_FLUSH_TIMEOUT for the task to finish before unlinking, and warns rather than proceeding silently when a handler with no read budget outlasts it
  • analyze.health no longer restarts the idle window. Every caller of it is a monitor — the console connector, the console's console_metrics MCP poll and tctl's probe — each dialling every 15 s against a 600 s window, which kept one trusty-analyze serve process resident for 46 hours. It is registered as a liveness method now, so answering it costs the daemon nothing (#6621).

  • ensure_daemon_running no longer races a launchd-supervised com.trusty.analyze unit onto its own socket. The PID-file check that coordinates this daemon's own bridges cannot see launchd, so during a bootout/bootstrap window left by a pre-#6350 install the socket read as "nothing is running" and a bridge would spawn a second, unsupervised process. The guard now asks trusty_common::launchd_claim first and waits for the unit instead of spawning — a no-op on the ordinary host, where ADR-0032 means no plist is installed at all (#6624).

Changed

  • core::redb_open::is_format_obsolete now delegates to trusty_common::redb_open::is_incompatible_format instead of carrying its own copy of the four-arm match (#5063). Same verdict for every input; the quarantine policy open_or_quarantine is unchanged and stays here, because it takes a caller-supplied suffix and recovery string that trusty-common's fixed-suffix helper does not offer.
  • MCP protocol primitives now come from the trusty-mcp crate instead of trusty_common::mcp — imports move from trusty_common::mcp::… to trusty_mcp::…, and the trusty-common/mcp feature is replaced by a direct trusty-mcp dependency. No behaviour change: the types and functions are byte-identical, only their home crate moved (ADR-0040, #5803)
  • serve names the interface it binds as LOOPBACK_BIND instead of an unnamed [127, 0, 0, 1] literal (#6038). Behaviour is identical — the daemon answers on the IPv4 loopback and only there (ADR-0018) — but a client whose default URL said localhost looked correct while resolving ::1 first on macOS, and nothing in this file stated the address a client has to match.
  • The MCP stdio server was an HTTP client of its own daemon; it is an RPC client of its own socket now. Every tool-handler call site is unchanged.
  • trusty-analyze runs on demand instead of as a resident daemon. serve now exits after ten minutes with no traffic (TRUSTY_ANALYZE_IDLE_TIMEOUT_SECS overrides it; 0 disables the exit), unlinking its socket on the way out, and trusty-analyze deep starts the server itself rather than failing when nothing is listening (#6350).
    • serve --mcp is the exception and serves until it is signalled. The stdio loop that process runs dials the socket once per tool call and never respawns it, so an idle exit would strand a live MCP session with a transport error for the rest of its life (#6355).
  • trusty-analyze service install, service status and service logs are removed — no LaunchAgent is installed any more. service uninstall remains as the migration: it unloads com.trusty.analyze and its legacy alias and deletes their plists. setup daemon runs the same eviction before doing anything else, so an upgrade moves off the resident unit without an explicit command (#6350).
  • service::rpc::release_stores is a plain drop again. The Arc::strong_count poll it grew in #6595 waited for connection tasks to release the router before the socket unlink; serve_until_idle now performs that drain itself on the shutdown path (#6601), so keeping the caller-side loop would be two implementations of one guarantee.
  • serve_options inherits RpcServeOptions::shutdown_drain from the shared default (shutdown::plannable_grace()). An override to this crate's 3 s SHUTDOWN_FLUSH_TIMEOUT was reverted before release: it rested on the supervisor SIGKILLing this server at ANALYZE_SIGTERM_PATIENCE, which no supervisor path does. A bound analyze child is detached, so ensure_running never enters it in the supervised population and no reap path reaches it; trusty-analyze stop sends SIGTERM, polls 5 s and only reports. The 3 s drain averted no SIGKILL and abandoned the #6595 guarantee — every redb handle released before the socket unlink — three seconds into a multi-minute analyze.review.
  • SHUTDOWN_FLUSH_TIMEOUT rises from 1 s to 3 s and is now an alias of trusty_common::uds::ANALYZE_SHUTDOWN_FLUSH rather than a second literal asserted equal to it. It bounds the supervisor's spawn-failure kill — the one path that signals an analyze child — leaving 2 s of the 5 s patience for the socket unlink, the redb store drop and exit.

Documentation

  • Repaired every broken rustdoc intra-doc link in this crate and added #![deny(rustdoc::broken_intra_doc_links)] to its crate root(s), so a new one fails the build instead of shipping as dead text on docs.rs (#5744).

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)
18 earlier releases
  • 0.8.0 2026-07-27
  • 0.7.4 2026-07-27
  • 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

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

0.36.0 2026-09-13

Added

  • FindingCategory::Style (wire token "style") tags a pure taste, naming, formatting or idiom nit. A review whose only substantive findings are style nits now grades APPROVE — grade::derive_verdict_with applies it as a ceiling, so a model-proposed REQUEST_CHANGES or BLOCK is brought down too, not merely floored. Style findings also contribute nothing to the severity floor, so a High-effort style opinion can never reach the BLOCK tier. The ceiling lifts the moment one non-style substantive finding is present: a style nit alongside a real blocker still blocks. The reviewer response schema offers "style" and tells the model when to pick it, and an inline comment on a style finding leads with "Informational (style / preference) — does not block." FindingCategory is now #[non_exhaustive], so future categories are additive for downstream matchers.
  • trusty-review version [--json]. --json emits the DOC-1 capability-discovery envelope (contract_version, tool, tool_version, verbs) that tctl doctor --self-check trusty-review spawns and parses. The subcommand did not exist, so clap exited 2 with a usage error and the probe reported trusty-review version --json exited with exit status: 2 (#6913). It answers from the binary alone — no config file, no tokio runtime, no network.

Fixed

  • Bedrock Converse failures now report the AWS error code and message (for example ResourceNotFoundException: Model use case details have not been submitted for this account.) instead of the SDK's flattened literal service error, which made a wrong region, a missing credential, and an unapproved model read identically (#6912).
  • A test-coverage finding on its own no longer drives REQUEST_CHANGES or BLOCK. FindingCategory::TestCoverage has been documented as advisory since #1418, but the severity floor partitioned only method-conformance out of the correctness bucket, so a high-effort coverage gap floored exactly like a correctness bug. TestCoverage now reports is_informational, joining Style under the advisory ceiling (#7036).
  • A coverage-gap finding's inline PR comment now carries a coverage-specific "does not block" label instead of the style/preference one (#7036).
  • The CAST DD template's Report Metadata table no longer hardcodes CAST (CAST Software) — CAST Highlight + CAST Imaging as the Vendor / methodology value. That static string carried no provenance marker, unlike every other row in the same table, and could read as a factual claim that CAST Software's platform produced the analysis — no CAST product is invoked; trusty-analyze/trusty-search did the analysis. The row now renders {{vendor_methodology}}, the same self-known, provenance-tagged value the generic technical-DD template already used.
  • The CAST DD template's ## 3. CAST Scoring Model & Normalization section no longer cites the unmeasured historical CAST benchmark figure ("~3,467 apps") as fact. The "Peer-benchmark population" row now names this reporter's own analysis-corpus population instead of borrowing CAST's proprietary-corpus number, matching the disclaimer the per-application Peer Benchmark Position table already carries.

Changed

  • BedrockProvider builds its region and Converse client through trusty_common::inference::BedrockAdapter rather than its own copy of the region walk and its own aws_config::defaults(...) call. Region precedence (explicit > TRUSTY_AWS_REGION > AWS_REGION > us-east-1), the error text, the retry policy, cost estimation, and tool-use extraction are unchanged. BedrockProvider::new is now synchronous and takes only the model id — the AWS client is built lazily on the first call, and the explicit-region parameter every caller passed None for is gone
  • The dependency inventory now resolves a declared range against the checkout's lockfile and records which file answered. poetry.lock, uv.lock and the == pins of requirements.txt are read for the first time, so a pyproject.toml project no longer reaches the report as ranges only — previously every python row was unresolved, the largest share of the 515 of 1230 unscannable rows a 59-repository run produced. Dependency gains resolved (true only when the locked cell is one exact version) and source (the filename it came from); the Dependency Inventory table gains a Resolved from column that reads not resolved when no lockfile answered. A lockfile that is present and fails to parse no longer degrades silently: it is named in DependencyInventory::lockfile_warnings and rendered under the section, and the pass carries on with that ecosystem's declared ranges. Only the resolved name/version pairs are kept, so the inventory does not grow by the size of the lockfile it read. Dependency and DependencyInventory are now #[non_exhaustive] (issue #6794).
  • llm::enforce_strict_mode delegates to trusty_common::inference::strict_json_schema instead of carrying its own recursive walk. Behavior is unchanged for every schema this crate sends, and the function, its signature, and ResponseSchema::new's use of it all stay put; what moves is the implementation, so the same defect cannot be fixed here and stay broken in another crate — which is what #7082 was. The shared version also descends into $defs, definitions, and anyOf/oneOf/allOf, which the local one did not. Refs #7082

0.35.0 2026-09-06

Fixed

  • Both total analyze-lane collapses now lead their Gaps & Caveats line with the same "trusty-analyze lane DID NOT RUN" headline. The client-build-failure path led with "trusty-analyze data unavailable", which the audit bundle index does not recognise, so under --allow-degraded a report whose static-analysis lane never ran was indexed as one whose lane ran (#6784).
  • The investigation budget now scales with repository size instead of being a flat per-repository cap, so coverage no longer collapses on the largest repositories — the ones a due-diligence reader needs most. A 3,000-file repository is read at 300 files rather than 40, and a repository small enough that the flat default already covered it resolves exactly as before. A cap an operator pinned through --investigate-max-files, the manifest, or the environment is used verbatim and never scaled (#6784).
  • An investigation batch whose response could not be parsed is retried once instead of failing closed on the first attempt, so the files it carried are still read. 37 of 59 repositories in one engagement logged unparseable response on at least one batch, and every such batch was dropped with no second call — which is what collapsed investigation coverage on the largest repositories (#6784).
  • parse_findings now decodes three response shapes it used to reject outright: a conforming object behind a prose preamble, an untagged ``` fence, and an object followed by trailing prose. Any one of them dropped a whole batch (#6784).
  • A response the provider cut off mid-object without setting finish_reason is classified as a truncation rather than an unparseable answer, so it reaches the concise retry built for exactly that case instead of skipping it (#6784).
  • The analyze lane's own outcome is now a recorded fact in Gaps & Caveats, not something a reader has to infer from the per-repository reason lines. A run where every repository's fetch failed leads its gap list with trusty-analyze lane DID NOT RUN — 0 of N application(s) assessed, N failed, and a partly degraded run states M of N application(s) assessed. Before this, a 59-repository bundle whose analyze lane never ran carried the same shape of line as one where the lane worked for 58 of 59, so every CAST health factor read as absent rather than unassessed and downstream readers concluded static analysis had run and found nothing. Per-repository fail-open in analyze_adapter.rs is unchanged: nothing aborts, and a lane that populated everything it attempted still adds no line (#6811).

Changed

  • trusty-review report --analyze now FAILS, with a non-zero exit and a message naming the lane and both counts, when the analyze lane assessed nothing at all (0 of N applications). Such a report carries a finding count, a complexity figure and a health factor for every application and not one of them was measured, which #6783 shipped across 59 repositories and downstream readers took for "static analysis ran and found nothing" (#6811).
  • The new --allow-degraded flag writes that report anyway, with the 0 of N coverage line still in it. Partial degradation (M of N, M > 0) stays a warning at every setting and never fails the run: a 58-of-59 run carries 58 assessed applications (#6811).

0.34.0 2026-09-04

Added

  • Investigation findings can now carry CWE weakness classes, so a consumer counting ISO-5055-style structural flaws no longer has to infer them from prose (#6779).
    • Schema: the investigation response schema declares an optional cwe_id array and names the weakness classes the crate reads back, so the model picks from a known vocabulary rather than inventing a spelling.
    • Ingestion: report::investigate::cwe::resolve_all admits, per entry, a well-formed CWE-<number> id (upper-casing it) or a class NAME from one table of 12 weakness classes, then de-duplicates. Anything else — a malformed id, an unknown class, an empty entry — is dropped; one bad entry never costs a good one, a finding is never rejected for this field, and an id is never repaired into a neighbouring one.
    • Serialisation: cwe_id is skipped when empty, so a finding with no identifiable weakness class carries no field at all in investigation.json. A GREEN finding names a strength, so it carries no weakness class either.
    • Report: a classified finding renders its ids next to the title as SQL injection [CWE-89], or [CWE-798, CWE-532] where several apply; an unclassified one renders exactly as before.
    • Breaking: VerifiedFinding and FindingProse each gain a public field, so an out-of-tree struct literal for either needs it. Hence the 0.34.0 MINOR bump.

Fixed

  • investigation.json now carries every dependency a repository declares. The inventory was truncated to 30 rows before it was serialised, so a machine consumer reading repos[].deps — trusty-audit's OSV vulnerability lookup — saw 30 packages for a workspace declaring 134 and reported partial coverage as complete. The 30-row cap now applies only when the markdown Dependency Inventory table is rendered, so the report page and its "and N more" line are unchanged (#6788).
36 earlier releases
  • 0.33.1 2026-09-03
  • 0.33.0 2026-09-03
  • 0.32.0 2026-09-02
  • 0.31.0 2026-08-31
  • 0.30.0 2026-08-31
  • 0.20.0 2026-08-19
  • 0.15.0 2026-08-12
  • 0.14.1 2026-08-11
  • 0.14.0 2026-08-10
  • 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

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

7.1.0 2026-09-04

Fixed

  • tga collect and tga audit can now fetch from an SSH-scheme origin. The git2 dependency was built without the ssh feature, so libgit2 had no libssh2 transport and rejected every git@host:org/repo.git or ssh:// remote with unsupported URL protocol; class=Net (12) before the fetch's credential callback ran — 59 of 59 repositories in a client audit, each collected from clone-time refs. Adding the feature links libssh2 from its own vendored source and reuses the openssl this crate already vendors, so no new system library or runtime dependency (#6782).
  • The non-interactive credential chain now offers each source at most once per fetch instead of answering from the top every time libgit2 re-enters the callback. ssh-agent running with no identities loaded reports success, so the old behaviour re-offered the empty agent until libgit2 gave up — 120 seconds per repository — and never reached ~/.ssh/id_ed25519 (#6782).
  • A repository collected from stale local refs now leads the report's Gaps & Caveats section with git history is stale: fetch failed (…), ahead of the failed stages, and is named on stderr during the run. It was one unemphasised sentence mid-list, which a reader taking the commit and pull-request figures at face value could pass over (#6782).

7.0.0 2026-09-03

Added

  • tga backfill pm-effort scores the complexity of every meaningful PM ticket into the new fact_pm_effort table (#3915) — the EFFORT tier of the Activity / Work / Effort model, above the fact_pm_work tier #3916 added. The v1 formula (formula_version = "pm-effort-1") sums a base of 1.0 with five independently capped terms — child count, description length, comment count, status-transition count and story points — into a 1.0–50.0 score, bucketed LOW / MEDIUM / HIGH. Every weight, cap and boundary lives in one core::pm_effort::thresholds block, because issue #3915 marks them all "TBD, refine with product": a retune ships as a new formula_version string, never as an edit of the v1 values, so a stored score always names the weight set that produced it.
  • Two guards the raw formula does not provide. A ticket of a decomposable type (epic, feature, initiative) younger than 7 days is recorded as DEFERRED_RECENT with a NULL score rather than a low one — an epic filed yesterday has no children because nobody has broken it down yet, not because it is simple. And only tickets fact_pm_work marks meaningful are scored at all: an excluded ticket gets no row, and one that later becomes excluded loses the row an earlier run wrote.
  • Story points degrade rather than zero the score. They are 76% NULL across four per-project custom-field IDs on the source JIRA instance, so the term is simply dropped when absent, out of range, or unparseable, and the row's inputs_present column names which terms actually fired. The offline extractor reuses JiraClient::get_story_point_field's discovery shape — match by field name first, then fall back to the known ID list — because one global lookup cannot cover four spellings.
  • tga backfill pm-work classifies every work_items row's meaningfulness and persists the verdict to the new fact_pm_work table, the PM-side WORK tier of the Activity/Work/Effort model (#3916)
    • Deterministic v1 rules (formula_version = "pm-work-1") exclude terse decomposition stubs (TERSE_TITLE), tickets a bot filed that nobody moved (AUTO_GENERATED), and tickets a bot filed that a human later transitioned (BOT_FILED). No LLM tier.
    • Idempotent: UPSERT on (work_item_id, work_item_source), so a re-run rewrites the same rows and adds none. --dry-run reports the candidate count without writing.
    • PM work rows are counted in tickets and must never share a visualization axis with fact_commit_effort rows (#3917).
  • tga install runs without a terminal. --host <local|github|bitbucket> and --pm <none|github|jira|linear> select the non-interactive path, along with --org, --workspace, --repo, --repo-path, --repo-cache, --host-token, the --jira-* / --linear-* credentials, --output-dir, --llm-provider and --llm-api-key; --non-interactive forces it, and stdin not being a terminal implies it. A flag value wins over its environment variable (GITHUB_TOKEN, BITBUCKET_TOKEN, JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN, LINEAR_API_KEY), and a credential taken from the environment is written to the config as a ${VAR} reference rather than in the clear. Run with no terminal and no flags, install now names every missing flag at once instead of blocking on the first prompt. (#5216)
  • tga install --host github --org <ORG> derives the repository set from the GitHub API — discover_org_repos was already paging GET /orgs/{org}/repos for PR collection and is now what populates a generated config's repositories: list, so an operator no longer has to name paths that already exist locally. An org the token cannot see records that in the config instead of emitting a silently empty one. (#5216)
  • Linear joins JIRA and GitHub Issues in the project-management choices, and Bitbucket Cloud joins GitHub in the host choices, in both the wizard and the flag path. Bitbucket takes an explicit --repo <workspace/slug> list and says workspace discovery is not available yet (#5220) rather than producing an empty repository set. (#5216)
  • tga inspect schema prints every table, view, and column the database in front of you actually holds, with row counts and the free-text columns marked (#5218). It reads the file rather than the migration set, because a database collected by an older tga is missing later tables and nothing in src/core/db/sql/ says so.
  • tga inspect attest states the data-handling claim — "tga's database stores no file content, diffs, patches, hunks, or blobs" — and prints the live evidence for it: the scan that found no BLOB and no diff-named column, and a per-column reading of every free-text column in that database. The claim is never "contains no code", because a pasted snippet in a commit message is stored verbatim; the caveat saying so travels with it, and claim_never_says_no_code fails if a later edit loosens either. DOC-67 §10 quotes these two strings rather than paraphrasing them.
  • work_items.raw_json is read at runtime rather than cited from 0005_work_items.sql. Today's writer serializes a struct with no description field, but that is a property of the writer, not of the column. The diff probe unescapes JSON line breaks before matching, because a diff serialized into that column carries the two-character \n escape rather than a newline byte — four of the five markers anchor on a real line start and would otherwise miss the one column the attestation most needs to read. The probe reads plain text and that escaping; a base64-encoded or compressed diff reads as opaque text and is not counted, which is part of why the "not a claim that the database contains no code" caveat is not optional.
  • Both subcommands open the database read-only and refuse a missing path, a directory, or a non-SQLite file, each naming the cause. The shared Database::open would have CREATED and migrated a missing file, so an inspection routed through it would print a complete, empty, freshly-minted schema and exit 0 for a database the caller cannot read. attest also exits non-zero when its verdict is findings, so a hand-over script can gate on it.
  • Two standing guards replace one-time reads. every_text_column_is_classified fails when a migration adds a TEXT column that is in none of the three inventories in core::inspect::text_columns, so the free-text list cannot go stale silently. diff_for_commit_callers_match_the_attestation re-derives the non-test callers of collect::git::diff::diff_for_commit from the source tree and compares them against the pinned list — #5218 asked for "zero callers", which #5465 has since made false, so the enforceable property is now "these callers and no others".
  • Bitbucket Cloud workspace-to-repository discovery. bitbucket.workspaces is a new config list whose every entry is paged over GET /2.0/repositories/{workspace} — Bitbucket's next-cursor convention, the same shape discover_org_repos has had for a GitHub org since #742. The discovered repositories are unioned with the singular bitbucket.workspace/repo_slug pair, and one BitbucketClient now collects pull requests across the whole set instead of one repository. A workspace that fails is logged and skipped, and a repository that fails no longer discards the rest of the batch. (#5220)
  • tga install --host bitbucket --workspace <WORKSPACE> derives the repository set from the API, so --repo is now optional there. The generated config emits bitbucket.workspaces, so tga collect --validate-only accepts it. The generated block used to name a workspace with fetch_prs: true and no repo_slug — it deserialized, but validation then refused it with "Bitbucket config incomplete: repo_slug is required when fetch_prs = true". (#5220)

Fixed

  • A Bitbucket workspace that cannot be read no longer reads as a workspace with no repositories. A rejected credential surfaces as CollectError::BitbucketApi carrying the HTTP status and Bitbucket's own explanation, a rate limit as CollectError::Throttled with any Retry-After hint, and the 5 000-repository page cap logs that the set is partial. (#5220)
  • Commits classified by an older AI detector are re-classified on the next tga collect instead of keeping the retired detector's verdict forever (#6748). Around 700 commits carrying a literal AI trailer sat at is_ai_assisted = 0 in a downstream warehouse because they were walked before the #1334 detection fix landed; the discriminator was ingest date, not message content, and the consumer could not repair them because fact_commits has no message column (duettoresearch/cto-reports#140).
    • Migration v28 adds commits.ai_detector_version, defaulting to 0. Every row in an existing database therefore sorts as older than the shipped detector generation and is re-classified once; rows already at the current generation are never re-read. The composite index (ai_detector_version, id) and the query's matching ORDER BY keep the settled-corpus scan an index range walk over the stale rows only — no sort, and no full table read of every commit message. The query pins the index with INDEXED BY, because the statistics on a settled corpus leave an unpinned planner free to choose a full scan instead.
    • DETECTOR_VERSION is a hand-maintained constant. Nothing ties it to the marker table, so a change to the marker set that forgets to bump it silently skips re-classification; bump it in the same change that alters what the detector returns.
    • The pass runs in 1000-row batches, one transaction each, so an interrupted run leaves every completed batch stamped and the next tga collect finishes the remainder. A row's verdict and its generation are written by the same statement, so neither can be stored without the other.
    • The count goes to stderr as one line when anything was stale, and nothing is printed when the corpus is settled. Nothing is written to stdout.
    • tga backfill ai-detection-commits stamps the current generation across the slice it scanned, so a hand-run repair is not redone by the automatic pass.

Changed

  • The wizard and the flag path render config through one function. Both collect the same answers and hand them to commands::install_plan::render_yaml, so a scripted install and a hand-walked one produce identical output for identical answers. (#5216)
  • bitbucket.repo_slug and bitbucket.workspace are required only when bitbucket.workspaces is empty. A config that names workspaces to discover supplies neither. (#5220)
  • BitbucketClient::fetch_pr_commits takes the workspace and repository slug as arguments rather than reading them off the client, which now covers many repositories. (#5220)
  • InstallArgs exposes only output and force publicly again. The 17 flag fields #5216 added at the unpublished 6.0.1 are pub(crate): they are read by commands::install and commands::install_flags and by nothing else, and main.rs pattern-matches the whole struct without touching a field (#6744). This does not clear the 6.0.0 baseline — a struct that was exhaustively constructible cannot gain a private field without a major bump either, so bash scripts/check_semver.sh --crate tga still reports BREAK and tga still owes 7.0.0 at its next publish. What it buys is that the flag after this one costs nothing: from 7.0.0 on, InstallArgs is no longer constructible from outside the crate, so adding a field to it stops being a public-API change.

Documentation

  • docs/trusty-git-analytics/requirements/database-schema.md is rebuilt from the migrations. The migration-history table stopped at 0013 and now runs through 0027, one row per migration naming its file; two of the rows it did have were wrong (0012 is pull_requests_repository, not repository_analysis_status, and 0013 puts complexity on classifications, not on commits). Thirteen tables and all four DORA views had no section at all. Eight existing sections described columns the shipped schema does not have — work_items still listed the provider / external_id / work_item_type / state shape migration 0005 never used, linear_issues named issue_id instead of identifier, commit_work_items and classification_overrides described integer keys where both use composite text ones, repository_analysis_status named four columns it does not have, and pull_requests listed a merge_commit_sha that has never existed.

6.0.0 2026-09-02

Added

  • tga collect records the head SHA, ref name and walk SCOPE each completed full-history walk reached, per repository, in the extract database (schema v25). A later collect skips the walk when the repository is unchanged, walks only the new commits when the head advanced, and re-walks in full — naming the reason — when the recorded commit is no longer reachable, when the previous walk did not complete, when --force is passed, or when this run's --branch / --head-only / merge scope differs from the recorded one. A scoped run therefore never licenses a later full-scope run to skip. (#6073)
  • The end-of-collect summary line reports how many repository full-history walks were skipped, the only figure separating a skipped walk from one that ran and found nothing new. (#6073)
  • The authorship summary carries an identity_merge_risk flag when a high-confidence but unconfirmed alias suggestion touches a top-ranked author, naming the affected metrics, how many identities are involved, and tga aliases suggest. Suggestions are still never merged automatically, and a pair the operator already merged never counts as unmerged. (#6142)

Fixed

  • tga collect: Linear enrichment no longer spends a GraphQL lookup on documentation, standard, digest and advisory tokens that merely share a ticket's shape (#5664). LinearClient::extract_issue_ids now drops UTF-8, SHA-256, ADR-0029, RFC-2119, ISO-8601, ECMA-48, RUSTSEC-2026 and their families before any request is issued; a live 52-week collect on this repository sent 369 such lookups, none of which could ever resolve. The decision is made offline by collect::ticket::is_non_ticket_identifier, which extract_ticket_id, is_ticketed and branch_ticket_key already used under its former name — one prefix list, so the pre-lookup gate and the subject-position rule cannot drift apart. That list widened from four documentation prefixes to the measured families, so a subject led by CVE-2024-3094 or SHA-256 no longer counts as a declared ticket key either. Ticket-shaped tokens that simply do not resolve (WI-1, AC-1, CREDPANEL-01) are deliberately still looked up: nothing separates them from another organization's real board keys, and DOC-70 §9.1 reads an unresolved key as the signal it is.
  • A history walk whose revwalk stops early — a corrupt or unreadable object — now fails the repository's collect stage instead of returning as a completed walk. The rows it already wrote are kept, but the partial traversal is never recorded as complete, so the next collect re-walks rather than skipping on it. (#6073)
  • The recorded walk scope encodes its branch list as JSON rather than joining on ,, so a branch whose name contains a comma is no longer indistinguishable from two branches — a run scoped to either used to skip the other's walk. tga collect --dry-run also stops printing a full-history-walk skip count: a dry run writes to an empty in-memory database, so that figure was structurally always zero. (#6073)
  • The authorship report applies confirmed identity merges recorded in authors.aliases before computing bus factor and top-author share, so a merge an operator accepted no longer comes apart when a later collect re-observes the source email. The map is applied to the identity resolver's own answer as well as to unlinked commits, so a re-created source row does not split the merge back apart. (#6142)
  • tga collect no longer re-creates an author row that tga aliases merge deleted. The resolver routes an email already recorded as a confirmed alias to the identity that absorbed it, so a merge survives every later collect rather than only the report that follows it. (#6142)
  • The unmerged-identity scan runs once per audit rather than once per repository. It cross-joins every identity against every other, and the authors table is shared across all repositories in one extract database, so the per-repository call repeated an O(n²) scan for an identical answer. (#6142)
  • The scan now receives the configured team.canonical_domain. It previously ran with no domain, which muted the .local hostname, GitHub-noreply and domain-typo signals — the identities issue #6142 exists to surface. (#6142)
  • tga audit names a failed unmerged-identity scan as a gap on the DD manifest instead of dropping the identity-merge risk flag with only a log line, so bus factor and top-author share never print with an unstated check beside them. An authors.aliases value that will not parse as JSON is likewise named on stderr — with the author it belongs to — at both the collect and report sites, rather than silently reading as "this author has no confirmed merges". (#6142)
  • tga collect: a GitHub secondary rate limit during the reviewer pass no longer fails the run (#6553). The pass now tells a throttled pull request apart from a broken one, records the shortfall once — naming how many pull requests got no reviewer rows — and records it at ItemSkipped severity, so tga collect exits 0 with pr_reviewers partial rather than exiting 1 with every other stage's data already persisted. The reviewer query is forward-only, so the next run resumes at the pull requests the throttled one never reached. This replaces the #6084 abort, which made github.fetch_pr_reviews: true unusable on an unattended schedule and emitted one warning line per remaining pull request (21,230 in the reported run).
  • tga collect's rate-limit sleep allowance is now per-RUN rather than per-client (#6565). #6084 gave each GitHub client a FetchBudget, which bounds a client and not a run: one collect builds several — org discovery, the PR sweep, the reviewer pass — so the 120 s ceiling was charged once per client and the wall-clock a run could spend asleep scaled with the number of clients instead of being the fixed bound the constant reads as. A breaker latched during one pass also did not stop the next from spiralling again. The new RunBudget is a shared handle every client takes via GitHubClient::with_run_budget, constructed once on CollectionPipeline, so there is one allowance, one breaker, and one truncation ledger for the whole run.
  • TGA_RATE_LIMIT_SLEEP_BUDGET_SECS overrides the 120 s total. The same ceiling now covers strictly more work than it did per-client, so a long multi-org sweep that legitimately needs a larger allowance has a way to ask for one. A zero or unparseable value falls back to the shipped default rather than latching the breaker on the first rate-limited response.
  • TGA_RATE_LIMIT_SLEEP_BUDGET_SECS is now discoverable (#6565). The knob shipped with the per-run budget but appeared in no tga --help, no tga collect --help, and no documentation — RATE_LIMIT_SLEEP_BUDGET_ENV was pub(crate) and never surfaced, so an operator whose sweep needed a larger allowance had no way to learn one existed. tga collect --help now carries an ENVIRONMENT: section naming the variable, its 120 s default, and the rule that zero, empty, and unparseable values fall back to that default; the same detail is in the crate README's configuration section and in the workspace environment-variable table. A test renders the subcommand's help and asserts the constant's own value appears in it, so renaming the constant without updating the help fails rather than silently un-documenting the variable.
  • The audit module's # Spec References blocks name DOC-67 by its repo-root-relative path rather than a ../../../../ traversal, which DOC-38 §2.1 permits only in a Markdown visible section. Eleven references were silently unchecked as a result (#6605). run_full_sweep's stage-order note moved out of the reference block, where its prose closed the block and left two more references unscanned.

Changed

  • The audit guards' analyze.health and search.health frames carry "params": {} (#6555). Both sent no params at all, which decodes to Value::Null and works only because those methods are bound to NoParams; binding either to a struct would have turned the omission into a -32602, which each guard reads as "the daemon cannot serve the report"
  • extract_owner_repo_from_url delegates to trusty_common::github_path::parse_remote_url instead of parsing the URL itself, and the second copy of it under commands::deployments is now a re-export of the first (#6657). The accepted forms are unchanged — HTTPS, scp-style SSH, ssh://, and https://user@, GitHub hosts only.

Documentation

  • audit::repo_index::index_id_for's agreement note points at trusty_review::report::index_registry::derive_index_id, the module that derivation moved to (#6677).
  • Repointed build_authorship_summary_with's Test: citation from single_author_subsystem_detected, a name that never shipped, at shared_subsystem_is_not_single_author; the positive case is already asserted inside builds_from_seeded_commits (#6678).
40 earlier releases
  • 5.0.2 2026-08-31
  • 3.2.0 2026-08-19
  • 3.1.0 2026-08-19
  • 2.17.0 2026-08-12
  • 2.16.0 2026-08-11
  • 2.15.0 2026-08-11
  • 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.

trusty-audit

Audit engagements at a client site

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

0.14.2 2026-09-06

Fixed

  • Declared trusty-common's uds feature, which the grounding daemons, hotspots, and search_rpc modules need for their UDS framed-request client. Workspace feature unification hid the missing declaration from every gate except cargo publish's isolated verification build, which failed with 11 x E0433 could not find 'uds' in 'trusty_common' and burned the trusty-audit-v0.14.1 tag before it could ship.

0.14.1 2026-09-06

Added

  • The run index (index.md, and the copy inside the return package) states how much of each repository the investigation pass actually read — files read, tracked files, the share, and whether that repository's static-analysis lane ran at all — plus the estate total, least-covered first. The figure was in every report's JSON twin and nowhere a recipient would look, so answering "how much of this estate was read" meant opening all 59 reports (#6784, #6811).

Fixed

  • The run index's dead-analyze-lane count now keys on the shared trusty_common::review_gap_contract headline instead of a literal of its own, so it recognises every gap line trusty-review writes for a total collapse. It previously missed the client-build-failure path and undercounted Rollup::analyze_lanes_dead() (#6784).

0.14.0 2026-09-04

Added

  • An opt-in OSV.dev lookup over the dependency inventory each repository's report already measures (#6780). [collectors] osv = true in engagement.toml turns it on; a config that does not declare it runs exactly as before. Each repository's directory gains an osv.json carrying {package, ecosystem, version, vulns: [{id, aliases, summary, severity}]} plus queried, matched and errors; the advisories also reach [report].findings, which a re-render renders under a "Known vulnerabilities (OSV)" section; and index.md gains a severity count table with the worst advisories beneath it, or a "not run (opt-in)" line when the collector is off.
    • Queries go to POST /v1/querybatch in chunks of 1000, the cap OSV documents, retrying a 429 or a 5xx three times with exponential backoff and stopping at a per-repository time cap ([osv] time_cap_secs, default 120).
    • Answers are cached under the working directory's state/osv-cache/, keyed by (ecosystem, name, version) with a TTL ([osv] cache_ttl_hours, default 168). [osv] offline = true — or TRUSTY_AUDIT_OSV_OFFLINE in the environment — answers from that cache alone and opens no socket, recording each miss as a named gap.
    • Every degradation is named twice, in the run's gap list and in osv.json's errors: a package with no locked version, an ecosystem the collector does not map onto OSV, a batch that went unanswered, and the row cap the renderer applies to the inventory this scan reads. A repository whose every batch failed says it has no OSV coverage rather than shipping an empty scan that reads as clean.
    • Every OSV request carries an explicit timeout of the repository's remaining budget, so an endpoint that accepts a connection and never answers cannot outlive the cap or block the sweep; response bodies are accumulated against an 8 MiB ceiling rather than read whole; a 429 is paced by the server's own Retry-After when it states a usable one; and a response carrying fewer results than the batch had queries names the coordinates it left unanswered instead of dropping them.
    • TRUSTY_AUDIT_OSV_OFFLINE recognises only truthy spellings (1, true, yes, on). Any non-empty value used to turn offline on, so =false silently cost a run every answer it had not cached.
    • A re-render's index.md states the OSV result of the package it rendered FROM, so one bundle can no longer read as "not run (opt-in)" in one index and report its advisories in another.
  • A bundle-level technical-debt roll-up (#6781). A sweep, a re-render, and a return package now write report.json beside index.md, carrying counts of every declared finding by tier, by dimension, by repository, and by tier × dimension, plus the total they all sum to. The tiers are the collectors' own RED / AMBER bands and the dimensions their own dependencies / license / secrets / churn categories — nothing is re-banded or re-labelled.
    • index.md gains a "Technical debt by tier" table across repositories, rendered from that one computed value rather than counting the findings a second time, so the table and report.json cannot state different numbers.

Fixed

  • The engagement template's [tools] pins now name the versions this release train ships. scripts/refresh-engagement-pins.sh sets each pin to its crate's current workspace version and --check reports the stale ones; scripts/preflight-publish.sh CHECK 10 runs that check when publishing trusty-audit and fails the release when a pin lags a sibling whose workspace version is not yet on crates.io, so the copy compiled into instructions::ENGAGEMENT_TEMPLATE and written out by taudit distribute can no longer ship a version behind the one that just published (#6772).
  • The run index and the return package's reports/index.md now state, in a repository's own section, that its git history is stale because the fetch failed. The fact reached the recipient only as one bullet inside section 9 of that repository's report, which is read after its commit and pull-request figures rather than before them (#6782).
  • A 409 Conflict from POST /indexes no longer costs a repository its whole search-derived evidence tier. trusty-audit reads the daemon's registry, reuses the registration that already names this checkout, deregisters the stale row when one holds the id at another root or holds this tree under an obsolete id, and retries the create once. Deregistration never destroys the corpus and is guarded by the root it was decided on (#6783).
  • Every arm that loses the search index now leads with one phrase — evidence tier degraded: search index unavailable (<error>) — and names the trusty-analyze pass that did not run with it, so a skipped analyze pass is the same headline rather than a separate silent gap (#6783).
  • A run's index.md counts the repositories audited without search evidence and qualifies its coverage line, so an "M of M" run whose search tier was empty no longer reads as complete (#6783).
11 earlier releases
  • 0.13.3 2026-09-03
  • 0.13.2 2026-09-03
  • 0.13.1 2026-09-02
  • 0.12.0 2026-08-29
  • 0.11.1 2026-08-28
  • 0.10.0 2026-08-25
  • 0.9.0 2026-08-22
  • 0.8.1 2026-08-22
  • 0.8.0 2026-08-22
  • 0.7.0 2026-08-20
  • 0.6.0 2026-08-19

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

The rest of the workspace

Only the 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.