Commit Graph

14 Commits

Author SHA1 Message Date
Maximilian Roos cfd6f099bc fix(codex): clear activity marker on session end (#3660)
Codex now exposes `SessionEnd`, but Worktrunk’s Codex plugin only
returned the activity marker to idle at turn end. This adds a
main-session exit hook that clears the marker, using Codex’s
three-second maximum hook timeout so cleanup has the best chance to
complete without delaying shutdown.

The CLI help, plugin-layout guidance, user documentation, generated
mirrors, metadata test, and help snapshot now describe and verify the
complete Codex lifecycle.

Tested with `cargo run -- hook pre-merge --yes` (4,564 tests passed; 1
skipped).

> _This was written by Claude Code on behalf of max_.
2026-07-29 20:57:26 -07:00
Maximilian Roos 36ba57bdca fix(plugin): ship skills to Codex installs via a generated real-file mirror (#3440)
Codex installs of the worktrunk plugin carried no skills. `codex plugin
add` copies the plugin into `$CODEX_HOME/plugins/cache/` via
`copy_dir_recursive` (codex-rs core-plugins), which handles only regular
files and directories — so the `skills -> ../../skills` symlink (and the
nested `reference/README.md` link inside the tree) were silently
dropped, and sessions load from that cache copy. Verified against
codex-cli 0.144.1 both by reading the tagged sources and by installing
this repo's plugin into a scratch `CODEX_HOME`: the installed root had
no `skills/` at all and `codex debug prompt-input` showed an empty
plugin skill inventory.

This PR cuts the plugin's `skills` symlink over to a **generated
real-file mirror** of the authored repo-root `skills/`, kept current by
a new `sync_plugin_skills_mirror` stage in `test_docs_are_in_sync`
(dereferences symlinks, deletes stale files, self-heals and fails on
drift — same pattern as the other generated mirrors). Repo-root
`skills/` stays the authored home: Gemini hard-probes it at the
extension root, the docs sync writes into it, and Windows checkouts read
it. Reversing the symlink direction instead would put a symlink at the
repo root, breaking Gemini's install copy and every Windows checkout —
which also surfaces a latent bug this fixes: symlinks materialize as
plain text files on Windows clones, so installs from a Windows checkout
shipped no skills to Claude or Codex either.

It also drops the Codex manifest's `skills: "./skills/"` key: with no
key, Codex scans `<plugin-root>/skills/` by convention
(`default_skill_roots` in `codex-rs/core-plugins/src/loader.rs`), and
the explicit path resolves to the same directory, so the key was
redundant — symmetric with the Claude manifest cutover in #3431.

**For the reviewer:**

- The 20 files under `plugins/worktrunk/skills/` are the generated
mirror (byte-identical to repo-root `skills/`; git stores shared blobs
once). `plugins/worktrunk/CLAUDE.md` → "Plugin skills are a generated
mirror" documents the rationale with codex-rs citations.
- `sync_plugin_skills_mirror` in
`tests/integration_tests/readme_sync.rs` is the sync stage (Step 3b of
the pipeline); it already proved itself once in this branch — merging
main regenerated `reference/{config,list}.md` in the mirror.
- `test_plugin_layout_is_consolidated` now pins the mirror shape
cross-platform (real directory, no symlinks anywhere under it),
replacing the unix-only symlink assertion.

**Verification:** fresh scratch-`CODEX_HOME` install now carries both
skills into the cache and `codex debug prompt-input` lists `worktrunk`
and `wt-switch-create`; `claude plugin validate` passes and `claude
--plugin-dir … plugin details` discovers Skills (2) plus all 8 hooks;
Gemini's repo-root path is untouched. A 2×2 probe matrix (manifest key
present/absent × real dir/symlink) confirmed key-absence changes nothing
and symlinks ship nothing.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:48:30 -07:00
Maximilian Roos 0fc3518f8d docs(plugins): record the deliberate absence of a manifest version field (#3434)
Follow-up to #3431: records in `plugins/worktrunk/CLAUDE.md` why the
Claude manifest deliberately omits a `version` field — installs pin the
marketplace git SHA, so a semver would be a second version to maintain
with nothing consuming it — so a future cleanup pass doesn't "fix" the
`claude plugin validate` warning by adding one.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:12:26 -07:00
Maximilian Roos 6c122a5d08 refactor(plugin): adopt Claude's canonical manifest layout, convention-only discovery (#3431)
## What this does

Moves the Claude plugin manifest to the canonical
`plugins/worktrunk/.claude-plugin/plugin.json` and strips it to metadata
only, dropping the `hooks` and `skills` keys so that both component
types load purely by Claude Code's convention discovery. Rewrites the
layout docs and inverts `test_plugin_layout_is_consolidated` to pin the
new invariants.

## Background

The plugin layout has been in flux:
[#3382](https://github.com/max-sixty/worktrunk/pull/3382) Claude-scoped
the hooks filename, which silently broke hook loading
([#3417](https://github.com/max-sixty/worktrunk/issues/3417)), and
[#3418](https://github.com/max-sixty/worktrunk/pull/3418) reverted it.
That revert restored working hooks but left two claims baked into the
layout, the docs, and the test that this PR found to be stale.

## Research method

Everything below was verified empirically against claude-cli 2.1.207
(the current release), not from docs alone:

- **Scratch marketplace probe**: a throwaway marketplace mirroring
worktrunk's exact shape (repo-root `.claude-plugin/marketplace.json`,
plugin in a `./plugins/<name>` subdir source, `skills` as a symlink
pointing outside the plugin dir, hooks at the conventional path) with
two plugin variants — manifest in `.claude-plugin/plugin.json` vs. bare
`plugin.json` at the plugin root — installed through the real `claude
plugin marketplace add` + `claude plugin install` flow.
- **Component inventory**: `claude plugin details` and `claude plugin
validate` on both variants.
- **Live sessions**: headless `claude -p` runs confirming skills appear
in-session and hook commands actually execute (marker files, and reading
the 🤖 activity marker from inside a live session).
- **Loader inspection**: the manifest resolution order read directly out
of the claude-code binary's bundled loader code.

## Findings

1. **The "no `.claude-plugin/` wrapper" claim no longer holds.** The
repo documented — and the test pinned — that Claude wants `plugin.json`
bare at the plugin root, and that a `.claude-plugin/` wrapper in a
subdir-source plugin fails with "Plugin not found". On 2.1.207 the
wrapper form installs, loads skills, and fires hooks end-to-end. The
loader tries `.claude-plugin/plugin.json` first with root `plugin.json`
only as a runtime fallback, several marketplace-resolution paths read
*only* the wrapper, and `claude plugin validate` rejects the root-level
form outright ("No manifest found in directory. Expected
.claude-plugin/marketplace.json or .claude-plugin/plugin.json").
2. **"Claude has no skill auto-discovery" is wrong.** Skills are
auto-discovered by scanning `skills/*/SKILL.md`; a `skills` manifest
array only *adds* directories to that scan (entries equal to the default
dir are filtered). An unlisted skill behind the out-of-plugin symlink
loaded fine — the installer dereferences the symlink into a real
directory in the install cache. Dropping the array also makes the old
test guard ("every skill must be listed or it's silently invisible")
structurally unnecessary: a new repo-root skill now ships to Claude,
Codex, and Gemini with no manifest change anywhere.
3. **The `hooks` manifest key was dead config.** #3417 established the
string-path override isn't honored for plugin loads; convention
discovery of `hooks/hooks.json` is the mechanism that works. A key
pointing at the conventional path adds nothing and could mask a future
mislocated file, so it's gone (one mechanism per guarantee).

## Changes

- `plugins/worktrunk/plugin.json` →
`plugins/worktrunk/.claude-plugin/plugin.json`; content reduced to
`name`/`description`/`author`.
- `plugins/worktrunk/CLAUDE.md`: layout diagram and Claude
path-resolution bullet rewritten to the convention-only model; the
refuted wrapper claim and the stale no-auto-discovery claim removed.
- Root `CLAUDE.md`: the Plugin Layout pointer updated (dropped the stale
"Codex no-hooks re-enablement" phrase — Codex hooks shipped inline in
#3364).
- `test_plugin_layout_is_consolidated`: now asserts the wrapper manifest
exists, a root-level `plugin.json` does not, the manifest carries no
`hooks`/`skills` keys, the plugin `skills` symlink resolves to the
repo-root `skills/` (unix only — Windows checkouts without
`core.symlinks` materialize it as a file), and every repo-root skill dir
carries a `SKILL.md` (the scan silently ignores a dir without one).

Codex and Gemini are untouched: Codex reads only its `.codex-plugin/`
wrapper, and its inline `hooks` object still overrides Codex's own
convention discovery, so the #3362 collision stays closed independent of
any of this.

## Verification

- `claude plugin validate plugins/worktrunk` passes (it failed on the
old layout). Remaining warnings are deliberate: no `version` field
(installs pin the git SHA as the version; a hand-maintained semver would
go stale) and the plugin-root `CLAUDE.md` (developer docs, not plugin
context).
- A live `claude --plugin-dir` session on this exact tree loads both
skills and the `UserPromptSubmit` hook sets the 🤖 marker, observed from
inside the session.
- The scratch-marketplace probe covered the same shape through the full
marketplace install flow, including the symlinked skills dir.
- Full pre-merge gate: 4389 tests passed plus lints, doctests, and
docs-sync.

## Risk

The one scenario this could regress is a Claude Code version old enough
to read *only* the root-level manifest. That couldn't be tested
directly; the mitigations are that the wrapper is the documented
convention, the loader prefers it today, and Claude Code self-updates
aggressively. The old layout's "verified end-to-end" claim most likely
dates from an earlier 2.1.x behavior or a misdiagnosed failure.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:35:51 -07:00
Worktrunk Bot 69d6afb1fe fix: restore Claude plugin hooks to conventional hooks/hooks.json (#3418)
## Problem

Since [#3382](https://github.com/max-sixty/worktrunk/pull/3382)
("Claude-scope the shared hooks payload"), the Claude plugin's hooks
stopped loading in Claude Code. Skills and commands still load, so the
plugin looks healthy — but `/hooks` shows none of worktrunk's handlers
and the 🤖/💬 activity markers never update. It fails silently.

Root cause (confirmed by the reporter on Claude Code 2.1.207): Claude
Code discovers plugin hooks by **convention** at `hooks/hooks.json`. It
does **not** honor `plugin.json`'s string-path `"hooks":
"./hooks/claude-hooks.json"` override for plugin loads. The #3382 rename
moved the file off the only path Claude's loader actually reads.

The rename was intended to stop Codex's convention discovery from
surfacing Claude's events
([#3362](https://github.com/max-sixty/worktrunk/issues/3362)). But the
Codex manifest already defined its hooks **inline** (`Some(Inline)`
branch of `resolve_manifest_hooks`), which overrides Codex's convention
discovery — and that inline definition **predates** the rename (verified
against `2677f05^`). So the rename was always redundant for the Codex
scoping, while it broke Claude's discovery.

## Solution

Restore the file to the conventional
`plugins/worktrunk/hooks/hooks.json` and point `plugin.json`'s `hooks`
back at it. `plugin.json` reverts byte-for-byte to its pre-#3382 state.

This does not resurface the #3362 collision: the Codex manifest's inline
`hooks` object keeps Codex off the shared `hooks/hooks.json` regardless
of the filename. The two toolchains now coexist on one file — Claude
discovers it, Codex ignores it via the inline override. The
`test_plugin_layout_is_consolidated` assertions covering the Codex
inline manifest are unchanged and still pass.

Also updates `plugins/worktrunk/CLAUDE.md` (layout diagram + Known
Limitations) and drops the now-reverted #3382 CHANGELOG entry, replacing
it with a Fixed entry (both were in the unreleased 0.67.0, so the net
change is nil).

## Testing

Reproduction is captured in `test_plugin_layout_is_consolidated`
(`tests/integration_tests/config_show.rs`), whose invariant was inverted
from "the Claude file must be Claude-scoped" to "the Claude file must
sit at the conventional `hooks/hooks.json` that Claude Code's loader
discovers." Against pre-fix production it fails (`left:
"./hooks/claude-hooks.json"`); after the rename it passes.

- `test_plugin_layout_is_consolidated` ✅
- `test_claude_hook_commands_parse_in_all_shells` ✅ (reads the renamed
file; shell-integration-tests)
- Full `integration_tests::config_show` module: 136 passed ✅
- `test_docs_are_in_sync` ✅

**Not verified end-to-end from CI** (per running-tend "Don't ship fixes
you can't verify"): the bot cannot drive a live Claude Code or Codex
session to observe the hooks firing. The fix rests on the reporter's
confirmed symptom + repro (symlinking `hooks/hooks.json` →
`claude-hooks.json` immediately restores the hooks) and on Claude Code's
[documented convention
path](https://code.claude.com/docs/en/plugins-reference.md)
(`hooks/hooks.json` in plugin root). The Codex-stays-clean claim rests
on the existing code analysis in CLAUDE.md and the inline manifest
predating the rename, not a live Codex run.

---
Closes #3417 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-07-10 20:37:21 -07:00
Worktrunk Bot 2677f05d0c feat(plugin): Claude-scope the shared hooks payload (#3382) 2026-07-08 02:15:50 -07:00
Worktrunk Bot 512a94de2f feat(plugin): ship Codex-native activity hooks (#3364) 2026-07-06 23:05:15 -07:00
Worktrunk Bot 33deef94c7 docs: fix stale marker config key and bare-repo test comment (#3066) 2026-06-13 07:41:38 -07:00
Maximilian Roos fd3ae5bff4 feat(plugin): optional branch name and single-route flow for /wt-switch-create (#3058)
The `/wt-switch-create` branch argument becomes optional (a name is
picked from the task when omitted), and the skill's workflow is rebuilt
from a guard-heavy three-route flow into one route plus one error-driven
fallback.

**The new flow**: pick a branch name if none was given, create the
worktree with `wt -C <repo> switch --create <branch> --no-cd
--format=json` in Bash (rerunning without `--create` when the user named
an existing branch), re-root the session with `EnterWorktree({path})`,
and on a graceful rejection (different repo, pinned cwd, nested session)
work in the worktree via absolute paths instead.

**Why the rewrite**: the old flow's guards encoded hypotheses about
Claude Code that turned out wrong or untested. The load-bearing
discoveries, each verified by binary inspection, official docs, or live
tests and documented in the new `skills/wt-switch-create/rationale.md`:
`EnterWorktree({path})` accepts worktrunk's sibling layout on first
entry; every rejection it can raise is side-effect-free, so
try-then-fallback replaces prediction; the previous
`EnterWorktree({name})`/hook route silently auto-removes a clean
worktree at session exit, which is wrong for durable worktrunk worktrees
(path-entered worktrees persist); and the "pinned cwd" the old guards
defended against was a misdiagnosis of the harness resetting `cd` that
leaves the session's working directories.

**Hook fix**: both `WorktreeCreate` and `WorktreeRemove` pipelines are
wrapped in `bash -c 'set -o pipefail; ...'`. Without it the trailing
`jq` exits 0 on empty input, so a `wt` failure surfaced as a
"successful" hook with an empty path. The wrapper is explicit `bash`
because hooks spawn via `/bin/sh -c`, where dash rejects `set -o
pipefail` fatally. `xargs -I{}` also fixes worktree paths containing
spaces. Tested end-to-end in both directions (success exits 0 with the
path; failure exits 1 with empty stdout).

Docs, README, and plugin CLAUDE.md are aligned with the new mechanism.
The agent-isolation use of the `WorktreeCreate` hook is unchanged.

Review trail: the design survived an evidence audit (every rationale
claim independently re-verified), a code review (bare directory-named
tokens no longer parse as the repo; mid-session moves now stash
uncommitted work across), and a level check that considered and rejected
fixing this in `wt` itself (an idempotent `--create` cannot encode who
chose the branch name, which determines the correct recovery).

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:01:13 -07:00
Worktrunk Bot a35dd7f598 fix(plugin): add waiting-for-input marker transitions for Claude hooks (#3023) 2026-06-10 00:56:34 -07:00
Adam Wróbel 6fd1bf4566 Fix Claude hooks for Fish shell users. (#2962) 2026-06-02 14:51:18 -07:00
Maximilian Roos 1cc89d172e docs(claude): slim root CLAUDE.md, relocate detail to sibling files (#2816)
The root `CLAUDE.md` loads into every conversation, so its ~510 lines
were paid on every session. This cuts it to ~190 lines without losing
any documented behavior or invariant.

Three things happened: derivable/stale content was removed (the JSON
Output Format pointer, just a redirect to `wt list --help`), prose was
tightened throughout, and task-specific detail was pushed down to the
sibling `CLAUDE.md` that loads where that work happens — the root keeps
a one-line pointer.

**Relocations** (substance preserved at the destination, root keeps a
pointer):

- Testing commands, the `mock-stub` filtered-run gotcha, Claude Code web
setup, the shell/PTY nextest SIGTTOU suspension, and codecov
investigation mechanics → `tests/CLAUDE.md`
- Doc sync taxonomy (the three categories + PRIMARY SOURCE),
three-context help-text authoring, the `.gitattributes`
`linguist-generated=false` exemption, config-TOML double-comment rule →
`docs/CLAUDE.md`
- Plugin layout, the Codex no-hooks re-enablement conditions, the
accepted `wt-switch-create` tradeoff → `plugins/worktrunk/CLAUDE.md`
(its back-pointers into the root were made self-contained so nothing
dangles)
- The add-a-CLI-command recipe → `src/commands/CLAUDE.md`

**Kept in the root, tightened but substance intact:** the governing
invariants — Data Safety, the Command Execution Principles
(`shell_exec::Cmd`, structured output, network policy, signal handling),
Project Commands Run Only After Approval (verbatim — the #2806 TOCTOU
fix depends on its wording), the codecov merge gate, Config Deprecation,
accessor naming, and the Worktree Model.

A third commit consolidates the doc-sync explanation in
`docs/CLAUDE.md`: the new "Doc sync taxonomy" section was overlapping
with the file's existing "Command documentation" area on "edit
`src/cli/mod.rs`" and `test_docs_are_in_sync`. Now the taxonomy is the
single entry point and "Command page generation" reads as the category-1
mechanism details.

Every CLAUDE.md heading quoted in a code comment is preserved verbatim
so the breadcrumbs still resolve under a case-sensitive grep: `Plugin
Layout` (`src/commands/config/codex.rs`), `Project Commands Run Only
After Approval` (`src/commands/hook_plan.rs`,
`src/commands/picker/mod.rs`), `Network Access`
(`src/git/repository/config.rs`), and `Signal Handling`
(`src/git/error.rs`, `src/commands/run_pipeline.rs`,
`src/commands/command_executor.rs`). The first push downcased the latter
two; the automated reviewer caught it and the second commit restored
Title Case across all Command Execution Principles subheadings.
`test_docs_are_in_sync` passes (docs-only change; no code touched).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:09:50 -07:00
Maximilian Roos d7e33efc6c refactor(plugin): relocate Gemini extension to repo root (#2807)
Follow-up from a full review of the plugin implementation.

**The finding:** Gemini hard-probes `${extensionPath}/{hooks,skills}/`
at the extension root with no manifest path indirection, and `gemini
extensions install owner/repo` reads `gemini-extension.json` only at the
clone root. The `plugins/gemini/` sibling dir fought that — it needed a
`skills` symlink and a byte-identical bundled `wt.sh` purely to survive
`gemini extensions install`'s `fs.cp`, and github-URL install was broken
entirely (manifest invisible under a subdir). One install-fragile
artifact (`wt.sh`) was bundled to solve this; the other (`skills`) was
left a symlink that the guard test actively pinned — so the extension
shipped no skills to real users.

**The fix:** treat Gemini's manifest as the loader-mandated root pointer
it is — the third tool pointer alongside Claude's
(`.claude-plugin/marketplace.json`) and Codex's
(`.agents/plugins/marketplace.json`). With the manifest at the repo
root, `${extensionPath}` is the repo root, so `${extensionPath}/skills/`
resolves to the real single-sourced repo-root `skills/` and the hooks
call the canonical `${extensionPath}/plugins/worktrunk/hooks/wt.sh` by
its real path. This is net deletion: the symlink, the duplicated
`wt.sh`, and its drift guard all go away, and native `gemini extensions
install owner/repo` now works.

Verified with `gemini extensions validate .` against gemini-cli 0.42. A
true end-to-end `gemini extensions install` was not exercised — it
blocks on an interactive trust prompt — so the native-install claim
rests on documented Gemini behaviour plus `validate`, not a live
install.

Also hardens `test_plugin_layout_is_consolidated` (two other review
findings):

- Every repo-root `skills/` subdir must be listed in the Claude
`plugin.json` `skills` array. Claude has no skill auto-discovery
(confirmed against the official plugin reference), so an unlisted skill
is silently invisible to Claude while Codex/Gemini pick it up — this
turns that latent drift into a build failure.
- All four manifest descriptions (Claude ×2, Codex, Gemini) must share
the canonical product opening sentence. Byte-identical across all four
is schema-impossible (Codex omits the activity clause, Gemini says
"extension"), but the shared stem is a meaningful cross-tool drift
guard.

`CLAUDE.md` and `plugins/worktrunk/CLAUDE.md` updated: the repo root now
carries one loader-mandated pointer per tool (three, not two); tree
diagram, path-resolution rules, and the `wt-switch-create`
shared-`skills/` tradeoff all extended to Gemini.

Full pre-merge gate green locally: 3745 tests, 0 skipped; all lints; no
snapshot churn.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:09:18 -07:00
Maximilian Roos d22917702e refactor(plugins): consolidate Claude + Codex into one payload dir (#2789)
The Claude plugin lived at the repo root (`.claude-plugin/`) while the
Codex plugin lived in `plugins/worktrunk/` — two homes for one logical
plugin, with the description string duplicated across both and drifting
independently. This collapses them into a single payload directory.

**What moved.** `git mv
.claude-plugin/{plugin.json,hooks/,CLAUDE.md,README.md} →
plugins/worktrunk/` (history-preserving renames). The repo root keeps
only the two marketplace pointers — Claude and Codex each hardcode their
marketplace path with no fallback, so two pointer files is the
irreducible floor. Both pointers' `source` now resolves to
`./plugins/worktrunk`. `.claude-plugin/` is now a single 2-line file.

**The load-bearing constraint** (verified live against claude-cli
2.1.x): for a *subdirectory* `source`, Claude expects `plugin.json` at
the plugin root **without** a `.claude-plugin/` wrapper — that wrapper
is marketplace-root-only. (First attempt with the wrapper failed `Plugin
not found`; the corrected layout installs cleanly.) Codex keeps its own
required `.codex-plugin/` wrapper. So inside `plugins/worktrunk/`:
`plugin.json` is Claude's, `.codex-plugin/plugin.json` is Codex's,
`hooks/` is Claude's, `skills → ../../skills` is shared.

**Path edits inside moved files:** `plugin.json` `hooks` →
`./hooks/hooks.json`; `hooks.json`
`${CLAUDE_PLUGIN_ROOT}/.claude-plugin/hooks/wt.sh` →
`${CLAUDE_PLUGIN_ROOT}/hooks/wt.sh`.

**Reviewer navigation:** `git log` shows the moves as renames.
`plugins/worktrunk/CLAUDE.md` was rewritten to document the unified
layout + per-tool path resolution; the repo `CLAUDE.md` "Plugin Layout"
section likewise. `tests/integration_tests/config_show.rs` adds
`test_plugin_layout_is_consolidated`, which locks the layout invariants
(`.claude-plugin/` is marketplace-only, Claude manifest at plugin root,
hooks relative to it) **and** asserts the duplicated Claude description
stays byte-identical across the marketplace pointer and the manifest —
that was the standing follow-up; JSON can't `include!`, so a drift test
is the right tool, not a generator.

**Verification:** Live end-to-end on both real CLIs, twice — once with
copied skill dirs, once mirroring the real tree *including the
`plugins/worktrunk/skills` symlink* (Claude's `skills` array resolves
through it). Full pre-merge gate green: 3720 passed, 0 skipped, all
pre-commit lints, no snapshot churn. No user-facing docs/help text
changed (the consolidation is repo-internal), so `test_docs_are_in_sync`
needed no resync.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:35:25 -07:00