diff --git a/.agents/skills/add-host-tool/SKILL.md b/.agents/skills/add-host-tool/SKILL.md new file mode 100644 index 00000000..22b10df5 --- /dev/null +++ b/.agents/skills/add-host-tool/SKILL.md @@ -0,0 +1,46 @@ +--- +name: add-host-tool +description: >- + Adds or changes a managed host tool across the ptr727/ProjectTemplate fleet contract, Linux and + Windows installers, platform documentation, and tests. Use this whenever adding, removing, + renaming, or changing the source, probe, version floor, install, report, upgrade, or dry-run + behavior of a tool in host-setup or spec/host-tools.json. Triggers even when the request names + only one platform, because a required fleet tool needs an executable remedy everywhere it + applies and native verification must stay on the platform being tested. +--- + +# Add Host Tool + +## Establish the Contract + +1. Read the issue and all follow-up comments before choosing a source or package identifier. +2. Add the tool to `spec/host-tools.json` in name order. +3. Use the executable's real version banner for the probe and pattern. +4. Set a floor only when it is measured or anchored to every supported distribution. +5. Provide `source` and executable `remedy` entries for every applicable platform. + +## Implement Each Platform + +- Keep the existing named-tool interface and default selection behavior. +- Prefer the distribution package when it meets the floor. +- Use the platform's established package manager and official package identifier. +- Keep install and upgrade idempotent. +- Before an apt-managed install, detect and remove an unowned downloaded copy that shadows it. +- Before a downloaded install, detect and remove a conflicting package-managed copy. +- Preserve report, list, explicit selection, install, upgrade, reinstall, and dry-run behavior. +- Do not test a Windows mutation on Linux or a Linux mutation on Windows. + +When a platform is unavailable, verify its registry and tests without claiming a native install. Hand off the exact native commands and expected observations to the operator. + +## Update the Complete Surface + +Update the platform installers, `spec/host-tools.json`, `docs/host-setup.md`, and the applicable platform READMEs. Update installer and host-gate tests for selection, reporting, installation, upgrade, and dry-run behavior. Sweep prose that describes tool sources or the managed set. + +## Verify + +1. Run the spec validator and the focused installer and host-gate tests. +2. Run the repository's formatting, lint, type, and test gates required by the changed files. +3. On the current native platform, exercise list and report first. +4. Exercise install and upgrade dry runs. +5. Apply the install, repeat it to prove idempotence, and run the host gate. +6. Record untested platforms explicitly and leave cross-platform verification open. diff --git a/.claude-plugin/fleet-skills/.claude-plugin/plugin.json b/.claude-plugin/fleet-skills/.claude-plugin/plugin.json index dbe5a08a..b37904ff 100644 --- a/.claude-plugin/fleet-skills/.claude-plugin/plugin.json +++ b/.claude-plugin/fleet-skills/.claude-plugin/plugin.json @@ -6,6 +6,7 @@ "name": "ptr727" }, "skills": [ + "./skills/add-host-tool", "./skills/agent-conduct", "./skills/audit-a-repo", "./skills/carried-instruction-file-guard", diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index b03fb574..5084f91e 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -cf555123c5d2982d +0209e62c88c2a759 diff --git a/.claude-plugin/fleet-skills/skills/add-host-tool/SKILL.md b/.claude-plugin/fleet-skills/skills/add-host-tool/SKILL.md new file mode 100644 index 00000000..22b10df5 --- /dev/null +++ b/.claude-plugin/fleet-skills/skills/add-host-tool/SKILL.md @@ -0,0 +1,46 @@ +--- +name: add-host-tool +description: >- + Adds or changes a managed host tool across the ptr727/ProjectTemplate fleet contract, Linux and + Windows installers, platform documentation, and tests. Use this whenever adding, removing, + renaming, or changing the source, probe, version floor, install, report, upgrade, or dry-run + behavior of a tool in host-setup or spec/host-tools.json. Triggers even when the request names + only one platform, because a required fleet tool needs an executable remedy everywhere it + applies and native verification must stay on the platform being tested. +--- + +# Add Host Tool + +## Establish the Contract + +1. Read the issue and all follow-up comments before choosing a source or package identifier. +2. Add the tool to `spec/host-tools.json` in name order. +3. Use the executable's real version banner for the probe and pattern. +4. Set a floor only when it is measured or anchored to every supported distribution. +5. Provide `source` and executable `remedy` entries for every applicable platform. + +## Implement Each Platform + +- Keep the existing named-tool interface and default selection behavior. +- Prefer the distribution package when it meets the floor. +- Use the platform's established package manager and official package identifier. +- Keep install and upgrade idempotent. +- Before an apt-managed install, detect and remove an unowned downloaded copy that shadows it. +- Before a downloaded install, detect and remove a conflicting package-managed copy. +- Preserve report, list, explicit selection, install, upgrade, reinstall, and dry-run behavior. +- Do not test a Windows mutation on Linux or a Linux mutation on Windows. + +When a platform is unavailable, verify its registry and tests without claiming a native install. Hand off the exact native commands and expected observations to the operator. + +## Update the Complete Surface + +Update the platform installers, `spec/host-tools.json`, `docs/host-setup.md`, and the applicable platform READMEs. Update installer and host-gate tests for selection, reporting, installation, upgrade, and dry-run behavior. Sweep prose that describes tool sources or the managed set. + +## Verify + +1. Run the spec validator and the focused installer and host-gate tests. +2. Run the repository's formatting, lint, type, and test gates required by the changed files. +3. On the current native platform, exercise list and report first. +4. Exercise install and upgrade dry runs. +5. Apply the install, repeat it to prove idempotence, and run the host gate. +6. Record untested platforms explicitly and leave cross-platform verification open. diff --git a/.github/skills/add-host-tool/SKILL.md b/.github/skills/add-host-tool/SKILL.md new file mode 100644 index 00000000..22b10df5 --- /dev/null +++ b/.github/skills/add-host-tool/SKILL.md @@ -0,0 +1,46 @@ +--- +name: add-host-tool +description: >- + Adds or changes a managed host tool across the ptr727/ProjectTemplate fleet contract, Linux and + Windows installers, platform documentation, and tests. Use this whenever adding, removing, + renaming, or changing the source, probe, version floor, install, report, upgrade, or dry-run + behavior of a tool in host-setup or spec/host-tools.json. Triggers even when the request names + only one platform, because a required fleet tool needs an executable remedy everywhere it + applies and native verification must stay on the platform being tested. +--- + +# Add Host Tool + +## Establish the Contract + +1. Read the issue and all follow-up comments before choosing a source or package identifier. +2. Add the tool to `spec/host-tools.json` in name order. +3. Use the executable's real version banner for the probe and pattern. +4. Set a floor only when it is measured or anchored to every supported distribution. +5. Provide `source` and executable `remedy` entries for every applicable platform. + +## Implement Each Platform + +- Keep the existing named-tool interface and default selection behavior. +- Prefer the distribution package when it meets the floor. +- Use the platform's established package manager and official package identifier. +- Keep install and upgrade idempotent. +- Before an apt-managed install, detect and remove an unowned downloaded copy that shadows it. +- Before a downloaded install, detect and remove a conflicting package-managed copy. +- Preserve report, list, explicit selection, install, upgrade, reinstall, and dry-run behavior. +- Do not test a Windows mutation on Linux or a Linux mutation on Windows. + +When a platform is unavailable, verify its registry and tests without claiming a native install. Hand off the exact native commands and expected observations to the operator. + +## Update the Complete Surface + +Update the platform installers, `spec/host-tools.json`, `docs/host-setup.md`, and the applicable platform READMEs. Update installer and host-gate tests for selection, reporting, installation, upgrade, and dry-run behavior. Sweep prose that describes tool sources or the managed set. + +## Verify + +1. Run the spec validator and the focused installer and host-gate tests. +2. Run the repository's formatting, lint, type, and test gates required by the changed files. +3. On the current native platform, exercise list and report first. +4. Exercise install and upgrade dry runs. +5. Apply the install, repeat it to prove idempotence, and run the host gate. +6. Record untested platforms explicitly and leave cross-platform verification open. diff --git a/AGENTS.md b/AGENTS.md index a77ca505..5620ea71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,3 +101,5 @@ Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md). R A row above with no Skill annotation is doc-only by decision, not by omission. A Skill surfaces rules at a trigger moment, and each unannotated section either binds always or carries no moment narrower than reading it: `Foundational Principles` is rationale read once rather than a procedure, `Repository Boundaries and Write Safety` and `Representative Data in Agent-Authored Text` are always-on law that must bind even when no Skill fires (the `gh-write-guard` hook and the host-wide instruction blocks the agent-safety installer maintains are their enforcement layer, and the one moment in the boundaries section narrow enough to surface, isolating into a worktree at task start, gets the `repo-worktree` Skill on top of that law rather than instead of it), and `Hub-Hosted Tooling`, `Supported Development Platforms`, `Devcontainer`, `Editor and Tasks`, `Repository Details`, and `Repository Layout` are short reference sections a task reads at the moment it touches their subject, each already routed to by the procedures and Skills that need it. Some of the rules above are also packaged as Claude Code / opencode / Codex Skills, hand-authored at `.agents/skills/` in the hub (not a repo-relative link here, since that path is hub-local and not carried into every fleet repo), so they surface automatically instead of needing to be re-read every session. `scripts/` is hub-hosted and reached rather than carried, per "Hub-Hosted Tooling", so run the installer from a hub checkout: `python3 scripts/skills_install.py` (or the `.sh`/`.ps1` wrapper) once per machine, from `github.com/ptr727/ProjectTemplate`, installs them for every repo touched from that machine. `python3 scripts/skills_install.py --report`, also from a hub checkout, says whether this machine is current. A rule that keeps needing to be restated is a sign the install is missing or stale, not that the rule does not exist. Keeping a repo's own carried `.github/copilot-instructions.md` in sync with the hub, without losing that repo's own "Disproved Claims" ledger entries in the process, is `copilot-instructions-keeper`, a skill about maintaining that file rather than a rule extracted from it, since the file itself is read directly by the Copilot bot and stays fully intact everywhere it is carried. Checking, from inside this repo's own session with no operator watching, whether this repo and this machine are actually current against the hub is `fleet-conformance-check`, new content rather than a rule extracted from a section, the counterpart to `resync-a-repo` that needs no standing hub checkout or named target beyond the repo the session is already in, even though its own check fetches a hub checkout to reach `scripts/skills_install.py`. Opening a pull request against a repository outside this fleet, one the maintainer does not control, follows a different workflow entirely, new content rather than a rule extracted from a section, packaged as `upstream-contribution-workflow` and independent of the target repo's own type or workflow model. Isolating a task into its own worktree before its first file edit, with the base-branch choice, the layout convention, and the cleanup mechanics, is `repo-worktree`, the task-start surface of the `Repository Boundaries and Write Safety` law, which keeps the rule. Creating, changing, or retiring one of these skills is itself packaged as `skill-lifecycle`, hub-context only, since `.agents/skills/` exists only in the hub and the generated plugin tree is never hand-edited. + +Adding or changing a managed host tool is packaged as `add-host-tool`. It keeps the cross-platform contract, installer, documentation, test, and native-verification surfaces together. diff --git a/docs/host-setup.md b/docs/host-setup.md index d28ad9ef..a84ffc06 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -20,6 +20,7 @@ This section is the **contract**: which tools a host needs and which repo proced | `gh` | the PR and review loop, `gh api` queries, `repo-config/configure.sh` | `gh --version` | **2.47.0**, measured | | Python 3 | `scripts/` and `spec/` (standard library only, no packages to install) | `python3 --version`, or `py -3 --version` on native Windows | **3.13**, target | | `jq` | the ruleset normalizer in `repo-config/configure.sh`, the ruleset diff in [`AUDIT.md`][audit] section 6, and payload regeneration | `jq --version` | **1.7**, target | +| Ripgrep | repository searches and file discovery by coding agents | `rg --version` | **13.0.0**, target | | `docker` | the four linters, which run as pinned images rather than local installs | `docker --version` | **29.6.2**, target | | `uv` / `uvx` | coverage runs, and the Python toolchain (`ruff`, `pyright` or `mypy`) in a Python repo | `uv --version` | **0.12.2**, target | @@ -45,6 +46,8 @@ Presence is the weaker half of this contract. Both host defects this fleet has a **The rest of the table takes the distribution's package, and one more does not.** `git` and the Python interpreter come from the distribution, because each keeps up well enough that a second source buys nothing and costs a repository to trust. `jq` is the same on a current Debian or Ubuntu, which carries a version at or above the floor, and the upstream release binary is the answer only where it does not. `uv` is published by its authors as a release archive and packaged by neither distribution, so upstream is the only source there is. Where a repository needs `node`, the distribution's package trails upstream by whole release lines, so it comes from the NodeSource repository on the line upstream currently carries as long term support. Where a repository needs `dotnet`, the distribution's feed is preferred where it carries an SDK and Microsoft's feed is the fallback, because mixing the two is what breaks a host rather than either one alone, and Microsoft's carries `amd64` only. Where a repository needs `pwsh`, that same feed is the only source, since no distribution packages it, and the tooling installs it as the `powershell` tool under `--optional`, with `pwsh` the command that tool provides. +Ripgrep also comes from the distribution, where every supported current release meets its floor. It provides `rg` search and `rg --files` discovery for coding agents. + Neither `node`, `dotnet`, nor `pwsh` is in the table above, deliberately: they serve the repositories that need them rather than the fleet contract, and a repository needing one declares it in a `host-tools.json` of its own, which [`scripts/host_gate.py`][host-gate] merges over this one. The merge tightens only, so a repository may raise a floor or add one and may not lower or remove one. **A host being stood up needs no Python.** The tooling under [`host-setup/`][host-setup-dir] is shell and PowerShell, deliberately, because requiring an interpreter to upgrade a package or install a tool would make the first step of standing a host up depend on the thing that step exists to provide. The Python floor above is a development requirement, meaning [`scripts/`][scripts-dir] and [`spec/`][spec-dir], and a host that only runs services never has to meet it. `bootstrap.sh` needs `curl` and `tar`, both of which a base install carries or can install without a network tool of its own. `bootstrap.ps1` needs only `tar.exe`, which has shipped with Windows since 1803, and installs its one further dependency, PowerShell 7, itself through `winget`. The one exception is the skills step at the end of a stand-up, which drives the Python installer in [`scripts/`][scripts-dir], and it runs last for exactly that reason: `install-tools` has provided the interpreter by then, and run alone on a host without one it stops and names the tools step as its prerequisite. diff --git a/host-setup/linux/README.md b/host-setup/linux/README.md index 99a439d0..98964a6b 100644 --- a/host-setup/linux/README.md +++ b/host-setup/linux/README.md @@ -35,7 +35,7 @@ Each script is LF with a shebang, and its executable bit is tracked in git. A fr The Windows registry has one source because `winget` tracks upstream. Here the distro package trails upstream on `gh`, on `node`, and on `uv`, so a tool comes from whichever source keeps up: -- **The distro**, for `git` and `python`, where apt's own package is current enough. +- **The distro**, for `git`, `python`, and Ripgrep, where apt's own package is current enough. - **An upstream apt repository**, for `gh`, `node`, `docker`, and `powershell`, and for `dotnet` as a fallback, where upstream publishes one. - **A released binary into `/usr/local/bin`**, for `jq`, `uv`, and `git-restore-mtime`, where upstream publishes no repository. diff --git a/host-setup/linux/install-tools.sh b/host-setup/linux/install-tools.sh index 605fe5db..b2f468fa 100755 --- a/host-setup/linux/install-tools.sh +++ b/host-setup/linux/install-tools.sh @@ -22,7 +22,7 @@ readonly SUDO_TIMESTAMP_TIMEOUT=60 # Managed tools, in dependency order: node asks jq to read the upstream release index. # The one optional member, powershell, joins the default selection only under --optional. -readonly TOOLS=(git gh jq git-restore-mtime node python uv docker dotnet powershell) +readonly TOOLS=(git gh jq git-restore-mtime node python ripgrep uv docker dotnet powershell) # Package sets. # The default set is what a tool needs to be useful, and the optional set is what is useful often enough to name but not always wanted, installed only with --optional. @@ -508,6 +508,37 @@ python_install() { apt_install "${packages[@]}" } +# --- ripgrep --- + +ripgrep_source() { printf 'distro'; } +ripgrep_version() { apt_installed_version ripgrep; } +ripgrep_target() { apt_candidate_version ripgrep; } + +ripgrep_download_path() { + local resolved + resolved=$(type -P rg 2> /dev/null || true) + [[ $resolved == /* ]] || return 0 + dpkg-query -S "$resolved" > /dev/null 2>&1 || printf '%s' "$resolved" +} + +ripgrep_remove_download() { + local resolved + while true; do + resolved=$(ripgrep_download_path) + [[ -n $resolved ]] || break + log "ripgrep: removing downloaded copy at $resolved before the distro package install" + confirm " Remove $resolved?" || die "Declined, downloaded Ripgrep left as it is" + run_root rm -f "$resolved" + [[ $DRY_RUN == true ]] && break + hash -r + done +} + +ripgrep_install() { + ripgrep_remove_download + apt_install ripgrep +} + # --- uv --- uv_source() { printf 'astral-sh/uv'; } @@ -870,6 +901,13 @@ tool_note() { note "python" "optional set not selected: ${PYTHON_OPTIONAL[*]}" fi ;; + ripgrep) + local resolved + resolved=$(ripgrep_download_path) + if [[ -n $resolved ]]; then + note "ripgrep" "$resolved is an unowned downloaded copy and an install or upgrade removes it before apt installs Ripgrep" + fi + ;; dotnet) local -a sdks=() readarray -t sdks < <(dotnet_sdk_packages) @@ -950,13 +988,17 @@ report() { done } -# Remove a copy of a managed tool found earlier on PATH than $BIN_DIR, so the managed copy is what PATH resolves to afterward. -# Only jq, uv, and git-restore-mtime install as loose binaries outside apt, and uv's companion uvx is unshadowed alongside it. +# Remove a copy of a managed tool found earlier on PATH than its managed destination, so the managed copy is what PATH resolves to afterward. +# Ripgrep migrates an unowned download to apt, while jq, uv, and git-restore-mtime install as loose binaries in $BIN_DIR. tool_unshadow() { local tool="$1" local -a names=() case "$tool" in jq | git-restore-mtime) names=("$tool") ;; + ripgrep) + ripgrep_remove_download + return 0 + ;; uv) names=(uv uvx) ;; *) return 0 ;; esac diff --git a/host-setup/windows/README.md b/host-setup/windows/README.md index 9861cd15..8de758b2 100644 --- a/host-setup/windows/README.md +++ b/host-setup/windows/README.md @@ -33,6 +33,8 @@ host-setup\windows\setup-wsl.ps1 -Status Every tool the contract names has a winget package, so nothing here carries a fallback. That is the whole difference from the Linux script, which needs three kinds of source because the distribution's package trails upstream on `gh`, on `node` and on `uv`. Where `winget` tracks upstream, the machinery that exists to work around a stale feed has nothing to do. +Ripgrep uses the upstream project's documented `BurntSushi.ripgrep.MSVC` package. It provides the `rg` command used for repository searches and `rg --files` discovery. + A tool that turns out to have no winget package is a finding to raise rather than a second source to add quietly, because the moment one tool comes from somewhere else this directory stops being one program and becomes two. ## Elevation and Scope diff --git a/host-setup/windows/install-tools.ps1 b/host-setup/windows/install-tools.ps1 index 1fb7c2b0..76c75bd1 100644 --- a/host-setup/windows/install-tools.ps1 +++ b/host-setup/windows/install-tools.ps1 @@ -363,6 +363,7 @@ $TOOLS = @( @{ Name = 'gh'; Package = 'GitHub.cli'; Probe = 'gh'; Optional = @(); Family = '' } @{ Name = 'jq'; Package = 'jqlang.jq'; Probe = 'jq'; Optional = @(); Family = '' } @{ Name = 'python'; Package = 'Python.Python.3.13'; Probe = 'py'; Optional = @(); Family = '' } + @{ Name = 'ripgrep'; Package = 'BurntSushi.ripgrep.MSVC'; Probe = 'rg'; Optional = @(); Family = '' } @{ Name = 'uv'; Package = 'astral-sh.uv'; Probe = 'uv'; Optional = @(); Family = '' } @{ Name = 'docker'; Package = 'Docker.DockerDesktop'; Probe = 'docker'; Optional = @(); Family = '' } @{ Name = 'node'; Package = 'OpenJS.NodeJS.LTS'; Probe = 'node'; Optional = @(); Family = 'OpenJS.NodeJS' } diff --git a/scripts/tests/test_bootstrap.py b/scripts/tests/test_bootstrap.py index a3708705..2eecd7c3 100755 --- a/scripts/tests/test_bootstrap.py +++ b/scripts/tests/test_bootstrap.py @@ -222,6 +222,21 @@ def test_every_required_windows_tool_is_installable(self) -> None: """A tool the spec requires on Windows is one the tooling can provide, or a recorded exception.""" self._assert_coverage("windows", self.declared_windows_tools(), "install-tools.ps1") + def test_ripgrep_uses_the_platform_package_managers(self) -> None: + """Ripgrep uses apt on Linux and the upstream project's documented winget package.""" + linux = (LINUX / "install-tools.sh").read_text(encoding="utf-8") + windows = (WINDOWS / "install-tools.ps1").read_text(encoding="utf-8") + install = re.search( + r"ripgrep_install\(\) \{(?P.*?)^\}", linux, re.MULTILINE | re.DOTALL + ) + self.assertIsNotNone(install) + body = install.group("body") if install else "" + self.assertLess(body.index("ripgrep_remove_download"), body.index("apt_install ripgrep")) + self.assertRegex( + windows, + r"Name = 'ripgrep'; Package = 'BurntSushi\.ripgrep\.MSVC'; Probe = 'rg'", + ) + def test_every_declared_floor_carries_a_total_remedy_mapping(self) -> None: """Each floored tool names a runnable remedy on every platform, or carries a recorded exception. diff --git a/scripts/tests/test_host_gate.py b/scripts/tests/test_host_gate.py index e138074b..4206eab6 100755 --- a/scripts/tests/test_host_gate.py +++ b/scripts/tests/test_host_gate.py @@ -784,7 +784,10 @@ def test_the_declared_floors_are_the_ones_with_a_stated_reason(self): one in the data, which is what caught the python3 floor being added without this line. """ floors = {t["name"] for t in self.data["tools"] if t["minimum"] is not None} - self.assertEqual(floors, {"docker", "gh", "git-restore-mtime", "jq", "python3", "uv"}) + self.assertEqual( + floors, + {"docker", "gh", "git-restore-mtime", "jq", "python3", "ripgrep", "uv"}, + ) def test_the_contract_table_carries_every_declared_floor(self): """docs/host-setup.md restates the floors, so the doc goes stale the moment the data moves. @@ -852,7 +855,12 @@ def test_a_target_floor_says_so_rather_than_implying_a_defect(self): fails here instead of reading as measured. Each one is named individually, since a check that only counted them would pass on a wrong substitution. """ - targets = {"docker": "29.6.2", "jq": "1.7", "python3": "3.13"} + targets = { + "docker": "29.6.2", + "jq": "1.7", + "python3": "3.13", + "ripgrep": "13.0.0", + } by_name = {t["name"]: t for t in self.data["tools"]} for name, floor in targets.items(): entry = by_name[name] diff --git a/spec/host-tools.json b/spec/host-tools.json index 187e0a9a..61d40303 100644 --- a/spec/host-tools.json +++ b/spec/host-tools.json @@ -99,6 +99,24 @@ "windows": "host-setup/windows/install-tools.ps1 -Upgrade python" } }, + { + "name": "ripgrep", + "required": true, + "probes": [["rg", "--version"]], + "pattern": "ripgrep (\\d+(?:\\.\\d+)*)", + "minimum": "13.0.0", + "why": "Used for repository searches by the agent instructions, including file discovery through rg --files, so a host without it forces every search onto slower fallback tools. The floor is a target rather than a measured breakage. Debian 12 provides 13.0.0, so every supported current Debian and Ubuntu distribution can satisfy it from its own package. Issue #800 compared Debian 13's 14.1.1 package with upstream 15.2.0. That upstream release improves large-tree traversal, respects two Git configuration environment variables, and fixes ignore matching across multiple directory arguments. None is required for the fleet's ordinary recursive search and file discovery, so the distribution package is adequate without matching the newest upstream release. A host below the floor is unverified rather than known broken.", + "source": { + "linux": "The distribution package, which meets the floor on supported current Debian and Ubuntu releases.", + "macos": "Homebrew, which tracks upstream releases.", + "windows": "winget (BurntSushi.ripgrep.MSVC), the upstream project's documented Windows package." + }, + "remedy": { + "linux": "host-setup/linux/install-tools.sh --upgrade ripgrep", + "macos": "brew upgrade ripgrep", + "windows": "host-setup/windows/install-tools.ps1 -Upgrade ripgrep" + } + }, { "name": "uv", "required": true,