# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning].

[Keep a Changelog]: https://keepachangelog.com/en/1.1.0/
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html

## [Unreleased]

### Post-release checklist

- [ ] Move completed `Unreleased` items into a new version section.
- [ ] Update the `[Unreleased]` compare link to the new tag.
- [ ] Create and push a signed `vX.Y.Z` tag from `master`.

## [0.7.1] - 2026-05-10

### Fixed

- Turborepo configs resolve under both `turbo.json` and `turbo.jsonc`
  filenames, and either is parsed as JSONC (line/block comments and
  trailing commas), matching Turborepo v2's own parser. Detection
  previously hardcoded `turbo.json` and the parser was strict JSON, so
  `.jsonc` files were invisible to detection and any JSONC syntax
  surfaced parse errors. The qualified-task syntax also accepts
  `turbo.jsonc:task` (and `deno.jsonc:task`, fixed in the same line for
  parity). Fixes #10.
- Root Tasks in `turbo.json` — entries written with the `//#name`
  prefix, invoked via `turbo run name` against the workspace root —
  now surface in `runner list` under their bare name. Workspace-scoped
  entries (`pkg#task`) remain filtered, and the result set is
  deduplicated when both `name` and `//#name` are defined. Fixes #11.

### Changed

- `crates-release` CI workflow uses crates.io trusted publishing via
  OIDC. Replaces the long-lived `CARGO_REGISTRY_TOKEN` secret with a
  short-lived token minted per run by `rust-lang/crates-io-auth-action`;
  the secret-presence preflight is gone.
- README and the landing page promote `cargo install runner-run` (from
  crates.io) as the primary cargo install command, with the git and
  local-checkout forms remaining as fallbacks for unreleased commits
  and development work.
- README adds a crates.io shields badge alongside the existing npm
  one; the landing page footer adds crates.io and npm registry links
  next to the source and changelog references.

## [0.7.0] - 2026-05-10

### Added

- crates.io publishing: new `crates-release` workflow publishes the
  crate to crates.io when a maintainer publishes the GitHub release
  that `release.yml` cuts as a draft. Verifies the tag matches
  `Cargo.toml`, runs `cargo publish --locked --dry-run` first, then
  publishes via `CARGO_REGISTRY_TOKEN` under the `crates-io`
  environment. `workflow_dispatch` is preserved for manual republishes.

### Changed

- Renamed the published package from `runner` to `runner-run` (the
  bare `runner` name is taken on crates.io). The library crate name
  is pinned to `runner` via `[lib]` so existing `runner::…` imports
  in `src/main.rs` and `src/bin/run.rs` keep working unchanged.
- GitHub composite action: `uses: kjanat/runner@vX.Y.Z` installs the
  `runner` and `run` binaries on PATH in CI. Pinned tag refs make zero
  API calls (tag resolved from `github.action_ref`); `version: latest`
  triggers a single `releases/latest` lookup. Parallel `curl` of
  archive + sha256 with `--retry-all-errors --retry 5`, sha256
  verification before extract, `actions/cache@v4` keyed on `tag +
  triple` (cache hits ~250 ms, cold install 600–900 ms), and a
  `runner --version` smoke test on every install so a bad cache or
  missing asset surfaces here, not at the consumer's first task.
- Cargo aliases as a `runner` task source. The hierarchical
  `.cargo/config.toml` chain (cwd up to filesystem root, then
  `$CARGO_HOME/config{,.toml}`) is merged with cargo's precedence
  rules, recursive alias chains are expanded so `runner list` shows
  the fully-resolved command (`l → clippy --all-targets --all-features
  -- -D warnings`, `recursive_example → run --release --example
  recursions`), and built-ins (`b/c/d/t/r/rm`) always surface even in
  projects without a user config. User attempts to redefine a built-in
  are silently ignored to match cargo's own rule. `runner run <alias>`
  shells out to `cargo <alias> <args...>` so cargo's runtime resolution
  stays authoritative.
- Landing page at <https://runner.kjanat.com>, deployed to Cloudflare
  Workers Assets from `site/`. Single static page, dark mode via
  `prefers-color-scheme`, click-to-copy install commands with polite
  ARIA live-region announcements, tab-completion section, custom 404
  styled as a fake `runner <path>` error line. Page weight squeezed
  under TCP IW10 (~14.5 KB uncompressed, ~3.6 KB brotli) so the whole
  first response lands in a single round-trip; `_headers` ships
  strict CSP, HSTS, and edge cache.
- Templated site build (`site/build.ts` + `site/dev.ts`): Bun bundles
  `index.html` / `404.html` from `src/` into `dist/` and substitutes
  `{{version}}`, `{{repo}}`, `{{authorName}}` from `Cargo.toml`, so
  the site and the crate share one source of truth for metadata.
  `dev.ts` serves `dist/`, watches `src/` / `public/` / `Cargo.toml`
  with an 80 ms debounce, and injects a WebSocket live-reload snippet
  into served HTML.
- `site/build.ts` returns the emitted file list with bytes; the CLI
  prints a sorted `raw / gzip / br` table at quality 9/11 to mirror
  what a CDN actually serves, surfacing budget regressions next to
  the build. HTML post-processing reads from `Bun.build` outputs in
  memory instead of re-reading `dist/`, and `copyTree` returns the
  bytes it copied so `public/` files land in the same `DistFile[]`
  the summary walks.
- `BuildOptions.dir` (`"relative" | "full"`) toggles the size-summary
  path column between `dist/`-relative and absolute, surfaced via the
  `FULL` env in the local build script. Public files respect the
  toggle too so the rendered table is consistent across sources.
- GitHub Pages-aware `publicPath`: under GitHub Actions the asset
  prefix derives to `https://<owner>.github.io/<repo>/` from
  `GITHUB_REPOSITORY`, so PR previews of forks load their own assets
  without a config flag. `PUBLIC_PATH` env still overrides everything.
- `CF_BEACON_TOKEN` env overrides the inlined Cloudflare Web
  Analytics token; the literal stays as a fallback so production
  builds without the env still report correctly.
- External sourcemaps emit when `SENTRY_DSN` is set; otherwise the
  build skips them entirely so the published site stays
  one-round-trip-sized for end users.
- README links the landing page and npm package, and adds shields.io
  badges for the `runner-run` npm version and the MIT licence.

### Changed

- Cloudflare Web Analytics beacon now only injects in CI / GitHub
  Actions builds. Local `bun run build` and library imports leave the
  snippet out so dev previews don't phone home, and a missing
  `</body>` warns instead of throwing so partial HTML fragments don't
  fail the build.
- `npm/scripts/build-packages.ts` swaps the deprecated
  `cargo read-manifest` for
  `cargo metadata --no-deps --format-version 1`, picking the
  workspace's `workspace_default_members[0]` by id (falls back to the
  first package for non-virtual single-member workspaces). `maxBuffer`
  bumped to 64 MiB so large workspaces don't trip Node's 1 MiB
  default.
- `.github/scripts/publish/npm.sh` derives `REQUIRED_PLATFORMS` and
  `OPTIONAL_PLATFORMS` from `npm/targets.json` at runtime via `jq`
  instead of hardcoding two parallel lists; `npm-release.yml`
  sparse-checkout adds `npm/targets.json` so the script can read it.
  Optional == `experimental: true`, matching the workflow's existing
  `continue-on-error` semantic. One source of truth for the platform
  matrix.
- Drop the unused `NODE_AUTH_TOKEN` env from the npm publish step;
  auth flows through the OIDC token via `id-token: write` and
  `npm publish --provenance`, not a long-lived `NPM_TOKEN`.
- Move the landing-page primary domain from `runner.kjanat.com` to
  `runner.kjanat.dev` across the root README, site docs, site package
  metadata, and the page canonical URL so published links and metadata
  point at the new host.
- Add `runner.kjanat.dev` as a Cloudflare custom-domain route while
  keeping the existing `.com` route active during the transition.
- Reflow the npm facade README intro for cleaner package-page rendering.
- Set `Cargo.toml` `[package].homepage` and `npm/facade/package.json`
  `homepage` to `https://runner.kjanat.dev` (was the GitHub README
  URL), so the published crate and npm package surface the landing
  page on registry pages.

### Fixed

- `site/build.ts` `publicPath` precedence: the original
  `env["PUBLIC_PATH"] || isCI ? X : Y` parsed as
  `(... || ...) ? X : Y`, so a literal `PUBLIC_PATH` value never
  reached `Bun.build` — it acted as a boolean toggle. The hardcoded
  `runner.kjanat.com/` fallback also leaked into Cloudflare Workers
  preview deploys (`*.workers.dev`) and tripped CSP `'self'`,
  blocking every asset on every PR preview. Replaced with
  `env["PUBLIC_PATH"] || githubPagesUrl() || "/"`: explicit override
  wins, GitHub Pages still gets its `/<repo>/` prefix, everything
  else stays same-origin.
- `public/_headers` drops the dead `/favicon.ico` Content-Type
  override block; the icon ships as an SVG via `<link rel="icon">`,
  so nothing routes through `/favicon.ico` to need it.
- `.github/scripts/build/package-release-asset.sh` writes checksum
  files as `<basename>.sha256` (not `<basename>.tar.gz.sha256`),
  matching `taiki-e/upload-rust-binary-action`'s convention and what
  `verify-checksum.sh` enforces — the previous mismatch would have
  broken the npm pipeline's checksum verification on release.
- `npm/scripts/build-packages.ts`: `Target.build` union now covers
  all five schema enum values (previously only `cargo` | `cross`,
  missing the three variants added when the BSD build paths landed)
  so type narrowing matches reality and a stray build-tool name
  fails the build instead of silently shipping.
- Remove the stale `openbsd-x64` entry from `npm.sh`'s
  `OPTIONAL_PLATFORMS` and the matching matrix-gen comments in
  `release.yml` / `release-dryrun.yml`; the openbsd build path was
  scrapped earlier and the residue would have failed
  `optionalDependencies` validation if the platform was ever
  re-expected.
- Tab completion for turborepo monorepos no longer triple-emits
  the same task. A `package.json` script is classified as a turbo
  passthrough at detection time when its command body literally
  invokes `turbo run <name>` (or the shorthand `turbo <name>`) for
  a same-named target, optionally followed by flag tokens
  (`--filter web`, `--concurrency=4`) or — after a bare `--`
  end-of-options separator (POSIX/getopt convention) — args
  forwarded to the underlying task; the full bash control set
  (`&&`, `||`, `;`, `;;`, `;&`, `;;&`, `|`, `|&`, `&`, `!`, `{`,
  `}`, `(`, `)`), fd-style redirects (bare `>`/`<`/`>>`/`<<<`,
  combined-fd `&>`/`>&`, fd-prefixed `2>`, composite `2>&1`,
  `2>/dev/null`, `&>file.log`), shell expansion (parameter
  `$X`/`${X}`/`${X:-def}`/`${X//a/b}`, special vars `$@`/`$*`/
  `$#`/`$?`, command substitution `$(cmd)` and backtick
  `` `cmd` ``, arithmetic `$((expr))`, double-quoted forms with
  embedded expansion `"${X}"`) — including those positioned after
  a value-expecting flag or after `--` — and extra positional
  targets all reject the match so scripts that do real work
  beyond dispatching to turbo stay visible. Only thin passthroughs
  are dropped from completion when a same-named `turbo.json` task
  also exists. Real scripts like `"build": "vite build"` keep
  their qualified form even when they happen to share a name with
  a turbo task. `runner list` still surfaces both sources for
  transparency, `runner build` already dispatched through turbo
  per `source_priority`, and a third source (e.g. Makefile) keeps
  its qualified form alongside `turbo.json:build` for
  disambiguation.

## [0.6.1] - 2026-05-08

### Added

- GitHub composite action: `uses: kjanat/runner@vX.Y.Z` installs the
  `runner` and `run` binaries on PATH in CI. Pinned tag refs make zero
  API calls (tag resolved from `github.action_ref`); `version: latest`
  triggers a single `releases/latest` lookup. Parallel `curl` of
  archive + sha256 with `--retry-all-errors --retry 5`, sha256
  verification before extract, `actions/cache@v4` keyed on `tag +
  triple` (cache hits ~250 ms, cold install 600–900 ms), and a
  `runner --version` smoke test on every install so a bad cache or
  missing asset surfaces here, not at the consumer's first task.
- Cargo aliases as a `runner` task source. The hierarchical
  `.cargo/config.toml` chain (cwd up to filesystem root, then
  `$CARGO_HOME/config{,.toml}`) is merged with cargo's precedence
  rules, recursive alias chains are expanded so `runner list` shows
  the fully-resolved command (`l → clippy --all-targets --all-features
  -- -D warnings`, `recursive_example → run --release --example
  recursions`), and built-ins (`b/c/d/t/r/rm`) always surface even in
  projects without a user config. User attempts to redefine a built-in
  are silently ignored to match cargo's own rule. `runner run <alias>`
  shells out to `cargo <alias> <args...>` so cargo's runtime resolution
  stays authoritative.
- Landing page at <https://runner.kjanat.com>, deployed to Cloudflare
  Workers Assets from `site/`. Single static page, dark mode via
  `prefers-color-scheme`, click-to-copy install commands with polite
  ARIA live-region announcements, tab-completion section, custom 404
  styled as a fake `runner <path>` error line. Page weight squeezed
  under TCP IW10 (~14.5 KB uncompressed, ~3.6 KB brotli) so the whole
  first response lands in a single round-trip; `_headers` ships
  strict CSP, HSTS, and edge cache.
- Templated site build (`site/build.ts` + `site/dev.ts`): Bun bundles
  `index.html` / `404.html` from `src/` into `dist/` and substitutes
  `{{version}}`, `{{repo}}`, `{{authorName}}` from `Cargo.toml`, so
  the site and the crate share one source of truth for metadata.
  `dev.ts` serves `dist/`, watches `src/` / `public/` / `Cargo.toml`
  with an 80 ms debounce, and injects a WebSocket live-reload snippet
  into served HTML.
- `site/build.ts` returns the emitted file list with bytes; the CLI
  prints a sorted `raw / gzip / br` table at quality 9/11 to mirror
  what a CDN actually serves, surfacing budget regressions next to
  the build. HTML post-processing reads from `Bun.build` outputs in
  memory instead of re-reading `dist/`, and `copyTree` returns the
  bytes it copied so `public/` files land in the same `DistFile[]`
  the summary walks.
- `BuildOptions.dir` (`"relative" | "full"`) toggles the size-summary
  path column between `dist/`-relative and absolute, surfaced via the
  `FULL` env in the local build script. Public files respect the
  toggle too so the rendered table is consistent across sources.
- GitHub Pages-aware `publicPath`: under GitHub Actions the asset
  prefix derives to `https://<owner>.github.io/<repo>/` from
  `GITHUB_REPOSITORY`, so PR previews of forks load their own assets
  without a config flag. `PUBLIC_PATH` env still overrides everything.
- `CF_BEACON_TOKEN` env overrides the inlined Cloudflare Web
  Analytics token; the literal stays as a fallback so production
  builds without the env still report correctly.
- External sourcemaps emit when `SENTRY_DSN` is set; otherwise the
  build skips them entirely so the published site stays
  one-round-trip-sized for end users.
- README links the landing page and npm package, and adds shields.io
  badges for the `runner-run` npm version and the MIT licence.

### Changed

- Cloudflare Web Analytics beacon now only injects in CI / GitHub
  Actions builds. Local `bun run build` and library imports leave the
  snippet out so dev previews don't phone home, and a missing
  `</body>` warns instead of throwing so partial HTML fragments don't
  fail the build.
- `npm/scripts/build-packages.ts` swaps the deprecated
  `cargo read-manifest` for
  `cargo metadata --no-deps --format-version 1`, picking the
  workspace's `workspace_default_members[0]` by id (falls back to the
  first package for non-virtual single-member workspaces). `maxBuffer`
  bumped to 64 MiB so large workspaces don't trip Node's 1 MiB
  default.
- `.github/scripts/publish/npm.sh` derives `REQUIRED_PLATFORMS` and
  `OPTIONAL_PLATFORMS` from `npm/targets.json` at runtime via `jq`
  instead of hardcoding two parallel lists; `npm-release.yml`
  sparse-checkout adds `npm/targets.json` so the script can read it.
  Optional == `experimental: true`, matching the workflow's existing
  `continue-on-error` semantic. One source of truth for the platform
  matrix.
- Drop the unused `NODE_AUTH_TOKEN` env from the npm publish step;
  auth flows through the OIDC token via `id-token: write` and
  `npm publish --provenance`, not a long-lived `NPM_TOKEN`.

### Fixed

- `site/build.ts` `publicPath` precedence: the original
  `env["PUBLIC_PATH"] || isCI ? X : Y` parsed as
  `(... || ...) ? X : Y`, so a literal `PUBLIC_PATH` value never
  reached `Bun.build` — it acted as a boolean toggle. The hardcoded
  `runner.kjanat.com/` fallback also leaked into Cloudflare Workers
  preview deploys (`*.workers.dev`) and tripped CSP `'self'`,
  blocking every asset on every PR preview. Replaced with
  `env["PUBLIC_PATH"] || githubPagesUrl() || "/"`: explicit override
  wins, GitHub Pages still gets its `/<repo>/` prefix, everything
  else stays same-origin.
- `public/_headers` drops the dead `/favicon.ico` Content-Type
  override block; the icon ships as an SVG via `<link rel="icon">`,
  so nothing routes through `/favicon.ico` to need it.
- `.github/scripts/build/package-release-asset.sh` writes checksum
  files as `<basename>.sha256` (not `<basename>.tar.gz.sha256`),
  matching `taiki-e/upload-rust-binary-action`'s convention and what
  `verify-checksum.sh` enforces — the previous mismatch would have
  broken the npm pipeline's checksum verification on release.
- `npm/scripts/build-packages.ts`: `Target.build` union now covers
  all five schema enum values (previously only `cargo` | `cross`,
  missing the three variants added when the BSD build paths landed)
  so type narrowing matches reality and a stray build-tool name
  fails the build instead of silently shipping.
- Remove the stale `openbsd-x64` entry from `npm.sh`'s
  `OPTIONAL_PLATFORMS` and the matching matrix-gen comments in
  `release.yml` / `release-dryrun.yml`; the openbsd build path was
  scrapped earlier and the residue would have failed
  `optionalDependencies` validation if the platform was ever
  re-expected.
- Tab completion for turborepo monorepos no longer triple-emits
  the same task. A `package.json` script is classified as a turbo
  passthrough at detection time when its command body literally
  invokes `turbo run <name>` (or the shorthand `turbo <name>`) for
  a same-named target, optionally followed by flag tokens
  (`--filter web`, `--concurrency=4`) or — after a bare `--`
  end-of-options separator (POSIX/getopt convention) — args
  forwarded to the underlying task; the full bash control set
  (`&&`, `||`, `;`, `;;`, `;&`, `;;&`, `|`, `|&`, `&`, `!`, `{`,
  `}`, `(`, `)`), fd-style redirects (bare `>`/`<`/`>>`/`<<<`,
  combined-fd `&>`/`>&`, fd-prefixed `2>`, composite `2>&1`,
  `2>/dev/null`, `&>file.log`), shell expansion (parameter
  `$X`/`${X}`/`${X:-def}`/`${X//a/b}`, special vars `$@`/`$*`/
  `$#`/`$?`, command substitution `$(cmd)` and backtick
  `` `cmd` ``, arithmetic `$((expr))`, double-quoted forms with
  embedded expansion `"${X}"`) — including those positioned after
  a value-expecting flag or after `--` — and extra positional
  targets all reject the match so scripts that do real work
  beyond dispatching to turbo stay visible. Only thin passthroughs
  are dropped from completion when a same-named `turbo.json` task
  also exists. Real scripts like `"build": "vite build"` keep
  their qualified form even when they happen to share a name with
  a turbo task. `runner list` still surfaces both sources for
  transparency, `runner build` already dispatched through turbo
  per `source_priority`, and a third source (e.g. Makefile) keeps
  its qualified form alongside `turbo.json:build` for
  disambiguation.

## [0.6.0] - 2026-05-05

### Added

- npm distribution: install prebuilt binaries via `npm install -g
  runner-run` (or `pnpm`/`yarn`/`bun`). The façade package
  (`runner-run`) declares one `@runner-run/<platform>-<arch>[-<libc>]`
  package per supported target in `optionalDependencies`; npm/pnpm/yarn
  filter at install time using each sub-package's `os` / `cpu` /
  `libc` fields, so only the matching binary is fetched. No
  `postinstall` script and no network access during install. Façade
  shims (`bin/runner.cjs`, `bin/run.cjs`) resolve the platform
  sub-package via `require.resolve()` at runtime through
  `lib/resolve.cjs` and `lib/launch.cjs`, with helpful diagnostics when
  no matching sub-package is installed.
- Release matrix expanded from Linux musl x86_64/aarch64 to 13 targets
  across Linux (gnu/musl × x64/arm64 + armv7), macOS (x64, arm64),
  Windows (x64, arm64, ia32), FreeBSD (x64, arm64), and NetBSD x64.
  Tier-3 BSD targets are marked `experimental: true` and do not block
  the release. Per-target runner / build-tool selection is data-driven
  from `npm/targets.json` (validated by `npm/targets.schema.json`).
- New `npm-release` workflow downloads the GitHub Release tarballs,
  verifies SHA-256 checksums, generates per-platform packages from
  `npm/targets.json`, and publishes to npm with provenance, optional
  dry-run, and configurable dist-tag.
- `build.rs` build script reads `[[package.metadata.authors]]` from
  `Cargo.toml` and exposes the primary author as compile-time env vars
  `RUNNER_AUTHOR_NAME` (always) and `RUNNER_AUTHOR_EMAIL` (when set),
  consumed by the help byline via `env!` / `option_env!`.
- Pin Rust toolchain via `rust-toolchain.toml` (channel `1.95`,
  components `rustfmt` / `clippy` / `rust-analyzer`, profile
  `minimal`).
- Add `justfile` with developer recipes for building both bins,
  generating per-target npm packages (`build-packages`), and
  end-to-end-testing the façade resolution against the host triple
  (`test-release`).
- README documents the npm install path and the façade pattern
  (per-platform sub-package via `optionalDependencies`, install-time
  filtering, no postinstall, no network).
- `.github/scripts/build/` helpers: `build-npm-packages.sh`,
  `derive-dist-dry.sh`, `download-release-archives.sh`,
  `verify-checksum.sh`, plus `.github/scripts/publish/npm.sh` for the
  publish path.

### Changed

- Move authors metadata from `package.authors` to a structured
  `[[package.metadata.authors]]` table (with `name` / `email` fields)
  consumed by the new build script; `src/lib.rs` drops the runtime
  `primary_author` / `authors` regex-style parser in favour of
  `env!("RUNNER_AUTHOR_NAME")` / `option_env!("RUNNER_AUTHOR_EMAIL")`.
  `help_byline(stdout_is_terminal: bool) -> String` replaces the prior
  `Option<String>`-returning helper and is now part of the public API
  along with `requests_version`.
- **Breaking (Cargo features):** rename feature `run-alias` → `run`;
  the `run` binary's `required-features` follows. Builds passing
  `--features run-alias` no longer enable the alias.
- Bump MSRV from `1.88` to `1.95` (matches the pinned toolchain).
- Add `build = "build.rs"` and a `[package.metadata.npm]` block
  (`name`, `subpkgscope`, `bugs`, `repository`, `engines`) consumed by
  the npm build pipeline so target naming / scope live in one place.
- Promote `colored`, `json5`, `serde_json`, `shlex`, and `yaml-rust2`
  into a single `[dependencies]` table; add `serde` + `toml` as
  `[build-dependencies]` for the build script.
- Migrate npm build/publish scripts from `.mjs` to TypeScript
  (`npm/scripts/build-packages.ts`, `npm/scripts/publish.ts`); add
  `tsconfig.json`. Build-script type narrowing (`narrowRepository` /
  `narrowBugs` / `narrowAuthor`) now throws on wrong-typed optional
  fields instead of silently dropping them, so Cargo metadata drift
  fails the build instead of shipping a façade with missing fields.
- Release workflow archive layout updated to package per-target
  artifacts consumable by the npm build, and adds an experimental flag
  so tier-3 BSD targets do not block a release.
- Editor / formatting config: `.dprint.json` plugin update,
  `.gitattributes` added for line-ending consistency, `.gitignore`
  ignores generated npm artefacts, `.zed/settings.json` checked in.

### Fixed

- Help-byline rendering no longer depends on parsing
  `clap::crate_authors!()` at runtime; the email-aware OSC-8 hyperlink
  path is driven by `option_env!("RUNNER_AUTHOR_EMAIL")` set at compile
  time, removing the runtime string-split fallback.

### Security

- Harden the npm release pipeline with five fail-loud input-validation
  guards (commit `b404098`):
  - `derive-dist-dry.sh` validates `INPUT_DIST_TAG` against
    `^[A-Za-z][A-Za-z0-9._-]*$` so a malformed override cannot smuggle
    flag-like or whitespace values into `npm publish --tag`, and
    normalises `INPUT_DRY_RUN` to strict `true` / `false` (previously
    `True` / `1` / `yes` fell through as `dry_run=false`, i.e. a real
    publish disguised as a dry run).
  - `release.yml` smoke-tests the packaged `linux-x64-gnu` binary with
    `--version` before uploading the `npm/dist` artefact, catching a
    broken bin at build time instead of as `ENOENT` post-publish.
  - `npm.sh` validates `optionalDependencies` in `publish_allowed`:
    the façade must list every required platform under the scope at
    exactly `EXPECTED_VERSION`, and platform sub-packages must declare
    none — closing a vector where a tampered platform package could
    smuggle attacker-controlled transitive deps.
  - `npm view` and `npm publish` are wrapped in `timeout 120s` with
    explicit `124` handling so a hung registry cannot burn the full
    job budget.

## [0.5.0] - 2026-04-21

### Added

- Detect justfile aliases (`alias b := build`) and surface them as
  first-class tasks in `runner list` and `runner <alias>`. Private
  aliases (prefixed `_`, tagged `[private]`, or pointing at a private
  recipe) are hidden. Works via `just --dump-format json` when `just`
  is on PATH and via the regex fallback parser otherwise.
- Render justfile aliases distinctly from recipes in `runner list` and
  shell tab completions: aliases appear under a dedicated
  `justfile (aliases)` group with `name → target` annotations instead
  of duplicating the target recipe's description.

## [0.4.1] - 2026-04-21

### Fixed

- Stop zsh completion from leaking unmatched glob patterns into the
  user's prompt. The completer function now scopes `NULL_GLOB` via
  `emulate -L zsh -o NULL_GLOB`, so globs evaluated by `_files`
  internals or user zstyles (e.g. specs tagged `globbed-files`)
  silently drop when they match nothing — fixing both the
  `no matches found: *:globbed-files` error under the default
  `NOMATCH`, and the subsequent `*(/)` / `*(-/)` residue that would
  otherwise appear on the command line under `NO_NOMATCH` when
  completing a directory-typed flag in a directory with no subdirs.
- Actually stop the `*(-/)` / `*(/)` glob-pattern residue that `--dir
  <TAB>` in an empty directory would type into the prompt. The
  initial switch to `NULL_GLOB` was the right idea but was defeated
  by a function-scoped `setopt noglob` sitting on top of the `_files`
  call: that option disabled globbing inside `_path_files` as well,
  so its internal `tmp1=( $~tmp1 )` never expanded `*(-/)` and the
  literal pattern was handed to `compadd` as a candidate. Replace
  the option with a `noglob` *precommand modifier* on the `_files`
  call so globs like `*(*)` still reach `_files` unexpanded while
  its internals run with `NULL_GLOB` semantics as intended.
- Also enable `EXTENDED_GLOB` in the completer's `emulate -L zsh`
  scope. zsh's own `_files` builds qualifier patterns like `*(#q-/)`
  and uses `(#b)` backreferences internally; `emulate -L zsh` resets
  to plain-zsh defaults (extended glob off), so `_files -/` would
  emit `bad pattern: *(#q-/):globbed-files` on every TAB even once
  the residue bug above was fixed.

## [0.4.0] - 2026-04-17

### Added

- Add `shfmt` for shell script formatting via dprint command integration.
- `runner run <target>` now falls back to executing `<target>` through the
  detected package manager (`npx` / `pnpm exec` / `bunx` / `uv run` / …) when
  no matching task is defined, unifying task execution and ad-hoc command
  execution under one entrypoint.
- Shorthand `runner <name>` prefers a same-named task when one exists in the
  project, so tasks called `clean`, `install`, `list`, `info`, or
  `completions` are no longer shadowed by built-in subcommands. Passing any
  built-in-specific flag (`--frozen`, `-y`, `--include-framework`, `--raw`,
  the `completions` shell positional) keeps the built-in path as an escape
  hatch; `runner i` / `runner ls` aliases always hit the built-in.
- `run` alias binary now uses a dedicated parser (no subcommands), so
  `run clean`, `run install`, and friends always run the task/command even
  when the name matches a `runner` built-in.
- `runner completions <shell>` emits registration scripts for both `runner`
  and `run` in one invocation, so a single `eval "$(runner completions
  zsh)"` registers completion for both CLIs.
- `runner completions` now accepts `--output <PATH>` (`-o`) to write the
  script directly to a file instead of stdout. Parent directories are not
  auto-created; an existing file is overwritten; a stderr confirmation
  line (`wrote completion script to <PATH>`) is printed on success.
- Zsh completion honours path hints: `--dir <TAB>` (and any arg carrying
  `ValueHint::DirPath` / `FilePath` / `AnyPath` / `ExecutablePath`)
  delegates to zsh's native `_files` so `~/`, `~named-dir/`, globs, and
  `cdpath` all work.
- Declare MSRV `rust-version = "1.88"` in `Cargo.toml` (matches the
  let-chain syntax used throughout the crate).

### Changed

- Make `install.sh` accept both `X.Y.Z` and `vX.Y.Z` version arguments and
  environment overrides.
- Quiet installer downloads and checksum verification, and switch install
  output to a more compact structured summary with the installed version.
- Streamline install docs around the installer script and custom destination
  override details.
- `runner`'s shorthand for a name that matches a detected task is now
  preferred over the built-in subcommand when no built-in flag is set.
  **Breaking** for projects that relied on `runner install` / `runner clean`
  always hitting the built-in while also defining a same-named task.
- `run` alias binary now parses positionals unconditionally (no built-in
  subcommands); previously it inherited `runner`'s parser, so positional
  names that matched a built-in would dispatch there.
- Added `clap::ValueHint::DirPath` to the `--dir` flag on both CLIs so
  shell completion knows to offer directories.

### Removed

- **Breaking:** `runner exec <cmd>` is gone. Use `runner run <cmd>` (which
  now falls through to the package manager when no task matches) or the
  `run` alias binary.
- Remove the `tool::deno::exec_cmd` and `tool::cargo_pm::exec_cmd` helpers:
  `deno run <target>` treats the target as a local script, and
  `cargo <target>` dispatches to a cargo subcommand/plugin — neither runs
  arbitrary package binaries like `npx` does. `runner run <target>` in a
  Deno- or Cargo-only project now spawns `<target>` directly via `PATH`.

### Fixed

- Stop zsh completion from leaking caller-side `XTRACE` / alias / word-split
  state into the prompt: the completer function now starts with
  `emulate -L zsh`.
- `runner run <name>` in a Deno project no longer fires off
  `deno run <name>` (which would misinterpret `<name>` as a script path).
- Scope completion flag-hint lookup to the active subcommand chain instead
  of recursing through every subcommand: a sibling subcommand's `--flag`
  with a different `ValueHint` no longer bleeds into an unrelated context,
  and a subcommand-local boolean `--flag` correctly shadows an ancestor's
  value-taking definition.

## [0.3.1] - 2026-04-15

### Added

- Help output now includes a quiet `by Kaj Kowalski` attribution line, with an
  OSC8 `mailto:` link when rendered to a terminal.

### Changed

- Enable clap's `cargo`, `env`, and `wrap_help` features, and use clap cargo
  macros for package description/version metadata.
- Shorten help copy for `runner completions` and `--dir`, and show
  `RUNNER_DIR` directly in `--help` output.
- Make clap value parsers explicit for `--dir <PATH>` and the optional
  completions shell argument.

### Fixed

- Resolve Deno tasks, `deno.json(c)` configs, and `package.json` scripts from
  the nearest applicable ancestor config, while stopping at VCS boundaries and
  ignoring workspace roots that do not list the current path as a member.
- Prefer the nearest Deno task source when duplicate task names exist across
  ancestor configs.

## [0.3.0] - 2026-04-15

### Added

- Add global `--dir <PATH>` and `RUNNER_DIR` overrides to scan and run tasks
  against another project directory.

### Fixed

- Resolve Deno tasks and configs from the nearest applicable ancestor config,
  while stopping at VCS boundaries and ignoring workspace roots that do not
  list the current path as a member.
- Limit task source OSC8 hyperlinks to visible filename text so alignment
  padding is not clickable.
- Add repo and release-tag hyperlinks to `runner --version` and the `runner`
  info header version display.

## [0.2.1] - 2026-04-15

### Changed

- Bump `clap_complete` to `4.6.2`.

### Fixed

- Detect Deno projects from `packageManager: "deno@..."` and `deno.lock`
  instead of defaulting those repos to `npm`.
- Keep `package.json` tasks available for Deno projects during task discovery.

## [0.2.0] - 2026-03-29

### Added

- Add `install.sh` convenience installer for Linux release assets, including
  latest/pinned version resolution, checksum verification, and arch selection.
- Dynamic shell completions with live task candidates, source tags, and
  descriptions instead of static subcommand lists.
- Auto-detect shell from `$SHELL` when no completion argument is given.
- `description` field on `Task`, threaded from justfile doc comments and
  go-task `desc` fields into completion candidates.
- Tag-grouped zsh completions — candidates render under section headers
  (e.g. `-- justfile --`, `-- Commands --`) via custom `_describe` adapter.

### Changed

- Make installer destination fallback explicit as nested precedence:
  `RUNNER_INSTALL_DIR` -> `XDG_BIN_HOME` -> `~/.local/bin`.
- Extract zsh completion script to standalone `grouped.zsh` file, embedded
  via `include_str!` for syntax highlighting and linting support.

### Fixed

- Correct checksum filename in installer.

## [0.1.0] - 2026-03-27

### Added

- Initial `runner` CLI release for unified project task execution.
- Auto-detection for package managers and task sources across ecosystems.
- `run` alias binary for shorter invocation.
- Unified commands for task run/list, dependency install, clean, and exec.

[Unreleased]: https://github.com/kjanat/runner/compare/v0.7.1...HEAD
[0.7.1]: https://github.com/kjanat/runner/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/kjanat/runner/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/kjanat/runner/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/kjanat/runner/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/kjanat/runner/compare/v0.4.1...v0.5.0
[0.4.1]: https://github.com/kjanat/runner/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/kjanat/runner/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/kjanat/runner/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/kjanat/runner/compare/v0.2.1...v0.3.0
[0.2.1]: https://github.com/kjanat/runner/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/kjanat/runner/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/kjanat/runner/releases/tag/v0.1.0

<!--markdownlint-disable-file no-duplicate-heading-->
